Skip to content

fix(tui): honor display.busy_input_mode in TUI v2#17110

Merged
OutThisLife merged 4 commits into
mainfrom
bb/tui-busy-input-mode
Apr 28, 2026
Merged

fix(tui): honor display.busy_input_mode in TUI v2#17110
OutThisLife merged 4 commits into
mainfrom
bb/tui-busy-input-mode

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

display.busy_input_mode (interrupt | queue | steer) is honored by classic CLI and gateway adapters but the TUI v2 frontend hard-coded queue-on-busy in useSubmission.ts. Sending a message during a running turn always landed in the queue regardless of config.

This wires the value through the existing config-sync surface so the TUI honors the user's setting, with one intentional split: the TUI default is queue, while CLI / messaging adapters keep interrupt (the existing framework default).

Changes

  • applyDisplay() reads display.busy_input_mode, falls back to queue (TUI-only override), and stores it in a new UiState.busyInputMode field.
  • dispatchSubmission (and the queue-edit fall-through) now route through a shared handleBusyInput helper:
    • queue — append to queueRef (existing behavior).
    • steer — call session.steer; on rejection or RPC failure, fall back to queue with a sys note.
    • interruptturnController.interruptTurn(...) then send(...) so the new prompt actually moves.
  • uiStore initial value matches: busyInputMode: 'queue'.
  • Mtime polling in useConfigSync already re-applies config.full, so flipping the value in ~/.hermes/config.yaml propagates on the next ~5s tick — no TUI restart.

Why the TUI default differs

In a full-screen TUI you typically author the next prompt while the agent is still streaming. An unintended interrupt loses in-flight typing and feels punitive. The CLI's interrupt default makes sense in a terminal where you tend to wait for a response before typing again; the TUI's authoring loop is different. Users who want CLI-parity behavior set display.busy_input_mode: interrupt explicitly.

Test plan

  • npm run type-check (in ui-tui/) — clean.
  • npm test -- --run (in ui-tui/) — 394/394 pass.
  • Tests:
    • normalizeBusyInputMode mirrors the Python allow-list (queue/steer/interrupt); default is queue.
    • applyDisplay → busy_input_mode covers the UiState fan-out, invalid values, and missing-value fallback.

Refs

  • Audit thread: busy_input_mode: interrupt configuration completely ignored by TUI v2.
  • Python source of truth: hermes_cli/config.py (busy_input_mode: "interrupt" framework default), tui_gateway/server.py::_load_busy_input_mode.

@OutThisLife OutThisLife force-pushed the bb/tui-busy-input-mode branch from 11820a8 to 8b67c68 Compare April 28, 2026 18:51
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/config Config system, migrations, profiles labels Apr 28, 2026
@OutThisLife OutThisLife requested a review from Copilot April 28, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Wires display.busy_input_mode from config into the TUI v2 frontend so “send while busy” behavior matches the rest of the project (CLI/gateway), instead of always queueing.

Changes:

  • Adds UiState.busyInputMode (default interrupt) and normalizes display.busy_input_mode during config sync.
  • Updates submission dispatch to honor queue vs steer vs interrupt via a shared handleBusyInput helper.
  • Adds unit tests for normalizeBusyInputMode and applyDisplay propagation/fallback behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ui-tui/src/gatewayTypes.ts Adds display.busy_input_mode to the config type surface.
ui-tui/src/app/useSubmission.ts Implements mode-specific handling when submitting while a turn is busy (queue/steer/interrupt).
ui-tui/src/app/useConfigSync.ts Normalizes busy_input_mode and applies it into UI state during config polling.
ui-tui/src/app/uiStore.ts Adds busyInputMode default to the UI store initial state.
ui-tui/src/app/interfaces.ts Introduces BusyInputMode and adds busyInputMode to UiState.
ui-tui/src/tests/useConfigSync.test.ts Adds tests for normalization and UI-state fan-out for busy_input_mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ui-tui/src/app/useSubmission.ts
Comment thread ui-tui/src/app/useSubmission.ts Outdated
Comment thread ui-tui/src/gatewayTypes.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ui-tui/src/app/useSubmission.ts Outdated
The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.
@OutThisLife OutThisLife force-pushed the bb/tui-busy-input-mode branch from 966df2b to 42733ba Compare April 28, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
@OutThisLife OutThisLife merged commit af6b1a3 into main Apr 28, 2026
7 of 10 checks passed
@OutThisLife OutThisLife deleted the bb/tui-busy-input-mode branch April 28, 2026 22:52
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
* fix(tui): honor display.busy_input_mode in TUI v2

