c0de-agent 1.14.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -13,6 +13,28 @@ import { headChars } from '../../shared/utils/string.js';
|
|
|
13
13
|
import { apiError } from '../middleware/error.js';
|
|
14
14
|
import { readJsonObject } from '../util/json-body.js';
|
|
15
15
|
import { safeResolve } from '../util/safe-path.js';
|
|
16
|
+
/** 二进制嗅探:NUL 字节出现在文件开头 8 KiB 内,或整段不是合法 UTF-8。
|
|
17
|
+
*
|
|
18
|
+
* 读取接口此前一律 `readFile(resolved, 'utf-8')`——任何扩展名都按文本解码,
|
|
19
|
+
* .zip/.woff2/.wasm/.db 之类解码成一片 U+FFFD 后照样进前端编辑器;用户敲一个字
|
|
20
|
+
* 点保存,PUT 就用文本把原文件 truncate 重写一遍,原字节不可恢复(实测 4096B 的
|
|
21
|
+
* zip 被写成 7476B 的 U+FFFD 文本)。这里在字节层把「不是文本」说清楚,别让前端
|
|
22
|
+
* 拿乱码当可编辑内容。
|
|
23
|
+
*
|
|
24
|
+
* 判据与 git 的文本检测同族(NUL 快速通道 + UTF-8 合法性)。合法性必须按**整段**
|
|
25
|
+
* 判定:只看前 8 KiB 会在多字节字符被切断处误报成二进制。
|
|
26
|
+
*/
|
|
27
|
+
function looksBinary(buf) {
|
|
28
|
+
if (buf.subarray(0, 8192).includes(0))
|
|
29
|
+
return true;
|
|
30
|
+
try {
|
|
31
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buf);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
/** 递归搜索时跳过的目录(体积大/为元数据噪音,避免递归进入)。
|
|
17
39
|
* P3:补齐常见构建/缓存产物目录——此前只跳 .git/node_modules,
|
|
18
40
|
* 大仓库每击键全量 walk 时 dist/.cache 等目录拖垮延迟。 */
|
|
@@ -521,7 +543,12 @@ ${headChars(summary.diff, 8000)}`;
|
|
|
521
543
|
: {}),
|
|
522
544
|
});
|
|
523
545
|
}
|
|
524
|
-
const
|
|
546
|
+
const buf = await readFile(resolved);
|
|
547
|
+
// 二进制不按文本返回:前端拿到的每一段乱码都可能原样写回磁盘。
|
|
548
|
+
if (looksBinary(buf)) {
|
|
549
|
+
return apiError(c, 415, 'BINARY_FILE', '这是二进制文件,不能以文本方式显示或编辑(需要查看请下载/用外部工具打开)');
|
|
550
|
+
}
|
|
551
|
+
const content = buf.toString('utf-8');
|
|
525
552
|
return c.json({ path, content });
|
|
526
553
|
}
|
|
527
554
|
catch {
|
|
@@ -11,6 +11,10 @@ type DirEntry = {
|
|
|
11
11
|
*
|
|
12
12
|
* 输入 path 为空时返回 home 目录列表。
|
|
13
13
|
* `~` 开头自动展开为 home 目录。
|
|
14
|
+
*
|
|
15
|
+
* 读不到(无权访问 / 已被删除 / 不是目录)必须抛出:此前吞成 `[]`,
|
|
16
|
+
* 选择器无法区分「目录是空的」与「目录读不到」,把权限错误渲染成
|
|
17
|
+
* 「(空)」——用户据此认定「里面没有东西」,既不重试也不去修权限。
|
|
14
18
|
*/
|
|
15
19
|
declare function listDirectories(dirPath: string): Promise<DirEntry[]>;
|
|
16
20
|
/** 递归搜索目录:从 directory 起收集 type=directory 的相对路径。
|
|
@@ -2,21 +2,20 @@ import { readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { relative as relativePath, resolve as resolvePath } from 'node:path';
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
|
+
import { apiError } from '../middleware/error.js';
|
|
5
6
|
/**
|
|
6
7
|
* 列出指定目录下的子目录(仅目录,不含文件)。
|
|
7
8
|
* 用于项目添加时的路径自动补全——可浏览文件系统任意位置。
|
|
8
9
|
*
|
|
9
10
|
* 输入 path 为空时返回 home 目录列表。
|
|
10
11
|
* `~` 开头自动展开为 home 目录。
|
|
12
|
+
*
|
|
13
|
+
* 读不到(无权访问 / 已被删除 / 不是目录)必须抛出:此前吞成 `[]`,
|
|
14
|
+
* 选择器无法区分「目录是空的」与「目录读不到」,把权限错误渲染成
|
|
15
|
+
* 「(空)」——用户据此认定「里面没有东西」,既不重试也不去修权限。
|
|
11
16
|
*/
|
|
12
17
|
async function listDirectories(dirPath) {
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
entries = await readdir(dirPath, { withFileTypes: true });
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
18
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
20
19
|
const dirs = entries
|
|
21
20
|
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
22
21
|
.map((e) => ({
|
|
@@ -90,8 +89,22 @@ function createFilesystemRoute(ctx) {
|
|
|
90
89
|
const target = expandPath(rawPath);
|
|
91
90
|
// 校验:ctx 可用于将来做沙箱限制(当前允许浏览任意路径)
|
|
92
91
|
void ctx;
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
try {
|
|
93
|
+
const dirs = await listDirectories(target);
|
|
94
|
+
return c.json({ path: target, directories: dirs });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// 读不到就说读不到:调用方(目录选择器)据此展示原因与重试,
|
|
98
|
+
// 而不是把失败当成空目录。
|
|
99
|
+
const errno = err?.code;
|
|
100
|
+
if (errno === 'ENOENT' || errno === 'ENOTDIR') {
|
|
101
|
+
return apiError(c, 404, 'PATH_NOT_FOUND', `目录不存在或已被删除:${target}`);
|
|
102
|
+
}
|
|
103
|
+
if (errno === 'EACCES' || errno === 'EPERM') {
|
|
104
|
+
return apiError(c, 403, 'PATH_FORBIDDEN', `没有权限读取该目录:${target}`);
|
|
105
|
+
}
|
|
106
|
+
return apiError(c, 500, 'BROWSE_FAILED', `读取目录失败:${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
95
108
|
});
|
|
96
109
|
// 递归搜索目录(用于项目路径自动补全——命中深层目录)
|
|
97
110
|
app.get('/search', async (c) => {
|
|
@@ -82,10 +82,15 @@ function createTodoRoute(ctx) {
|
|
|
82
82
|
const updatedPhases = todoCtx.todoState.get();
|
|
83
83
|
// 如果操作成功且 phases 有变化,持久化为 tool 消息
|
|
84
84
|
if (result._tag === 'success' && body.op !== 'view') {
|
|
85
|
+
// tool_call 与 tool_result 必须共用同一个 id:前端 normalizeParts 按 id 配对,
|
|
86
|
+
// 两个 generateId() 会让它们配不上——时间线上留下一个永远停在「运行中」
|
|
87
|
+
// (⏳ 永不结束)的 todo 块,正文还因拿不到 output 而谎报「Todo list is empty.」,
|
|
88
|
+
// 另加一张孤立的 result 卡。实测从「任务本会话待办」面板点「添加」后必现。
|
|
89
|
+
const toolMessageId = generateId();
|
|
85
90
|
// 构造 tool_result 消息内容
|
|
86
91
|
const toolResultContent = {
|
|
87
92
|
_tag: 'tool_result',
|
|
88
|
-
id:
|
|
93
|
+
id: toolMessageId,
|
|
89
94
|
tool: 'todo',
|
|
90
95
|
output: {
|
|
91
96
|
_tag: 'success',
|
|
@@ -95,7 +100,7 @@ function createTodoRoute(ctx) {
|
|
|
95
100
|
};
|
|
96
101
|
const toolCallContent = {
|
|
97
102
|
_tag: 'tool_call',
|
|
98
|
-
id:
|
|
103
|
+
id: toolMessageId,
|
|
99
104
|
tool: 'todo',
|
|
100
105
|
input: body,
|
|
101
106
|
};
|
|
@@ -111,16 +111,24 @@ export const taskTool = {
|
|
|
111
111
|
const label = (i) => inp.tasks[i]?.description ?? inp.tasks[i]?.role ?? 'task';
|
|
112
112
|
const completed = [];
|
|
113
113
|
const failures = [];
|
|
114
|
+
// 逐子任务的终态:交给 UI 画工作流图。此前图里所有节点都套用父工具的
|
|
115
|
+
// 状态——批量里失败一个,成功的那几个也一起被画成「失败」,与下方
|
|
116
|
+
// 「Completed: …」的文本自相矛盾(实测 3 个里 2 成 1 败,图上 3 个全红)。
|
|
117
|
+
// 只带下标不带文案:节点标题仍由 UI 从输入派生,避免两处各写一套兜底文案。
|
|
118
|
+
const subtasks = [];
|
|
114
119
|
results.forEach((res, i) => {
|
|
115
120
|
if (res._tag === 'error') {
|
|
116
121
|
failures.push(`[${label(i)}] ${res.error}`);
|
|
122
|
+
subtasks.push({ index: i, status: 'failed' });
|
|
117
123
|
return;
|
|
118
124
|
}
|
|
119
125
|
if (res._tag === 'running') {
|
|
120
126
|
completed.push(`[${label(i)}] background started (jobId: ${res.jobId})`);
|
|
127
|
+
subtasks.push({ index: i, status: 'running' });
|
|
121
128
|
return;
|
|
122
129
|
}
|
|
123
130
|
completed.push(`[${label(i)}] ${res.output}`);
|
|
131
|
+
subtasks.push({ index: i, status: 'completed' });
|
|
124
132
|
});
|
|
125
133
|
if (failures.length > 0) {
|
|
126
134
|
// 失败不吞掉已完成任务的结果:模型据此只重派失败的子任务
|
|
@@ -128,9 +136,10 @@ export const taskTool = {
|
|
|
128
136
|
_tag: 'error',
|
|
129
137
|
error: `Sub-agent failed: ${failures.join('; ')}` +
|
|
130
138
|
(completed.length > 0 ? `\n\nCompleted:\n${completed.join('\n\n')}` : ''),
|
|
139
|
+
metadata: { subtasks },
|
|
131
140
|
};
|
|
132
141
|
}
|
|
133
|
-
return { _tag: 'success', output: completed.join('\n\n') };
|
|
142
|
+
return { _tag: 'success', output: completed.join('\n\n'), metadata: { subtasks } };
|
|
134
143
|
}
|
|
135
144
|
// 单任务模式
|
|
136
145
|
const single = inp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c0de-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "Open-source AI coding assistant with Browser-Server architecture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,63 +49,63 @@
|
|
|
49
49
|
"node": ">=22.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "^2.5.
|
|
52
|
+
"@biomejs/biome": "^2.5.15",
|
|
53
53
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
54
|
-
"@codemirror/state": "^6.7.
|
|
54
|
+
"@codemirror/state": "^6.7.6",
|
|
55
55
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
56
|
-
"@codemirror/view": "^6.43.
|
|
57
|
-
"@shikijs/langs": "^4.
|
|
58
|
-
"@shikijs/themes": "^4.
|
|
56
|
+
"@codemirror/view": "^6.43.13",
|
|
57
|
+
"@shikijs/langs": "^4.5.0",
|
|
58
|
+
"@shikijs/themes": "^4.5.0",
|
|
59
59
|
"@testing-library/jest-dom": "^7.0.1",
|
|
60
60
|
"@testing-library/react": "^16.3.3",
|
|
61
61
|
"@testing-library/user-event": "^14.6.7",
|
|
62
|
-
"@types/node": "^26.6.
|
|
62
|
+
"@types/node": "^26.6.4",
|
|
63
63
|
"@types/pg": "^8.23.1",
|
|
64
64
|
"@types/react": "^19.3.0",
|
|
65
65
|
"@types/react-dom": "^19.3.0",
|
|
66
|
-
"@types/ws": "^8.18.
|
|
66
|
+
"@types/ws": "^8.18.2",
|
|
67
67
|
"@vitejs/plugin-react": "^6.1.1",
|
|
68
68
|
"@wyw-in-js/vite": "^2.5.1",
|
|
69
|
-
"drizzle-kit": "^0.31.
|
|
69
|
+
"drizzle-kit": "^0.31.11",
|
|
70
70
|
"happy-dom": "^20.14.5",
|
|
71
|
-
"node-gyp": "^13.0
|
|
72
|
-
"pg": "^8.23.
|
|
73
|
-
"tsx": "^4.23.
|
|
71
|
+
"node-gyp": "^13.1.0",
|
|
72
|
+
"pg": "^8.23.1",
|
|
73
|
+
"tsx": "^4.23.15",
|
|
74
74
|
"typescript": "^6.0.3",
|
|
75
|
-
"vite": "^8.3.
|
|
75
|
+
"vite": "^8.3.2",
|
|
76
76
|
"vite-plugin-pwa": "^1.3.0",
|
|
77
|
-
"vitest": "^5.0.
|
|
77
|
+
"vitest": "^5.0.3"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@codemirror/commands": "^6.11.1",
|
|
81
81
|
"@dnd-kit/core": "^6.3.1",
|
|
82
82
|
"@dnd-kit/sortable": "^10.0.0",
|
|
83
83
|
"@electric-sql/pglite": "^0.5.8",
|
|
84
|
-
"@hono/node-server": "^2.1.
|
|
84
|
+
"@hono/node-server": "^2.1.3",
|
|
85
85
|
"@linaria/core": "^8.2.0",
|
|
86
86
|
"@linaria/react": "^8.2.0",
|
|
87
87
|
"@native-router/core": "^1.17.0",
|
|
88
88
|
"@native-router/react": "^1.16.1",
|
|
89
|
-
"@tanstack/react-query": "^5.
|
|
89
|
+
"@tanstack/react-query": "^5.104.1",
|
|
90
90
|
"@tanstack/react-virtual": "^3.14.13",
|
|
91
91
|
"@xterm/addon-fit": "^0.11.0",
|
|
92
92
|
"@xterm/addon-web-links": "^0.12.0",
|
|
93
93
|
"@xterm/xterm": "^6.0.0",
|
|
94
94
|
"diff": "^9.0.0",
|
|
95
|
-
"drizzle-orm": "^0.45.
|
|
95
|
+
"drizzle-orm": "^0.45.3",
|
|
96
96
|
"fetch-fun": "^0.15.0",
|
|
97
97
|
"fuzzysort": "^4.0.2",
|
|
98
|
-
"haze-ui": "^1.
|
|
99
|
-
"hono": "^4.13.
|
|
100
|
-
"lucide-react": "^1.
|
|
101
|
-
"marked": "^18.0.
|
|
98
|
+
"haze-ui": "^1.31.0",
|
|
99
|
+
"hono": "^4.13.12",
|
|
100
|
+
"lucide-react": "^1.50.0",
|
|
101
|
+
"marked": "^18.0.14",
|
|
102
102
|
"node-pty": "^1.1.0",
|
|
103
103
|
"react": "^19.3.0",
|
|
104
104
|
"react-dom": "^19.3.0",
|
|
105
105
|
"react-use-control": "^1.6.1",
|
|
106
|
-
"shiki": "^4.
|
|
106
|
+
"shiki": "^4.5.0",
|
|
107
107
|
"trash": "^10.1.1",
|
|
108
|
-
"undici": "^8.
|
|
109
|
-
"ws": "^8.
|
|
108
|
+
"undici": "^8.11.2",
|
|
109
|
+
"ws": "^8.22.0"
|
|
110
110
|
}
|
|
111
111
|
}
|