Port the team-enhancement feature layer from feat/multi-tool-exec onto
feat/team-test so the agent-teams stack is buildable and testable on top
of the current CLI/runtime.
Subagent model (config wiring):
- Add subagent_model field to RuntimeFeatureConfig + RuntimeConfig::subagent_model()
accessor + parse_optional_subagent_model() so the subagentModel setting
(already validated by config_validate.rs) is now actually read and stored.
- Agent tool's resolve_agent_model falls back to subagentModel from config
when no explicit model is passed.
Parallel tool execution:
- Re-export ToolCall/ToolResult/TurnProgressReporter from runtime so the
CliToolExecutor::execute_batch override type-checks.
- Override execute_batch to classify read-only tools (read_file, glob/grep
search, WebFetch/WebSearch, LSP, Git*, ToolSearch, Skill, Agent*,
TeamStatus, Task*) as parallel-safe and run them concurrently via
std:🧵:scope; everything else stays sequential. Results return in
original model order.
Team coordination layer (cherry-picked from feat/multi-tool-exec):
- AgentMessage, TaskClaim, TeamStatus tools + shared mailbox directory +
mode presets (tiny/1x ... mega/6x) + enriched TeamCreate/Agent tool
descriptions + background team watcher.
- /team slash command: on/off/status/toggle of CLAWD_AGENT_TEAMS (the flag
TeamCreate requires), replacing the info-only stub. Spec updated to
[on|off|status].
- resolve_conflicts resolved: de-duplicated SlashCommand::Team/Setup arms
and ReadOutcome::TeamToggle arms that the cherry-picks overlapped with
the existing /team parse fix.
Verification:
- cargo build --workspace OK
- cargo test -p tools --lib: 111 passed (1 env failure: repl_executes_python_code)
- cargo test -p runtime --lib team_cron: 17 passed
- cargo test -p commands --lib: 42 passed (incl /team parse regression)
- cargo test -p rusty-claude-cli --bin claw: 345 passed
- scripts/fmt.sh --check OK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Agents post completion/failure to team inbox on termination
(.clawd-agents/mailbox/team/{team_id}/{agent_id}-{ts}.json)
- Team watcher reads from inbox instead of polling .json files
- New TeamStatus action=inbox reads team messages from the inbox
- AgentOutput carries team_id, persisted in manifest
- AgentInput accepts team_id from TeamCreate
- TeamCreate passes team_id to each spawned agent
- Inbox cleaned up when all agents finish
Co-authored-by: GLM 5.1 FP8 via Crush <crush@charm.land>
- TeamStatus tool with 3 actions:
- status: live snapshot (running/completed/failed counts, agent details)
- summary: final results when agents finish (includes result content)
- events: timeline from append-only event log
- Background team watcher thread spawned by TeamCreate:
- Polls agent .json files every 2s
- Prints [team] progress to stderr on agent completion/failure
- Updates team manifest status when all agents finish
- Writes events to .clawd-agents/teams/{team_id}-events.jsonl
- TeamStatus added to PARALLEL_SAFE_TOOLS and all agent allowed_tools
Co-authored-by: GLM 5.1 FP8 via Crush <crush@charm.land>
Instead of erroring when neither mode nor tasks are specified,
default to "2x" (2 Explore + 2 Plan + 2 Verification = 6 agents).
Co-authored-by: GLM 5.1 FP8 via Crush <crush@charm.land>
Add 'mode' field to TeamCreate that auto-generates agent teams:
- "2x" = 2 Explore + 2 Plan + 2 Verification = 6 agents
- "4x" = 4 Explore + 4 Plan + 4 Verification = 12 agents
- "6x" = 6 Explore + 6 Plan + 6 Verification = 18 agents
When mode is set, 'prompt' provides the shared task description and
'tasks' is ignored. Also add 'subagent_type' and 'model' fields to
manual task items for per-task role and model control.
Co-authored-by: GLM 5.1 FP8 via Crush <crush@charm.land>
- Add missing AgentMessage dispatch entry in execute_tool_with_enforcer
(tool spec existed but couldn't actually be called)
- Add AgentMessage to Explore/Plan/Verification/general-purpose
allowed_tools lists so sub-agents can communicate
- Add debug logging to resolve_agent_model and load_subagent_model_from_config
to diagnose subagentModel config chain
Co-authored-by: GLM 5.1 FP8 via Crush <crush@charm.land>
- Agent tool is now parallel-safe: multiple Agent calls execute concurrently
- AgentMessage tool: send/read/broadcast between agents via shared mailbox
- TeamCreate rewired to spawn real Agent threads instead of TaskRegistry
- Agents set CLAWD_AGENT_ID env var so they can identify themselves for messaging
- AgentMessage added to Explore/Plan/Verification sub-agent tool lists
- Team manifest persisted to .clawd-agents/teams/{team_id}.json
The /team spec existed in slash_command_specs() but validate_slash_command_input
had no "team" match arm, so /team always produced SlashCommand::Unknown and the
REPL reported "unknown slash command" — even while suggesting /team as a fix.
- Add the "team" => SlashCommand::Team { action } parse arm (action optional:
/team, /team list, /team help). Extra args are rejected like other commands.
- Give SlashCommand::Team a dedicated REPL handler (print_team_command) so
/team prints guidance on the model-invokable TeamCreate/TeamDelete/Cron*
tools and /team list explains that live registry listing is not yet wired
into the CLI layer (the team registry is owned by the in-process tool
executor, not the session/runtime).
- Regression test: assert /team and /team list parse correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AgentOutput has no team_id or task_id fields, but the lane_completion
tests::test_output() helper initialized both. This broke compilation of
the tools lib test crate, which in turn blocked `cargo test --workspace`.
The fields were never present on AgentOutput (team_id/task_id live on
the Task registry entries, not on agent output). Remove the stale
initializers so the test compiles and the lane-completion suite runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fix palette navigation: Up/Down now move selection; only Enter runs the
selected command. Previously any palette key immediately dispatched.
- Add Action::TopCommandsPalette bound to Ctrl+Shift+D. It opens a filtered
palette with high-stakes slash commands: /permissions, /setup, /model,
/env, /plugins, /mcp, /team.
- Capture stdout/stderr while running slash commands inside the TUI so
/status, /cost, /memory, etc. are visible in the conversation pane.
/setup also shows a confirmation message with the new provider/model.
- Empty Ctrl+C opens the full command palette; with text it still clears.
- Document TUI keybindings and palette behavior in USAGE.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- run_tui_repl now treats handle_repl_command's return as a persist hint,
not an exit signal, so /permissions re-enters the TUI instead of returning
to the CLI screen.
- Added Action::RunSlashCommand(idx) and wired it in both component and
legacy TUI dispatch paths.
- Command palette now includes every registered slash command so users can
discover and run /permissions, /theme, /model, etc. from Ctrl+K.
- Updated palette filter test to account for the new slash entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Custom (OpenAI-compat) /setup option was saving kind: openai and
injecting OPENAI_API_KEY / OPENAI_BASE_URL. That collides with users who
have real OpenAI/NeuralWatt credentials in their environment.
Introduce a dedicated custom-openai provider kind that uses its own
environment variables:
- CLAWCUSTOMOPENAI_API_KEY
- CLAWCUSTOMOPENAI_BASE_URL
A new custom/ routing prefix selects the OpenAI-compatible client with
those env vars and is stripped on the wire, so the proxy receives the
bare model id. /setup now saves kind: custom-openai and prompts for the
new env vars. Bare model names saved by /setup are normalized to
custom/<model>.
Manual verification against http://100.96.49.42:4001/v1 succeeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The previous fix normalized bare model names like openclaw to
openai/openclaw so validation passed, but the full prefixed name was sent
as the wire model, causing custom proxies to return 404.
Instead, normalize to local/openclaw. The local/ prefix forces OpenAI-
compatible routing and is always stripped by wire_model_for_base_url(),
so the backend receives the bare model id the user configured while the
CLI still validates and routes correctly.
- Revert validation-time bare-name acceptance (it leaked the user's
OPENAI_BASE_URL env var into unrelated tests).
- Add unit tests for the local/ normalization and config env injection.
- Fix an unused_mut warning in tui/app.rs terminal-guard tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Move config-to-env injection out of AnthropicRuntimeClient::new so
parallel unit tests are not affected by global env mutations.
- Call inject_config_as_env_fallbacks() once at binary startup in run(),
preserving the env-var > .env > stored-config precedence.
- Normalize bare model names (e.g. openclaw) to openai/openclaw when a
custom OpenAI-compatible base URL is configured, so validation passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /setup wizard saves apiKey and baseUrl to ~/.claw/settings.json,
but the API client constructors (OpenAiCompatClient::from_env,
AnthropicClient::from_env) only read environment variables. This caused
saved provider settings to be silently ignored — you'd run /setup,
set a custom URL and API key, and the runtime would still try to use
the default endpoint.
Now AnthropicRuntimeClient::new() calls inject_config_as_env_fallbacks()
before constructing the API client. This function loads the config file's
provider settings and sets the corresponding env vars (OPENAI_API_KEY,
OPENAI_BASE_URL, etc.) only when they aren't already set — preserving
the 3-tier resolution order: env var > .env file > stored config.
This is a process-level env injection (set_var), so it only affects
the current claw process and its children, not the parent shell.
- Populate the TUI conversation pane from existing session messages
when entering via /tui — no more empty TUI
- Add /menu command to return from TUI to the plain REPL
- Add run_repl_from_cli() to resume the REPL with the same LiveCli
- Ctrl+D and /exit both break the loop and exit cleanly
- Ctrl+C clears input and stays in TUI
- Handle MessageRole::Tool in the populate loop
All slash commands now route through SlashCommand::parse() and
LiveCli::handle_repl_command() — the same path as the plain REPL.
This gives the TUI access to all 60+ slash commands instead of just
the 8 that were reimplemented in the TUI dispatcher.
TUI-specific commands (/theme, /keys, /exit) are still handled
locally because they modify TuiApp state directly. Everything else
leave_for_turn → handle_repl_command → reenter_after_turn.
The output from REPL commands appears in the normal terminal while
alternate screen is left, then the TUI re-enters with a clean redraw.
Also: alias now points to our debug build without hardcoding a model,
so the config file's provider is used.
- event_bus is now accessible via event_sender() for background threads
- drain_events() processes TuiEvent variants and updates components:
StreamTextDelta, TurnComplete, TurnError, TurnStarted, etc.
- read_line() drains events on each tick before polling for keys
- capture_turn() adds infrastructure for incremental output capture
- Fix flaky capture test (BufferRedirect shares fd across test threads)
- Current turn loop remains synchronous; background threading is next step
- Adds capture_turn() that wraps a closure with stdout/stderr capture
and calls an on_output callback with captured text
- Infrastructure for the streaming event bridge — when turns run on a
background thread, this will feed incremental output to the TUI
- For now, the turn loop continues to use leave-and-return via
TerminalGuard because CliPermissionPrompter needs stdin access
- RAII guard owns alternate screen + raw mode lifecycle
- Drop guarantees cleanup even on panic — replaces panic hook
- Consolidates 6 ad-hoc methods into leave_for_turn/reenter_after_turn
- main.rs uses guard-based lifecycle instead of suspend/resume
The panic hook in tui_update is no longer needed for cleanup —
the guard's Drop fires on panic and restores terminal state.
- mark input bar turn-in-progress on submit
- re-enable input after turn resumes
- clear input area before redraw
- clear completion popup before redraw
- resume re-enters alternate screen and clears cleanly
This prevents typed text from overwriting itself and stops redraws from
spilling word-wrapped text outside the input box.
The TUI is not active during tool execution. The flow becomes:
1. leave_for_turn() — exit alt screen, enable raw mode
2. cli.run_turn_to() — stdout/stderr goes to normal terminal
3. wait_to_return() — 'Press any key to return to TUI...'
4. reenter_after_turn() — re-enter alt screen, redraw
This guarantees zero output bleeding. Replaces suspend/resume approach.
- Add dependency for safe stdout/stderr capture during turns
- Replace unsafe libc::dup/dup2 stdout suppression with capture_output()
- Tool stdout/stderr is now captured and rendered inside conversation pane
- Add compact StatusBar component for focused layout
- Redraw layout: status bar top, large conversation, compact input,
optional dashboard on the right for wide terminals
- Eliminates output bleeding onto the TUI frame
- 321 tests pass
The Component trait's render(&self) couldn't mutate the cache.
Switched to RefCell<RenderCache> for interior mutability so the
word-wrapped line cache rebuilds on dirty during render().
Also adds Dashboard::clear_dirty() and marks components clean
after each draw_screen() cycle.
THE REAL FIX: Before each model turn, save fd 1 (stdout) via dup(),
redirect it to /dev/null via dup2(), run the turn, then restore the
original fd. This catches EVERY possible stdout write during the turn:
runtime streaming, tool executor formatted output, bash subprocess
inheritance, crossterm escape codes, println! — all silently swallowed.
Previous approaches (emit_output=false, Vec<u8> buffer, screen clear)
were insufficient because the runtime's consume_stream writes to the
real io::stdout() through separate code paths that bypass our buffer.
Changed workspace unsafe_code from forbid→warn, and allow it in
rusty-claude-cli specifically. The dup/dup2/close calls are
single-threaded POSIX syscalls with well-understood semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The run_turn_to buffer + emit_output=false was correctly wired,
but runtime internals (child process stderr, tokio tracing, or
unstructured error paths) can still write to the real terminal fd.
Added a crossterm Clear(All) + MoveTo(0,0) after each turn to wipe
any stdout debris that may have landed on the alternate screen,
followed by a full TUI redraw to re-render both panes cleanly.
This is the belt-and-suspenders approach: buffer captures what we can,
screen clear catches what we can't.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fix failing test_filter_by_label: use 'Z' (matches nothing) and 'Help'
(matches specific entries) instead of 'e' which matched everything
- Replace all 36 hardcoded Color:: in tui.rs and markdown.rs with theme
lookups (theme.conversation_user, theme.dashboard_key, etc.)
- MarkdownRenderer now stores a TuiTheme, render_diff() accepts &TuiTheme
- section() and kv() helpers take theme param for key/color lookups
- draw_left_pane/draw_right_pane use theme for all colors
- push_user_input/push_system_message/push_output read from self.theme
- Only remaining Color:: in production code: Color::Reset (unstyled bg)
and Color::Rgb (dynamic syntect output) — both acceptable
283 tests pass, 0 failures. Clippy clean. Workspace builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
S1-1: Fix ProviderSwap — use suspend()/resume() instead of restore_terminal()
so TUI re-enters cleanly after the setup wizard runs.
S1-2: Fix status message race — remove immediate ds.status_message.clear()
after set_status("Done") so the message is visible until next turn.
S1-4: Bound conversation memory — add MAX_CONVERSATION_LINES (10,000).
auto_scroll() now drains oldest entries and inserts a trim notice.
S1-5: Wire real token tracking — use runtime::UsageTracker::from_session()
to read cumulative TokenUsage (input, output, cache_creation, cache_read)
and pricing_for_model() to estimate cost. No more char÷4 guess.
Sprint 1 of 7. 218 tests pass.
Authored by TheArchitectit
- Create src/tui_update.rs with canonical strip_ansi() (handles CSI, OSC, DCS)
- Add DashboardUpdate struct to decouple from private LiveCli
- Remove duplicate strip_ansi_escapes from tui.rs and strip_ansi from main.rs
- update_dashboard() now delegates to tui_update::update_dashboard_from()
- 12 unit tests for ANSI stripping (CSI, OSC/BEL, OSC/ST, DCS, 256-color, RGB)
Sprint 0, Story S0-1. Pure extraction — no behavioral changes.
Authored by TheArchitectit
Architecture C from expert panel review: add run_turn_to<W: Write>
that writes all output (spinners, markdown, compaction logs) to a
custom writer instead of hardcoding stdout.
In the TUI path, we pass a Vec<u8> buffer. Zero bytes hit the real
terminal during a turn, so nothing bleeds past the conversation pane
boundary into the dashboard. After the turn, the buffer contents are
stripped of ANSI codes and pushed into the conversation pane where
wrap_line() constrains them to the pane width.
Key changes:
- run_turn() is now a thin wrapper: run_turn_to(input, stdout, true)
- run_turn_to<W: Write>(input, out, emit_output) is the real impl
- All println! → writeln!(out, ...), &mut stdout → out
- emit_output=false in TUI mode prevents runtime tool-stream output
- Add strip_ansi() helper for cleaning captured buffer text
- Remove Clear(ClearType::All) from redraw_after_turn (no debris to wipe)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add 'mod tui;' to main.rs (was missing — TUI code was dead)
- Add /tui slash command that switches from the plain REPL to the
split-pane TUI mode at runtime
- Show startup hint: 'Tip: type /tui for the split-pane dashboard view'
- Add run_tui_repl() that manages the TuiApp event loop, captures
assistant output from the session, and feeds it to the conversation pane
- Add update_dashboard() helper that syncs runtime stats (model, session,
turns, provider) into the shared dashboard state
- ProviderSwap and TeamToggle work inside TUI mode
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three fixes for the rendering mess when output goes off-screen:
1. Word-wrap conversation pane lines — long output no longer bleeds
into the dashboard. The wrap_line() function soft-wraps at word
boundaries and hard-breaks long tokens. The viewport math is
updated to count *visual* rows (not logical Line items).
2. Dashboard kv() aligned columns — keys are right-padded to a
fixed 12-char column (KV_KEY_WIDTH) so values line up cleanly
regardless of key length.
3. Context gauge positioned by row tracking, not hard-coded y offset.
The gauge_row counter moves with the section content, so it stays
correct whether git_branch, LSP, or Team sections are shown/hidden.
Also switched dashboard Wrap to trim: false to preserve spacing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Split multi-line user input and system messages on newlines so each
visual line gets its own ConversationLine (same pattern push_output
already used). This fixes text misalignment when Enter is pressed.
- Don't enable Wrap on the conversation Paragraph — the FIFO viewport
counts Line items to fill the pane, so soft-wrapping would throw off
the row count and misalign content.
- Restore non-TUI files from upstream/main to clear conflict artifacts.
- Fix clippy warnings in lsp_client, lsp_process/parse, and trident.
- Add HandlerSwap and TeamToggle match arms in main.rs ReadOutcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Removed app.suspend()/app.resume() calls around REPL TUI turns
- Added assistant message capture from session for conversation pane
- Command errors now show as system messages instead of breaking TUI