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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Unreleased

## 2.0.12 - 2026-09-14

Candidate small release for Dashboard send stability and interaction correctness. GitHub CI, tag and Release status will be recorded after publication.

- A successful `/api/turns` response no longer rewrites the session or draft after the user has already switched away.
- While a turn is running, the main button queues when the composer has text or attachments, and interrupts only when it is empty.
- Session retention cleanup no longer blocks first paint or a new Dashboard send. Long session resume reads from the tail of the archive; turn start waits up to 60 seconds instead of 15.
- Approval Tab stays inside the panel in the TUI. Gateway 429 responses honor `Retry-After` up to 30 seconds.

Validation, upgrade steps and limitations: [Chinese release note](docs/releases/2.0.12-dashboard-interaction-send-stability_zh.md).

## 2.0.11 - 2026-09-10

Published 2026-09-10 09:14:36 UTC (2026-09-10 17:14:36 UTC+08:00): [v2.0.11](https://github.com/wicm84266964/Ant-Code/releases/tag/v2.0.11).
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.11`. If you previously linked a
`ant-code --version` should print `2.0.12`. If you previously linked a
JavaScript install, run `npm link` again so the global command points at
`src/cli/index.ts`.

Expand Down
42 changes: 42 additions & 0 deletions docs/releases/2.0.12-dashboard-interaction-send-stability_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Ant Code v2.0.12:Dashboard 交互收口与发送稳定

发布日期:2026-09-14

这是 2.0 TypeScript 运行时上的小版本。Dashboard 日常发送不再被会话清理或超长历史恢复卡住,迟到的提交也不会改写已经切走的会话和草稿。同时收口运行中主按钮、审批按键和网关 429 等待。权限模式 id 不变。本说明处于公开发布候选阶段,不表示 CI、tag 或 GitHub Release 已经完成。

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

- Dashboard 提交 `/api/turns` 成功后,如果已经切到另一个会话,界面不再被拉回去,也不会清掉等待期间改过的输入。
- 任务运行中:输入框或附件有内容时,主按钮是「加入队列」;空输入时才是「中断」。
- 刚打开 Dashboard 或会话很多时,首屏和会话列表不再等待过期会话清理。新建任务也不再和清理抢同一把锁,发送可以马上打到网关。
- 续跑很长的历史会话时,恢复上下文改为从尾部按预算读取;发送 `/api/turns` 等到 60 秒。不再因为本机恢复超过 15 秒就显示「请求超时」、请求其实还没发出。
- 权限确认标题使用中文工具名。PDF 页范围在回形针上可见。窄屏空状态改为「文件在「文件」页」。
- TUI 审批或需求核对打开时,Tab / Shift+Tab 只在该面板内移动,不再先切侧栏或权限档。`/keybindings` 与真实按键一致。
- 网关返回 429 时优先尊重 `Retry-After`,上限仍 30 秒。live health 允许两次探测并限制错误体大小。

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

- 不要求手动迁移配置或历史会话。已有网关、权限和 `.lab-agent` 会话可继续使用。
- 过期会话清理策略不变:默认仍按保留天数在后台清理;保存「保留天数」仍会立即清理。清理不会删除当前内存里正在用的会话。
- 本次不更新其他产品的内嵌运行时,不同步真实会话、凭据或研究数据。自动化测试不等于所有外部模型供应商或每份私人研究数据都已验证。

## 升级与产物位置

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

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

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

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

- 开发阶段已覆盖迟到提交不改写当前会话、运行中入队/中断、会话清理不挡首屏和新建任务、超长会话恢复裁剪、发送超时、审批 Tab、网关 429 与 health 探测;用户已完成本地 Dashboard 验收,包括长任务交互。
- 开发仓完整 `npm run check` 通过:单元/集成 1,342 项、浏览器 38 项,均无失败或跳过;语法、类型检查 0 诊断、依赖、来源和资源一致性检查通过。跨仓一致性检查和正式仓复验在脱敏同步后进行。
- 候选提交后仍须 GitHub PR 和 main CI 通过,随后创建标签与 Release;实际完成状态记录在公开 CHANGELOG。
- 未对每个外部模型供应商做真实账户验收。超长会话恢复仍受本机磁盘和上下文预算限制;网关 503 等上游不可用不是本版本修复范围。
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.11",
"version": "2.0.12",
"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
4 changes: 2 additions & 2 deletions src/cli/tui/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ export function updateActivity(current: TuiActivity, event: TuiRuntimeEvent): Tu
return {
...current,
status: event.interrupted ? "工具已中断" : event.ok ? "工具完成" : event.blocked ? "工具被阻止" : "工具失败",
lastTool: `${event.name} ${event.interrupted ? "interrupted" : event.ok ? "done" : event.blocked ? "blocked" : "failed"}`,
lastTool: `${event.name} ${event.interrupted ? "已中断" : event.ok ? "完成" : event.blocked ? "被阻止" : "失败"}`,
toolCount: current.toolCount + 1,
blockedTools: current.blockedTools + (event.blocked ? 1 : 0),
failedTools: current.failedTools + (!event.ok && !event.blocked && !event.interrupted ? 1 : 0)
};
}
if (event.type === "review_gate") {
return { ...current, status: "等待复核", lastTool: "review gate" };
return { ...current, status: "等待复核", lastTool: "复核门" };
}
if (event.type === "assistant_final") {
return { ...current, status: "就绪", assistantBytes: event.outputBytes ?? current.assistantBytes };
Expand Down
4 changes: 2 additions & 2 deletions src/cli/tui/app-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,9 @@ export function useTuiAppActions(s: ReturnType<typeof useTuiAppPanels>) {
`详情:/agents task ${taskId}`
].join("\n");
if (stateRef.current.entries?.some((entry) => entry.id === entryId)) {
updateEntryById(entryId, { kind: "agent", title: "子任务已启动", body, taskId, profile, taskStatus: "running" });
updateEntryById(entryId, { kind: "agent", title: "子任务后台运行中", body, taskId, profile, taskStatus: "running" });
} else {
addEntry("agent", "子任务已启动", body, { id: entryId, taskId, profile, taskStatus: "running" });
addEntry("agent", "子任务后台运行中", body, { id: entryId, taskId, profile, taskStatus: "running" });
}
setSideView("tasks");
setSideOffset(0);
Expand Down
34 changes: 20 additions & 14 deletions src/cli/tui/app-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,15 @@ export function useTuiAppKeys(s: ReturnType<typeof useTuiAppTurn>) {
return;
}
if (isCtrlKey(inputValue, key, "l")) {
setEntries([]);
setEntries([withEntryIdentity({
kind: "session",
title: "已清屏",
body: "只隐藏当前可见记录,会话文件未删除。",
at: new Date().toLocaleTimeString()
})]);
setTranscriptOffset(0);
setStreamOffset(0);
setActivity((value) => ({ ...value, status: "已清屏" }));
return;
}
const canScrollConversation = current.mode === "input"
Expand Down Expand Up @@ -833,19 +839,6 @@ export function useTuiAppKeys(s: ReturnType<typeof useTuiAppTurn>) {
setHistoryIndex(null);
return;
}
if (key.shift && key.tab) {
cyclePermissionMode(current, "ink-shift-tab");
return;
}
if (key.tab) {
setSideView((value) => {
const next = nextSideView(value);
setActivity((currentActivity) => ({ ...currentActivity, status: `panel ${next}` }));
return next;
});
setSidePanelOffset(0);
return;
}
if (current.mode === "approval") {
handleApprovalInput(inputValue, key, current, sessionApprovals.current, addEntry, setActivity, setMode, setPendingApproval, setApprovalChoiceIndex);
return;
Expand All @@ -861,6 +854,19 @@ export function useTuiAppKeys(s: ReturnType<typeof useTuiAppTurn>) {
});
return;
}
if (key.shift && key.tab) {
cyclePermissionMode(current, "ink-shift-tab");
return;
}
if (key.tab) {
setSideView((value) => {
const next = nextSideView(value);
setActivity((currentActivity) => ({ ...currentActivity, status: `panel ${next}` }));
return next;
});
setSidePanelOffset(0);
return;
}
if (current.modelPickerOpen) {
if (key.escape) {
setModelPickerOpen(false);
Expand Down
13 changes: 7 additions & 6 deletions src/cli/tui/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { LabModel } from "../../model-gateway/models.ts";
import { listKeybindings } from "../../commands/registry.ts";
import { buildTaskTree } from "../../agents/orchestrator.ts";
import { summarizeContextWindow } from "../../core/context-window.ts";
import { getAntCodeVersionSync } from "../../version.ts";
import { formatTuiGoalFooter } from "./goal.ts";
import { DEFAULT_TUI_THEME, themeColor } from "./theme.ts";
import {
Expand Down Expand Up @@ -365,8 +366,8 @@ const EMPTY_ACTIVITY: TuiActivity = {

export function StatusBar({ session, cwd = "", activity, pulse = 0, detailMode = "compact", width = 100, theme = DEFAULT_TUI_THEME }: TuiViewProps & { session: AgentSession }) {
const gateway = session.config.lab.gatewayUrl
? `${session.config.lab.gatewayProtocol ?? "openai-chat"} configured`
: "gateway missing";
? `${session.config.lab.gatewayProtocol ?? "openai-chat"} 已配置`
: "网关未配置";
const context = summarizeContextWindow(session);
const inputBrief = Number.isFinite(context.promptTokens)
? ` input ${formatTokenCount(context.promptTokens)}`
Expand All @@ -381,7 +382,7 @@ export function StatusBar({ session, cwd = "", activity, pulse = 0, detailMode =
: `${spinnerFrame(pulse)} `;
const cwdWidth = Math.min(44, Math.max(18, Math.floor(width * 0.35)));
const statusWidth = Math.max(24, (width ?? 100) - cwdWidth - 12);
const statusText = truncateMiddle(` v1.1 - ${activityPrefix}${activityStatus} - ${session.model} - ${contextBrief} - ${gateway} - ${permissionModeLabel(session)} - ${session.networkMode} - ${detailModeLabel(detailMode)} - events=${activity?.eventCount ?? 0}`, statusWidth);
const statusText = truncateMiddle(` v${getAntCodeVersionSync()} - ${activityPrefix}${activityStatus} - ${session.model} - ${contextBrief} - ${gateway} - ${permissionModeLabel(session)} - ${session.networkMode} - ${detailModeLabel(detailMode)} - 事件=${activity?.eventCount ?? 0}`, statusWidth);
return h(Box, { paddingX: 1, justifyContent: "space-between" },
h(Text, null,
h(Text, { color: themeColor(theme, "identity", "cyan"), bold: true }, "Ant Code"),
Expand Down Expand Up @@ -427,8 +428,8 @@ export function LogPane({ entries, width, height, stream, pulse = 0, detailMode
...Array.from({ length: scrollbackMode ? 0 : Math.max(0, viewportRows - viewport.lines.length) }, () => line(""))
];
const counter = viewport.totalRows === 0
? "0 rows"
: `${viewport.firstRow}-${viewport.lastRow}/${viewport.totalRows}${viewport.offset > 0 ? ` +${viewport.offset}` : scrollbackMode ? " scrollback" : " bottom"}`;
? "0 "
: `${viewport.firstRow}-${viewport.lastRow}/${viewport.totalRows}${viewport.offset > 0 ? ` +${viewport.offset}` : scrollbackMode ? " 回看" : " 底部"}`;
const readingHistory = viewport.offset > 0;
const selectionRange = transcriptSelection
? {
Expand Down Expand Up @@ -1238,7 +1239,7 @@ function statusMark(status: string) {
export function startupEntry(session: AgentSession) {
return {
kind: "startup",
title: "Ant Code TUI v1.1",
title: `Ant Code TUI v${getAntCodeVersionSync()}`,
body: startupBannerLines(session).join("\n"),
at: new Date().toLocaleTimeString()
};
Expand Down
53 changes: 31 additions & 22 deletions src/cli/tui/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentSession } from "../../core/session.ts";
import { toolLabel } from "../../tools/labels.ts";
import { composerSegments, displayWidth } from "./input-editor.ts";
import { parseMarkdownBlocks, renderTable } from "./markdown-table.ts";
export { approvalKeyFor } from "../../permissions/approval-keys.ts";
Expand Down Expand Up @@ -629,7 +630,7 @@ export function promptLines(
if (mode === "approval") {
return [
{ text: "权限弹窗已打开" },
{ text: " 用方向键/Tab 后按 Enter,或 Y 允许一次、A 本会话允许、N 拒绝Esc 取消。", dim: true }
{ text: " 用方向键/Tab 后按 Enter,或 Y 允许一次、A 本会话允许、N 拒绝Esc 取消并拒绝本次请求。", dim: true }
];
}
if (mode === "question") {
Expand Down Expand Up @@ -802,7 +803,7 @@ export function permissionModalLines(
const sensitive = request.decision?.sensitive === true;
return [
line("需要权限", false, "yellow"),
line(`${riskBadge(risk)} ${toolName}`, false, riskColor(risk)),
line(`${riskBadge(risk)} ${toolLabel(toolName)}`, false, riskColor(risk)),
line(reason, true),
...(sensitive
? [
Expand Down Expand Up @@ -1046,11 +1047,11 @@ function compactUserPromptLines(body: string | undefined, detailMode: string): T
* @param {{ title?: string; body?: string }} entry
*/
export function toolCardLines(entry: { title?: string; body?: string } = {}, detailMode: string = "compact"): TuiLine[] {
const title = entry.title ?? "tool";
const title = entry.title ?? "工具";
const state = inferToolState(title, entry.body);
const name = title.replace(/\s+(running|done|blocked|failed)$/i, "");
const name = title.replace(/\s+(running|done|blocked|failed|interrupted|运行中|完成|被阻止|失败|已中断)$/i, "");
return [
line(`${toolStatusMarker(state)} ${toolClassLabel(name)} - ${title}`, false, toolStateColor(state)),
line(`${toolStatusMarker(state)} ${toolClassLabel(name)} - ${toolLabel(name)} ${toolRuntimeStateLabel(state)}`, false, toolStateColor(state)),
...foldBodyLines(splitFoldableLines(entry.body ?? ""), detailMode, { compact: 2, detailed: 8 }).map((item) => line(` ${item.text}`, true))
];
}
Expand Down Expand Up @@ -1116,54 +1117,62 @@ function foldBodyLines(lines: string[], detailMode: string, limits: FoldLimits):

function inferToolState(title: string, body: string | undefined): string {
const value = `${title} ${body ?? ""}`.toLowerCase();
if (value.includes("blocked")) {
if (value.includes("blocked") || value.includes("阻止")) {
return "blocked";
}
if (value.includes("failed") || value.includes("error=")) {
if (value.includes("failed") || value.includes("失败") || value.includes("error=")) {
return "failed";
}
if (value.includes("cancelled") || value.includes("interrupted")) {
if (value.includes("cancelled") || value.includes("interrupted") || value.includes("中断")) {
return "interrupted";
}
if (value.includes("done") || value.includes("approved")) {
if (value.includes("done") || value.includes("approved") || value.includes("完成")) {
return "done";
}
return "running";
}

function toolClassLabel(name: string) {
if (["read_file", "list_files", "glob", "grep", "git_status", "git_diff"].includes(name)) {
return "read";
if (["read_file", "list_files", "glob", "grep", "rg_search", "rg_files", "rg_files_with_matches", "rg_count", "git_status", "git_diff", "git_log", "git_show"].includes(name)) {
return "读取";
}
if (["write_file", "edit_file", "todo_write", "plan_update"].includes(name)) {
return "edit";
return "写入";
}
if (["powershell", "bash"].includes(name)) {
return "shell";
return "命令";
}
if (name === "mcp_call") {
return "mcp";
if (name === "mcp_call" || name === "mcp_list") {
return "MCP";
}
if (name === "ask_user") {
return "ask";
return "询问";
}
return "tool";
return "工具";
}

function toolRuntimeStateLabel(state: string): string {
if (state === "done") return "完成";
if (state === "blocked") return "被阻止";
if (state === "failed") return "失败";
if (state === "interrupted") return "已中断";
return "运行中";
}

function toolStatusMarker(state: string): string {
if (state === "done") {
return "[ok]";
return "[完成]";
}
if (state === "blocked") {
return "[blocked]";
return "[阻止]";
}
if (state === "failed") {
return "[failed]";
return "[失败]";
}
if (state === "interrupted") {
return "[stopped]";
return "[中断]";
}
return "[run]";
return "[运行]";
}

function toolStateColor(state: string): string {
Expand Down
8 changes: 7 additions & 1 deletion src/cli/tui/input-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function handleApprovalInput(
if (!current.pendingApproval) {
return;
}
if (key.leftArrow || key.upArrow) {
if (key.leftArrow || key.upArrow || (key.tab && key.shift)) {
setApprovalChoiceIndex((value) => (value + APPROVAL_CHOICES.length - 1) % APPROVAL_CHOICES.length);
return;
}
Expand Down Expand Up @@ -128,6 +128,12 @@ export function handleQuestionInput(inputValue: unknown, key: InkKey, current: T
setPendingQuestion((pending: TuiUiState["pendingQuestion"]) => pending ? { ...pending, focusedIndex: nextIndex } : pending);
return;
}
if (hasChoices && key.tab) {
const delta = key.shift ? -1 : 1;
const nextIndex = (prompt.focusedIndex + delta + prompt.choices.length) % prompt.choices.length;
setPendingQuestion((pending: TuiUiState["pendingQuestion"]) => pending ? { ...pending, focusedIndex: nextIndex } : pending);
return;
}
if (hasChoices && inputValue === " " && (!prompt.allowCustom || current.questionBuffer.length === 0)) {
if (prompt.multiple) {
const selected = new Set(prompt.selectedIndices);
Expand Down
3 changes: 2 additions & 1 deletion src/cli/tui/inspector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentSession } from "../../core/session.ts";
import { getAntCodeVersionSync } from "../../version.ts";
import { line, permissionModeLabel, splitLines, truncate } from "./format.ts";

export const MAX_INSPECTOR_ITEMS = 30;
Expand Down Expand Up @@ -85,7 +86,7 @@ const EMPTY_INSPECTOR: InspectorItem = {};
const EMPTY_TEXT_OPTIONS: InspectorTextOptions = {};

export function initialInspector(session: AgentSession): InspectorItem {
return makeInspector("欢迎", "Ant Code TUI v1.0", [
return makeInspector("欢迎", `Ant Code TUI v${getAntCodeVersionSync()}`, [
`模型:${session.model}`,
`模式:${permissionModeLabel(session)}`,
`网络:${session.networkMode}`,
Expand Down
Loading