diff --git a/docs/operations.md b/docs/operations.md index 447c9b3..41ec9ad 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -281,7 +281,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. On the Project tab, ↑ and ↓ select the previous or next task in its task list (the first or last when none is selected), staying on the Project tab; the tab takes keyboard focus when it appears unless a text field has it. With nothing typed, the last rows are one "New task in …" row per project, the right pane's project first, then the rest by most recently used (the latest visit to any of the project's tasks, ties in the server's order); each creates an untitled task with no session in its project and opens it with its composer focused. ⌘N (File ▸ New Task) creates one in the right pane's project (the first project when there is none) without the switcher. Typing filters tasks and projects and replaces each of those rows 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"). +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. On the Project tab, ↑ and ↓ select the previous or next task in its task list (the first or last when none is selected), staying on the Project tab, and Enter opens the selected task on the Transcript tab, as clicking its row does; the tab takes keyboard focus when it appears unless a text field has it. With nothing typed, the last rows are one "New task in …" row per project, the right pane's project first, then the rest by most recently used (the latest visit to any of the project's tasks, ties in the server's order); each creates an untitled task with no session in its project and opens it with its composer focused. ⌘N (File ▸ New Task) creates one in the right pane's project (the first project when there is none) without the switcher. Typing filters tasks and projects and replaces each of those rows 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. diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index 22f0ee6..986edb0 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -39,6 +39,13 @@ struct ProjectView: View { step(press.key == .downArrow ? 1 : -1) return .handled } + .onKeyPress(.return) { + guard let id = store.selectedTaskId, store.focusedProject() != nil else { + return .ignored + } + store.focusTask(id) + return .handled + } .onAppear { if !(overlays.window?.firstResponder is NSTextView) { keys = true