@duckmind/dm-darwin-x64 0.33.0 → 0.33.2

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