@duckmind/dm-darwin-x64 0.13.5 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +39 -15
  3. package/extensions/dm-multicodex/package-lock.json +302 -1814
  4. package/extensions/dm-phone/README.md +23 -0
  5. package/extensions/dm-phone/index.ts +12 -0
  6. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  7. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  8. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  9. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  10. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  23. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  24. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  25. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  26. package/extensions/dm-phone/package-lock.json +66 -0
  27. package/extensions/dm-phone/package.json +35 -0
  28. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  29. package/extensions/dm-phone/public/app/attachments.js +233 -0
  30. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  31. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  32. package/extensions/dm-phone/public/app/bindings.js +178 -0
  33. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  34. package/extensions/dm-phone/public/app/commands.js +370 -0
  35. package/extensions/dm-phone/public/app/constants.js +60 -0
  36. package/extensions/dm-phone/public/app/formatters.js +131 -0
  37. package/extensions/dm-phone/public/app/handlers.js +442 -0
  38. package/extensions/dm-phone/public/app/main.js +6 -0
  39. package/extensions/dm-phone/public/app/markdown.js +105 -0
  40. package/extensions/dm-phone/public/app/messages.js +418 -0
  41. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  42. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  43. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  44. package/extensions/dm-phone/public/app/state.js +95 -0
  45. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  46. package/extensions/dm-phone/public/app/transport.js +176 -0
  47. package/extensions/dm-phone/public/app/ui.js +409 -0
  48. package/extensions/dm-phone/public/app.js +1 -0
  49. package/extensions/dm-phone/public/icon.svg +15 -0
  50. package/extensions/dm-phone/public/index.html +147 -0
  51. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  52. package/extensions/dm-phone/public/styles.css +1139 -0
  53. package/extensions/dm-phone/public/sw.js +78 -0
  54. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  55. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  56. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  57. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  58. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  59. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  60. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  61. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  62. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  63. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  64. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  65. package/extensions/dm-phone/src/extension/types.ts +73 -0
  66. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  67. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  68. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  69. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  70. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  71. package/extensions/dm-subagents/artifacts.ts +11 -5
  72. package/extensions/dm-subagents/async-execution.ts +4 -1
  73. package/extensions/dm-subagents/index.ts +1 -1
  74. package/extensions/dm-subagents/schemas.ts +1 -1
  75. package/extensions/dm-subagents/settings.ts +6 -4
  76. package/extensions/dm-subagents/subagent-runner.ts +167 -50
  77. package/extensions/dm-subagents/types.ts +62 -2
  78. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ import { state } from "./state.js";
2
+ import { openTokenModal, renderHeader, renderQuota, showBanner, showToast } from "./ui.js";
3
+
4
+ export function clearReconnectTimer() {
5
+ if (!state.reconnectTimer) return;
6
+ clearTimeout(state.reconnectTimer);
7
+ state.reconnectTimer = null;
8
+ }
9
+
10
+ export function sendRpc(command) {
11
+ if (state.socket?.readyState !== WebSocket.OPEN) {
12
+ showToast("Not connected to DM.", "error");
13
+ return false;
14
+ }
15
+ state.socket.send(JSON.stringify({ kind: "rpc", command }));
16
+ return true;
17
+ }
18
+
19
+ export function sendLocalCommand(command) {
20
+ if (state.socket?.readyState !== WebSocket.OPEN) {
21
+ showToast("Not connected to DM.", "error");
22
+ return false;
23
+ }
24
+
25
+ state.socket.send(JSON.stringify({ kind: "local-command", command }));
26
+ return true;
27
+ }
28
+
29
+ export function requestReload() {
30
+ if (state.status?.isStreaming || state.snapshotState?.isStreaming) {
31
+ showToast("Wait for the current response to finish before reloading.", "error");
32
+ return false;
33
+ }
34
+
35
+ if (state.snapshotState?.isCompacting) {
36
+ showToast("Wait for compaction to finish before reloading.", "error");
37
+ return false;
38
+ }
39
+
40
+ return sendLocalCommand("reload");
41
+ }
42
+
43
+ export async function refreshQuota({ force = false } = {}) {
44
+ const model = state.snapshotState?.model;
45
+ const currentModel = model && typeof model === "object"
46
+ ? {
47
+ provider: typeof model.provider === "string" ? model.provider : "",
48
+ modelId: typeof model.id === "string" ? model.id : "",
49
+ }
50
+ : null;
51
+
52
+ if (!currentModel || currentModel.provider !== "openai-codex" || !/^gpt-/i.test(currentModel.modelId || "")) {
53
+ state.quota = null;
54
+ renderQuota();
55
+ return;
56
+ }
57
+
58
+ const requestId = ++state.quotaRequestId;
59
+
60
+ try {
61
+ const url = new URL("/api/quota", window.location.origin);
62
+ url.searchParams.set("provider", currentModel.provider);
63
+ url.searchParams.set("modelId", currentModel.modelId);
64
+ if (force) url.searchParams.set("force", "1");
65
+
66
+ const response = await fetch(url, { cache: "no-store" });
67
+ if (!response.ok) throw new Error(`Quota request failed (${response.status})`);
68
+
69
+ const quota = await response.json();
70
+ if (requestId !== state.quotaRequestId) return;
71
+ state.quota = quota;
72
+ } catch {
73
+ if (requestId !== state.quotaRequestId) return;
74
+ if (!state.quota?.visible) {
75
+ state.quota = null;
76
+ }
77
+ }
78
+
79
+ renderQuota();
80
+ }
81
+
82
+ export function refreshAll(options = {}) {
83
+ const { forceQuota = false } = options;
84
+
85
+ if (state.socket?.readyState === WebSocket.OPEN) {
86
+ state.socket.send(JSON.stringify({ kind: "refresh" }));
87
+ sendRpc({ type: "get_commands" });
88
+ sendRpc({ type: "get_available_models" });
89
+ }
90
+
91
+ void refreshQuota({ force: forceQuota });
92
+ }
93
+
94
+ export function connectSocket({ handleEnvelope, handleAuthFailure }) {
95
+ clearReconnectTimer();
96
+ if (state.socket && (state.socket.readyState === WebSocket.OPEN || state.socket.readyState === WebSocket.CONNECTING)) {
97
+ return;
98
+ }
99
+
100
+ const protocol = window.location.protocol === "https:" ? "wss" : "ws";
101
+ const url = new URL(`${protocol}://${window.location.host}/ws`);
102
+ if (state.token) url.searchParams.set("token", state.token);
103
+
104
+ const socket = new WebSocket(url);
105
+ state.socket = socket;
106
+ renderHeader();
107
+
108
+ socket.addEventListener("open", () => {
109
+ clearReconnectTimer();
110
+ showBanner("");
111
+ renderHeader();
112
+ refreshAll();
113
+ });
114
+
115
+ socket.addEventListener("message", (event) => {
116
+ try {
117
+ handleEnvelope(JSON.parse(event.data));
118
+ } catch {
119
+ showToast("Received malformed data from server.", "error");
120
+ }
121
+ });
122
+
123
+ socket.addEventListener("close", (event) => {
124
+ if (state.socket === socket) {
125
+ state.socket = null;
126
+ }
127
+ renderHeader();
128
+ if (event.code === 4009) {
129
+ showBanner("This DM Phone instance was opened from another device or tab.", "error");
130
+ return;
131
+ }
132
+ if (event.code === 4010) {
133
+ showBanner("DM Phone stopped due to inactivity. Run /phone-start again when needed.", "error");
134
+ return;
135
+ }
136
+ if (event.code === 1008) {
137
+ handleAuthFailure();
138
+ return;
139
+ }
140
+ if (event.code === 1006) {
141
+ showBanner("Connection lost. Retrying…", "error");
142
+ }
143
+ if (!state.manuallyClosed) {
144
+ clearReconnectTimer();
145
+ state.reconnectTimer = setTimeout(() => connectSocket({ handleEnvelope, handleAuthFailure }), 1800);
146
+ }
147
+ });
148
+
149
+ socket.addEventListener("error", () => {
150
+ renderHeader();
151
+ });
152
+ }
153
+
154
+ export async function loadHealth() {
155
+ const response = await fetch("/api/health", { cache: "no-store" });
156
+ if (!response.ok) throw new Error(`Health check failed (${response.status})`);
157
+ state.health = await response.json();
158
+ state.status = state.health;
159
+ renderHeader();
160
+ }
161
+
162
+ export async function boot({ handleEnvelope, handleAuthFailure }) {
163
+ try {
164
+ await loadHealth();
165
+ } catch (error) {
166
+ showBanner(error instanceof Error ? error.message : "Failed to reach server.", "error");
167
+ return;
168
+ }
169
+
170
+ if (state.health?.hasToken && !state.token) openTokenModal();
171
+ else connectSocket({ handleEnvelope, handleAuthFailure });
172
+
173
+ if ("serviceWorker" in navigator) {
174
+ navigator.serviceWorker.register("/sw.js").catch(() => {});
175
+ }
176
+ }
@@ -0,0 +1,409 @@
1
+ import { THEME_CSS_VARIABLES, TOKEN_STORAGE_KEY } from "./constants.js";
2
+ import { formatCwdDisplay, formatTokenCount, stripTerminalControlSequences } from "./formatters.js";
3
+ import { el, state } from "./state.js";
4
+
5
+ let composerLayoutFrame = 0;
6
+ let messageScrollFrame = 0;
7
+ let pendingMessageScroll = { force: false, streaming: false, behavior: "smooth" };
8
+
9
+ const NEAR_BOTTOM_THRESHOLD = 120;
10
+ const STREAM_FOLLOW_INTERVAL_MS = 320;
11
+ const STREAM_FOLLOW_MIN_HEIGHT_DELTA = 16;
12
+ const PROGRAMMATIC_SCROLL_GUARD_MS = 700;
13
+
14
+ export function storeToken(token) {
15
+ if (token) localStorage.setItem(TOKEN_STORAGE_KEY, token);
16
+ else localStorage.removeItem(TOKEN_STORAGE_KEY);
17
+ }
18
+
19
+ export function resetToken({ clearInput = false } = {}) {
20
+ state.token = "";
21
+ storeToken("");
22
+ if (clearInput) el.tokenInput.value = "";
23
+ }
24
+
25
+ export function applyThemePalette(themePayload) {
26
+ const root = document.documentElement;
27
+ const colors = themePayload?.colors || {};
28
+
29
+ for (const [colorKey, cssVariable] of Object.entries(THEME_CSS_VARIABLES)) {
30
+ const value = typeof colors[colorKey] === "string" ? colors[colorKey].trim() : "";
31
+ if (value) root.style.setProperty(cssVariable, value);
32
+ else root.style.removeProperty(cssVariable);
33
+ }
34
+
35
+ if (themePayload?.name) root.dataset.piTheme = themePayload.name;
36
+ else delete root.dataset.piTheme;
37
+ }
38
+
39
+ function currentQuotaModel() {
40
+ const model = state.snapshotState?.model;
41
+ if (!model || typeof model !== "object") return null;
42
+ return {
43
+ provider: typeof model.provider === "string" ? model.provider : "",
44
+ modelId: typeof model.id === "string" ? model.id : "",
45
+ };
46
+ }
47
+
48
+ function shouldShowQuotaForModel(model = currentQuotaModel()) {
49
+ if (!model) return false;
50
+ return model.provider === "openai-codex" && /^gpt-/i.test(model.modelId || "");
51
+ }
52
+
53
+ function quotaPillClassName(leftPercent) {
54
+ if (!Number.isFinite(leftPercent)) return "";
55
+ if (leftPercent <= 10) return "danger";
56
+ if (leftPercent <= 25) return "warn";
57
+ return "good";
58
+ }
59
+
60
+ function contextPillClassName(percent) {
61
+ if (!Number.isFinite(percent)) return "";
62
+ if (percent > 90) return "danger";
63
+ if (percent > 70) return "warn";
64
+ return "";
65
+ }
66
+
67
+ function currentContextUsage() {
68
+ const snapshot = state.snapshotState;
69
+ if (!snapshot || typeof snapshot !== "object") return null;
70
+
71
+ const contextWindow = Number(snapshot.contextUsage?.contextWindow ?? snapshot.model?.contextWindow);
72
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) return null;
73
+
74
+ const percent = typeof snapshot.contextUsage?.percent === "number"
75
+ ? snapshot.contextUsage.percent
76
+ : null;
77
+ const percentDisplay = percent === null ? "?" : `${percent.toFixed(1)}%`;
78
+
79
+ return {
80
+ percent,
81
+ text: `${percentDisplay}/${formatTokenCount(contextWindow)}`,
82
+ };
83
+ }
84
+
85
+ export function syncComposerReserve() {
86
+ if (!el.composerWrap) return;
87
+ const reserve = Math.max(144, Math.ceil(el.composerWrap.getBoundingClientRect().height + 16));
88
+ document.documentElement.style.setProperty("--composer-reserve", `${reserve}px`);
89
+ }
90
+
91
+ export function scheduleComposerLayoutSync() {
92
+ if (composerLayoutFrame) return;
93
+ composerLayoutFrame = requestAnimationFrame(() => {
94
+ composerLayoutFrame = 0;
95
+ syncComposerReserve();
96
+ });
97
+ }
98
+
99
+ function isAnyModalOpen() {
100
+ return !el.sheetModal.classList.contains("hidden") || !el.uiModal.classList.contains("hidden") || !el.loginModal.classList.contains("hidden");
101
+ }
102
+
103
+ function scrollingElement() {
104
+ return document.scrollingElement || document.documentElement;
105
+ }
106
+
107
+ export function isNearBottom(threshold = NEAR_BOTTOM_THRESHOLD) {
108
+ const root = scrollingElement();
109
+ if (!root) return true;
110
+ const scrollTop = typeof window.scrollY === "number" ? window.scrollY : root.scrollTop;
111
+ const viewportHeight = window.innerHeight || root.clientHeight || 0;
112
+ return root.scrollHeight - (scrollTop + viewportHeight) <= threshold;
113
+ }
114
+
115
+ export function updateJumpToLatestButton() {
116
+ if (!el.jumpToLatestButton) return;
117
+ const hasMessages = Boolean(state.messages.length || state.liveAssistant || state.liveTools.size);
118
+ const shouldShow = hasMessages && !isAnyModalOpen() && !state.followLatest && !isNearBottom();
119
+ el.jumpToLatestButton.classList.toggle("hidden", !shouldShow);
120
+ }
121
+
122
+ export function setFollowLatest(value) {
123
+ state.followLatest = Boolean(value);
124
+ if (state.followLatest) {
125
+ state.lastAutoFollowAt = 0;
126
+ state.lastAutoFollowHeight = 0;
127
+ }
128
+ updateJumpToLatestButton();
129
+ }
130
+
131
+ export function scrollMessagesToBottom({ force = false, streaming = false, behavior = "smooth" } = {}) {
132
+ if (isAnyModalOpen()) {
133
+ updateJumpToLatestButton();
134
+ return;
135
+ }
136
+
137
+ pendingMessageScroll = {
138
+ force: pendingMessageScroll.force || force,
139
+ streaming: pendingMessageScroll.streaming || streaming,
140
+ behavior,
141
+ };
142
+
143
+ if (messageScrollFrame) return;
144
+ messageScrollFrame = requestAnimationFrame(() => {
145
+ messageScrollFrame = 0;
146
+ const nextScroll = pendingMessageScroll;
147
+ pendingMessageScroll = { force: false, streaming: false, behavior: "smooth" };
148
+
149
+ if (isAnyModalOpen()) {
150
+ updateJumpToLatestButton();
151
+ return;
152
+ }
153
+
154
+ syncComposerReserve();
155
+ const root = scrollingElement();
156
+ if (!root) {
157
+ updateJumpToLatestButton();
158
+ return;
159
+ }
160
+
161
+ if (!nextScroll.force && !state.followLatest && !isNearBottom()) {
162
+ updateJumpToLatestButton();
163
+ return;
164
+ }
165
+
166
+ const scrollTop = typeof window.scrollY === "number" ? window.scrollY : root.scrollTop;
167
+ const viewportHeight = window.innerHeight || root.clientHeight || 0;
168
+ const targetTop = Math.max(0, root.scrollHeight - viewportHeight);
169
+ const now = Date.now();
170
+ const heightDelta = Math.abs(root.scrollHeight - state.lastAutoFollowHeight);
171
+
172
+ if (!nextScroll.force && nextScroll.streaming) {
173
+ if (state.lastAutoFollowAt && now - state.lastAutoFollowAt < STREAM_FOLLOW_INTERVAL_MS) {
174
+ updateJumpToLatestButton();
175
+ return;
176
+ }
177
+ if (state.lastAutoFollowHeight && heightDelta < STREAM_FOLLOW_MIN_HEIGHT_DELTA) {
178
+ updateJumpToLatestButton();
179
+ return;
180
+ }
181
+ }
182
+
183
+ if (Math.abs(targetTop - scrollTop) < 2) {
184
+ state.lastAutoFollowAt = now;
185
+ state.lastAutoFollowHeight = root.scrollHeight;
186
+ state.followLatest = true;
187
+ updateJumpToLatestButton();
188
+ return;
189
+ }
190
+
191
+ state.ignoreScrollTrackingUntil = now + PROGRAMMATIC_SCROLL_GUARD_MS;
192
+ window.scrollTo({ top: targetTop, behavior: nextScroll.behavior });
193
+ state.lastAutoFollowAt = now;
194
+ state.lastAutoFollowHeight = root.scrollHeight;
195
+ state.followLatest = true;
196
+ updateJumpToLatestButton();
197
+ });
198
+ }
199
+
200
+ export function showBanner(text, kind = "info") {
201
+ const cleanText = stripTerminalControlSequences(text || "").trim();
202
+ if (!cleanText) {
203
+ el.banner.classList.add("hidden");
204
+ el.banner.textContent = "";
205
+ el.banner.classList.remove("error");
206
+ return;
207
+ }
208
+ el.banner.textContent = cleanText;
209
+ el.banner.classList.toggle("error", kind === "error");
210
+ el.banner.classList.remove("hidden");
211
+ }
212
+
213
+ export function showToast(text, kind = "info") {
214
+ const cleanText = stripTerminalControlSequences(text || "").trim();
215
+ if (!cleanText) return;
216
+ const toast = document.createElement("div");
217
+ toast.className = `toast ${kind === "error" ? "error" : ""}`;
218
+ toast.textContent = cleanText;
219
+ el.toastHost.appendChild(toast);
220
+ setTimeout(() => toast.remove(), 3500);
221
+ }
222
+
223
+ export function renderQuota() {
224
+ const cwd = state.status?.cwd || state.health?.cwd || "";
225
+ const contextUsage = currentContextUsage();
226
+
227
+ if (cwd) {
228
+ el.quotaCwd.textContent = formatCwdDisplay(cwd);
229
+ el.quotaCwd.title = cwd;
230
+ el.quotaCwd.setAttribute("aria-label", `Working directory ${cwd}`);
231
+ el.quotaCwd.className = "quota-pill cwd-pill mono";
232
+ } else {
233
+ el.quotaCwd.textContent = "";
234
+ el.quotaCwd.title = "";
235
+ el.quotaCwd.removeAttribute("aria-label");
236
+ el.quotaCwd.className = "quota-pill cwd-pill mono hidden";
237
+ }
238
+
239
+ if (contextUsage) {
240
+ el.quotaContext.textContent = contextUsage.text;
241
+ el.quotaContext.title = "Current context usage";
242
+ el.quotaContext.setAttribute("aria-label", `Current context usage ${contextUsage.text}`);
243
+ el.quotaContext.className = `quota-pill quota-context-pill mono ${contextPillClassName(contextUsage.percent)}`.trim();
244
+ } else {
245
+ el.quotaContext.textContent = "";
246
+ el.quotaContext.title = "";
247
+ el.quotaContext.removeAttribute("aria-label");
248
+ el.quotaContext.className = "quota-pill quota-context-pill mono hidden";
249
+ }
250
+
251
+ const quotaSupported = shouldShowQuotaForModel();
252
+ if (!quotaSupported) {
253
+ state.quota = null;
254
+ }
255
+
256
+ const primary = quotaSupported ? state.quota?.primaryWindow : null;
257
+ const secondary = quotaSupported ? state.quota?.secondaryWindow : null;
258
+ const hasQuotaPills = Boolean(contextUsage || (state.quota?.visible && (primary || secondary)));
259
+
260
+ if (primary) {
261
+ el.quotaPrimary.textContent = primary.text;
262
+ el.quotaPrimary.title = `${primary.label} quota remaining`;
263
+ el.quotaPrimary.setAttribute("aria-label", `${primary.label} quota remaining ${primary.text}`);
264
+ el.quotaPrimary.className = `quota-pill ${quotaPillClassName(primary.leftPercent)}`.trim();
265
+ } else {
266
+ el.quotaPrimary.textContent = "";
267
+ el.quotaPrimary.title = "";
268
+ el.quotaPrimary.removeAttribute("aria-label");
269
+ el.quotaPrimary.className = "quota-pill hidden";
270
+ }
271
+
272
+ if (secondary) {
273
+ el.quotaSecondary.textContent = secondary.text;
274
+ el.quotaSecondary.title = `${secondary.label} quota remaining`;
275
+ el.quotaSecondary.setAttribute("aria-label", `${secondary.label} quota remaining ${secondary.text}`);
276
+ el.quotaSecondary.className = `quota-pill ${quotaPillClassName(secondary.leftPercent)}`.trim();
277
+ } else {
278
+ el.quotaSecondary.textContent = "";
279
+ el.quotaSecondary.title = "";
280
+ el.quotaSecondary.removeAttribute("aria-label");
281
+ el.quotaSecondary.className = "quota-pill hidden";
282
+ }
283
+
284
+ const hasMetaRow = Boolean(cwd);
285
+ el.quotaMetaRow.classList.toggle("hidden", !hasMetaRow);
286
+ el.quotaPillsRow.classList.toggle("hidden", !hasQuotaPills);
287
+ el.quotaRow.classList.toggle("hidden", !(hasMetaRow || hasQuotaPills));
288
+ scheduleComposerLayoutSync();
289
+ }
290
+
291
+ function updateComposerState() {
292
+ const streaming = Boolean(state.status?.isStreaming || state.snapshotState?.isStreaming);
293
+ const sendLabel = streaming ? "Queue message" : "Send message";
294
+
295
+ el.abortButton.disabled = !streaming;
296
+ if (el.stopButton) {
297
+ el.stopButton.disabled = !streaming;
298
+ el.stopButton.classList.toggle("hidden", !streaming);
299
+ }
300
+ el.sendButton.textContent = ">";
301
+ el.sendButton.setAttribute("aria-label", sendLabel);
302
+ el.sendButton.setAttribute("title", sendLabel);
303
+ el.steerButton.classList.toggle("hidden", !streaming);
304
+ scheduleComposerLayoutSync();
305
+ }
306
+
307
+ export function renderHeader() {
308
+ const connected = state.socket?.readyState === WebSocket.OPEN;
309
+ el.connectionPill.textContent = connected ? "Connected" : "Offline";
310
+ el.connectionPill.classList.toggle("offline", !connected);
311
+
312
+ const status = state.status || state.health || {};
313
+ applyThemePalette(status.theme || state.health?.theme || null);
314
+ const snapshotMatchesActive = !state.snapshotWorkerId || !state.activeSessionId || state.snapshotWorkerId === state.activeSessionId;
315
+ const snapshot = snapshotMatchesActive ? (state.snapshotState || {}) : {};
316
+ const activeSession = state.activeSessions.find((session) => session.id === state.activeSessionId) || null;
317
+ el.cwdValue.textContent = status.cwd || "—";
318
+ el.sessionValue.textContent = snapshot.sessionName || snapshot.sessionId || activeSession?.label || "Current session";
319
+ el.modelValue.textContent = snapshot.model?.name || snapshot.model?.id || activeSession?.model?.name || "Default";
320
+ el.thinkingValue.textContent = snapshot.thinkingLevel || "—";
321
+ const owner = status.controlOwner || "cli";
322
+ el.streamingValue.textContent = `${status.isStreaming || snapshot.isStreaming ? "Streaming" : "Idle"} · ${owner}`;
323
+ el.serverValue.textContent = status.port ? `${status.host || "127.0.0.1"}:${status.port}` : "—";
324
+ updateComposerState();
325
+ renderQuota();
326
+ }
327
+
328
+ export function autoResizeTextarea() {
329
+ el.promptInput.style.height = "auto";
330
+ el.promptInput.style.height = `${Math.min(el.promptInput.scrollHeight, 220)}px`;
331
+ scheduleComposerLayoutSync();
332
+ }
333
+
334
+ export function openTokenModal() {
335
+ if (el.loginModal.classList.contains("hidden")) {
336
+ el.tokenInput.value = state.token;
337
+ }
338
+ el.loginModal.classList.remove("hidden");
339
+ setTimeout(() => el.tokenInput.focus(), 10);
340
+ }
341
+
342
+ export function closeTokenModal() {
343
+ el.loginModal.classList.add("hidden");
344
+ }
345
+
346
+ export function clearUiModal() {
347
+ state.pendingUiRequest = null;
348
+ el.uiModal.classList.add("hidden");
349
+ el.uiModalOptions.innerHTML = "";
350
+ el.uiModalButtons.innerHTML = "";
351
+ el.uiModalInput.value = "";
352
+ el.uiModalInput.classList.add("hidden");
353
+ }
354
+
355
+ export function openUiModalForRequest(request, onResponse) {
356
+ state.pendingUiRequest = request;
357
+ el.uiModalTitle.textContent = request.title || "Action required";
358
+ el.uiModalMessage.textContent = request.message || "";
359
+ el.uiModalOptions.innerHTML = "";
360
+ el.uiModalButtons.innerHTML = "";
361
+ el.uiModalInput.value = request.prefill || "";
362
+ el.uiModalInput.classList.add("hidden");
363
+
364
+ const addCancel = () => {
365
+ const cancelButton = document.createElement("button");
366
+ cancelButton.className = "secondary";
367
+ cancelButton.textContent = "Cancel";
368
+ cancelButton.addEventListener("click", () => onResponse({ id: request.id, cancelled: true }));
369
+ el.uiModalButtons.appendChild(cancelButton);
370
+ };
371
+
372
+ if (request.method === "select") {
373
+ for (const option of request.options || []) {
374
+ const button = document.createElement("button");
375
+ button.textContent = option;
376
+ button.className = "secondary";
377
+ button.addEventListener("click", () => onResponse({ id: request.id, value: option }));
378
+ el.uiModalOptions.appendChild(button);
379
+ }
380
+ addCancel();
381
+ } else if (request.method === "confirm") {
382
+ const denyButton = document.createElement("button");
383
+ denyButton.className = "secondary";
384
+ denyButton.textContent = "No";
385
+ denyButton.addEventListener("click", () => onResponse({ id: request.id, confirmed: false }));
386
+
387
+ const confirmButton = document.createElement("button");
388
+ confirmButton.textContent = "Yes";
389
+ confirmButton.addEventListener("click", () => onResponse({ id: request.id, confirmed: true }));
390
+
391
+ el.uiModalButtons.appendChild(denyButton);
392
+ el.uiModalButtons.appendChild(confirmButton);
393
+ } else if (request.method === "input" || request.method === "editor") {
394
+ el.uiModalInput.classList.remove("hidden");
395
+ el.uiModalInput.placeholder = request.placeholder || "";
396
+
397
+ const submitButton = document.createElement("button");
398
+ submitButton.textContent = "Submit";
399
+ submitButton.addEventListener("click", () => onResponse({ id: request.id, value: el.uiModalInput.value }));
400
+
401
+ addCancel();
402
+ el.uiModalButtons.appendChild(submitButton);
403
+ }
404
+
405
+ el.uiModal.classList.remove("hidden");
406
+ setTimeout(() => {
407
+ if (request.method === "input" || request.method === "editor") el.uiModalInput.focus();
408
+ }, 10);
409
+ }
@@ -0,0 +1 @@
1
+ import "./app/main.js";
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#12233a" />
5
+ <stop offset="100%" stop-color="#08111b" />
6
+ </linearGradient>
7
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
8
+ <stop offset="0%" stop-color="#7cc4ff" />
9
+ <stop offset="100%" stop-color="#5da7e6" />
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="512" height="512" rx="120" fill="url(#bg)" />
13
+ <rect x="72" y="72" width="368" height="368" rx="92" fill="none" stroke="url(#accent)" stroke-width="18" />
14
+ <path d="M186 154h112c61 0 103 40 103 98s-42 98-103 98h-58v76h-54V154zm54 53v90h55c34 0 53-18 53-45s-19-45-53-45h-55z" fill="#ecf3ff" />
15
+ </svg>