fix: show operation details in OpenCode tool records
Preserve OpenCode operation titles in main-session tool summaries, including permission waits, failures and interrupted calls. Keep title trimming and length limits aligned with subagent records. Update tool display documentation and include the activity-menu wording in UI-DESIGN.md. Validation: 132 focused tests passed; main-process typecheck and changed-file lint passed. Full-suite checks were affected by concurrent activity-menu changes and a test timeout. Release note: OpenCode main-session tool records now show file paths, commands or operation descriptions beside tool names, matching subagent records.
This commit is contained in:
+2
-1
@@ -285,7 +285,7 @@
|
||||
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
|
||||
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
|
||||
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
|
||||
- 项目选择器下方承载跨项目活动汇总,空闲时隐藏;项目计数与活动对话框遵守
|
||||
- 项目选择器下方常驻跨项目活动汇总,空闲时显示“暂无活动”;项目计数与项目、会话级联菜单遵守
|
||||
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
|
||||
活动数量不改变项目菜单的稳定分组和顺序。
|
||||
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
|
||||
@@ -629,6 +629,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- OpenCode 原生 Task 调用在本地项目和托管 SSH 项目中统一解析为子 Agent 状态卡,不显示为普通工具卡。状态卡持续显示 Agent 类型、任务说明、运行状态、完成输出或失败原因;身份徽标、Agent 名称和状态位于首层,任务说明作为主信息,Runtime 来源作为次级元数据,输出与错误在同一卡片内按需展开。父 Agent 的推理不冒充子 Agent 过程,必须与状态卡保持明确间距。状态不能只靠颜色表达。推理、子 Agent、工具和正文统一按事件首次出现顺序进入消息块;增量更新只更新原位置,Task 临时工具块重分类后必须原位替换,不能把子 Agent 卡统一提升到消息顶部。同一调用的增量事件按稳定调用 ID 更新,不能因 ACP 首帧尚无参数而永久保留重复工具卡或活动记录。一次请求内已观察到的子 Agent 均保留在消息与活动记录中;父请求取消、失败或意外中断时仍处于活动状态的卡片必须结算为对应终态,父请求成功结束但子 Agent 未报告终态时必须显示为失败而不是推断为完成。
|
||||
- Agent 回复进行中锁定模型、专家角色、工作模式和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。
|
||||
- 普通工具在主会话和子 Agent 过程中使用同一轻量折叠记录:不显示外层“工具执行”标题、计数、背景和边框,保留无障碍区域名称。记录纵向左对齐,头部按内容收宽且不超过消息宽度;展开箭头在左,工具名作为主标题,有额外信息的单行摘要弱化显示。右侧以绿色对勾显示成功完成,以图标显示运行和待执行状态,提供悬停文字和无障碍名称;失败和可恢复状态保留文字。仅重复工具名和执行状态的已知 Runtime 模板摘要在头部与详情中均隐藏;刷新重试、审批等额外提示保留。默认折叠,状态和输出更新不改变用户的展开选择;折叠时露出一行错误,展开详情使用消息可用宽度、浅背景和左侧细线,按错误、结果、独立折叠的输入参数阅读。详情保留原始文本,完整 JSON 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。
|
||||
- OpenCode 主会话和子 Agent 的工具记录均保留 Runtime 提供的操作标题作为摘要,显示文件路径、命令或操作描述;本地 SDK 标题去除首尾空白后最多保留 240 字符,没有标题时沿用工具名回退规则。主会话工具失败或中断时保留已收到的标题;远端 ACP 继续在记录名称中显示其提供的标题。
|
||||
- 工具取消后保留“已取消”文字,避免与失败图标混淆。
|
||||
- Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
|
||||
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
|
||||
|
||||
@@ -2517,7 +2517,11 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
|
||||
await runtime.dispose();
|
||||
});
|
||||
|
||||
it("keeps non-Task tools with Task-shaped input as generic tools", async () => {
|
||||
it.each([
|
||||
[" Custom delegate ", "Custom delegate"],
|
||||
["", "OpenCode 工具:custom_delegate"],
|
||||
["x".repeat(300), "x".repeat(240)],
|
||||
])("keeps generic tool titles bounded: %s", async (title, summary) => {
|
||||
const setup = runClient([
|
||||
{
|
||||
id: "custom-tool-completed",
|
||||
@@ -2536,7 +2540,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
|
||||
prompt: "Inspect the complete source tree.",
|
||||
},
|
||||
output: "Custom tool result.",
|
||||
title: "Custom delegate",
|
||||
title,
|
||||
metadata: {},
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
@@ -2559,12 +2563,73 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
|
||||
callId: "call-custom-1",
|
||||
name: "custom_delegate",
|
||||
state: "completed",
|
||||
summary,
|
||||
}),
|
||||
);
|
||||
expect(events.some((event) => event.type === "subagent")).toBe(false);
|
||||
await runtime.dispose();
|
||||
});
|
||||
|
||||
it.each(["error", "interrupted"])("retains the running tool title after %s", async (ending) => {
|
||||
const part = {
|
||||
id: "part-read-1",
|
||||
callID: "call-read-1",
|
||||
type: "tool" as const,
|
||||
tool: "read",
|
||||
};
|
||||
const setup = runClient([
|
||||
{
|
||||
id: "read-running",
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
part: {
|
||||
...part,
|
||||
state: {
|
||||
status: "running",
|
||||
input: { filePath: "README.md" },
|
||||
title: "README.md",
|
||||
time: { start: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(ending === "error" ? [{
|
||||
id: "read-error",
|
||||
type: "message.part.updated" as const,
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
part: {
|
||||
...part,
|
||||
state: {
|
||||
status: "error" as const,
|
||||
input: { filePath: "README.md" },
|
||||
error: "File not found",
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}] : []),
|
||||
]);
|
||||
const runtime = embeddedRuntime(setup.client);
|
||||
const events: RuntimeEvent[] = [];
|
||||
await expect((async () => {
|
||||
for await (const event of runtime.run({
|
||||
requestId: "3f496642-f47d-4e0a-8944-a32c77b0d6ef",
|
||||
conversationId: "conversation-1",
|
||||
prompt: "Read README.md",
|
||||
workMode: "execute",
|
||||
}, new AbortController().signal)) {
|
||||
events.push(event);
|
||||
}
|
||||
})()).rejects.toThrow();
|
||||
expect(events.filter((event) => event.type === "tool")).toEqual([
|
||||
expect.objectContaining({ state: "running", summary: "README.md" }),
|
||||
expect.objectContaining({ state: "failed", summary: "README.md" }),
|
||||
]);
|
||||
await runtime.dispose();
|
||||
});
|
||||
|
||||
it("does not impose a default wall-clock deadline on OpenCode runs", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
@@ -2356,6 +2356,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
summary: string;
|
||||
state: "pending" | "running" | "completed" | "failed";
|
||||
input?: string;
|
||||
output?: string;
|
||||
@@ -2605,6 +2606,11 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
throw new Error("OpenCode 工具调用 ID 格式无效");
|
||||
}
|
||||
const toolName = part.tool.slice(0, 200);
|
||||
const title = "title" in part.state ? part.state.title : undefined;
|
||||
const summary =
|
||||
(typeof title === "string" ? title.trim().slice(0, 240) : "") ||
|
||||
toolStates.get(callId)?.summary ||
|
||||
`OpenCode 工具:${toolName}`;
|
||||
const nativeControlTool = toolName.trim().toLowerCase() === "question";
|
||||
const state =
|
||||
part.state.status === "error" ? "failed" : part.state.status;
|
||||
@@ -2636,6 +2642,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
: undefined;
|
||||
toolStates.set(callId, {
|
||||
name: toolName,
|
||||
summary,
|
||||
state,
|
||||
...(input ? { input } : {}),
|
||||
...(output ? { output } : {}),
|
||||
@@ -2657,7 +2664,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
callId,
|
||||
name: toolName,
|
||||
state,
|
||||
summary: `OpenCode 工具:${toolName}`,
|
||||
summary,
|
||||
...(input ? { input } : {}),
|
||||
...(output ? { output } : {}),
|
||||
...(error ? { error } : {}),
|
||||
@@ -2811,14 +2818,15 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
permissionRequest.tool?.callID ?? permissionRequest.id;
|
||||
const toolName = permissionRequest.permission.slice(0, 200);
|
||||
if (permissionSessionId === sessionId) {
|
||||
toolStates.set(callId, { name: toolName, state: "pending" });
|
||||
const summary = toolStates.get(callId)?.summary ?? `OpenCode 工具:${toolName}`;
|
||||
toolStates.set(callId, { name: toolName, summary, state: "pending" });
|
||||
yield {
|
||||
requestId: request.requestId,
|
||||
type: "tool",
|
||||
callId,
|
||||
name: toolName,
|
||||
state: "pending",
|
||||
summary: `OpenCode 工具:${toolName}`,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
const allowKnowledge =
|
||||
@@ -2939,7 +2947,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
callId,
|
||||
name: tool.name,
|
||||
state: "failed",
|
||||
summary: `OpenCode 工具:${tool.name}`,
|
||||
summary: tool.summary,
|
||||
...(tool.input ? { input: tool.input } : {}),
|
||||
...(tool.output ? { output: tool.output } : {}),
|
||||
...(tool.error ? { error: tool.error } : {}),
|
||||
|
||||
Reference in New Issue
Block a user