Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added strict two-phase Plan turns with an explicit Approve, Revise, or
Abandon review boundary, plus `Ctrl+O` Send now for promoting a prompt ahead
of ordinary queued follow-ups after settling the active turn.

### Fixed

- Made TUI Auto mode genuinely non-interactive for every operation that
survives explicit policy and workspace hard denials, including tool-owned
confirmation escalation. Late confirmation events resolve automatically in
both directions, queued turns retain their submission-time mode, and Plan
remains strictly read-only.

## [0.9.6] - 2026-07-17

### Added
Expand Down
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,8 @@ input prefixes:

| Area | What A3S Code TUI provides |
| --- | --- |
| Coding loop | Chat with the coding agent, stream semantic tool cards, approve or deny tools, switch `/auto`, run direct shell turns with `!`, run a durable Ultracode `/goal`, and fork or clear sessions when needed. `/relay` resumes a saved A3S Code session with its per-session settings or hands the latest task from a workspace-scoped Claude Code, Codex, or WorkBuddy transcript to the active session. |
| Coding loop | Chat with the coding agent, stream semantic tool cards, choose Default, Plan, or Auto execution, run direct shell turns with `!`, run a durable Ultracode `/goal`, and fork or clear sessions when needed. `/relay` resumes a saved A3S Code session with its per-session settings or hands the latest task from a workspace-scoped Claude Code, Codex, or WorkBuddy transcript to the active session. |
| Execution modes | Default is the interactive HITL mode. Plan exposes only read-only discovery tools, stages the completed plan behind an explicit Approve, Revise, or Abandon decision, and starts approved implementation as a new Default turn. Auto executes every operation that survives explicit policy and workspace hard denials without entering HITL; hard denials fail directly instead of opening an approval prompt. A queued turn retains the mode captured when it was submitted. |
| Workspace UI | `/ide` opens a superfile-style tree and editor with terminal-stable file marks, `/config` edits the active config in the same editor, `Ctrl+T` opens the complete semantic transcript, and file edits render bounded diffs through the shared `DiffView` component. Diff headers use green `+N` and red `-N` counts; Markdown uses Codex-spaced section headings, responsive tables, syntax highlighting, and terminal hyperlinks. |
| Models and effort | `/model` switches configured providers, OS gateway models, and signed-in account tabs. Codex account discovery delegates refresh and entitlement checks to the installed Codex CLI, so an expired identity token does not hide models while reusable account access remains. WorkBuddy `hy3` tagged calls are converted into native tool events without exposing protocol markup in streamed messages. `/effort` scales thinking budget, tool-round budget, auto-continuation, and model-agnostic rigor guidance from `low` through `max` and `ultracode`. A3S Code 5.2.4 structured calls use native JSON Schema or forced-tool output only when the active client advertises that capability; unknown custom OpenAI-compatible endpoints retain the bounded prompt fallback instead of receiving an assumed `tool_choice`. |
| Dynamic workflows | `ultracode` and `?` DeepResearch can use `DynamicWorkflowRuntime`, a local A3S Flow-backed workflow runner. It records workflow/step history while PTC scripts perform ordinary tool work. This is separate from `/flow`, which is OS Workflow as a Service for persisted workflow assets. |
Expand Down Expand Up @@ -753,9 +754,12 @@ Pending host turns are scheduled by `a3s_lane::PriorityQueue`: explicit user
input has priority over host-generated continuations, while equal-priority
messages remain FIFO. A queue item is committed only after Core admits its
stream; `SessionBusy` and admission timeouts restore the same priority and FIFO
position. Esc cancels and settles the active worker before consuming exactly
one queued successor. The bottom queue strip contains pending turns only and
removes a message as soon as Lane claims it for execution.
position. Enter appends a follow-up to that immutable FIFO queue. `Ctrl+O`
performs Send now: it cancels and settles the active turn, then promotes the
new prompt ahead of normal follow-ups without cancelling a durable goal. Each
queued row retains its submission-time execution mode even if the composer mode
changes later. The bottom queue strip contains pending turns only and removes a
message as soon as Lane claims it for execution.
The transcript uses Codex-style `•` headers with `└` detail and `│` command
continuations, groups adjacent reads/lists/searches into one Explore cell, and
reflows semantic arguments, output, diffs, and Markdown after a resize. User
Expand All @@ -775,8 +779,8 @@ instead of rebuilding the full viewport.
| Transcript | Assistant text, reasoning, tool cards, diff summaries, task updates, memory recall/store notices, compaction notices, and RemoteUI action links stay in one scrollable history. Drag-select copies transcript text on release. |
| Input line | Type a normal prompt, use `Shift+Enter` for multiline input, prefix `!` for a direct shell turn, prefix `?` for DeepResearch, use `@<path>` to attach a workspace file through the clickable picker, or paste an image with `Ctrl+V`. |
| Slash menu | Press `/` or type a slash command to open a wheel-browsable, clickable command palette backed by the same command registry used by `/help`. Commands are grouped into model/config, workspace, context, OS, asset, and operations surfaces. |
| Approvals | Mutating tools pause in a confirmation overlay with arguments and result context. Default mode prompts, plan mode auto-approves read-only discovery, and auto mode approves later tool calls in the session. |
| Footer | The footer shows model/provider, effort, mode, context fill, active asset, login/runtime state, and session hints. Context warnings re-arm after compaction, clear, or model switch. |
| Approvals | In Default mode, gated tools pause in a confirmation overlay with arguments and result context. Plan is a strict read-only boundary followed by Approve, Revise, or Abandon review. Auto never enters HITL for an operation that survives explicit hard denials. |
| Footer | The footer shows model/provider, effort, the active turn mode, a distinct `next:` composer mode when it differs, context fill, active asset, login/runtime state, and session hints. Context warnings re-arm after compaction, clear, or model switch. |
| Tool calls | Live tool status appears inline while running. Inline `program` calls summarize structured intent, research scope, workflow phase, and completed nested-call results instead of repeating JavaScript wrapper source. |
| Semantic transcript | `Ctrl+T` opens the complete live session transcript in a dedicated full-width viewport, preserving user-surface, tool-state, and diff colors while showing reasoning, plans, every tool lifecycle and full output, subagent state, and the current live Markdown tail. |
| Workspace editor | `/ide` opens a full-screen file browser/editor. `/config` reuses the editor for the active ACL config. Both surfaces use terminal-safe, type-aware file and folder sigils, semantic icon colors, aligned disclosure rows, icon-bearing breadcrumbs, and a ruled line-number gutter while keeping edits inside the workspace backend and normal permission path. |
Expand All @@ -789,8 +793,9 @@ Key interactions:
| Key or input | Behavior |
| --- | --- |
| `Enter` | Send the prompt; when a turn is busy, queue the next message. |
| `Ctrl+O` | Send now: cancel the active turn and promote this prompt ahead of normal queued follow-ups. |
| `Shift+Enter` | Insert a newline in the input. |
| `Shift+Tab` | Cycle run mode: default, plan, auto. |
| `Shift+Tab` | Cycle the composer mode: Default, strict read-only Plan, non-interactive Auto. Running and queued turns keep their submission-time mode. |
| `Up` / `Down` | Recall input history or move through menus/panels. |
| `PgUp` / `PgDn` | Scroll the transcript or the active full-screen panel. |
| `Shift+End` | Jump to the latest transcript output. |
Expand Down Expand Up @@ -832,14 +837,17 @@ choice continues the next goal iteration without changing the restored execution
mode; the second enters the session with the goal still paused, where
`/goal resume` can continue it later.

The TUI owns HITL confirmation for gated tools. In default mode, mutating tools
prompt through a wheel-browsable, clickable approval overlay; `a` or `/auto` approves later tool calls for
the session, while Shift+Tab cycles default, plan, and auto modes. Plan mode
auto-approves read-only discovery tools but still asks before writes. Auto mode
silently approves every operation that reaches HITL; hard permission denials
remain non-bypassable and never enter the confirmation overlay. Tool
timeouts and confirmation timeouts are tracked separately so a human approval
pause does not consume the command runtime budget.
The TUI owns HITL confirmation for gated tools. In Default mode, mutating tools
prompt through a wheel-browsable, clickable approval overlay. Shift+Tab cycles
Default, Plan, and Auto for future submissions. Plan exposes only read-only
discovery tools. When planning completes, the TUI freezes queue draining at an
explicit Approve, Revise, or Abandon boundary; approval starts implementation
as a separate Default turn. Auto resolves allowed tools directly in Core and
bypasses tool-owned confirmation escalation, so it never opens HITL. Explicit
policy denials and workspace guardrails remain authoritative, and hard denials
fail without opening an approval prompt. Tool timeouts and confirmation
timeouts are tracked separately so a human approval pause does not consume the
command runtime budget.

All local filesystem work stays under the active workspace services and A3S Code
permission policy. OS operations require `/login`; before login the TUI can
Expand Down Expand Up @@ -1166,7 +1174,7 @@ These commands are available outside the asset-specific flows:
| `/clear` | Start a fresh conversation in the current session surface. |
| `/fork` | Branch the current transcript into a new session id. |
| `/relay` | Open the A3S Code, Claude Code, Codex, and WorkBuddy session picker. Native sessions resume in place with their saved model, effort, execution mode, theme, and paused goal; external sessions relay their latest user task into the current A3S Code session. |
| `/auto` | Switch the session into auto-approve mode. |
| `/auto` | Switch future submissions to non-interactive Auto mode. Operations that survive explicit policy and workspace hard denials execute without HITL. |
| `/plugin` / `/reload` | Manage and hot-reload skills/plugins, including wheel browsing and click-to-toggle skill state in the TUI. |
| `/update` | Upgrade the CLI and restart back into the saved session. |
| `/exit` | Quit `a3s code` after session persistence runs. |
Expand Down
15 changes: 14 additions & 1 deletion src/tui/app/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ impl App {
}

pub(super) fn session_status_chips(&self) -> Vec<SessionStatusChip> {
let mut chips = vec![mode_status_chip(self.mode)];
let effective_mode = self.active_turn_mode.unwrap_or(self.mode);
let mut chips = vec![mode_status_chip(effective_mode)];
if self
.active_turn_mode
.is_some_and(|active| active != self.mode)
{
chips.push(
SessionStatusChip::new("↪", format!("next:{}", self.mode.name()))
.color(COMPOSER_CHROME.secondary),
);
}
if self.plan_review.is_some() {
chips.push(SessionStatusChip::new("◆", "plan review").color(COMPOSER_CHROME.active));
}

if self.goal.is_some() {
chips.push(goal_status_chip(self.goal_since));
Expand Down
20 changes: 12 additions & 8 deletions src/tui/app/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,16 +440,20 @@ impl App {
);
}
self.update_viewport_with_stream();
if self.mode.auto_approves_confirmation() {
// Silent: the mode indicator already shows auto-approve is on;
// a line per tool is just noise. Do NOT start another
// spinner_tick here — the turn's tick loop is already running
// (state stays Streaming through auto-approval). Stacking one
// per auto-approved tool made the spinner advance several
// frames per 80ms = the speed-up / slow-down cadence.
if let Some(approved) = self.execution_policy.auto_confirmation_decision(
&tool_name,
&args,
Path::new(&self.cwd),
) {
// Auto is non-interactive in both directions: approve calls
// that survive the hard guardrail and reject hard denials.
// Do NOT start another spinner tick here — the turn's tick
// loop is already running.
let session = self.session.clone();
let reason =
(!approved).then(|| "Denied by the Auto mode hard guardrail.".to_string());
return Some(cmd::cmd(move || async move {
let _ = session.confirm_tool_use(&tool_id, true, None).await;
let _ = session.confirm_tool_use(&tool_id, approved, reason).await;
Msg::Resume
}));
}
Expand Down
22 changes: 16 additions & 6 deletions src/tui/app/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ pub(crate) async fn run_in(
.and_then(TuiSessionState::effort_index)
.or_else(load_tui_effort_preference)
.unwrap_or(DEFAULT_TUI_EFFORT_INDEX);
let initial_mode = tui_session_state
.as_ref()
.map(TuiSessionState::mode)
.unwrap_or(Mode::Default);
let sidecar_model_preference = tui_session_state
.as_ref()
.and_then(|state| state.model.clone());
Expand Down Expand Up @@ -473,6 +477,7 @@ pub(crate) async fn run_in(
let initial_auto_delegation = effort_uses_automatic_delegation(initial_effort);
let deep_research_report_tool_gate = DeepResearchReportToolGate::default();
deep_research_report_tool_gate.set_workspace(Path::new(&workspace));
let execution_policy = TuiExecutionPolicy::new(initial_mode);
// Claude Code compatibility: inject CLAUDE.md (AGENTS.md is auto-loaded by
// the core) into the system prompt via prompt slots.
let instructions = project_instructions(&workspace);
Expand Down Expand Up @@ -523,9 +528,10 @@ pub(crate) async fn run_in(
session_id.as_str(),
apply_launch_model_options(
with_instr(with_recent_workspace_context(
tui_session_options_with_gate(
tui_session_options_with_gate_and_execution(
confirmation.clone(),
deep_research_report_tool_gate.clone(),
execution_policy.clone(),
)
.with_session_store(store.clone())
.with_workspace_backend(workspace_services.clone())
Expand Down Expand Up @@ -564,9 +570,10 @@ pub(crate) async fn run_in(
workspace.clone(),
Some(apply_launch_model_options(
with_instr(with_recent_workspace_context(
tui_session_options_with_gate(
tui_session_options_with_gate_and_execution(
confirmation.clone(),
deep_research_report_tool_gate.clone(),
execution_policy.clone(),
)
.with_session_store(store.clone())
.with_session_id(session_id.as_str())
Expand Down Expand Up @@ -731,10 +738,6 @@ pub(crate) async fn run_in(

remote_ui::prime_webview_lookup();

let initial_mode = tui_session_state
.as_ref()
.map(TuiSessionState::mode)
.unwrap_or(Mode::Default);
let initial_paused_goal = tui_session_state
.as_ref()
.and_then(|state| state.paused_goal.clone());
Expand Down Expand Up @@ -885,6 +888,7 @@ pub(crate) async fn run_in(
host_tool_call_id: None,
interrupting: false,
pending_tools: VecDeque::new(),
execution_policy,
approval_sel: 0,
history: history_seed,
history_pos: None,
Expand All @@ -896,12 +900,18 @@ pub(crate) async fn run_in(
anim: 0,
mode: initial_mode,
queue: PriorityQueue::new(),
queued_turn_modes: HashMap::new(),
queued_plan_drafts: HashMap::new(),
active_queued_turn: None,
active_queued_turn_token: None,
active_turn_mode: None,
active_plan_draft: None,
queue_retry_generation: 0,
queue_retry_attempt: 0,
running_task: None,
plan: PlanProjection::default(),
pending_plan_review: None,
plan_review: None,
ide: None,
memory: None,
asset_list: None,
Expand Down
Loading
Loading