The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.

* review(copilot): narrow busy_input_mode type, preserve queue order on steer fallback

* review(copilot): clarify handleBusyInput comment (option, not return value)

* fix(tui): default busy_input_mode to queue in TUI (CLI keeps interrupt)

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
* fix(tui): honor display.busy_input_mode in TUI v2

The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.

* review(copilot): narrow busy_input_mode type, preserve queue order on steer fallback

* review(copilot): clarify handleBusyInput comment (option, not return value)

* fix(tui): default busy_input_mode to queue in TUI (CLI keeps interrupt)

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
* fix(tui): honor display.busy_input_mode in TUI v2

The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.

* review(copilot): narrow busy_input_mode type, preserve queue order on steer fallback

* review(copilot): clarify handleBusyInput comment (option, not return value)

* fix(tui): default busy_input_mode to queue in TUI (CLI keeps interrupt)

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
* fix(tui): honor display.busy_input_mode in TUI v2

The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.

* review(copilot): narrow busy_input_mode type, preserve queue order on steer fallback

* review(copilot): clarify handleBusyInput comment (option, not return value)

* fix(tui): default busy_input_mode to queue in TUI (CLI keeps interrupt)

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
* fix(tui): honor display.busy_input_mode in TUI v2

The TUI v2 frontend hard-coded `composerActions.enqueue(full)` whenever
`ui.busy` was true. The classic CLI and gateway adapters honor the
`display.busy_input_mode` config key (`interrupt` | `queue` | `steer`),
but Ink ignored it — sending a message during a long-running turn always
landed in the queue regardless of config. The config default is already
`interrupt` (hermes_cli/config.py), so users who explicitly opted into
that experience were silently stuck on the legacy queue path.

This wires the value through the existing config-sync surface:

* `applyDisplay` now reads `display.busy_input_mode`, defaults to
  `interrupt` (matching `_load_busy_input_mode` in tui_gateway), and
  drops it into a new `UiState.busyInputMode` field.
* `dispatchSubmission` and the queue-edit fall-through call a shared
  `handleBusyInput` helper that branches on the mode:
    * `queue`     — legacy behavior, append to the queue.
    * `steer`     — call `session.steer`; on rejection, fall back to
                    queue with a sys note.
    * `interrupt` — `turnController.interruptTurn(...)` then `send()`,
                    so the new prompt actually moves.
* Mtime polling in `useConfigSync` already re-applies `config.full`, so
  flipping `display.busy_input_mode` in `~/.hermes/config.yaml` takes
  effect on the next 5s tick without restarting the TUI.

Tests:
* `applyDisplay → busy_input_mode` covers normalization + UiState fan-out.
* `normalizeBusyInputMode` mirrors the Python side's allow-list.

Validation:
* `npm run type-check` (in `ui-tui/`) — clean.
* `npm test --run` (in `ui-tui/`) — 394/394.

* review(copilot): narrow busy_input_mode type, preserve queue order on steer fallback

* review(copilot): clarify handleBusyInput comment (option, not return value)

* fix(tui): default busy_input_mode to queue in TUI (CLI keeps interrupt)

In a full-screen TUI users typically author the next prompt while the
agent is still streaming, so an unintended interrupt loses in-flight
typing.  TUI fallback now defaults to `queue`; CLI / messaging
adapters keep `interrupt` as the framework default.

Override per-config via `display.busy_input_mode: interrupt` (or
`steer`) — the normalize/wire path is unchanged, only the missing-
value branch differs from the Python default.

uiStore initial value also flipped to `queue` so first-frame render
before `config.full` lands matches the eventual normalized value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants