Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc74b7d
Design WTA CLI terminal action proposals
vanzue Jul 23, 2026
f1daca5
Route CLI proposals through master
vanzue Jul 23, 2026
78eabb6
Route terminal action proposals directly to helpers
vanzue Jul 24, 2026
b74b582
Merge origin/main into dev/vanzue/cli-terminal-action-proposals
vanzue Jul 27, 2026
5f92aa2
Merge remote-tracking branch 'origin/main' into dev/vanzue/cli-termin…
vanzue Jul 27, 2026
21e13cd
Remove assistant JSON proposal fallback
vanzue Jul 28, 2026
9731244
Merge origin/main into dev/vanzue/cli-terminal-action-proposals
vanzue Jul 28, 2026
b53b4a9
Merge origin/main into dev/vanzue/cli-terminal-action-proposals
vanzue Jul 28, 2026
664538c
Address direct proposal review findings
vanzue Jul 28, 2026
dbb2b46
Fix proposal test spelling
vanzue Jul 28, 2026
a0e7966
Fix proposal test identifiers
vanzue Jul 28, 2026
826b65d
Clarify unified proposal path
vanzue Jul 28, 2026
53992fe
Route proposals through WTA execution alias
vanzue Jul 29, 2026
95ecbf1
Merge origin/main into terminal action proposals
vanzue Jul 29, 2026
6e0bb4d
Merge origin/main into dev/vanzue/cli-terminal-action-proposals
vanzue Jul 30, 2026
d016e80
Merge latest origin/main into terminal action proposals
vanzue Jul 30, 2026
e2fb5cc
Refresh PR mergeability after main merge
vanzue Jul 30, 2026
2eea584
Merge CLI handler extraction into terminal action proposals
vanzue Jul 30, 2026
7d25c99
Hide proposal tool calls before permission
vanzue Jul 30, 2026
7559709
Keep command probes out of terminal proposals
vanzue Jul 30, 2026
169a3a2
Prefer profile-aware command resolution
vanzue Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions doc/release-check-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,11 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not
- [ ] `C218` `[new]` `[UT✓]` `[E2E]` **Image paste (Alt+V) works:** A copied screenshot (`CF_DIB`/`CF_DIBV5`) or image file is queued and sent to the agent as an ACP image content block on the next prompt; the action is gated on the agent advertising image support. When the agent does not support images, or the clipboard has no image, it does not paste but surfaces a clear system message (e.g. "image not supported" / "clipboard empty") rather than silently ignoring the keypress. _(UT: `clipboard_image` + `mock_agent_tests` `seen_images` side-channel; #354.)_
- [ ] `C066` `[E2E]` **Keyboard navigation works:** Arrow keys, Tab completion, Ctrl combinations, and Esc behave correctly.
- [x] `C067` `[UT✓]` `[E2E]` `[MANUAL]` **IME/non-ASCII input works:** IME and non-ASCII input are usable if the release supports localized typing. _(UT: `render_agent_input_accepts_non_ascii` types accented-Latin/Greek/CJK via the real key handler and asserts the input buffer holds them verbatim (multi-byte caret advance) + they render. E2E send path (wtcli send-keys) cannot carry non-ASCII, so the product side is UT-covered; IME composition stays MANUAL.)_
- [ ] `C068` `[UT✓]` `[E2E]` **Streaming output renders correctly:** Agent response chunks, tool calls, plans, and status lines render without corruption. _(UT: `streaming_two_chunks_coalesce_in_app_chat`, `tool_call_surfaces_card_in_chat`, `tool_call_completion_updates_card_status` (in-place, no dup), `plan_surfaces_card_in_chat`, `render_chat_all_message_variants`; streaming-JSON unwrap incl. emoji/surrogate pairs in `ui::chat::tests`.)_
- [ ] `C068` `[UT✓]` `[E2E]` **Streaming output renders correctly:** Agent response chunks, tool calls, plans, status lines, and literal JSON render without corruption. _(UT: `streaming_two_chunks_coalesce_in_app_chat`, `tool_call_surfaces_card_in_chat`, `tool_call_completion_updates_card_status` (in-place, no dup), `plan_surfaces_card_in_chat`, `render_chat_all_message_variants`; Assistant JSON remains ordinary chat text.)_
- [x] `C069` `[UT✓]` `[E2E]` **Permission UI works:** When the agent requests a command/tool permission, the user can allow or reject it. _(UT: `permission_allow_round_trips_to_agent`, `permission_reject_round_trips_to_agent`, `permission_quick_allow/reject_key_round_trips_to_agent`, `render_permission_card_shows_options`, `render_permission_compact_shows_hint`; the `y`/`n` quick-key case-match bug was fixed here.)_
- [ ] `C070` `[E2E]` **Insert into pane works:** Agent-proposed command/text can be inserted into the target terminal pane without running.
- [ ] `C071` `[E2E]` **Run in pane works:** Agent-proposed command can be run in the target terminal pane.
- [ ] `C072` `[E2E]` **Command target is correct:** Insert/run applies to the intended active pane, not the agent pane itself or another tab.
- [ ] `C070` `[E2E]` **Insert into pane works:** A validated Direct Helper Proposal can be inserted into the target terminal pane without running.
- [ ] `C071` `[E2E]` **Run in pane works:** A validated Direct Helper Proposal can be run in the target terminal pane.
- [ ] `C072` `[E2E]` **Command target is correct:** The Helper-injected trusted target routes Insert/Run to the intended active pane, not the agent pane itself or another tab.

### Agent pane slash commands

Expand Down Expand Up @@ -188,7 +188,7 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not

## 3. Autofix flow

**Feature definition:** Autofix detects terminal command failures, captures relevant pane context, asks the configured agent for a fix, and lets the user insert or run the suggested command.
**Feature definition:** Autofix detects terminal command failures, captures relevant pane context, asks the configured agent for a fix, and accepts actionable fixes only through a Direct Helper Proposal before offering Insert or Run.

### Shell integration and detection

Expand Down Expand Up @@ -217,8 +217,8 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not
- [ ] `C100` `[E2E]` **Run suggestion works:** Suggested fix can be run in the source pane.
- [ ] `C101` `[UT✓]` `[E2E]` **Reject/dismiss works:** User can dismiss an autofix suggestion without side effects. _(UT: `trigger_echo_pane_clears_when_state_returns_to_idle`.)_
- [ ] `C102` `[UT✓]` `[E2E]` **Autofix target pane is correct:** Failure in one pane does not offer/run a fix in the wrong pane. _(UT: target-tab routing — busy-pane tests + `autofix_still_triggers_for_non_agent_pane`.)_
- [ ] `C103` `[E2E]` `[MANUAL]` **Autofix with Copilot works:** Copilot returns a useful suggestion.
- [ ] `C104` `[E2E]` **Autofix with non-Copilot agents works:** Autofix produces a usable suggestion with a non-Copilot built-in agent (Claude/Codex/Gemini) and a custom ACP agent — same path as Copilot, covered once across the available agents.
- [ ] `C103` `[E2E]` `[MANUAL]` **Autofix with Copilot works:** Copilot submits a valid Direct Helper Proposal and the card presents a useful suggestion.
- [ ] `C104` `[E2E]` **Autofix with non-Copilot agents works:** A non-Copilot built-in agent (Claude/Codex/Gemini) and a custom ACP agent each execute the canonical command, complete permission arming, and submit a usable Direct Helper Proposal through the same path.
- [ ] `C221` `[new]` `[E2E]` `[MANUAL]` **Environment-aware answers/fixes:** For a failed or "how do I use X" prompt, the agent investigates the live environment first — checks whether the command actually exists on PATH and surfaces local scripts / near-matches for a mistyped command — instead of giving generic advice or fixing a nonexistent command. _(#306.)_

### Autofix across layout changes
Expand Down Expand Up @@ -406,4 +406,4 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not
- Slash commands: `tools\wta\src\commands.rs`.
- Session state model: `tools\wta\src\agent_sessions.rs`, `tools\wta\AGENTS.md`.
- Multi-window agent pane architecture: `doc\specs\Multi-window-agent-pane.md`.
- Autofix flow and logging/runtime layout: `AGENTS.md`.
- Autofix flow and logging/runtime layout: `AGENTS.md`.
357 changes: 357 additions & 0 deletions doc/specs/WTA-CLI-terminal-action-proposals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
# WTA CLI terminal-action proposals

## Status

Implemented direct-only design. Terminal-action proposals travel from a
short-lived WTA CLI to the owning Helper.

## Summary

Autofix and Terminal Agent use a short-lived WTA CLI to submit typed action
proposals directly to the Helper that owns the current turn:

```text
Agent CLI
-> wta propose-terminal-actions
-> per-Helper named pipe
-> existing recommendation card
-> user confirmation
-> existing wtcli/COM executor
```

The proposal command cannot mutate Windows Terminal. It can only ask the
owning Helper to display a recommendation card. The existing card confirmation
remains the sole mutation boundary.

ACP Assistant text is always chat content. It is rendered verbatim and is never
parsed into terminal actions or recommendation cards.

wta-master remains responsible for the shared agent process, ACP multiplexing,
session-to-Helper routing, and forwarding permission requests to the Helper
that owns the ACP session. It does not mint proposal tokens, receive proposal
payloads, correlate proposal results, or acknowledge cards.

## Goals

- Use WTA CLI, not MCP, for typed terminal-action proposals.
- Have the agent session execute one canonical command directly.
- Route the short-lived CLI directly to the owning Helper.
- Keep session, Helper, tab, window, and pane identifiers out of proposal JSON.
- Reject wrong-Helper, stale-turn, unapproved, modified, and replayed requests.
- Give the agent immediate validation feedback and final user-decision feedback.
- Preserve the existing Run, Insert, Open, Split, and Delegate card UI.
- Preserve exactly one visible user confirmation before terminal mutation.

## Non-goals

- Reintroducing MCP or adding another shared server.
- Using wta-master as a proposal router.
- Letting the proposal CLI execute terminal actions.
- Parsing terminal actions from ACP Assistant text.
- Treating arbitrary model-authored shell commands as trusted.
- Proving that arbitrary proposed shell input is non-destructive.
- Reporting whether a confirmed shell command eventually succeeded.
- Persisting proposal channels across Helper process restarts.

## End-to-end flow

```text
1. Helper starts a turn and creates one opaque channel.
2. Helper injects the channel and canonical invocation contract into the prompt.
3. Agent emits the exact canonical WTA command with inline proposal JSON.
4. Before executing it, the Agent CLI sends session/request_permission over ACP.
5. Master routes that request by trusted ACP session ownership to the Helper.
6. Helper parses the command before creating a Permission AppEvent.
7. For an exact current-channel invocation, Helper records the payload digest,
moves the channel from Issued to Armed, and silently selects AllowOnce.
8. Agent runs the short-lived WTA CLI.
9. The CLI derives the per-Helper pipe from the opaque channel and connects
directly to it.
10. Helper verifies channel state, lease, turn freshness, and payload digest,
then atomically consumes the armed attempt.
11. Helper validates the typed proposal and stages the existing card.
12. CLI receives a validation response. If accepted, it remains connected.
13. User confirms or cancels the card, or lifecycle invalidation ends the turn.
14. CLI receives the final response and exits.
```

The permission request still crosses master because that is part of the ACP
topology. Proposal data and proposal state do not.

## Channel and endpoint

Each Helper process creates a random instance identifier and one stable named
pipe:

```text
channel: v1.<helper-instance-id>.<turn-nonce>
pipe: \\.\pipe\IntelligentTerminal.Proposal.<helper-instance-id>
```

Both identifiers use lower-case UUID simple form: 32 hexadecimal characters
without braces or separators. The agent copies the complete channel as an
opaque string. It never constructs or receives separate Helper, session, tab,
window, pane, or prompt identifiers.

The Helper instance and pipe survive pane stash/restore and `/new`. A Helper
process restart creates a new instance and pipe, permanently invalidating all
old channels.

Only one channel is active per Helper. Starting a newer turn invalidates the
previous channel before the new one is issued.

## Canonical invocation

The only auto-approvable PowerShell form is:

```powershell
& "$env:WTA_CLI_PATH" propose-terminal-actions --channel <channel> --payload-json '<compact-json>'
```

For a packaged installation, WTA sets `WTA_CLI_PATH` to the package-family
specific App Execution Alias at
`%LOCALAPPDATA%\Microsoft\WindowsApps\<package-family>\wta.exe`. This lets an
external Agent launch WTA through the registered package boundary instead of
directly executing the protected package file. An unpackaged development build
uses its trusted current executable path instead. Proposal JSON must be compact
UTF-8 JSON encoded as one PowerShell single-quoted argument; literal
apostrophes are escaped by doubling them. The command has no pipeline,
redirection, here-string, command substitution, temporary file, extra
argument, or alternate executable spelling.

The Helper uses one renderer/parser implementation for prompt generation and
permission matching. It does not infer safety from the agent-authored tool
title.

The permission request must describe one execute operation with a `command`
field and a single-entry `commands` array containing the same canonical command.
Agent adapters that emit another permission envelope fail arming and therefore
surface their integration gap instead of falling back to Assistant text.

Permission policy has three outcomes:

| Input | Outcome |
|---|---|
| Exact canonical command for the current channel and compact JSON payload | Silently `AllowOnce`; arm channel with SHA-256 payload digest |
| Recognizable proposal command with unsafe or non-canonical syntax | Silently cancel |
| Any unrelated command | Use the existing Permission UI |

An exact canonical command for a different or stale channel may be allowed
once, but it is not armed; its CLI request receives a structured routing error.
`AllowAlways` is never selected because every proposal must pass through
per-turn arming.

## CLI contract

```text
wta propose-terminal-actions --channel <channel> --payload-json <compact-json>
```

The command does not accept stdin, `--payload-file`, a master pipe, a Helper
pipe, or separate routing identifiers. Inline payload size is limited to
8 KiB UTF-8 before deserialization.

The CLI parses the channel, derives the pipe name, sends one request frame, and
reads newline-delimited compact JSON responses. stdout is protocol-only.
Diagnostics go to stderr.

Protocol-complete rejections exit successfully so the agent can interpret the
response and decide whether to retry. CLI syntax, malformed channel, broken
transport, and internal failures use a nonzero exit status.

## Pipe protocol

Protocol version 1 uses UTF-8 JSON Lines with a maximum encoded line size of
49 KiB, covering worst-case JSON escaping for the 8 KiB payload plus protocol
overhead. One connection carries exactly one request and its responses.

Request:

```json
{"version":1,"channel":"v1.<helper>.<turn>","payload":"{...}"}
```

Immediate validation success:

```json
{"phase":"validation","status":"accepted","proposal_id":"<uuid>","retryable":false}
```

Immediate validation failure:

```json
{"phase":"validation","status":"invalid_schema","reason":"...","retryable":true}
```

Final response after an accepted validation:

```json
{"phase":"final","status":"confirmed","proposal_id":"<uuid>"}
```

Validation statuses:

- `accepted`
- `unknown_channel`
- `helper_mismatch`
- `not_armed`
- `stale`
- `superseded`
- `expired`
- `digest_mismatch`
- `already_consumed`
- `invalid_schema`
- `rejected`
- `unavailable`

Final statuses:

- `confirmed`
- `cancelled`
- `superseded`
- `session_replaced`
- `timed_out`
- `unavailable`

`confirmed` means the selected card action was dispatched to the existing
executor. It does not claim that a target shell command finished successfully.

## Helper channel state

```text
Issued
-> Armed
-> Validating
-> AwaitingUser
-> Confirmed | Cancelled | Superseded | SessionReplaced
| TimedOut | Unavailable
```

The Helper stores:

```text
ProposalChannelManager
helper_instance_id
session_epoch
active_channel
bounded_tombstones
```

An active channel contains its nonce, session epoch, Helper-local prompt
identity, state, retry count, optional digest, lease deadline, and optional
pending final responder. It does not contain model-authored target identity.

Before accepting a pipe request, the Helper checks in order:

1. protocol version and frame limits;
2. Helper instance encoded in the channel;
3. active channel or bounded tombstone;
4. channel state is `Armed`;
5. 30-second armed lease has not expired;
6. session epoch and prompt identity are still current;
7. SHA-256 of the exact payload bytes matches the armed digest;
8. atomic one-use transition to `Validating`;
9. strict proposal schema and origin policy;
10. trusted active target injection by App.

After schema rejection, the channel returns to `Issued` and clears its digest.
The agent may correct the payload, request permission again, and retry up to
two times. An accepted proposal is one-use. Lifecycle and user-decision
terminal states are not retryable.

## Proposal schema and trusted target

The public payload is the versioned schema defined by
`terminal_action_proposal.rs`. It uses `deny_unknown_fields`, explicit count
and size limits, and hand-written conversion to `RecommendationSet`.

It never accepts:

- ACP session or Helper identifiers;
- window, tab, pane, prompt, or pipe identifiers;
- model-authored parent pane identifiers;
- arbitrary agent executable paths.

Terminal Agent proposals contain one to three choices. App binds `send` and
panel actions to the trusted pane captured for that turn and resolves delegate
actions only through configured policy.

Autofix accepts exactly one choice with one `send` action. App binds it to the
recorded failing pane and matching Autofix generation.

## Lifecycle

The ownership hierarchy is:

```text
Helper process
-> ACP session epoch
-> active turn channel
```

Lifecycle transitions invalidate the channel before replacing the owning
session or process:

| Event | Result |
|---|---|
| New prompt in same session | Previous channel becomes `superseded`; issue a new channel |
| `/stop` | In-flight channel becomes `cancelled` |
| `/new` or load another session | Increment epoch; old channel becomes `session_replaced`; pipe remains |
| `/restart` | Helper and pipe are destroyed; waiting clients become unavailable |
| Pane stash/restore | Preserve Helper, pipe, session, channel, and card |
| Card confirm | Atomically claim the live proposal, dispatch through the existing executor, then send `confirmed` |
| Card cancel/dismiss | Send `cancelled` |
| User-decision timeout | Send `timed_out` after 10 minutes |
| ACP transport lost | Send `unavailable` and refuse new channels |
| Tab/window close or Ctrl+C twice | Destroy Helper and pipe |

The Helper retains at most four terminal tombstones for three minutes.
Tombstones contain only a channel hash, terminal status, and timestamp. They
improve errors for late clients without retaining payload, digest, or target
data. They are not persisted.

## Security model

The channel is an unguessable, short-lived bearer handle. The per-user named
pipe ACL, Helper-instance routing, per-turn nonce, ACP-session permission
routing, payload digest, short armed lease, and one-use transition prevent
normal cross-tab mistakes, stale turns, payload changes, and replay.

The Helper-side permission decision is essential: knowing an issued channel is
not enough to submit a proposal; the exact payload must first be armed through
the owning ACP session.

A malicious process that can read the complete armed channel and exact payload
from the agent process and race that process can still use the bearer handle.
The direct-pipe design does not claim process attestation. This residual risk
is bounded because the CLI only proposes a visible card and user confirmation
is still required before mutation.

## Validation

Direct Helper routing is the only card-producing path for every configured ACP
agent. Integrations must prove canonical command execution, permission routing,
and Windows/WSL reachability directly; Assistant text cannot mask a broken
proposal path.

Automated and live coverage must include:

- channel parsing, uniqueness, one-use, lease, retries, tombstones, and epochs;
- canonical rendering/parsing, quoting, extra-token rejection, and auto-policy;
- pipe framing, size limits, disconnects, and two-phase responses;
- wrong Helper, wrong turn, stale, unarmed, digest mismatch, and replay;
- schema and origin policy validation with trusted target injection;
- card confirm, cancel, supersede, timeout, and Helper shutdown;
- `/stop`, `/new`, session load, `/restart`, stash/restore, tab/window close;
- multi-tab and multi-window isolation;
- no Permission UI for an exact canonical proposal;
- Assistant JSON remains visible chat text and never surfaces a card;
- no terminal mutation before card confirmation;
- explicit Windows-target WTA build and packaged live verification.

## Related work

- PR #428: superseded MCP-based implementation.
- PR #484: WTA CLI proposal implementation and this direct Helper revision.
- Issue #445: delegate creation latency after card confirmation; independent of
proposal transport.
Loading
Loading