Skip to content

feat(opencode): add runtime agent switching (/agent command + AgentSwitcher) with footer agent display - #1666

Open
vicat47 wants to merge 16 commits into
chenhg5:mainfrom
vicat47:main
Open

feat(opencode): add runtime agent switching (/agent command + AgentSwitcher) with footer agent display#1666
vicat47 wants to merge 16 commits into
chenhg5:mainfrom
vicat47:main

Conversation

@vicat47

@vicat47 vicat47 commented Aug 11, 2026

Copy link
Copy Markdown

Summary

为 cc-connect 增加运行时 agent 切换能力:opencode 通过 --agent 支持多种 agent(build/plan/brainstorm 等),此前无法在 IM 侧切换或查看。本次新增 AgentSwitcher 接口 + opencode 适配(含合法性和 subagent 校验)、/agent 命令(卡片 + 文本双视图)、配置持久化与启动校验,并在回复脚注中显示当前 agent · model,让用户在飞书等平台上随时确认/切换会话使用的 agent。

Type of change

  • New feature (non-breaking change that adds functionality)

Testing

Automated tests added in this PR

  • TestAgentSwitcher_SetAgentAndGetAgent / TestAgentSwitcher_SetAgent_RejectsSubagent / TestAgentSwitcher_SetAgent_RejectsUnknown / TestAgentSwitcher_SetAgent_ClearRestoresDefault / TestAgentSwitcher_AvailableAgents_FiltersSubagents in agent/opencode/agent_switch_test.go — SetAgent 校验(subagent/未知名拒绝、清空恢复默认)与枚举过滤
  • TestListAgents_* in agent/opencode/agent_list_test.goopencode agent list 解析、失败降级
  • /agent 命令 engine + CUJ 测试(core/engine_test.gocore/cuj_test.go)— 切换、回退、枚举为空降级、workspace 隔离
  • TestReplyFooterDisplayModel / TestBuildReplyFooter_ShowsAgentPrefix / TestBuildClaudeStatusLineFooter_ShowsAgentPrefix in core/reply_footer_agent_test.go — 脚注渲染 agent 前缀(三条 footer 路径)

For bug fixes only — regression test

  • Regression test name: TestBuildReplyFooter_ShowsAgentPrefix(脚注丢失 agent 名是本次合入后实测暴露的问题)
  • Manual verification this test catches the regression:
    • 本地将 replyFooterDisplayModel 退化为 replyFooterModel 模拟修复前代码,回归测试如预期失败;恢复后通过

Critical User Journeys (CUJ) impact

  • B — session lifecycle(/agent 切换属于会话 agent 生命周期)
  • I — UI rendering correctness(/agent 卡片、脚注 agent 前缀)
  • go test ./core/ -run TestCUJ passes locally(含 /agent CUJ)

Manual / user-visible behavior change

  • 用户可发 /agent 查看可选 agent 列表并切换(卡片含按钮 / 文本命令两种形态)
  • 切换持久化,重启后保持;/agent 空参显示当前 agent
  • 配置了非法/subagent agent 时启动警告
  • 回复脚注显示 agent · 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 下全过)
  • AGENTS.md Pre-Commit Checklist items are satisfied
  • No new hardcoded platform/agent names in core//agent 命令通过 AgentSwitcher 接口能力断言,无硬编码)
  • i18n strings have all-language translations(/agent 命令文案已入 i18n 全语言)
  • No secrets / credentials in source

Related

  • Issue: 无
  • Related PR: 无(本仓库 main 直推,无独立 PR 链)

vicat47 added 15 commits August 10, 2026 15:03
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).
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
@vicat47
vicat47 requested a review from chenhg5 as a code owner August 11, 2026 04:17
Fix staticcheck QF1012: sb.WriteString(fmt.Sprintf(...)) in the two
/agent list renderers (text and card variants).
@vicat47

vicat47 commented Aug 11, 2026

Copy link
Copy Markdown
Author
image

@chenhg5 chenhg5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论: 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-blockingvalidateConfiguredAgentAtStartup 只 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 输出格式可能变化(regex agentListLineRe 假设 <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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants