@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,131 @@
1
+ export function escapeHtml(text = "") {
2
+ return String(text)
3
+ .replaceAll("&", "&")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#039;");
8
+ }
9
+
10
+ export function escapeAttribute(text = "") {
11
+ return escapeHtml(text).replaceAll("`", "&#096;");
12
+ }
13
+
14
+ export function formatCwdDisplay(path = "") {
15
+ const value = String(path || "").trim();
16
+ if (!value) return "";
17
+
18
+ const homeMatch = value.match(/^\/(?:home|Users)\/[^/]+(\/.*)?$/);
19
+ if (homeMatch) {
20
+ const suffix = homeMatch[1] || "";
21
+ const parts = suffix.split("/").filter(Boolean);
22
+ if (!parts.length) return "~";
23
+ if (parts.length === 1) return `~/${parts[0]}`;
24
+ return `~/…/${parts[parts.length - 1]}`;
25
+ }
26
+
27
+ if (value === "/") return value;
28
+
29
+ const parts = value.split("/").filter(Boolean);
30
+ if (parts.length <= 2) return value;
31
+ return `/…/${parts[parts.length - 1]}`;
32
+ }
33
+
34
+ export function formatTimestamp(timestamp) {
35
+ if (!timestamp) return "";
36
+ const date = new Date(timestamp);
37
+ return date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
38
+ }
39
+
40
+ export function formatDateTime(value) {
41
+ if (!value) return "";
42
+ const date = new Date(value);
43
+ if (Number.isNaN(date.getTime())) return "";
44
+ return date.toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
45
+ }
46
+
47
+ export function formatBytes(bytes) {
48
+ if (!Number.isFinite(bytes)) return "";
49
+ if (bytes < 1024) return `${bytes} B`;
50
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
51
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
52
+ }
53
+
54
+ export function formatTokenCount(count) {
55
+ if (!Number.isFinite(count) || count <= 0) return "";
56
+ if (count < 1000) return String(Math.round(count));
57
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
58
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
59
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
60
+ return `${Math.round(count / 1000000)}M`;
61
+ }
62
+
63
+ export function normalizeNewlines(text = "") {
64
+ return String(text ?? "").replace(/\r\n?/g, "\n");
65
+ }
66
+
67
+ export function stripTerminalControlSequences(text = "") {
68
+ return String(text ?? "")
69
+ .replace(/\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "")
70
+ .replace(/\u009D[^\u009C]*\u009C/g, "")
71
+ .replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "")
72
+ .replace(/\u009B[0-?]*[ -/]*[@-~]/g, "")
73
+ .replace(/\u001B[@-_]/g, "")
74
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, "");
75
+ }
76
+
77
+ export function countTextLines(text = "") {
78
+ const normalized = normalizeNewlines(text);
79
+ if (!normalized.length) return 0;
80
+ return normalized.split("\n").length;
81
+ }
82
+
83
+ export function asRecord(value) {
84
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
85
+ return value;
86
+ }
87
+
88
+ export function contentToText(content) {
89
+ if (typeof content === "string") return stripTerminalControlSequences(content);
90
+ if (!Array.isArray(content)) return "";
91
+ return stripTerminalControlSequences(content
92
+ .map((part) => {
93
+ if (part.type === "text") return part.text || "";
94
+ if (part.type === "image") return "[image]";
95
+ if (part.type === "thinking") return "";
96
+ if (part.type === "toolCall") return "";
97
+ return "";
98
+ })
99
+ .join(" ")
100
+ .trim());
101
+ }
102
+
103
+ export function countImages(content) {
104
+ if (!Array.isArray(content)) return 0;
105
+ return content.filter((part) => part.type === "image").length;
106
+ }
107
+
108
+ export function assistantParts(content) {
109
+ const parts = { text: "", thinking: "", toolCalls: [] };
110
+ if (!Array.isArray(content)) return parts;
111
+
112
+ for (const block of content) {
113
+ if (block.type === "text") parts.text += stripTerminalControlSequences(block.text || "");
114
+ if (block.type === "thinking") parts.thinking += stripTerminalControlSequences(block.thinking || "");
115
+ if (block.type === "toolCall") {
116
+ parts.toolCalls.push({ id: block.id || "", name: block.name || "tool", arguments: block.arguments || {} });
117
+ }
118
+ }
119
+
120
+ return parts;
121
+ }
122
+
123
+ export function toDetailString(details) {
124
+ if (details == null) return "";
125
+ if (typeof details === "string") return details;
126
+ try {
127
+ return JSON.stringify(details, null, 2);
128
+ } catch {
129
+ return String(details);
130
+ }
131
+ }
@@ -0,0 +1,442 @@
1
+ import { contentToText } from "./formatters.js";
2
+ import {
3
+ clearSnapshotView,
4
+ clearTransientState,
5
+ handleAssistantEvent,
6
+ renderMessages,
7
+ renderWidgets,
8
+ transformMessage,
9
+ upsertLiveTool,
10
+ } from "./messages.js";
11
+ import { closeSheet } from "./sheet-navigation.js";
12
+ import { renderSheet } from "./sheets-view.js";
13
+ import { state } from "./state.js";
14
+ import { refreshAll, refreshQuota, sendRpc } from "./transport.js";
15
+ import {
16
+ autoResizeTextarea,
17
+ clearUiModal,
18
+ openTokenModal,
19
+ openUiModalForRequest,
20
+ renderHeader,
21
+ resetToken,
22
+ showBanner,
23
+ showToast,
24
+ } from "./ui.js";
25
+ import { renderCommandSuggestions } from "./autocomplete-controller.js";
26
+ import { renderAutocompleteItems } from "./autocomplete.js";
27
+
28
+ export function handleAuthFailure() {
29
+ resetToken({ clearInput: true });
30
+ state.socket = null;
31
+ renderHeader();
32
+ openTokenModal();
33
+ showBanner("Access token required. Enter the current /phone-start token.", "error");
34
+ }
35
+
36
+ function sendUiResponse(payload) {
37
+ sendRpc({ type: "extension_ui_response", ...payload });
38
+ clearUiModal();
39
+ }
40
+
41
+ function handleExtensionUiRequest(request) {
42
+ if (request.method === "notify") {
43
+ showToast(request.message || "Notification");
44
+ return;
45
+ }
46
+
47
+ if (request.method === "setStatus") {
48
+ state.footerStatus = request.statusText || "";
49
+ renderWidgets();
50
+ return;
51
+ }
52
+
53
+ if (request.method === "setWidget") {
54
+ if (request.widgetLines?.length) state.widgets.set(request.widgetKey || "widget", request.widgetLines);
55
+ else state.widgets.delete(request.widgetKey || "widget");
56
+ renderWidgets();
57
+ return;
58
+ }
59
+
60
+ if (request.method === "setTitle") {
61
+ document.title = request.title || "DM Phone";
62
+ return;
63
+ }
64
+
65
+ if (request.method === "set_editor_text") {
66
+ const promptInput = document.querySelector("#prompt-input");
67
+ if (promptInput) promptInput.value = request.text || "";
68
+ autoResizeTextarea();
69
+ renderCommandSuggestions();
70
+ return;
71
+ }
72
+
73
+ if (!["select", "confirm", "input", "editor"].includes(request.method)) {
74
+ showToast(`Unsupported UI request: ${request.method || "unknown"}`);
75
+ return;
76
+ }
77
+
78
+ openUiModalForRequest(request, sendUiResponse);
79
+ }
80
+
81
+ function handleRpcPayload(payload) {
82
+ if (!payload) return;
83
+
84
+ if (payload.type === "response") {
85
+ if (!payload.success) {
86
+ if (payload.command === "path_suggestions") {
87
+ renderAutocompleteItems([]);
88
+ return;
89
+ }
90
+ showToast(payload.error || `Command failed: ${payload.command}`, "error");
91
+ return;
92
+ }
93
+
94
+ if (payload.command === "get_state") {
95
+ state.snapshotState = payload.data || state.snapshotState;
96
+ renderHeader();
97
+ void refreshQuota();
98
+ return;
99
+ }
100
+
101
+ if (payload.command === "get_messages") {
102
+ state.messages = (payload.data?.messages || []).flatMap(transformMessage);
103
+ clearTransientState();
104
+ renderMessages();
105
+ return;
106
+ }
107
+
108
+ if (payload.command === "get_commands") {
109
+ state.commands = payload.data?.commands || [];
110
+ renderCommandSuggestions();
111
+ renderSheet();
112
+ return;
113
+ }
114
+
115
+ if (payload.command === "path_suggestions") {
116
+ const context = state.autocompleteContext;
117
+ if (!context || context.type !== "path") return;
118
+ if (Number(payload.data?.requestId) !== state.autocompleteRemoteRequestId) return;
119
+ if (payload.data?.mode !== context.mode) return;
120
+ if ((payload.data?.query || "") !== context.query) return;
121
+
122
+ const suggestions = Array.isArray(payload.data?.suggestions) ? payload.data.suggestions : [];
123
+ renderAutocompleteItems(suggestions.map((suggestion) => ({
124
+ kind: "path",
125
+ label: context.mode === "mention" ? `@${suggestion.value}` : suggestion.value,
126
+ badge: suggestion.kind === "previous" ? "recent" : suggestion.isDirectory ? "dir" : "file",
127
+ description: suggestion.description || suggestion.value,
128
+ value: suggestion.value,
129
+ isDirectory: Boolean(suggestion.isDirectory),
130
+ title: suggestion.description || suggestion.value,
131
+ })));
132
+ return;
133
+ }
134
+
135
+ if (payload.command === "cd") {
136
+ showToast(`Changed directory to ${payload.data?.cwd || "the selected path"}.`);
137
+ refreshAll();
138
+ return;
139
+ }
140
+
141
+ if (payload.command === "get_available_models") {
142
+ state.models = payload.data?.models || [];
143
+ renderSheet();
144
+ return;
145
+ }
146
+
147
+ if (payload.command === "get_session_stats") {
148
+ state.stats = payload.data || null;
149
+ renderSheet();
150
+ return;
151
+ }
152
+
153
+ if (payload.command === "phone_list_sessions") {
154
+ state.sessions = payload.data?.sessions || [];
155
+ renderSheet();
156
+ return;
157
+ }
158
+
159
+ if (payload.command === "phone_get_tree") {
160
+ state.tree = payload.data || null;
161
+ renderSheet();
162
+ return;
163
+ }
164
+
165
+ if (payload.command === "new_parent_session") {
166
+ clearTransientState();
167
+ refreshAll();
168
+ showToast("Started a new parent session.");
169
+ return;
170
+ }
171
+
172
+ if (payload.command === "new_session") {
173
+ clearTransientState();
174
+ refreshAll();
175
+ showToast("Started a new DM session.");
176
+ return;
177
+ }
178
+
179
+ if (payload.command === "compact") {
180
+ showToast("Compaction triggered.");
181
+ refreshAll();
182
+ return;
183
+ }
184
+
185
+ if (payload.command === "slash_command") {
186
+ if (payload.data?.source === "extension") {
187
+ refreshAll({ forceQuota: true });
188
+ }
189
+ return;
190
+ }
191
+
192
+ if (payload.command === "reload") {
193
+ clearTransientState();
194
+ showToast("Reloaded extensions, skills, prompts, and themes.");
195
+ refreshAll({ forceQuota: true });
196
+ return;
197
+ }
198
+
199
+ if (payload.command === "set_model") {
200
+ showToast("Model updated.");
201
+ refreshAll();
202
+ return;
203
+ }
204
+
205
+ if (payload.command === "set_thinking_level") {
206
+ showToast("Thinking level updated.");
207
+ refreshAll();
208
+ return;
209
+ }
210
+
211
+ if (payload.command === "switch_session") {
212
+ showToast("Session switched.");
213
+ refreshAll();
214
+ closeSheet();
215
+ return;
216
+ }
217
+
218
+ if (payload.command === "fork") {
219
+ showToast("Fork created.");
220
+ refreshAll();
221
+ closeSheet();
222
+ return;
223
+ }
224
+
225
+ if (payload.command === "phone_open_branch_path") {
226
+ showToast("Opened selected branch path as a new session.");
227
+ refreshAll();
228
+ closeSheet();
229
+ return;
230
+ }
231
+
232
+ return;
233
+ }
234
+
235
+ if (payload.type === "agent_start") {
236
+ state.status = { ...(state.status || {}), isStreaming: true };
237
+ renderHeader();
238
+ return;
239
+ }
240
+
241
+ if (payload.type === "agent_end") {
242
+ state.status = { ...(state.status || {}), isStreaming: false };
243
+ renderHeader();
244
+ refreshAll({ forceQuota: true });
245
+ return;
246
+ }
247
+
248
+ if (payload.type === "message_update") {
249
+ handleAssistantEvent(payload.assistantMessageEvent);
250
+ return;
251
+ }
252
+
253
+ if (payload.type === "message_end" && payload.message?.role === "assistant") {
254
+ const transformed = transformMessage(payload.message, Date.now())[0];
255
+ if (transformed) {
256
+ state.liveAssistant = { ...transformed, live: false };
257
+ renderMessages();
258
+ }
259
+ return;
260
+ }
261
+
262
+ if (payload.type === "tool_execution_start") {
263
+ upsertLiveTool(payload.toolCallId, {
264
+ id: `tool-live-${payload.toolCallId}`,
265
+ kind: "tool",
266
+ toolCallId: payload.toolCallId,
267
+ toolName: payload.toolName || "tool",
268
+ args: payload.args || {},
269
+ command: payload.args?.command || "",
270
+ live: true,
271
+ title: payload.toolName || "tool",
272
+ text: JSON.stringify(payload.args || {}, null, 2),
273
+ meta: "Running…",
274
+ status: "running",
275
+ rawContent: null,
276
+ });
277
+ return;
278
+ }
279
+
280
+ if (payload.type === "tool_execution_update") {
281
+ upsertLiveTool(payload.toolCallId, {
282
+ ...(state.liveTools.get(payload.toolCallId) || {}),
283
+ id: `tool-live-${payload.toolCallId}`,
284
+ kind: "tool",
285
+ toolCallId: payload.toolCallId,
286
+ toolName: payload.toolName || "tool",
287
+ args: payload.args || state.liveTools.get(payload.toolCallId)?.args || {},
288
+ command: payload.args?.command || state.liveTools.get(payload.toolCallId)?.command || "",
289
+ live: true,
290
+ title: payload.toolName || "tool",
291
+ text: contentToText(payload.partialResult?.content) || JSON.stringify(payload.args || {}, null, 2),
292
+ meta: "Running…",
293
+ status: "running",
294
+ details: payload.partialResult?.details,
295
+ rawContent: payload.partialResult?.content || state.liveTools.get(payload.toolCallId)?.rawContent || null,
296
+ });
297
+ return;
298
+ }
299
+
300
+ if (payload.type === "tool_execution_end") {
301
+ upsertLiveTool(payload.toolCallId, {
302
+ ...(state.liveTools.get(payload.toolCallId) || {}),
303
+ id: `tool-live-${payload.toolCallId}`,
304
+ kind: "tool",
305
+ toolCallId: payload.toolCallId,
306
+ toolName: payload.toolName || "tool",
307
+ args: payload.args || state.liveTools.get(payload.toolCallId)?.args || {},
308
+ command: payload.args?.command || state.liveTools.get(payload.toolCallId)?.command || "",
309
+ live: false,
310
+ title: payload.toolName || "tool",
311
+ text: contentToText(payload.result?.content),
312
+ meta: payload.isError ? "Failed" : "Done",
313
+ status: payload.isError ? "error" : "done",
314
+ details: payload.result?.details,
315
+ rawContent: payload.result?.content || state.liveTools.get(payload.toolCallId)?.rawContent || null,
316
+ });
317
+ return;
318
+ }
319
+
320
+ if (payload.type === "extension_ui_request") {
321
+ handleExtensionUiRequest(payload);
322
+ return;
323
+ }
324
+
325
+ if (payload.type === "auto_retry_start") {
326
+ showBanner(`Retrying after error: ${payload.errorMessage || "temporary failure"}`);
327
+ return;
328
+ }
329
+
330
+ if (payload.type === "auto_retry_end") {
331
+ showBanner(payload.success ? "" : `Retry failed: ${payload.finalError || "Unknown error"}`, payload.success ? "info" : "error");
332
+ }
333
+ }
334
+
335
+ export async function handleEnvelope(event) {
336
+ if (event.channel === "sessions" && event.event === "catalog") {
337
+ const nextActiveSessionId = event.data?.activeSessionId || state.activeSessionId;
338
+ const activeSessionChanged = nextActiveSessionId !== state.activeSessionId;
339
+
340
+ state.activeSessions = event.data?.sessions || [];
341
+ state.activeSessionId = nextActiveSessionId;
342
+
343
+ if (activeSessionChanged && state.snapshotWorkerId && state.snapshotWorkerId !== state.activeSessionId) {
344
+ clearSnapshotView();
345
+ renderMessages();
346
+ }
347
+
348
+ renderHeader();
349
+ renderSheet();
350
+ return;
351
+ }
352
+
353
+ if (event.channel === "snapshot") {
354
+ if (event.sessionWorkerId && state.activeSessionId && event.sessionWorkerId !== state.activeSessionId) {
355
+ return;
356
+ }
357
+
358
+ state.snapshotState = event.state || null;
359
+ state.snapshotWorkerId = event.sessionWorkerId || state.activeSessionId || null;
360
+ state.status = { ...(state.status || {}), isStreaming: Boolean(event.state?.isStreaming) };
361
+ state.messages = (event.messages || []).flatMap(transformMessage);
362
+ state.commands = event.commands || state.commands;
363
+ clearTransientState();
364
+
365
+ if (event.liveAssistantMessage?.role === "assistant") {
366
+ const assistant = transformMessage(event.liveAssistantMessage, Date.now())[0];
367
+ if (assistant) {
368
+ state.liveAssistant = { ...assistant, id: "assistant-live", live: true };
369
+ }
370
+ }
371
+
372
+ for (const tool of event.liveTools || []) {
373
+ const text =
374
+ contentToText(tool.result?.content)
375
+ || contentToText(tool.partialResult?.content)
376
+ || JSON.stringify(tool.args || {}, null, 2);
377
+
378
+ state.liveTools.set(tool.toolCallId, {
379
+ id: `tool-live-${tool.toolCallId}`,
380
+ kind: "tool",
381
+ toolCallId: tool.toolCallId,
382
+ toolName: tool.toolName || "tool",
383
+ args: tool.args || {},
384
+ command: tool.args?.command || "",
385
+ live: !tool.result,
386
+ title: tool.toolName || "tool",
387
+ text,
388
+ meta: tool.result ? (tool.isError ? "Failed" : "Done") : "Running…",
389
+ details: tool.result?.details || tool.partialResult?.details,
390
+ status: tool.isError ? "error" : (tool.result ? "done" : "running"),
391
+ rawContent: tool.result?.content || tool.partialResult?.content || null,
392
+ });
393
+ }
394
+
395
+ renderHeader();
396
+ renderMessages();
397
+ renderSheet();
398
+ renderCommandSuggestions();
399
+ void refreshQuota();
400
+ return;
401
+ }
402
+
403
+ if (event.channel === "server") {
404
+ if (event.event === "status") {
405
+ state.status = event.data;
406
+ renderHeader();
407
+ return;
408
+ }
409
+ if (event.event === "stderr") {
410
+ showBanner(event.data?.text?.trim() || "", "error");
411
+ return;
412
+ }
413
+ if (event.event === "reloading") {
414
+ showBanner(event.data?.message || "");
415
+ return;
416
+ }
417
+ if (event.event === "session-spawned") {
418
+ showToast(event.data?.message || "Opened new parallel session.");
419
+ return;
420
+ }
421
+ if (event.event === "single-client-replaced") {
422
+ showBanner(event.data?.message || "This phone session was replaced by another client.", "error");
423
+ return;
424
+ }
425
+ if (event.event === "idle-timeout") {
426
+ showBanner(event.data?.message || "DM Phone stopped because it was idle.", "error");
427
+ return;
428
+ }
429
+ if (["startup-error", "snapshot-error", "client-error"].includes(event.event)) {
430
+ showToast(event.data?.message || "Server error", "error");
431
+ return;
432
+ }
433
+ if (event.event === "agent-exit") {
434
+ showBanner(event.data?.message || "DM rpc exited.", "error");
435
+ return;
436
+ }
437
+ }
438
+
439
+ if (event.channel === "rpc") {
440
+ handleRpcPayload(event.payload);
441
+ }
442
+ }
@@ -0,0 +1,6 @@
1
+ import { initializeBindings } from "./bindings.js";
2
+ import { handleAuthFailure, handleEnvelope } from "./handlers.js";
3
+ import { boot } from "./transport.js";
4
+
5
+ initializeBindings({ handleEnvelope, handleAuthFailure });
6
+ boot({ handleEnvelope, handleAuthFailure });
@@ -0,0 +1,105 @@
1
+ import { escapeHtml } from "./formatters.js";
2
+
3
+ function findInlineCodeMarker(text, startIndex = 0) {
4
+ for (let index = Math.max(0, startIndex); index < text.length; index += 1) {
5
+ if (text[index] !== "`") continue;
6
+ if (text[index - 1] === "`" || text[index + 1] === "`") continue;
7
+ return index;
8
+ }
9
+ return -1;
10
+ }
11
+
12
+ function renderStrongText(text = "") {
13
+ let html = "";
14
+ let cursor = 0;
15
+
16
+ while (cursor < text.length) {
17
+ const open = text.indexOf("**", cursor);
18
+ if (open === -1) {
19
+ html += escapeHtml(text.slice(cursor));
20
+ break;
21
+ }
22
+
23
+ const close = text.indexOf("**", open + 2);
24
+ if (close === -1) {
25
+ html += escapeHtml(text.slice(cursor));
26
+ break;
27
+ }
28
+
29
+ html += escapeHtml(text.slice(cursor, open));
30
+ html += `<strong>${escapeHtml(text.slice(open + 2, close))}</strong>`;
31
+ cursor = close + 2;
32
+ }
33
+
34
+ return html;
35
+ }
36
+
37
+ function renderInlineMarkdown(text = "") {
38
+ let html = "";
39
+ let cursor = 0;
40
+
41
+ while (cursor < text.length) {
42
+ const open = findInlineCodeMarker(text, cursor);
43
+ if (open === -1) {
44
+ html += renderStrongText(text.slice(cursor));
45
+ break;
46
+ }
47
+
48
+ const close = findInlineCodeMarker(text, open + 1);
49
+ if (close === -1) {
50
+ html += renderStrongText(text.slice(cursor));
51
+ break;
52
+ }
53
+
54
+ html += renderStrongText(text.slice(cursor, open));
55
+ html += `<code>${escapeHtml(text.slice(open + 1, close))}</code>`;
56
+ cursor = close + 1;
57
+ }
58
+
59
+ return html;
60
+ }
61
+
62
+ function renderTextBlocks(text = "") {
63
+ const blocks = text
64
+ .split(/\n{2,}/)
65
+ .map((block) => (block.trim() ? `<p>${renderInlineMarkdown(block)}</p>` : ""))
66
+ .filter(Boolean);
67
+
68
+ if (blocks.length) return blocks.join("");
69
+ return text.trim() ? `<p>${renderInlineMarkdown(text)}</p>` : "";
70
+ }
71
+
72
+ function renderCodeBlock(code = "") {
73
+ return `
74
+ <pre class="message-code-block"><code>${escapeHtml(code)}</code></pre>
75
+ `;
76
+ }
77
+
78
+ export function renderMarkdownLite(text = "") {
79
+ const normalized = String(text || "").replace(/\r\n?/g, "\n");
80
+ const fencePattern = /```([^`\n]*)\n([\s\S]*?)```/g;
81
+ const parts = [];
82
+ let cursor = 0;
83
+ let match;
84
+
85
+ while ((match = fencePattern.exec(normalized))) {
86
+ if (match.index > cursor) {
87
+ parts.push({ type: "text", value: normalized.slice(cursor, match.index) });
88
+ }
89
+
90
+ parts.push({ type: "code", value: match[2].replace(/\n$/, "") });
91
+ cursor = match.index + match[0].length;
92
+ }
93
+
94
+ if (cursor < normalized.length) {
95
+ parts.push({ type: "text", value: normalized.slice(cursor) });
96
+ }
97
+
98
+ const html = parts.map((part) => (
99
+ part.type === "code"
100
+ ? renderCodeBlock(part.value)
101
+ : renderTextBlocks(part.value)
102
+ )).join("");
103
+
104
+ return html || '<p><span class="label">(no text)</span></p>';
105
+ }