Skip to content

feat(cli): full-screen interactive TUI runtime#234

Draft
michaeljabbour wants to merge 8 commits into
microsoft:mainfrom
michaeljabbour:upstream/full-screen-interactive-tui
Draft

feat(cli): full-screen interactive TUI runtime#234
michaeljabbour wants to merge 8 commits into
microsoft:mainfrom
michaeljabbour:upstream/full-screen-interactive-tui

Conversation

@michaeljabbour

Copy link
Copy Markdown

Draft purpose

This is an upstream-preparation draft for the full-screen interactive CLI work validated and merged in michaeljabbour/amplifier-app-cli#2.

The repository currently says external contributions are not accepted, so this is intentionally not review-ready. It gives maintainers a concrete, tested branch for architecture/scope feedback before any proposal to merge.

User-facing result

  • full-height transcript with a stable multiline composer and pinned footer
  • streamed Markdown, tool activity, task/agent state, token usage, and cost without duplicate output
  • text and image paste, transcript selection/copy, scrolling, tail-follow, resize, and terminal restoration
  • inline approvals and independently cycled interaction/permission modes
  • slash-command discovery and exact routing for modes, permissions, model, effort, context, status, and sessions
  • explicit model and effort overrides that survive exit/resume
  • parent/child task view via Ctrl-T

Architecture

Interactive orchestration is split into focused app-layer boundaries:

  • amplifier_app_cli/runtime/interactive_* for lifecycle, resource setup, input, turns, cleanup, and persistence
  • amplifier_app_cli/ui/layered_* for layout, transcript, navigation, approvals, composer, and footer
  • amplifier_app_cli/ui/command_* for the command registry, palette, modes, configuration, and session actions
  • amplifier_app_cli/ui/task_* and agent_lanes.py for work-tree status
  • amplifier_app_cli/runtime/amplifier_compat.py as the isolated compatibility boundary for remaining private Amplifier seams

ADR-0005 and ADR-0006 document the interaction and terminal decisions.

Verification

Fork PR #2 passed four required Ubuntu/Python 3.11 jobs:

  • lint
  • types
  • tests
  • integration

Local acceptance on this exact tree:

  • uv sync --all-groups --locked
  • uv lock --check
  • Ruff clean
  • Pyright: 0 errors
  • default suite: 1,976 passed, 18 integration tests deselected, 1 expected xfail
  • real-PTY integration suite: 18 passed
  • wheel and sdist build
  • live macOS PTY: full-screen takeover, pinned footer, mode/permission cycles, paste, command routing, resume persistence, and clean terminal restoration

Scope and review risks

This remains a large cohesive change: 305 files, primarily UI/runtime modules, acceptance tests, and transcript goldens. Before marking it ready, maintainers should decide whether to review it as one atomic interactive-runtime replacement or request a staged extraction plan.

Known reconciliation areas:

No merge is requested while this PR remains in draft.

michaeljabbour and others added 8 commits July 20, 2026 16:19
The bottom persistent status bar's stage text and the top per-turn transcript
record (the committed Plan block) both derived from the same turn title, so
when nothing more specific was happening the bar fell back to
f"Working on {title}" -- a verbatim duplicate of the permanent record already
written to scrollback.

Introduce current_activity_label() in layered_repl_status.py as the single
source of truth for what's happening right now in the bar. Precedence: active
agent-lane delegation > an active plan/tool step > a streaming response > a
distinct working idle indicator. The turn title is no longer part of this
precedence at all -- it stays exclusively in the permanent per-turn record, so
the two surfaces never collide.

_working_stage() now delegates to the shared helper instead of building its
own fallback chain (which included the removed title echo). Lane activity now
also outranks a stale _task_tracker.active_step_text() and a lingering stream
preview, since delegated work is the most specific signal available.

The same layered_repl_status.py diff also carries footer colorization
(_footer_fragments/_footer_part, applying class:mode.*/status.risk accents to
the plain toolbar text), palette-visibility and queued-message-bar rendering
(queued_bar_text, the palette_open/lane_focused hint precedence), and the
capability_hint_overrides plumbing into the footer call -- none of which are
mentioned by the summary above; they are included here because they already
shipped as part of this same file in the original commit and are exercised by
its own test coverage.

