Skip to content

Approvals: one way to ask, worker buttons, a Summary page and one compact chat card - #1311

Merged
parsakhaz merged 17 commits into
mainfrom
approvals-system
Nov 12, 2026
Merged

parsakhaz merged 17 commits into
mainfrom
approvals-system

Conversation

@tbrownio

@tbrownio tbrownio commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Agents in Pane now ask through one set of rules. A worker's card gets real buttons, every card gets a subtitle and an HTML Summary, and a Session chat shows one compact approval card at a time. After you send, a status line says which agent is reading your reply.

Visual Overview

Before: a worker's 640-character question becomes one paragraph card with no buttons, two full cards stack in the chat beside Claude's own card, and "Reading your reply…" names nobody. After: notify, report and MCP notify all pass one card check that refuses two clear mistakes, tidies old paragraphs and warns on the rest; the card has a subtitle, per-question buttons and a Summary page; the chat shows one compact card with a pager, and Send closes the column while a status line names the reader.

PR artifact: companion page (Grain)

Plan: the approvals-system cover sheet r2, approved as D-348 (design reference: mock-ups v3). Companion PR in Agent Farm: greenfield-inc/agent-farm#211.

Labels used below. P1–P8 are the plan's packages and J1–J11 its QA journeys. D-n and Q-n are numbered decisions and questions from the plan's decision log; Q-n also prefixes an approval's title (for example "Q-42: …"). The dock is the strip above a Session chat's message box where approval cards show. Autopilot is Pane's always-on agent above all Sessions; it can read a reply when nobody else can.

Why this change exists

Approvals were hard to answer. Take Q-42, a real approval used as the example throughout this PR: a worker agent sent two questions, their recommendations and two absolute file paths as one 640-character --question. Pane showed it as one paragraph with no buttons and no subtitle, and its Summary only restated the card. In a Session chat, two full cards stacked above the composer. When the orchestrator used Claude's own question card (AskUserQuestion), the question showed twice in that chat: Claude's card in the conversation and a copy as an approval card in the dock. After you sent a reply, the line said "Reading your reply…" without saying who was reading it, and a "last replied" strip followed you onto unrelated approvals. The rules that tell agents how to ask were copied by hand into five places, and they disagreed.

The outcome the plan asks for: one rule for which card to use, cards Pane checks and can tidy, a Summary you can read, one compact card in the chat, and a clear line saying who has your reply. Non-goals: no database migration (new fields live in the approval's existing JSON), no change to who reads a reply (D-258), no model-written Summary, and no redesign of the phone layout.

Concepts and approach

  • Approval card. What runpane notify, runpane report --state blocked and the MCP notify tool post. It has a title, a context (now shown as the subtitle), choices or several questions (each with its own choices), and links.
  • Claude's own card. The built-in AskUserQuestion. The new which-card rule keeps it for quick questions in the chat the person is typing in, and sends everything else to an approval. Workers and relaying orchestrators never use it.
  • Status file. A worker's JSON file whose needs_you block holds what it needs from the person (questions, options with (recommended), files and a top-level why). Passed as --summary-file, it becomes the card.
  • Reader. Whoever reads your typed reply: the asking tab, the orchestrator that relayed the card, or Autopilot.

The approach puts one gate (admitCard) where every way of asking meets, builds the Summary in one place (notify:summary-page), and shares the reply-state wording between the desktop and the phone (shared/approvalStatus.ts).

Behavior and diff walkthrough

P3 · Worker cards with buttons, and one card check

main/src/services/notify/workerContext.ts now turns two to four needs_you questions that all have options into sub-questions, each with its own buttons. Each question's why shows under it, and the top-level why becomes the subtitle. Before this, they were listed as text. The same applies to a needs_you block sent as runpane notify --ui ask --context. A card that can't take buttons there (another ui, or an --update) lists the questions in its subtitle instead.

cardCheck.ts holds the contract and cardAdmit.ts runs it on runpane notify (and so the MCP tool, which shells out to the CLI), on --relays, on --update's new title or questions, and on runpane report --state blocked, before the report is recorded:

Card Result
Title over 160 characters, or with a line break Refused: ERR_NOTIFY_TITLE, with a next step
Two recommended options in one question Refused: ERR_NOTIFY_RECOMMENDED
Title over 100, no subtitle, subtitle over 240, no recommended option, more than 4 options, options with no consequence, unlinked absolute paths Posted, with a Warning: … line each (warnings in --json)

Both CLIs (npm and Python) print the warnings, and their result schemas accept the field. Info and secret cards, and Claude's own bridged questions, aren't checked.

P8 · Tidying old paragraph cards

cardTidy.ts runs before the check, only on a card with no buttons whose title breaks the contract. It applies fixed rules, never a model: the first sentence becomes the title; 1) … 2) … become questions; Recommended: yes|no after a yes-or-no question and A or B (recommended) become options; Files: lists and absolute paths become links. It only applies when every question splits into options. Otherwise the card stays as sent, and the check refuses it. The original title and context are kept on the item as asSent, so the card reads "Tidied by Pane" and Show as sent shows them exactly. Q-42's real text becomes this:

