@trim21/personal-pi-extensions 0.1.612 → 0.1.619

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.
Files changed (77) hide show
  1. package/package.json +1 -1
  2. package/src/aft/config.ts +9 -3
  3. package/src/aft/index.ts +6 -2
  4. package/src/aft/logger.ts +9 -3
  5. package/src/aft/tools.ts +3 -1
  6. package/src/bwrap/README.md +19 -10
  7. package/src/bwrap/approval-rules.ts +42 -14
  8. package/src/bwrap/approval-suggest.ts +12 -4
  9. package/src/bwrap/core.ts +39 -13
  10. package/src/bwrap/dcg-scan.ts +16 -6
  11. package/src/bwrap/holder.ts +3 -1
  12. package/src/bwrap/mihomo-config.ts +18 -6
  13. package/src/bwrap/network-stack.ts +69 -24
  14. package/src/bwrap/runtime.ts +30 -10
  15. package/src/bwrap/sandbox.ts +1 -1
  16. package/src/claude-code/common.ts +6 -2
  17. package/src/claude-code/edit-utils.ts +3 -1
  18. package/src/claude-code/files.ts +40 -14
  19. package/src/claude-code/glob.ts +12 -4
  20. package/src/claude-code/grep.ts +41 -14
  21. package/src/claude-code/session-tools.ts +18 -6
  22. package/src/claude-code/shell.ts +9 -3
  23. package/src/gh/base.ts +72 -24
  24. package/src/gh/tools/download-release-assets.ts +12 -4
  25. package/src/gh/tools/list-releases.ts +3 -1
  26. package/src/gh/tools/list-workflow-runs.ts +9 -3
  27. package/src/gh/tools/read-ci-logs.ts +21 -7
  28. package/src/gh/tools/read-repo.ts +3 -1
  29. package/src/lib/cli-args.ts +6 -2
  30. package/src/lib/cli.ts +48 -16
  31. package/src/lib/file-reads.ts +24 -8
  32. package/src/lib/github.ts +35 -12
  33. package/src/lib/lsp/adapter.ts +6 -2
  34. package/src/lib/lsp/bin.ts +12 -4
  35. package/src/lib/lsp/client.ts +205 -69
  36. package/src/lib/lsp/diagnostic.ts +6 -2
  37. package/src/lib/lsp/inspect-tool.ts +25 -9
  38. package/src/lib/lsp/lsp.ts +138 -46
  39. package/src/lib/lsp/rename-tool.ts +11 -4
  40. package/src/lib/lsp/rename.ts +25 -8
  41. package/src/lib/lsp/server-config.ts +41 -14
  42. package/src/lib/lsp/watcher.ts +51 -17
  43. package/src/lib/path.ts +18 -6
  44. package/src/lib/proc.ts +3 -1
  45. package/src/lib/proxy.ts +9 -3
  46. package/src/lib/request-policy.ts +3 -1
  47. package/src/lib/seq-state.ts +3 -1
  48. package/src/lib/ui.ts +23 -8
  49. package/src/lib/write-guard.ts +21 -7
  50. package/src/openai-cost/config.ts +3 -1
  51. package/src/openai-cost/cost.ts +54 -18
  52. package/src/openai-cost/index.ts +3 -1
  53. package/src/openai-cost/provider.ts +6 -2
  54. package/src/opencode/bash.ts +9 -3
  55. package/src/opencode/edit-engine.ts +24 -8
  56. package/src/opencode/files.ts +30 -10
  57. package/src/opencode/glob.ts +3 -1
  58. package/src/opencode/grep.ts +12 -4
  59. package/src/opencode/question.ts +3 -1
  60. package/src/opencode/ripgrep.ts +18 -6
  61. package/src/opencode/todo.ts +3 -1
  62. package/src/session-name.ts +39 -13
  63. package/src/spawn-agent-agents.ts +18 -6
  64. package/src/spawn-agent.ts +53 -18
  65. package/src/system-prompt/index.ts +9 -3
  66. package/src/talk/core.ts +143 -48
  67. package/src/talk/format.ts +12 -4
  68. package/src/talk/group.ts +9 -3
  69. package/src/talk/index.ts +30 -10
  70. package/src/talk/mailbox.ts +30 -10
  71. package/src/talk/policy.ts +6 -2
  72. package/src/talk/registry.ts +33 -11
  73. package/src/talk/storage.ts +3 -1
  74. package/src/vision-agent.ts +41 -14
  75. package/src/web/config.ts +3 -1
  76. package/src/web/fetch.ts +75 -25
  77. 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") return text;
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) return text;
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) break;
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) continue;
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)) return true;
401
- if (oldLines === 1) return false;
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) continue;
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) continue;
467
+ if (index !== lastIndex) {
468
+ continue;
469
+ }
454
470
  return (
455
471
  content.slice(0, Math.max(0, index)) +
456
472
  newString +
@@ -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) return false;
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)) return false;
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) return false;
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) return false;
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) return true;
186
- if (byte < 9 || (byte > 13 && byte < 32)) nonPrintableCount++;
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) return readTailLines(filePath, opts);
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) continue;
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) head += 1;
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) existing = undefined;
829
+ if (bytesRead < 3) {
830
+ existing = undefined;
831
+ }
812
832
  } finally {
813
833
  await fh.close();
814
834
  }
