@victor-software-house/pi-acp 0.1.2 → 0.3.0

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/dist/index.mjs CHANGED
@@ -41,6 +41,62 @@ function resolveTerminalLaunchCommand() {
41
41
  };
42
42
  }
43
43
  //#endregion
44
+ //#region src/acp/auth-required.ts
45
+ /**
46
+ * Detect common auth/credential errors from pi and surface them as ACP AUTH_REQUIRED.
47
+ */
48
+ const AUTH_ERROR_PATTERNS = [
49
+ "api key",
50
+ "apikey",
51
+ "missing key",
52
+ "no key",
53
+ "not configured",
54
+ "unauthorized",
55
+ "authentication",
56
+ "permission denied",
57
+ "forbidden",
58
+ "401",
59
+ "403"
60
+ ];
61
+ function detectAuthError(err) {
62
+ const lower = (err instanceof Error ? err.message : String(err ?? "")).toLowerCase();
63
+ if (!AUTH_ERROR_PATTERNS.some((p) => lower.includes(p))) return null;
64
+ return RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
65
+ }
66
+ //#endregion
67
+ //#region src/acp/client-capabilities.ts
68
+ /**
69
+ * Extract well-known capability flags from ACP `ClientCapabilities`.
70
+ *
71
+ * Reads from:
72
+ * - `_meta.terminal_output` (terminal output rendering)
73
+ * - `_meta.terminal-auth` (terminal auth with command metadata)
74
+ * - `auth._meta.gateway` (gateway auth, future use)
75
+ */
76
+ function parseClientCapabilities(caps) {
77
+ if (caps === void 0 || caps === null) return {
78
+ terminalOutput: false,
79
+ terminalAuth: false,
80
+ gatewayAuth: false
81
+ };
82
+ const meta = caps._meta;
83
+ const terminalOutput = typeof meta === "object" && meta !== null && meta["terminal_output"] === true;
84
+ const terminalAuth = typeof meta === "object" && meta !== null && meta["terminal-auth"] === true;
85
+ let gatewayAuth = false;
86
+ if ("auth" in caps) {
87
+ const auth = caps.auth;
88
+ if (typeof auth === "object" && auth !== null && "_meta" in auth) {
89
+ const authMeta = auth._meta;
90
+ if (typeof authMeta === "object" && authMeta !== null && "gateway" in authMeta) gatewayAuth = authMeta["gateway"] === true;
91
+ }
92
+ }
93
+ return {
94
+ terminalOutput,
95
+ terminalAuth,
96
+ gatewayAuth
97
+ };
98
+ }
99
+ //#endregion
44
100
  //#region src/acp/pi-settings.ts
45
101
  /**
46
102
  * Read pi settings from global and project config files.
@@ -100,62 +156,77 @@ function quietStartupEnabled(cwd) {
100
156
  return false;
101
157
  }
102
158
  //#endregion
103
- //#region src/acp/translate/pi-tools.ts
104
- /**
105
- * Extract displayable text from a pi tool result.
106
- *
107
- * Pi tool results have varying shapes depending on the tool. This function
108
- * tries content blocks first, then falls back to details fields (diff, stdout/stderr),
109
- * and finally JSON serialization as a last resort.
110
- */
159
+ //#region src/acp/translate/tool-content.ts
111
160
  const textBlockSchema = z.object({
112
161
  type: z.literal("text"),
113
162
  text: z.string()
114
163
  });
