From d530ec5edc7c35fb9e4d2c7e4741583dffeadc71 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 20 Oct 2026 08:57:37 -0700 Subject: [PATCH] task description auto --- api/openapi.yaml | 9 +- docs/macos.md | 2 +- docs/operations.md | 6 +- internal/agents/guidance.go | 10 +- internal/agents/manager.go | 2 +- internal/agents/mergein.go | 35 ++ internal/api/api.gen.go | 579 ++++++++++++----------- internal/server/server.go | 16 +- internal/store/store.go | 15 + mac/Sources/Byre/API.swift | 6 + mac/Sources/Byre/ByreApp.swift | 2 +- mac/Sources/Byre/Chat.swift | 2 +- mac/Sources/Byre/MergeDialog.swift | 4 +- mac/Sources/Byre/Notifications.swift | 2 +- mac/Sources/Byre/Panes.swift | 2 +- mac/Sources/Byre/Project.swift | 6 +- mac/Sources/Byre/Status.swift | 4 +- mac/Sources/Byre/Store.swift | 2 +- mac/Sources/Byre/Switcher.swift | 69 +-- mac/Sources/Byre/TranscriptBuilder.swift | 3 + 20 files changed, 434 insertions(+), 342 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index ed4ea44..897da83 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1311,7 +1311,7 @@ components: properties: id: { type: string } project_id: { type: string } - title: { type: string } + title: { type: string, description: "Empty until the task's agent names it with set_task_title, when the task was created without one." } merge_title: { type: string, description: "The title of the squash commit Merge makes: the task title until the agent sets another with set_merge_title." } branch: { type: string } worktree_path: { type: string } @@ -1391,9 +1391,12 @@ components: TaskCreate: type: object - required: [title] properties: - title: { type: string } + title: + type: string + description: > + The task's title. Without it the task starts untitled, and its agent names it with set_task_title. + Required with workflow. prompt: type: string description: Starts the task's agent with this as its first message, in the same request. Not with workflow. diff --git a/docs/macos.md b/docs/macos.md index ce0d606..6ba60c1 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -149,7 +149,7 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima ## Keyboard and menus -The app menu has About, Check for Updates…, Forget Server (for the server the window shows), Hide, Hide Others and Quit; File has Add Server… ⌘N, which shows the connection screen; Edit has the standard items, with Cut, Copy, Paste and Select All being the app's own commands so Paste can attach images (see "Gaps in SwiftUI"); a **Go** menu has Switch Task ⌘K, Project ⌘P, Lead ⌘L, Merge ⌘S, Previous Server ⌘[, Next Server ⌘] and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items and the window. The Go commands act on the server the window shows (its `ServerFocus`, a focused scene value holding its store and `Overlays`), and are disabled while that server's alert or modal is open (`dialogOpen`); the switcher and the More menu don't block them, and ⌘L closes the switcher so the Lead composer gets the keys. Previous and Next Server switch the window to the saved server before or after the current one, in saved order, wrapping round; from the connection screen they go to the first. The Ctrl variants (Ctrl-K, P, L, S and /) run the same actions from the key monitor in `Overlays`, except while the terminal's `TerminalView` is the first responder (`terminalFocused`), where they go to the shell. ⌘ combinations always reach the menu, the terminal included. Escape closes the open overlay. +The app menu has About, Check for Updates…, Forget Server (for the server the window shows), Hide, Hide Others and Quit; File has New Task ⌘N, which creates an untitled task in the right pane's project and opens it with its composer focused, and Add Server…, which shows the connection screen; Edit has the standard items, with Cut, Copy, Paste and Select All being the app's own commands so Paste can attach images (see "Gaps in SwiftUI"); a **Go** menu has Switch Task ⌘K, Project ⌘P, Lead ⌘L, Merge ⌘S, Previous Server ⌘[, Next Server ⌘] and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items and the window. New Task and the Go commands act on the server the window shows (its `ServerFocus`, a focused scene value holding its store and `Overlays`), and are disabled while that server's alert or modal is open (`dialogOpen`); the switcher and the More menu don't block them, and ⌘L closes the switcher so the Lead composer gets the keys. Previous and Next Server switch the window to the saved server before or after the current one, in saved order, wrapping round; from the connection screen they go to the first. The Ctrl variants (Ctrl-K, N, P, L, S and /) run the same actions from the key monitor in `Overlays`, except while the terminal's `TerminalView` is the first responder (`terminalFocused`), where they go to the shell. ⌘ combinations always reach the menu, the terminal included. Escape closes the open overlay. ## Connection diff --git a/docs/operations.md b/docs/operations.md index 1702a4f..7e383ab 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -79,6 +79,8 @@ The UI's labels (see "Task state" under the task switcher) read `state` for **Wo `POST /api/tasks/{id}/merge-in` (the transcript's **Sync from main** button, labelled with the default branch) brings the default branch into the task branch through the agent: it sends `mergeInPrompt` (`internal/agents/mergein.go`) as a `byre` message to the task's session, or starts a session with it when the task has none. The prompt has the agent run `git merge --no-ff --no-commit`, resolve conflicts, regenerate generated files rather than hand-merging them, check the build and tests, and leave the merge uncommitted; byre's end-of-turn commit concludes it as a merge commit. If resolving the conflicts took substantive changes rather than combining both sides as written, the prompt tells the agent to stop there and end its turn describing them; the turn's snapshot then gets its review like any other (see "Reviews"). byre doesn't inspect the merge itself: whether a change is substantive, and whether to stop, is the agent's call. On a Merge turn the prompt adds that byre squashes and pushes when the turn ends, and tells the agent to call the `stop_merge` MCP tool first if it stops (see below). The prompt ends by showing the task's current merge title and asking the agent to set a better one with `set_merge_title` if it no longer describes all the work on the branch. It is refused (409) while a session is `running`. +A task's `title` is optional at creation (`POST /api/projects/{id}/tasks` without `title` stores it empty). Task agents name and rename their task with the `set_task_title` MCP tool (`internal/agents/mergein.go`): one non-empty line, stored in `tasks.title` by `Store.SetTitle` and recorded as a `task.title` event that the transcript shows as a note and that makes the app reload the project's tasks. It is refused once the task is closed and while `merging_by`, `finalizing` or `merge_queued_by` is set, since a merge title that follows the task title must not change under a merge. The guidance (`taskGuidance` in `internal/agents/guidance.go`) tells the agent to call it first thing with a very short, distinct, to-the-point title when the task has none, or when its title isn't one, and again whenever the work moves on. + A task's `merge_title` is the title of the squash commit Merge makes. It is the task title until the agent calls the `set_merge_title` MCP tool (`internal/agents/mergein.go`), which it may do until a merge is confirmed: one line of at most 72 characters (`maxMergeTitle`, checked by `cleanMergeTitle`), stored in `tasks.merge_title` (empty there means "use the title"), recorded as a `task.merge_title` event that the transcript shows as a note. Task titles have no length limit (a TODO item becomes the title whole), so when an implementer session's agent process starts with a merge title over 72 characters, `longTitleGuidance` (`internal/agents/guidance.go`) adds a line to its guidance quoting the title and its length and telling the agent to call `set_merge_title` early in its first turn; a process started once the title fits gets no such line. Malformed titles are rejected with a corrective message, and so is a call once the task is closed or while `merging_by`, `finalizing` or `merge_queued_by` is set. The Merge confirm dialog shows the title in a text field the human can edit (Enter confirms), lists the co-authors, and sends the field's value as `merge_title` in the body of `POST /api/tasks/{id}/merge`. Merge validates it like the tool does (400 when malformed) and, when it differs from the stored title, stores it with the human as the event's actor before starting or holding the merge. A task's participants are the users who put a message bubble with their name in its transcript, derived from its events (`Store.Participants`): the actors of its `user.message` events, joined to `users` so `byre` and `agent` drop out. That covers messages sent to the agent and inbox answers, whose delivery message carries the name of the first responder in it (`Deliver` in `internal/agents/inbox.go`); dismissing an item puts nothing in the transcript and doesn't count. A fork's participants also include the user who forked it, the actor of its `task.created` event (whose payload carries `forked_from`), whose "forked this task" note opens its transcript. They are ordered by their first such event. `GET /api/tasks/{id}/co-authors` returns the participants other than the caller, with their git name and email from `PUT /api/me` (empty for a user who hasn't set one). The Merge confirm fetches it and lists `Name ` for each co-author, and separately those with no git identity, who get no trailer. @@ -278,7 +280,7 @@ Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and **Drafts.** Unsent composer text (task and lead) and unsent inbox answers (checked choices and Other text) are kept in memory by `Drafts` (`mac/Sources/Byre/Drafts.swift`) and saved to a SwiftData `DraftRecord` per server and login (`docs/macos.md`, "Drafts"), one JSON object with `composer` (keyed by `message:` or `message:lead`, each `{text, images}`), `checked` and `texts` (both keyed by inbox item id), and `comments`, the Changes view's pending line comments keyed by task id, each `{anchor: {path, line, old, quote}, text}`. A comment shows only under a diff line whose path, line number, side and text all still match, so after the agent moves lines a comment never attaches to a different line. Every pending comment not shown under a line (its file closed or not loaded, or its line gone from the diff) is listed at the top of the Changes view with its `path:line`, the quoted line, its editable text and **remove**, so nothing is sent unseen. Writes are debounced (400 ms) and flushed at once when a composer or answer box loses focus, on send and on quit. A composer draft is cleared only once its send succeeds, and an inbox answer only once its response is accepted; each send in flight is tracked on its own and the stored draft holds the text of every unconfirmed send (oldest first, then the composer's current text, separated by blank lines) and counts its images, so a relaunch mid-send brings back everything not yet confirmed. A failed send's text goes back into the composer ahead of whatever is there now (text typed since, or earlier failures), separated by a blank line, rather than replacing it. The composer follows its `Drafts` text as it changes, so text put back from outside it (a withdrawn queued message, a draft synced in from another Mac) shows at once. Drafts for tasks that are no longer active and for inbox items no longer open are pruned whenever the task list or inbox reloads. A record that doesn't decode is logged and ignored, as is a write that fails; drafts then live only in memory. Pending images are not stored, only their count: after a relaunch the composer shows how many need re-attaching, until dismissed or the next successful send. -The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks are grouped by state (`taskRank` in `mac/Sources/Byre/Store.swift`, which also orders the Project tab's task list, ties in the server's order, subtasks sorted among their siblings under their parent): with inbox items first, then **Ready to merge** or **Ready to discard**, then idle, then working or reviewing, and merging, held or discarding last; within each group, the task least recently visited comes first (never-visited tasks before all others, the right pane's own task after all others, ties in the server's order: projects by name, tasks newest first), so ⌘K Enter repeated cycles through the tasks. Visit times are SwiftData `VisitRecord`s synced through iCloud (`docs/macos.md`, "Persistence"), stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows the user's default model (see "Agent sessions"), the model the composer offers first for the new task's first message; the task itself starts with no session. Below each create row, a "Start in …" row creates the same task and starts its agent on the default model with the title as its first message, through `POST /api/projects/{id}/tasks` with `prompt` set to the title. Picking a task points the right pane at it on the Transcript tab, scrolled to its oldest open question when it has open inbox items, otherwise to its latest summary when that is unread (see below and "Lead agent"). +The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks are grouped by state (`taskRank` in `mac/Sources/Byre/Store.swift`, which also orders the Project tab's task list, ties in the server's order, subtasks sorted among their siblings under their parent): with inbox items first, then **Ready to merge** or **Ready to discard**, then idle, then working or reviewing, and merging, held or discarding last; within each group, the task least recently visited comes first (never-visited tasks before all others, the right pane's own task after all others, ties in the server's order: projects by name, tasks newest first), so ⌘K Enter repeated cycles through the tasks. Visit times are SwiftData `VisitRecord`s synced through iCloud (`docs/macos.md`, "Persistence"), stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. With nothing typed, a last "New task in …" row creates an untitled task with no session in the right pane's project (the first project when there is none) and opens it with its composer focused; it shows the user's default model (see "Agent sessions"), the model the composer offers first for the new task's first message. ⌘N (File ▸ New Task) does the same without the switcher. Typing filters tasks and projects and replaces that row with a "Start in …" row, which creates an untitled task and starts its agent on the default model with the typed text as its first message, through `POST /api/projects/{id}/tasks` with `prompt` set to the text and no `title`; Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and the create rows for each of those projects with "fix" as the message. When no project name starts with it, the whole text is the message, so "Fix: login" stays as typed. The app shows an untitled task as "New task" until its agent names it. Picking a task points the right pane at it on the Transcript tab, scrolled to its oldest open question when it has open inbox items, otherwise to its latest summary when that is unread (see below and "Lead agent"). **Task state.** The switcher and the Project tab show one label per task for its `state` (see "Task state in the store"; `stateTitles` in `mac/Sources/Byre/Store.swift`), with a dot and a tooltip saying why: **Needs your answer** (`asking`), **Merging** (`merging`), **Merge held** (`merge_held`), **Merge after review** (`merge_after_review`), **Reviewing** (`review_pending` or `reviewing`), **Working** (`running`), **Discarding** (`discarding`), **Ready to merge** (`ready_to_merge`), **Ready to discard** (`discard_suggested`), **Idle** (`idle` or `no_agent`). The Project tab, in its task list and TODO items, draws a spinner in place of the dot for **Reviewing**, **Working**, **Discarding** and **Merging**. The label follows the tasks the app already holds, so it needs no extra fetch. @@ -477,7 +479,7 @@ A workflow is a named graph of stages, each a stage name, the stages it comes af - **Validation.** `workflow.Validate` refuses a definition (400, everything wrong listed) with no stages, a stage name that isn't a valid one-line name or repeats another stage's, an empty model without `agents` or a model with them, `agents` that fail `workflow.TeamProblems`, an empty prompt, a `base` other than `parent` or `main`, a `base` on a stage with more than one `after` stage, an `after` naming the stage itself, a name twice or a name that isn't a stage, a cycle, or anything but exactly one final stage (one no other stage comes after). A stage's vendor follows from its model (`workflow.VendorOf`, the same rule sessions use: `gpt-*` is Codex, `gemini-*` Antigravity, everything else Claude Code); a file with a `vendor` key is refused as an unknown field. - **API.** `GET /api/workflows` lists every workflow's name and description. `GET /api/workflows/{name}` downloads its file; `PUT` takes a file as the body and creates or replaces the workflow; `DELETE` removes it. Each save or delete records a `workflow.updated` or `workflow.deleted` event carrying the name. `sqlite3 byre.db 'SELECT name FROM workflows'` lists them. Saves and reads go through the Manager (`internal/agents/workflows.go`) so the HTTP API and the lead's tools share them. - **Lead tools.** `list_workflows`, `read_workflow` (`name`, or `task_id` for a task's frozen copy), `save_workflow` (`name` and `definition`, the file as YAML) and `delete_workflow`, with the same checks and errors as the API. Task agents don't get them. -- **Starting.** The lead's `create_task` and `start_todo` take an optional `workflow` (404 when there is none of that name) and then start a run of it instead of an agent on the prompt (`Manager.StartWorkflow`); `model` together with `workflow` is refused, since each stage names its own. `POST /api/projects/{id}/tasks` takes the same optional `workflow`, with the title as the run's brief, and returns the run's first stage task. The task switcher (`mac/Sources/Byre/Switcher.swift`) offers it as a "Start workflow…" row after each "New task in …" and "Start in …" pair: picking that lists `GET /api/workflows`, filtered by what is typed next (Esc goes back to the task list), and picking a workflow starts the run with the typed title and opens its first stage task. A run gets a new id and creates a task for every stage with no `after` stages, branched from the default branch, or for a stage with `base: parent`, from the lead's `from_task_id` HEAD when given. Each stage task copies the workflow's name and definition as they are when the run starts into `tasks.workflow_name` and `tasks.workflow_definition`, so later saves or deletes don't change it; its brief into `tasks.workflow_brief` (the lead's `create_task` `prompt`, optional with a workflow and may be empty, or for `start_todo` the TODO item as `todoPrompt` words it); the run's title into `tasks.workflow_title`; the run id into `tasks.workflow_run`; and its stage's name into `tasks.workflow_stage`. Stage tasks are titled `: <stage>`, with the run's title as their merge title, and carry the run's TODO item. The Task's `workflow`, `workflow_run` and `workflow_stage` are those; `GET /api/tasks/{id}/workflow` returns the copy as `{name, definition}` with the definition as YAML (404 for a task without one). A task agent's `create_subtask`, `POST /api/projects/{id}/tasks` without `workflow` and the Project tab's Start create tasks without one. A fork of a stage task copies its frozen workflow, brief, title, run and stage, so it is another attempt at the same stage of the same run. `sqlite3 byre.db "SELECT id, status, workflow_stage, stage_output != '' FROM tasks WHERE workflow_run = '<run>'"` lists a run's tasks and which have finished. +- **Starting.** The lead's `create_task` and `start_todo` take an optional `workflow` (404 when there is none of that name) and then start a run of it instead of an agent on the prompt (`Manager.StartWorkflow`); `model` together with `workflow` is refused, since each stage names its own. `POST /api/projects/{id}/tasks` takes the same optional `workflow`, with the title, required then, as the run's brief, and returns the run's first stage task. The task switcher (`mac/Sources/Byre/Switcher.swift`) offers it as a "Start workflow…" row after each "Start in …" row: picking that lists `GET /api/workflows`, filtered by what is typed next (Esc goes back to the task list), and picking a workflow starts the run with the typed text as its title and brief and opens its first stage task. A run gets a new id and creates a task for every stage with no `after` stages, branched from the default branch, or for a stage with `base: parent`, from the lead's `from_task_id` HEAD when given. Each stage task copies the workflow's name and definition as they are when the run starts into `tasks.workflow_name` and `tasks.workflow_definition`, so later saves or deletes don't change it; its brief into `tasks.workflow_brief` (the lead's `create_task` `prompt`, optional with a workflow and may be empty, or for `start_todo` the TODO item as `todoPrompt` words it); the run's title into `tasks.workflow_title`; the run id into `tasks.workflow_run`; and its stage's name into `tasks.workflow_stage`. Stage tasks are titled `<title>: <stage>`, with the run's title as their merge title, and carry the run's TODO item. The Task's `workflow`, `workflow_run` and `workflow_stage` are those; `GET /api/tasks/{id}/workflow` returns the copy as `{name, definition}` with the definition as YAML (404 for a task without one). A task agent's `create_subtask`, `POST /api/projects/{id}/tasks` without `workflow` and the Project tab's Start create tasks without one. A fork of a stage task copies its frozen workflow, brief, title, run and stage, so it is another attempt at the same stage of the same run. `sqlite3 byre.db "SELECT id, status, workflow_stage, stage_output != '' FROM tasks WHERE workflow_run = '<run>'"` lists a run's tasks and which have finished. - **Stages.** A stage task's agent is an implementer session on the stage's model (for a team stage, one per agent, see "Teams"), created by the user who started the run; any later session of the task (a Compact handoff's, one started by a message) runs the same stage, since the stage belongs to the task. Starting a stage records a `task.workflow_stage` event on its task (`workflow`, `run`, `stage` the name, `after` the ids of the tasks whose outputs it gets; actor the lead for the stages a run starts with, `byre` after) and sends the session its first message, actor `byre`, so the transcript shows it collapsed under its first line: `Workflow "<name>", stage "<stage>"`, then the stage's prompt, a `## Brief` section when the brief isn't empty, a `## From stage "<stage>" (task <id>, branch <branch>)` section with each `after` stage's output, and a line saying where the task starts: from the default branch, with the branches above to `git merge` or read as it needs, or from the commit its one `after` stage finished at. Nothing else of earlier stages is passed on; their branches carry their work. - **Advancing.** A stage session gets the `finish_stage` tool (`output`, required) and a guidance line naming the workflow, its stage and the stages after it. The call stores the output in `agent_sessions.stage_output`, replacing any earlier one; it is refused once the task's stage has finished. When a turn of that session ends normally (not an error or interrupt, not a handoff, not a Merge turn that squashes) with an output stored, byre sets `agent_sessions.stage_finished` before marking the session idle, and then advances (`nextStage`): it records the output and the worktree's HEAD in `tasks.stage_output` and `tasks.stage_commit`, then, holding `workflowMu` so two stages finishing at once can't both start the one after them, goes through each stage after this one. A stage starts once every stage it comes after has a finished task in the run (the latest one, when a fork made several); its task branches from that task's `stage_commit` for `base: parent` with one `after` stage, else from the default branch. Once every stage after it has been dealt with, byre clears `stage_output` and `stage_finished` on the session. The session stays idle in its task for more messages, and queued messages and held answers are delivered to it once advancing is done. When the final stage finishes, byre starts discarding every other active task of the run (as `byre`, through the same `Discard` as the button, so each gets its summary turn first and closes when that turn ends; one already merging or being discarded is skipped and logged as `stage task not discarded`), then records `task.workflow_finished` (`workflow`, `output`) on the final task and clears the flags in the same transaction, leaving the final task for the human to merge. A turn that ends without the call leaves the task in its stage. The output belongs to the turn that stored it: when that turn ends any other way (an error, an interrupt, a handoff, a Merge turn that squashes, an agent process that exits mid-turn, or a session that can't be resumed after a restart), byre clears `stage_output` before marking the session idle, so a later turn can't hand off a stale output; the agent calls `finish_stage` again. A turn cut off by a byre restart and resumed keeps it. If a stage can't start, byre logs `advance workflow stage` and records the error in the finishing task's transcript, and the flag stays set. On startup, before resuming running sessions, `recoverStages` advances every session with `stage_finished` set, skipping the steps already done: the recorded output isn't overwritten; a stage that already has a task in the run isn't created again, but when that task is active and has no session, its sessions are started, and each of its sessions that never got a message (`SessionStarted`) is sent the stage's first message (a team member's with its own prompt), and the restart notice isn't sent to them. `sqlite3 byre.db 'SELECT id, task_id FROM agent_sessions WHERE stage_finished = 1'` lists stages waiting to advance. `list_tasks` shows each stage task's workflow and `stage "<name>"`, with `(finished)` once its stage has finished and `workflow finished` once any task of the run has a `task.workflow_finished` event. Nothing reads or writes `agent_sessions.workflow_stage`; it stays in the schema so builds from older branches keep working against the same database. - **Progress.** `GET /api/tasks/{id}/workflow/run` (`Manager.WorkflowRun`) returns the run of a stage task (404 for a task without one): the definition that task froze (typed, not YAML), name, title, brief, `finished` (any task of the run has `task.workflow_finished`), and per stage in definition order its resolved `after`, `waiting_on` (the `after` stages with no finished attempt), its `attempts` and a `state`. An attempt is every task of the run at that stage, closed ones included, oldest first (the first and its forks), with its `output` and `commit` once `tasks.stage_output` is set, `forked_from`, `discarded_by` (the `task.discarded` actor) and `inputs`: the tasks named in the `after` of its latest `task.workflow_stage` event, each with its stage, so it says which attempt fed it; a fork has no such event and takes its source's. An attempt's state is `merged`, `cleaned_up` (discarded after finishing; `byre` as `discarded_by` is the final stage's cleanup), `discarded` (before finishing), `finished`, `running` (a session is running) or `idle`. A stage is `waiting` with no attempts, else it takes the state of its latest finished attempt, or its latest attempt when none finished. The Workflow tab (`mac/Sources/Byre/Workflow.swift`) draws the graph natively with the flowchart layout from `Diagram.swift`, each stage's node coloured by its state (`stateColors`, from the palette's tint and accent pairs: waiting grey, running accent, idle warn, finished, merged and cleaned up ok, discarded danger) and redrawn whenever a refetch changes a state; then a card per stage with its state, its attempts linking to their tasks, each attempt's inputs ("started from") and the attempts it fed, and its output collapsed as Markdown. It refetches on any stored `task.*` or `session.*` event of a task in the run, and on a `task.created` whose payload carries the run, so a new stage or fork shows up. diff --git a/internal/agents/guidance.go b/internal/agents/guidance.go index 15e8a8b..65e9643 100644 --- a/internal/agents/guidance.go +++ b/internal/agents/guidance.go @@ -101,9 +101,13 @@ func (m *Manager) takeBounce(ctx context.Context, l *live, final string) (bool, func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, error) { g := guidance + fmt.Sprintf("\n- This task's ports are %d to %d; $PORT holds the first. Run dev servers and anything else that listens on these, so parallel tasks don't collide, and listen on 127.0.0.1. To show the human a running server, call byre's expose_port tool."+ - "\n- This worktree starts with only what git tracks. Before other work in a new worktree, do any setup the project's AGENTS.md describes."+ - "\n- This task's title is %q. Whoever created the task wrote it, and it may be the only statement of what they want.", - task.PortBase, task.PortBase+store.PortBlock-1, task.Title) + "\n- This worktree starts with only what git tracks. Before other work in a new worktree, do any setup the project's AGENTS.md describes.", + task.PortBase, task.PortBase+store.PortBlock-1) + if task.Title == "" { + g += "\n- This task has no title yet. First thing, before any other work, call set_task_title with a very short, distinct, to-the-point title (two to four words), and call it again whenever the work moves on." + } else { + g += fmt.Sprintf("\n- This task's title is %q. First thing, if it isn't very short (two to four words), distinct and to the point, call set_task_title with one that is, and call it again whenever the work moves on.", task.Title) + } if task.TodoItem != nil { g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.", *task.TodoItem, todo.File, todo.File) diff --git a/internal/agents/manager.go b/internal/agents/manager.go index ea0681f..7fe955d 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -742,7 +742,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api if err != nil { return nil, err } - tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool()}, append(m.subtaskTools(l), m.clientTools(l)...)...) + tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTaskTitleTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool()}, append(m.subtaskTools(l), m.clientTools(l)...)...) lead := true if sess.Role == api.Implementer { g += longTitleGuidance(task) diff --git a/internal/agents/mergein.go b/internal/agents/mergein.go index ceb1fe9..ccf1efd 100644 --- a/internal/agents/mergein.go +++ b/internal/agents/mergein.go @@ -638,6 +638,41 @@ func (m *Manager) setMergeTitle(ctx context.Context, l *live, raw json.RawMessag return fmt.Sprintf("Merge commit title set to %q.", title), nil } +func (m *Manager) setTaskTitleTool(l *live) mcp.Tool { + return mcp.Tool{ + Name: "set_task_title", + Description: "Set this task's title, which names it in byre's task list and switcher. " + + "Keep it very short (two to four words), distinct from the project's other tasks, and to the point, for example \"webhook retries\". " + + "Until set_merge_title is called, the merge commit title follows it. It replaces any earlier title.", + InputSchema: json.RawMessage(`{ + "type": "object", + "properties": { + "title": {"type": "string", "description": "One short line."} + }, + "required": ["title"] + }`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + args := setMergeTitleArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed set_task_title call: %w", err) + } + title := strings.TrimSpace(args.Title) + if title == "" || strings.ContainsAny(title, "\r\n") { + return "", fmt.Errorf("%w: title must be one non-empty line", ErrInvalid) + } + err = m.store.SetTitle(ctx, l.taskID, title, agentActor) + if errors.Is(err, store.ErrNotFound) { + return "", fmt.Errorf("%w: this task is closed or merging", ErrConflict) + } + if err != nil { + return "", err + } + return fmt.Sprintf("Task title set to %q.", title), nil + }, + } +} + type stopMergeArgs struct { Reason string `json:"reason"` } diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index d16924d..b2e6cbf 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1355,8 +1355,10 @@ type Task struct { Status TaskStatus `json:"status"` // SummarySeq The seq of the task's latest turn.ended event with summary: true; 0 when it has none. - SummarySeq int64 `json:"summary_seq"` - Title string `json:"title"` + SummarySeq int64 `json:"summary_seq"` + + // Title Empty until the task's agent names it with set_task_title, when the task was created without one. + Title string `json:"title"` // TodoItem The title of the TODO.md item the task was started from; absent for other tasks. TodoItem *string `json:"todo_item,omitempty"` @@ -1395,7 +1397,9 @@ type TaskCreate struct { // Prompt Starts the task's agent with this as its first message, in the same request. Not with workflow. Prompt *string `json:"prompt,omitempty"` - Title string `json:"title"` + + // Title The task's title. Without it the task starts untitled, and its agent names it with set_task_title. Required with workflow. + Title *string `json:"title,omitempty"` // Workflow The name of a saved workflow to run instead of creating one task with no agent: each stage with no after stages becomes its own task, titled "<title>: <stage>", with the title as the run's brief. The response is the first of those tasks. Workflow *string `json:"workflow,omitempty"` @@ -4127,290 +4131,291 @@ 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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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/server.go b/internal/server/server.go index f592624..fd1e44a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -314,9 +314,9 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st return } - if strings.TrimSpace(req.Title) == "" { - writeError(w, http.StatusBadRequest, errors.New("title is required")) - return + title := "" + if req.Title != nil { + title = strings.TrimSpace(*req.Title) } p, err := s.store.GetProject(r.Context(), projectID) @@ -330,12 +330,16 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st writeError(w, http.StatusBadRequest, errors.New("prompt can't be given with workflow")) return } + if title == "" { + writeError(w, http.StatusBadRequest, errors.New("title is required with workflow")) + return + } u, err := s.store.GetUser(r.Context(), actor(r)) if err != nil { writeStoreError(w, err) return } - tasks, err := s.agents.StartWorkflow(r.Context(), p, u, req.Title, p.DefaultBranch, u.Login, *req.Workflow, req.Title, nil) + tasks, err := s.agents.StartWorkflow(r.Context(), p, u, title, p.DefaultBranch, u.Login, *req.Workflow, title, nil) if err != nil { writeAgentError(w, err) return @@ -345,7 +349,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st } if req.Prompt == nil { - t, err := s.agents.CreateTask(r.Context(), p, req.Title, actor(r), p.DefaultBranch, actor(r)) + t, err := s.agents.CreateTask(r.Context(), p, title, actor(r), p.DefaultBranch, actor(r)) if err != nil { writeAgentError(w, err) return @@ -367,7 +371,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st if req.Model != nil { model = *req.Model } - t, err := s.agents.StartTask(r.Context(), p, u, req.Title, *req.Prompt, model) + t, err := s.agents.StartTask(r.Context(), p, u, title, *req.Prompt, model) if err != nil { writeAgentError(w, err) return diff --git a/internal/store/store.go b/internal/store/store.go index ae02017..ed8fe7b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -391,6 +391,21 @@ func (s *Store) SetMergeTitle(ctx context.Context, id, title, actor string) erro }) } +func (s *Store) SetTitle(ctx context.Context, id, title, actor string) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + projectID := "" + err := tx.QueryRowContext(ctx, `UPDATE tasks SET title = ? WHERE id = ? AND status = ? AND merging_by = '' AND finalizing = '' AND merge_queued_by = '' RETURNING project_id`, + title, id, string(api.TaskStatusActive)).Scan(&projectID) + if errors.Is(err, sql.ErrNoRows) { + return ErrNotFound + } + if err != nil { + return err + } + return emit(api.Event{Kind: "task.title", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id, "title": title}}) + }) +} + func (s *Store) SetMergingBy(ctx context.Context, id, login string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" diff --git a/mac/Sources/Byre/API.swift b/mac/Sources/Byre/API.swift index df17c3c..bffdcbe 100644 --- a/mac/Sources/Byre/API.swift +++ b/mac/Sources/Byre/API.swift @@ -19,6 +19,12 @@ typealias TaskTab = Components.Schemas.TaskTab typealias Snapshot = Components.Schemas.Snapshot typealias ServerStatus = Components.Schemas.Status +extension ByreTask { + var name: String { + title.isEmpty ? "New task" : title + } +} + struct ApiError: Error, CustomStringConvertible { let status: Int let message: String diff --git a/mac/Sources/Byre/ByreApp.swift b/mac/Sources/Byre/ByreApp.swift index a963524..b010d77 100644 --- a/mac/Sources/Byre/ByreApp.swift +++ b/mac/Sources/Byre/ByreApp.swift @@ -92,11 +92,11 @@ struct ByreCommands: Commands { .disabled(focus == nil) } CommandGroup(replacing: .newItem) { + goButton("New Task", "n") Button("Add Server…") { Servers.shared.current = nil openWindow(id: "main") } - .keyboardShortcut("n") } PasteboardCommands() CommandMenu("Go") { diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index d788850..a969e9d 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -1598,7 +1598,7 @@ struct NoteView: View { out += AttributedString(id) continue } - var a = AttributedString(task.title) + var a = AttributedString(task.name) if task.status == .active { a.link = AppLink.task(task.id) a.foregroundColor = taskPalette[task.color] diff --git a/mac/Sources/Byre/MergeDialog.swift b/mac/Sources/Byre/MergeDialog.swift index 337ccae..959d7c8 100644 --- a/mac/Sources/Byre/MergeDialog.swift +++ b/mac/Sources/Byre/MergeDialog.swift @@ -107,7 +107,7 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays) async } let title = mergeDrafts[taskId] ?? task.mergeTitle mergeDrafts[taskId] = nil - let model = MergeModel(taskId: taskId, target: target, parentTitle: parent?.status == .active ? parent?.title : nil, coAuthors: coAuthors, title: title) + let model = MergeModel(taskId: taskId, target: target, parentTitle: parent?.status == .active ? parent?.name : nil, coAuthors: coAuthors, title: title) model.leave = { [weak model] go in if let model { mergeDrafts[taskId] = model.sentTitle @@ -346,7 +346,7 @@ private struct MergeContent: View { @MainActor func confirmDiscard(_ task: ByreTask, _ store: Store, _ overlays: Overlays, anchor: CGRect?) async { - if !(await overlays.showConfirm(["Discard \"\(task.title)\"?"], action: "Discard", danger: true, anchor: anchor)) { + if !(await overlays.showConfirm(["Discard \"\(task.name)\"?"], action: "Discard", danger: true, anchor: anchor)) { return } do { diff --git a/mac/Sources/Byre/Notifications.swift b/mac/Sources/Byre/Notifications.swift index 7b15e8c..59d31de 100644 --- a/mac/Sources/Byre/Notifications.swift +++ b/mac/Sources/Byre/Notifications.swift @@ -57,7 +57,7 @@ private func enqueue(_ op: @escaping @MainActor () async -> Void) { private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: String) { let line = trimmed(text).split(separator: "\n", omittingEmptySubsequences: false).first.map(String.init) ?? "" let content = UNMutableNotificationContent() - content.title = store.findTask(taskId).map { "byre · \($0.title)" } ?? "byre" + content.title = store.findTask(taskId).map { "byre · \($0.name)" } ?? "byre" content.body = line.count > 200 ? "\(line.prefix(199))…" : line content.sound = .default content.userInfo = ["task_id": taskId, "origin": store.api.origin.absoluteString] diff --git a/mac/Sources/Byre/Panes.swift b/mac/Sources/Byre/Panes.swift index baddf77..325ba34 100644 --- a/mac/Sources/Byre/Panes.swift +++ b/mac/Sources/Byre/Panes.swift @@ -226,7 +226,7 @@ private struct TaskBar: View { var body: some View { let task = store.selectedTaskId.flatMap { store.findTask($0) } let project = store.focusedProject() - let label = task.map { "\(project?.name ?? "") ▸ \($0.title)" } ?? project?.name ?? "No task" + let label = task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task" HStack(spacing: 0) { Color.clear.frame(maxWidth: .infinity, maxHeight: 1) Button { diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index 4025a5e..3add719 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -174,10 +174,10 @@ private struct TaskRow: View { if let parent = task.parentTaskId { Text("↳") .foregroundStyle(Palette.muted) - .help("Subtask of \(store.findTask(parent)?.title ?? parent)") + .help("Subtask of \(store.findTask(parent)?.name ?? parent)") } StateMark(state: ts, color: color) - Text(task.title) + Text(task.name) .foregroundStyle(color) .lineLimit(1) .truncationMode(.tail) @@ -190,7 +190,7 @@ private struct TaskRow: View { .font(Fonts.font(11, weight: 700)) .foregroundStyle(Palette.accent) .help("A turn summary you haven't read") - .accessibilityLabel("New summary from \(task.title), unread") + .accessibilityLabel("New summary from \(task.name), unread") } else { Text(ts.label) .font(Fonts.font(11)) diff --git a/mac/Sources/Byre/Status.swift b/mac/Sources/Byre/Status.swift index b416b1c..5ae8679 100644 --- a/mac/Sources/Byre/Status.swift +++ b/mac/Sources/Byre/Status.swift @@ -192,7 +192,7 @@ private struct TaskLinkText: View { Button { store.focusTask(taskId) } label: { - Text("\(project.map { "\($0.name) ▸ " } ?? "")\(task?.title ?? taskId)") + Text("\(project.map { "\($0.name) ▸ " } ?? "")\(task?.name ?? taskId)") .foregroundStyle(store.taskColor(taskId) ?? Palette.accent) } .buttonStyle(.plain) @@ -257,7 +257,7 @@ private struct TurnList: View { if let project { Text("\(project.name) ▸ ").foregroundStyle(Palette.muted) } - Text(task?.title ?? t.taskId).foregroundStyle(color) + Text(task?.name ?? t.taskId).foregroundStyle(color) } .lineLimit(1) .truncationMode(.tail) diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index fc0444c..f3592e1 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -618,7 +618,7 @@ final class Store { spawn("load projects") { try await self.loadProjects() } - case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created": + case "task.created", "task.title", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created": if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && !["answer", "fork"].contains(str(ev.payload.value, "trigger"))) { clearTask(self, taskId) } diff --git a/mac/Sources/Byre/Switcher.swift b/mac/Sources/Byre/Switcher.swift index 7b1f3a6..c536510 100644 --- a/mac/Sources/Byre/Switcher.swift +++ b/mac/Sources/Byre/Switcher.swift @@ -4,7 +4,7 @@ import SwiftUI typealias WorkflowSummary = Components.Schemas.WorkflowSummary -let goKeys: Set<String> = ["k", "p", "l", "s", "/"] +let goKeys: Set<String> = ["k", "n", "p", "l", "s", "/"] @MainActor func runGo(_ key: String, _ store: Store, _ overlays: Overlays) { @@ -12,6 +12,11 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) { switch key { case "k": overlays.openSwitcher(store) + case "n": + overlays.switcher?.close() + if let project = store.focusedProject() ?? store.projects.first { + createTask(project.id, .init(), store, overlays) + } case "p": store.focusTab(.project) case "l": @@ -31,6 +36,19 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) { } } +@MainActor +func createTask(_ projectId: String, _ body: Components.Schemas.TaskCreate, _ store: Store, _ overlays: Overlays) { + Task { + do { + let task = try await store.api.client.createTask(path: .init(projectId: projectId), body: .json(body)).created.body.json + store.composerFocus = task.id + store.focusTask(task.id, .transcript) + } catch { + overlays.showAlert(errorText(error)) + } + } +} + @MainActor func ctrlGo(_ key: String, _ store: Store, _ overlays: Overlays) -> Bool { if terminalFocused() { @@ -43,6 +61,7 @@ func ctrlGo(_ key: String, _ store: Store, _ overlays: Overlays) -> Bool { private let shortcuts: [(String, [(String, String)])] = [ ("Anywhere", [ ("⌘K", "Switch task, or create one"), + ("⌘N", "New task in the current project"), ("⌘P", "Open the Project tab"), ("⌘L", "Open the Lead tab and its composer"), ("⌘S", "Merge the right pane's task"), @@ -53,7 +72,7 @@ private let shortcuts: [(String, [(String, String)])] = [ ("Task switcher", [ ("↑ ↓", "Move the highlight"), ("Enter", "Pick the highlighted row"), - ("name: title", "Create the task in the project whose name starts with name"), + ("name: message", "Start the task in the project whose name starts with name"), ("Esc", "Back from the workflow list to the task list"), ]), ("Composer and answers", [ @@ -101,14 +120,14 @@ private func includes(_ s: String, _ q: String) -> Bool { q.isEmpty || s.contains(q) } -private let switcherPlaceholder = "Go to a task, or type a title to create one…" +private let switcherPlaceholder = "Go to a task, or type a first message to start one…" private let projectPrefix = Re(#"^([^:\s]+):\s*(.*)$"#) struct SwitcherRow: Identifiable { enum Kind { case task(Project, ByreTask, Int) case project(Project) - case newTask(Project, String) + case newTask(Project) case startTask(Project, String) case startWorkflow(Project, String) case workflow(Project, String, WorkflowSummary) @@ -199,12 +218,15 @@ final class SwitcherModel { rows.append(SwitcherRow(id: "project:\(p.id)", kind: .project(p))) } let fallback = store.focusedProject() ?? store.projects.first - for project in title.isEmpty ? [] : targets ?? (fallback.map { [$0] } ?? []) { - rows.append(SwitcherRow(id: "new:\(project.id)", kind: .newTask(project, title))) + for project in targets ?? (fallback.map { [$0] } ?? []) { + if title.isEmpty { + rows.append(SwitcherRow(id: "new:\(project.id)", kind: .newTask(project))) + continue + } rows.append(SwitcherRow(id: "go:\(project.id)", kind: .startTask(project, title))) rows.append(SwitcherRow(id: "start:\(project.id)", kind: .startWorkflow(project, title))) } - return (rows, "No active tasks. Type a title to create one.") + return (rows, "No active tasks. Type a first message to start one.") } func setText(_ value: String) { @@ -232,10 +254,10 @@ final class SwitcherModel { case .project(let p): close() store.focusProject(p.id) - case .newTask(let p, let title): - create(p.id, .init(title: title)) - case .startTask(let p, let title): - create(p.id, .init(title: title, prompt: title, model: store.defaultModel)) + case .newTask(let p): + create(p.id, .init()) + case .startTask(let p, let message): + create(p.id, .init(prompt: message, model: store.defaultModel)) case .startWorkflow(let p, let title): pickWorkflow(p, title) case .workflow(let p, let title, let w): @@ -271,14 +293,7 @@ final class SwitcherModel { private func create(_ projectId: String, _ body: Components.Schemas.TaskCreate) { close() - Task { - do { - let task = try await store.api.client.createTask(path: .init(projectId: projectId), body: .json(body)).created.body.json - go(task.id) - } catch { - overlays.showAlert(errorText(error)) - } - } + createTask(projectId, body, store, overlays) } private func pickWorkflow(_ project: Project, _ title: String) { @@ -420,7 +435,7 @@ private struct SwitcherRowView: View { .font(Fonts.font(11, weight: 700)) .foregroundStyle(Palette.accent) .help("A turn summary you haven't read") - .accessibilityLabel("New summary from \(task.title), unread") + .accessibilityLabel("New summary from \(task.name), unread") } else { Text(ts.label) .font(Fonts.font(11)) @@ -435,22 +450,22 @@ private struct SwitcherRowView: View { .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) state("project") - case .newTask(let p, let title): - Text(created([("New task in ", nil), (p.name, Palette.muted), (": \(title)", nil)])) + case .newTask(let p): + Text(created([("New task in ", nil), (p.name, Palette.muted)])) .foregroundStyle(Palette.accent) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) state(store.defaultModel) .help("Model for the first message") - case .startTask(let p, let title): - Text(created([("Start in ", nil), (p.name, Palette.muted), (": \(title)", nil)])) + case .startTask(let p, let message): + Text(created([("Start in ", nil), (p.name, Palette.muted), (": \(message)", nil)])) .foregroundStyle(Palette.accent) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) state(store.defaultModel) - .help("The title is the agent's first message") + .help("The agent's first message; the agent names the task") case .startWorkflow(let p, let title): Text(created([("Start workflow… in ", nil), (p.name, Palette.muted), (": \(title)", nil)])) .foregroundStyle(Palette.accent) @@ -486,9 +501,9 @@ private struct SwitcherRowView: View { private func taskTitle(_ project: Project, _ task: ByreTask, _ color: Color) -> AttributedString { var parts: [(String, Color?)] = [(project.name, Palette.muted), (" ", nil)] if let parent = task.parentTaskId { - parts.append(("\(store.findTask(parent)?.title ?? parent) ↳ ", Palette.muted)) + parts.append(("\(store.findTask(parent)?.name ?? parent) ↳ ", Palette.muted)) } - parts.append((task.title, color)) + parts.append((task.name, color)) return created(parts) } diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift index 698732d..1c86072 100644 --- a/mac/Sources/Byre/TranscriptBuilder.swift +++ b/mac/Sources/Byre/TranscriptBuilder.swift @@ -758,6 +758,9 @@ final class TranscriptBuilder { case "task.parent_merge_stopped": return note([.text("the merge into the parent task didn't start")], .error) + case "task.title": + return note([.text("\(ev.actor) set the task title to \"\(jsString(p["title"]))\"")]) + case "task.merge_title": return note([.text("\(ev.actor) set the merge commit title to \"\(jsString(p["merge_title"]))\"")])