@trim21/personal-pi-extensions 0.1.611 → 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 +2 -2
- 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 +5 -3
- package/src/bwrap/approval-rules.ts +51 -18
- 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 +36 -14
- 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 +55 -22
- 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 +154 -58
- 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 +63 -41
- 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 +23 -9
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +40 -16
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +58 -21
- 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 +10 -5
- package/src/talk/registry.ts +35 -12
- 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
|
|
|
@@ -119,20 +121,21 @@ const LineTrimmedReplacer: Replacer = function* (content, find) {
|
|
|
119
121
|
break;
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
|
-
if (matches) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
if (!matches) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let matchStartIndex = 0;
|
|
128
|
+
for (let k = 0; k < i; k++) {
|
|
129
|
+
matchStartIndex += originalLines[k].length + 1;
|
|
130
|
+
}
|
|
131
|
+
let matchEndIndex = matchStartIndex;
|
|
132
|
+
for (let k = 0; k < searchLines.length; k++) {
|
|
133
|
+
matchEndIndex += originalLines[i + k].length;
|
|
134
|
+
if (k < searchLines.length - 1) {
|
|
135
|
+
matchEndIndex += 1;
|
|
133
136
|
}
|
|
134
|
-
yield content.slice(matchStartIndex, matchEndIndex);
|
|
135
137
|
}
|
|
138
|
+
yield content.slice(matchStartIndex, matchEndIndex);
|
|
136
139
|
}
|
|
137
140
|
};
|
|
138
141
|
|
|
@@ -230,26 +233,29 @@ const BlockAnchorReplacer: Replacer = function* (content, find) {
|
|
|
230
233
|
} else {
|
|
231
234
|
similarity = 1;
|
|
232
235
|
}
|
|
233
|
-
if (similarity
|
|
234
|
-
|
|
235
|
-
bestMatch = candidate;
|
|
236
|
+
if (similarity <= maxSimilarity) {
|
|
237
|
+
continue;
|
|
236
238
|
}
|
|
239
|
+
|
|
240
|
+
maxSimilarity = similarity;
|
|
241
|
+
bestMatch = candidate;
|
|
237
242
|
}
|
|
238
|
-
if (bestMatch
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
if (!bestMatch || maxSimilarity < MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const { startLine, endLine } = bestMatch;
|
|
247
|
+
let matchStartIndex = 0;
|
|
248
|
+
for (let k = 0; k < startLine; k++) {
|
|
249
|
+
matchStartIndex += originalLines[k].length + 1;
|
|
250
|
+
}
|
|
251
|
+
let matchEndIndex = matchStartIndex;
|
|
252
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
253
|
+
matchEndIndex += originalLines[k].length;
|
|
254
|
+
if (k < endLine) {
|
|
255
|
+
matchEndIndex += 1;
|
|
250
256
|
}
|
|
251
|
-
yield content.slice(matchStartIndex, matchEndIndex);
|
|
252
257
|
}
|
|
258
|
+
yield content.slice(matchStartIndex, matchEndIndex);
|
|
253
259
|
};
|
|
254
260
|
|
|
255
261
|
function normalizeWhitespace(text: string): string {
|
|
@@ -297,7 +303,9 @@ const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
|
|
|
297
303
|
function removeIndentation(text: string): string {
|
|
298
304
|
const lines = text.split("\n");
|
|
299
305
|
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
300
|
-
if (nonEmptyLines.length === 0)
|
|
306
|
+
if (nonEmptyLines.length === 0) {
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
301
309
|
const minIndent = Math.min(
|
|
302
310
|
...nonEmptyLines.map((line) => {
|
|
303
311
|
const match = /^(\s*)/.exec(line);
|
|
@@ -323,7 +331,9 @@ const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
|
323
331
|
let startIndex = 0;
|
|
324
332
|
for (;;) {
|
|
325
333
|
const index = content.indexOf(find, startIndex);
|
|
326
|
-
if (index === -1)
|
|
334
|
+
if (index === -1) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
327
337
|
yield find;
|
|
328
338
|
startIndex = index + find.length;
|
|
329
339
|
}
|
|
@@ -359,7 +369,9 @@ const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
359
369
|
const firstLine = findLines[0].trim();
|
|
360
370
|
const lastLine = findLines.at(-1)?.trim() ?? "";
|
|
361
371
|
for (let i = 0; i < contentLines.length; i++) {
|
|
362
|
-
if (contentLines[i].trim() !== firstLine)
|
|
372
|
+
if (contentLines[i].trim() !== firstLine) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
363
375
|
for (let j = i + 2; j < contentLines.length; j++) {
|
|
364
376
|
if (contentLines[j].trim() === lastLine) {
|
|
365
377
|
const blockLines = contentLines.slice(i, j + 1);
|
|
@@ -370,11 +382,13 @@ const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
370
382
|
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
371
383
|
const blockLine = blockLines[k].trim();
|
|
372
384
|
const findLine = findLines[k].trim();
|
|
373
|
-
if (blockLine.length
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
if (blockLine.length === 0 && findLine.length === 0) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
totalNonEmptyLines++;
|
|
390
|
+
if (blockLine === findLine) {
|
|
391
|
+
matchingLines++;
|
|
378
392
|
}
|
|
379
393
|
}
|
|
380
394
|
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
|
@@ -391,8 +405,12 @@ const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
391
405
|
function isDisproportionateMatch(search: string, oldString: string) {
|
|
392
406
|
const oldLines = oldString.split("\n").length;
|
|
393
407
|
const searchLines = search.split("\n").length;
|
|
394
|
-
if (searchLines >= Math.max(oldLines + 3, oldLines * 2))
|
|
395
|
-
|
|
408
|
+
if (searchLines >= Math.max(oldLines + 3, oldLines * 2)) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (oldLines === 1) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
396
414
|
return (
|
|
397
415
|
search.trim().length > Math.max(oldString.trim().length + 500, oldString.trim().length * 4)
|
|
398
416
|
);
|
|
@@ -432,7 +450,9 @@ export function replace(
|
|
|
432
450
|
]) {
|
|
433
451
|
for (const search of replacer(content, oldString)) {
|
|
434
452
|
const index = content.indexOf(search);
|
|
435
|
-
if (index === -1)
|
|
453
|
+
if (index === -1) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
436
456
|
notFound = false;
|
|
437
457
|
if (isDisproportionateMatch(search, oldString)) {
|
|
438
458
|
throw new Error(
|
|
@@ -444,7 +464,9 @@ export function replace(
|
|
|
444
464
|
return content.replaceAll(search, newString);
|
|
445
465
|
}
|
|
446
466
|
const lastIndex = content.lastIndexOf(search);
|
|
447
|
-
if (index !== lastIndex)
|
|
467
|
+
if (index !== lastIndex) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
448
470
|
return (
|
|
449
471
|
content.slice(0, Math.max(0, index)) +
|
|
450
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,17 +92,23 @@ 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
|
-
if (items.length
|
|
92
|
-
|
|
93
|
-
stop();
|
|
99
|
+
if (items.length <= options.limit) {
|
|
100
|
+
return;
|
|
94
101
|
}
|
|
102
|
+
|
|
103
|
+
state.truncated = true;
|
|
104
|
+
stop();
|
|
95
105
|
};
|
|
96
106
|
|
|
97
107
|
child.stderr.setEncoding("utf8");
|
|
98
108
|
child.stderr.on("data", (chunk: string) => {
|
|
99
|
-
if (stderr.length < ERROR_BYTES)
|
|
109
|
+
if (stderr.length < ERROR_BYTES) {
|
|
110
|
+
stderr += chunk;
|
|
111
|
+
}
|
|
100
112
|
});
|
|
101
113
|
|
|
102
114
|
child.stdout.setEncoding("utf8");
|
|
@@ -105,7 +117,9 @@ export function runRipgrep<R>(
|
|
|
105
117
|
const lines = pending.split("\n");
|
|
106
118
|
// 末尾没有换行的部分是不完整记录,留到下一块
|
|
107
119
|
pending = lines.pop() ?? "";
|
|
108
|
-
for (const line of lines)
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
parseLine(line);
|
|
122
|
+
}
|
|
109
123
|
});
|
|
110
124
|
|
|
111
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("")
|
|
@@ -171,11 +181,13 @@ export function extractFirstUserPrompt(
|
|
|
171
181
|
currentPrompt: string,
|
|
172
182
|
): string | undefined {
|
|
173
183
|
const trimmed = currentPrompt.trim();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
for (const entry of branch) {
|
|
185
|
+
if (entry.type !== "message") {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const text = messageText(entry.message?.content);
|
|
189
|
+
if (text) {
|
|
190
|
+
return text;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
return trimmed || undefined;
|
|
@@ -184,8 +196,12 @@ export function extractFirstUserPrompt(
|
|
|
184
196
|
/** 折叠空白、限制长度;空结果返回 undefined */
|
|
185
197
|
export function sanitizeName(raw: string, maxLength = DEFAULT_MAX_LENGTH): string | undefined {
|
|
186
198
|
const collapsed = raw.replaceAll(/\s+/g, " ").trim();
|
|
187
|
-
if (!collapsed)
|
|
188
|
-
|
|
199
|
+
if (!collapsed) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
if (collapsed.length <= maxLength) {
|
|
203
|
+
return collapsed;
|
|
204
|
+
}
|
|
189
205
|
return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
|
|
190
206
|
}
|
|
191
207
|
|
|
@@ -269,7 +285,9 @@ export async function generateSessionName(
|
|
|
269
285
|
);
|
|
270
286
|
if (raw) {
|
|
271
287
|
const name = sanitizeName(raw, maxLength);
|
|
272
|
-
if (name)
|
|
288
|
+
if (name) {
|
|
289
|
+
return name;
|
|
290
|
+
}
|
|
273
291
|
}
|
|
274
292
|
options.onModelFallback?.("model-error");
|
|
275
293
|
} else {
|
|
@@ -309,7 +327,9 @@ export async function nameSession(
|
|
|
309
327
|
}
|
|
310
328
|
},
|
|
311
329
|
});
|
|
312
|
-
if (!name)
|
|
330
|
+
if (!name) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
313
333
|
pi.setSessionName(name);
|
|
314
334
|
if (ctx.hasUI) {
|
|
315
335
|
ctx.notify?.(`会话已命名为: ${name}`);
|
|
@@ -323,9 +343,13 @@ export default function sessionNameExtension(pi: ExtensionAPI) {
|
|
|
323
343
|
// 会话切换 / reload 后捕获的 pi 会抛 stale 错误,被 catch 忽略,名字
|
|
324
344
|
// 绝不会写到错误的 session。
|
|
325
345
|
pi.on("before_agent_start", (event, ctx) => {
|
|
326
|
-
if (pi.getSessionName())
|
|
346
|
+
if (pi.getSessionName()) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
327
349
|
const text = extractFirstUserPrompt(ctx.sessionManager.getBranch(), event.prompt);
|
|
328
|
-
if (!text)
|
|
350
|
+
if (!text) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
329
353
|
void nameSession(pi, text, {
|
|
330
354
|
hasUI: ctx.hasUI,
|
|
331
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
|
}
|