Title:  Q-42: Okta sign-out rework (option A holds; cover sheet ready for approval)
Q1:     With Require SSO off, should a person Okta removes keep a password session for their other orgs?
        [Yes ★ — they keep it and lose the account's orgs; …]  [No]
Q2:     Should Require SSO end password sessions for everyone on the domain or only the account's members?
        [Everyone on the domain ★ — same as sign-in]  [Only the account's members]
Links:  decisions.html, cover-sheet.html

cardTidy.test.ts checks that no word appears that wasn't sent, apart from the "No" a yes-or-no question offers.

P4 · The Summary is one HTML page

A new daemon command, notify:summary-page <id> [{theme}], returns the asker's own page or Pane's template. The asker's page is an .html passed as a worker's --summary-file, or summary in needs_you. Pane's template (summaryPage.ts) holds the title and state, who asked, the subtitle, each option with its consequence (the recommended one starred), where it stands (step and checks from the worker's status file) and references with real paths. Either way, the HTML gets a Content-Security-Policy that allows no scripts and no network. The desktop shows it in an <iframe sandbox="">, read through machines:read so another machine's approval works the same way. The phone shows it in a WebView with JavaScript off and navigation refused. If the asker's page is gone, the template shows instead and says so. The page is read or built each time it's opened; no copy is saved.

P5, P6 · One compact card in the chat

SessionApprovalCards.tsx now shows one card. Its header has the Q-n, who asks, how many questions and the age. The title and subtitle (two lines each) span the card. The bottom row has ‹ n of m ›, dots, "N references · Summary" and Open. The pager follows the column's open item, and ‹ › move the column too when it's open. The card open on the right keeps D-343's grey outline and Open label from #1302. The dock now hides a question the chat already shows as Claude's own card: an approval copied ("bridged") from the same chat panel, marked by replyIsAnswer, optionsOnly or agentPrompt. Approvals and the phone keep that copy, and whichever place answers first (the chat or the copy) settles the question; the other closes. It also leaves out approvals whose reply is being read, until they come back ready again.

In the column (ApprovalColumn.tsx), the first tab is now Decide. It shows the subtitle, each question's why, the "Tidied by Pane" banner, reference chips and a "Read the Summary" link. Recommended picks start selected. The reply box grows with what you type up to half the column, then scrolls (it was fixed at two rows).

P7 · After Send

  • Send: in a Session, Send closes the column instead of jumping to the next approval.
  • Status line: a line above the compact card follows the Session's most recent reply, with Reopen. It reads, in turn:
    • "okta-sso-account is reading your reply to Q-42…"
    • "… is getting more context"
    • "Q-42 · Updated · ready again"
    • "Taken as your answer…" with its fix
  • One source of wording: these strings come from shared/approvalStatus.ts, which names the reader for every kind of reader. The phone uses the same module.
  • Reply strip: the app-wide "last replied" strip is gone, so "Taken as your answer" shows only on its approval, its row and the status line.
  • Approvals list: under Most recent, Waiting splits into Waiting on you and With the agent once something is with an agent, and Answered is now Done today.

Phone (native)

Rows and the approval screen show the subtitle. Readers are named through approvalStatus. The groups are Waiting on you, With the agent and Done today, the last counted from local midnight. Each question shows its why, and recommended picks are pre-selected. The tidied banner has Show as sent, and a Summary screen calls notify:summary-page. The Summary button shows only for this machine's own approvals.

P1 · One rules text

main/src/services/approvalsRules.ts holds the which-card rule, the card contract and the reply rule. The generated pane-orchestrator skill (all four copies), the bundled pane-orchestration skill (between approvals-rules markers), each Session's AGENTS.md and the pane home skill (now version 4) carry it word for word. The bundled orchestrate-sessions skill gains references/relay.md, covering clarify handling and "a check, not a pipe". approvalsRules.test.ts fails if any copy drifts, or if any of these texts (or the notify guidance and the repo agent-context block in contract.json) tells an agent to use AskUserQuestion. The generated contract files, this repo's AGENTS.md block, docs/PANE_NOTIFY.md, docs/SESSIONS.md and docs/CHAT_TABS.md are updated to match.

Assumptions made while building

  • Tidying versus refusal: tidying runs only when it yields buttons. A plain 200-character title, or a title with a line break, is refused, which keeps P3's refusals and P8's tidying (J2 and J3) consistent.
  • Warnings arrive late on a waiting card: a waiting runpane notify gets its warnings with the answer, because the call returns then. report and --returnAfterMs calls get them at once.
  • Agent Farm profiles: they can't load a different skill per host, so in a Pane Session the split between Pane's and Agent Farm's orchestrate-sessions is made by instruction text (see feat: boot hosted cloud VMs into headless daemon #211).

Validation and limitations

Run on 3d0dde0db, which merges origin/main up to #1303 and #1300 into this branch:

  • pnpm check:fast: typecheck, lint, renderer test types, frontend, native and glasses unit tests pass. Markdown links weren't checked locally (no lychee); CI's Markdown Links job passes.
  • Main unit tests: 4,388 pass. In the full local run, three tests failed outside this PR's code:
    • Two skillCacheManager.test.ts launcher tests fail because this Mac has a global runpane on PATH.
    • remoteTerminalInput.test.ts ("Unexpected fixture request: HEAD /") failed 1 of 3 runs alone. It is a flake in a file this PR doesn't touch, and CI retries it once.
  • Fixes added on this branch: the first three came from a fresh agent's correctness review of the full diff; the last from Windows CI.
    • A two-question needs_you sent as --context lost its questions. It now gets buttons, or a written list when the card can't take buttons. Tested in notify.test.ts.
    • Relaying a worker question reported more than 15 minutes ago returned waiting at once on every call. This made a tight resume loop for clients that pass returnAfterMs, such as Cursor. Each relay call now waits a full round. Tested in notifyQueue.lifecycle.test.ts.
    • A card re-asked well formed (same tab, same Q-n) kept the old "Tidied by Pane" banner. A one-line fix, no new test.
    • summaryPage.test.ts expected POSIX paths and failed on Windows CI; it now resolves them per platform.
  • App QA (isolated dev Pane: temporary PANE_DIR, HOME and browser profile):
    • A 200-character title is refused with ERR_NOTIFY_TITLE and a next step.
    • A Q-42-style paragraph (same questions, with this repo's AGENTS.md as its file) is tidied into two questions with recommended picks pre-selected, a "Tidied by Pane" banner, a working Show as sent, and AGENTS.md as a reference.
    • A two-question card shows its subtitle, each question's why and option details.
    • The column's Summary tab (beside Decide) shows Pane's sandboxed template.
    • Sending returns both picks to the waiting runpane notify call, together with the card check's warnings for that card.
    • Answered cards move to Done today.
  • CI: green on this head (required checks and CI result).
Tidied paragraph card Two questions with subtitle
Tidied Two questions
Summary page Done today
Summary Done today

Not verified in the real app:

  • The Session chat's compact card and status line. tests/approvals-system.spec.ts covers them in CI's Functional Smoke.
  • Phone rendering, including the WebView's scripts-off and no-navigation behaviour.
  • The Summary viewed from a second machine, and answering on the phone.

Follow-ups from the review, not fixed here:

  • Pre-selected recommended picks are sent along with a typed reply. If the person only types a question, the agent still receives the recommended pick and can take it as their answer.
  • cardTidy's "A or B (recommended)" split takes as many words before "or" as follow it, so "ship the new version or wait" gives the buttons Version and Wait.
  • The status line keeps its dismissals in component state, so an old "Taken as your answer" line can come back after a remount.

Validation level: full, with the real-app gaps listed above. Review was a fresh-agent correctness review; no human review.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 3d0dde0.

Tyler Brown and others added 8 commits November 11, 2026 18:36
…le, a relative needs_you summary is found from the worker's folder, the chat line says Updated · ready again, list rows keep their titles, tidy banner and help copy
…g it; name a missing Summary page where the worker meant it
…its questions, a relay of an old worker question waits a full round, a re-asked card drops a stale Tidied banner
# Conflicts:
#	packages/runpane-py/src/runpane/generated_contract.py
@parsakhaz
parsakhaz marked this pull request as ready for review November 12, 2026 05:17
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T05:21:21.289521Z 3d0dde0 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d0dde0db4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const choices: NotifyChoice[] = onLeft
? [{ ...recommended, label: capitalized(a) }, { label: capitalized(right) }]
: [{ label: capitalized(a) }, { ...recommended, label: capitalized(right) }];
return { title: `${plain.trim()}?`, choices };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve text following a question when tidying its options

When a paragraph uses a parenthetical recommendation, this return discards everything after the first question mark. For example, Release approval.\nDeploy to production (recommended) or wait? Only deploy after the database backup is verified. becomes a card whose context is only “Deploy to production or wait?”, with the deployment option preselected. The safety condition survives only behind “Show as sent” and is also absent from the generated Summary. Preserve the trailing text in the visible card, or refuse to tidy inputs containing text this pattern cannot account for.

Useful? React with 👍 / 👎.

Comment on lines +35 to +36
const head = /<head(\s[^>]*)?>/iu.exec(html);
if (head) return `${html.slice(0, head.index + head[0].length)}${meta}${html.slice(head.index + head[0].length)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Insert the Summary policy into actual HTML markup

For a worker-supplied Summary beginning with <!-- <head> -->, this regex matches the tag inside the comment and inserts the only CSP meta element inside that comment. The resulting document therefore has no effective Summary policy. Both viewers disable JavaScript, but that does not block image requests; the desktop renderer's inherited policy also explicitly allows HTTPS images. Consequently, opening such a Summary can contact arbitrary remote endpoints despite the promised network isolation. Insert the policy before untrusted markup in a guaranteed valid head, rather than locating it with this regex.

Useful? React with 👍 / 👎.

@parsakhaz
parsakhaz merged commit 355ca43 into main Nov 12, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants