From Chat UI to Agent Workbench: How the Interaction Layer of a Terminal Coding Agent Upgraded

🇨🇳 中文版

Origin: From the Codex Open Source to Rust + TUI

Let me first explain where this project comes from. resolve-tui (repo: github.com/erishen/resolve-tui) is a pure-Rust, TUI-based terminal coding agent, and its starting point was reading OpenAI's open-source implementation of Codex — the realization that "terminal + natural language" can drive an agent that writes code. What struck me wasn't how strong a model it used, but that it turned the interaction into a workbench rather than a chat box: the agent reads files, runs commands, and changes code directly, and what you watch is it working in your project, not just a stream of answers.

That form fascinated me. I decided to build my own in Rust + TUI. I had earlier written a Python harness (resolve-harness) that validated the "never call a model when code can compute it" mechanics; but the Rust version is not a port of it — it's an independent rewrite that references openai/codex's "agent loop + tools + model client" three-layer architecture, reimplementing the deterministic fast path, codegen cache, agent main loop, and sandbox isolation in Rust (that's exactly how lib.rs divides things: model / llm / tools / sandbox / sessions / agent), and adding the skills system and MCP integration on top, finally wrapping it in a terminal interface with ratatui.

The resolve-harness article covered the Python version's "never call a model when code can compute it" — fastpath short-circuits, codegen lets the model write its own detectors, PSE multi-agent orchestration. This article is about what the Rust resolve-tui adds beyond those mechanisms: how the interaction layer upgraded from "chat" to "workbench" — the event loop, built-in tools and their safety boundary, the skills system, MCP integration, and the command-line interaction patterns.

Common Practice: Chat UI Is the Default Answer

If you ask most AI coding assistants how they interact with users, the answer is almost always: a chat interface (Chat UI). You type a question in a dialog, the reply streams in, and you can copy, scroll back, and render images and tables. It's also the most mainstream form today — ChatGPT, Claude, and Cursor all do this.

The choice has good reasons. Chat is the most familiar interaction paradigm, with zero learning cost: you ask, it answers, you take away the useful parts. For advisory questions like "how do I implement an LRU cache" or "what's wrong with this code," Chat UI is perfect — the answer is the product, and the model turns knowledge into text for you.

But when the people you serve are programmers and the assistant is expected to actually modify code, Chat UI exposes a structural misalignment: it positions the agent as an "advisor," not an "executor."

In a Chat UI, a "help me refactor this function" conversation looks like this:

  1. You copy your code into the dialog;
  2. The model returns a refactor suggestion;
  3. You copy the suggestion back into your editor and apply it yourself.

The problem isn't the chat box itself — it's that the agent and your work are separated: the agent lives in the conversation, your code lives in your editor, and the two talk through copy-paste. So the agent only ever offers "suggestions," never actually "does" anything. Across multiple iterations, that moving loop repeats every round — context shuttles between two windows, but the hand that changes the code is always yours.

This is really the difference between two interaction models, not between two frontend technologies:

  • Chat UI: the agent answers questions in a conversation, isolated from your filesystem. It's Q&A.
  • Agent Workbench: the agent acts in your project — reads files, runs commands, changes code, verifies results. It's execution.

What resolve-tui does is land the latter — the Agent Workbench — in the terminal. The terminal is natively a programmer's workbench: the filesystem, the shell, and the editor are all inside it, so the agent can read, write, execute, and verify in the same context as the work you're doing. This isn't "using the terminal against the web" — it's "upgrading the agent from advisor to executor." The rest of this article is about how I made that upgrade usable, extensible, and controllable in Rust + TUI.

Questioning

Wait, hold the applause.

"Upgrading the agent from advisor to executor" sounds beautiful, but this upgrade path has one glaring objection: when the agent starts actually doing things, its mistakes also upgrade — from 'wrong advice' to 'a broken project.' I have to honestly lay out a few counter-examples.

First, an agent that acts = bigger risk. In a Chat UI, when the agent says something wrong, you still have a chance to judge before copy-pasting; in an Agent Workbench, the agent directly calls write_file and shell — if it misjudges, files get overwritten and commands produce side effects. The cost of an error goes from "a useless answer" to "a corrupted repo." This is the question any upgrade must answer: why trust the agent to act? The only answer is a safety boundary, not "the model is smart."

Second, more tools = less predictability. The Workbench's selling point is extensibility — built-in tools, skills, and MCP servers can all be mounted. But every added tool widens the agent's decision space and makes its behavior harder to predict. An agent that can read files, write files, run commands, and call external services is an entirely different order of uncontrolled potential than an agent that "only talks" in a chat box.

Third, interaction complexity goes up. A Chat UI has one input box; a Workbench has hotkeys, slash commands, approval flows, session management, and sandbox workspaces. All of this must be learned, maintained, and debugged. If that complexity doesn't buy real execution capability, it's pure burden.

If these counter-examples hold, is "Agent Workbench" over-engineering — making a simple thing complicated?

My answer: these risks are real, but they are controllable — while "the agent can only be an advisor and never truly act" is a structural ceiling of Chat UI. The question isn't "is the Workbench always better than Chat," it's "when the agent needs to actually change code, are you willing to trade a set of safety boundaries for an executable workbench." In the Alternatives section I'll show how I used the event loop, command panel, tool policy, and skills/MCP systems to keep the Workbench's risks under control and amplify its execution capability beyond what Chat UI can provide.

Alternatives

Going back to the starting point, I actually considered three paths.

The first path was staying with a Chat UI: a web chat box, type a question, stream the answer. This is the smoothest path because the chat paradigm is mature, and I have built such things. But as covered above, it can't escape that structural misalignment — the agent is separated from your work, only an advisor, never an executor. And it often adds a layer of "inter-process communication": the agent engine and the UI must serialize, transmit, and deserialize, each step adding complexity.

The second path was pure CLI: resolve-tui "task" runs once and exits. Great for single-shot tasks, but there's no interaction: you can't follow up, scroll history, approve tool calls, or iterate. A CLI is "one-shot," while coding is a "continuous" process — you want to watch it, adjust it, and accept its work, all of which need a persistent interface.

The third path is what I ultimately took: an Agent Workbench — a persistent, interactive terminal interface. It takes the intersection of "terminal-native" and "interactive": still a terminal (no browser, no IPC), but with an event loop running (multi-turn conversation, approval, history scrolling), and the agent acting directly in your project. That's the form of resolve-tui, and it's where the "workbench" upgrade lands.

At the heart of the TUI is an event loop that wires three things together: keyboard input, the agent event stream, and timer refresh.

let mut reader = EventStream::new();
let mut tick = interval(Duration::from_millis(80));
loop {
    tokio::select! {
        maybe = reader.next() => {
            if let Some(Ok(ev)) = maybe {
                handle_key(ev, &mut app, &cmd_tx, &approval_tx);
            }
        }
        Some(ev) = rx.recv() => {
            app.on_event(ev);
        }
        _ = tick.tick() => {
            app.ticks = app.ticks.wrapping_add(1);
        }
    }
    terminal.draw(|f| ui(f, &mut app))?;
    if app.should_quit { break; }
}

This code hides several key TUI decisions:

  • EventStream is an async keyboard source: turning crossterm's raw event stream into a stream tokio can select on, so the UI never blocks on a stuck read.
  • rx is the agent event channel: the agent task pushes AgentEvents (Token / ToolCall / ToolResult / System / Error…) through an mpsc::unbounded_channel; the UI updates the screen incrementally via on_event. UI and agent are two tasks in the same process communicating through a channel — avoiding network IPC and protocol-level serialization overhead.
  • tick is an 80ms refresh timer: for time-driven animation like cursor blink, and to keep tokio::select! waking periodically even with no events.

The result is a continuous-conversation terminal interface: input at the bottom, history above, agent streaming tokens, tool calls and results distinguished by color in the history.

Command-line interaction patterns: hotkeys and the command panel

A TUI has no mouse, so interaction converges on two entry points: hotkeys (acting on the current focus) and slash commands (acting on the session). The command surface is the batch listed by /help:

Commands:
  /list              List archived sessions (git-stash style)
  /create [name]     Archive the current conversation as a session and start fresh
  /apply <index|name>  Load a session to continue (same as /load)
  /save [name|path]  Snapshot the current conversation (non-blocking)
  /clear             Clear the current conversation
  /rm <index|name>   Delete a session
  /model [name]      Switch model (no arg shows current)
  /pse [on|off]      Toggle multi-agent role-triangle mode
  /sandbox [clean]   Inspect sandbox workspaces / clean task dirs
  /reasoning         Toggle reasoning display (or Ctrl-R)
  /export [path]     Export the current conversation as Markdown
  /tools [on|off name]  Inspect / toggle tools (built-in + MCP)
  /skills [reload]   Inspect skills; reload hot-reloads the skills dir
  /remember [fact]   Long-term memory: no arg to view; with arg to append
  /examples          Print tool usage examples (Markdown)
  /mcp [add|remove|reload]  MCP status / attach dynamically and persist
  /quit | /exit | /q  Quit (bare q / exit / quit also work)
Hotkeys:
  Enter submit · PageUp/PageDown history · Ctrl-R reasoning · Ctrl-Y copy answer
  Esc abort generation while running · Esc / Ctrl-C quit when idle

These commands aren't decoration; they're interactions that only exist in a terminal:

  • /model: switch models at runtime without restart or editing config files. Because model is an Arc<Mutex<String>> shared between UI and agent, changing one value makes the next turn use the new model.
  • /pse: toggle single-agent / multi-agent role-triangle mode at runtime. pse is a shared Arc<AtomicBool>; the UI flips it, and the agent side switches to submit_roles on the next turn. Zero reconnect/restart cost in the architecture — that's the benefit of two tasks in one process sharing state.
  • /sandbox clean: clean all task workspaces. The sandbox root lives under the project, and one prune_task_workspaces call reclaims disk.
  • /export: export the current conversation as Markdown for archiving or pasting into docs.

Meanwhile Enter submit, Esc cancel, PageUp/PageDown history, and Ctrl-Y copy upgrade "single-shot submission" into a full conversation you can control, review, and interrupt. Especially Esc cancel: the agent task and the UI share the same cancel: Arc<AtomicBool>; pressing Esc while running sets the flag, and the drive loop checks it every iteration and aborts immediately — in a pure chat interface, you'd usually have to wait for it to finish the current reply, or kill the session outright.

Built-in tools: the agent goes from "advisor" to "executor"

If the TUI solves "split context," the tool system solves "vague permissions." resolve-tui ships four built-in tools, all executed inside the sandbox:

pub fn builtin_tools() -> Vec<ResponseTool> {
    vec![
        tool("shell", "Execute a shell command in the sandbox, returning stdout/stderr.", ...),
        tool("read_file", "Read a text file's contents.", ...),
        tool("write_file", "Write (overwrite) a text file. Paths are relative to the current sandbox workspace…", ...),
        tool("list_dir", "List the entries of a directory.", ...),
    ]
}

The point of this four-piece set: the agent can read files, write files, run commands, and list directories directly in your project — instead of handing you code "please go apply yourself" in a chat box. It shares the filesystem with the work you're doing.

But "direct operation" also means "it could break things," so every tool is constrained by SandboxPolicy:

  • write_file / shell can only land in the current task's workspace (<sandbox_root>/task-<nanos>-<pid>/); nothing writable outside it.
  • read_file / list_dir can only read the project directory + workspace + system temp dir; paths outside are refused — keeping sensitive files like ~/.ssh from being read into context and exfiltrated.
  • shell runs under macOS sandbox-exec / Linux bwrap isolation, offline by default, writes limited to a whitelist.

Each task gets an independent workspace; tasks never overwrite each other. The system prompt injects "current writable workspace + whitelist + read scope," so the model doesn't guess paths.

One more engineering detail: output truncation. A single cat big-file could instantly blow up the model context, so single tool outputs are capped at 8KB, keeping head and tail with a marker in between:

pub(crate) fn truncate_output(s: String) -> String {
    let total = s.chars().count();
    if total <= MAX_TOOL_CHARS {
        return s;
    }
    let keep = MAX_TOOL_CHARS / 2;
    let head: String = s.chars().take(keep).collect();
    let tail: String = s.chars().skip(total - keep).collect();
    format!("{head}\n…[output too long, truncated: ~{total} chars omitted in the middle]…\n{tail}")
}

Truncating by chars rather than bytes avoids splitting multi-byte CJK — an agent that writes Chinese should have tools that respect UTF-8.

Skills: extending agent ability with plain text

Built-in tools are fixed, but an agent's "domain capability" should be pluggable. That's the resolve-skills skill pack — a set of <skill>/SKILL.md prompt packs aligned with the Agent Skills open standard:

---
name: rust-review
description: Rust code review
triggers: review, code-review
---
1. Check ownership
2. Check error handling
…

A skill is "front-matter metadata + instruction body," possibly with scripts/, references/, and assets/ directories. The loading strategy is adaptive:

  • Skills with triggers only inject their body into the current turn's system prompt when the user input hits a keyword (saving tokens);
  • Skills without triggers are "model-chosen," their body resident, applied at the model's discretion.
pub fn prompt_appendix(skills: &[Skill], user_text: &str) -> Option<String> {
    let mut parts = Vec::new();
    if let Some(idx) = index_prompt(skills) {
        parts.push(idx);
    }
    parts.extend(active_bodies(skills, user_text));
    ...
}

What's resident in every turn is only the index (name + description + trigger words, saving tokens); the body is injected only on a hit. That way dozens of skills don't bloat every request's system prompt.

The skill directory lookup order is interesting too — $HARNESS_SKILLS_DIR → current dir .resolve-tui-skills/ → bundled resolve-skills/skills/ (git submodule) → install-dir fallback. In other words, the skills are yours: drop a .resolve-tui-skills/ into your project and the agent auto-discovers it, no code changes. In the TUI, /skills reload hot-reloads — edit a skill and it takes effect immediately.

More crucial is the contract design: parse_skill ignores all unknown front-matter keys (when_to_use, allowed-tools, agents — fields from Claude Code / Codex). That means skill packs produced by Claude Code or Codex load into resolve-tui with zero modification — the skill ecosystem is interoperable.

MCP: wiring the agent's toolbox to the outside world

Built-in tools cover files and shell, but a real coding agent needs more: GitHub, databases, browsers… reimplementing each protocol by hand is untenable. That's what MCP (Model Context Protocol) solves — tools as a service, letting the agent mount any server's capabilities over standard JSON-RPC.

resolve-tui embeds a minimal MCP stdio client:

pub struct McpManager {
    clients: Vec<McpClient>,
    tools: Vec<ResponseTool>,                        // remote tools merged into the LLM tool list
    routing: HashMap<String, (String, String)>,      // exposed name → (server, original tool)
    status: Vec<String>,
}

On startup, connect_all spawns each configured server as a child process, speaking newline-delimited JSON-RPC 2.0: initialize handshake → notifications/initializedtools/list to fetch tools, then remote tools are merged into the LLM tool list under the exposed name mcp_<server>_<tool>; on invocation the name is routed back to the server's tools/call:

pub(crate) fn exposed_name(server: &str, tool: &str) -> String {
    format!("mcp_{}_{}", sanitize_name(server), sanitize_name(tool))
}

Three design points worth noting:

  • Failure isolation: a single server that fails to connect just records status and is skipped, without blocking overall startup — /mcp shows each server's connection state.
  • Dynamic mounting: /mcp add fs npx -y @modelcontextprotocol/server-filesystem /path mounts a new server at runtime and persists the config into config.toml (preserving the file's formatting, only appending the [mcp_servers.<name>] section). /mcp remove detaches and syncs the config removal. No restart, no hand-editing files.
  • Exposed-name dedup: same-named tools across servers — first come, first served; conflicts are dropped with a warning.

MCP's significance: the agent's toolbox is no longer a fixed four, but extensible. Mount a GitHub server and the agent can open issues; mount a database server and it can query tables. The philosophy is the same as skills — capabilities are pluggable, and the user decides what the agent can touch.

Sessions & memory: the terminal is a "persistent workbench"

A TUI is persistent, so it naturally carries session management. resolve-tui has a git-stash-style session system:

  • Auto-archive on exit: the agent task saves the current conversation to last.json before exiting.
  • Auto-resume on startup: if last.json exists and no explicit --resume was given, resume automatically — "exit saves, start restores."
  • /list /create /apply /save /load /rm: git-stash-style session management; the CLI side has --resume list to list and --resume <index|name|path> to load.

Together with /remember long-term memory (written to MEMORY.md in the system config dir, permission 0600), the agent can remember your conventions across sessions, like "always run cargo fmt before deploying."

Evidence

The TUI interaction layer is not a "working demo" but an engineering effort with clear boundaries, tests, and extensibility. The evidence comes from four directions: the tools' safety boundary, the skills' pluggable contract, MCP's failure isolation, and the complete command-line interaction loop.

Evidence 1: the tools' safety boundary is "provable," not "verbal"

write_file dares to let the agent write to disk because every tool call passes SandboxPolicy validation first:

"write_file" => {
    let raw = str_arg(&args, "path")?;
    let path = policy.resolve(std::path::Path::new(raw));
    let content = str_arg(&args, "content")?;
    if !policy.is_writable(&path) {
        return Err(HarnessError::tool(format!("path not in writable whitelist: {path:?}")));
    }
    ...
}

If the path isn't in the whitelist, it returns an error without writing. read_file likewise checks is_readable. This boundary doesn't rely on "model discipline" — it's enforced by the policy layer. If the model tries to read ~/.ssh/id_rsa, is_readable refuses.

Evidence 2: the skills' "zero-modification compatibility" is testable

parse_skill's handling of unknown front-matter keys is pinned down by a test:

#[test]
fn ignores_unknown_frontmatter_keys() {
    let content = "---\nname: foo\ndescription: demo\nwhen_to_use: during review\nallowed-tools: [Read, Grep]\nagents: openai.yaml\n---\nbody X\n";
    let s = parse_skill(content, "fallback").expect("unknown keys must not fail");
    assert_eq!(s.name, "foo");
    assert!(s.body.contains("body X"));
    ...
}

Foreign fields like when_to_use, allowed-tools, and agents are ignored and the skill still parses. This test proves resolve-skills is interoperable with Claude Code / Codex skills — not as a claim, but guaranteed by a regression test.

Evidence 3: MCP's failure isolation is end-to-end verified

#[test]
fn bad_server_does_not_block_good_ones() {
    let status = mgr.status_lines();
    assert_eq!(status.len(), 2);
    assert!(status[0].contains("connect failed"), "bad server must be skipped: {status:?}");
    assert!(status[1].contains("connected"), "good server must be unaffected: {status:?}");
}

A bad server not blocking startup is an explicitly asserted behavior in a test, not a coincidence.

Evidence 4: the command-line interaction loop is wired end-to-end, from startup to exit

main.rs lays out the entry points clearly:

// Multi-agent switch: override config to enable the role-triangle orchestration
let config = if multi_agent_flag { ... };
// `--resume list` lists sessions then exits (git-stash style)
if resume.as_deref() == Some("list") { print_session_list(); return; }
// Explicit --resume forces TUI mode
let tui = tui_flag || resume.is_some();
  • CLI mode: resolve-tui "task" runs once and exits — for scripts and single-shot tasks.
  • TUI mode: resolve-tui --tui (or with --resume) enters the interactive interface for multi-turn conversation, approval, and history.
  • --multi-agent: one flag starts PSE role-triangle mode.
  • codegen list/delete/clear: manage cached detector plugins (the Rust implementation of the mechanism covered in the resolve-harness article, mentioned here only in passing).

Plus a panic hook that restores the terminal on crash (leaving the alternate screen + disabling raw mode); otherwise the whole terminal would be left broken after a crash — a "factory detail" that only TUI projects have.

These snippets together answer a question: why can a terminal TUI be the primary interface of an AI coding assistant? Because every capability has code behind it: the event loop catches interaction, the tool policy holds the safety line, the skill contract guarantees interoperability, MCP makes capabilities extensible, and the session system keeps the workbench persistent. It's not "something that can't be done" or "barely usable" — it's engineering with clear boundaries.

Boundaries

Conclusion first: this "Agent Workbench" interaction design is not universally applicable. Its premise is — your users are programmers, the agent will actually change code, and interaction is keyboard-driven. Once the premise changes, going back to Chat UI is the more reasonable choice.

Let me lay this boundary out.

First, where the output is primarily "answers," Chat UI is still better. If the agent's role is to give you an explanation, a plan, or a code snippet (advisory questions), the chat interface is the right form — lightweight rendering, zero learning cost, and the Workbench's complexity is just burden. The Workbench's value is in "the agent acts," not "the agent talks."

Second, where users aren't programmers / aren't terminal-familiar, Chat UI is a must. Hotkeys, slash commands, raw mode, alternate screens — these are things terminal users take for granted and regular users find baffling. For AI assistants aimed at non-technical users, the chat interface is right.

Third, in pure-automation scenarios, the Workbench gives way to the CLI. If the agent is just one step in a pipeline (running a lint fix in CI, batch-processing files), interaction itself is redundant — a single resolve-tui "task" is more appropriate. The Workbench's persistent interaction only serves "human-in-the-loop" scenarios.

Where the Workbench is the right call has a clear profile:

  • Users are programmers, and their workbench is already the terminal;
  • The agent needs to read files, write files, run commands — in the same context as the user;
  • Multi-round iteration, mid-way cancel (Esc), and tool-call approval are needed;
  • External capabilities (MCP servers) and domain knowledge (skills) need to be mounted dynamically.

My judgment: "AI coding assistant" usually refers to programmers changing code, so it most likely falls in the Agent Workbench's territory — but this isn't absolute. If what you're building is actually an "AI Q&A assistant" or "AI analytics/reporting assistant," please don't copy my choice — your users and your output form determine that you should stay in Chat UI.

Let me also mention a few hard boundaries specific to a terminal Workbench — constraints you must accept if you choose it.

The first is terminal crash recovery. Once raw mode is on, if the program crashes without restoring, the entire terminal is left broken. So main.rs installs a panic hook:

let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
    crossterm::terminal::disable_raw_mode().ok();
    crossterm::execute!(
        std::io::stdout(),
        crossterm::terminal::LeaveAlternateScreen,
        crossterm::event::DisableBracketedPaste
    ).ok();
    default_hook(info);
}));

This isn't optional robustness — it's a hard requirement; one crash would otherwise destroy the user's entire terminal session.

The second is the watchdog. If the agent task panics and crashes, the UI side must be notified and reset the "running" state; otherwise the input box is stuck in running mode forever (Esc is blocked by running), and the user can only Ctrl-C out:

tokio::spawn(async move {
    if let Err(e) = agent.await {
        let _ = watchdog_tx.send(AgentEvent::Error(format!("agent task exited abnormally: {e}")));
        let _ = watchdog_tx.send(AgentEvent::Finished);
    }
});

The third is output pollution outside the alternate screen. Once the TUI enters the alternate screen, anything printed directly to stderr will "flash" on exit. So startup diagnostics (config warnings, skill-load warnings, .env permission hints) are collected into startup_notes and shown as system messages inside the TUI, rather than printed to stderr.

Finally, let me ground this boundary in a concrete judgment: when should you step back from the Agent Workbench to Chat UI?

The answer is simple — when what you want isn't "the agent acts," but "the agent answers." If your usage falls back to "copy-paste context, take the advice, apply it yourself," then the Workbench's execution capability isn't being used, and the lightness of Chat UI fits better. But for my scenario (local, single-user, programmer, high-frequency, touching code), the Workbench isn't nostalgia; it's a choice made after doing the math.

Source Code Navigation

Why upgrade from Chat UI to an Agent Workbench?

Because the users of a coding assistant are programmers, their workbench is the terminal, and the agent must actually change code. In a Chat UI the agent is separated from your work and can only be an advisor (copy-paste, apply yourself); a Workbench puts the agent and user in the same filesystem context, letting the agent read files, run commands, and change code directly, with shared Arc<AtomicBool>/Arc<Mutex> giving zero-IPC control over cancel and model switching. For pure-answer, non-programmer, or pure-automation scenarios, stay in Chat UI or CLI.

What's the relationship between resolve-tui and resolve-harness?

They are two independent projects, not a “engine + UI” dependency. resolve-harness is a Python (LangGraph/LiteLLM) implementation that validated the “never call a model when code can compute it” mechanics; resolve-tui is a Rust implementation that references openai/codex’s architecture, reimplementing fastpath/codegen/agent main loop/sandbox in Rust, and adding the skills system and MCP integration. They share design philosophy but are separate codebases.

What does the TUI interaction pattern look like?

Two entry points: hotkeys (Enter submit, Esc abort/quit, PageUp/PageDown history, Ctrl-R reasoning, Ctrl-Y copy) and slash commands (/model switch model, /pse toggle multi-agent, /sandbox clean workspaces, /skills reload skills, /mcp add/remove mount MCP servers, /export session, etc.). While running, Esc immediately aborts the agent via a shared cancel signal.

