Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

## Unreleased

## 2.0.14 - 2026-09-14

This is a small Responses-search and Dashboard layout release on the 2.0 TypeScript runtime.
Hosted `web_search_call` events from a Responses gateway no longer abort the turn. Todo and plan stay in the header strip instead of duplicating into the chat. The settings gear is rotationally symmetric. Permission mode ids are unchanged.

### Fixed

- Responses hosted search no longer fails the turn as a protocol mismatch. Local function calls and visible text still run.
- Dashboard todo/plan live only in the top progress strip. Chat no longer inserts a second workflow panel or “任务状态已同步” cards.
- The settings gear icon is an eight-tooth symmetric mark.

### Upgrade

```sh
git pull
npm ci
npm run verify:install
npm link
ant-code --version
```

`ant-code --version` should print `2.0.14`. Restart a running Dashboard
and hard-refresh the browser. Gateway config and `.lab-agent` sessions
do not need to be recreated.

## 2.0.13 - 2026-09-14

Published 2026-09-14 13:40:52 UTC (2026-09-14 21:40:52 UTC+08:00): [v2.0.13](https://github.com/wicm84266964/Ant-Code/releases/tag/v2.0.13).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ ant-code doctor
ant-code
```

`ant-code --version` should print `2.0.13`. If you previously linked a
`ant-code --version` should print `2.0.14`. If you previously linked a
JavaScript install, run `npm link` again so the global command points at
`src/cli/index.ts`.

Expand Down
37 changes: 37 additions & 0 deletions docs/releases/2.0.14-responses-search-workflow-strip_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Ant Code v2.0.14:Responses 搜索不再掐断,任务进度只留顶部栏

发布日期:2026-09-14

这是 2.0 TypeScript 运行时上的小版本。Responses 网关夹带托管搜索时不再把整轮标成失败;Dashboard 待办和计划只显示在顶部任务条。设置按钮齿轮改为上下左右对称。权限模式 id 不变。本说明处于公开发布候选阶段,不表示 CI、tag 或 GitHub Release 已经完成。

## 对用户工作流的直接变化

- 使用 Grok 等 Responses 协议网关时,上游若返回原生 `web_search_call`,聊天不再报协议错配并中断。本轮若还有本地工具调用或可见正文,会继续执行。Chat Completions 协议不受影响。
- Dashboard 待办清单和任务计划只保留在顶部「任务进度」条,可展开查看。聊天记录不再插入同一份进度面板,也不再刷「任务状态已同步」。
- 设置按钮的齿轮图标改为八齿对称图形,避免看起来缺角。

## 配置、数据与安全边界

- 不要求手动迁移配置或历史会话。已有网关、权限和 `.lab-agent` 会话可继续使用。
- 托管搜索事件仍只记在内部原始载荷里,不会伪装成本地 `web_search` 工具结果。
- 本次不更新其他产品的内嵌运行时,不同步真实会话、凭据或研究数据。自动化测试不等于所有外部模型供应商都已验证。

## 升级与产物位置

需要 Node.js 22.18+。从源码更新:

```sh
git pull
npm ci
npm run verify:install
npm link
ant-code --version
```

`ant-code --version` 应显示 `2.0.14`。若以前 link 过旧安装,需要重新 `npm link`。正在跑的 Dashboard 需要重启并硬刷新浏览器。现有网关配置和会话不必重建。

## 验证、限制与待完成门禁

- 开发阶段已覆盖 Responses 托管搜索不中断、待办/计划只更新顶部栏,以及设置图标路径。
- 开发仓完整 `npm run check` 与跨仓一致性检查在脱敏同步时记录实际数字。候选提交后仍须 GitHub PR 和 main CI 通过,随后创建标签与 Release;实际完成状态记录在公开 CHANGELOG。
- 未对每个外部模型供应商做真实账户验收。托管搜索若既无本地 tool call 也无可见正文,仍可能走空正文或短停兜底,不会再以协议错配失败。
4 changes: 2 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ant-code/cli",
"version": "2.0.13",
"version": "2.0.14",
"description": "AGPL-licensed local coding agent with a terminal UI, local dashboard, tool permissions, skills, MCP integration, and model gateway adapters.",
"type": "module",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function mapSessionEventToDashboard(event: Record<string, unknown>) {
})];
}
if (type === "workflow_updated") {
return [activity("workflow-updated", "任务状态已同步", `已完成 ${event.todosCompleted ?? 0} 个待办、${event.planStepsCompleted ?? 0} 个计划步骤`, "completed", "session", event, { coalesceKey: "workflow" })];
return [];
}
if (type === "subagent_group_started") {
return [backgroundSubagentActivity(event, {
Expand Down
33 changes: 4 additions & 29 deletions src/dashboard/public/app-ui3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,47 +788,22 @@ export function renderWorkflowPanel(workflow: unknown, summary: unknown = null)
const todos = Array.isArray(record?.todos) ? record.todos : [];
const plan = isPlainObject(record?.plan) ? record.plan : null;
const steps = Array.isArray(plan?.steps) ? plan.steps : [];
if (state.workflowNode) {
state.workflowNode.remove();
state.workflowNode = null;
}
if (!record || (!todos.length && !steps.length)) {
state.workflow = null;
state.workflowExpanded = false;
renderWorkflowStrip();
return;
}
hideEmptyState();
state.workflow = {
...record,
todos,
plan: plan ? { ...plan, steps } : undefined
};
if (!state.workflowNode) {
state.workflowNode = document.createElement("section");
state.workflowNode.className = "workflow-panel";
appendTranscriptNode(state.workflowNode);
}
const totals = isPlainObject(summary)
? {
total: Number(summary.total ?? 0),
completed: Number(summary.completed ?? 0),
pending: Number(summary.pending ?? 0),
in_progress: Number(summary.in_progress ?? 0),
cancelled: Number(summary.cancelled ?? 0)
}
: summarizeWorkflow({ todos, plan: plan ? { steps } : undefined });
const percent = Number(totals.total) > 0 ? Math.round((Number(totals.completed) / Number(totals.total)) * 100) : 0;
state.workflowNode.innerHTML = `
<div class="workflow-head">
<div>
<div class="workflow-kicker">任务进度</div>
<div class="workflow-title">${totals.completed}/${totals.total} 已完成</div>
</div>
<div class="workflow-percent">${percent}%</div>
</div>
<div class="workflow-meter"><span style="width: ${percent}%"></span></div>
${todos.length ? workflowSection("Todo", todos) : ""}
${steps.length ? workflowSection("Plan", steps) : ""}
`;
renderWorkflowStrip();
scrollTranscript();
}

export function renderWorkflowStrip() {
Expand Down
31 changes: 4 additions & 27 deletions src/dashboard/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2993,45 +2993,22 @@ function renderWorkflowPanel3(workflow, summary = null) {
const todos = Array.isArray(record?.todos) ? record.todos : [];
const plan = isPlainObject(record?.plan) ? record.plan : null;
const steps = Array.isArray(plan?.steps) ? plan.steps : [];
if (state.workflowNode) {
state.workflowNode.remove();
state.workflowNode = null;
}
if (!record || !todos.length && !steps.length) {
state.workflow = null;
state.workflowExpanded = false;
renderWorkflowStrip2();
return;
}
hideEmptyState3();
state.workflow = {
...record,
todos,
plan: plan ? { ...plan, steps } : void 0
};
if (!state.workflowNode) {
state.workflowNode = document.createElement("section");
state.workflowNode.className = "workflow-panel";
appendTranscriptNode3(state.workflowNode);
}
const totals = isPlainObject(summary) ? {
total: Number(summary.total ?? 0),
completed: Number(summary.completed ?? 0),
pending: Number(summary.pending ?? 0),
in_progress: Number(summary.in_progress ?? 0),
cancelled: Number(summary.cancelled ?? 0)
} : summarizeWorkflow3({ todos, plan: plan ? { steps } : void 0 });
const percent = Number(totals.total) > 0 ? Math.round(Number(totals.completed) / Number(totals.total) * 100) : 0;
state.workflowNode.innerHTML = `
<div class="workflow-head">
<div>
<div class="workflow-kicker">任务进度</div>
<div class="workflow-title">${totals.completed}/${totals.total} 已完成</div>
</div>
<div class="workflow-percent">${percent}%</div>
</div>
<div class="workflow-meter"><span style="width: ${percent}%"></span></div>
${todos.length ? workflowSection3("Todo", todos) : ""}
${steps.length ? workflowSection3("Plan", steps) : ""}
`;
renderWorkflowStrip2();
scrollTranscript();
}
function renderWorkflowStrip2() {
if (!state.workflow || !state.workflow.todos?.length && !state.workflow.plan?.steps?.length) {
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<div class="header-actions">
<button class="header-settings" id="settings-button" type="button" aria-label="打开设置" aria-controls="settings-view" aria-pressed="false" title="设置">
<svg class="settings-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M10.2 3.4h3.6l.4 2.1a6.8 6.8 0 0 1 1.7.96l2-.9 1.8 3.12-1.6 1.5c.1.5.16 1 .16 1.52s-.06 1.02-.16 1.52l1.6 1.5-1.8 3.12-2-.9a6.8 6.8 0 0 1-1.7.96l-.4 2.1h-3.6l-.4-2.1a6.8 6.8 0 0 1-1.7-.96l-2 .9-1.8-3.12 1.6-1.5A6.6 6.6 0 0 1 8 12c0-.52.06-1.02.16-1.52l-1.6-1.5 1.8-3.12 2 .9a6.8 6.8 0 0 1 1.7-.96l.4-2.1ZM12 9.4A2.6 2.6 0 1 0 12 14.6 2.6 2.6 0 0 0 12 9.4Z"/>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M10.253 6.103 10.296 2.807h3.408l.043 3.296 1.188.492 2.361-2.301 2.41 2.41-2.301 2.361.492 1.188 3.296.043v3.408l-3.296.043-.492 1.188 2.301 2.361-2.41 2.41-2.361-2.301-1.188.492-.043 3.296h-3.408l-.043-3.296-1.188-.492-2.361 2.301-2.41-2.41 2.301-2.361-.492-1.188L2.807 13.704v-3.408l3.296-.043.492-1.188L4.294 6.704l2.41-2.41 2.361 2.301.492-1.188Zm1.747 2.547a3.35 3.35 0 1 0 0 6.7 3.35 3.35 0 0 0 0-6.7Z"/>
</svg>
<span class="header-settings-label">设置</span>
</button>
Expand Down
6 changes: 4 additions & 2 deletions src/dashboard/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -686,8 +686,10 @@ button {

.settings-icon {
display: block;
height: 15px;
width: 15px;
flex: 0 0 auto;
height: 16px;
overflow: visible;
width: 16px;
}

.workspace {
Expand Down
11 changes: 0 additions & 11 deletions src/model-gateway/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,17 +399,6 @@ export function createLabModelGateway(config: import("../config/load-config.ts")
continue;
}

if (protocol === "openai-responses" && isRecord(data.raw) && data.raw.nativeWebSearch === true
&& Array.isArray(gatewayRequest.tools)
&& !gatewayRequest.tools.some((tool) => isRecord(tool) && ["web_search", "web_search_preview"].includes(String(tool.type)))
&& gatewayRequest.tools.some((tool) => isRecord(tool) && tool.type === "function" && tool.name === "web_search")) {
return { ok: false, error: normalizeGatewayError(null, {
code: "GATEWAY_TOOL_PROTOCOL_MISMATCH",
message: "网关返回了未请求的原生搜索事件,而不是本地 web_search 工具调用;本轮未正常完成。请检查网关协议映射或重置此会话的网关绑定。",
details: { receivedToolType: "web_search_call", expectedToolType: "function_call" },
protocol
}) };
}
return { ok: true, data };
}

Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fsPromises from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const FALLBACK_VERSION = "2.0.13";
const FALLBACK_VERSION = "2.0.14";

/**
* @param {NodeJS.ProcessEnv} [env]
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/dashboard-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import assert from "node:assert/strict";
import test from "node:test";
import { mapSessionEventToDashboard } from "../../src/dashboard/events.ts";

test("dashboard keeps workflow updates out of the transcript", () => {
const events = mapSessionEventToDashboard({
type: "workflow_updated",
todosCompleted: 2,
planStepsCompleted: 1
});

assert.deepEqual(events, []);
});

test("dashboard maps thinking to folded activity without text", () => {
const events = mapSessionEventToDashboard({
type: "assistant_thinking_delta",
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/dashboard-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,8 @@ test("dashboard exposes responsive navigation and accessible interaction semanti
const html = await fs.readFile(path.resolve("src/dashboard/public/index.html"), "utf8");
const css = await fs.readFile(path.resolve("src/dashboard/public/styles.css"), "utf8");

assert.doesNotMatch(app, /appendTranscriptNode\(state\.workflowNode\)/);
assert.match(app, /function renderWorkflowStrip\(/);
const header = html.slice(html.indexOf("workspace-header"), html.indexOf("workflow-strip"));
assert.match(header, /id="connection-status"/);
assert.match(header, /正在读取配置/);
Expand Down
41 changes: 38 additions & 3 deletions tests/unit/gateway-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,38 @@ test("gateway client retries transient fetch failures before response", async ()
}
});

test("Responses rejects unsolicited hosted search instead of silently completing", async () => {
test("Responses hosted search does not abort a turn that also has local tool calls or text", async () => {
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = async () => new Response(JSON.stringify({
status: "completed",
output: [
{ type: "web_search_call", id: "search-1", status: "completed" },
{ type: "function_call", call_id: "call_local", name: "web_search", arguments: "{\"query\":\"ants\"}" },
{ type: "message", content: [{ type: "output_text", text: "searching" }] }
]
}), { headers: { "content-type": "application/json" } });
const gateway = createLabModelGateway({ modelAlias: "test", networkMode: "offline", allowedHosts: [], lab: {
gatewayUrl: "http://127.0.0.1/v1/responses", gatewayProtocol: "openai-responses", gatewayMaxRetries: 0
} });
const result = await gateway.sendChat({
messages: [{ role: "user", content: "search" }],
tools: [{ name: "web_search", description: "Search", inputSchema: { type: "object", properties: {} } }]
});
assert.equal(result.ok, true);
if (!result.ok) {
return;
}
assert.equal(result.data.raw?.nativeWebSearch, true);
assert.equal(result.data.text, "searching");
assert.equal(result.data.toolCalls[0]?.name, "web_search");
assert.equal(result.data.stopReason, "tool_calls");
} finally {
globalThis.fetch = originalFetch;
}
});

test("Responses hosted-search-only completions stay successful instead of protocol-mismatch abort", async () => {
const originalFetch = globalThis.fetch;
try {
for (const streaming of [false, true]) {
Expand All @@ -780,8 +811,12 @@ test("Responses rejects unsolicited hosted search instead of silently completing
} });
const result = await gateway.sendChat({ messages: [{ role: "user", content: "search" }],
tools: [{ name: "web_search", description: "Search", inputSchema: { type: "object", properties: {} } }], stream: streaming });
assert.equal(result.ok, false);
assert.equal(result.error?.code, "GATEWAY_TOOL_PROTOCOL_MISMATCH");
assert.equal(result.ok, true);
if (!result.ok) {
return;
}
assert.equal(result.data.raw?.nativeWebSearch, true);
assert.equal(result.error, undefined);
}
} finally { globalThis.fetch = originalFetch; }
});
Expand Down