feat(opencode): add runtime agent switching (/agent command + AgentSwitcher) with footer agent display - #1666
Open
vicat47 wants to merge 16 commits into
Open
feat(opencode): add runtime agent switching (/agent command + AgentSwitcher) with footer agent display#1666vicat47 wants to merge 16 commits into
vicat47 wants to merge 16 commits into
Conversation
…tion change artifacts
New optional core.AgentSwitcher interface (SetAgent/GetAgent/ AvailableAgents) modeled on ModelSwitcher. opencode implements SetAgent with mutex-protected storage, rejecting internal agent names (compaction/title/summary) and unknown names via a cached opencode agent list enumeration. GetAgent reads the mutable value for StartSession snapshots.
Adds config.SaveAgentName (writes [projects.agent.options].agent, empty value clears the key) and the engine.SetAgentSaveFunc wiring in cmd/cc-connect, mirroring SaveAgentModel/SetModelSaveFunc.
AvailableAgents runs the shared listAgents (opencode agent list), caches the full result for SetAgent validation, and exposes only primary/all-mode agents. SetAgent rejects subagents and unknown names when the enumeration is available, and falls back to rejecting only internal hidden names when enumeration failed (graceful degradation per spec).
…t-config-validation spec
cmdAgent mirrors cmdModel: no-arg shows current agent plus the switchable list (card with select or text with inline buttons); /agent switch <n|name> validates the target against the enumeration, calls SetAgent, persists via agentSaveFunc (rolled back on failure), and cleans up the interactive state so the next message resumes the session with the new --agent. Rejects subagent/unknown targets and replies not-supported for agents without AgentSwitcher. Wires nav:/agent and act:/agent card actions plus /help entries.
Engine unit tests cover the card and text branches, index/name switching, subagent/unknown rejection, enumeration-failure name fallback, persistence-failure rollback, not-supported reply, multi-workspace (no global persist), and nav:/act: card wiring. Adds TestCUJ_F5_AgentSwitchRuntimeAndBack: /agent view -> switch to brainstorm -> next message runs with brainstorm -> view shows new current -> switch back to build, asserting persisted values. Also documents /agent in config.example.toml and marks tasks done.
…mand Bring in the finalized startup agent validation (ValidateConfiguredAgent) and the archived fix-agent-config-validation change (published openspec/specs/agent-config-validation). One conflict in the Agent struct field block (agentName comment alignment) resolved keeping both fields.
AgentSwitcher agents (opencode) now render 'agent · model' in the reply footer. The legacy footer paths (buildReplyFooter, used by non-Claude agents without cache-token signals, and buildClaudeStatusLineFooter) also carry the agent prefix via the new replyFooterDisplayModel helper, fixing the agent name being silently dropped on Feishu cards. Includes regression tests that fail on the pre-fix code: - TestBuildReplyFooter_ShowsAgentPrefix - TestBuildClaudeStatusLineFooter_ShowsAgentPrefix
Fix staticcheck QF1012: sb.WriteString(fmt.Sprintf(...)) in the two /agent list renderers (text and card variants).
Author
chenhg5
approved these changes
Aug 13, 2026
chenhg5
left a comment
Owner
There was a problem hiding this comment.
结论: Approve
总体判断: 一个大但架构清晰的 feature PR。核心设计是 AgentSwitcher interface(与既有 ModelSwitcher 平行),加 opencode 适配 + /agent 命令 + 卡片 UI + 脚注 agent 前缀 + 配置持久化 + 启动校验。所有改动都沿着既有 /model 模式的同构路径走,所以可读性和一致性都很好。建议合入。
Review 范围:
- 看了
core/interfaces.go中的AgentSwitcher定义(27 行,3 方法)。 - 看了
agent/opencode/opencode.go的 SetAgent / GetAgent / AvailableAgents 实现(+111/-6),重点核对锁使用、validation 顺序、subagent 过滤逻辑。 - 看了
agent/opencode/agent_list.go(新增 137 行),opencode agent list子进程调用 + output 解析 + hidden agent 过滤。 - 看了
core/engine.go中新增 cmdAgent / switchAgentOnAgent / resolveAgentSwitchTarget / renderAgentCard / handleAgentCardAction / replyFooterAgent / replyFooterDisplayModel 等 9 个函数(+312/-7)。 - 看了
config/config.go中新增 SaveAgentName / clearProjectAgentOption / removeTomlStringKey(+73/-0),确认用 surgical text edit 保留注释。 - CI: run 31458279197 全绿(lint 1m50s / unit-test 4m25s / smoke 24s / regression 26s / performance 50s)。
✅ 做得好的地方:
- 接口设计清洁 + 与既有 ModelSwitcher 完全平行:
AgentSwitcher三个方法(SetAgent / GetAgent / AvailableAgents)的 contract 明确(空名清空、subagent/internal/unknown 拒绝、枚举失败时 graceful degrade)。这让 owner 后续为 claudecode / codex / cursor 等加 agent 切换是「补一个适配器」的工作量,不是「改 engine」。 - Validation 两层防御:SetAgent 内部先看
agentList缓存(精确拒绝 subagent/internal/unknown),缓存为空时降级到「只拒绝 internal」——这样 configured 值在 CLI 不可用时仍可切换(通过--agent=configured_name让 opencode 自己报错,而不是 cc-connect 误报 invalid)。这是「user config 优先于 cc-connect hard block」的明智设计。 - Cache 完整保存:
a.agentList保存 full enumeration(含 subagent),返回给 engine 的AvailableAgents过滤掉 subagent。这样 SetAgent 收到一个 subagent name 时也能精确拒绝——避免「UI 看不到 subagent 但 user 输名字就能切到 subagent」的 loophole。 - switchAgentOnAgent 的 rollback 设计:save 失败时调用
SetAgent(oldName)恢复——但 rollback 失败时只 log,不 panic(避免 cascading failure)。这是处理「持久化失败 vs 状态不一致」的标准 pattern。 - 启动校验 non-blocking:
validateConfiguredAgentAtStartup只 warn 不 fail——配置错误时让 opencode 自己决定 fallback,不阻塞 cc-connect 启动。这与「配置错的 agent 应该 fail fast vs 配置错的 platform 应该 fail fast」的取舍不同(agent 配置有 CLI default fallback,platform 没有)。 - i18n 完整覆盖:PR body 自述「
/agent命令文案已入 i18n 全语言」+ 实际core/i18n.go+111 行 6 条新 key(MsgAgentNotSupported / MsgAgentDefault / MsgAgentCurrent / MsgAgentListTitle / MsgAgentListUnavailable / MsgAgentUsage)。这个习惯比很多 PR 都好。
🚨/🔴 必须处理:
- 未发现。
🟠 建议改进(不阻塞):
- PR 体积过大:1898 行 / 14 文件。建议 owner merge 后立即打一个 release note:「新增 /agent 命令 + AgentSwitcher interface」——这是 user-visible feature,需要在 v1.4.x release notes 显式列出。
agentListTimeout = 10s:与 model discovery 共用 10s,但 agent list 通常更快(CLI 输出短)。可以考虑 5s,但当前值是 safe default,不改也行。AvailableAgents每次调用都重跑opencode agent list子进程:在 /agent 命令的「无参列表」分支会跑一次。建议加一个短 TTL 缓存(例如 30s),避免 user 在 IM 里反复点 /agent 时频繁 fork 子进程。可在 follow-up 优化。replyFooterDisplayModel的命名:DisplayModel这个名字不够直白——读者会以为它是「显示模型的函数」。看实现是「在 model footer 前加 agent 前缀」。建议改为replyFooterModelWithAgentPrefix或类似。不阻塞,但 future reader 会困惑。cmdAgent的 default 路径:当前只在不支持卡片时走「fetch + render 文本+按钮」分支,但replyWithButtons在某些 platform(lark / wecom)可能不支持 ButtonOption 排版——建议加一个t.Run("platform unsupported buttons", ...)的测试覆盖 replyWithButtons fallback。低优先。
🔵 可选优化:
cmdAgent中 button row 上限是 3(if len(row) >= 3),但 agent 数量可能超过 3,需要多 row——这是 OK 设计,但建议在测试里加一个 5-agent fixture 验证多 row 排版。validateConfiguredAgentAtStartup可以从New()移到 startup hook(让 New() 保持纯构造),但当前实现简洁可读性更好。保留。
Testing / Risk:
- 已看到的验证: 6 个测试文件覆盖 6 个独立维度(agent switch validation、agent list parsing、/agent engine command、CUJ、footer rendering、reply footer);CI 全绿。
- 未覆盖风险: 真实 opencode CLI 不同版本的
agent list输出格式可能变化(regexagentListLineRe假设<name> (<mode>)格式)。建议作者在 follow-up 加一个TestListAgents_VariousCLIVersions用 sample outputs 钉死兼容性契约。 - Blast radius: 改动 core/engine.go 但只新增分支、不改既有路径;
AgentSwitcher是 optional interface,未实现 AgentSwitcher 的 agent 完全不受影响。 - 并发: SetAgent / GetAgent 用 mutex 保护(已读代码确认);AvailableAgents 写入 agentList 也是 mutex 锁内。无竞态。
Next step:
- 建议 owner 直接 merge。这是个 user-visible feature(/agent 命令在 IM 端可用),scope 大但架构干净,测试充分。merge 后强烈建议在 release notes 加 entry。
- post-merge 验证: 用真实飞书 / Telegram / Discord 各跑一次 /agent 命令,确认 card 渲染 + button 点击 → switch agent → next session 用新 agent 启动 → footer 显示新 agent name。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
为 cc-connect 增加运行时 agent 切换能力:opencode 通过
--agent支持多种 agent(build/plan/brainstorm 等),此前无法在 IM 侧切换或查看。本次新增AgentSwitcher接口 + opencode 适配(含合法性和 subagent 校验)、/agent命令(卡片 + 文本双视图)、配置持久化与启动校验,并在回复脚注中显示当前agent · model,让用户在飞书等平台上随时确认/切换会话使用的 agent。Type of change
Testing
Automated tests added in this PR
TestAgentSwitcher_SetAgentAndGetAgent/TestAgentSwitcher_SetAgent_RejectsSubagent/TestAgentSwitcher_SetAgent_RejectsUnknown/TestAgentSwitcher_SetAgent_ClearRestoresDefault/TestAgentSwitcher_AvailableAgents_FiltersSubagentsinagent/opencode/agent_switch_test.go— SetAgent 校验(subagent/未知名拒绝、清空恢复默认)与枚举过滤TestListAgents_*inagent/opencode/agent_list_test.go—opencode agent list解析、失败降级/agent命令 engine + CUJ 测试(core/engine_test.go、core/cuj_test.go)— 切换、回退、枚举为空降级、workspace 隔离TestReplyFooterDisplayModel/TestBuildReplyFooter_ShowsAgentPrefix/TestBuildClaudeStatusLineFooter_ShowsAgentPrefixincore/reply_footer_agent_test.go— 脚注渲染 agent 前缀(三条 footer 路径)For bug fixes only — regression test
TestBuildReplyFooter_ShowsAgentPrefix(脚注丢失 agent 名是本次合入后实测暴露的问题)replyFooterDisplayModel退化为replyFooterModel模拟修复前代码,回归测试如预期失败;恢复后通过Critical User Journeys (CUJ) impact
/agent切换属于会话 agent 生命周期)/agent卡片、脚注 agent 前缀)go test ./core/ -run TestCUJpasses locally(含/agentCUJ)Manual / user-visible behavior change
/agent查看可选 agent 列表并切换(卡片含按钮 / 文本命令两种形态)/agent空参显示当前 agentagent · model(如brainstorm · deepseek/deepseek-v4-pro),非 AgentSwitcher agent 不变Checklist (reviewer will verify)
go build ./...passes(cmd/cc-connect需-tags no_web,仓库缺web/dist,环境既有问题)go test ./...passes(同上,-tags no_web下全过)core/(/agent命令通过AgentSwitcher接口能力断言,无硬编码)/agent命令文案已入 i18n 全语言)Related