diff --git a/AGENTS.md b/AGENTS.md index a44da74f7..501d49362 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ This repository is used with [Pane](https://runpane.com). Drive it with the CLI 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Open later agents with `runpane panels create --pane ` (MCP: `panels_create` or `agents_start` with `pane`), not a new Pane. -When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question ""`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets. +When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. Claude's own card (AskUserQuestion) is only for a quick question in the chat the person is typing in, about their latest message, that steers only this turn; everything else is an approval card, and Codex and other agents always use one. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question ""`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets. CLI: `npm i -g runpane` (or `npx --yes runpane@latest`), then `runpane doctor --json`. Full command reference: `runpane agent-context --json`. diff --git a/contracts/runpane/contract.json b/contracts/runpane/contract.json index 629086214..b6272aaa4 100644 --- a/contracts/runpane/contract.json +++ b/contracts/runpane/contract.json @@ -2546,7 +2546,7 @@ { "name": "--context", "value": "", - "description": "notify: background shown under Details." + "description": "notify: the card's subtitle: one or two sentences on why it matters (up to 240 characters)." }, { "name": "--choices", @@ -4138,7 +4138,7 @@ "Options:", " --ui options, approve, ask, or info", " --title The question or update in one line, about 80 characters", - " --context Why it matters, shown under Details", + " --context Subtitle: why it matters, one or two sentences", " --choices options: 1 to 9 of [{\"label\":\"...\",\"detail\":\"...\",\"recommended\":true}]", " --questions options, instead of --choices: 2 to 4 of [{\"title\":\"...\",\"choices\":[...]}], one pick each", " --detail approve: the command or SQL to approve", @@ -4707,7 +4707,7 @@ "Options:", " --ui options, approve, or ask", " --title The question in one line", - " --context Background shown under Details", + " --context Subtitle: why it matters, one or two sentences", " --choices options: 1 to 9 of [{\"label\":\"...\",\"detail\":\"...\",\"recommended\":true}]", " --suggestions ask: suggested answers, such as [\"main\",\"release\"]", " --links Up to 6 related files or pages, such as [{\"label\":\"Notes\",\"path\":\"docs/RELEASE.md\"}]", @@ -6052,7 +6052,7 @@ "Options:", " --ui options, approve, ask, or info", " --title The question or update in one line, about 80 characters", - " --context Why it matters, shown under Details", + " --context Subtitle: why it matters, one or two sentences", " --choices options: 1 to 9 of [{\"label\":\"...\",\"detail\":\"...\",\"recommended\":true}]", " --questions options, instead of --choices: 2 to 4 of [{\"title\":\"...\",\"choices\":[...]}], one pick each", " --detail approve: the command or SQL to approve", @@ -6575,7 +6575,7 @@ "Options:", " --ui options, approve, or ask", " --title The question in one line", - " --context Background shown under Details", + " --context Subtitle: why it matters, one or two sentences", " --choices options: 1 to 9 of [{\"label\":\"...\",\"detail\":\"...\",\"recommended\":true}]", " --suggestions ask: suggested answers, such as [\"main\",\"release\"]", " --links Up to 6 related files or pages, such as [{\"label\":\"Notes\",\"path\":\"docs/RELEASE.md\"}]", @@ -23045,7 +23045,7 @@ "name": "--context", "value": "", "required": false, - "description": "Why it matters, shown under Details." + "description": "The card's subtitle: one or two sentences on why it matters, up to 240 characters." }, { "name": "--choices", @@ -23547,7 +23547,7 @@ "name": "--context", "value": "", "required": false, - "description": "Background the person can open under Details." + "description": "The card's subtitle: one or two sentences on why it matters, up to 240 characters." }, { "name": "--choices", @@ -23621,7 +23621,7 @@ "", "1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Open later agents with `runpane panels create --pane ` (MCP: `panels_create` or `agents_start` with `pane`), not a new Pane.", "", - "When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question \"\"`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets.", + "When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. Claude's own card (AskUserQuestion) is only for a quick question in the chat the person is typing in, about their latest message, that steers only this turn; everything else is an approval card, and Codex and other agents always use one. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question \"\"`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets.", "", "CLI: `npm i -g runpane` (or `npx --yes runpane@latest`), then `runpane doctor --json`. Full command reference: `runpane agent-context --json`.", "", @@ -23629,11 +23629,12 @@ ], "notifyGuidance": { "rules": [ + "Which card: Claude's own card (AskUserQuestion) only when you are answering the person's latest message in the chat they are typing in, the answer only steers this turn, and the question needs no context, links or Summary. Everything else is an approval card (`notify`, or `runpane report` for a worker): a worker's question in a Session, a question a Session orchestrator relays, and anything that waits, blocks other work, is recorded as a decision or approves an ask-first action. Unsure? Use an approval card. Codex and other agents without a built-in card always do.", "In a Session, only its orchestrator asks the person. A worker in a Session (`PANE_SESSION_DIR` is set and `PANE_ORCHESTRATION_SESSION_ID` isn't, or your brief names an orchestrator or a parent status file) doesn't call `notify`: it reports blocked with `runpane report --state blocked --question \"\"`, and the answer reaches its tab as `answer: Q-n → (by you)`; `closed: Q-n (by you)` and `correction: Q-n → (by you)` lines are the person's word too. A Session orchestrator (`PANE_ORCHESTRATION_SESSION_ID` is set) posts each item that waits on the person once, as a background `runpane notify ... --json`, as its pane-orchestration guide says. Any other agent calls `notify`.", "Ask only what you can't find out yourself.", "Put related questions in one card.", "Send `info` rarely, only for what the person must know now.", - "Write the card as its Summary: a title of about 80 characters; context saying why it matters; each choice saying what it does, with one recommended; links to every file, page or approval it mentions.", + "Write the card as its Summary: a title of about 80 to 100 characters on one line; context as a subtitle of one or two sentences saying why it matters; 2 to 4 choices, each saying what it does, with exactly one recommended; links to every file, page or approval it mentions. Pane refuses a title over 160 characters or with a line break, and two recommended choices.", "A `reply:` line (or a `{reply, replyId}` result) is the person's typed reply: an answer or a question. If it answers, run `runpane notify --took answer`. If it asks, run `--took question`, find out, then `runpane notify --update --asked \"\" --what-changed \"\"` with any changed `--title`, `--context`, `--choices`, `--questions` or `--links` (a reworded choice adds `\"was\"`, its old label) instead of asking again. A `correction:` line fixes how it was taken. From a `{reply, replyId}` result, pass its replyId to --took and --update.", "For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`." ], diff --git a/docs/CHAT_TABS.md b/docs/CHAT_TABS.md index 5b261e26a..cb89e2bb5 100644 --- a/docs/CHAT_TABS.md +++ b/docs/CHAT_TABS.md @@ -185,7 +185,9 @@ says, and after running the chat tab suites against it. plan itself. - **Questions** (ACP form elicitation, such as Claude's AskUserQuestion) dock as a form. Pane declares `elicitation.form`; without it Claude turns - AskUserQuestion off. + AskUserQuestion off. Agents use it only for a quick question in this chat; + anything else goes to an approval card + ([PANE_NOTIFY.md](PANE_NOTIFY.md#what-an-agent-can-send), "Which card"). - The composer is the input over one row of controls: attach image, the permission mode chip, then one menu showing model and effort together ("Opus 5.5 · Medium"; a trailing "(recommended)" and an effort of "Default" diff --git a/docs/PANE_NOTIFY.md b/docs/PANE_NOTIFY.md index 04862b005..bd13b75d4 100644 --- a/docs/PANE_NOTIFY.md +++ b/docs/PANE_NOTIFY.md @@ -35,10 +35,28 @@ for another approval (its id or Q-n). Older Panes ignore them. In Approvals, clicking a chip opens it as a tab beside the card: a file in an editor tab, a page in a browser tab, another approval in place, with **Back**. -Pane builds each approval's **Summary** from these fields (D-225), so agents -write them to read on their own: a title of about 80 characters, `context` as -why it matters, each choice with what it does (`detail`) and one -`recommended`, and links to every reference: files, PRs and other approvals. +Pane builds each approval's **Summary** from these fields (D-225), unless the +asker linked its own `.html` Summary page (a worker's `--summary-file`, or +`summary` in its `needs_you`). Agents write a card to read on its own: a +one-line title of about 80 to 100 characters, `context` of one or two sentences +on why it matters (up to 240 characters), 2 to 4 choices each with what it does +(`detail`) and exactly one `recommended`, and links to every reference: files, +PRs and other approvals. Every way of asking goes through one check +(`main/src/services/notify/cardCheck.ts`): Pane refuses a title over 160 +characters or with a line break, and two recommended options in one question, +and returns a warning for each softer miss, which `runpane notify` and +`runpane report` print. An old paragraph card (the whole question in its title, +with no buttons) is first split by fixed rules into a title, options and +links (`cardTidy.ts`); the card reads "Tidied by Pane" and **Show as sent** +shows what the agent sent. + +**Which card.** Pane's approvals text (`main/src/services/approvalsRules.ts`, +"Asking the person in Pane") is the one rule every Pane text carries: Claude's +own card (AskUserQuestion) only for a quick question that answers the person's +latest message in the chat they are typing in and only steers that turn; +everything else, including any worker's question in a Session, is an approval +card. Codex and other agents without a built-in card always use an approval. +`main/src/services/approvalsRules.test.ts` fails when a copy drifts. When Approvals is off or Pane isn't running, the result is `{unavailable, next}` ("Approvals are off in Pane.") and the agent asks in its @@ -113,7 +131,7 @@ the first answer from the terminal, Approvals, the strip or the phone wins. **No**, and its alert carries the same two buttons. The question's sheet has every choice. - **Answered in the terminal**: Pane closes the question and lists it in - Answered as "Answered in the terminal". + Done today as "Answered in the terminal". - **The whole command, plan or tool input** is on the card, so Yes never approves something you couldn't read. One over 32,000 characters is too long to show: its question offers no choices and says to answer in the terminal. @@ -188,14 +206,14 @@ Codex panel also closes when that panel sits idle at its prompt for 10 seconds. shows an amber count of the approvals waiting from it. The row's count is the sum of every Session's count plus items not in a Session, across saved machines; in the one-machine sidebar its tooltip says how many of them wait on other machines. Beside - it, the queue has **Waiting** and **Answered** tabs and a **Group by** menu, + it, the queue has **Waiting** and **Done today** tabs and a **Group by** menu, remembered per device: **Most recent** (the default) lists items flat, newest first, each row naming its Session and machine; **By Session** and **By project** group them, each group placed by its newest item; **Sidebar order** groups by Session in the order the sidebar shows them (the All machines arrangement when that's on), following a drag there, with Sessions the sidebar doesn't show last, newest first. Each group is boxed under a header - with its name, machine mark and waiting count. Waiting sorts by when it was asked and Answered by when it was + with its name, machine mark and waiting count. Waiting sorts by when it was asked and Done today by when it was answered. A new item never moves the selection or the selected row. The list stays compact rows, each with a chip for "Reading your reply…", "Getting more context…" or "Updated · ready again". The selected item opens @@ -206,14 +224,21 @@ Codex panel also closes when that panel sits idle at its prompt for 10 seconds. standalone item shows the asking Pane's tabs, with **Open in Pane** (or **Open on ** for another machine's item). Below about 1,100px wide, the list stacks above the column. After a pick, the column shows "Sent: - …" with **Undo** for 3 seconds; the item then moves to Answered, which lists - the last 24 hours, updates (`ui info`) included. Inside Pane there is no + …" with **Undo** for 3 seconds; the item then moves to **Done today**, + which lists the last 24 hours, updates (`ui info`) included. Under Most + recent, once a typed reply is with an agent, Waiting splits into **Waiting + on you** and **With the agent**, each row naming who reads the reply. Inside Pane there is no floating strip and no Notify row in the sidebar. - **The approval column**: one tabbed column for an approval, the same on every surface. Its header pages through what's waiting (‹ n of N ›, dots, ← - and → while it has focus) and ✕ closes it. Its tabs are **Questions & - answers** (the options and one reply box), **Summary** (written by Pane from - the approval), each linked file (in the document viewer), URL (in the + and → while it has focus) and ✕ closes it. Its tabs are **Decide** (the + subtitle, each question with its why and options, recommended ones + pre-selected, the references and one reply box that grows up to half the + column), **Summary** (one HTML page: the asker's own, an `.html` passed as a + worker's `--summary-file` or `summary` in `needs_you`, else one Pane writes + from the card and the worker's status file; it renders with scripts and + network off, the same on the Mac, from another machine and on the phone), + each linked file (in the document viewer), URL (in the in-app browser) or other approval (in place, with Back), and the chat where it was asked, left out when you're already in it. In a **Session**, the column lays over the Session's right split view without resizing it, and ✕ @@ -226,10 +251,16 @@ Codex panel also closes when that panel sits idle at its prompt for 10 seconds. sidebar (Overview, Files, Changes) is never covered. The Session's title bar shows **Approvals n · n ready**, and a Session row's count in the sidebar opens the newest ready-again approval, or the first waiting one. A - Session's orchestrator chat docks light cards above its composer: title, - one line, a one-question approval's options as buttons, and **Open on the - right**. The card open in the column has a darker grey outline and an - **Open** label. In **Autopilot**, the column covers the Stack (✕ "Back to the + Session's orchestrator chat docks one compact card above its composer: + who asks, the title and subtitle across the card, and a bottom row with ‹ n + of m ›, the reference count, Summary and **Open**; its pager stays in step + with the column's. The chat's own AskUserQuestion isn't docked: the chat + already shows Claude's card, and Approvals and the phone keep the copy. + **Send** closes the column, and a status line in the chat names the agent + reading the reply ("okta-sso-account is reading your reply to Q-42"), then + how it was taken, with **Reopen**. "Taken as your answer" shows only on that + approval, its row and that line. The card open in the column has a darker + grey outline and an **open** label. In **Autopilot**, the column covers the Stack (✕ "Back to the Stack"), and Autopilot's decision cards gain **Open**. In a Session and in Autopilot, the chat tab is live too: an orchestrator's item shows its Session, and a worker's or a Pane's item shows the tab that asked. @@ -373,9 +404,9 @@ so older items simply have none. | State | Shows | | --- | --- | | Waiting, no reply | Waiting | -| Reply delivered, not yet taken | "Reading your reply…" ("Autopilot is reading your reply…") | +| Reply delivered, not yet taken | " is reading your reply…", naming the reader for every kind ("okta-sso-account", "The Doozy orchestrator", "Autopilot") | | Taken as an answer | "Taken as your answer", with **It was a question** | -| Taken as a question | "Taken as a question: asking " and its status ("Getting more context…"), with **It was my answer** | +| Taken as a question | "Taken as a question: is getting more context", with its status when the reader gave one, and **It was my answer** | | Updated | "Updated · ready again": your question quoted ("You asked"), What changed and by whom, choices marked NEW, REWORDED (the old label struck) or REMOVED, and a collapsed **Earlier version** | | No reader | "No agent can read replies right now. You can still pick an option." | | A chat tab's built-in question | "Sent to ": the typed reply is its answer, so there is no fix | @@ -401,7 +432,7 @@ An approval exists only while you can act on it and someone can take the answer | Pane restarts | Items whose asking tab still exists stay waiting. | | Unanswered for 3 days | Stale: moved to a collapsed **Stale** group at the bottom of Waiting. It never rings or pushes again. | | You close one | One action on any card, with an optional reason. Its asker's open call returns "The person closed this question.", or `closed: Q-n (by you)` is typed into its tab. **Reopen** puts it back. | -| You or Autopilot answered | Answered lists it for 24 hours. | +| You or Autopilot answered | Done today lists it for 24 hours. | The store keeps answered and closed items 7 days for the record and for orchestrators catching up on decisions (`runpane inbox`), then drops them. On the diff --git a/docs/RUNPANE_CLI_CONTRACT.md b/docs/RUNPANE_CLI_CONTRACT.md index a1b59a625..ecd2501f7 100644 --- a/docs/RUNPANE_CLI_CONTRACT.md +++ b/docs/RUNPANE_CLI_CONTRACT.md @@ -622,7 +622,7 @@ This repository is used with [Pane](https://runpane.com). Drive it with the CLI 1 feature = 1 worktree = 1 branch = 1 Pane; reviews, audits, fixes and QA for it are tabs in that Pane. Open later agents with `runpane panels create --pane ` (MCP: `panels_create` or `agents_start` with `pane`), not a new Pane. -When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question ""`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets. +When you need the person (a choice, an approval, a short answer), call the `notify` MCP tool (or `runpane notify`) instead of asking in the terminal. If it returns `waiting`, call it again with `resume`; if it returns `unavailable`, ask in the terminal. Claude's own card (AskUserQuestion) is only for a quick question in the chat the person is typing in, about their latest message, that steers only this turn; everything else is an approval card, and Codex and other agents always use one. A worker in a Pane Session (`PANE_SESSION_DIR` set, `PANE_ORCHESTRATION_SESSION_ID` unset) doesn't call `notify`: it reports blocked to its orchestrator with `runpane report --state blocked --question ""`. Its orchestrator relays that card with `runpane notify --relays ...`, the answer goes straight to the worker's tab, and the orchestrator records it with `runpane inbox ack --decision `. For an API key, token or password, call `secrets_request` (or `runpane secrets request`), never `ask`; if that returns `unavailable`, ask the person to save it in Pane under Settings > Secrets. CLI: `npm i -g runpane` (or `npx --yes runpane@latest`), then `runpane doctor --json`. Full command reference: `runpane agent-context --json`. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 022bcb1fe..916d6caac 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1423,20 +1423,19 @@ default), a Session has one voice to the person: its orchestrator. person") tell the orchestrator that only it asks the person; to keep a running list and keep the work moving; when the person asks about one thing while other work is open, to remind them and offer the next step; at - the end of a turn, to have an approval waiting or be done; and to ask - through Approvals with context, in the Summary's format (D-225): a title of - about 80 characters, why it matters, each option with what it does and the - recommended one first, and links to every reference, other approvals - included (`{label, approval}`); and to read a typed reply as an answer or a - question. These are instructions; Pane doesn't check turns. + the end of a turn, to have an approval waiting or be done. All three carry + Pane's approvals text, "Asking the person in Pane" + (`main/src/services/approvalsRules.ts`, also in the `pane` home skill): which + card to use, what a card holds and how to read a reply + ([PANE_NOTIFY.md](PANE_NOTIFY.md#what-an-agent-can-send)). These are + instructions; Pane doesn't check turns, only cards. - **The orchestrator relays and posts.** The same three carry the posting rule: keep every item that waits on the person (a worker's needs_you or blocked question, a plan or design approval, a PR ready to merge, a next step only they can take) posted to Approvals exactly once, titled with its Q-n and with the worker's files and PR as `--links`. Post each one with `runpane notify ... --json` as a background command (in Claude Code, Bash with `run_in_background`), not the MCP `notify` tool, - which returns `waiting` after a while, and not a blocking - `AskUserQuestion`. A worker's question already has a card, so the orchestrator + which returns `waiting` after a while, and never `AskUserQuestion`. A worker's question already has a card, so the orchestrator relays into it: `runpane notify --relays ...`, with the id from `runpane inbox`, gives that card its choices, context and links and rings it, and the call waits on it as on any notify. The answer goes straight to the worker's diff --git a/frontend/src/components/approvals/ApprovalColumn.test.tsx b/frontend/src/components/approvals/ApprovalColumn.test.tsx index 11090e004..9e3c9a004 100644 --- a/frontend/src/components/approvals/ApprovalColumn.test.tsx +++ b/frontend/src/components/approvals/ApprovalColumn.test.tsx @@ -38,26 +38,26 @@ function text(markup: string): string { } describe('ApprovalColumn', () => { - beforeEach(() => useApprovalColumnStore.setState({ open: {}, lastReplied: null })); + beforeEach(() => useApprovalColumnStore.setState({ open: {} })); it('waiting: the pager, tabs in order, both questions and one reply box', () => { const visible = text(show(q20, ['a', 'q20', 'b'], 'Back to doozy-overhaul')); expect(visible).toContain('2 of 3'); expect(visible).toContain('Back to doozy-overhaul'); - expect(visible).toMatch(/Questions & answers Summary PR #2480 doozy-overhaul's tab/); + expect(visible).toMatch(/Decide Summary PR #2480 doozy-overhaul's tab/); expect(visible).toContain('Question 1 of 2'); expect(visible).toContain('Question 2 of 2'); expect(visible).toContain('Recommended'); - expect(visible).toContain('Pick an option, or reply in your own words: an answer, or a question about it.'); + expect(visible).toContain('Recommended picks are pre-selected; change any before you send.'); }); it('Reading your reply…', () => { - expect(text(show({ ...q20, reply: { id: 'r', text: 'why?', at: 1, state: 'reading' } }))).toContain('Reading your reply…'); + expect(text(show({ ...q20, reply: { id: 'r', text: 'why?', at: 1, state: 'reading' } }))).toContain('doozy-overhaul is reading your reply…'); }); it('Taken as a question, with its fix, in amber', () => { const markup = show({ ...q20, reply: { id: 'r', text: 'why?', at: 1, state: 'question', reader: { kind: 'asker', name: 'doozy-overhaul' } } }); - expect(text(markup)).toContain('Taken as a question: asking doozy-overhaul. Getting more context…'); + expect(text(markup)).toContain('Taken as a question: doozy-overhaul is getting more context…'); expect(text(markup)).toContain('It was my answer'); expect(markup).toContain('data-tone="warning"'); }); diff --git a/frontend/src/components/approvals/ApprovalColumn.tsx b/frontend/src/components/approvals/ApprovalColumn.tsx index 9ca449476..cda58027c 100644 --- a/frontend/src/components/approvals/ApprovalColumn.tsx +++ b/frontend/src/components/approvals/ApprovalColumn.tsx @@ -8,18 +8,21 @@ import { leadingQn } from '../../../../shared/autopilotDecision'; import { useAllMachinesStore } from '../../stores/allMachinesStore'; import { useApprovalColumnStore, type ApprovalSurface } from '../../stores/approvalColumnStore'; import { AutopilotDocument, AutopilotWebPage } from '../autopilot/AutopilotDocument'; +import { isLightTheme, useTheme } from '../../contexts/ThemeContext'; +import { sentenceStart } from '../../../../shared/approvalStatus'; +import { readSummaryPage, type SummaryRead } from './summaryPage'; import { ItemCloseForm, NotifyCard } from '../notify/NotifyCard'; import { relayReadOnlyLine } from './approvalGroups'; import { relativeAge } from '../notify/notifyFormat'; import { useNow } from '../notify/useNow'; import { - askedBy, canSend, cardChip, replyBeingRead, cardSummary, cardTabs, dotTone, markedQuestions, nextAfterSend, previousVersion, questionsOf, replyLine, + askedBy, canSend, cardChip, replyBeingRead, cardTabs, dotTone, markedQuestions, previousVersion, questionsOf, recommendedPicks, replyLine, type CardChip, type CardQuestion, type CardTab, type ReplyLine, } from './approvalCard'; /** * The approval column (P3, D-243): one tabbed column, shared by a Session, Autopilot and the - * Approvals screen. Questions & answers first, with the pager and one reply box; then Summary, + * Approvals screen. Decide first, with the pager and one reply box; then Summary, * each reference, and the chat where it was asked. */ export interface ApprovalColumnProps { @@ -45,14 +48,12 @@ export interface ApprovalColumnProps { focused?: boolean; /** The items to read instead of the Notify store's (tests). */ items?: readonly NotifyItem[]; + /** After an accepted Send (P7, D-344): a chat's column closes; the Approvals screen stays on the approval. */ + onSent?: () => void; } const NO_PICKS: string[] = []; -/** "the Infra orchestrator" opens a sentence as "The Infra orchestrator". */ -function capitalized(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1); -} /** Below this width the header keeps the pager and ✕, and drops the group's name, the dots and the chip. */ const NARROW_PX = 520; @@ -84,7 +85,7 @@ const TONE_BOX = { info: 'bg-status-info/10', } satisfies Record; -export function ApprovalColumn({ surface, itemId, order, onSelect, groupLabel, closeLabel, onClose, inChat, renderOrigin, focused, items: given }: ApprovalColumnProps) { +export function ApprovalColumn({ surface, itemId, order, onSelect, groupLabel, closeLabel, onClose, inChat, renderOrigin, focused, items: given, onSent }: ApprovalColumnProps) { const stored = useNotifyStore((state) => state.snapshot?.items); const items = given ?? stored; const byId = useMemo(() => new Map((items ?? []).map((item) => [item.id, item])), [items]); @@ -133,9 +134,9 @@ export function ApprovalColumn({ surface, itemId, order, onSelect, groupLabel, c nested ? openNested(surface, itemId, undefined)} /> // A new version starts afresh: its options may have changed under the picks. - : + : setTab(surface, itemId, key)} /> )} - {tab?.kind === 'summary' && setTab(surface, itemId, key)} tabs={tabs} />} + {tab?.kind === 'summary' && } {tab?.kind === 'link' && openNested(surface, itemId, resolveApproval(items ?? [], id))} />} {tab?.kind === 'origin' && (renderOrigin?.(item) ??

Asked in {item.source.name}.

)} @@ -167,9 +168,9 @@ function Chip({ chip, testId }: { chip: NonNullable; testId?: string } const tone = chip.tone === 'info' ? 'bg-status-info/10 text-status-info-on-tint' : chip.tone === 'warning' ? 'bg-status-warning/10 text-status-warning-on-tint' : chip.tone === 'success' ? 'bg-status-success/10 text-status-success-on-tint' : 'bg-surface-tertiary text-text-secondary'; const Icon = chip.tone === 'info' ? Sparkles : chip.tone === 'warning' ? Loader2 : chip.tone === 'success' ? Check : Loader2; return ( - - - {chip.text} + + + {chip.text} ); } @@ -195,7 +196,7 @@ function ColumnHeader({ index, total, groupLabel, closeLabel, onClose, onStep, d ))} - {/* Narrow, the Questions & answers tab says it's ready again; the header keeps to the pager and ✕. */} + {/* Narrow, the Decide tab says it's ready again; the header keeps to the pager and ✕. */} {chip && chip.tone !== 'success' && !narrow && } {onClose && closeLabel && ( + + {showSent && ( +
+

{tidied.title}

+ {tidied.context &&

{tidied.context}

} +
+ )} + + )} + {item.context &&

{item.context}

} {item.revision && waiting && (
@@ -357,6 +378,24 @@ function QuestionsTab({ item, order, byId, onSelect }: { item: NotifyItem; order ))}
)} + {references.length > 0 && ( +
+ References + {references.map((reference) => { + const Icon = tabIcon(reference) ?? FileText; + return ( + + ); + })} +
+ )} + {before && earlier.length > 0 && (
{earlier.map((version) => ( @@ -388,7 +427,6 @@ function QuestionsTab({ item, order, byId, onSelect }: { item: NotifyItem; order {ended && line &&
} {!plain && !ended && (
- {replied && noteReplied(null)} named />} {line && } {item.state === 'sent' && (
@@ -398,13 +436,14 @@ function QuestionsTab({ item, order, byId, onSelect }: { item: NotifyItem; order )} {waiting && !item.agentPrompt && !item.optionsOnly && (