@duckmind/dm-darwin-arm64 0.13.6 → 0.13.8

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 +26 -2
  3. package/extensions/dm-phone/README.md +23 -0
  4. package/extensions/dm-phone/index.ts +12 -0
  5. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  6. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  7. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  8. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  9. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  10. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  23. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  24. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  25. package/extensions/dm-phone/package-lock.json +66 -0
  26. package/extensions/dm-phone/package.json +35 -0
  27. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  28. package/extensions/dm-phone/public/app/attachments.js +233 -0
  29. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  30. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  31. package/extensions/dm-phone/public/app/bindings.js +178 -0
  32. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  33. package/extensions/dm-phone/public/app/commands.js +370 -0
  34. package/extensions/dm-phone/public/app/constants.js +60 -0
  35. package/extensions/dm-phone/public/app/formatters.js +131 -0
  36. package/extensions/dm-phone/public/app/handlers.js +442 -0
  37. package/extensions/dm-phone/public/app/main.js +6 -0
  38. package/extensions/dm-phone/public/app/markdown.js +105 -0
  39. package/extensions/dm-phone/public/app/messages.js +418 -0
  40. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  41. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  42. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  43. package/extensions/dm-phone/public/app/state.js +95 -0
  44. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  45. package/extensions/dm-phone/public/app/transport.js +176 -0
  46. package/extensions/dm-phone/public/app/ui.js +409 -0
  47. package/extensions/dm-phone/public/app.js +1 -0
  48. package/extensions/dm-phone/public/icon.svg +15 -0
  49. package/extensions/dm-phone/public/index.html +147 -0
  50. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  51. package/extensions/dm-phone/public/styles.css +1139 -0
  52. package/extensions/dm-phone/public/sw.js +78 -0
  53. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  54. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  55. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  56. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  57. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  58. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  59. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  60. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  61. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  62. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  63. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  64. package/extensions/dm-phone/src/extension/types.ts +73 -0
  65. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  66. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  67. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  68. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  69. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  70. package/extensions/dm-subagents/agent-management.ts +15 -6
  71. package/extensions/dm-subagents/agent-manager-detail.ts +12 -2
  72. package/extensions/dm-subagents/agent-manager-edit.ts +75 -23
  73. package/extensions/dm-subagents/agent-manager-list.ts +9 -2
  74. package/extensions/dm-subagents/agent-manager.ts +199 -11
  75. package/extensions/dm-subagents/agents.ts +315 -20
  76. package/extensions/dm-ultrathink/README.md +5 -0
  77. package/extensions/dm-ultrathink/src/naming.ts +75 -3
  78. package/package.json +1 -1
@@ -0,0 +1,734 @@
1
+ import { calculateContextTokens, estimateTokens } from "@mariozechner/pi-coding-agent";
2
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ import type {
5
+ PendingClientResponse,
6
+ PendingRequest,
7
+ SessionSnapshot,
8
+ SessionSummary,
9
+ SessionWorkerOptions,
10
+ } from "./types";
11
+ import { contentToPreviewText, shortId } from "./utils";
12
+
13
+ let workerCounter = 0;
14
+
15
+ export class PhoneSessionWorker {
16
+ id = `active-session-${++workerCounter}`;
17
+ kind = "parallel" as const;
18
+ cwd: string;
19
+ previousCwd: string | null = null;
20
+ currentSessionFile: string | null;
21
+ child: ChildProcessWithoutNullStreams | null = null;
22
+ lastError = "";
23
+ lastState: any = null;
24
+ lastMessages: any[] = [];
25
+ lastCommands: any[] = [];
26
+ isStreaming = false;
27
+ lastActivityAt = Date.now();
28
+ pendingUiRequest: any = null;
29
+ liveAssistantMessage: any = null;
30
+ liveTools = new Map<string, any>();
31
+
32
+ private readonly options: SessionWorkerOptions<PhoneSessionWorker>;
33
+ private readonly decoder = new StringDecoder("utf8");
34
+ private stdoutBuffer = "";
35
+ private startPromise: Promise<void> | null = null;
36
+ private requestCounter = 0;
37
+ private pendingRequests = new Map<string, PendingRequest>();
38
+ private pendingClientResponses = new Map<string, PendingClientResponse>();
39
+ private snapshotRefreshTimer: NodeJS.Timeout | null = null;
40
+ private reloadPromise: Promise<void> | null = null;
41
+ private isRestarting = false;
42
+ private disposed = false;
43
+ private firstUserPreview = "";
44
+ private lastUserPreview = "";
45
+
46
+ constructor(options: SessionWorkerOptions<PhoneSessionWorker>, sessionFile: string | null = null) {
47
+ this.options = options;
48
+ this.cwd = options.cwd;
49
+ this.currentSessionFile = sessionFile;
50
+ }
51
+
52
+ private touch() {
53
+ this.lastActivityAt = Date.now();
54
+ this.options.onActivity();
55
+ this.options.onStateChange();
56
+ }
57
+
58
+ private updateMessagePreviews() {
59
+ const firstUser = this.lastMessages.find((message) => message?.role === "user");
60
+ const lastUser = [...this.lastMessages].reverse().find((message) => message?.role === "user");
61
+ this.firstUserPreview = firstUser ? contentToPreviewText(firstUser.content) : "";
62
+ this.lastUserPreview = lastUser ? contentToPreviewText(lastUser.content) : "";
63
+ }
64
+
65
+ private setMessages(messages: any[]) {
66
+ this.lastMessages = Array.isArray(messages) ? messages : [];
67
+ this.updateMessagePreviews();
68
+ this.syncDerivedState();
69
+ this.options.onStateChange();
70
+ }
71
+
72
+ private estimateContextTokens(messages: any[]) {
73
+ let lastUsageIndex = -1;
74
+ let usageTokens = 0;
75
+
76
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
77
+ const message = messages[index];
78
+ if (message?.role !== "assistant") continue;
79
+ if (message.stopReason === "aborted" || message.stopReason === "error") continue;
80
+ if (!message.usage) continue;
81
+
82
+ usageTokens = calculateContextTokens(message.usage);
83
+ lastUsageIndex = index;
84
+ break;
85
+ }
86
+
87
+ if (lastUsageIndex === -1) {
88
+ let estimated = 0;
89
+ for (const message of messages) {
90
+ estimated += estimateTokens(message);
91
+ }
92
+ return estimated;
93
+ }
94
+
95
+ let trailingTokens = 0;
96
+ for (let index = lastUsageIndex + 1; index < messages.length; index += 1) {
97
+ trailingTokens += estimateTokens(messages[index]);
98
+ }
99
+
100
+ return usageTokens + trailingTokens;
101
+ }
102
+
103
+ private buildContextUsage() {
104
+ const contextWindow = Number(this.lastState?.model?.contextWindow);
105
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
106
+ return null;
107
+ }
108
+
109
+ let latestCompactionIndex = -1;
110
+ for (let index = this.lastMessages.length - 1; index >= 0; index -= 1) {
111
+ if (this.lastMessages[index]?.role === "compactionSummary") {
112
+ latestCompactionIndex = index;
113
+ break;
114
+ }
115
+ }
116
+
117
+ if (latestCompactionIndex !== -1) {
118
+ let hasPostCompactionUsage = false;
119
+
120
+ for (let index = this.lastMessages.length - 1; index > latestCompactionIndex; index -= 1) {
121
+ const message = this.lastMessages[index];
122
+ if (message?.role !== "assistant") continue;
123
+ if (message.stopReason === "aborted" || message.stopReason === "error") continue;
124
+
125
+ if (message.usage && calculateContextTokens(message.usage) > 0) {
126
+ hasPostCompactionUsage = true;
127
+ }
128
+ break;
129
+ }
130
+
131
+ if (!hasPostCompactionUsage) {
132
+ return {
133
+ tokens: null,
134
+ contextWindow,
135
+ percent: null,
136
+ };
137
+ }
138
+ }
139
+
140
+ const tokens = this.estimateContextTokens(this.lastMessages);
141
+ return {
142
+ tokens,
143
+ contextWindow,
144
+ percent: (tokens / contextWindow) * 100,
145
+ };
146
+ }
147
+
148
+ private syncDerivedState() {
149
+ if (!this.lastState || typeof this.lastState !== "object") return;
150
+
151
+ const contextUsage = this.buildContextUsage();
152
+ if (!contextUsage) {
153
+ const { contextUsage: _ignored, ...nextState } = this.lastState;
154
+ this.lastState = nextState;
155
+ return;
156
+ }
157
+
158
+ this.lastState = {
159
+ ...this.lastState,
160
+ contextUsage,
161
+ };
162
+ }
163
+
164
+ private rememberState(state: any) {
165
+ if (!state || typeof state !== "object") return;
166
+ this.lastState = state;
167
+ if (typeof state.isStreaming === "boolean") {
168
+ this.isStreaming = state.isStreaming;
169
+ }
170
+ if (typeof state.sessionFile === "string" && state.sessionFile.trim()) {
171
+ this.currentSessionFile = state.sessionFile;
172
+ }
173
+ this.syncDerivedState();
174
+ this.options.onStateChange();
175
+ }
176
+
177
+ private buildSpawnArgs(sessionFile = this.currentSessionFile) {
178
+ const args = ["--mode", "rpc"];
179
+ if (sessionFile) {
180
+ args.push("--session", sessionFile);
181
+ }
182
+ return args;
183
+ }
184
+
185
+ getStatus() {
186
+ return {
187
+ childRunning: Boolean(this.child),
188
+ cwd: this.cwd,
189
+ previousCwd: this.previousCwd,
190
+ isStreaming: this.isStreaming,
191
+ isCompacting: Boolean(this.lastState?.isCompacting),
192
+ lastError: this.lastError,
193
+ childPid: this.child?.pid ?? null,
194
+ sessionWorkerId: this.id,
195
+ sessionKind: "parallel" as const,
196
+ };
197
+ }
198
+
199
+ setTrackedCwd(cwd: string, previousCwd: string | null = this.cwd) {
200
+ this.previousCwd = previousCwd;
201
+ this.cwd = cwd;
202
+ this.options.onStateChange();
203
+ }
204
+
205
+ getSummary(): SessionSummary {
206
+ const sessionId = this.lastState?.sessionId || null;
207
+ const sessionName = this.lastState?.sessionName || null;
208
+ const label = sessionName || this.firstUserPreview || (sessionId ? `Session ${shortId(sessionId)}` : `Session ${shortId(this.id)}`);
209
+ const secondaryLabel = sessionName ? this.firstUserPreview || shortId(sessionId) || "" : shortId(sessionId) || "";
210
+
211
+ return {
212
+ id: this.id,
213
+ kind: "parallel",
214
+ sessionId,
215
+ sessionFile: this.currentSessionFile || this.lastState?.sessionFile || null,
216
+ sessionName,
217
+ label,
218
+ secondaryLabel,
219
+ firstUserPreview: this.firstUserPreview || null,
220
+ lastUserPreview: this.lastUserPreview || null,
221
+ model: this.lastState?.model
222
+ ? {
223
+ id: this.lastState.model.id,
224
+ name: this.lastState.model.name,
225
+ provider: this.lastState.model.provider,
226
+ }
227
+ : null,
228
+ isRunning: Boolean(this.child),
229
+ isStreaming: this.isStreaming,
230
+ isCompacting: Boolean(this.lastState?.isCompacting),
231
+ messageCount: this.lastState?.messageCount ?? this.lastMessages.length,
232
+ pendingMessageCount: this.lastState?.pendingMessageCount ?? 0,
233
+ hasPendingUiRequest: Boolean(this.pendingUiRequest),
234
+ lastError: this.lastError,
235
+ lastActivityAt: this.lastActivityAt,
236
+ childPid: this.child?.pid ?? null,
237
+ cwd: this.cwd,
238
+ };
239
+ }
240
+
241
+ private cachedSnapshot(): SessionSnapshot {
242
+ return {
243
+ state: this.lastState,
244
+ messages: this.lastMessages,
245
+ commands: this.lastCommands,
246
+ liveAssistantMessage: this.liveAssistantMessage,
247
+ liveTools: [...this.liveTools.values()],
248
+ };
249
+ }
250
+
251
+ getCachedSnapshot(): SessionSnapshot {
252
+ return this.cachedSnapshot();
253
+ }
254
+
255
+ async ensureStarted(startOptions: { sessionFile?: string | null } = {}) {
256
+ if (this.disposed) {
257
+ throw new Error("Session worker disposed.");
258
+ }
259
+
260
+ if (this.child) return;
261
+ if (this.startPromise) return this.startPromise;
262
+
263
+ const sessionFile = startOptions.sessionFile ?? this.currentSessionFile;
264
+ this.stdoutBuffer = "";
265
+
266
+ this.startPromise = new Promise<void>((resolvePromise, rejectPromise) => {
267
+ const spawned = spawn("dm", this.buildSpawnArgs(sessionFile), {
268
+ cwd: this.cwd,
269
+ env: {
270
+ ...process.env,
271
+ DM_PHONE_CHILD: "1",
272
+ PI_PHONE_CHILD: "1",
273
+ },
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+
277
+ let settled = false;
278
+
279
+ const failStart = (error: Error) => {
280
+ if (settled) return;
281
+ settled = true;
282
+ this.lastError = error.message;
283
+ this.child = null;
284
+ this.options.onStateChange();
285
+ rejectPromise(error);
286
+ };
287
+
288
+ spawned.once("error", (error) => {
289
+ failStart(error instanceof Error ? error : new Error(String(error)));
290
+ });
291
+
292
+ spawned.stdout.on("data", (chunk) => {
293
+ this.handleStdoutChunk(chunk);
294
+ });
295
+
296
+ spawned.stderr.on("data", (chunk) => {
297
+ const text = chunk.toString();
298
+ this.lastError = text.trim() || this.lastError;
299
+ this.options.onEnvelope(this, { channel: "server", event: "stderr", data: { text } });
300
+ this.touch();
301
+ });
302
+
303
+ spawned.once("exit", (code, signal) => {
304
+ const message = `dm rpc exited${code !== null ? ` with code ${code}` : ""}${signal ? ` (${signal})` : ""}`;
305
+ const restarting = this.isRestarting;
306
+
307
+ if (!settled) {
308
+ failStart(new Error(message));
309
+ return;
310
+ }
311
+
312
+ this.child = null;
313
+ this.isStreaming = false;
314
+ this.lastState = this.lastState ? { ...this.lastState, isStreaming: false } : this.lastState;
315
+ this.pendingUiRequest = null;
316
+ this.liveAssistantMessage = null;
317
+ this.liveTools.clear();
318
+ this.rejectAllPending(new Error(restarting ? "DM rpc is reloading." : message));
319
+
320
+ if (restarting || this.disposed) {
321
+ this.lastError = "";
322
+ this.options.onStateChange();
323
+ return;
324
+ }
325
+
326
+ this.lastError = message;
327
+ this.options.onEnvelope(this, { channel: "server", event: "agent-exit", data: { code, signal, message } });
328
+ this.touch();
329
+
330
+ if (this.options.shouldAutoRestart(this)) {
331
+ setTimeout(() => {
332
+ if (this.disposed) return;
333
+ this.ensureStarted({ sessionFile: this.currentSessionFile })
334
+ .then(() => this.refreshCachedSnapshot().catch(() => {}))
335
+ .catch((error) => {
336
+ this.lastError = error instanceof Error ? error.message : String(error);
337
+ this.options.onStateChange();
338
+ });
339
+ }, 1500);
340
+ }
341
+ });
342
+
343
+ this.child = spawned;
344
+ this.lastError = "";
345
+ this.touch();
346
+
347
+ setTimeout(() => {
348
+ if (settled) return;
349
+ settled = true;
350
+ resolvePromise();
351
+ }, 300);
352
+ }).finally(() => {
353
+ this.startPromise = null;
354
+ });
355
+
356
+ return this.startPromise;
357
+ }
358
+
359
+ private rejectAllPending(error: Error) {
360
+ for (const pending of this.pendingRequests.values()) {
361
+ clearTimeout(pending.timer);
362
+ pending.reject(error);
363
+ }
364
+ this.pendingRequests.clear();
365
+
366
+ for (const [id, meta] of this.pendingClientResponses.entries()) {
367
+ this.options.send(meta.ws, {
368
+ channel: "rpc",
369
+ payload: {
370
+ type: "response",
371
+ id,
372
+ command: meta.responseCommand || "unknown",
373
+ success: false,
374
+ error: error.message,
375
+ },
376
+ });
377
+ }
378
+ this.pendingClientResponses.clear();
379
+ }
380
+
381
+ private handleStdoutChunk(chunk: Buffer | string) {
382
+ this.stdoutBuffer += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
383
+
384
+ while (true) {
385
+ const newlineIndex = this.stdoutBuffer.indexOf("\n");
386
+ if (newlineIndex === -1) break;
387
+
388
+ let line = this.stdoutBuffer.slice(0, newlineIndex);
389
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
390
+ if (line.endsWith("\r")) line = line.slice(0, -1);
391
+ if (!line.length) continue;
392
+ this.handleRpcLine(line);
393
+ }
394
+ }
395
+
396
+ private scheduleSnapshotRefresh(delayMs = 80) {
397
+ if (this.snapshotRefreshTimer || this.disposed) return;
398
+
399
+ this.snapshotRefreshTimer = setTimeout(() => {
400
+ this.snapshotRefreshTimer = null;
401
+ this.refreshCachedSnapshot().catch(() => {});
402
+ }, delayMs);
403
+ }
404
+
405
+ private handleRpcLine(line: string) {
406
+ let payload: any;
407
+ try {
408
+ payload = JSON.parse(line);
409
+ } catch (error) {
410
+ this.lastError = `Failed to parse child rpc output: ${line.slice(0, 200)}`;
411
+ this.options.onEnvelope(this, { channel: "server", event: "parse-error", data: { line, error: String(error) } });
412
+ this.touch();
413
+ return;
414
+ }
415
+
416
+ this.touch();
417
+
418
+ if (payload.type === "response" && typeof payload.id === "string") {
419
+ if (payload.success && payload.command === "get_state") {
420
+ this.rememberState(payload.data);
421
+ }
422
+
423
+ if (payload.success && payload.command === "get_messages") {
424
+ this.setMessages(payload.data?.messages || []);
425
+ }
426
+
427
+ if (payload.success && payload.command === "get_commands") {
428
+ this.lastCommands = payload.data?.commands || [];
429
+ this.options.onStateChange();
430
+ }
431
+
432
+ const pending = this.pendingRequests.get(payload.id);
433
+ if (pending) {
434
+ clearTimeout(pending.timer);
435
+ this.pendingRequests.delete(payload.id);
436
+ pending.resolve(payload);
437
+ }
438
+
439
+ const clientMeta = this.pendingClientResponses.get(payload.id);
440
+ if (clientMeta) {
441
+ this.pendingClientResponses.delete(payload.id);
442
+
443
+ try {
444
+ if (payload.success) clientMeta.onSuccess?.(payload);
445
+ else clientMeta.onError?.(payload);
446
+ } catch {
447
+ // Ignore local response side effects and still forward the payload.
448
+ }
449
+
450
+ const normalizedPayload = payload.success && payload.command === "get_state"
451
+ ? { ...payload, data: this.lastState || payload.data }
452
+ : payload;
453
+
454
+ const nextPayload = {
455
+ ...normalizedPayload,
456
+ ...(clientMeta.responseCommand ? { command: clientMeta.responseCommand } : {}),
457
+ ...(normalizedPayload.success && clientMeta.responseData
458
+ ? { data: { ...(normalizedPayload.data || {}), ...clientMeta.responseData } }
459
+ : {}),
460
+ };
461
+ this.options.send(clientMeta.ws, { channel: "rpc", payload: nextPayload });
462
+ }
463
+
464
+ if (payload.success && !payload.data?.cancelled && ["new_session", "switch_session", "set_session_name", "reload"].includes(payload.command)) {
465
+ this.pendingUiRequest = null;
466
+ this.liveAssistantMessage = null;
467
+ this.liveTools.clear();
468
+ this.scheduleSnapshotRefresh(40);
469
+ }
470
+
471
+ return;
472
+ }
473
+
474
+ if (payload.type === "agent_start") {
475
+ this.isStreaming = true;
476
+ this.lastState = this.lastState ? { ...this.lastState, isStreaming: true } : this.lastState;
477
+ this.options.onStateChange();
478
+ }
479
+
480
+ if (payload.type === "agent_end") {
481
+ this.isStreaming = false;
482
+ this.lastState = this.lastState ? { ...this.lastState, isStreaming: false } : this.lastState;
483
+ this.liveAssistantMessage = null;
484
+ this.liveTools.clear();
485
+ this.options.onStateChange();
486
+ this.scheduleSnapshotRefresh(30);
487
+ }
488
+
489
+ if (payload.type === "message_start" && payload.message?.role === "assistant") {
490
+ this.liveAssistantMessage = payload.message;
491
+ this.options.onStateChange();
492
+ }
493
+
494
+ if (payload.type === "message_update" && payload.message?.role === "assistant") {
495
+ this.liveAssistantMessage = payload.message;
496
+ this.options.onStateChange();
497
+ }
498
+
499
+ if (payload.type === "message_end" && payload.message?.role === "assistant") {
500
+ this.liveAssistantMessage = null;
501
+ this.options.onStateChange();
502
+ }
503
+
504
+ if (payload.type === "tool_execution_start") {
505
+ this.liveTools.set(payload.toolCallId, {
506
+ toolCallId: payload.toolCallId,
507
+ toolName: payload.toolName || "tool",
508
+ args: payload.args || {},
509
+ partialResult: null,
510
+ result: null,
511
+ isError: false,
512
+ });
513
+ this.options.onStateChange();
514
+ }
515
+
516
+ if (payload.type === "tool_execution_update") {
517
+ const current = this.liveTools.get(payload.toolCallId) || {};
518
+ this.liveTools.set(payload.toolCallId, {
519
+ ...current,
520
+ toolCallId: payload.toolCallId,
521
+ toolName: payload.toolName || current.toolName || "tool",
522
+ args: payload.args || current.args || {},
523
+ partialResult: payload.partialResult || current.partialResult || null,
524
+ result: current.result || null,
525
+ isError: current.isError || false,
526
+ });
527
+ this.options.onStateChange();
528
+ }
529
+
530
+ if (payload.type === "tool_execution_end") {
531
+ const current = this.liveTools.get(payload.toolCallId) || {};
532
+ this.liveTools.set(payload.toolCallId, {
533
+ ...current,
534
+ toolCallId: payload.toolCallId,
535
+ toolName: payload.toolName || current.toolName || "tool",
536
+ args: payload.args || current.args || {},
537
+ partialResult: current.partialResult || null,
538
+ result: payload.result || null,
539
+ isError: Boolean(payload.isError),
540
+ });
541
+ this.options.onStateChange();
542
+ }
543
+
544
+ if (payload.type === "extension_ui_request" && ["select", "confirm", "input", "editor"].includes(payload.method)) {
545
+ this.pendingUiRequest = payload;
546
+ this.options.onStateChange();
547
+ }
548
+
549
+ this.options.onEnvelope(this, { channel: "rpc", payload });
550
+ }
551
+
552
+ async request(command: Record<string, unknown>, timeoutMs = 30000) {
553
+ await this.ensureStarted();
554
+ if (!this.child) throw new Error("dm rpc child is not running");
555
+
556
+ const id = `srv-${++this.requestCounter}`;
557
+ const payload = { ...command, id };
558
+
559
+ return new Promise<any>((resolvePromise, rejectPromise) => {
560
+ const timer = setTimeout(() => {
561
+ this.pendingRequests.delete(id);
562
+ rejectPromise(new Error(`Timed out waiting for child response to ${String(command.type)}`));
563
+ }, timeoutMs);
564
+
565
+ this.pendingRequests.set(id, {
566
+ resolve: resolvePromise,
567
+ reject: rejectPromise,
568
+ timer,
569
+ });
570
+
571
+ this.child!.stdin.write(`${JSON.stringify(payload)}\n`);
572
+ });
573
+ }
574
+
575
+ async refreshCachedSnapshot(timeoutMs = 4500): Promise<SessionSnapshot> {
576
+ await this.ensureStarted();
577
+
578
+ const [stateResponse, messagesResponse, commandsResponse] = await Promise.all([
579
+ this.request({ type: "get_state" }, timeoutMs),
580
+ this.request({ type: "get_messages" }, timeoutMs),
581
+ this.request({ type: "get_commands" }, timeoutMs),
582
+ ]);
583
+
584
+ if (stateResponse?.success) {
585
+ this.rememberState(stateResponse.data || null);
586
+ }
587
+
588
+ if (messagesResponse?.success) {
589
+ this.setMessages(messagesResponse.data?.messages || []);
590
+ }
591
+
592
+ if (commandsResponse?.success) {
593
+ this.lastCommands = commandsResponse.data?.commands || [];
594
+ this.options.onStateChange();
595
+ }
596
+
597
+ return this.cachedSnapshot();
598
+ }
599
+
600
+ async getSnapshot(): Promise<SessionSnapshot> {
601
+ const hasCache = Boolean(this.lastState) || this.lastMessages.length > 0 || this.lastCommands.length > 0;
602
+
603
+ if (this.isStreaming || this.pendingUiRequest) {
604
+ if (hasCache) {
605
+ return this.cachedSnapshot();
606
+ }
607
+
608
+ try {
609
+ return await this.refreshCachedSnapshot(2500);
610
+ } catch {
611
+ return this.cachedSnapshot();
612
+ }
613
+ }
614
+
615
+ try {
616
+ return await this.refreshCachedSnapshot(4500);
617
+ } catch (error) {
618
+ if (hasCache || this.pendingUiRequest) {
619
+ return this.cachedSnapshot();
620
+ }
621
+ throw error;
622
+ }
623
+ }
624
+
625
+ async sendClientCommand(command: Record<string, unknown>, meta?: PendingClientResponse) {
626
+ await this.ensureStarted();
627
+ if (!this.child) throw new Error("dm rpc child is not running");
628
+
629
+ const nextCommand = { ...command } as Record<string, any>;
630
+
631
+ if (nextCommand.type === "extension_ui_response") {
632
+ this.pendingUiRequest = null;
633
+ this.options.onStateChange();
634
+ } else if (!nextCommand.id) {
635
+ nextCommand.id = `cli-${++this.requestCounter}`;
636
+ }
637
+
638
+ if (nextCommand.type !== "extension_ui_response" && nextCommand.id && meta?.ws) {
639
+ this.pendingClientResponses.set(String(nextCommand.id), meta);
640
+ }
641
+
642
+ this.touch();
643
+ this.child.stdin.write(`${JSON.stringify(nextCommand)}\n`);
644
+ return nextCommand.id as string | undefined;
645
+ }
646
+
647
+ private async stopChildForRestart() {
648
+ const runningChild = this.child;
649
+ if (!runningChild) return;
650
+
651
+ await new Promise<void>((resolvePromise) => {
652
+ let settled = false;
653
+ const finish = () => {
654
+ if (settled) return;
655
+ settled = true;
656
+ clearTimeout(forceKillTimer);
657
+ resolvePromise();
658
+ };
659
+
660
+ const forceKillTimer = setTimeout(() => {
661
+ try {
662
+ runningChild.kill("SIGKILL");
663
+ } catch {
664
+ finish();
665
+ }
666
+ }, 2000);
667
+
668
+ runningChild.once("exit", finish);
669
+
670
+ try {
671
+ runningChild.kill("SIGTERM");
672
+ } catch {
673
+ finish();
674
+ }
675
+ });
676
+ }
677
+
678
+ async reload() {
679
+ if (this.reloadPromise) return this.reloadPromise;
680
+
681
+ this.reloadPromise = (async () => {
682
+ await this.ensureStarted();
683
+
684
+ const stateResponse = await this.request({ type: "get_state" });
685
+ if (!stateResponse?.success) {
686
+ throw new Error(stateResponse?.error || "Failed to read DM state before reload.");
687
+ }
688
+
689
+ const nextState = stateResponse.data || {};
690
+ this.rememberState(nextState);
691
+
692
+ if (nextState.isStreaming) {
693
+ throw new Error("Wait for the current response to finish before reloading.");
694
+ }
695
+
696
+ if (nextState.isCompacting) {
697
+ throw new Error("Wait for compaction to finish before reloading.");
698
+ }
699
+
700
+ this.isRestarting = true;
701
+ this.options.onEnvelope(this, {
702
+ channel: "server",
703
+ event: "reloading",
704
+ data: { message: "Reloading extensions, skills, prompts, and themes…" },
705
+ });
706
+
707
+ await this.stopChildForRestart();
708
+ await this.ensureStarted({ sessionFile: this.currentSessionFile });
709
+ await this.refreshCachedSnapshot(5000);
710
+ })().finally(() => {
711
+ this.isRestarting = false;
712
+ this.options.onEnvelope(this, { channel: "server", event: "reloading", data: { message: "" } });
713
+ this.reloadPromise = null;
714
+ });
715
+
716
+ return this.reloadPromise;
717
+ }
718
+
719
+ async dispose() {
720
+ this.disposed = true;
721
+ if (this.snapshotRefreshTimer) {
722
+ clearTimeout(this.snapshotRefreshTimer);
723
+ this.snapshotRefreshTimer = null;
724
+ }
725
+ this.rejectAllPending(new Error("dm phone session stopped"));
726
+ await this.stopChildForRestart();
727
+ this.child = null;
728
+ this.isStreaming = false;
729
+ this.pendingUiRequest = null;
730
+ this.liveAssistantMessage = null;
731
+ this.liveTools.clear();
732
+ this.options.onStateChange();
733
+ }
734
+ }