Fix-back (this revision): a prior push of this commit did not build or test
standalone -- confirmed by re-running `git worktree add` + a fresh `uv sync`
against the commit in isolation, which reproduced:
  - ImportError: cannot import name 'TOKENS' from
    amplifier_app_cli.ui.layered_repl_style
  - pyright: no parameter named "hint_overrides"/"palette_open"/"lane_focused"
    on format_bottom_toolbar_text (layered_repl_status.py:82,110,111)
  - a latent AttributeError risk: lane.render_tree(...) has no implementation
    on AgentLaneSnapshot
  - (found during isolated re-verification, not in the original review) an
    additional ImportError in tests/test_layered_repl.py for
    amplifier_app_cli.ui.layered_repl_keys, and a runtime AttributeError from
    the still-unimplemented capability_hint_overrides() on LayeredReplApp

Root cause: this file was authored and verified against a dirty working tree
that already contained later, uncommitted work (a token-based theme system,
a capability-hint-label catalog, and a keybinding-table refactor). The
production diff for *this* commit only ever touched layered_repl_status.py,
but its verification ("1946 passed", "pyright clean") was run against the
dirty tree, not the isolated commit -- so the missing dependencies went
unnoticed.

Fix: add the minimum each existing reference in layered_repl_status.py needs
to resolve, without pulling in the larger uncommitted refactor those
dependencies eventually belong to:
  - amplifier_app_cli/ui/layered_repl_style.py: add a `TOKENS` dict with only
    the four keys layered_repl_status.py and its test coverage reference
    (dimmer, green, orange, bg_chrome), matching the hex values already used
    for the same roles in the existing LAYERED_REPL_STYLE. LAYERED_REPL_STYLE
    itself is untouched.
  - amplifier_app_cli/ui/footer.py: add `palette_open`, `lane_focused`
    (threaded into `_hint_levels()` with a literal-string hint tuple in the
    same style already used for `approval_pending`) and `hint_overrides`
    (accepted and intentionally unused -- no keybinding-label catalog exists
    at this revision, matching the existing `del image_paste_available`
    precedent in the same function).
  - amplifier_app_cli/ui/agent_lanes.py: add `render_tree()` to
    AgentLaneSnapshot, reusing the existing `_status_summary`/`_format_cost`/
    `_truncate_cells` helpers already used by `render()`.
  - amplifier_app_cli/ui/layered_repl_terminal.py: add a minimal
    `capability_hint_overrides()` returning `None` on LayeredReplTerminalMixin
    -- layered_repl_status.py calls `self.capability_hint_overrides()` at
    runtime and no concrete implementation existed anywhere in this commit's
    tree, which crashed 51 tests with AttributeError.
  - tests/test_layered_repl.py, tests/test_layered_repl_visual_layout.py:
    revert the handful of assertions (and one net-new test) that had leaked
    in from the same dirty tree and exercised functionality this commit never
    implements: the layered_repl_keys/build_layered_key_bindings rename (the
    production code here still defines _build_key_bindings in
    layered_repl_layout.py), the composer edge-window/separator-row/queued-
    container layout, the "[mode] then prompt-marker" ordering swap, the
    "shift-tab"->"shift+tab" and "ctrl-o expand"->"click or ctrl-o expand"
    hint-text rewording, palette height/grouping changes, the "mode <id>"
    footer prefix, the y-shortcut approval keybinding, and "Plan ·" prefix
    labeling in the committed plan record/plan pane. None of the production
    code for any of that exists in this commit; restoring the original
    assertions makes the test diff match what this commit actually ships.
    The assertions that do test this commit's real behavior change are kept:
    "Coordinating N agents" (not the echoed title) taking precedence in the
    working bar and live-agent-tree layout, and the working bar's distinct
    idle indicator.

Verified in total isolation (git worktree add of this commit alone + a fresh
uv sync, matching the reviewer's method): uv run pytest -q fully collects and
passes, uv run pyright amplifier_app_cli/ui/layered_repl_status.py is clean,
and ruff check/format are clean on every file this commit touches.

Generated with Amplifier (https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
TranscriptReflowController deferred rewrapping already-printed transcript
history for the entire duration of a running turn, not just while output
was actively being appended. `_reflow_stream_active` gated on
`execution_state["running"]`, which stays true for a whole turn -- including
long stretches with nothing new printed yet (e.g. mid-tool-call waiting on
a shell command). A resize during that idle window was silently held until
the turn completed, creating a visible width seam between stale-wrapped
history and the correctly-wrapped live preview.

Narrow the gate to check only whether a stream preview is actively present
(genuinely being painted), dropping the turn-wide running check. A running
turn with no live append now reflows immediately on resize; a turn with an
actively streaming preview still defers the rebuild, preserving the
original guarantee of never repainting history under live output.

Adds regression coverage in tests/test_resize_reflow.py:
- reflow is not deferred when running but nothing is streaming (new)
- reflow is still deferred while a stream preview is present (guard)

Fixes: "resize doesn't work perfectly" mid-turn report.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…le per ADR-0005 amendment

- Shift-Tab (InteractionController.cycle()) now cycles mode only (chat → build → plan → auto → brainstorm → chat), removing old special-cases where mode/posture were entangled
- New Ctrl-P (InteractionController.cycle_permission()) cycles permission posture only (chat → build → plan → auto → bypass → chat), reusing TrustState.cycle()
- Explicit-selection latch: any posture chosen via Ctrl-P latches _trust_explicitly_set, so mode-only cycling never silently reverts the chosen posture
- Ctrl-P wired through layered_repl_layout → layered_repl_config → interactive_resources → interactive_host
- ADR-0005 doc updated with amendment section
- Tests extended: mode/posture cycle independence, posture cycle order, latch semantics, footer permission-display regression
- Verification: full pytest run zero regressions vs baseline (+7 new passing tests; pre-existing failures unrelated to this work)

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…governance hardening

v3 TUI rewrite: token-based theme system (layered_repl_style), table-driven keybindings (key_bindings_table.py + layered_repl_keys.py extraction), capability-hint catalog, transcript width-reflow + click spans + block render cache, terminal probe, keyboard protocol, layered transcript control, golden-based visual tests (footer + transcript goldens with regen_goldens.py), CI workflow, migration/design docs (tui-v3-cohesive.md, MIGRATION-main-decomposition.md).

Governance/classifier hardening: `_parse_verdict` now tolerates provider thinking blocks (root cause of every tool call failing closed — the evaluator's own reasoning_effort triggered Anthropic extended thinking, and the 1-block contract rejected the response); fail-closed path now logs the swallowed exception and carries bounded detail (StageEvaluation.detail). Still fails closed on genuinely malformed content.

Width-reflow scroll-anchor fix: reflow_to_width() preserves the anchor offset WITHIN the anchor span (was snapping to span start, jumping merged raw spans to top on width-only resize — Linux-manifesting, caught in DTU validation). New deterministic unit test.

Test-debt cleanup: 20 stale-format failures reconciled to v3 rendering, stale keybinding test ported to the new surface, footer hint cap 3→4 with ctrl-p perms hint added (footer + composer placeholder), goldens regenerated and reviewed.

Ctrl-P permission hint completes the ADR-0005 amendment shipped in 9d00f25.

Validation evidence: local suite 1951 passed / 0 failed / 0 errors, ruff clean, pyright 0 errors; independently validated in an isolated Linux DTU (full suite 1951/0/0, scripted real-TUI PTY check PASS with ctrl-p hint rendered, width-reflow fix deterministic under tmux 3.7b).

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ers; surface denial detail

Root cause found by live probe against claude-fable-5: the Anthropic provider module
has no response_format handling, so the classifier's JSON verdict schema never reaches
the model — sonnet-5 happened to guess the field names, fable-5 returned {"verdict": ...}
→ invalid shape → ValueError → fail-closed DENY on EVERY tool call ("classifier failed
closed").

Fixes:
1. Authorization prompt now spells out the exact verdict JSON object inline (schema
   independence)
2. _parse_verdict selects text blocks (allowlist) instead of excluding known thinking
   types — robust to any non-text block type
3. Provider refusals (empty content + finish_reason="refusal", from fable-5's
   content-level refusal classifier on dangerous payloads) map to first-class DENY
   (provider-refused-action) instead of generic exception
4. Classifier failure detail (StageEvaluation.detail) now folds into denial reason, so
   the TUI blocked-block, tool_result fed to agent, and session events all show WHY a
   denial happened

Live-validated against claude-fable-5: benign→allow 3/3, git push --force→deny 3/3,
all parseable. 37 governance tests pass.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Fresh (non-resumed) TUI sessions unconditionally started in mode=chat / posture=chat —
no code path seeded a new session from settings. New config.tui.startup_mode and
config.tui.startup_permission settings keys seed brand-new sessions via
AppSettings.get_tui_startup_config() (merged global→project→local→session).

A settings-declared permission preset is an explicit user choice per ADR-0005
("choosing the bypass permissions preset"), so it latches _trust_explicitly_set and
mode cycling never silently reverts it. Resumed sessions' persisted state always
wins. Invalid values are dropped with a log and fall back to the safe chat default.

Three pre-existing tests hardened to isolate from ambient project settings.

Also: .amplifier/ directory (.gitignore'd) is now a personal project-scope settings
location for startup preferences. Shipping bypass-by-default in the repo would be wrong.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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.

1 participant