Skip to content

feat(tui): steer messages into the ongoing stream, add a /settings send-mode switch#3553

Merged
dgageot merged 3 commits into
mainfrom
feat/tui-steer-while-streaming
Jul 9, 2026
Merged

feat(tui): steer messages into the ongoing stream, add a /settings send-mode switch#3553
dgageot merged 3 commits into
mainfrom
feat/tui-steer-while-streaming

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Jul 9, 2026

Copy link
Copy Markdown
Member

Closes #3547.

While the agent is working, a plain send in the full TUI now attaches the message to the ongoing stream via the runtime steer queue (the behavior the lean TUI already has). The previous end-of-turn behavior remains available as a persisted setting: the /custom command becomes /settings, a tabbed dialog whose Behavior tab switches busy sends between Steer (default) and Queue.

Issue expectations

Issue expectation Implementation
"Send a new message should attach to the ongoing stream" Plain Enter while working resolves the input and enqueues it on Runtime.Steer; the loop drains it mid-turn and emits UserMessageEvent, so the transcript shows the message where the agent saw it
"A new shortcut like ctrl + q to add messages to queue" Implemented as a saved setting instead of a shortcut: the Behavior tab of /settings holds a Steer/Queue radio switch, persisted as settings.busy_send_mode in the user config and applied to every tab

The /settings dialog

/custom (layout dialog) is renamed to /settings and gains tabs:

Tab Content
Visuals Previous /custom content: live schematic preview, sidebar position, section spacing, section visibility
Behavior While agent is working: Steer (attach mid-turn, default) or Queue (hold until the turn ends)

Actual render of the Behavior tab:

╭──────────────────────────────────────────────────────────╮
│                                                          │
│                         Settings                         │
│  ──────────────────────────────────────────────────────  │
│                                                          │
│                   Visuals    Behavior                    │
│                                                          │
│  While agent is working                                  │
│                                                          │
│  › ● Steer   send to the working agent mid-turn          │
│    ○ Queue   hold until the current turn ends            │
│                                                          │
│  ↑/↓ navigate  ←/→ change  tab switch tab  enter apply   │
│                        esc cancel                        │
│                                                          │
╰──────────────────────────────────────────────────────────╯
  • Enter persists everything (Settings updated toast); Esc cancels and rolls back the layout preview. The send mode never previews, so it needs no rollback.
  • With --sidebar=false the dialog opens with only the Behavior tab (previously /custom was rejected entirely). Lean mode still drops the open message: it has no overlay support, an open dialog would invisibly capture keystrokes.
  • Defaults are not written out: busy_send_mode is cleared when set back to steer, keeping the config file minimal.

Flow

Enter while busy
  └─ chatPage.handleSendMsg
       ├─ Queue flag / queue send mode / fork-mode skill / no app → local queue
       └─ otherwise → steerMessage (tea.Cmd goroutine)
            ├─ ResolveInput (skill/agent command expansion off the UI loop)
            ├─ App.SteerMessage → Runtime.Steer (attachments become MultiContent)
            ├─ ok → toast; bubble appears when the runtime drains the message
            └─ rejected → steerFailedMsg → re-dispatched with Queue=true (never dropped)

Notes

  • Fork-mode skills always queue: they spawn their own stream, which cannot attach to the running one.
  • No optimistic transcript bubble: the user message renders when the runtime drains it and emits UserMessageEvent, so transcript order matches what the model actually saw.
  • No ctrl+q binding and no new editor_queue action: the key stays free for user remaps and the docs keep their previous examples. SendMsg.Queue remains as an internal flag for the steer-rejection fallback.
  • App.Run's attachment processing was extracted to buildUserMultiContent and reused by App.SteerMessage; no behavior change.
  • /custom shipped in v1.102.0; no alias is kept for it. If a transition alias seems safer, it can be added.

Testing

Layer Coverage
Unit steer on busy send, queue send mode routing (incl. mid-session switch), steer rejection fallback, fork-skill queueing, nil-app fallback, dialog tab switching, radio toggling, apply/cancel semantics, config round-trip (defaults cleared)
E2E TestChat_SteerWhileStreaming and TestChat_QueueSendModeWhileStreaming drive the real TUI through tuitest against a VCR proxy in simulated-stream mode (250ms per SSE chunk); the queue test flips the switch through the actual /settings dialog and asserts busy_send_mode: queue lands in the config file
Existing suites pkg/tui/..., pkg/app, pkg/runtime, pkg/userconfig, full e2e/tui; golangci-lint reports 0 issues

Sayt-0 added 2 commits July 9, 2026 13:20
While the agent is working, a plain send now attaches the message to
the ongoing stream through the runtime steer queue, matching the lean
TUI. The agent picks it up mid-turn without breaking the stream, and
the transcript shows the message at the point the agent actually saw
it.

A new ctrl+q binding (action editor_queue, remappable) submits the
message to the local queue instead, preserving the previous
end-of-turn behavior. Fork-mode skill commands always queue since they
spawn their own stream. A rejected steer (full queue, stream just
stopped) falls back to the queue path so the message is never dropped.

Issue #3547
Drive the real TUI through the tuitest harness against a replaying VCR
proxy in simulated-stream mode (250ms per SSE chunk, one character per
chunk) so the tests get a deterministic mid-stream window. The steering
scenario submits a follow-up while the first answer streams and asserts
the steering toast, the injected user bubble, and the follow-up answer
within the same stream. The ctrl+q scenario asserts the queue toast and
end-of-turn dispatch.

The follow-up draft is pasted as a single tea.PasteMsg because per-key
sendSync roundtrips are slow enough under -race to consume the whole
streaming window.
@Sayt-0 Sayt-0 requested a review from a team as a code owner July 9, 2026 11:21
@aheritier aheritier added area/docs Documentation changes area/tui For features/issues/fixes related to the TUI kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Jul 9, 2026
… send mode

- rename the layout dialog command to /settings with Visuals and Behavior tabs
- the Behavior tab switches what busy sends do: steer (default) or queue,
  persisted as settings.busy_send_mode in the user config
- drop the ctrl+q editor_queue shortcut in favor of the saved switch
- open /settings with only the Behavior tab when the sidebar is hidden
@Sayt-0 Sayt-0 changed the title feat(tui): steer messages into the ongoing stream while the agent is working feat(tui): steer messages into the ongoing stream, add a /settings send-mode switch Jul 9, 2026
@dgageot dgageot merged commit 22236ea into main Jul 9, 2026
16 checks passed
@dgageot dgageot deleted the feat/tui-steer-while-streaming branch July 9, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation changes area/tui For features/issues/fixes related to the TUI kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to send intermediate chat messsage like copilot cli

3 participants