From d6f73a1560970ffa5b003097d5debd7a7ad77754 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 16 Oct 2026 10:50:29 -0700 Subject: [PATCH] Let the lead keep notes on how its user works --- api/openapi.yaml | 32 +++ docs/operations.md | 7 +- internal/agents/lead.go | 11 +- internal/agents/leadnotes.go | 106 +++++++++ internal/agents/leadtools.go | 2 +- internal/agents/manager.go | 6 + internal/agents/process.go | 4 +- internal/api/api.gen.go | 432 ++++++++++++++++++++--------------- internal/server/lead.go | 18 ++ internal/store/leadnotes.go | 86 +++++++ internal/store/store.go | 9 + web/dist/api.js.map | 2 +- web/dist/settings.js | 59 ++++- web/dist/settings.js.map | 2 +- web/src/api.gen.ts | 84 +++++++ web/src/api.ts | 1 + web/src/settings.ts | 77 ++++++- web/style.css | 29 +++ 18 files changed, 759 insertions(+), 208 deletions(-) create mode 100644 internal/agents/leadnotes.go create mode 100644 internal/store/leadnotes.go diff --git a/api/openapi.yaml b/api/openapi.yaml index 8827b68..54445fc 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -76,6 +76,29 @@ paths: schema: { $ref: "#/components/schemas/AgentSession" } "400": { $ref: "#/components/responses/Error" } + /api/me/lead/notes: + get: + operationId: listLeadNotes + description: What the calling user's lead agent has noted about how they work, oldest first. The lead keeps them with its remember and forget tools. + responses: + "200": + description: The notes. + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/LeadNote" } + + /api/me/lead/notes/{noteId}: + parameters: + - { name: noteId, in: path, required: true, schema: { type: string } } + delete: + operationId: deleteLeadNote + responses: + "204": + description: Deleted. + "404": { $ref: "#/components/responses/Error" } + /api/me/focus: get: operationId: getFocus @@ -1375,6 +1398,15 @@ components: items: { $ref: "#/components/schemas/TaskTab" } tab: { $ref: "#/components/schemas/TaskTab" } + LeadNote: + type: object + required: [id, text, created_at, updated_at] + properties: + id: { type: string } + text: { type: string } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + PaneFocus: type: object required: [focused, drafting] diff --git a/docs/operations.md b/docs/operations.md index c975eb9..c50aad4 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -181,12 +181,13 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie - **Sessions.** A lead session is an `agent_sessions` row with `owner` set to the user's login, `role` `lead` and no `task_id`. Its events carry `owner` and no project or task. It runs in `leads/` with `leadGuidance` (passed and re-attached the same way as a task agent's guidance) and the lead tools instead of the task tools. It has no Stop-hook bounce, no snapshots, no end-of-turn commit and no turn summary; a turn that ends in error records the error on its `session.state` event, which the pane shows as a note. - **Messages.** `POST /api/me/lead/messages` sends the caller's message to their lead, starting a lead session when there is none. While the lead is mid-turn the message is held in `lead_inputs`; the lead's PostToolUse hook (Codex: `turn/steer` after each completed tool item) hands held messages over after its next tool call, like a task's queued messages (see "Agent sessions"), and any still held become its next turn. Signals are never handed over mid-turn. The composer stays enabled. `GET /api/me/lead/sessions` lists the caller's lead sessions; interrupt and compact use the usual session endpoints. -- **Signals.** Every published event goes through `Manager.Observe`. These become signals in `lead_inputs` for every user who has a lead session: `inbox.created` (a question with its choices and recommendation, an error or rate limit), `inbox.updated` when a human answered a question or error, `task.created` (naming the TODO item it was started from, or a subtask's parent, if any), `task.merge_started` (" is merging, started by ."), `task.merged`, `task.discarded`, `turn.ended` with `summary: true` (" finished a turn:" with its `result` cut to 1500 characters), `task.workflow_stage` (" started stage "" of workflow "".") and `task.workflow_finished` (" finished workflow "":" with its output cut to 1500 characters), `review.finished` (" review of snapshot :" with the review's summary, `change` and findings, or why it failed or was skipped; `reviewLine` in `internal/agents/review.go`), and `user.message` on a task from a human (see "Pane lock"). The human switching the right pane is not a signal. A signal caused by the lead itself (actor ` (lead)`) is skipped, as is the update from an answer it gave. A task's `task.created` event carries the actor that created it, separately from its `created_by` login: tasks the lead creates with `create_task` or `start_todo` keep the human's login as `created_by` but have actor ` (lead)`. Whenever the lead is idle, byre sends the oldest held message from the human as its own turn, or else every held signal as one `byre` message headed "Signals since your last turn (n)", followed by "Tasks now:" with the `list_tasks` text and a line saying what the lead's right pane shows (task or project, and selected tab) and whether the human is working in it (`leadStateText` in `internal/agents/lead.go`). If collecting that fails, for example because a project's repository is gone, the batch is still sent, ending instead with "The task list and right pane state are unavailable:" and the error, which is also logged as `lead state`. Its guidance tells it not to respond to the human's own actions, and to make its final message exactly `NO_REPLY` when a batch needs nothing from it. A transcript (`quietReply` in `web/src/task.ts`) never draws an `agent.message` whose last line is `NO_REPLY` (the model sometimes writes a sentence before it), and hides the live partial message while its text is still a prefix of it; the partial message starts hidden and is first shown by its 100ms render, so an empty one never takes up space. A lead turn that byre starts without a handoff (a signal batch or a handoff continuation) is held (`ctx.held`): its `byre` message is never drawn, and the turn isn't opened, so the scanner stays off, and its partial message stays hidden, until the lead draws something else, such as a tool call, thinking or a real message. When such a turn ends in `NO_REPLY`, everything it drew is removed with it (tool calls, thinking), so the transcript keeps nothing of it; a turn that ends in a real message keeps all of it. -- **Tools.** `list_tasks` (each project's active tasks, each subtask noting its parent, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`; closed tasks too), `search_closed_tasks` (see "Closing a task"), `read_inbox`, `answer_question` (an `option`, `other` or `dismiss` response recorded as the user, through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; refused while it is mid-turn), `create_task` and `start_todo` (each with an optional `workflow` to freeze and run), `interrupt`, `compact_task`, `merge_task` (optional `target`: `main`, or `parent` for a subtask, see "Subtasks"), `discard_task`, `show`, `compact_self`, and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents have actor ` (lead)`, so they don't make the user a participant; tasks, merges, discards and answers are done as the user. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks. +- **Signals.** Every published event goes through `Manager.Observe`. These become signals in `lead_inputs` for every user who has a lead session: `inbox.created` (a question with its choices and recommendation, an error or rate limit), `inbox.updated` when a human answered a question or error (a question's signal also carries its text and the agent's recommendation, so the lead sees where the human departed from it), `task.created` (naming the TODO item it was started from, or a subtask's parent, if any), `task.merge_started` (" is merging, started by ."), `task.merged`, `task.discarded`, `turn.ended` with `summary: true` (" finished a turn:" with its `result` cut to 1500 characters), `task.workflow_stage` (" started stage "" of workflow "".") and `task.workflow_finished` (" finished workflow "":" with its output cut to 1500 characters), `review.finished` (" review of snapshot :" with the review's summary, `change` and findings, or why it failed or was skipped; `reviewLine` in `internal/agents/review.go`), and `user.message` on a task from a human (see "Pane lock"). The human switching the right pane is not a signal. A signal caused by the lead itself (actor ` (lead)`) is skipped, as is the update from an answer it gave. A task's `task.created` event carries the actor that created it, separately from its `created_by` login: tasks the lead creates with `create_task` or `start_todo` keep the human's login as `created_by` but have actor ` (lead)`. Whenever the lead is idle, byre sends the oldest held message from the human as its own turn, or else every held signal as one `byre` message headed "Signals since your last turn (n)", followed by "Tasks now:" with the `list_tasks` text and a line saying what the lead's right pane shows (task or project, and selected tab) and whether the human is working in it (`leadStateText` in `internal/agents/lead.go`). If collecting that fails, for example because a project's repository is gone, the batch is still sent, ending instead with "The task list and right pane state are unavailable:" and the error, which is also logged as `lead state`. Its guidance tells it not to respond to the human's own actions, and to make its final message exactly `NO_REPLY` when a batch needs nothing from it. A transcript (`quietReply` in `web/src/task.ts`) never draws an `agent.message` whose last line is `NO_REPLY` (the model sometimes writes a sentence before it), and hides the live partial message while its text is still a prefix of it; the partial message starts hidden and is first shown by its 100ms render, so an empty one never takes up space. A lead turn that byre starts without a handoff (a signal batch or a handoff continuation) is held (`ctx.held`): its `byre` message is never drawn, and the turn isn't opened, so the scanner stays off, and its partial message stays hidden, until the lead draws something else, such as a tool call, thinking or a real message. When such a turn ends in `NO_REPLY`, everything it drew is removed with it (tool calls, thinking), so the transcript keeps nothing of it; a turn that ends in a real message keeps all of it. +- **Tools.** `list_tasks` (each project's active tasks, each subtask noting its parent, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`; closed tasks too), `search_closed_tasks` (see "Closing a task"), `read_inbox`, `answer_question` (an `option`, `other` or `dismiss` response recorded as the user, through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; refused while it is mid-turn), `create_task` and `start_todo` (each with an optional `workflow` to freeze and run), `interrupt`, `compact_task`, `merge_task` (optional `target`: `main`, or `parent` for a subtask, see "Subtasks"), `discard_task`, `show`, `compact_self`, the note tools (see "Notes") and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents have actor ` (lead)`, so they don't make the user a participant; tasks, merges, discards and answers are done as the user. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks. - **Focus.** `users.focus` holds what the user's right pane shows: `project_id`, `task_id`, `tabs` (the tabs put first, in order) and `tab` (the selected one). With a `task_id`, `project_id` is always that task's project, filled in by the server; with only a `project_id`, the pane shows that project with no task, which only the Project tab has anything for. `GET /api/me/focus` reads it; `PUT /api/me/focus` and the lead's `show` set it, refusing an unknown tab, a task that isn't active or an unknown project, and record a `focus.updated` event owned by the user. The page applies the focus on load and on every `focus.updated` it didn't cause itself; the human's own changes (⌘K, the right pane's task picker, clicking a tab, a task link, a notification) are sent with `PUT /api/me/focus`. The URL follows the focus: `#/task/`, `#/project/`, or `#/` for neither. - **Pane lock.** A `show` of what the pane already shows (the same task, or the same project with no task, on the same selected tab; `sameView`) tells the lead so and applies only its `tabs` order, whether or not the human is in the pane, so the human's selected tab stays selected. Otherwise, while the human is working in the right pane, the lead's `show` is refused with an error telling it to say in chat what it would have shown, unless its selected tab is `project` (the fallback the lead shows when nothing needs the human, so the human being there doesn't hold it) or the pane is finished (`Manager.paneFinished`): its task is merged, discarded or merging (`merging_by` set), or its selected tab is `inbox` and the task has no open items, or the human is waiting on the task's agent (`Manager.paneWaiting`): the selected tab is `transcript`, the task's implementer session is running, the page reports no unsent text in the pane, and either the `user.message` that started the running turn came from the user after the pane last moved, or the pane last moved 10 seconds ago or more (`watchHold`). The pane's last move is the last `PUT /api/me/focus` or `show` that set it (`Manager.paneMoved`, in memory, so a restart counts as long ago). For the user's own message, unsent text reported before the turn started doesn't count, since the page's report of the composer emptying on send can arrive after the message does. A `user.message` on a task from a human (not `byre`, an agent or a lead) signals every lead " sent a message; its agent is working on it.", and the sender's own lead, when its pane shows that task and is waiting that way, also gets "Your right pane is free: the human is waiting on 's agent." Each `PUT /api/me/focus` from the user starts a 10-second timer (`endWatchHold`); when it fires, if the pane hasn't moved since, the page still reports the human in it, the user hasn't messaged the task since the move (that already freed it, above), and the pane is waiting that way, the lead gets the signal "Your right pane is free: the human has watched 's agent work for 10 seconds without typing." The lead's own `show` starts no timer. When an `inbox.updated`, `task.merge_started`, `task.merged` or `task.discarded` event leaves a user's focused task finished that way, that user's signal for it gets a line "Your right pane is free: has nothing left in its inbox." (or "is merging."/"is merged."/"is discarded."), even when the event itself makes no signal, such as a dismissed error; `inbox.updated` on a task that is no longer active adds none. The lead's guidance tells it to show the next thing that needs the human on that line, or, when nothing does, the project's Project tab with a short status in chat: one line per active task, then up to three TODO items it would start next, each with a reason, starting one only when the human picks it. It writes each item as a link to `#/todo//`; Markdown views draw that (`web/src/todolink.ts`) as the title followed by small accent-coloured `start · discuss` text links (borderless buttons) that call `POST /api/projects/{id}/todo/tasks` with the first model, as the Project tab's do, or, once an active task was started from that item, the task's colour, dot and state, linking to it. A chat view refills them whenever its transcript grows or tasks or sessions change. When that status says nothing new since its last one, it replies `NO_REPLY` instead. The page sends `PUT /api/me/pane-focus` with `focused` and `drafting` whenever either changes: `focused` is true while the browser window has focus and the focused element is inside the right pane, and `drafting` while any textarea in the right pane (other than xterm's helper) holds non-blank text, checked on focus changes, `input` and `submit`. The pane itself is focusable (`tabIndex` -1), so a click anywhere in it counts, not only on inputs and buttons; a re-render that removes the focused element hands focus back to the pane rather than counting as leaving. The page sends false on `pagehide` (with `keepalive`) and resends true when its event stream reconnects. byre keeps both flags and the time of the last report in memory per user (`Manager.paneFocus`, `Manager.paneReported`), so they start false after a restart until the page resends them. - **Start hold.** Start on a TODO item in the Project tab leaves the pane where it is. When the user starts one themselves (`StartTodo` with the user as actor, not the lead's `start_todo`), byre records the time in `Manager.startedTodo` for them, and for 10 seconds after it (`startHold`), while the pane's selected tab is `project`, every `show` that would change the view is refused, focused or not, with an error telling the lead the human is likely to start more. The user's own `PUT /api/me/focus` (switching task or tab) or a message to the lead clears it. The refusal and the `show` tool's description both tell the lead the hold lasts 10 seconds. When the 10 seconds pass without it being cleared or restarted by another Start (`endStartHold`, on a timer), byre clears it and, if the pane's selected tab is still `project`, sends the lead the signal "The start hold is over: …" so it can show what it held back; its guidance tells it to reply `NO_REPLY` when nothing was. It is in memory, so a restart clears it. - **Compaction.** The pane's **Compact** button asks the lead for a handoff (`leadHandoffPrompt`). The lead can't see its own context size, so the first time one of its turns ends (other than a handoff or a `compact_self` turn) with the latest `session.context` `used` at half its `window` or more, byre holds a signal for it giving the percentage and asking it to call `compact_self` at the next logical point (`warnLeadContext` in `internal/agents/lead.go`); it is sent once per live process, so a byre restart can send it again once the window is known. The lead calls `compact_self`; when that turn ends normally, byre sends the same request. The handoff turn ends the session, and a new lead session starts from `leadHandoffPreamble` plus the handoff; held input follows once that turn ends. +- **Notes.** The lead keeps notes on how its user works (priorities, preferences, the reasoning behind choices) in `lead_notes`, one row per note keyed by `owner`, through its `remember` (add, or replace the text of an `id`), `forget` and `list_notes` tools (`internal/agents/leadnotes.go`). Each change records a `lead.note_saved` or `lead.note_deleted` event owned by the user. When a lead's agent process starts, `notesText` lists every note with its id after `leadGuidance` in the session's instructions (`--append-system-prompt` for Claude, which keeps what the session's first request carried; `developerInstructions` for Codex), but not in the per-turn guidance reminder, so a new or compacted lead session starts from the current notes and notes written mid-session are already in its context. The guidance tells the lead when to write a note (an answer that departs from a recommendation, a correction, a stated preference), to cite a note's id when it shapes a recommendation, and that it may answer a task agent's question itself when a note settles it outright. `GET /api/me/lead/notes` lists the caller's notes and `DELETE /api/me/lead/notes/{noteId}` deletes one (`Manager.DeleteLeadNote`), and, when the user has a live lead session, holds a message for it from `byre` naming the note's id and text and telling it to stop applying it, since the running session still has the note in its instructions. It is held as a message, not a signal, so it keeps its place in line with the human's own messages: it goes out ahead of any message they send after the delete, as its own turn (drawn like a signal batch's, and folded away on `NO_REPLY`) or mid-turn after the lead's next tool call; the Settings tab lists them with a delete link each, refetching on either event. `sqlite3 byre.db "SELECT owner, id, text FROM lead_notes ORDER BY created_at"` shows them. - **Restarts.** On shutdown, byre marks each running lead session idle with "byre stopped mid-turn" and holds a restart notice for it, so a lead is never left `running` for a build without lead support to find. After a crash, startup resumes a running lead like a task session. Startup then delivers any held input. The UI is two panes filling the window, with no navbar. The left has three tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Status and Settings, and at the end of its tab strip the connection dot and the user's name. Which left tab is selected is kept only in the page; the Lead view stays mounted while another tab shows, and Status or Settings mounts each time its tab is selected. The right shows the focused task, or project with no task: a top row holding the task picker, centred, and the task's preview links at its right end (see "Previews"), then the tab strip, then the selected tab's view with its own header. The tabs are Transcript, Changes, Files, Terminal, Inbox, Reviews and Project, the lead's `tabs` first. Reviews shows only once it has a `review.*` event, and Inbox only while the task has open items; the selected tab always shows. Tabs that don't fit in the strip move, from the end, into a **More ▾** menu (`web/src/panes.ts`); the selected tab never moves there, and More carries the Inbox highlight and count when Inbox is in it. ⌘P (Ctrl+P elsewhere) selects the Project tab, keeping the right pane's task, and replaces the browser's print shortcut. ⌘L (Ctrl+L elsewhere) selects the Lead tab and puts the cursor in the lead's composer, and replaces the browser's address bar shortcut. ⌘S (Ctrl+S elsewhere) opens the right pane's task's Merge confirmation, from any tab, when the Merge button would be enabled, and replaces the browser's save shortcut. ⌘/ (Ctrl+/ elsewhere) opens a dialog listing every keyboard shortcut; the list is `shortcuts` in `web/src/switcher.ts`, kept in step with the handlers by hand. The ⌘ shortcuts do nothing while a dialog is open. @@ -303,7 +304,7 @@ A workflow is a named graph of stages, each a stage name, the stages it comes af ## Events -`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. `?task_id=` limits the stream, replay and live, to one task's events (404 for an unknown task); `?lead=true` to the events of the caller's lead agent sessions. An event with an `owner` (a lead agent's events, a user's `focus.updated`) goes only to that user's streams; the others never see it. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. Once the replay is written, before anything live, the server sends an `event: ready`. +`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. `?task_id=` limits the stream, replay and live, to one task's events (404 for an unknown task); `?lead=true` to the events of the caller's lead agent sessions. An event with an `owner` (a lead agent's events, a user's `focus.updated`, `lead.note_saved` and `lead.note_deleted`) goes only to that user's streams; the others never see it. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. Once the replay is written, before anything live, the server sends an `event: ready`. The page keeps every active task's events in memory, one `Feed` per task (`taskFeeds` in `web/src/state.ts`), so switching tasks makes no request. Each `loadTasks` creates a feed for any active task without one, which pages through `GET /api/tasks/{taskId}/events` (1000 at a time) for its history; selecting a task that has no feed (a closed one) does the same. Live events reach every feed through the page's one global stream, which already carries every task's unowned events; a feed holds back what arrives while its history loads and drops the duplicates by `seq`. Feeds of closed tasks are dropped once they aren't selected. A failed history load is logged to the console and leaves the feed waiting for its history; the next `loadTasks` or selection of that task loads it again into the same feed. A feed takes its history, then the live events held back during the load, skipping each event whose `seq` is at or below the last one it kept, so an event in both is kept once. The lead's feed replays its own `?lead=true` stream and holds back what arrives until that stream's first `ready`. Either way the whole history reaches the transcript at once, so it draws in one pass, already scrolled to the bottom; until then the transcript shows "loading…". The web UI (`subscribe` in `web/src/api.ts`) tracks the last seq it saw and opens a new stream from it when nothing (event or ping) has arrived for 40s, 2s after the browser gives up on one (as it does when `tailscale serve` answers 502 while byre restarts), when the browser comes back online, and on becoming visible if the stream is stale. While disconnected the left pane's tab strip shows a red dot and "reconnecting…"; partial output (`agent.delta`) sent while disconnected is lost, and the finished `agent.message` replaces it. diff --git a/internal/agents/lead.go b/internal/agents/lead.go index 0b86218..9ea7cd1 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -25,7 +25,8 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left - The changes tab only when the human asks to see the diff; they rarely want it. - Show, don't tell. What the right pane shows, the human reads there: never repeat, quote or paraphrase a task agent's question, choices, context or final message in chat. Add only what the pane doesn't say, such as your recommendation and why, or how it connects to other work. - Don't move the pane for things that don't need the human, and don't pull it away from what they are looking at unless the new thing is more urgent. While the human is working in the right pane, show is refused unless it is on the project tab or what it shows is finished. It is also refused on the project tab for 10 seconds after the human starts a TODO item there, since they are likely to start more. When show is refused, say in chat what you would have shown, and carry on. "The start hold is over" means those 10 seconds passed with the pane still on the project tab: show what you held back, if it still matters, or make your final message NO_REPLY. -- Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, or the human already decided it. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how. +- Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, the human already decided it, or one of your notes settles it outright. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how, naming the note by its id when one decided it. +- Notes: you learn how the human works and keep it with remember, forget and list_notes. byre adds every note to your instructions at the start of each lead session, so they outlast compaction; the human can read and delete them in the Settings tab. Write a note when the human shows a priority, a preference or the reasoning behind a choice that will recur: an answer that departs from the agent's recommendation (the answered signal shows both), a correction or overrule of you or a task agent, or a statement of how they want things done. Record the choice and the why, in their terms, general enough to apply to the next case and specific enough to act on, for example "Prefers replacing a state label over adding a tag beside it: keeps the picker uncluttered." Don't note one-off facts about a single task, and don't duplicate: update an existing note with remember and its id instead. When the human contradicts a note, update it or forget it. Before recommending anything, check your notes, and when one shaped the recommendation, cite its id. - Finished turns: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress. - Reviews: after each new snapshot of a task, a reviewer agent from the other vendor reviews the change. The results are on the task's reviews tab, read_transcript shows each review's findings, and a question shows the review of the turn that asked it. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding. - Dig into context with list_tasks, read_transcript and read_inbox, and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message. @@ -146,7 +147,7 @@ func (m *Manager) DeliverLead(ctx context.Context, owner string) error { } batch, actor, text, images := nextLeadTurn(inputs) - if actor == "byre" { + if batch[0].Kind == store.LeadSignal { state, err := m.leadStateText(ctx, owner) if err != nil { slog.Error("lead state", "owner", owner, "err", err) @@ -555,7 +556,11 @@ func (m *Manager) answeredText(ctx context.Context, item api.InboxItem) string { if r.Text != nil && strings.TrimSpace(*r.Text) != "" { answer = strings.TrimSpace(answer + " " + *r.Text) } - return fmt.Sprintf("%s: %s answered %s %s: %s", m.taskLabel(ctx, item.TaskId), r.User, item.Kind, item.Id, clip(answer, 600)) + if item.Kind != api.Question { + return fmt.Sprintf("%s: %s answered %s %s: %s", m.taskLabel(ctx, item.TaskId), r.User, item.Kind, item.Id, clip(answer, 600)) + } + recommended := strings.Join(anyStrings(item.Payload["recommended"]), "; ") + return fmt.Sprintf("%s: %s answered question %s %q: %s\nThe agent recommended: %s", m.taskLabel(ctx, item.TaskId), r.User, item.Id, clip(fmt.Sprint(item.Payload["question"]), 300), clip(answer, 600), clip(recommended, 600)) } func remarshal(from any, to any) error { diff --git a/internal/agents/leadnotes.go b/internal/agents/leadnotes.go new file mode 100644 index 0000000..ea12df4 --- /dev/null +++ b/internal/agents/leadnotes.go @@ -0,0 +1,106 @@ +package agents + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "g.fc.run/misc/byre/internal/api" + "g.fc.run/misc/byre/internal/mcp" + "g.fc.run/misc/byre/internal/store" +) + +func (m *Manager) noteTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool { + return []mcp.Tool{ + tool("remember", + "Save a note about how the human works: a priority, a preference, or reasoning behind a choice, worded so you can apply it later. "+ + "Without id it adds a note; with id it replaces that note's text. byre gives you every note at the start of each new lead session.", + `{"type": "object", "properties": { + "text": {"type": "string"}, + "id": {"type": "string", "description": "The note to replace."} + }, "required": ["text"]}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + Text string `json:"text"` + ID string `json:"id"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + text := strings.TrimSpace(args.Text) + if text == "" { + return "", errors.New("text is empty") + } + n, err := m.store.SaveLeadNote(ctx, user.Login, args.ID, text, leadActor(user.Login)) + if err != nil { + return "", err + } + if args.ID != "" { + return fmt.Sprintf("Note %s updated.", n.Id), nil + } + return fmt.Sprintf("Saved as note %s.", n.Id), nil + }), + + tool("forget", + "Delete a note that no longer holds, for example because the human contradicted it.", + `{"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + ID string `json:"id"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + _, err = m.store.DeleteLeadNote(ctx, user.Login, args.ID, leadActor(user.Login)) + if err != nil { + return "", err + } + return fmt.Sprintf("Note %s deleted.", args.ID), nil + }), + + tool("list_notes", + "List your notes about how the human works, with their ids, as they are now. The human can delete notes in byre's Settings tab.", + `{"type": "object", "properties": {}}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + return m.notesText(ctx, user.Login) + }), + } +} + +func (m *Manager) DeleteLeadNote(ctx context.Context, login, id string) error { + text, err := m.store.DeleteLeadNote(ctx, login, id, login) + if err != nil { + return err + } + _, found, err := m.activeLead(ctx, login) + if err != nil || !found { + return err + } + signal := fmt.Sprintf("The human deleted your note %s: %q. Stop applying it from now on, in recommendations, answers and handoffs, and don't write it again unless they say so. If this needs nothing from you to the human, make your final message NO_REPLY.", id, text) + err = m.store.AddLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadMessage, Actor: "byre", Text: signal}) + if err != nil { + return err + } + m.deliverLead(login) + return nil +} + +func (m *Manager) notesText(ctx context.Context, owner string) (string, error) { + notes, err := m.store.LeadNotes(ctx, owner) + if err != nil { + return "", err + } + if len(notes) == 0 { + return fmt.Sprintf("You have no notes yet on how %s works.", owner), nil + } + b := &strings.Builder{} + fmt.Fprintf(b, "Your notes on how %s works (%d):", owner, len(notes)) + for _, n := range notes { + fmt.Fprintf(b, "\n- [%s] %s", n.Id, n.Text) + } + return b.String(), nil +} diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index e5d8d39..40216c4 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -430,7 +430,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { m.mu.Unlock() return "byre will ask for your handoff once this turn ends.", nil }), - }, m.workflowTools(tool)...) + }, append(m.workflowTools(tool), m.noteTools(tool)...)...) } func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor string) error { diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 6cde434..a16432d 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -78,6 +78,7 @@ type live struct { taskID string projectID string owner string + notes string reviewID string stage string stopping bool @@ -599,6 +600,11 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap return nil, err } g = leadGuidance + notes, err := m.notesText(ctx, l.owner) + if err != nil { + return nil, err + } + l.notes = "\n\n" + notes tools = m.leadTools(l) env = nil } diff --git a/internal/agents/process.go b/internal/agents/process.go index ff6b906..10abfa3 100644 --- a/internal/agents/process.go +++ b/internal/agents/process.go @@ -70,7 +70,7 @@ func (m *Manager) startClaude(ctx context.Context, sess api.AgentSession, l *liv GitName: l.gitName, GitEmail: l.gitEmail, Env: env, - Guidance: g, + Guidance: g + l.notes, MCPServers: servers, StdioMCP: stdio, Hooks: hooks, @@ -157,7 +157,7 @@ func (m *Manager) startCodex(ctx context.Context, sess api.AgentSession, l *live GitName: l.gitName, GitEmail: l.gitEmail, Env: append(env, "CODEX_HOME="+m.codexHome), - Guidance: g, + Guidance: g + l.notes, Reminder: g, MCPServers: servers, StdioMCP: stdio, diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 470af63..0f690c6 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -684,6 +684,14 @@ type InboxResponseType string // InboxState defines model for InboxState. type InboxState string +// LeadNote defines model for LeadNote. +type LeadNote struct { + CreatedAt time.Time `json:"created_at"` + Id string `json:"id"` + Text string `json:"text"` + UpdatedAt time.Time `json:"updated_at"` +} + // MessageCreate defines model for MessageCreate. type MessageCreate struct { // Images Blob ids from uploadBlob, sent to the agent as images alongside the text. @@ -1237,6 +1245,12 @@ type ServerInterface interface { // (POST /api/me/lead/messages) MessageLead(w http.ResponseWriter, r *http.Request) + // (GET /api/me/lead/notes) + ListLeadNotes(w http.ResponseWriter, r *http.Request) + + // (DELETE /api/me/lead/notes/{noteId}) + DeleteLeadNote(w http.ResponseWriter, r *http.Request, noteId string) + // (GET /api/me/lead/sessions) ListLeadSessions(w http.ResponseWriter, r *http.Request) @@ -1667,6 +1681,46 @@ func (siw *ServerInterfaceWrapper) MessageLead(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// ListLeadNotes operation middleware +func (siw *ServerInterfaceWrapper) ListLeadNotes(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListLeadNotes(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteLeadNote operation middleware +func (siw *ServerInterfaceWrapper) DeleteLeadNote(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "noteId" ------------- + var noteId string + + err = runtime.BindStyledParameterWithOptions("simple", "noteId", r.PathValue("noteId"), ¬eId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "noteId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteLeadNote(w, r, noteId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListLeadSessions operation middleware func (siw *ServerInterfaceWrapper) ListLeadSessions(w http.ResponseWriter, r *http.Request) { @@ -2955,6 +3009,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/notifications", wrapper.UpdateNotificationPrefs) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/lead/sessions", wrapper.ListLeadSessions) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/me/lead/messages", wrapper.MessageLead) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/lead/notes", wrapper.ListLeadNotes) + m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/me/lead/notes/{noteId}", wrapper.DeleteLeadNote) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/focus", wrapper.GetFocus) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/focus", wrapper.SetFocus) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/pane-focus", wrapper.SetPaneFocus) @@ -3010,193 +3066,195 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "GXubU/QY8p2KG3/69H8DAAD//w==", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/lead.go b/internal/server/lead.go index d7becfe..6ae817f 100644 --- a/internal/server/lead.go +++ b/internal/server/lead.go @@ -52,6 +52,24 @@ func (s *Server) MessageLead(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusAccepted, sess) } +func (s *Server) ListLeadNotes(w http.ResponseWriter, r *http.Request) { + notes, err := s.store.LeadNotes(r.Context(), actor(r)) + if err != nil { + writeStoreError(w, err) + return + } + writeJSON(w, http.StatusOK, notes) +} + +func (s *Server) DeleteLeadNote(w http.ResponseWriter, r *http.Request, noteId string) { + err := s.agents.DeleteLeadNote(r.Context(), actor(r), noteId) + if err != nil { + writeAgentError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + func (s *Server) GetFocus(w http.ResponseWriter, r *http.Request) { f, err := s.store.GetFocus(r.Context(), actor(r)) if err != nil { diff --git a/internal/store/leadnotes.go b/internal/store/leadnotes.go new file mode 100644 index 0000000..1b1fca9 --- /dev/null +++ b/internal/store/leadnotes.go @@ -0,0 +1,86 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "g.fc.run/misc/byre/internal/api" +) + +func (s *Store) LeadNotes(ctx context.Context, owner string) ([]api.LeadNote, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, text, created_at, updated_at FROM lead_notes WHERE owner = ? ORDER BY created_at, id`, owner) + if err != nil { + return nil, err + } + defer rows.Close() + + notes := []api.LeadNote{} + for rows.Next() { + n := api.LeadNote{} + createdAt := "" + updatedAt := "" + err = rows.Scan(&n.Id, &n.Text, &createdAt, &updatedAt) + if err != nil { + return nil, err + } + n.CreatedAt, err = parseTime(createdAt) + if err != nil { + return nil, err + } + n.UpdatedAt, err = parseTime(updatedAt) + if err != nil { + return nil, err + } + notes = append(notes, n) + } + return notes, rows.Err() +} + +func (s *Store) SaveLeadNote(ctx context.Context, owner, id, text, actor string) (api.LeadNote, error) { + at := time.Now().UTC() + n := api.LeadNote{Id: id, Text: text, CreatedAt: at, UpdatedAt: at} + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + if id == "" { + n.Id = NewID() + _, err := tx.ExecContext(ctx, `INSERT INTO lead_notes (id, owner, text, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, + n.Id, owner, text, formatTime(at), formatTime(at)) + if err != nil { + return err + } + } else { + createdAt := "" + err := tx.QueryRowContext(ctx, `UPDATE lead_notes SET text = ?, updated_at = ? WHERE id = ? AND owner = ? RETURNING created_at`, + text, formatTime(at), id, owner).Scan(&createdAt) + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("note %s: %w", id, ErrNotFound) + } + if err != nil { + return err + } + n.CreatedAt, err = parseTime(createdAt) + if err != nil { + return err + } + } + return emit(api.Event{Kind: "lead.note_saved", Owner: &owner, Actor: actor, Payload: ToPayload(n)}) + }) + return n, err +} + +func (s *Store) DeleteLeadNote(ctx context.Context, owner, id, actor string) (string, error) { + text := "" + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + err := tx.QueryRowContext(ctx, `DELETE FROM lead_notes WHERE id = ? AND owner = ? RETURNING text`, id, owner).Scan(&text) + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("note %s: %w", id, ErrNotFound) + } + if err != nil { + return err + } + return emit(api.Event{Kind: "lead.note_deleted", Owner: &owner, Actor: actor, Payload: map[string]any{"id": id, "text": text}}) + }) + return text, err +} diff --git a/internal/store/store.go b/internal/store/store.go index d677900..96ae812 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -429,6 +429,15 @@ CREATE INDEX tasks_parent ON tasks(parent_task_id) WHERE parent_task_id != ''; ALTER TABLE reviews ADD COLUMN finding_states TEXT NOT NULL DEFAULT '{}'; UPDATE events SET payload = json_set(payload, '$.finding_states', json('{}')) WHERE kind IN ('review.started', 'review.finished') AND json_type(payload, '$.finding_states') IS NULL; +`}, {"add-lead-notes", ` +CREATE TABLE lead_notes ( + id TEXT PRIMARY KEY, + owner TEXT NOT NULL REFERENCES users(login), + text TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE INDEX lead_notes_owner ON lead_notes(owner, created_at); `}} const ( diff --git a/web/dist/api.js.map b/web/dist/api.js.map index afd76a5..064ecde 100644 --- a/web/dist/api.js.map +++ b/web/dist/api.js.map @@ -1 +1 @@ -{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAiDA,MAAM,OAAO,QAAS,SAAQ,KAAK;IACZ;IAArB,YAAqB,MAAc,EAAE,OAAe;QAClD,KAAK,CAAC,OAAO,CAAC,CAAC;QADI,WAAM,GAAN,MAAM,CAAQ;IAEnC,CAAC;CACF;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,IAAyC;IACrE,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAC;IAChE,IAAI,GAAG,GAAW,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,GAAW,EAAE,EAAE;QAC9D,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC,EAAE,CAAC;QACzF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACnB,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAS,EACT,IAAO,EACP,OAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,KAAK,EAAE,CAAC;IAC/F,IAAI,IAAI,CAAC,IAAI,YAAY,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,0BAA0B,EAAE,CAAC;QAChF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;SAAM,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,OAAO,SAA6B,CAAC;IACvC,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAG,IAAyB,CAAC,OAAO,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,IAAwB,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,CAA6B,IAAO,EAAE,IAA4B,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACrH,MAAM,CAAC,MAAM,IAAI,GAAG,CAA8B,IAAO,EAAE,IAA6B,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACzH,MAAM,CAAC,MAAM,GAAG,GAAG,CAA6B,IAAO,EAAE,IAA4B,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACrH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAgC,IAAO,EAAE,IAA+B,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAE9H,MAAM,UAAU,MAAM,CAA6B,IAAO,EAAE,IAA2B;IACrF,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,GAAG,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACtH,EAAE,CAAC,UAAU,GAAG,aAAa,CAAC;IAC9B,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,YAAY,GAAG,MAAM,CAAC;AAC5B,MAAM,YAAY,GAAG,KAAK,CAAC;AAE3B,MAAM,UAAU,SAAS,CACvB,KAAa,EACb,MAA4C,EAC5C,OAAgC,EAChC,OAAqC,EACrC,OAAmB;IAEnB,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,MAA+B,CAAC;IACpC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACvC,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5B,CAAC;QACD,MAAM,CAAC,GAAG,IAAI,WAAW,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC;QAClD,MAAM,GAAG,CAAC,CAAC;QACX,CAAC,CAAC,MAAM,GAAG,GAAG,EAAE;YACd,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,CAAC,CAAC,OAAO,GAAG,GAAG,EAAE;YACf,OAAO,CAAC,KAAK,CAAC,CAAC;YACf,IAAI,CAAC,CAAC,UAAU,KAAK,WAAW,CAAC,MAAM,EAAE,CAAC;gBACxC,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC,CAAC;QACF,CAAC,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE;YAC9B,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAC/B,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,SAAS,GAAG,CAAC,GAAyB,EAAE,EAAE;YAC1C,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAc,CAAC;YAC7C,IAAI,EAAE,CAAC,GAAG,GAAG,IAAI,EAAE,CAAC;gBAClB,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC;YAChB,CAAC;YACD,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,YAAY,EAAE,CAAC;YACtC,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,CAAC;QACf,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,IAAI,QAAQ,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YAC3C,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC3C,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;IACzD,OAAO,EAAE,CAAC;IACV,OAAO,GAAG,EAAE;QACV,aAAa,CAAC,QAAQ,CAAC,CAAC;QACxB,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACvC,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;QAC5D,MAAM,EAAE,KAAK,EAAE,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/web/dist/settings.js b/web/dist/settings.js index 0d07ab5..8021b7f 100644 --- a/web/dist/settings.js +++ b/web/dist/settings.js @@ -1,5 +1,5 @@ -import { put } from "./api.js"; -import { errorText, h, replace, showAlert } from "./dom.js"; +import { del, get, put } from "./api.js"; +import { errorText, h, replace, showAlert, showConfirm } from "./dom.js"; import { on, state } from "./state.js"; const kinds = [ { key: "questions", label: "Questions" }, @@ -51,10 +51,59 @@ export function mountSettings(root) { }; replace(device, h("p", {}, "This browser doesn't show notifications."), button); }; - replace(root, h("div", { class: "settings-view" }, h("h2", {}, "Notifications"), device, prefs)); - const off = on("me", renderPrefs); + const notes = h("div", { class: "lead-notes" }); + let loads = 0; + const loadNotes = async () => { + const n = ++loads; + let list; + try { + list = await get("/api/me/lead/notes"); + } + catch (err) { + replace(notes, h("p", { class: "hint" }, errorText(err))); + return; + } + if (n !== loads) { + return; + } + if (list.length === 0) { + replace(notes, h("p", { class: "hint" }, "Your lead hasn't noted anything about how you work yet.")); + return; + } + replace(notes, h("ul", {}, ...list.map((note) => { + const remove = h("button", {}, "delete"); + remove.onclick = async () => { + if (!(await showConfirm([h("p", {}, "Delete this note? Your lead stops applying it."), h("p", {}, note.text)], "Delete", true, remove))) { + return; + } + try { + await del("/api/me/lead/notes/{noteId}", { path: { noteId: note.id } }); + } + catch (err) { + void showAlert(errorText(err)); + } + }; + return h("li", {}, h("span", {}, note.text), remove); + }))); + }; + replace(root, h("div", { class: "settings-view" }, h("h2", {}, "Notifications"), device, prefs, h("h2", {}, "What your lead has learned"), h("p", { class: "hint" }, "Your lead notes your priorities and preferences as it works with you, and starts every session with them."), notes)); + const offs = [ + on("me", renderPrefs), + on("event", (detail) => { + const ev = detail; + if (ev.kind === "lead.note_saved" || ev.kind === "lead.note_deleted") { + void loadNotes(); + } + }), + ]; renderPrefs(); renderDevice(); - return off; + void loadNotes(); + return () => { + loads++; + for (const off of offs) { + off(); + } + }; } //# sourceMappingURL=settings.js.map \ No newline at end of file diff --git a/web/dist/settings.js.map b/web/dist/settings.js.map index effac7b..63a9bc2 100644 --- a/web/dist/settings.js.map +++ b/web/dist/settings.js.map @@ -1 +1 @@ -{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/src/api.gen.ts b/web/src/api.gen.ts index 8be72b8..5d6e0f9 100644 --- a/web/src/api.gen.ts +++ b/web/src/api.gen.ts @@ -71,6 +71,41 @@ export interface paths { patch?: never; trace?: never; }; + "/api/me/lead/notes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description What the calling user's lead agent has noted about how they work, oldest first. The lead keeps them with its remember and forget tools. */ + get: operations["listLeadNotes"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/me/lead/notes/{noteId}": { + parameters: { + query?: never; + header?: never; + path: { + noteId: string; + }; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteLeadNote"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/me/focus": { parameters: { query?: never; @@ -1236,6 +1271,14 @@ export interface components { tabs: components["schemas"]["TaskTab"][]; tab: components["schemas"]["TaskTab"]; }; + LeadNote: { + id: string; + text: string; + /** Format: date-time */ + created_at: string; + /** Format: date-time */ + updated_at: string; + }; PaneFocus: { /** @description True while the browser window has focus and the focused element is inside the right pane. */ focused: boolean; @@ -1458,6 +1501,47 @@ export interface operations { 400: components["responses"]["Error"]; }; }; + listLeadNotes: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The notes. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LeadNote"][]; + }; + }; + }; + }; + deleteLeadNote: { + parameters: { + query?: never; + header?: never; + path: { + noteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deleted. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 404: components["responses"]["Error"]; + }; + }; getFocus: { parameters: { query?: never; diff --git a/web/src/api.ts b/web/src/api.ts index 0a2e312..507079b 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -20,6 +20,7 @@ export type Snapshot = Schemas["Snapshot"]; export type Participant = Schemas["Participant"]; export type Focus = Schemas["Focus"]; export type TaskTab = Schemas["TaskTab"]; +export type LeadNote = Schemas["LeadNote"]; type Method = "get" | "post" | "put" | "delete"; diff --git a/web/src/settings.ts b/web/src/settings.ts index 26321d3..81de38f 100644 --- a/web/src/settings.ts +++ b/web/src/settings.ts @@ -1,5 +1,5 @@ -import { put, type NotificationPrefs } from "./api.js"; -import { errorText, h, replace, showAlert } from "./dom.js"; +import { del, get, put, type ByreEvent, type LeadNote, type NotificationPrefs } from "./api.js"; +import { errorText, h, replace, showAlert, showConfirm } from "./dom.js"; import { on, state } from "./state.js"; const kinds: { key: keyof NotificationPrefs; label: string }[] = [ @@ -60,10 +60,77 @@ export function mountSettings(root: HTMLElement): () => void { replace(device, h("p", {}, "This browser doesn't show notifications."), button); }; - replace(root, h("div", { class: "settings-view" }, h("h2", {}, "Notifications"), device, prefs)); - const off = on("me", renderPrefs); + const notes = h("div", { class: "lead-notes" }); + let loads = 0; + const loadNotes = async () => { + const n = ++loads; + let list: LeadNote[]; + try { + list = await get("/api/me/lead/notes"); + } catch (err) { + replace(notes, h("p", { class: "hint" }, errorText(err))); + return; + } + if (n !== loads) { + return; + } + if (list.length === 0) { + replace(notes, h("p", { class: "hint" }, "Your lead hasn't noted anything about how you work yet.")); + return; + } + replace( + notes, + h( + "ul", + {}, + ...list.map((note) => { + const remove = h("button", {}, "delete"); + remove.onclick = async () => { + if (!(await showConfirm([h("p", {}, "Delete this note? Your lead stops applying it."), h("p", {}, note.text)], "Delete", true, remove))) { + return; + } + try { + await del("/api/me/lead/notes/{noteId}", { path: { noteId: note.id } }); + } catch (err) { + void showAlert(errorText(err)); + } + }; + return h("li", {}, h("span", {}, note.text), remove); + }), + ), + ); + }; + + replace( + root, + h( + "div", + { class: "settings-view" }, + h("h2", {}, "Notifications"), + device, + prefs, + h("h2", {}, "What your lead has learned"), + h("p", { class: "hint" }, "Your lead notes your priorities and preferences as it works with you, and starts every session with them."), + notes, + ), + ); + const offs = [ + on("me", renderPrefs), + on("event", (detail) => { + const ev = detail as ByreEvent; + if (ev.kind === "lead.note_saved" || ev.kind === "lead.note_deleted") { + void loadNotes(); + } + }), + ]; renderPrefs(); renderDevice(); + void loadNotes(); - return off; + return () => { + loads++; + for (const off of offs) { + off(); + } + }; } diff --git a/web/style.css b/web/style.css index eed33c1..71514b7 100644 --- a/web/style.css +++ b/web/style.css @@ -2210,6 +2210,35 @@ button.danger-primary { accent-color: var(--accent); } +.lead-notes ul { + margin: 0; + padding: 0; + list-style: none; +} + +.lead-notes li { + display: flex; + gap: 12px; + align-items: baseline; + padding: 6px 0; + border-top: 1px solid var(--border); +} + +.lead-notes li span { + flex: 1; +} + +.lead-notes button { + padding: 0; + border: none; + background: none; + color: var(--accent); +} + +.lead-notes button:hover { + text-decoration: underline; +} + .fatal { padding: 24px; color: var(--danger);