我为什么没做另一套 AI 聊天界面,而是给 Codex / Claude Code 做了个本地控制台

我为什么没做另一套 AI 聊天界面,而是给 Codex / Claude Code 做了个本地控制台

只开一个 coding agent 的时候,一个终端标签就是最好的界面。

开到五个、十个,散落在几个项目里,问题就变了。你不再是在跟一个 Agent 对话,而是在管理一组处于不同状态的工作:一个还在跑,一个在等你确认,一个已经失败,还有一个十分钟前就跑完了,但那个标签页被压在最后面。

我想解决的是一个很具体的问题:**在不改变 Codex 和 Claude Code 使用方式的前提下,拿到一个跨会话视图。 **

边界:不重建 provider 的 UI

Agent Console 没有做统一聊天前端。它扫描 provider 已持久化的会话,按 workspace 分组,归一化成 working / waiting / idle / failed 四种状态,需要进入时调用 provider 自己的 resume 流程。进去之后,你面对的仍然是原生的 Codex 或 Claude Code 终端界面。

这个边界很重要。统一前端看起来整齐,但它会落后于 provider 的新能力,并且把 resume 行为、快捷键、审批流、渲染细节都变成第二套兼容层。而原生界面之上那一层 ------ 发现、状态、提醒、搜索、导航 ------ 小到可以做对。

Rust 这边刻意保守:ratatui 渲染,portable-pty 管子进程,vt100 做保留 pane 的终端模拟,rusqlite 存状态,完全没有 async runtime。整个程序就是一个同步循环,加上若干持有 PTY 的线程。

做下来学到的四件事

1. 发现会话 = 解析别人的私有格式

两家都没有「列出我的会话」这种 API,但都会持久化 transcript。所以发现会话意味着去读 ~/.codex/sessions/**/rollout-*.jsonl~/.claude/projects/**/<uuid>.jsonl,从一堆从没打算当公开接口的记录里反推结构。

当观测通道可以,当契约就很糟 ------ provider 每次发版都可能挪字段。应对办法是:除了一张表,任何地方都不再按 provider 分支。

rust 复制代码
pub struct ProviderAdapter {

    pub kind: AgentKind,

    /// 该文件是否属于这个 provider 的会话 transcript

    pub accepts: fn(&Path) -> bool,

    /// 解析一个 transcript。Ok(None) 表示「不是可用的会话」

    pub parse: fn(&Path) -> io::Result<Option<Session>>,

    /// 可选的 provider 专属增强

    pub enrich: Option<fn(&Path, &mut [Session])>,

}

发现逻辑遍历这张表,而不是在五个地方 match 枚举:

rust 复制代码
for adapter in providers::enabled() {

    let root = paths.root(adapter.kind);

    let files = provider_files(root, adapter.accepts);

    let mut parsed = parse_cached_files(files, adapter, cache);

    if let Some(enrich) = adapter.enrich {

        enrich(root, &mut parsed);

    }

    sessions.extend(parsed);

}

用裸 fn 指针而不是 Box<dyn Fn>,是为了让这张表能是 const ------ 加一个 provider 就是加一条表项,零分配。AGENT_CONSOLE_PROVIDERS=codex 可以在运行时裁剪同一张表,这在两家都开始自带多会话视图、某一侧可能变成重复劳动的当下挺有用。

我最在意的失败模式是:这个变量打错字,绝不能变成一个静悄悄的空面板。所以无法识别的值会记录原因并保持全部 provider 启用。

2. 持有 PTY 就要决定谁跟 TUI 一起死

最直觉的设计是从 TUI 进程里 spawn agent。那样关掉面板就杀掉所有 agent ------ 而这恰恰是让人宁愿开十二个终端标签的原因。

所以托管的 PTY 放在一个独立的常驻 daemon 里。关闭或崩溃 TUI 只是 detach,新的 TUI 重连并回放一段有界的 tail。不依赖 tmux,也不强制 git worktree ------ 会话就在原地跑。

原地跑意味着两个面板可能同时 resume 同一个 provider 会话、写坏同一份 transcript,所以每个 provider session ID 都由跨进程 lease 保护,带持有者信息、安全拒绝和显式强制接管。这块恰恰是 Rust 帮助最小的地方:类型系统对「同一台机器上的第二个进程」无话可说,靠的是进程卫生。

3. 让模型做摘要,不做判断

working / waiting / failed 来自 provider 的 hook 事件和进程状态,用固定优先级确定性归约。模型只负责把长对话压成一眼能扫的任务摘要,它走会话自己的 provider、在 coding conversation 之外运行,并且能用 AGENT_CONSOLE_SUMMARIZER=off 完全关掉。

还有一个只有真跑起来才会暴露的细节:alert 是「运行期观测到的状态跃迁」,不是「状态」。面板启动时就已经在 waiting 的会话,不算新消息。这个区分搞错,提醒列表一天之内就会变成噪音。

4. 时间都花在终端模拟上

pty.rs 有 6,600 行 ------ 是最大的模块,比 discovery、状态机和整个 UI 加起来还大。每个 pane 的保留回滚、给需要的 provider 做鼠标上报、alternate screen 处理、选择与剪贴板、resize、重连回放,每一项看起来都很小,实际都不小。

两个具体教训。托管的 Codex 会话用 --no-alt-screen 启动,这样 workspace pane 在 Codex 的任何状态下都能保留并滚动 transcript。以及 ------ 在 Codex 和 Claude Code 各自占掉自己的键之后,真正空闲的 Ctrl 组合只剩三个Ctrl-\Ctrl-^Ctrl-Q,其余全部原样转发给子进程。

目前状态

v0.0.11,支持 Codex 和 Claude Code,提供 macOS / Linux / Windows 包,macOS 二进制已签名并通过 notarization。

sh 复制代码
cargo install agent-console

agent-console doctor   # 检查 provider、hook、剪贴板、daemon、权限

agent-console

MIT OR Apache-2.0 双授权。源码:github.com/buhuipao/ag...

还很早期。我关心的问题比「再加十个 provider 图标」更基础:状态是否可信、提醒是否真的减少了切换、Shell 是否留在正确的 workspace、原生 resume 是否足够顺滑。

如果你每天同时跑好几个 coding agent ------ 跨会话控制层应该给你看什么,又绝不该接管什么?

相关推荐
晴天164 小时前
AI应用开发(Claude Code的技术原理)-Day03
人工智能·ai编程
沸点小助手4 小时前
掘金VibeLaunch 沸点秀获奖公示🎊
aigc·ai编程
9i编程4 小时前
AI BI Helper 开发实录 01:文本向量与 Milvus 向量数据库集成
人工智能·openai·ai编程
leeyi4 小时前
Document 组件源码:Loader / Transformer / Parser 为什么分成三个接口(第67篇-E53)
aigc·agent·ai编程
唐老板4 小时前
Prompt 已死,Loop 永生
ai编程
ClouGence5 小时前
Kimi 暂停新订阅后,如何用上 Kimi K3?
aigc·ai编程
AI编程实验室5 小时前
用 npm + Three.js 做一颗西瓜:把夏天的清凉感放进浏览器
前端·后端·ai编程
JavaGuide5 小时前
我最推荐的 4 个 AI 编程 Skills:grill-me、research、diagnosing-bugs、code-review
前端·后端·ai编程
神奇霸王龙5 小时前
Qwen3.7-Max屠榜:推理成本仅GPT-5.5的1/25
人工智能·python·gpt·ai·aigc·ai编程