115
- const toolDetailsSchema = z.object({
116
- diff: z.string().optional(),
164
+ const imageBlockSchema = z.object({ type: z.literal("image") });
165
+ const contentBlockSchema = z.union([textBlockSchema, imageBlockSchema]);
166
+ const bashDetailsSchema = z.object({
117
167
  stdout: z.string().optional(),
118
168
  stderr: z.string().optional(),
119
169
  output: z.string().optional(),
120
170
  exitCode: z.number().optional(),
121
171
  code: z.number().optional()
122
172
  });
123
- const toolResultSchema = z.object({
173
+ const bashResultSchema = z.object({
124
174
  content: z.array(z.unknown()).optional(),
125
- details: toolDetailsSchema.optional(),
175
+ details: bashDetailsSchema.optional(),
126
176
  stdout: z.string().optional(),
127
177
  stderr: z.string().optional(),
128
178
  output: z.string().optional(),
129
179
  exitCode: z.number().optional(),
130
180
  code: z.number().optional()
131
181
  });
132
- function toolResultToText(result) {
133
- if (result === null || result === void 0 || typeof result !== "object") return "";
134
- const parsed = toolResultSchema.safeParse(result);
135
- if (!parsed.success) try {
136
- return JSON.stringify(result, null, 2);
137
- } catch {
138
- return String(result);
139
- }
182
+ /**
183
+ * Extract stdout/stderr and exit code from a pi bash/tmux result.
184
+ */
185
+ function extractBashOutput(result) {
186
+ if (result === null || result === void 0 || typeof result !== "object") return {
187
+ output: "",
188
+ exitCode: void 0
189
+ };
190
+ const parsed = bashResultSchema.safeParse(result);
191
+ if (!parsed.success) return {
192
+ output: "",
193
+ exitCode: void 0
194
+ };
140
195
  const r = parsed.data;
196
+ const d = r.details;
141
197
  if (r.content !== void 0) {
142
198
  const texts = r.content.map((block) => textBlockSchema.safeParse(block)).filter((res) => res.success).map((res) => res.data.text);
143
- if (texts.length > 0) return texts.join("");
199
+ if (texts.length > 0) {
200
+ const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
201
+ return {
202
+ output: texts.join(""),
203
+ exitCode
204
+ };
205
+ }
144
206
  }
145
- const d = r.details;
146
- const diff = d?.diff;
147
- if (diff !== void 0 && diff.trim() !== "") return diff;
148
207
  const stdout = d?.stdout ?? r.stdout ?? d?.output ?? r.output;
149
208
  const stderr = d?.stderr ?? r.stderr;
150
209
  const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
151
- const hasStdout = stdout !== void 0 && stdout.trim() !== "";
152
- const hasStderr = stderr !== void 0 && stderr.trim() !== "";
153
- if (hasStdout || hasStderr) {
154
- const parts = [];
155
- if (hasStdout) parts.push(stdout);
156
- if (hasStderr) parts.push(`stderr:\n${stderr}`);
157
- if (exitCode !== void 0) parts.push(`exit code: ${exitCode}`);
158
- return parts.join("\n\n").trimEnd();
210
+ const parts = [];
211
+ if (stdout !== void 0 && stdout.trim() !== "") parts.push(stdout);
212
+ if (stderr !== void 0 && stderr.trim() !== "") parts.push(stderr);
213
+ return {
214
+ output: parts.join("\n"),
215
+ exitCode
216
+ };
217
+ }
218
+ /**
219
+ * Extract text content from a pi tool result (generic).
220
+ */
221
+ function extractTextContent(result) {
222
+ if (result === null || result === void 0 || typeof result !== "object") return "";
223
+ if ("content" in result && Array.isArray(result.content)) {
224
+ const texts = [];
225
+ for (const block of result.content) {
226
+ const parsed = textBlockSchema.safeParse(block);
227
+ if (parsed.success) texts.push(parsed.data.text);
228
+ }
229
+ if (texts.length > 0) return texts.join("");
159
230
  }
160
231
  try {
161
232
  return JSON.stringify(result, null, 2);
@@ -163,6 +234,134 @@ function toolResultToText(result) {
163
234
  return String(result);
164
235
  }
165
236
  }
237
+ /**
238
+ * Extract content blocks from a pi result, preserving type information.
239
+ * Used for read results where images need to be preserved.
240
+ */
241
+ function extractContentBlocks(result) {
242
+ if (result === null || result === void 0 || typeof result !== "object") return [];
243
+ if (!("content" in result) || !Array.isArray(result.content)) return [];
244
+ const blocks = [];
245
+ for (const raw of result.content) {
246
+ const parsed = contentBlockSchema.safeParse(raw);
247
+ if (parsed.success) blocks.push(parsed.data);
248
+ }
249
+ return blocks;
250
+ }
251
+ /**
252
+ * Escape text that would be interpreted as markdown formatting.
253
+ *
254
+ * Prevents file content from being rendered as headings, links, code
255
+ * blocks, or horizontal rules when displayed in an ACP client.
256
+ */
257
+ function markdownEscape(text) {
258
+ return text.replace(/^(#{1,6})\s/gm, "\\$1 ").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/^([-*_])\1{2,}$/gm, "\\$1$1$1").replace(/</g, "\\<");
259
+ }
260
+ /**
261
+ * Format tool output into `ToolCallContent[]` by tool name.
262
+ *
263
+ * Returns the appropriate content shape for each tool type:
264
+ * - bash/tmux: console code fences
265
+ * - read: markdown-escaped text (images preserved)
266
+ * - edit/write: empty (diff handled separately)
267
+ * - lsp: code fences
268
+ * - errors: code fences with failed status
269
+ * - everything else: plain text
270
+ */
271
+ function formatToolContent(toolName, result, isError) {
272
+ if (isError) {
273
+ const text = extractTextContent(result);
274
+ if (text === "") return [];
275
+ return [{
276
+ type: "content",
277
+ content: {
278
+ type: "text",
279
+ text: `\`\`\`\n${text}\n\`\`\``
280
+ }
281
+ }];
282
+ }
283
+ switch (toolName) {
284
+ case "bash":
285
+ case "tmux": return formatBashContent(result);
286
+ case "read": return formatReadContent(result);
287
+ case "edit":
288
+ case "write": return [];
289
+ case "lsp": return formatLspContent(result);
290
+ default: return formatFallbackContent(result);
291
+ }
292
+ }
293
+ function formatBashContent(result) {
294
+ const { output, exitCode } = extractBashOutput(result);
295
+ if (output === "" && exitCode === void 0) return [];
296
+ const parts = [];
297
+ if (output !== "") parts.push(`\`\`\`console\n${output}\n\`\`\``);
298
+ if (exitCode !== void 0 && exitCode !== 0) parts.push(`exit code: ${exitCode}`);
299
+ const text = parts.join("\n\n");
300
+ if (text === "") return [];
301
+ return [{
302
+ type: "content",
303
+ content: {
304
+ type: "text",
305
+ text
306
+ }
307
+ }];
308
+ }
309
+ function formatReadContent(result) {
310
+ const blocks = extractContentBlocks(result);
311
+ if (blocks.length === 0) {
312
+ if (typeof result === "object" && result !== null && "content" in result && Array.isArray(result.content) && result.content.length === 0) return [];
313
+ const text = extractTextContent(result);
314
+ if (text === "") return [];
315
+ return [{
316
+ type: "content",
317
+ content: {
318
+ type: "text",
319
+ text: markdownEscape(text)
320
+ }
321
+ }];
322
+ }
323
+ const content = [];
324
+ for (const block of blocks) if (block.type === "text") content.push({
325
+ type: "content",
326
+ content: {
327
+ type: "text",
328
+ text: markdownEscape(block.text)
329
+ }
330
+ });
331
+ return content;
332
+ }
333
+ function formatLspContent(result) {
334
+ const text = extractTextContent(result);
335
+ if (text === "") return [];
336
+ return [{
337
+ type: "content",
338
+ content: {
339
+ type: "text",
340
+ text: `\`\`\`\n${text}\n\`\`\``
341
+ }
342
+ }];
343
+ }
344
+ function formatFallbackContent(result) {
345
+ const text = extractTextContent(result);
346
+ if (text === "") return [];
347
+ return [{
348
+ type: "content",
349
+ content: {
350
+ type: "text",
351
+ text
352
+ }
353
+ }];
354
+ }
355
+ /**
356
+ * Wrap streaming output text in a console code fence for bash/tmux.
357
+ *
358
+ * Each streaming update is self-contained (full accumulated buffer),
359
+ * following the codex-acp pattern.
360
+ */
361
+ function wrapStreamingBashOutput(text) {
362
+ if (text === "") return "";
363
+ return `\`\`\`console\n${text}\n\`\`\``;
364
+ }
166
365
  //#endregion
167
366
  //#region src/acp/session.ts
168
367
  function findUniqueLineNumber(text, needle) {
@@ -187,10 +386,70 @@ function toToolKind(toolName) {
187
386
  case "read": return "read";
188
387
  case "write":
189
388
  case "edit": return "edit";
190
- case "bash": return "execute";
389
+ case "bash":
390
+ case "tmux": return "execute";
391
+ case "lsp": return "search";
191
392
  default: return "other";
192
393
  }
193
394
  }
395
+ const MAX_TITLE_LEN = 80;
396
+ function truncateTitle(text) {
397
+ const oneLine = text.replace(/\n/g, " ").trim();
398
+ if (oneLine.length <= MAX_TITLE_LEN) return oneLine;
399
+ return `${oneLine.slice(0, MAX_TITLE_LEN - 1)}…`;
400
+ }
401
+ function capitalize(s) {
402
+ if (s.length === 0) return s;
403
+ return s.charAt(0).toUpperCase() + s.slice(1);
404
+ }
405
+ /**
406
+ * Build a descriptive tool title from tool name and args.
407
+ *
408
+ * Returns a short human-readable label like "Read src/index.ts" or "Run ls -la".
409
+ */
410
+ function buildToolTitle(toolName, args) {
411
+ const p = args.path;
412
+ switch (toolName) {
413
+ case "read": return p !== void 0 ? `Read ${p}` : "Read";
414
+ case "write": return p !== void 0 ? `Write ${p}` : "Write";
415
+ case "edit": return p !== void 0 ? `Edit ${p}` : "Edit";
416
+ case "bash": {
417
+ const command = typeof args["command"] === "string" ? args["command"] : typeof args["cmd"] === "string" ? args["cmd"] : void 0;
418
+ return command !== void 0 ? truncateTitle(`Run ${command}`) : "bash";
419
+ }
420
+ case "lsp": {
421
+ const action = typeof args["action"] === "string" ? args["action"] : void 0;
422
+ const file = typeof args["file"] === "string" ? args["file"] : void 0;
423
+ const query = typeof args["query"] === "string" ? args["query"] : void 0;
424
+ const line = typeof args["line"] === "number" ? args["line"] : void 0;
425
+ if (action !== void 0) {
426
+ const target = file !== void 0 ? line !== void 0 ? `${file}:${line}` : file : query;
427
+ return target !== void 0 ? truncateTitle(`${capitalize(action)} ${target}`) : capitalize(action);
428
+ }
429
+ return "LSP";
430
+ }
431
+ case "tmux": {
432
+ const action = typeof args["action"] === "string" ? args["action"] : void 0;
433
+ const command = typeof args["command"] === "string" ? args["command"] : void 0;
434
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
435
+ if (action === "run" && command !== void 0) return truncateTitle(`Tmux: ${command}`);
436
+ if (action !== void 0 && name !== void 0) return truncateTitle(`Tmux ${action} ${name}`);
437
+ if (action !== void 0) return `Tmux ${action}`;
438
+ return "Tmux";
439
+ }
440
+ case "context_tag": {
441
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
442
+ return name !== void 0 ? `Tag ${name}` : "Tag";
443
+ }
444
+ case "context_log": return "Context log";
445
+ case "context_checkout": {
446
+ const target = typeof args["target"] === "string" ? args["target"] : void 0;
447
+ return target !== void 0 ? truncateTitle(`Checkout ${target}`) : "Checkout";
448
+ }
449
+ case "claudemon": return "Check quota";
450
+ default: return toolName;
451
+ }
452
+ }
194
453
  /**
195
454
  * Map pi assistant stopReason to ACP StopReason.
196
455
  * pi: "stop" | "length" | "toolUse" | "error" | "aborted"
@@ -222,6 +481,19 @@ function toToolArgs(raw) {
222
481
  const result = toolArgsSchema.safeParse(raw);
223
482
  return result.success ? result.data : {};
224
483
  }
484
+ /** Build the `_meta.piAcp` tool name metadata. */
485
+ function buildToolMeta(toolName, extra) {
486
+ const base = { piAcp: { toolName } };
487
+ if (extra !== void 0) return {
488
+ ...base,
489
+ ...extra
490
+ };
491
+ return base;
492
+ }
493
+ /** Tools that produce terminal-style output. */
494
+ function isTerminalTool(toolName) {
495
+ return toolName === "bash" || toolName === "tmux";
496
+ }
225
497
  var SessionManager$1 = class {
226
498
  sessions = /* @__PURE__ */ new Map();
227
499
  disposeAll() {
@@ -255,12 +527,15 @@ var PiAcpSession = class {
255
527
  cwd;
256
528
  mcpServers;
257
529
  piSession;
530
+ supportsTerminalOutput;
258
531
  startupInfo = null;
259
532
  startupInfoSent = false;
260
533
  conn;
261
534
  cancelRequested = false;
262
535
  pendingTurn = null;
263
536
  currentToolCalls = /* @__PURE__ */ new Map();
537
+ /** Map of toolCallId -> toolName for streaming updates (Phase 5). */
538
+ toolCallNames = /* @__PURE__ */ new Map();
264
539
  editSnapshots = /* @__PURE__ */ new Map();
265
540
  lastAssistantStopReason = null;
266
541
  lastEmit = Promise.resolve();
@@ -271,6 +546,7 @@ var PiAcpSession = class {
271
546
  this.mcpServers = opts.mcpServers;
272
547
  this.piSession = opts.piSession;
273
548
  this.conn = opts.conn;
549
+ this.supportsTerminalOutput = opts.supportsTerminalOutput ?? false;
274
550
  this.unsubscribe = this.piSession.subscribe((ev) => this.handlePiEvent(ev));
275
551
  }
276
552
  dispose() {
@@ -338,10 +614,10 @@ var PiAcpSession = class {
338
614
  this.handleToolStart(ev.toolCallId, ev.toolName, toToolArgs(ev.args));
339
615
  break;
340
616
  case "tool_execution_update":
341
- this.handleToolUpdate(ev.toolCallId, ev.partialResult);
617
+ this.handleToolUpdate(ev.toolCallId, ev.toolName, ev.partialResult);
342
618
  break;
343
619
  case "tool_execution_end":
344
- this.handleToolEnd(ev.toolCallId, ev.result, ev.isError);
620
+ this.handleToolEnd(ev.toolCallId, ev.toolName, ev.result, ev.isError);
345
621
  break;
346
622
  case "agent_end":
347
623
  this.handleAgentEnd();
@@ -382,18 +658,20 @@ var PiAcpSession = class {
382
658
  this.emit({
383
659
  sessionUpdate: "tool_call",
384
660
  toolCallId: toolCall.id,
385
- title: toolCall.name,
661
+ title: buildToolTitle(toolCall.name, rawInput),
386
662
  kind: toToolKind(toolCall.name),
387
663
  status,
388
664
  ...locations ? { locations } : {},
389
- rawInput
665
+ rawInput,
666
+ _meta: buildToolMeta(toolCall.name)
390
667
  });
391
668
  } else this.emit({
392
669
  sessionUpdate: "tool_call_update",
393
670
  toolCallId: toolCall.id,
394
671
  status,
395
672
  ...locations ? { locations } : {},
396
- rawInput
673
+ rawInput,
674
+ _meta: buildToolMeta(toolCall.name)
397
675
  });
398
676
  }
399
677
  }
@@ -401,92 +679,152 @@ var PiAcpSession = class {
401
679
  if ("role" in msg && msg.role === "assistant") this.lastAssistantStopReason = msg.stopReason;
402
680
  }
403
681
  handleToolStart(toolCallId, toolName, args) {
682
+ this.toolCallNames.set(toolCallId, toolName);
404
683
  let line;
405
- if (toolName === "edit" && args.path !== void 0) try {
684
+ if ((toolName === "edit" || toolName === "write") && args.path !== void 0) try {
406
685
  const abs = isAbsolute(args.path) ? args.path : resolve(this.cwd, args.path);
407
- const oldText = readFileSync(abs, "utf8");
686
+ let oldText = "";
687
+ try {
688
+ oldText = readFileSync(abs, "utf8");
689
+ } catch {}
408
690
  this.editSnapshots.set(toolCallId, {
409
691
  path: abs,
410
692
  oldText
411
693
  });
412
- line = findUniqueLineNumber(oldText, args.oldText ?? "");
694
+ if (toolName === "edit") line = findUniqueLineNumber(oldText, args.oldText ?? "");
413
695
  } catch {}
414
696
  const locations = resolveToolPath(args, this.cwd, line);
697
+ const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_info: {
698
+ terminal_id: toolCallId,
699
+ cwd: this.cwd
700
+ } } : void 0);
701
+ const terminalContent = this.supportsTerminalOutput && isTerminalTool(toolName) ? [{
702
+ type: "terminal",
703
+ terminalId: toolCallId
704
+ }] : void 0;
415
705
  if (!this.currentToolCalls.has(toolCallId)) {
416
706
  this.currentToolCalls.set(toolCallId, "in_progress");
417
707
  this.emit({
418
708
  sessionUpdate: "tool_call",
419
709
  toolCallId,
420
- title: toolName,
710
+ title: buildToolTitle(toolName, args),
421
711
  kind: toToolKind(toolName),
422
712
  status: "in_progress",
423
713
  ...locations ? { locations } : {},
424
- rawInput: args
714
+ ...terminalContent !== void 0 ? { content: terminalContent } : {},
715
+ rawInput: args,
716
+ _meta: meta
425
717
  });
426
718
  } else {
427
719
  this.currentToolCalls.set(toolCallId, "in_progress");
428
720
  this.emit({
429
721
  sessionUpdate: "tool_call_update",
430
722
  toolCallId,
723
+ title: buildToolTitle(toolName, args),
431
724
  status: "in_progress",
432
725
  ...locations ? { locations } : {},
433
- rawInput: args
726
+ ...terminalContent !== void 0 ? { content: terminalContent } : {},
727
+ rawInput: args,
728
+ _meta: meta
434
729
  });
435
730
  }
436
731
  }
437
- handleToolUpdate(toolCallId, partialResult) {
438
- const text = toolResultToText(partialResult);
439
- this.emit({
440
- sessionUpdate: "tool_call_update",
441
- toolCallId,
442
- status: "in_progress",
443
- content: text ? [{
444
- type: "content",
445
- content: {
446
- type: "text",
447
- text
448
- }
449
- }] : null,
450
- rawOutput: partialResult
451
- });
732
+ handleToolUpdate(toolCallId, toolName, partialResult) {
733
+ const name = this.toolCallNames.get(toolCallId) ?? toolName;
734
+ if (this.supportsTerminalOutput && isTerminalTool(name)) {
735
+ const text = extractStreamingText(partialResult);
736
+ this.emit({
737
+ sessionUpdate: "tool_call_update",
738
+ toolCallId,
739
+ status: "in_progress",
740
+ _meta: buildToolMeta(name, { terminal_output: {
741
+ terminal_id: toolCallId,
742
+ data: text
743
+ } }),
744
+ rawOutput: partialResult
745
+ });
746
+ } else if (isTerminalTool(name)) {
747
+ const wrapped = wrapStreamingBashOutput(extractStreamingText(partialResult));
748
+ this.emit({
749
+ sessionUpdate: "tool_call_update",
750
+ toolCallId,
751
+ status: "in_progress",
752
+ content: wrapped ? [{
753
+ type: "content",
754
+ content: {
755
+ type: "text",
756
+ text: wrapped
757
+ }
758
+ }] : null,
759
+ _meta: buildToolMeta(name),
760
+ rawOutput: partialResult
761
+ });
762
+ } else {
763
+ const text = extractStreamingText(partialResult);
764
+ this.emit({
765
+ sessionUpdate: "tool_call_update",
766
+ toolCallId,
767
+ status: "in_progress",
768
+ content: text ? [{
769
+ type: "content",
770
+ content: {
771
+ type: "text",
772
+ text
773
+ }
774
+ }] : null,
775
+ _meta: buildToolMeta(name),
776
+ rawOutput: partialResult
777
+ });
778
+ }
452
779
  }
453
- handleToolEnd(toolCallId, result, isError) {
454
- const text = toolResultToText(result);
780
+ handleToolEnd(toolCallId, toolName, result, isError) {
455
781
  const snapshot = this.editSnapshots.get(toolCallId);
456
782
  let content = null;
457
783
  if (!isError && snapshot) try {
458
784
  const newText = readFileSync(snapshot.path, "utf8");
459
- if (newText !== snapshot.oldText) content = [{
460
- type: "diff",
461
- path: snapshot.path,
462
- oldText: snapshot.oldText,
463
- newText
464
- }, ...text ? [{
785
+ if (newText !== snapshot.oldText) {
786
+ const formatted = formatToolContent(toolName, result, isError);
787
+ content = [{
788
+ type: "diff",
789
+ path: snapshot.path,
790
+ oldText: snapshot.oldText,
791
+ newText
792
+ }, ...formatted];
793
+ }
794
+ } catch {}
795
+ const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
796
+ terminal_id: toolCallId,
797
+ exit_code: extractExitCode(result),
798
+ signal: null
799
+ } } : void 0);
800
+ if (content === null) {
801
+ const formatted = formatToolContent(toolName, result, isError);
802
+ content = formatted.length > 0 ? formatted : null;
803
+ }
804
+ if (content === null && !isError && toolName !== "edit" && toolName !== "write") {
805
+ const text = extractStreamingText(result);
806
+ if (text) content = [{
465
807
  type: "content",
466
808
  content: {
467
809
  type: "text",
468
810
  text
469
811
  }
470
- }] : []];
471
- } catch {}
472
- if (!content && text) content = [{
473
- type: "content",
474
- content: {
475
- type: "text",
476
- text
477
- }
478
- }];
812
+ }];
813
+ }
479
814
  this.emit({
480
815
  sessionUpdate: "tool_call_update",
481
816
  toolCallId,
482
817
  status: isError ? "failed" : "completed",
483
818
  content,
819
+ _meta: meta,
484
820
  rawOutput: result
485
821
  });
486
822
  this.currentToolCalls.delete(toolCallId);
487
823
  this.editSnapshots.delete(toolCallId);
824
+ this.toolCallNames.delete(toolCallId);
488
825
  }
489
826
  handleAgentEnd() {
827
+ this.emitUsageUpdate();
490
828
  this.flushEmits().finally(() => {
491
829
  const reason = this.cancelRequested ? "cancelled" : mapPiStopReason(this.lastAssistantStopReason);
492
830
  this.lastAssistantStopReason = null;
@@ -494,7 +832,79 @@ var PiAcpSession = class {
494
832
  this.pendingTurn = null;
495
833
  });
496
834
  }
835
+ /**
836
+ * Emit a usage_update notification with current context and cost data.
837
+ */
838
+ emitUsageUpdate() {
839
+ const contextUsage = this.piSession.getContextUsage?.();
840
+ const stats = this.piSession.getSessionStats();
841
+ const used = contextUsage?.tokens ?? 0;
842
+ const size = contextUsage?.contextWindow ?? 0;
843
+ this.emit({
844
+ sessionUpdate: "usage_update",
845
+ used,
846
+ size,
847
+ cost: stats.cost > 0 ? {
848
+ amount: stats.cost,
849
+ currency: "USD"
850
+ } : null
851
+ });
852
+ }
853
+ /**
854
+ * Build ACP Usage data from pi session stats for prompt response.
855
+ */
856
+ getUsage() {
857
+ const stats = this.piSession.getSessionStats();
858
+ return {
859
+ inputTokens: stats.tokens.input,
860
+ outputTokens: stats.tokens.output,
861
+ cachedReadTokens: stats.tokens.cacheRead,
862
+ cachedWriteTokens: stats.tokens.cacheWrite
863
+ };
864
+ }
865
+ /**
866
+ * Get cumulative session cost.
867
+ */
868
+ getCost() {
869
+ return this.piSession.getSessionStats().cost;
870
+ }
497
871
  };
872
+ function isTextBlock$1(v) {
873
+ return typeof v === "object" && v !== null && "type" in v && v.type === "text" && "text" in v && typeof v.text === "string";
874
+ }
875
+ function extractStreamingText(result) {
876
+ if (result === null || result === void 0) return "";
877
+ if (typeof result === "string") return result;
878
+ if (typeof result !== "object") return String(result);
879
+ if ("content" in result && Array.isArray(result.content)) {
880
+ const texts = [];
881
+ for (const raw of result.content) if (isTextBlock$1(raw)) texts.push(raw.text);
882
+ if (texts.length > 0) return texts.join("");
883
+ }
884
+ if ("details" in result) {
885
+ const details = result.details;
886
+ if (typeof details === "object" && details !== null) {
887
+ if ("stdout" in details && typeof details.stdout === "string" && details.stdout.trim() !== "") return details.stdout;
888
+ if ("output" in details && typeof details.output === "string" && details.output.trim() !== "") return details.output;
889
+ }
890
+ }
891
+ if ("output" in result && typeof result.output === "string" && result.output.trim() !== "") return result.output;
892
+ if ("stdout" in result && typeof result.stdout === "string" && result.stdout.trim() !== "") return result.stdout;
893
+ return "";
894
+ }
895
+ function extractExitCode(result) {
896
+ if (result === null || result === void 0 || typeof result !== "object") return null;
897
+ if ("details" in result) {
898
+ const details = result.details;
899
+ if (typeof details === "object" && details !== null) {
900
+ if ("exitCode" in details && typeof details.exitCode === "number") return details.exitCode;
901
+ if ("code" in details && typeof details.code === "number") return details.code;
902
+ }
903
+ }
904
+ if ("exitCode" in result && typeof result.exitCode === "number") return result.exitCode;
905
+ if ("code" in result && typeof result.code === "number") return result.code;
906
+ return null;
907
+ }
498
908
  /**
499
909
  * Type guard to narrow AgentSessionEvent to the AgentEvent subset
500
910
  * (the variants we handle). Session-specific events like auto_compaction
@@ -514,10 +924,6 @@ function extractUserMessageText(content) {
514
924
  if (!Array.isArray(content)) return "";
515
925
  return content.filter(isTextBlock).map((b) => b.text).join("");
516
926
  }
517
- function extractAssistantText(content) {
518
- if (!Array.isArray(content)) return "";
519
- return content.filter(isTextBlock).map((b) => b.text).join("");
520
- }
521
927
  //#endregion
522
928
  //#region src/acp/translate/prompt.ts
523
929
  function acpPromptToPiMessage(blocks) {
@@ -699,12 +1105,26 @@ function parseArgs(input) {
699
1105
  if (current !== "") args.push(current);
700
1106
  return args;
701
1107
  }
1108
+ const SESSION_TITLE_MAX = 100;
1109
+ function truncateSessionTitle(text) {
1110
+ const trimmed = text.trim();
1111
+ if (trimmed === "") return null;
1112
+ const oneLine = trimmed.replace(/\n/g, " ");
1113
+ if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
1114
+ return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
1115
+ }
702
1116
  const pkg = readNearestPackageJson(import.meta.url);
703
1117
  var PiAcpAgent = class {
704
1118
  conn;
705
1119
  sessions = new SessionManager$1();
706
1120
  /** Cache of sessionId → file path, populated by listSessions and newSession. */
707
1121
  sessionPaths = /* @__PURE__ */ new Map();
1122
+ /** Parsed client capability flags from initialize(). */
1123
+ clientCapabilities = {
1124
+ terminalOutput: false,
1125
+ terminalAuth: false,
1126
+ gatewayAuth: false
1127
+ };
708
1128
  dispose() {
709
1129
  this.sessions.disposeAll();
710
1130
  }
@@ -714,6 +1134,7 @@ var PiAcpAgent = class {
714
1134
  async initialize(params) {
715
1135
  const supportedVersion = 1;
716
1136
  const requested = params.protocolVersion;
1137
+ this.clientCapabilities = parseClientCapabilities(params.clientCapabilities);
717
1138
  return {
718
1139
  protocolVersion: requested === supportedVersion ? requested : supportedVersion,
719
1140
  agentInfo: {
@@ -721,7 +1142,7 @@ var PiAcpAgent = class {
721
1142
  title: "pi ACP adapter",
722
1143
  version: pkg.version
723
1144
  },
724
- authMethods: buildAuthMethods({ supportsTerminalAuthMeta: params.clientCapabilities?._meta?.["terminal-auth"] === true }),
1145
+ authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
725
1146
  agentCapabilities: {
726
1147
  loadSession: true,
727
1148
  mcpCapabilities: {
@@ -731,9 +1152,14 @@ var PiAcpAgent = class {
731
1152
  promptCapabilities: {
732
1153
  image: true,
733
1154
  audio: false,
734
- embeddedContext: false
1155
+ embeddedContext: true
735
1156
  },
736
- sessionCapabilities: { list: {} }
1157
+ sessionCapabilities: {
1158
+ list: {},
1159
+ close: {},
1160
+ resume: {},
1161
+ fork: {}
1162
+ }
737
1163
  }
738
1164
  };
739
1165
  }
@@ -744,6 +1170,8 @@ var PiAcpAgent = class {
744
1170
  try {
745
1171
  result = await createAgentSession({ cwd: params.cwd });
746
1172
  } catch (e) {
1173
+ const authErr = detectAuthError(e);
1174
+ if (authErr !== null) throw authErr;
747
1175
  const msg = e instanceof Error ? e.message : String(e);
748
1176
  throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
749
1177
  }
@@ -760,7 +1188,8 @@ var PiAcpAgent = class {
760
1188
  cwd: params.cwd,
761
1189
  mcpServers: params.mcpServers,
762
1190
  piSession,
763
- conn: this.conn
1191
+ conn: this.conn,
1192
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
764
1193
  });
765
1194
  this.sessions.register(session);
766
1195
  const quietStartup = quietStartupEnabled(params.cwd);
@@ -770,7 +1199,6 @@ var PiAcpAgent = class {
770
1199
  updateNotice
771
1200
  });
772
1201
  if (preludeText) session.setStartupInfo(preludeText);
773
- this.sessions.closeAllExcept(session.sessionId);
774
1202
  const modes = buildThinkingModes(piSession);
775
1203
  const models = buildModelState(piSession);
776
1204
  const configOptions = buildConfigOptions(modes, models);
@@ -799,7 +1227,9 @@ var PiAcpAgent = class {
799
1227
  }, 0);
800
1228
  return response;
801
1229
  }
802
- async authenticate(_params) {}
1230
+ async authenticate(_params) {
1231
+ return {};
1232
+ }
803
1233
  async prompt(params) {
804
1234
  const session = this.sessions.get(params.sessionId);
805
1235
  const { message, images } = acpPromptToPiMessage(params.prompt);
@@ -812,7 +1242,23 @@ var PiAcpAgent = class {
812
1242
  if (handled) return handled;
813
1243
  }
814
1244
  const result = await session.prompt(message, images);
815
- return { stopReason: result === "error" ? "end_turn" : result };
1245
+ const stopReason = result === "error" ? "end_turn" : result;
1246
+ const usage = session.getUsage();
1247
+ const cost = session.getCost();
1248
+ return {
1249
+ stopReason,
1250
+ usage: {
1251
+ inputTokens: usage.inputTokens,
1252
+ outputTokens: usage.outputTokens,
1253
+ cachedReadTokens: usage.cachedReadTokens,
1254
+ cachedWriteTokens: usage.cachedWriteTokens,
1255
+ totalTokens: usage.inputTokens + usage.outputTokens
1256
+ },
1257
+ _meta: cost > 0 ? { cost: {
1258
+ amount: cost,
1259
+ currency: "USD"
1260
+ } } : {}
1261
+ };
816
1262
  }
817
1263
  async cancel(params) {
818
1264
  await this.sessions.get(params.sessionId).cancel();
@@ -829,6 +1275,114 @@ var PiAcpAgent = class {
829
1275
  for (const s of all) this.sessionPaths.set(s.id, s.path);
830
1276
  return this.sessionPaths.get(sessionId) ?? null;
831
1277
  }
1278
+ /**
1279
+ * Replay persisted session messages as ACP session updates.
1280
+ *
1281
+ * Iterates through the message history, emitting structured updates for each
1282
+ * content block type: text, thinking, tool calls, and tool results. A map of
1283
+ * tool call IDs to their invocation data (from assistant messages) is built
1284
+ * to enrich subsequent tool result updates with rawInput and locations.
1285
+ */
1286
+ async replaySessionHistory(session, messages) {
1287
+ const toolCallMap = /* @__PURE__ */ new Map();
1288
+ for (const m of messages) {
1289
+ if (!("role" in m)) continue;
1290
+ if (m.role === "user") {
1291
+ const text = extractUserMessageText(m.content);
1292
+ if (text) await this.conn.sessionUpdate({
1293
+ sessionId: session.sessionId,
1294
+ update: {
1295
+ sessionUpdate: "user_message_chunk",
1296
+ content: {
1297
+ type: "text",
1298
+ text
1299
+ }
1300
+ }
1301
+ });
1302
+ continue;
1303
+ }
1304
+ if (m.role === "assistant") {
1305
+ const am = m;
1306
+ for (const block of am.content) if (block.type === "text" && block.text) await this.conn.sessionUpdate({
1307
+ sessionId: session.sessionId,
1308
+ update: {
1309
+ sessionUpdate: "agent_message_chunk",
1310
+ content: {
1311
+ type: "text",
1312
+ text: block.text
1313
+ }
1314
+ }
1315
+ });
1316
+ else if (block.type === "thinking" && block.thinking) await this.conn.sessionUpdate({
1317
+ sessionId: session.sessionId,
1318
+ update: {
1319
+ sessionUpdate: "agent_thought_chunk",
1320
+ content: {
1321
+ type: "text",
1322
+ text: block.thinking
1323
+ }
1324
+ }
1325
+ });
1326
+ else if (block.type === "toolCall") {
1327
+ const args = toToolArgs(block.arguments);
1328
+ toolCallMap.set(block.id, {
1329
+ name: block.name,
1330
+ args
1331
+ });
1332
+ const locations = resolveToolPath(args, session.cwd);
1333
+ await this.conn.sessionUpdate({
1334
+ sessionId: session.sessionId,
1335
+ update: {
1336
+ sessionUpdate: "tool_call",
1337
+ toolCallId: block.id,
1338
+ title: buildToolTitle(block.name, args),
1339
+ kind: toToolKind(block.name),
1340
+ status: "completed",
1341
+ rawInput: args,
1342
+ ...locations ? { locations } : {},
1343
+ _meta: { piAcp: { toolName: block.name } }
1344
+ }
1345
+ });
1346
+ }
1347
+ continue;
1348
+ }
1349
+ if (m.role === "toolResult") {
1350
+ const tr = m;
1351
+ const toolName = tr.toolName;
1352
+ const toolCallId = tr.toolCallId;
1353
+ const isError = tr.isError;
1354
+ const invocation = toolCallMap.get(toolCallId);
1355
+ const args = invocation?.args;
1356
+ const locations = args !== void 0 ? resolveToolPath(args, session.cwd) : void 0;
1357
+ if (invocation === void 0) await this.conn.sessionUpdate({
1358
+ sessionId: session.sessionId,
1359
+ update: {
1360
+ sessionUpdate: "tool_call",
1361
+ toolCallId,
1362
+ title: buildToolTitle(toolName, {}),
1363
+ kind: toToolKind(toolName),
1364
+ status: "completed",
1365
+ rawInput: null,
1366
+ rawOutput: m,
1367
+ _meta: { piAcp: { toolName } }
1368
+ }
1369
+ });
1370
+ const content = formatToolContent(toolName, m, isError);
1371
+ await this.conn.sessionUpdate({
1372
+ sessionId: session.sessionId,
1373
+ update: {
1374
+ sessionUpdate: "tool_call_update",
1375
+ toolCallId,
1376
+ status: isError ? "failed" : "completed",
1377
+ content: content.length > 0 ? content : null,
1378
+ rawOutput: m,
1379
+ ...locations ? { locations } : {},
1380
+ _meta: { piAcp: { toolName } }
1381
+ }
1382
+ });
1383
+ }
1384
+ }
1385
+ }
832
1386
  async listSessions(params) {
833
1387
  const cwd = params.cwd;
834
1388
  const raw = cwd !== void 0 && cwd !== null ? await SessionManager.list(cwd) : await SessionManager.listAll();
@@ -836,7 +1390,8 @@ var PiAcpAgent = class {
836
1390
  const sessions = raw.map((s) => ({
837
1391
  id: s.id,
838
1392
  cwd: s.cwd,
839
- name: s.name ?? "",
1393
+ name: s.name,
1394
+ firstMessage: s.firstMessage,
840
1395
  modified: s.modified,
841
1396
  messageCount: s.messageCount
842
1397
  }));
@@ -850,7 +1405,7 @@ var PiAcpAgent = class {
850
1405
  sessions: sessions.slice(start, start + PAGE_SIZE).map((s) => ({
851
1406
  sessionId: s.id,
852
1407
  cwd: s.cwd,
853
- title: s.name ?? null,
1408
+ title: (s.name !== void 0 && s.name !== "" ? s.name : null) ?? truncateSessionTitle(s.firstMessage) ?? null,
854
1409
  updatedAt: s.modified.toISOString()
855
1410
  })),
856
1411
  nextCursor: start + PAGE_SIZE < sessions.length ? String(start + PAGE_SIZE) : null,
@@ -870,6 +1425,8 @@ var PiAcpAgent = class {
870
1425
  sessionManager: sm
871
1426
  });
872
1427
  } catch (e) {
1428
+ const authErr = detectAuthError(e);
1429
+ if (authErr !== null) throw authErr;
873
1430
  const msg = e instanceof Error ? e.message : String(e);
874
1431
  throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
875
1432
  }
@@ -879,75 +1436,11 @@ var PiAcpAgent = class {
879
1436
  cwd: params.cwd,
880
1437
  mcpServers: params.mcpServers,
881
1438
  piSession,
882
- conn: this.conn
1439
+ conn: this.conn,
1440
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
883
1441
  });
884
1442
  this.sessions.register(session);
885
- this.sessions.closeAllExcept(session.sessionId);
886
- const messages = piSession.messages;
887
- for (const m of messages) {
888
- if (!("role" in m)) continue;
889
- if (m.role === "user") {
890
- const text = extractUserMessageText(m.content);
891
- if (text) await this.conn.sessionUpdate({
892
- sessionId: session.sessionId,
893
- update: {
894
- sessionUpdate: "user_message_chunk",
895
- content: {
896
- type: "text",
897
- text
898
- }
899
- }
900
- });
901
- }
902
- if (m.role === "assistant") {
903
- const text = extractAssistantText(m.content);
904
- if (text) await this.conn.sessionUpdate({
905
- sessionId: session.sessionId,
906
- update: {
907
- sessionUpdate: "agent_message_chunk",
908
- content: {
909
- type: "text",
910
- text
911
- }
912
- }
913
- });
914
- }
915
- if (m.role === "toolResult") {
916
- const tr = m;
917
- const toolName = tr.toolName;
918
- const toolCallId = tr.toolCallId;
919
- const isError = tr.isError;
920
- await this.conn.sessionUpdate({
921
- sessionId: session.sessionId,
922
- update: {
923
- sessionUpdate: "tool_call",
924
- toolCallId,
925
- title: toolName,
926
- kind: toolName === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
927
- status: "completed",
928
- rawInput: null,
929
- rawOutput: m
930
- }
931
- });
932
- const text = toolResultToText(m);
933
- await this.conn.sessionUpdate({
934
- sessionId: session.sessionId,
935
- update: {
936
- sessionUpdate: "tool_call_update",
937
- toolCallId,
938
- status: isError ? "failed" : "completed",
939
- content: text ? [{
940
- type: "content",
941
- content: {
942
- type: "text",
943
- text
944
- }
945
- }] : null,
946
- rawOutput: m
947
- }
948
- });
949
- }
950
- }
1443
+ await this.replaySessionHistory(session, piSession.messages);
951
1444
  const modes = buildThinkingModes(piSession);
952
1445
  const models = buildModelState(piSession);
953
1446
  const configOptions = buildConfigOptions(modes, models);
@@ -973,6 +1466,126 @@ var PiAcpAgent = class {
973
1466
  _meta: { piAcp: { startupInfo: null } }
974
1467
  };
975
1468
  }
1469
+ async unstable_closeSession(params) {
1470
+ if (this.sessions.maybeGet(params.sessionId) === void 0) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1471
+ this.sessions.close(params.sessionId);
1472
+ return {};
1473
+ }
1474
+ async unstable_resumeSession(params) {
1475
+ if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1476
+ const existing = this.sessions.maybeGet(params.sessionId);
1477
+ if (existing !== void 0) {
1478
+ const modes = buildThinkingModes(existing.piSession);
1479
+ const models = buildModelState(existing.piSession);
1480
+ return {
1481
+ configOptions: buildConfigOptions(modes, models),
1482
+ modes,
1483
+ models
1484
+ };
1485
+ }
1486
+ const sessionFile = await this.resolveSessionFile(params.sessionId);
1487
+ if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1488
+ let result;
1489
+ try {
1490
+ const sm = SessionManager.open(sessionFile);
1491
+ result = await createAgentSession({
1492
+ cwd: params.cwd,
1493
+ sessionManager: sm
1494
+ });
1495
+ } catch (e) {
1496
+ const authErr = detectAuthError(e);
1497
+ if (authErr !== null) throw authErr;
1498
+ const msg = e instanceof Error ? e.message : String(e);
1499
+ throw RequestError.internalError({}, `Failed to resume pi session: ${msg}`);
1500
+ }
1501
+ const piSession = result.session;
1502
+ const session = new PiAcpSession({
1503
+ sessionId: params.sessionId,
1504
+ cwd: params.cwd,
1505
+ mcpServers: params.mcpServers ?? [],
1506
+ piSession,
1507
+ conn: this.conn,
1508
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
1509
+ });
1510
+ this.sessions.register(session);
1511
+ this.sessionPaths.set(params.sessionId, sessionFile);
1512
+ const enableSkillCommands = skillCommandsEnabled(params.cwd);
1513
+ setTimeout(() => {
1514
+ (async () => {
1515
+ try {
1516
+ const commands = buildCommandList(piSession, enableSkillCommands);
1517
+ await this.conn.sessionUpdate({
1518
+ sessionId: session.sessionId,
1519
+ update: {
1520
+ sessionUpdate: "available_commands_update",
1521
+ availableCommands: mergeCommands(commands, builtinAvailableCommands())
1522
+ }
1523
+ });
1524
+ } catch {}
1525
+ })();
1526
+ }, 0);
1527
+ const modes = buildThinkingModes(piSession);
1528
+ const models = buildModelState(piSession);
1529
+ return {
1530
+ configOptions: buildConfigOptions(modes, models),
1531
+ modes,
1532
+ models
1533
+ };
1534
+ }
1535
+ async unstable_forkSession(params) {
1536
+ if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1537
+ const sourceFile = await this.resolveSessionFile(params.sessionId);
1538
+ if (sourceFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1539
+ let result;
1540
+ try {
1541
+ const sm = SessionManager.forkFrom(sourceFile, params.cwd);
1542
+ result = await createAgentSession({
1543
+ cwd: params.cwd,
1544
+ sessionManager: sm
1545
+ });
1546
+ } catch (e) {
1547
+ const authErr = detectAuthError(e);
1548
+ if (authErr !== null) throw authErr;
1549
+ const msg = e instanceof Error ? e.message : String(e);
1550
+ throw RequestError.internalError({}, `Failed to fork pi session: ${msg}`);
1551
+ }
1552
+ const piSession = result.session;
1553
+ const newSessionId = piSession.sessionManager.getSessionId();
1554
+ const newSessionFile = piSession.sessionManager.getSessionFile();
1555
+ if (newSessionFile !== void 0) this.sessionPaths.set(newSessionId, newSessionFile);
1556
+ const session = new PiAcpSession({
1557
+ sessionId: newSessionId,
1558
+ cwd: params.cwd,
1559
+ mcpServers: params.mcpServers ?? [],
1560
+ piSession,
1561
+ conn: this.conn,
1562
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
1563
+ });
1564
+ this.sessions.register(session);
1565
+ const enableSkillCommands = skillCommandsEnabled(params.cwd);
1566
+ setTimeout(() => {
1567
+ (async () => {
1568
+ try {
1569
+ const commands = buildCommandList(piSession, enableSkillCommands);
1570
+ await this.conn.sessionUpdate({
1571
+ sessionId: session.sessionId,
1572
+ update: {
1573
+ sessionUpdate: "available_commands_update",
1574
+ availableCommands: mergeCommands(commands, builtinAvailableCommands())
1575
+ }
1576
+ });
1577
+ } catch {}
1578
+ })();
1579
+ }, 0);
1580
+ const modes = buildThinkingModes(piSession);
1581
+ const models = buildModelState(piSession);
1582
+ return {
1583
+ sessionId: newSessionId,
1584
+ configOptions: buildConfigOptions(modes, models),
1585
+ modes,
1586
+ models
1587
+ };
1588
+ }
976
1589
  async setSessionMode(params) {
977
1590
  const session = this.sessions.get(params.sessionId);
978
1591
  const mode = String(params.modeId);
@@ -1414,10 +2027,15 @@ function buildCommandList(piSession, enableSkillCommands) {
1414
2027
  });
1415
2028
  return commands;
1416
2029
  }
2030
+ let cachedUpdateNotice;
1417
2031
  function buildUpdateNotice() {
2032
+ if (cachedUpdateNotice !== void 0) return cachedUpdateNotice;
1418
2033
  try {
1419
2034
  const installed = VERSION;
1420
- if (!installed || !isSemver(installed)) return null;
2035
+ if (!installed || !isSemver(installed)) {
2036
+ cachedUpdateNotice = null;
2037
+ return null;
2038
+ }
1421
2039
  const latestRes = spawnSync("npm", [
1422
2040
  "view",
1423
2041
  "@mariozechner/pi-coding-agent",
@@ -1427,10 +2045,18 @@ function buildUpdateNotice() {
1427
2045
  timeout: 800
1428
2046
  });
1429
2047
  const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
1430
- if (!latest || !isSemver(latest)) return null;
1431
- if (compareSemver(latest, installed) <= 0) return null;
1432
- return `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2048
+ if (!latest || !isSemver(latest)) {
2049
+ cachedUpdateNotice = null;
2050
+ return null;
2051
+ }
2052
+ if (compareSemver(latest, installed) <= 0) {
2053
+ cachedUpdateNotice = null;
2054
+ return null;
2055
+ }
2056
+ cachedUpdateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2057
+ return cachedUpdateNotice;
1433
2058
  } catch {
2059
+ cachedUpdateNotice = null;
1434
2060
  return null;
1435
2061
  }
1436
2062
  }