@@ -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) return "No files found";
37
+ if (files.length === 0) {
38
+ return "No files found";
39
+ }
38
40
  const output = [...files];
39
41
  if (truncated) {
40
42
  output.push(
@@ -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) return line;
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) return "No files found";
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 !== "") output.push("");
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 === "") return cwd;
134
+ if (path === undefined || path === "") {
135
+ return cwd;
136
+ }
129
137
  return isAbsolute(path) ? path : join(cwd, path);
130
138
  }
131
139
 
@@ -125,7 +125,9 @@ async function askSingle(
125
125
  ctx.ui,
126
126
  { signal },
127
127
  );
128
- if (result === undefined) return [];
128
+ if (result === undefined) {
129
+ return [];
130
+ }
129
131
  return result.prompted ? (result.input ? [result.input] : []) : [result.label];
130
132
  }
131
133
 
@@ -62,21 +62,27 @@ export function runRipgrep<R>(
62
62
  const items: R[] = [];
63
63
 
64
64
  const stop = () => {
65
- if (state.done) return;
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) return;
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) return;
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) return;
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) stderr += chunk;
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) parseLine(line);
120
+ for (const line of lines) {
121
+ parseLine(line);
122
+ }
111
123
  });
112
124
 
113
125
  child.on("error", (error) => {
@@ -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) return undefined;
151
+ if (visible.length === 0) {
152
+ return undefined;
153
+ }
152
154
  return visible.map((t) => formatTodoLine(t));
153
155
  }
154
156
 
@@ -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) return undefined; // 未配置 sessionName
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") return content.trim();
154
- if (!Array.isArray(content)) return "";
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") return part;
158
- if (isRecord(part) && typeof part.text === "string") return part.text;
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") continue;
185
+ if (entry.type !== "message") {
186
+ continue;
187
+ }
176
188
  const text = messageText(entry.message?.content);
177
- if (text) return 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) return undefined;
186
- if (collapsed.length <= maxLength) return collapsed;
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) return 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) return;
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()) return;
346
+ if (pi.getSessionName()) {
347
+ return;
348
+ }
325
349
  const text = extractFirstUserPrompt(ctx.sessionManager.getBranch(), event.prompt);
326
- if (!text) return;
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")) continue;
122
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
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) continue; // missing name/description or wrong field types → not an agent
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) return undefined;
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) return agents;
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) return "none";
248
+ if (agents.length === 0) {
249
+ return "none";
250
+ }
239
251
  return agents.map((a) => `${a.name}: ${a.description}`).join("; ");
240
252
  }
@@ -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") return part.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) parts.push(`error: ${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) parts.push(`output: ${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) return text;
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) return count.toString();
242
- if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
243
- if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
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) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
250
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
251
- if (usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
252
- if (model) parts.push(model);
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) return undefined;
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) return modelRuntime.getModel(agent.provider, agent.model);
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) lines.push(`thinking ( ${thinkingChars} chars )`);
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) result.model = msg.model;
572
+ if (!result.model) {
573
+ result.model = msg.model;
574
+ }
545
575
  result.stopReason = msg.stopReason;
546
- if (msg.errorMessage) result.errorMessage = 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) onAbort();
566
- else signal.addEventListener("abort", onAbort, { once: true });
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) return "";
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) return "";
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) return "";
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.",