@trim21/personal-pi-extensions 0.1.612 → 0.1.613
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.
- package/package.json +1 -1
- package/src/aft/config.ts +9 -3
- package/src/aft/index.ts +6 -2
- package/src/aft/logger.ts +9 -3
- package/src/aft/tools.ts +3 -1
- package/src/bwrap/approval-rules.ts +42 -14
- package/src/bwrap/approval-suggest.ts +12 -4
- package/src/bwrap/core.ts +39 -13
- package/src/bwrap/dcg-scan.ts +16 -6
- package/src/bwrap/holder.ts +3 -1
- package/src/bwrap/mihomo-config.ts +18 -6
- package/src/bwrap/network-stack.ts +30 -10
- package/src/bwrap/runtime.ts +30 -10
- package/src/claude-code/common.ts +6 -2
- package/src/claude-code/edit-utils.ts +3 -1
- package/src/claude-code/files.ts +40 -14
- package/src/claude-code/glob.ts +12 -4
- package/src/claude-code/grep.ts +41 -14
- package/src/claude-code/session-tools.ts +18 -6
- package/src/claude-code/shell.ts +9 -3
- package/src/gh/base.ts +72 -24
- package/src/gh/tools/download-release-assets.ts +12 -4
- package/src/gh/tools/list-releases.ts +3 -1
- package/src/gh/tools/list-workflow-runs.ts +9 -3
- package/src/gh/tools/read-ci-logs.ts +21 -7
- package/src/gh/tools/read-repo.ts +3 -1
- package/src/lib/cli-args.ts +6 -2
- package/src/lib/cli.ts +48 -16
- package/src/lib/file-reads.ts +24 -8
- package/src/lib/github.ts +35 -12
- package/src/lib/lsp/adapter.ts +6 -2
- package/src/lib/lsp/bin.ts +12 -4
- package/src/lib/lsp/client.ts +205 -69
- package/src/lib/lsp/diagnostic.ts +6 -2
- package/src/lib/lsp/inspect-tool.ts +25 -9
- package/src/lib/lsp/lsp.ts +138 -46
- package/src/lib/lsp/rename-tool.ts +11 -4
- package/src/lib/lsp/rename.ts +25 -8
- package/src/lib/lsp/server-config.ts +41 -14
- package/src/lib/lsp/watcher.ts +51 -17
- package/src/lib/path.ts +18 -6
- package/src/lib/proc.ts +3 -1
- package/src/lib/proxy.ts +9 -3
- package/src/lib/request-policy.ts +3 -1
- package/src/lib/seq-state.ts +3 -1
- package/src/lib/ui.ts +23 -8
- package/src/lib/write-guard.ts +21 -7
- package/src/openai-cost/config.ts +3 -1
- package/src/openai-cost/cost.ts +54 -18
- package/src/openai-cost/index.ts +3 -1
- package/src/openai-cost/provider.ts +6 -2
- package/src/opencode/bash.ts +9 -3
- package/src/opencode/edit-engine.ts +24 -8
- package/src/opencode/files.ts +30 -10
- package/src/opencode/glob.ts +3 -1
- package/src/opencode/grep.ts +12 -4
- package/src/opencode/question.ts +3 -1
- package/src/opencode/ripgrep.ts +18 -6
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +39 -13
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +53 -18
- package/src/system-prompt/index.ts +9 -3
- package/src/talk/core.ts +143 -48
- package/src/talk/format.ts +12 -4
- package/src/talk/group.ts +9 -3
- package/src/talk/index.ts +30 -10
- package/src/talk/mailbox.ts +30 -10
- package/src/talk/policy.ts +6 -2
- package/src/talk/registry.ts +33 -11
- package/src/talk/storage.ts +3 -1
- package/src/vision-agent.ts +41 -14
- package/src/web/config.ts +3 -1
- package/src/web/fetch.ts +75 -25
- package/src/web/search.ts +30 -10
|
@@ -28,7 +28,9 @@ export function normalizeToLF(text: string): string {
|
|
|
28
28
|
|
|
29
29
|
/** Convert LF text to the file's line ending. Input must already be LF. */
|
|
30
30
|
export function convertToLineEnding(text: string, ending: "\r\n" | "\n"): string {
|
|
31
|
-
if (ending === "\n")
|
|
31
|
+
if (ending === "\n") {
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
32
34
|
return text.replaceAll("\n", "\r\n");
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -301,7 +303,9 @@ const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
|
|
|
301
303
|
function removeIndentation(text: string): string {
|
|
302
304
|
const lines = text.split("\n");
|
|
303
305
|
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
304
|
-
if (nonEmptyLines.length === 0)
|
|
306
|
+
if (nonEmptyLines.length === 0) {
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
305
309
|
const minIndent = Math.min(
|
|
306
310
|
...nonEmptyLines.map((line) => {
|
|
307
311
|
const match = /^(\s*)/.exec(line);
|
|
@@ -327,7 +331,9 @@ const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
|
327
331
|
let startIndex = 0;
|
|
328
332
|
for (;;) {
|
|
329
333
|
const index = content.indexOf(find, startIndex);
|
|
330
|
-
if (index === -1)
|
|
334
|
+
if (index === -1) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
331
337
|
yield find;
|
|
332
338
|
startIndex = index + find.length;
|
|
333
339
|
}
|
|
@@ -363,7 +369,9 @@ const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
363
369
|
const firstLine = findLines[0].trim();
|
|
364
370
|
const lastLine = findLines.at(-1)?.trim() ?? "";
|
|
365
371
|
for (let i = 0; i < contentLines.length; i++) {
|
|
366
|
-
if (contentLines[i].trim() !== firstLine)
|
|
372
|
+
if (contentLines[i].trim() !== firstLine) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
367
375
|
for (let j = i + 2; j < contentLines.length; j++) {
|
|
368
376
|
if (contentLines[j].trim() === lastLine) {
|
|
369
377
|
const blockLines = contentLines.slice(i, j + 1);
|
|
@@ -397,8 +405,12 @@ const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
397
405
|
function isDisproportionateMatch(search: string, oldString: string) {
|
|
398
406
|
const oldLines = oldString.split("\n").length;
|
|
399
407
|
const searchLines = search.split("\n").length;
|
|
400
|
-
if (searchLines >= Math.max(oldLines + 3, oldLines * 2))
|
|
401
|
-
|
|
408
|
+
if (searchLines >= Math.max(oldLines + 3, oldLines * 2)) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (oldLines === 1) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
402
414
|
return (
|
|
403
415
|
search.trim().length > Math.max(oldString.trim().length + 500, oldString.trim().length * 4)
|
|
404
416
|
);
|
|
@@ -438,7 +450,9 @@ export function replace(
|
|
|
438
450
|
]) {
|
|
439
451
|
for (const search of replacer(content, oldString)) {
|
|
440
452
|
const index = content.indexOf(search);
|
|
441
|
-
if (index === -1)
|
|
453
|
+
if (index === -1) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
442
456
|
notFound = false;
|
|
443
457
|
if (isDisproportionateMatch(search, oldString)) {
|
|
444
458
|
throw new Error(
|
|
@@ -450,7 +464,9 @@ export function replace(
|
|
|
450
464
|
return content.replaceAll(search, newString);
|
|
451
465
|
}
|
|
452
466
|
const lastIndex = content.lastIndexOf(search);
|
|
453
|
-
if (index !== lastIndex)
|
|
467
|
+
if (index !== lastIndex) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
454
470
|
return (
|
|
455
471
|
content.slice(0, Math.max(0, index)) +
|
|
456
472
|
newString +
|
package/src/opencode/files.ts
CHANGED
|
@@ -134,9 +134,13 @@ const IMAGE_SIGNATURES: {
|
|
|
134
134
|
];
|
|
135
135
|
|
|
136
136
|
function startsWithAscii(buf: Uint8Array, offset: number, text: string): boolean {
|
|
137
|
-
if (buf.length < offset + text.length)
|
|
137
|
+
if (buf.length < offset + text.length) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
138
140
|
for (let i = 0; i < text.length; i++) {
|
|
139
|
-
if (buf[offset + i] !== text.codePointAt(i))
|
|
141
|
+
if (buf[offset + i] !== text.codePointAt(i)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
140
144
|
}
|
|
141
145
|
return true;
|
|
142
146
|
}
|
|
@@ -151,7 +155,9 @@ function detectImageMimeType(buffer: Uint8Array): string | null {
|
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
function startsWith(buffer: Uint8Array, bytes: Uint8Array): boolean {
|
|
154
|
-
if (buffer.length < bytes.length)
|
|
158
|
+
if (buffer.length < bytes.length) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
155
161
|
return bytes.every((b, i) => buffer[i] === b);
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -179,11 +185,17 @@ function isBinaryExtension(filePath: string): boolean {
|
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
function isBinaryFileBySample(sample: Uint8Array): boolean {
|
|
182
|
-
if (sample.length === 0)
|
|
188
|
+
if (sample.length === 0) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
183
191
|
let nonPrintableCount = 0;
|
|
184
192
|
for (const byte of sample) {
|
|
185
|
-
if (byte === 0)
|
|
186
|
-
|
|
193
|
+
if (byte === 0) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (byte < 9 || (byte > 13 && byte < 32)) {
|
|
197
|
+
nonPrintableCount++;
|
|
198
|
+
}
|
|
187
199
|
}
|
|
188
200
|
return nonPrintableCount / sample.length > 0.3;
|
|
189
201
|
}
|
|
@@ -222,7 +234,9 @@ export async function readLines(
|
|
|
222
234
|
filePath: string,
|
|
223
235
|
opts: { offset: number; limit: number; maxBytes?: number; signal?: AbortSignal },
|
|
224
236
|
): Promise<LinePage> {
|
|
225
|
-
if (opts.offset < 0)
|
|
237
|
+
if (opts.offset < 0) {
|
|
238
|
+
return readTailLines(filePath, opts);
|
|
239
|
+
}
|
|
226
240
|
const start = opts.offset - 1;
|
|
227
241
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
228
242
|
const raw: string[] = [];
|
|
@@ -238,7 +252,9 @@ export async function readLines(
|
|
|
238
252
|
// 大文件的全量扫描可能持续数秒:每行检查一次取消,别等到读完才响应
|
|
239
253
|
opts.signal?.throwIfAborted();
|
|
240
254
|
count += 1;
|
|
241
|
-
if (count <= start)
|
|
255
|
+
if (count <= start) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
242
258
|
|
|
243
259
|
if (raw.length >= opts.limit) {
|
|
244
260
|
more = true;
|
|
@@ -287,7 +303,9 @@ async function readTailLines(
|
|
|
287
303
|
const line =
|
|
288
304
|
text.length > MAX_LINE_LENGTH ? text.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : text;
|
|
289
305
|
window.push(line);
|
|
290
|
-
if (window.length > windowSize)
|
|
306
|
+
if (window.length > windowSize) {
|
|
307
|
+
head += 1;
|
|
308
|
+
}
|
|
291
309
|
}
|
|
292
310
|
} finally {
|
|
293
311
|
rl.close();
|
|
@@ -808,7 +826,9 @@ function registerWriteTool(
|
|
|
808
826
|
try {
|
|
809
827
|
existing = Buffer.alloc(3);
|
|
810
828
|
const { bytesRead } = await fh.read(existing, 0, 3, 0);
|
|
811
|
-
if (bytesRead < 3)
|
|
829
|
+
if (bytesRead < 3) {
|
|
830
|
+
existing = undefined;
|
|
831
|
+
}
|
|
812
832
|
} finally {
|
|
813
833
|
await fh.close();
|
|
814
834
|
}
|
package/src/opencode/glob.ts
CHANGED
|
@@ -34,7 +34,9 @@ function normalizeRipgrepPath(text: string): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function renderGlobOutput(files: readonly string[], truncated: boolean): string {
|
|
37
|
-
if (files.length === 0)
|
|
37
|
+
if (files.length === 0) {
|
|
38
|
+
return "No files found";
|
|
39
|
+
}
|
|
38
40
|
const output = [...files];
|
|
39
41
|
if (truncated) {
|
|
40
42
|
output.push(
|
package/src/opencode/grep.ts
CHANGED
|
@@ -57,7 +57,9 @@ function normalizeRipgrepPath(text: string): string {
|
|
|
57
57
|
/** 行文本:去掉行尾换行,超长截断(不留半个 surrogate pair)。 */
|
|
58
58
|
function normalizeLineText(text: string): string {
|
|
59
59
|
const line = text.replace(/[\r\n]+$/u, "");
|
|
60
|
-
if (line.length <= MAX_LINE_LENGTH)
|
|
60
|
+
if (line.length <= MAX_LINE_LENGTH) {
|
|
61
|
+
return line;
|
|
62
|
+
}
|
|
61
63
|
return line.slice(0, MAX_LINE_LENGTH).replace(/[\uD800-\uDBFF]$/u, "") + "...";
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -106,12 +108,16 @@ export function buildGrepArgs(
|
|
|
106
108
|
|
|
107
109
|
/** 渲染搜索结果:上游格式,按文件分组。 */
|
|
108
110
|
export function renderGrepOutput(matches: readonly GrepMatch[], truncated: boolean): string {
|
|
109
|
-
if (matches.length === 0)
|
|
111
|
+
if (matches.length === 0) {
|
|
112
|
+
return "No files found";
|
|
113
|
+
}
|
|
110
114
|
const output = [`Found ${matches.length} matches${truncated ? " (more matches available)" : ""}`];
|
|
111
115
|
let current = "";
|
|
112
116
|
for (const match of matches) {
|
|
113
117
|
if (current !== match.path) {
|
|
114
|
-
if (current !== "")
|
|
118
|
+
if (current !== "") {
|
|
119
|
+
output.push("");
|
|
120
|
+
}
|
|
115
121
|
current = match.path;
|
|
116
122
|
output.push(`${match.path}:`);
|
|
117
123
|
}
|
|
@@ -125,7 +131,9 @@ export function renderGrepOutput(matches: readonly GrepMatch[], truncated: boole
|
|
|
125
131
|
|
|
126
132
|
/** 搜索根:绝对路径原样,相对路径按调用 cwd 解析。 */
|
|
127
133
|
export function resolveSearchRoot(path: string | undefined, cwd: string): string {
|
|
128
|
-
if (path === undefined || path === "")
|
|
134
|
+
if (path === undefined || path === "") {
|
|
135
|
+
return cwd;
|
|
136
|
+
}
|
|
129
137
|
return isAbsolute(path) ? path : join(cwd, path);
|
|
130
138
|
}
|
|
131
139
|
|
package/src/opencode/question.ts
CHANGED
|
@@ -125,7 +125,9 @@ async function askSingle(
|
|
|
125
125
|
ctx.ui,
|
|
126
126
|
{ signal },
|
|
127
127
|
);
|
|
128
|
-
if (result === undefined)
|
|
128
|
+
if (result === undefined) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
129
131
|
return result.prompted ? (result.input ? [result.input] : []) : [result.label];
|
|
130
132
|
}
|
|
131
133
|
|
package/src/opencode/ripgrep.ts
CHANGED
|
@@ -62,21 +62,27 @@ export function runRipgrep<R>(
|
|
|
62
62
|
const items: R[] = [];
|
|
63
63
|
|
|
64
64
|
const stop = () => {
|
|
65
|
-
if (state.done)
|
|
65
|
+
if (state.done) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
66
68
|
state.done = true;
|
|
67
69
|
child.kill("SIGTERM");
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
/** 收尾(resolve/reject 只会发生一次,并摘掉 abort 监听)。 */
|
|
71
73
|
const settle = (finish: () => void) => {
|
|
72
|
-
if (state.settled)
|
|
74
|
+
if (state.settled) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
73
77
|
state.settled = true;
|
|
74
78
|
signal?.removeEventListener("abort", onAbort);
|
|
75
79
|
finish();
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
const parseLine = (line: string) => {
|
|
79
|
-
if (state.done || line.length === 0)
|
|
83
|
+
if (state.done || line.length === 0) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
80
86
|
let item: R | undefined;
|
|
81
87
|
try {
|
|
82
88
|
item = options.parse(line);
|
|
@@ -86,7 +92,9 @@ export function runRipgrep<R>(
|
|
|
86
92
|
stop();
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
89
|
-
if (item === undefined)
|
|
95
|
+
if (item === undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
90
98
|
items.push(item);
|
|
91
99
|
if (items.length <= options.limit) {
|
|
92
100
|
return;
|
|
@@ -98,7 +106,9 @@ export function runRipgrep<R>(
|
|
|
98
106
|
|
|
99
107
|
child.stderr.setEncoding("utf8");
|
|
100
108
|
child.stderr.on("data", (chunk: string) => {
|
|
101
|
-
if (stderr.length < ERROR_BYTES)
|
|
109
|
+
if (stderr.length < ERROR_BYTES) {
|
|
110
|
+
stderr += chunk;
|
|
111
|
+
}
|
|
102
112
|
});
|
|
103
113
|
|
|
104
114
|
child.stdout.setEncoding("utf8");
|
|
@@ -107,7 +117,9 @@ export function runRipgrep<R>(
|
|
|
107
117
|
const lines = pending.split("\n");
|
|
108
118
|
// 末尾没有换行的部分是不完整记录,留到下一块
|
|
109
119
|
pending = lines.pop() ?? "";
|
|
110
|
-
for (const line of lines)
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
parseLine(line);
|
|
122
|
+
}
|
|
111
123
|
});
|
|
112
124
|
|
|
113
125
|
child.on("error", (error) => {
|
package/src/opencode/todo.ts
CHANGED
|
@@ -148,7 +148,9 @@ export function buildTodoMarkdown(todos: readonly TodoInfo[]): string {
|
|
|
148
148
|
*/
|
|
149
149
|
export function buildTodoWidgetLines(todos: readonly TodoInfo[]): string[] | undefined {
|
|
150
150
|
const visible = todos;
|
|
151
|
-
if (visible.length === 0)
|
|
151
|
+
if (visible.length === 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
152
154
|
return visible.map((t) => formatTodoLine(t));
|
|
153
155
|
}
|
|
154
156
|
|
package/src/session-name.ts
CHANGED
|
@@ -132,7 +132,9 @@ export function loadSessionNameConfig(settingsPath = SETTINGS_PATH): SessionName
|
|
|
132
132
|
return undefined; // 结构不符(非 object / sessionName 非 object 等)
|
|
133
133
|
}
|
|
134
134
|
const sn = settings.sessionName;
|
|
135
|
-
if (!sn)
|
|
135
|
+
if (!sn) {
|
|
136
|
+
return undefined; // 未配置 sessionName
|
|
137
|
+
}
|
|
136
138
|
return {
|
|
137
139
|
provider: nonEmpty(sn.provider) ?? nonEmpty(settings.defaultProvider),
|
|
138
140
|
model: nonEmpty(sn.model),
|
|
@@ -150,12 +152,20 @@ function nonEmpty(value: string | undefined): string | undefined {
|
|
|
150
152
|
|
|
151
153
|
/** 从消息 content(字符串或分片数组)提取文本 */
|
|
152
154
|
function messageText(content: unknown): string {
|
|
153
|
-
if (typeof content === "string")
|
|
154
|
-
|
|
155
|
+
if (typeof content === "string") {
|
|
156
|
+
return content.trim();
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(content)) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
155
161
|
return content
|
|
156
162
|
.map((part: unknown) => {
|
|
157
|
-
if (typeof part === "string")
|
|
158
|
-
|
|
163
|
+
if (typeof part === "string") {
|
|
164
|
+
return part;
|
|
165
|
+
}
|
|
166
|
+
if (isRecord(part) && typeof part.text === "string") {
|
|
167
|
+
return part.text;
|
|
168
|
+
}
|
|
159
169
|
return "";
|
|
160
170
|
})
|
|
161
171
|
.join("")
|
|
@@ -172,9 +182,13 @@ export function extractFirstUserPrompt(
|
|
|
172
182
|
): string | undefined {
|
|
173
183
|
const trimmed = currentPrompt.trim();
|
|
174
184
|
for (const entry of branch) {
|
|
175
|
-
if (entry.type !== "message")
|
|
185
|
+
if (entry.type !== "message") {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
176
188
|
const text = messageText(entry.message?.content);
|
|
177
|
-
if (text)
|
|
189
|
+
if (text) {
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
return trimmed || undefined;
|
|
180
194
|
}
|
|
@@ -182,8 +196,12 @@ export function extractFirstUserPrompt(
|
|
|
182
196
|
/** 折叠空白、限制长度;空结果返回 undefined */
|
|
183
197
|
export function sanitizeName(raw: string, maxLength = DEFAULT_MAX_LENGTH): string | undefined {
|
|
184
198
|
const collapsed = raw.replaceAll(/\s+/g, " ").trim();
|
|
185
|
-
if (!collapsed)
|
|
186
|
-
|
|
199
|
+
if (!collapsed) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
if (collapsed.length <= maxLength) {
|
|
203
|
+
return collapsed;
|
|
204
|
+
}
|
|
187
205
|
return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -267,7 +285,9 @@ export async function generateSessionName(
|
|
|
267
285
|
);
|
|
268
286
|
if (raw) {
|
|
269
287
|
const name = sanitizeName(raw, maxLength);
|
|
270
|
-
if (name)
|
|
288
|
+
if (name) {
|
|
289
|
+
return name;
|
|
290
|
+
}
|
|
271
291
|
}
|
|
272
292
|
options.onModelFallback?.("model-error");
|
|
273
293
|
} else {
|
|
@@ -307,7 +327,9 @@ export async function nameSession(
|
|
|
307
327
|
}
|
|
308
328
|
},
|
|
309
329
|
});
|
|
310
|
-
if (!name)
|
|
330
|
+
if (!name) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
311
333
|
pi.setSessionName(name);
|
|
312
334
|
if (ctx.hasUI) {
|
|
313
335
|
ctx.notify?.(`会话已命名为: ${name}`);
|
|
@@ -321,9 +343,13 @@ export default function sessionNameExtension(pi: ExtensionAPI) {
|
|
|
321
343
|
// 会话切换 / reload 后捕获的 pi 会抛 stale 错误,被 catch 忽略,名字
|
|
322
344
|
// 绝不会写到错误的 session。
|
|
323
345
|
pi.on("before_agent_start", (event, ctx) => {
|
|
324
|
-
if (pi.getSessionName())
|
|
346
|
+
if (pi.getSessionName()) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
325
349
|
const text = extractFirstUserPrompt(ctx.sessionManager.getBranch(), event.prompt);
|
|
326
|
-
if (!text)
|
|
350
|
+
if (!text) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
327
353
|
void nameSession(pi, text, {
|
|
328
354
|
hasUI: ctx.hasUI,
|
|
329
355
|
notify: (message, level = "info") => ctx.ui.notify(message, level),
|
|
@@ -118,8 +118,12 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
|
|
|
118
118
|
|
|
119
119
|
const agents: AgentConfig[] = [];
|
|
120
120
|
for (const entry of entries) {
|
|
121
|
-
if (!entry.name.endsWith(".md"))
|
|
122
|
-
|
|
121
|
+
if (!entry.name.endsWith(".md")) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
123
127
|
|
|
124
128
|
const filePath = join(dir, entry.name);
|
|
125
129
|
let content: string;
|
|
@@ -131,7 +135,9 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
|
|
|
131
135
|
|
|
132
136
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
133
137
|
const fm = parseAgentFrontmatter(frontmatter);
|
|
134
|
-
if (!fm)
|
|
138
|
+
if (!fm) {
|
|
139
|
+
continue; // missing name/description or wrong field types → not an agent
|
|
140
|
+
}
|
|
135
141
|
|
|
136
142
|
agents.push({
|
|
137
143
|
name: fm.name,
|
|
@@ -172,7 +178,9 @@ export function loadSpawnAgentConfig(
|
|
|
172
178
|
} catch {
|
|
173
179
|
return undefined; // 结构不符(非 object / thinkingLevel 非法等)
|
|
174
180
|
}
|
|
175
|
-
if (!config.provider && !config.model && !config.thinkingLevel)
|
|
181
|
+
if (!config.provider && !config.model && !config.thinkingLevel) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
176
184
|
|
|
177
185
|
let fallback: SpawnAgentSettingsFallback | undefined;
|
|
178
186
|
try {
|
|
@@ -225,7 +233,9 @@ export function applyAgentDefaults(
|
|
|
225
233
|
agents: AgentConfig[],
|
|
226
234
|
defaults: SpawnAgentDefaults | undefined,
|
|
227
235
|
): AgentConfig[] {
|
|
228
|
-
if (!defaults)
|
|
236
|
+
if (!defaults) {
|
|
237
|
+
return agents;
|
|
238
|
+
}
|
|
229
239
|
return agents.map((agent) => ({
|
|
230
240
|
...agent,
|
|
231
241
|
provider: agent.provider ?? defaults.provider,
|
|
@@ -235,6 +245,8 @@ export function applyAgentDefaults(
|
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
export function formatAgentList(agents: AgentConfig[]): string {
|
|
238
|
-
if (agents.length === 0)
|
|
248
|
+
if (agents.length === 0) {
|
|
249
|
+
return "none";
|
|
250
|
+
}
|
|
239
251
|
return agents.map((a) => `${a.name}: ${a.description}`).join("; ");
|
|
240
252
|
}
|
package/src/spawn-agent.ts
CHANGED
|
@@ -189,7 +189,9 @@ function getFinalOutput(messages: AgentMessage[]): string {
|
|
|
189
189
|
const msg = messages[i];
|
|
190
190
|
if (msg.role === "assistant") {
|
|
191
191
|
for (const part of msg.content) {
|
|
192
|
-
if (part.type === "text")
|
|
192
|
+
if (part.type === "text") {
|
|
193
|
+
return part.text;
|
|
194
|
+
}
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -205,14 +207,18 @@ export function formatSubagentError(result: SubagentResult): { reason: string; m
|
|
|
205
207
|
const reason =
|
|
206
208
|
result.stopReason ?? (result.exitCode === 0 ? "failed" : `exit ${result.exitCode}`);
|
|
207
209
|
const parts: string[] = [];
|
|
208
|
-
if (result.errorMessage)
|
|
210
|
+
if (result.errorMessage) {
|
|
211
|
+
parts.push(`error: ${result.errorMessage}`);
|
|
212
|
+
}
|
|
209
213
|
const stderr = truncateTail(result.stderr, { maxBytes: MAX_STDERR_ERROR_BYTES });
|
|
210
214
|
if (stderr.content.trim()) {
|
|
211
215
|
const truncatedMark = stderr.truncated ? "\n[stderr truncated]" : "";
|
|
212
216
|
parts.push(`stderr: ${stderr.content.trim()}${truncatedMark}`);
|
|
213
217
|
}
|
|
214
218
|
const output = getFinalOutput(result.messages);
|
|
215
|
-
if (output)
|
|
219
|
+
if (output) {
|
|
220
|
+
parts.push(`output: ${output}`);
|
|
221
|
+
}
|
|
216
222
|
return { reason, message: parts.length > 0 ? parts.join("\n") : "(no output)" };
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -223,7 +229,9 @@ export function formatSubagentError(result: SubagentResult): { reason: string; m
|
|
|
223
229
|
* returned as-is.
|
|
224
230
|
*/
|
|
225
231
|
function foldProgressLine(text: string): string {
|
|
226
|
-
if (text.length <= MAX_PROGRESS_CHARS_PER_LINE)
|
|
232
|
+
if (text.length <= MAX_PROGRESS_CHARS_PER_LINE) {
|
|
233
|
+
return text;
|
|
234
|
+
}
|
|
227
235
|
const keep = Math.floor((MAX_PROGRESS_CHARS_PER_LINE - 3) / 2);
|
|
228
236
|
return `${text.slice(0, keep)} … ${text.slice(-keep)}`;
|
|
229
237
|
}
|
|
@@ -238,18 +246,32 @@ function sanitizeProgressLine(text: string): string {
|
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
function formatTokens(count: number): string {
|
|
241
|
-
if (count < 1000)
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
if (count < 1000) {
|
|
250
|
+
return count.toString();
|
|
251
|
+
}
|
|
252
|
+
if (count < 10_000) {
|
|
253
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
254
|
+
}
|
|
255
|
+
if (count < 1_000_000) {
|
|
256
|
+
return `${Math.round(count / 1000)}k`;
|
|
257
|
+
}
|
|
244
258
|
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
function formatUsageStats(usage: UsageStats, model?: string): string {
|
|
248
262
|
const parts: string[] = [];
|
|
249
|
-
if (usage.turns)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
263
|
+
if (usage.turns) {
|
|
264
|
+
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
265
|
+
}
|
|
266
|
+
if (usage.cost) {
|
|
267
|
+
parts.push(`$${usage.cost.toFixed(4)}`);
|
|
268
|
+
}
|
|
269
|
+
if (usage.contextTokens > 0) {
|
|
270
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
271
|
+
}
|
|
272
|
+
if (model) {
|
|
273
|
+
parts.push(model);
|
|
274
|
+
}
|
|
253
275
|
return parts.join(" ");
|
|
254
276
|
}
|
|
255
277
|
|
|
@@ -326,12 +348,16 @@ export function resolveModel(
|
|
|
326
348
|
agent: AgentConfig,
|
|
327
349
|
settingsManager: SettingsManager,
|
|
328
350
|
): ReturnType<ModelRuntime["getModel"]> {
|
|
329
|
-
if (!agent.model)
|
|
351
|
+
if (!agent.model) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
330
354
|
const slash = agent.model.indexOf("/");
|
|
331
355
|
if (slash > 0) {
|
|
332
356
|
return modelRuntime.getModel(agent.model.slice(0, slash), agent.model.slice(slash + 1));
|
|
333
357
|
}
|
|
334
|
-
if (agent.provider)
|
|
358
|
+
if (agent.provider) {
|
|
359
|
+
return modelRuntime.getModel(agent.provider, agent.model);
|
|
360
|
+
}
|
|
335
361
|
const defaultProvider = settingsManager.getDefaultProvider();
|
|
336
362
|
return defaultProvider ? modelRuntime.getModel(defaultProvider, agent.model) : undefined;
|
|
337
363
|
}
|
|
@@ -487,7 +513,9 @@ export async function runAgent(
|
|
|
487
513
|
const name = sanitizeProgressLine(result.agent);
|
|
488
514
|
const footer = usageLine ? `\`${name}\` ${usageLine}` : `\`${name}\``;
|
|
489
515
|
const lines = [...logLines];
|
|
490
|
-
if (thinkingChars !== undefined)
|
|
516
|
+
if (thinkingChars !== undefined) {
|
|
517
|
+
lines.push(`thinking ( ${thinkingChars} chars )`);
|
|
518
|
+
}
|
|
491
519
|
lines.push(footer);
|
|
492
520
|
onUpdate?.({
|
|
493
521
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -541,9 +569,13 @@ export async function runAgent(
|
|
|
541
569
|
result.usage.turns++;
|
|
542
570
|
result.usage.cost += msg.usage.cost.total;
|
|
543
571
|
result.usage.contextTokens = msg.usage.totalTokens;
|
|
544
|
-
if (!result.model)
|
|
572
|
+
if (!result.model) {
|
|
573
|
+
result.model = msg.model;
|
|
574
|
+
}
|
|
545
575
|
result.stopReason = msg.stopReason;
|
|
546
|
-
if (msg.errorMessage)
|
|
576
|
+
if (msg.errorMessage) {
|
|
577
|
+
result.errorMessage = msg.errorMessage;
|
|
578
|
+
}
|
|
547
579
|
}
|
|
548
580
|
emitUpdate();
|
|
549
581
|
|
|
@@ -562,8 +594,11 @@ export async function runAgent(
|
|
|
562
594
|
void session.abort();
|
|
563
595
|
};
|
|
564
596
|
if (signal) {
|
|
565
|
-
if (signal.aborted)
|
|
566
|
-
|
|
597
|
+
if (signal.aborted) {
|
|
598
|
+
onAbort();
|
|
599
|
+
} else {
|
|
600
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
601
|
+
}
|
|
567
602
|
}
|
|
568
603
|
|
|
569
604
|
try {
|
|
@@ -78,7 +78,9 @@ export function formatGuidelines(promptGuidelines: string[] | undefined): string
|
|
|
78
78
|
.map((g) => g.trim())
|
|
79
79
|
.filter((g) => g.length > 0)
|
|
80
80
|
.toSorted((a, b) => a.localeCompare(b));
|
|
81
|
-
if (items.length === 0)
|
|
81
|
+
if (items.length === 0) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
82
84
|
return `## Guidelines\n\n${items.join("\n\n")}`;
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -86,7 +88,9 @@ export function formatGuidelines(promptGuidelines: string[] | undefined): string
|
|
|
86
88
|
export function formatContextFiles(
|
|
87
89
|
contextFiles: { path: string; content: string }[] | undefined,
|
|
88
90
|
): string {
|
|
89
|
-
if (!contextFiles || contextFiles.length === 0)
|
|
91
|
+
if (!contextFiles || contextFiles.length === 0) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
90
94
|
const inner = contextFiles
|
|
91
95
|
.toSorted((a, b) => a.path.localeCompare(b.path))
|
|
92
96
|
.map(
|
|
@@ -105,7 +109,9 @@ export function formatSkills(skills: SkillLike[] | undefined): string {
|
|
|
105
109
|
const visible = (skills ?? [])
|
|
106
110
|
.filter((skill) => !skill.disableModelInvocation)
|
|
107
111
|
.toSorted((a, b) => a.name.localeCompare(b.name) || a.filePath.localeCompare(b.filePath));
|
|
108
|
-
if (visible.length === 0)
|
|
112
|
+
if (visible.length === 0) {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
109
115
|
const lines = [
|
|
110
116
|
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
111
117
|
"Use the read tool to load a skill's file when the task matches its description.",
|