@duckmind/dm-darwin-x64 0.32.9 → 0.33.1

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