How do the built-in tools guarantee safety?

All four tools (shell/read_file/write_file/list_dir) are constrained by SandboxPolicy: writes only land in the current task workspace; reads are limited to project+workspace+temp dirs; shell runs under sandbox-exec/bwrap isolation, offline by default. Path validation is enforced by the policy layer, not model discipline. Outputs over 8KB are truncated keeping head and tail.

How do skills work?

A skill is a <skill>/SKILL.md prompt pack (front matter + body), aligned with the Agent Skills standard. Adaptive activation: skills with triggers inject their body only on keyword hit (saving tokens); trigger-less skills stay resident. Unknown front-matter keys are ignored, so Claude Code/Codex skill packs load with zero modification. Directory lookup: $HARNESS_SKILLS_DIR → project .resolve-tui-skills/ → bundled submodule → install-dir fallback.

How is MCP integration used?

resolve-tui embeds a minimal MCP stdio client; on startup it spawns configured servers (initialize → tools/list → exposed as mcp__) and routes calls back to tools/call by exposed name. A single failing server doesn’t block startup; /mcp add mounts at runtime and persists config to config.toml; /mcp remove detaches and syncs config removal.

How are sessions and memory managed?

A git-stash-style session system: auto-archive to last.json on exit, auto-resume on startup; /list /create /apply /save /load /rm manage sessions; –resume list lists and –resume <index|name|path> loads. /remember writes long-term memory to MEMORY.md in the system config dir (permission 0600), effective across sessions.

Comments

Leave a reply

Your email address will not be published. Required fields are marked *

AI Engineering Practices & Open Source Projects

Shop Web Chat Nsbp About Privacy

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号