@duckmind/dm-darwin-x64 0.13.5 → 0.13.7

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 (78) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +39 -15
  3. package/extensions/dm-multicodex/package-lock.json +302 -1814
  4. package/extensions/dm-phone/README.md +23 -0
  5. package/extensions/dm-phone/index.ts +12 -0
  6. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  7. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  8. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  9. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  10. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  23. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  24. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  25. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  26. package/extensions/dm-phone/package-lock.json +66 -0
  27. package/extensions/dm-phone/package.json +35 -0
  28. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  29. package/extensions/dm-phone/public/app/attachments.js +233 -0
  30. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  31. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  32. package/extensions/dm-phone/public/app/bindings.js +178 -0
  33. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  34. package/extensions/dm-phone/public/app/commands.js +370 -0
  35. package/extensions/dm-phone/public/app/constants.js +60 -0
  36. package/extensions/dm-phone/public/app/formatters.js +131 -0
  37. package/extensions/dm-phone/public/app/handlers.js +442 -0
  38. package/extensions/dm-phone/public/app/main.js +6 -0
  39. package/extensions/dm-phone/public/app/markdown.js +105 -0
  40. package/extensions/dm-phone/public/app/messages.js +418 -0
  41. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  42. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  43. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  44. package/extensions/dm-phone/public/app/state.js +95 -0
  45. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  46. package/extensions/dm-phone/public/app/transport.js +176 -0
  47. package/extensions/dm-phone/public/app/ui.js +409 -0
  48. package/extensions/dm-phone/public/app.js +1 -0
  49. package/extensions/dm-phone/public/icon.svg +15 -0
  50. package/extensions/dm-phone/public/index.html +147 -0
  51. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  52. package/extensions/dm-phone/public/styles.css +1139 -0
  53. package/extensions/dm-phone/public/sw.js +78 -0
  54. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  55. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  56. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  57. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  58. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  59. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  60. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  61. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  62. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  63. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  64. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  65. package/extensions/dm-phone/src/extension/types.ts +73 -0
  66. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  67. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  68. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  69. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  70. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  71. package/extensions/dm-subagents/artifacts.ts +11 -5
  72. package/extensions/dm-subagents/async-execution.ts +4 -1
  73. package/extensions/dm-subagents/index.ts +1 -1
  74. package/extensions/dm-subagents/schemas.ts +1 -1
  75. package/extensions/dm-subagents/settings.ts +6 -4
  76. package/extensions/dm-subagents/subagent-runner.ts +167 -50
  77. package/extensions/dm-subagents/types.ts +62 -2
  78. package/package.json +1 -1
@@ -0,0 +1,881 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ SessionEntry,
6
+ } from "@mariozechner/pi-coding-agent";
7
+ import {
8
+ buildSessionContext,
9
+ parseFrontmatter,
10
+ stripFrontmatter,
11
+ } from "@mariozechner/pi-coding-agent";
12
+ import { readFileSync } from "node:fs";
13
+ import { dirname } from "node:path";
14
+ import type {
15
+ PendingClientResponse,
16
+ SessionController,
17
+ SessionSnapshot,
18
+ SessionStatus,
19
+ SessionSummary,
20
+ SessionWorkerOptions,
21
+ } from "./types";
22
+ import { contentToPreviewText, shortId } from "./utils";
23
+
24
+ const INLINE_IMAGE_TOKEN_PATTERN = /⟦img\d+⟧|\{img\d*\}/g;
25
+
26
+ function parseSlashCommandText(text: string) {
27
+ const value = String(text || "").trim();
28
+ if (!value.startsWith("/")) return null;
29
+
30
+ const body = value.slice(1).trim();
31
+ if (!body) return null;
32
+
33
+ const spaceIndex = body.indexOf(" ");
34
+ const name = spaceIndex === -1 ? body : body.slice(0, spaceIndex);
35
+ const args = spaceIndex === -1 ? "" : body.slice(spaceIndex + 1);
36
+
37
+ return {
38
+ text: `/${body}`,
39
+ name,
40
+ args,
41
+ };
42
+ }
43
+
44
+ function parseCommandArgs(argsString: string) {
45
+ const args: string[] = [];
46
+ let current = "";
47
+ let inQuote: string | null = null;
48
+
49
+ for (let i = 0; i < argsString.length; i += 1) {
50
+ const char = argsString[i];
51
+ if (inQuote) {
52
+ if (char === inQuote) {
53
+ inQuote = null;
54
+ } else {
55
+ current += char;
56
+ }
57
+ continue;
58
+ }
59
+
60
+ if (char === '"' || char === "'") {
61
+ inQuote = char;
62
+ continue;
63
+ }
64
+
65
+ if (char === " " || char === "\t") {
66
+ if (current) {
67
+ args.push(current);
68
+ current = "";
69
+ }
70
+ continue;
71
+ }
72
+
73
+ current += char;
74
+ }
75
+
76
+ if (current) {
77
+ args.push(current);
78
+ }
79
+
80
+ return args;
81
+ }
82
+
83
+ function substituteArgs(content: string, args: string[]) {
84
+ let result = content.replace(/\$(\d+)/g, (_match, num) => {
85
+ const index = Number.parseInt(num, 10) - 1;
86
+ return args[index] ?? "";
87
+ });
88
+
89
+ result = result.replace(/\$\{@:(\d+)(?::(\d+))?\}/g, (_match, startValue, lengthValue) => {
90
+ let start = Number.parseInt(startValue, 10) - 1;
91
+ if (start < 0) start = 0;
92
+ if (lengthValue) {
93
+ const length = Number.parseInt(lengthValue, 10);
94
+ return args.slice(start, start + length).join(" ");
95
+ }
96
+ return args.slice(start).join(" ");
97
+ });
98
+
99
+ const allArgs = args.join(" ");
100
+ result = result.replace(/\$ARGUMENTS/g, allArgs);
101
+ result = result.replace(/\$@/g, allArgs);
102
+ return result;
103
+ }
104
+
105
+ function normalizeModel(model: any) {
106
+ if (!model || typeof model !== "object") return null;
107
+ return {
108
+ id: model.id,
109
+ name: model.name,
110
+ provider: model.provider,
111
+ contextWindow: model.contextWindow,
112
+ };
113
+ }
114
+
115
+ function buildInlineContent(text: string, images: any[]) {
116
+ INLINE_IMAGE_TOKEN_PATTERN.lastIndex = 0;
117
+ const matches = [...text.matchAll(INLINE_IMAGE_TOKEN_PATTERN)];
118
+ if (matches.length === 0 || images.length === 0) {
119
+ const content = [] as any[];
120
+ if (text) content.push({ type: "text", text });
121
+ for (const image of images) {
122
+ if (image?.type === "image" && image.data && image.mimeType) {
123
+ content.push({ type: "image", data: image.data, mimeType: image.mimeType });
124
+ }
125
+ }
126
+ return content;
127
+ }
128
+
129
+ const content = [] as any[];
130
+ let lastIndex = 0;
131
+ let imageIndex = 0;
132
+
133
+ for (const match of matches) {
134
+ const token = match[0] || "";
135
+ const index = match.index ?? -1;
136
+ if (index < 0) continue;
137
+
138
+ const before = text.slice(lastIndex, index);
139
+ if (before) {
140
+ content.push({ type: "text", text: before });
141
+ }
142
+
143
+ const image = images[imageIndex];
144
+ if (image?.type === "image" && image.data && image.mimeType) {
145
+ content.push({ type: "image", data: image.data, mimeType: image.mimeType });
146
+ imageIndex += 1;
147
+ } else {
148
+ content.push({ type: "text", text: token });
149
+ }
150
+
151
+ lastIndex = index + token.length;
152
+ }
153
+
154
+ const after = text.slice(lastIndex);
155
+ if (after) {
156
+ content.push({ type: "text", text: after });
157
+ }
158
+
159
+ while (imageIndex < images.length) {
160
+ const image = images[imageIndex];
161
+ if (image?.type === "image" && image.data && image.mimeType) {
162
+ content.push({ type: "image", data: image.data, mimeType: image.mimeType });
163
+ }
164
+ imageIndex += 1;
165
+ }
166
+
167
+ return content;
168
+ }
169
+
170
+ function computeStats(messages: any[], sessionFile: string | null, sessionId: string | null) {
171
+ const userMessages = messages.filter((message) => message?.role === "user").length;
172
+ const assistantMessages = messages.filter((message) => message?.role === "assistant").length;
173
+ const toolResults = messages.filter((message) => message?.role === "toolResult").length;
174
+
175
+ let toolCalls = 0;
176
+ let totalInput = 0;
177
+ let totalOutput = 0;
178
+ let totalCacheRead = 0;
179
+ let totalCacheWrite = 0;
180
+ let totalCost = 0;
181
+
182
+ for (const message of messages) {
183
+ if (message?.role !== "assistant" || !Array.isArray(message.content) || !message.usage) continue;
184
+ toolCalls += message.content.filter((part: any) => part?.type === "toolCall").length;
185
+ totalInput += Number(message.usage.input) || 0;
186
+ totalOutput += Number(message.usage.output) || 0;
187
+ totalCacheRead += Number(message.usage.cacheRead) || 0;
188
+ totalCacheWrite += Number(message.usage.cacheWrite) || 0;
189
+ totalCost += Number(message.usage?.cost?.total) || 0;
190
+ }
191
+
192
+ return {
193
+ sessionFile,
194
+ sessionId,
195
+ userMessages,
196
+ assistantMessages,
197
+ toolCalls,
198
+ toolResults,
199
+ totalMessages: messages.length,
200
+ tokens: {
201
+ input: totalInput,
202
+ output: totalOutput,
203
+ cacheRead: totalCacheRead,
204
+ cacheWrite: totalCacheWrite,
205
+ total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
206
+ },
207
+ cost: totalCost,
208
+ };
209
+ }
210
+
211
+ type PendingUserMessage = {
212
+ baselineMessageCount: number;
213
+ message: any;
214
+ };
215
+
216
+ type ParentSessionWorkerOptions = SessionWorkerOptions<PhoneParentSessionWorker> & {
217
+ getCtx: () => ExtensionContext | null;
218
+ getCommandCtx: () => ExtensionCommandContext | null;
219
+ };
220
+
221
+ export class PhoneParentSessionWorker implements SessionController {
222
+ readonly id = "parent-session";
223
+ readonly kind = "parent" as const;
224
+ cwd: string;
225
+ previousCwd: string | null = null;
226
+ currentSessionFile: string | null = null;
227
+ lastError = "";
228
+ lastState: any = null;
229
+ lastMessages: any[] = [];
230
+ lastCommands: any[] = [];
231
+ isStreaming = false;
232
+ isCompacting = false;
233
+ lastActivityAt = Date.now();
234
+ pendingUiRequest: any = null;
235
+ liveAssistantMessage: any = null;
236
+ liveTools = new Map<string, any>();
237
+
238
+ private firstUserPreview = "";
239
+ private lastUserPreview = "";
240
+ private requestCounter = 0;
241
+ private pendingUserMessages: PendingUserMessage[] = [];
242
+
243
+ constructor(
244
+ private readonly options: ParentSessionWorkerOptions,
245
+ private readonly pi: ExtensionAPI,
246
+ ) {
247
+ this.cwd = options.cwd;
248
+ }
249
+
250
+ private touch() {
251
+ this.lastActivityAt = Date.now();
252
+ this.options.onActivity();
253
+ this.options.onStateChange();
254
+ }
255
+
256
+ private currentCtx() {
257
+ return this.options.getCtx();
258
+ }
259
+
260
+ private currentCommandCtx() {
261
+ return this.options.getCommandCtx();
262
+ }
263
+
264
+ private async withAutoConfirmedUi<T>(
265
+ ctx: ExtensionContext | ExtensionCommandContext | null,
266
+ action: () => Promise<T>,
267
+ ) {
268
+ const ui = ctx?.ui;
269
+ if (!ui || typeof ui.confirm !== "function") {
270
+ return action();
271
+ }
272
+
273
+ const originalConfirm = ui.confirm;
274
+ // Phone-triggered parent session resets should not block on a terminal confirmation dialog.
275
+ ui.confirm = async (_title: string, _message: string, _options?: unknown) => true;
276
+
277
+ try {
278
+ return await action();
279
+ } finally {
280
+ ui.confirm = originalConfirm;
281
+ }
282
+ }
283
+
284
+ private comparableContent(content: any) {
285
+ const preview = contentToPreviewText(content);
286
+ if (preview) {
287
+ return preview;
288
+ }
289
+
290
+ try {
291
+ return JSON.stringify(content ?? null);
292
+ } catch {
293
+ return String(content ?? "");
294
+ }
295
+ }
296
+
297
+ private displayedMessages() {
298
+ if (!this.pendingUserMessages.length) {
299
+ return this.lastMessages;
300
+ }
301
+ return [...this.lastMessages, ...this.pendingUserMessages.map((entry) => entry.message)];
302
+ }
303
+
304
+ private reconcilePendingUserMessages() {
305
+ if (!this.pendingUserMessages.length) return;
306
+
307
+ this.pendingUserMessages = this.pendingUserMessages.filter((pending) => {
308
+ const persistedCandidates = this.lastMessages.slice(Math.max(0, pending.baselineMessageCount));
309
+ return !persistedCandidates.some((message) => message?.role === "user" && this.comparableContent(message.content) === this.comparableContent(pending.message.content));
310
+ });
311
+ }
312
+
313
+ private updateMessagePreviews() {
314
+ const messages = this.displayedMessages();
315
+ const firstUser = messages.find((message) => message?.role === "user");
316
+ const lastUser = [...messages].reverse().find((message) => message?.role === "user");
317
+ this.firstUserPreview = firstUser ? contentToPreviewText(firstUser.content) : "";
318
+ this.lastUserPreview = lastUser ? contentToPreviewText(lastUser.content) : "";
319
+ }
320
+
321
+ private buildStateFromContext(ctx: ExtensionContext) {
322
+ const entries = ctx.sessionManager.getEntries() as SessionEntry[];
323
+ const sessionContext = buildSessionContext(entries, ctx.sessionManager.getLeafId());
324
+ const model = normalizeModel(ctx.model);
325
+ const contextUsage = ctx.getContextUsage?.();
326
+ const state = {
327
+ model,
328
+ thinkingLevel: this.pi.getThinkingLevel(),
329
+ isStreaming: this.isStreaming,
330
+ isCompacting: this.isCompacting,
331
+ sessionFile: ctx.sessionManager.getSessionFile() || null,
332
+ sessionId: ctx.sessionManager.getSessionId() || null,
333
+ sessionName: ctx.sessionManager.getSessionName() || null,
334
+ messageCount: sessionContext.messages.length + this.pendingUserMessages.length,
335
+ pendingMessageCount: ctx.hasPendingMessages() ? 1 : 0,
336
+ contextUsage: contextUsage || undefined,
337
+ };
338
+
339
+ return {
340
+ state,
341
+ messages: sessionContext.messages,
342
+ };
343
+ }
344
+
345
+ private rememberSnapshot(snapshot: { state: any; messages: any[]; commands: any[] }) {
346
+ const previousSessionFile = this.currentSessionFile;
347
+ const previousSessionId = this.lastState?.sessionId || null;
348
+
349
+ this.lastState = snapshot.state;
350
+ this.lastMessages = snapshot.messages;
351
+ this.lastCommands = snapshot.commands;
352
+ this.isStreaming = Boolean(snapshot.state?.isStreaming);
353
+ this.isCompacting = Boolean(snapshot.state?.isCompacting);
354
+ this.currentSessionFile = snapshot.state?.sessionFile || null;
355
+
356
+ const nextSessionId = snapshot.state?.sessionId || null;
357
+ if ((previousSessionFile && this.currentSessionFile && previousSessionFile !== this.currentSessionFile) || (previousSessionId && nextSessionId && previousSessionId !== nextSessionId)) {
358
+ this.pendingUserMessages = [];
359
+ } else {
360
+ this.reconcilePendingUserMessages();
361
+ }
362
+
363
+ this.updateMessagePreviews();
364
+ }
365
+
366
+ private buildResponse(id: string | undefined, command: string, success: boolean, data?: any, error?: string) {
367
+ return {
368
+ ...(id ? { id } : {}),
369
+ type: "response",
370
+ command,
371
+ success,
372
+ ...(success && data !== undefined ? { data } : {}),
373
+ ...(!success && error ? { error } : {}),
374
+ };
375
+ }
376
+
377
+ private activeCommands() {
378
+ return Array.isArray(this.lastCommands) && this.lastCommands.length > 0 ? this.lastCommands : this.pi.getCommands();
379
+ }
380
+
381
+ private expandPromptTemplate(text: string, filePath: string) {
382
+ const parsed = parseSlashCommandText(text);
383
+ if (!parsed) return text;
384
+ const content = readFileSync(filePath, "utf8");
385
+ const body = parseFrontmatter(content).body;
386
+ const args = parseCommandArgs(parsed.args);
387
+ return substituteArgs(body, args);
388
+ }
389
+
390
+ private expandSkillCommand(text: string, filePath: string, skillName: string) {
391
+ const parsed = parseSlashCommandText(text);
392
+ if (!parsed) return text;
393
+ const body = stripFrontmatter(readFileSync(filePath, "utf8")).trim();
394
+ const baseDir = dirname(filePath);
395
+ const skillBlock = `<skill name="${skillName}" location="${filePath}">\nReferences are relative to ${baseDir}.\n\n${body}\n</skill>`;
396
+ return parsed.args ? `${skillBlock}\n\n${parsed.args}` : skillBlock;
397
+ }
398
+
399
+ private async preparePromptText(text: string) {
400
+ const parsed = parseSlashCommandText(text);
401
+ if (!parsed) return text;
402
+
403
+ const command = this.activeCommands().find((entry: any) => entry?.name === parsed.name);
404
+ if (!command) return text;
405
+
406
+ if (command.source === "extension") {
407
+ throw new Error("Extension slash commands are not supported while mirroring the live CLI session. Open a parallel session to use them.");
408
+ }
409
+
410
+ if (command.source === "prompt" && typeof command.path === "string" && command.path) {
411
+ return this.expandPromptTemplate(text, command.path);
412
+ }
413
+
414
+ if (command.source === "skill" && typeof command.path === "string" && command.path) {
415
+ const skillName = parsed.name.replace(/^skill:/, "") || parsed.name;
416
+ return this.expandSkillCommand(text, command.path, skillName);
417
+ }
418
+
419
+ return text;
420
+ }
421
+
422
+ private async submitPrompt(message: string, images: unknown[] = [], streamingBehavior?: "steer" | "followUp") {
423
+ const ctx = this.currentCtx();
424
+ if (!ctx) {
425
+ throw new Error("Live CLI session context is not available yet.");
426
+ }
427
+ if (!ctx.isIdle() && !streamingBehavior) {
428
+ throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
429
+ }
430
+
431
+ const text = await this.preparePromptText(String(message || ""));
432
+ const normalizedImages = Array.isArray(images)
433
+ ? images.filter((image: any) => image?.type === "image" && image.data && image.mimeType)
434
+ : [];
435
+
436
+ const content = normalizedImages.length > 0 ? buildInlineContent(text, normalizedImages) : text;
437
+ const pendingMessage: PendingUserMessage = {
438
+ baselineMessageCount: this.lastMessages.length,
439
+ message: {
440
+ role: "user",
441
+ content,
442
+ timestamp: Date.now(),
443
+ },
444
+ };
445
+
446
+ this.pendingUserMessages.push(pendingMessage);
447
+ if (this.lastState) {
448
+ this.lastState = {
449
+ ...this.lastState,
450
+ messageCount: Number(this.lastState.messageCount || this.lastMessages.length) + 1,
451
+ };
452
+ }
453
+ this.updateMessagePreviews();
454
+ this.touch();
455
+ this.emitSnapshot();
456
+
457
+ try {
458
+ this.pi.sendUserMessage(content as any, streamingBehavior ? { deliverAs: streamingBehavior } : undefined);
459
+ } catch (error) {
460
+ this.pendingUserMessages = this.pendingUserMessages.filter((entry) => entry !== pendingMessage);
461
+ if (this.lastState) {
462
+ this.lastState = {
463
+ ...this.lastState,
464
+ messageCount: Math.max(this.lastMessages.length, Number(this.lastState.messageCount || this.lastMessages.length) - 1),
465
+ };
466
+ }
467
+ this.updateMessagePreviews();
468
+ this.emitSnapshot();
469
+ throw error;
470
+ }
471
+ }
472
+
473
+ private async switchParentSession(sessionPath: string) {
474
+ const commandCtx = this.currentCommandCtx();
475
+ if (!commandCtx) {
476
+ throw new Error("No active command context is available to switch the live CLI session.");
477
+ }
478
+
479
+ const result = await commandCtx.switchSession(sessionPath);
480
+ await this.refreshCachedSnapshot();
481
+ this.emitSnapshot();
482
+ return result;
483
+ }
484
+
485
+ private emitSnapshot() {
486
+ this.options.onEnvelope(this, {
487
+ channel: "snapshot",
488
+ sessionWorkerId: this.id,
489
+ state: this.lastState,
490
+ messages: this.displayedMessages(),
491
+ commands: this.lastCommands,
492
+ liveAssistantMessage: this.liveAssistantMessage,
493
+ liveTools: [...this.liveTools.values()],
494
+ });
495
+ }
496
+
497
+ async ensureStarted() {
498
+ await this.refreshCachedSnapshot();
499
+ }
500
+
501
+ async refreshCachedSnapshot(): Promise<SessionSnapshot> {
502
+ const ctx = this.currentCtx();
503
+ if (!ctx) {
504
+ throw new Error("Live CLI session context is not available yet.");
505
+ }
506
+
507
+ this.cwd = ctx.sessionManager.getCwd();
508
+ const { state, messages } = this.buildStateFromContext(ctx);
509
+ const commands = this.pi.getCommands();
510
+ this.rememberSnapshot({ state, messages, commands });
511
+ return this.getCachedSnapshot();
512
+ }
513
+
514
+ async getSnapshot(): Promise<SessionSnapshot> {
515
+ return this.refreshCachedSnapshot();
516
+ }
517
+
518
+ getCachedSnapshot(): SessionSnapshot {
519
+ return {
520
+ state: this.lastState,
521
+ messages: this.displayedMessages(),
522
+ commands: this.lastCommands,
523
+ liveAssistantMessage: this.liveAssistantMessage,
524
+ liveTools: [...this.liveTools.values()],
525
+ };
526
+ }
527
+
528
+ getStatus(): SessionStatus {
529
+ return {
530
+ childRunning: true,
531
+ cwd: this.cwd,
532
+ previousCwd: this.previousCwd,
533
+ isStreaming: this.isStreaming,
534
+ isCompacting: this.isCompacting,
535
+ lastError: this.lastError,
536
+ childPid: process.pid,
537
+ sessionWorkerId: this.id,
538
+ sessionKind: "parent",
539
+ };
540
+ }
541
+
542
+ getSummary(): SessionSummary {
543
+ const sessionId = this.lastState?.sessionId || null;
544
+ const sessionName = this.lastState?.sessionName || null;
545
+ const label = sessionName || this.firstUserPreview || (sessionId ? `Session ${shortId(sessionId)}` : "Current CLI session");
546
+ const secondaryLabel = sessionName ? this.firstUserPreview || shortId(sessionId) || "" : "mirroring cli";
547
+
548
+ return {
549
+ id: this.id,
550
+ kind: "parent",
551
+ sessionId,
552
+ sessionFile: this.currentSessionFile || this.lastState?.sessionFile || null,
553
+ sessionName,
554
+ label,
555
+ secondaryLabel,
556
+ firstUserPreview: this.firstUserPreview || null,
557
+ lastUserPreview: this.lastUserPreview || null,
558
+ model: this.lastState?.model
559
+ ? {
560
+ id: this.lastState.model.id,
561
+ name: this.lastState.model.name,
562
+ provider: this.lastState.model.provider,
563
+ }
564
+ : null,
565
+ isRunning: true,
566
+ isStreaming: this.isStreaming,
567
+ isCompacting: this.isCompacting,
568
+ messageCount: this.lastState?.messageCount ?? this.lastMessages.length,
569
+ pendingMessageCount: this.lastState?.pendingMessageCount ?? 0,
570
+ hasPendingUiRequest: false,
571
+ lastError: this.lastError,
572
+ lastActivityAt: this.lastActivityAt,
573
+ childPid: process.pid,
574
+ cwd: this.cwd,
575
+ mirrorsCli: true,
576
+ };
577
+ }
578
+
579
+ async request(command: Record<string, unknown>): Promise<any> {
580
+ const id = typeof command.id === "string" ? command.id : undefined;
581
+ const type = String(command.type || "unknown");
582
+
583
+ try {
584
+ if (type === "get_state") {
585
+ await this.refreshCachedSnapshot();
586
+ return this.buildResponse(id, type, true, this.lastState);
587
+ }
588
+
589
+ if (type === "get_messages") {
590
+ await this.refreshCachedSnapshot();
591
+ return this.buildResponse(id, type, true, { messages: this.lastMessages });
592
+ }
593
+
594
+ if (type === "get_commands") {
595
+ await this.refreshCachedSnapshot();
596
+ return this.buildResponse(id, type, true, { commands: this.lastCommands });
597
+ }
598
+
599
+ if (type === "get_available_models") {
600
+ const ctx = this.currentCtx();
601
+ if (!ctx) throw new Error("Live CLI session context is not available yet.");
602
+ return this.buildResponse(id, type, true, { models: ctx.modelRegistry.getAvailable() });
603
+ }
604
+
605
+ if (type === "get_session_stats") {
606
+ await this.refreshCachedSnapshot();
607
+ return this.buildResponse(id, type, true, computeStats(this.lastMessages, this.currentSessionFile, this.lastState?.sessionId || null));
608
+ }
609
+
610
+ if (type === "prompt") {
611
+ await this.submitPrompt(String(command.message || ""), Array.isArray(command.images) ? command.images : [], command.streamingBehavior === "steer"
612
+ ? "steer"
613
+ : command.streamingBehavior === "followUp"
614
+ ? "followUp"
615
+ : undefined);
616
+ return this.buildResponse(id, type, true);
617
+ }
618
+
619
+ if (type === "abort") {
620
+ const ctx = this.currentCtx();
621
+ if (!ctx) throw new Error("Live CLI session context is not available yet.");
622
+ ctx.abort();
623
+ return this.buildResponse(id, type, true);
624
+ }
625
+
626
+ if (type === "compact") {
627
+ const ctx = this.currentCtx();
628
+ if (!ctx) throw new Error("Live CLI session context is not available yet.");
629
+ ctx.compact(typeof command.customInstructions === "string" && command.customInstructions
630
+ ? { customInstructions: command.customInstructions }
631
+ : undefined);
632
+ return this.buildResponse(id, type, true, { started: true });
633
+ }
634
+
635
+ if (type === "new_session") {
636
+ const commandCtx = this.currentCommandCtx();
637
+ if (!commandCtx) throw new Error("No active command context is available to create a new live CLI session.");
638
+ const result = await this.withAutoConfirmedUi(commandCtx, () => commandCtx.newSession(typeof command.parentSession === "string" && command.parentSession
639
+ ? { parentSession: command.parentSession }
640
+ : undefined));
641
+ await this.refreshCachedSnapshot();
642
+ this.emitSnapshot();
643
+ return this.buildResponse(id, type, true, result);
644
+ }
645
+
646
+ if (type === "switch_session") {
647
+ const result = await this.switchParentSession(String(command.sessionPath || ""));
648
+ return this.buildResponse(id, type, true, result);
649
+ }
650
+
651
+ if (type === "fork") {
652
+ const commandCtx = this.currentCommandCtx();
653
+ if (!commandCtx) throw new Error("No active command context is available to fork the live CLI session.");
654
+ const result = await commandCtx.fork(String(command.entryId || ""));
655
+ await this.refreshCachedSnapshot();
656
+ this.emitSnapshot();
657
+ return this.buildResponse(id, type, true, { cancelled: result.cancelled });
658
+ }
659
+
660
+ if (type === "set_model") {
661
+ const ctx = this.currentCtx();
662
+ if (!ctx) throw new Error("Live CLI session context is not available yet.");
663
+ const models = ctx.modelRegistry.getAvailable();
664
+ const model = models.find((entry: any) => entry.provider === command.provider && entry.id === command.modelId);
665
+ if (!model) {
666
+ return this.buildResponse(id, type, false, undefined, `Model not found: ${String(command.provider || "")}/${String(command.modelId || "")}`);
667
+ }
668
+ const changed = await this.pi.setModel(model as any);
669
+ if (!changed) {
670
+ return this.buildResponse(id, type, false, undefined, `No API key found for ${String(command.provider || "")}.`);
671
+ }
672
+ await this.refreshCachedSnapshot();
673
+ this.emitSnapshot();
674
+ return this.buildResponse(id, type, true, model);
675
+ }
676
+
677
+ if (type === "set_thinking_level") {
678
+ this.pi.setThinkingLevel(command.level as any);
679
+ await this.refreshCachedSnapshot();
680
+ this.emitSnapshot();
681
+ return this.buildResponse(id, type, true);
682
+ }
683
+
684
+ if (type === "reload") {
685
+ const commandCtx = this.currentCommandCtx();
686
+ if (!commandCtx) throw new Error("No active command context is available to reload the live CLI session.");
687
+ await commandCtx.reload();
688
+ await this.refreshCachedSnapshot();
689
+ this.emitSnapshot();
690
+ return this.buildResponse(id, type, true);
691
+ }
692
+
693
+ if (type === "set_session_name") {
694
+ const name = String(command.name || "").trim();
695
+ if (!name) {
696
+ return this.buildResponse(id, type, false, undefined, "Session name cannot be empty");
697
+ }
698
+ this.pi.setSessionName(name);
699
+ await this.refreshCachedSnapshot();
700
+ this.emitSnapshot();
701
+ return this.buildResponse(id, type, true);
702
+ }
703
+
704
+ return this.buildResponse(id, type, false, undefined, `Unsupported live CLI command: ${type}`);
705
+ } catch (error) {
706
+ const message = error instanceof Error ? error.message : String(error);
707
+ this.lastError = message;
708
+ this.options.onStateChange();
709
+ return this.buildResponse(id, type, false, undefined, message);
710
+ }
711
+ }
712
+
713
+ async sendClientCommand(command: Record<string, unknown>, meta?: PendingClientResponse) {
714
+ const nextCommand = { ...command } as Record<string, any>;
715
+ if (!nextCommand.id) {
716
+ nextCommand.id = `parent-${++this.requestCounter}`;
717
+ }
718
+
719
+ const response = await this.request(nextCommand);
720
+
721
+ if (meta?.ws) {
722
+ try {
723
+ if (response.success) meta.onSuccess?.(response);
724
+ else meta.onError?.(response);
725
+ } catch {
726
+ // ignore local response side effects
727
+ }
728
+
729
+ const nextPayload = {
730
+ ...response,
731
+ ...(meta.responseCommand ? { command: meta.responseCommand } : {}),
732
+ ...(response.success && meta.responseData ? { data: { ...(response.data || {}), ...meta.responseData } } : {}),
733
+ };
734
+ this.options.send(meta.ws, { channel: "rpc", payload: nextPayload });
735
+ }
736
+
737
+ return String(nextCommand.id);
738
+ }
739
+
740
+ async reload() {
741
+ const response = await this.request({ type: "reload" });
742
+ if (!response?.success) {
743
+ throw new Error(response?.error || "Failed to reload the live CLI session.");
744
+ }
745
+ }
746
+
747
+ captureContext(ctx: ExtensionContext | ExtensionCommandContext, options: { emitSnapshot?: boolean } = {}) {
748
+ this.cwd = ctx.sessionManager.getCwd();
749
+ this.touch();
750
+ void this.refreshCachedSnapshot()
751
+ .then(() => {
752
+ if (options.emitSnapshot) {
753
+ this.emitSnapshot();
754
+ }
755
+ })
756
+ .catch((error) => {
757
+ this.lastError = error instanceof Error ? error.message : String(error);
758
+ this.options.onStateChange();
759
+ });
760
+ }
761
+
762
+ handleAgentStart(ctx: ExtensionContext | ExtensionCommandContext) {
763
+ this.cwd = ctx.sessionManager.getCwd();
764
+ this.isStreaming = true;
765
+ if (this.lastState) {
766
+ this.lastState = { ...this.lastState, isStreaming: true };
767
+ }
768
+ this.touch();
769
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "agent_start" } });
770
+ }
771
+
772
+ handleAgentEnd(ctx: ExtensionContext | ExtensionCommandContext) {
773
+ this.cwd = ctx.sessionManager.getCwd();
774
+ this.isStreaming = false;
775
+ this.liveAssistantMessage = null;
776
+ this.liveTools.clear();
777
+ if (this.lastState) {
778
+ this.lastState = { ...this.lastState, isStreaming: false };
779
+ }
780
+ this.touch();
781
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "agent_end" } });
782
+ this.captureContext(ctx, { emitSnapshot: true });
783
+ }
784
+
785
+ handleMessageStart(event: { message: any }, ctx: ExtensionContext | ExtensionCommandContext) {
786
+ if (event.message?.role === "assistant") {
787
+ this.liveAssistantMessage = event.message;
788
+ }
789
+ this.touch();
790
+ if (event.message?.role === "user") {
791
+ this.captureContext(ctx, { emitSnapshot: true });
792
+ }
793
+ }
794
+
795
+ handleMessageUpdate(event: { message: any; assistantMessageEvent: any }, ctx: ExtensionContext | ExtensionCommandContext) {
796
+ this.cwd = ctx.sessionManager.getCwd();
797
+ if (event.message?.role === "assistant") {
798
+ this.liveAssistantMessage = event.message;
799
+ }
800
+ this.touch();
801
+ this.options.onEnvelope(this, {
802
+ channel: "rpc",
803
+ payload: {
804
+ type: "message_update",
805
+ message: event.message,
806
+ assistantMessageEvent: event.assistantMessageEvent,
807
+ },
808
+ });
809
+ }
810
+
811
+ handleMessageEnd(event: { message: any }, ctx: ExtensionContext | ExtensionCommandContext) {
812
+ if (event.message?.role === "assistant") {
813
+ this.liveAssistantMessage = null;
814
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "message_end", message: event.message } });
815
+ this.captureContext(ctx, { emitSnapshot: true });
816
+ return;
817
+ }
818
+
819
+ this.captureContext(ctx, { emitSnapshot: true });
820
+ }
821
+
822
+ handleToolExecutionStart(event: { toolCallId: string; toolName: string; args: any }) {
823
+ this.liveTools.set(event.toolCallId, {
824
+ toolCallId: event.toolCallId,
825
+ toolName: event.toolName || "tool",
826
+ args: event.args || {},
827
+ partialResult: null,
828
+ result: null,
829
+ isError: false,
830
+ });
831
+ this.touch();
832
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "tool_execution_start", ...event } });
833
+ }
834
+
835
+ handleToolExecutionUpdate(event: { toolCallId: string; toolName: string; args: any; partialResult: any }) {
836
+ const current = this.liveTools.get(event.toolCallId) || {};
837
+ this.liveTools.set(event.toolCallId, {
838
+ ...current,
839
+ toolCallId: event.toolCallId,
840
+ toolName: event.toolName || current.toolName || "tool",
841
+ args: event.args || current.args || {},
842
+ partialResult: event.partialResult || current.partialResult || null,
843
+ result: current.result || null,
844
+ isError: current.isError || false,
845
+ });
846
+ this.touch();
847
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "tool_execution_update", ...event } });
848
+ }
849
+
850
+ handleToolExecutionEnd(event: { toolCallId: string; toolName: string; args: any; result: any; isError: boolean }) {
851
+ const current = this.liveTools.get(event.toolCallId) || {};
852
+ this.liveTools.set(event.toolCallId, {
853
+ ...current,
854
+ toolCallId: event.toolCallId,
855
+ toolName: event.toolName || current.toolName || "tool",
856
+ args: event.args || current.args || {},
857
+ partialResult: current.partialResult || null,
858
+ result: event.result || null,
859
+ isError: Boolean(event.isError),
860
+ });
861
+ this.touch();
862
+ this.options.onEnvelope(this, { channel: "rpc", payload: { type: "tool_execution_end", ...event } });
863
+ }
864
+
865
+ setCompacting(isCompacting: boolean, ctx: ExtensionContext | ExtensionCommandContext) {
866
+ this.isCompacting = isCompacting;
867
+ if (this.lastState) {
868
+ this.lastState = { ...this.lastState, isCompacting };
869
+ }
870
+ this.captureContext(ctx, { emitSnapshot: Boolean(!isCompacting) });
871
+ }
872
+
873
+ async dispose() {
874
+ this.isStreaming = false;
875
+ this.isCompacting = false;
876
+ this.pendingUiRequest = null;
877
+ this.liveAssistantMessage = null;
878
+ this.liveTools.clear();
879
+ this.options.onStateChange();
880
+ }
881
+ }