Public: anyone can read this; the crew can change it.

/talk takes a message while the agent is working: queue it next, or steer the current turn #8893

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`/talk` now keeps the composer open while the agent works. Send queues the message for after the turn, shown dimmed as "Queued" with a Cancel. Steer stops the turn and sends the message at once. The `keep_plan` tool wasn't available to me, so there is no plan on the issue.

shipped production is serving it deploy bbd5a0b

opened this

/talk now keeps the composer open while the agent works. Send queues the message for after the turn, shown dimmed as "Queued" with a Cancel. Steer stops the turn and sends the message at once. The keep_plan tool wasn't available to me, so there is no plan on the issue.

Tests pass for the new queue file, the conversation suites and the talk page. tsc, design-lint and gate.ts are clean. I ran only the tests covering this change, not the whole suite, which the forge's check runs on push.

  • Queue: while a turn runs, Send holds the message in memory. When the turn ends, the next one starts as its own turn, in order. At most 5 can wait, and a sixth is refused with 429 queue_full. Cancel puts the text back in the box.
  • Steer: stops the running turn, so the partial reply stays and a "Stopped." line follows it. The steering message goes ahead of anything already queued.
  • Stop: now a separate button, and it also clears the queue.
  • API: POST /api/v1/conversation/messages takes an optional during of queue or steer. Without it, callers like forge talk, ssh and MCP behave as before and the words are handed to the running turn. DELETE /api/v1/conversation/queue/:id cancels a queued message, and GET …/turns lists queued. Both are in the OpenAPI contract.
  • Page: the server draws queued messages on reload. The plain no-script form also queues.
  • Tests: test/conversation-queue.test.ts has the two you asked for, plus Stop clearing the queue. I added DOM tests for the buttons in test/talk-page.test.ts and replaced the old "Send becomes Stop" test, since Send no longer turns into Stop.

Changed on purpose:

  • Script size cap: the talk page's script cap in test/talk-page.test.ts goes from 11.6 KB to 13.9 KB for this feature. It is now 13.76 KB.
  • Design doc: .talk-queued is named in docs/design/areas/talk.md.

Left out:

  • Restart: the queue lives in memory, so a forge restart drops waiting messages. A restart already cuts and resumes the running turn, and I didn't persist the queue on top of that.
  • Attachments: they are attached when the queued message actually starts, so a picture queued behind a running turn works but isn't checked until then.
  • Queued edits: a queued message can't be edited, only cancelled and retyped.

To try it, send a message, then send another while the agent answers. It shows as queued. Cancel returns it to the box, and Steer interrupts the turn.

Closes #8819

commented

/talk now keeps the composer open while the agent works. Send queues the message for after the turn, shown dimmed as "Queued" with a Cancel. Steer stops the turn and sends the message at once. The keep_plan tool wasn't available to me, so there is no plan on the issue.

Tests pass for the new queue file, the conversation suites and the talk page. tsc, design-lint and gate.ts are clean. I ran only the tests covering this change, not the whole suite, which the forge's check runs on push.

  • Queue: while a turn runs, Send holds the message in memory. When the turn ends, the next one starts as its own turn, in order. At most 5 can wait, and a sixth is refused with 429 queue_full. Cancel puts the text back in the box.
  • Steer: stops the running turn, so the partial reply stays and a "Stopped." line follows it. The steering message goes ahead of anything already queued.
  • Stop: now a separate button, and it also clears the queue.
  • API: POST /api/v1/conversation/messages takes an optional during of queue or steer. Without it, callers like forge talk, ssh and MCP behave as before and the words are handed to the running turn. DELETE /api/v1/conversation/queue/:id cancels a queued message, and GET …/turns lists queued. Both are in the OpenAPI contract.
  • Page: the server draws queued messages on reload. The plain no-script form also queues.
  • Tests: test/conversation-queue.test.ts has the two you asked for, plus Stop clearing the queue. I added DOM tests for the buttons in test/talk-page.test.ts and replaced the old "Send becomes Stop" test, since Send no longer turns into Stop.

Changed on purpose:

  • Script size cap: the talk page's script cap in test/talk-page.test.ts goes from 11.6 KB to 13.9 KB for this feature. It is now 13.76 KB.
  • Design doc: .talk-queued is named in docs/design/areas/talk.md.

Left out:

  • Restart: the queue lives in memory, so a forge restart drops waiting messages. A restart already cuts and resumes the running turn, and I didn't persist the queue on top of that.
  • Attachments: they are attached when the queued message actually starts, so a picture queued behind a running turn works but isn't checked until then.
  • Queued edits: a queued message can't be edited, only cancelled and retyped.

To try it, send a message, then send another while the agent answers. It shows as queued. Cancel returns it to the box, and Steer interrupts the turn.

Changes requested — before the latest push

Queue/steer logic in conversation.ts reads sound (single-threaded drain, per-user queue, cancel scoped to the caller's conversation, escaping fine). Changes needed, all in one round:

  1. HIGH, both doors: nothing in src/tui/ gains queue/steer/cancel, and there is no docs/features/talk.md update. What a person does on the page must work in the terminal too; add the screen path and the how-to.
  2. HIGH, no API tests: nothing covers during validation (invalid_during 422), queue_full 429 on the 6th, steer via POST, DELETE /conversation/queue/:id (404 for another user's id, cross-origin 403, read-only token 403, unauthenticated 401), or the queued field of GET turns. Add them; each must fail without the change.
  3. HIGH, design/JS-off: the server-rendered queued item (src/web/pages/talk.ts) carries a Cancel <button type=button> that does nothing without JavaScript, and mutations must be a button in a post form. Make Cancel a post form to a route that works with JS off (or do not draw it without script).
  4. MEDIUM: Cancel in talk_client.ts overwrites whatever the person has since typed in the box (box.value = ...); append or only restore when empty. Also any non-ok response (403, 500) is treated as 'already gone' and removes the item; only 404 should.
  5. MEDIUM: queued messages keep their pending attachment ids un-claimed until they start, so the same file can be sent twice or deleted meanwhile; reserve or re-validate at queue time. Queued items also show only in the sender's tab, not other tabs/terminal viewers.
  6. LOW: stray formatting at conversation.ts stoppers.set(...); const signal (two statements on one line) - split it.
  7. LOW: the new tests poll with sleeps up to 4 s and a sh/sed stand-in; keep them quick and non-flaky. The 11.6 KB to 13.9 KB script cap raise is large; trim the client where possible.
commented

I've pushed fixes for all seven review items to the same branch. tsc, design-lint, gate.ts and the 504 tests for talk, conversation and the API contract pass here. The forge's full check runs on the push.

  1. Terminal: ssh <forge> talk /queue <words>, /steer <words>, /queued and /unqueue <id> now exist, and they work with --attach. docs/features/talk.md has a new "Queue and Steer while an answer runs" section covering web, API and terminal, and the queue row in its feature table now points at #8819.
  2. API tests: the new test/conversation-queue-api.test.ts covers these:

- during: 422 invalid_during for an unknown value or a non-string. - Queue limit: 429 queue_full on the sixth queued message. - Steer: steer by POST returns interrupted. - Cancel: DELETE of a queue id gives 401 signed out, 403 cross-origin, 403 for a read-only token, 404 for another user's id, and 200 then 404 for the owner. - Listing: the queued field of GET …/turns. - Files: the file-held rules. - Terminal and stream: the ssh commands and the stream's queued frame.

  1. Cancel without JavaScript: Cancel is now a post form to POST /talk/queue/:id/cancel. It checks the session and the same origin, and redirects with a flash message when JavaScript is off. The script only does it in place. The route has a test.
  2. Cancel in the browser: it now appends to whatever is in the box instead of overwriting it. Only a 404 removes the item; a 403 or 500 keeps it and shows an error. Both have DOM tests.
  3. Attachments and other tabs:

- A file in a queued message is held. It can't be removed (409), and a second message that names it is refused with file_taken. A file that is gone by the time the message runs is left out. - The stream sends a queued frame whenever the list changes, so every open tab sees the queue. The terminal sees it with talk /queued.

  1. The joined statement at conversation.ts:454 is split.
  2. I trimmed the script cap raise from 13.9 KB to 13.4 KB. The script is 13.3 KB and was 11.6 KB before this feature. I didn't squeeze it further.

Not changed: the new tests still poll with short sleeps (10–20 ms steps, giving up after a couple of seconds only when something has failed). They use the sh stand-in already used in this repo's other conversation tests. The API tests start turns through say() so the 12-a-minute limit doesn't affect them.

commented

The check failed because no commit message had a "Try:" line. I added an empty commit with one and pushed it. The code is unchanged, and I didn't re-run the gate, tsc or the tests here.

The line reads: "open /talk on the preview, send a message, and while the forge answers send another (it shows Queued with Cancel), then press Steer to stop the answer and send at once."

How to try it: on the preview of #8893, open /talk, send a message, and send another while the forge answers. The second shows as Queued with a Cancel. Press Steer to stop the answer and send the message at once.

Behaviour:

  • Queue: Send queues a message behind the running turn, up to 5. Each runs as its own turn, in order.
  • Steer: stops the turn, keeps what it had said and a "Stopped." line, then sends the message ahead of anything queued.
  • Stop: also clears the queue.
  • Restart: a forge restart drops anything waiting.

Doors:

  • API: POST /api/v1/conversation/messages takes during set to queue or steer. DELETE /api/v1/conversation/queue/:id takes a queued message back, and GET …/turns lists queued.
  • Page: Cancel is a post form to POST /talk/queue/:id/cancel, so it works without JavaScript. Every open tab sees the queue over the stream.
  • Terminal: ssh <forge> talk /queue, /steer, /queued and /unqueue <id>.

A file attached to a queued message is held, so it can't be removed or sent twice. The talk page's script cap is raised from 11.6 KB to 13.4 KB.

Approved

Every earlier ask is met: terminal door (/queue, /steer, /queued, /unqueue) with docs/features/talk.md (README untouched); API tests for during, queue_full, DELETE auth/origin/scope/ownership; Cancel as a post form that works without JS; Cancel appends to the box and only a 404 removes the item; file held in a queued message; split statement. Reads clean: queued text goes through html``, cancel is owner-scoped, same-origin and scope checked, queue capped at 5, Stop clears it. Non-blocking: the queue is memory-only, so a restart drops waiting messages (stated in the docs).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs