castle-web-cli 0.4.59 → 0.4.60

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.
@@ -11,26 +11,26 @@
11
11
  // because the popover lives outside the React root.
12
12
  function applyBackendSegs(settings) {
13
13
  const segs = [
14
- ['agent-router-seg', settings.router],
15
- ['agent-tasks-seg', settings.tasks],
16
- ['agent-model-seg', settings.claudeModel],
14
+ ["agent-router-seg", settings.router],
15
+ ["agent-tasks-seg", settings.tasks],
16
+ ["agent-model-seg", settings.claudeModel],
17
17
  ];
18
18
  for (const [segId, value] of segs) {
19
19
  if (!value)
20
20
  continue;
21
21
  for (const button of document.querySelectorAll(`#${segId} button`)) {
22
- button.classList.toggle('active', (button.dataset.backend ?? button.dataset.model) === value);
22
+ button.classList.toggle("active", (button.dataset.backend ?? button.dataset.model) === value);
23
23
  }
24
24
  }
25
25
  }
26
- const TERMINAL_TASK_STATUSES = ['done', 'failed', 'interrupted'];
27
- const ATTACHMENT_URL_PREFIX = '/__castle/agent/attachments/';
26
+ const TERMINAL_TASK_STATUSES = ["done", "failed", "interrupted"];
27
+ const ATTACHMENT_URL_PREFIX = "/__castle/agent/attachments/";
28
28
  function renderMarkdown(text) {
29
29
  try {
30
30
  return { __html: marked.parse(text, { breaks: true }) };
31
31
  }
32
32
  catch {
33
- return { __html: '' };
33
+ return { __html: "" };
34
34
  }
35
35
  }
36
36
  //
@@ -39,40 +39,44 @@ function renderMarkdown(text) {
39
39
  // run the terminal side (lazy PTY spawn, focus).
40
40
  //
41
41
  function focusChatInput() {
42
- document.getElementById('chat-input')?.focus();
42
+ document.getElementById("chat-input")?.focus();
43
43
  }
44
44
  function initPanelChrome() {
45
- const viewButtons = Array.from(document.querySelectorAll('#panel-view-seg button'));
45
+ const viewButtons = Array.from(document.querySelectorAll("#panel-view-seg button"));
46
46
  const applyView = (view) => {
47
- document.body.classList.toggle('term-view', view === 'terminal');
47
+ document.body.classList.toggle("term-view", view === "terminal");
48
48
  for (const button of viewButtons) {
49
- button.classList.toggle('active', button.dataset.view === view);
49
+ button.classList.toggle("active", button.dataset.view === view);
50
50
  }
51
51
  try {
52
- localStorage.setItem('castle-panel-view', view);
52
+ localStorage.setItem("castle-panel-view", view);
53
53
  }
54
54
  catch {
55
55
  /* storage disabled */
56
56
  }
57
57
  };
58
- document.addEventListener('castle-panel-view', (event) => {
59
- const detail = event.detail;
58
+ document.addEventListener("castle-panel-view", (event) => {
59
+ const detail = event
60
+ .detail;
60
61
  if (!detail)
61
62
  return;
62
- const view = detail.view === 'terminal' ? 'terminal' : 'chat';
63
+ const view = detail.view === "terminal" ? "terminal" : "chat";
63
64
  applyView(view);
64
- if (view === 'chat' && detail.focus)
65
+ if (view === "chat" && detail.focus)
65
66
  focusChatInput();
66
67
  });
67
68
  for (const button of viewButtons) {
68
- button.addEventListener('click', () => {
69
- const view = button.dataset.view === 'terminal' ? 'terminal' : 'chat';
70
- document.dispatchEvent(new CustomEvent('castle-panel-view', { detail: { view, focus: true } }));
69
+ button.addEventListener("click", () => {
70
+ const view = button.dataset.view === "terminal" ? "terminal" : "chat";
71
+ document.dispatchEvent(new CustomEvent("castle-panel-view", { detail: { view, focus: true } }));
71
72
  });
72
73
  }
73
- let storedView = 'chat';
74
+ let storedView = "chat";
74
75
  try {
75
- storedView = localStorage.getItem('castle-panel-view') === 'terminal' ? 'terminal' : 'chat';
76
+ storedView =
77
+ localStorage.getItem("castle-panel-view") === "terminal"
78
+ ? "terminal"
79
+ : "chat";
76
80
  }
77
81
  catch {
78
82
  /* storage disabled */
@@ -81,35 +85,36 @@ function initPanelChrome() {
81
85
  // When the panel is toggled open onto the chat view, focus the input.
82
86
  // ide-client's own click handler runs first and flips term-open; check the
83
87
  // final state a tick later.
84
- document.getElementById('term-toggle')?.addEventListener('click', () => {
88
+ document.getElementById("term-toggle")?.addEventListener("click", () => {
85
89
  window.setTimeout(() => {
86
- if (document.body.classList.contains('term-open') &&
87
- !document.body.classList.contains('term-view')) {
90
+ if (document.body.classList.contains("term-open") &&
91
+ !document.body.classList.contains("term-view")) {
88
92
  focusChatInput();
89
93
  }
90
94
  }, 0);
91
95
  });
92
- const progressButtons = Array.from(document.querySelectorAll('#chat-progress-seg button'));
96
+ const progressButtons = Array.from(document.querySelectorAll("#chat-progress-seg button"));
93
97
  const applyProgressStyle = (style) => {
94
- document.body.classList.toggle('progress-bars', style === 'bar');
98
+ document.body.classList.toggle("progress-bars", style === "bar");
95
99
  for (const button of progressButtons) {
96
- button.classList.toggle('active', button.dataset.progress === style);
100
+ button.classList.toggle("active", button.dataset.progress === style);
97
101
  }
98
102
  try {
99
- localStorage.setItem('castle-progress-style', style);
103
+ localStorage.setItem("castle-progress-style", style);
100
104
  }
101
105
  catch {
102
106
  /* storage disabled */
103
107
  }
104
108
  };
105
109
  for (const button of progressButtons) {
106
- button.addEventListener('click', () => {
107
- applyProgressStyle(button.dataset.progress === 'bar' ? 'bar' : 'pie');
110
+ button.addEventListener("click", () => {
111
+ applyProgressStyle(button.dataset.progress === "bar" ? "bar" : "pie");
108
112
  });
109
113
  }
110
- let storedStyle = 'pie';
114
+ let storedStyle = "pie";
111
115
  try {
112
- storedStyle = localStorage.getItem('castle-progress-style') === 'bar' ? 'bar' : 'pie';
116
+ storedStyle =
117
+ localStorage.getItem("castle-progress-style") === "bar" ? "bar" : "pie";
113
118
  }
114
119
  catch {
115
120
  /* storage disabled */
@@ -118,17 +123,19 @@ function initPanelChrome() {
118
123
  // Backend / model segs: clicks become events the React app forwards to the
119
124
  // server (the state lives in .castle/agent/settings.json).
120
125
  const serverSegs = [
121
- ['agent-router-seg', 'router'],
122
- ['agent-tasks-seg', 'tasks'],
123
- ['agent-model-seg', 'claudeModel'],
126
+ ["agent-router-seg", "router"],
127
+ ["agent-tasks-seg", "tasks"],
128
+ ["agent-model-seg", "claudeModel"],
124
129
  ];
125
130
  for (const [segId, segKey] of serverSegs) {
126
131
  for (const button of document.querySelectorAll(`#${segId} button`)) {
127
- button.addEventListener('click', () => {
132
+ button.addEventListener("click", () => {
128
133
  const value = button.dataset.backend ?? button.dataset.model;
129
134
  if (!value)
130
135
  return;
131
- document.dispatchEvent(new CustomEvent('castle-set-backend', { detail: { key: segKey, value } }));
136
+ document.dispatchEvent(new CustomEvent("castle-set-backend", {
137
+ detail: { key: segKey, value },
138
+ }));
132
139
  });
133
140
  }
134
141
  }
@@ -137,7 +144,7 @@ function initPanelChrome() {
137
144
  // React app.
138
145
  //
139
146
  function applyServerEvent(ev, setMessages, setTasks, setFeeds) {
140
- if (ev.type === 'hello') {
147
+ if (ev.type === "hello") {
141
148
  const messages = ev.messages ?? [];
142
149
  const tasks = ev.tasks ?? [];
143
150
  const feeds = ev.feeds ?? {};
@@ -147,40 +154,45 @@ function applyServerEvent(ev, setMessages, setTasks, setFeeds) {
147
154
  if (ev.settings)
148
155
  applyBackendSegs(ev.settings);
149
156
  }
150
- else if (ev.type === 'task-feed' && ev.id) {
157
+ else if (ev.type === "task-feed" && ev.id) {
151
158
  const id = ev.id;
152
- const entry = ev.entry ?? '';
153
- setFeeds((prev) => ({ ...prev, [id]: [...(prev[id] ?? []).slice(-79), entry] }));
159
+ const entry = ev.entry ?? "";
160
+ setFeeds((prev) => ({
161
+ ...prev,
162
+ [id]: [...(prev[id] ?? []).slice(-79), entry],
163
+ }));
154
164
  }
155
- else if (ev.type === 'settings' && ev.settings) {
165
+ else if (ev.type === "settings" && ev.settings) {
156
166
  applyBackendSegs(ev.settings);
157
167
  }
158
- else if (ev.type === 'message-add' && ev.message) {
168
+ else if (ev.type === "message-add" && ev.message) {
159
169
  const message = ev.message;
160
170
  setMessages((prev) => [...prev, message]);
161
171
  }
162
- else if (ev.type === 'message-delta' && ev.id) {
163
- setMessages((prev) => prev.map((m) => (m.id === ev.id ? { ...m, text: m.text + (ev.delta ?? '') } : m)));
172
+ else if (ev.type === "message-delta" && ev.id) {
173
+ setMessages((prev) => prev.map((m) => m.id === ev.id ? { ...m, text: m.text + (ev.delta ?? "") } : m));
164
174
  }
165
- else if (ev.type === 'message-activity' && ev.id) {
166
- setMessages((prev) => prev.map((m) => (m.id === ev.id ? { ...m, activity: ev.activity ?? null } : m)));
175
+ else if (ev.type === "message-activity" && ev.id) {
176
+ setMessages((prev) => prev.map((m) => m.id === ev.id ? { ...m, activity: ev.activity ?? null } : m));
167
177
  }
168
- else if (ev.type === 'message-done' && ev.id) {
178
+ else if (ev.type === "message-done" && ev.id) {
169
179
  setMessages((prev) => prev.map((m) => m.id === ev.id
170
180
  ? {
171
181
  ...m,
172
- text: ev.text ?? '',
173
- status: ev.status ?? 'done',
182
+ text: ev.text ?? "",
183
+ status: ev.status ?? "done",
174
184
  activity: null,
175
185
  interrupted: ev.interrupted === true,
176
186
  }
177
187
  : m));
178
188
  }
179
- else if (ev.type === 'task-update' && ev.task) {
189
+ else if (ev.type === "task-update" && ev.task) {
180
190
  const task = ev.task;
181
191
  setTasks((prev) => {
182
192
  const known = prev.some((t) => t.id === task.id);
183
- const next = known ? prev.map((t) => (t.id === task.id ? task : t)) : [...prev, task];
193
+ const next = known
194
+ ? prev.map((t) => (t.id === task.id ? task : t))
195
+ : [...prev, task];
184
196
  return next.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
185
197
  });
186
198
  if (TERMINAL_TASK_STATUSES.includes(task.status)) {
@@ -194,6 +206,53 @@ function applyServerEvent(ev, setMessages, setTasks, setFeeds) {
194
206
  }
195
207
  }
196
208
  }
209
+ // Drop "[Reading X]" lines whose file is edited anywhere -- a read that's later
210
+ // followed by an edit of the same file is redundant; just show the edit.
211
+ function rollupFeed(lines) {
212
+ const editRe = /^\[Editing (.+)\]$/;
213
+ const readRe = /^\[Reading (.+)\]$/;
214
+ const edited = new Set();
215
+ for (const line of lines) {
216
+ const m = editRe.exec(line.trim());
217
+ if (m)
218
+ for (const f of m[1].split(", "))
219
+ edited.add(f);
220
+ }
221
+ const out = [];
222
+ for (const line of lines) {
223
+ const r = readRe.exec(line.trim());
224
+ if (r) {
225
+ const keep = r[1].split(", ").filter((f) => !edited.has(f));
226
+ if (keep.length === 0)
227
+ continue;
228
+ out.push(`[Reading ${keep.join(", ")}]`);
229
+ continue;
230
+ }
231
+ out.push(line);
232
+ }
233
+ return out;
234
+ }
235
+ // Collapse consecutive same-verb tool actions into one comma-separated line:
236
+ // "[Editing A]" then "[Editing B]" -> "[Editing A, B]". Only word verbs merge
237
+ // (Editing/Reading/...), so prose stays separate.
238
+ function coalesceFeed(lines) {
239
+ const toolRe = /^\[([A-Za-z]+)\s+(.+)\]$/;
240
+ const out = [];
241
+ for (const line of lines) {
242
+ const m = toolRe.exec(line.trim());
243
+ if (m) {
244
+ const prev = out.length ? toolRe.exec(out[out.length - 1].trim()) : null;
245
+ if (prev && prev[1] === m[1]) {
246
+ if (!prev[2].split(", ").includes(m[2])) {
247
+ out[out.length - 1] = `[${m[1]} ${prev[2]}, ${m[2]}]`;
248
+ }
249
+ continue;
250
+ }
251
+ }
252
+ out.push(line);
253
+ }
254
+ return out;
255
+ }
197
256
  function TaskFeed(props) {
198
257
  const hostRef = React.useRef(null);
199
258
  React.useLayoutEffect(() => {
@@ -201,30 +260,66 @@ function TaskFeed(props) {
201
260
  if (host)
202
261
  host.scrollTop = host.scrollHeight;
203
262
  }, [props.lines]);
204
- return (React.createElement("div", { className: "task-feed", ref: hostRef, onClick: (event) => event.stopPropagation() }, props.lines.map((line, index) => (React.createElement("div", { key: index }, line)))));
263
+ return (React.createElement("div", { className: "task-feed", ref: hostRef, onClick: (event) => event.stopPropagation() }, coalesceFeed(rollupFeed(props.lines)).map((line, index) => {
264
+ // Tool actions arrive bracketed (e.g. "[Editing Ball.jsx]"); render
265
+ // those as indented monospace. Everything else is the agent's prose --
266
+ // markdown-render it so **bold** / `code` show through.
267
+ const tool = /^\[(.+)\]$/.exec(line.trim());
268
+ if (tool) {
269
+ return (React.createElement("div", { key: index, className: "task-feed-tool" }, tool[1]));
270
+ }
271
+ return (React.createElement("div", { key: index, className: "task-feed-msg", dangerouslySetInnerHTML: renderMarkdown(line) }));
272
+ })));
273
+ }
274
+ // Stopwatch label: "7m 23s" (or just "23s" under a minute). Counts whole seconds.
275
+ function fmtDuration(ms) {
276
+ const total = Math.max(0, Math.round(ms / 1000));
277
+ const m = Math.floor(total / 60);
278
+ const s = total % 60;
279
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
205
280
  }
206
281
  function TaskRow(props) {
207
282
  const { task, onAck } = props;
208
283
  const [expanded, setExpanded] = React.useState(false);
284
+ // Live stopwatch: re-render every second while running so the elapsed time
285
+ // ticks up; on finish the effect clears and the time freezes at the total.
286
+ const [nowMs, setNowMs] = React.useState(() => Date.now());
287
+ React.useEffect(() => {
288
+ if (task.status !== "running")
289
+ return undefined;
290
+ const timer = setInterval(() => setNowMs(Date.now()), 1000);
291
+ return () => clearInterval(timer);
292
+ }, [task.status]);
293
+ const startedMs = task.startedAt ? Date.parse(task.startedAt) : null;
294
+ const finishedMs = task.finishedAt ? Date.parse(task.finishedAt) : null;
209
295
  const finished = TERMINAL_TASK_STATUSES.includes(task.status);
210
296
  // A full circle means "click me to check off". A still-running task never
211
297
  // fills completely, even if its agent already reports 100.
212
- const pct = task.status === 'done' ? 100 : finished ? task.progress : Math.min(task.progress, 95);
213
- const notes = task.notes.trim() || task.resultSummary?.trim() || '(no notes from the agent yet)';
214
- return (React.createElement("div", { className: `task-card${task.status === 'waiting' ? ' waiting' : ''}`, onClick: () => setExpanded(!expanded) },
298
+ const pct = task.status === "done"
299
+ ? 100
300
+ : finished
301
+ ? task.progress
302
+ : Math.min(task.progress, 95);
303
+ const notes = task.notes.trim() || task.resultSummary?.trim() || "";
304
+ return (React.createElement("div", { className: `task-card${task.status === "waiting" ? " waiting" : ""}`, onClick: () => setExpanded(!expanded) },
215
305
  React.createElement("div", { className: "task-row" },
216
- React.createElement("span", { className: `task-pie${finished ? ' task-ack-pie' : ''}`, title: finished ? 'tested -- check off' : undefined, style: { background: `conic-gradient(#000000 ${pct}%, #eaeef2 0)` }, onClick: finished
306
+ React.createElement("span", { className: `task-pie${finished ? " task-ack-pie" : ""}`, title: finished ? "tested -- check off" : undefined, style: { background: `conic-gradient(#000000 ${pct}%, #eaeef2 0)` }, onClick: finished
217
307
  ? (event) => {
218
308
  event.stopPropagation();
219
309
  onAck(task.id, false);
220
310
  }
221
311
  : undefined }),
222
312
  React.createElement("span", { className: "task-title" }, task.title),
223
- task.status !== 'done' ? (React.createElement("span", { className: `task-status ${task.status}` }, task.status === 'running' ? `${pct}%` : task.status)) : null),
313
+ task.status === "running" && startedMs != null ? (React.createElement("span", { className: "task-time" }, fmtDuration(nowMs - startedMs))) : task.status === "done" &&
314
+ startedMs != null &&
315
+ finishedMs != null ? (React.createElement("span", { className: "task-time" }, fmtDuration(finishedMs - startedMs))) : task.status !== "done" ? (React.createElement("span", { className: `task-status ${task.status}` }, task.status)) : null),
224
316
  React.createElement("div", { className: "task-bar" },
225
317
  React.createElement("div", { style: { width: `${pct}%` } })),
226
- expanded && task.status === 'running' && props.feed && props.feed.length > 0 ? (React.createElement(TaskFeed, { lines: props.feed })) : null,
227
- expanded && task.status !== 'running' ? (React.createElement("div", { className: "task-notes", dangerouslySetInnerHTML: renderMarkdown(notes) })) : null));
318
+ expanded &&
319
+ task.status === "running" &&
320
+ props.feed &&
321
+ props.feed.length > 0 ? (React.createElement(TaskFeed, { lines: props.feed })) : null,
322
+ expanded && task.status !== "running" && notes ? (React.createElement("div", { className: "task-notes", dangerouslySetInnerHTML: renderMarkdown(notes) })) : null));
228
323
  }
229
324
  function TaskBoard(props) {
230
325
  // Hide only tasks that are acknowledged AND finished. An active
@@ -239,28 +334,30 @@ function Message(props) {
239
334
  const { msg } = props;
240
335
  // A reply that was nothing but task directives strips to empty -- the board
241
336
  // already shows the spawn, so don't render an empty husk of a card.
242
- if (msg.role === 'assistant' && msg.status !== 'streaming' && !msg.text.trim()) {
337
+ if (msg.role === "assistant" &&
338
+ msg.status !== "streaming" &&
339
+ !msg.text.trim()) {
243
340
  return null;
244
341
  }
245
- if (msg.role === 'log') {
342
+ if (msg.role === "log") {
246
343
  return React.createElement("div", { className: "msg msg-log" }, msg.text);
247
344
  }
248
- if (msg.role === 'user') {
345
+ if (msg.role === "user") {
249
346
  return (React.createElement("div", { className: "msg msg-user" },
250
347
  (msg.attachments ?? []).map((name) => (React.createElement("img", { key: name, className: "msg-image", src: `${ATTACHMENT_URL_PREFIX}${name}`, alt: "" }))),
251
348
  msg.text ? React.createElement("div", { className: "msg-text" }, msg.text) : null));
252
349
  }
253
- const classes = ['msg', 'msg-assistant'];
254
- if (msg.status === 'streaming')
255
- classes.push('streaming');
256
- if (msg.status === 'error')
257
- classes.push('msg-error');
258
- return (React.createElement("div", { className: classes.join(' ') },
350
+ const classes = ["msg", "msg-assistant"];
351
+ if (msg.status === "streaming")
352
+ classes.push("streaming");
353
+ if (msg.status === "error")
354
+ classes.push("msg-error");
355
+ return (React.createElement("div", { className: classes.join(" ") },
259
356
  React.createElement("div", { className: "msg-text", dangerouslySetInnerHTML: renderMarkdown(msg.text) }),
260
- msg.status === 'streaming' && msg.activity ? (React.createElement("div", { className: "msg-activity" },
357
+ msg.status === "streaming" && msg.activity ? (React.createElement("div", { className: "msg-activity" },
261
358
  msg.activity,
262
359
  "...")) : null,
263
- msg.interrupted ? React.createElement("div", { className: "msg-interrupted" }, "interrupted by your next message") : null));
360
+ msg.interrupted ? (React.createElement("div", { className: "msg-interrupted" }, "interrupted by your next message")) : null));
264
361
  }
265
362
  function MessageList(props) {
266
363
  const hostRef = React.useRef(null);
@@ -280,7 +377,8 @@ function MessageList(props) {
280
377
  if (!host)
281
378
  return;
282
379
  const observer = new ResizeObserver(() => {
283
- host.scrollTop = host.scrollHeight - host.clientHeight - fromBottomRef.current;
380
+ host.scrollTop =
381
+ host.scrollHeight - host.clientHeight - fromBottomRef.current;
284
382
  });
285
383
  observer.observe(host);
286
384
  return () => observer.disconnect();
@@ -289,7 +387,8 @@ function MessageList(props) {
289
387
  const host = hostRef.current;
290
388
  if (!host)
291
389
  return;
292
- fromBottomRef.current = host.scrollHeight - host.scrollTop - host.clientHeight;
390
+ fromBottomRef.current =
391
+ host.scrollHeight - host.scrollTop - host.clientHeight;
293
392
  pinnedRef.current = fromBottomRef.current < 48;
294
393
  };
295
394
  return (React.createElement("div", { id: "chat-messages", ref: hostRef, onScroll: handleScroll },
@@ -298,25 +397,25 @@ function MessageList(props) {
298
397
  }
299
398
  function readImageFiles(files, add) {
300
399
  for (const file of Array.from(files)) {
301
- if (!file.type.startsWith('image/'))
400
+ if (!file.type.startsWith("image/"))
302
401
  continue;
303
402
  const reader = new FileReader();
304
403
  reader.onload = () => {
305
- if (typeof reader.result === 'string')
404
+ if (typeof reader.result === "string")
306
405
  add({ name: file.name, dataUrl: reader.result });
307
406
  };
308
407
  reader.readAsDataURL(file);
309
408
  }
310
409
  }
311
410
  function InputRow(props) {
312
- const [value, setValue] = React.useState('');
411
+ const [value, setValue] = React.useState("");
313
412
  const [pending, setPending] = React.useState([]);
314
413
  const inputRef = React.useRef(null);
315
414
  const autosize = () => {
316
415
  const el = inputRef.current;
317
416
  if (!el)
318
417
  return;
319
- el.style.height = 'auto';
418
+ el.style.height = "auto";
320
419
  el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
321
420
  };
322
421
  React.useLayoutEffect(autosize, [value]);
@@ -328,7 +427,7 @@ function InputRow(props) {
328
427
  if (!text && pending.length === 0)
329
428
  return;
330
429
  props.onSend(text, pending);
331
- setValue('');
430
+ setValue("");
332
431
  setPending([]);
333
432
  };
334
433
  return (React.createElement(React.Fragment, null,
@@ -341,7 +440,10 @@ function InputRow(props) {
341
440
  readImageFiles(files, addImage);
342
441
  }
343
442
  }, onKeyDown: (event) => {
344
- if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.altKey) {
443
+ if (event.key === "Enter" &&
444
+ !event.shiftKey &&
445
+ !event.metaKey &&
446
+ !event.altKey) {
345
447
  event.preventDefault();
346
448
  send();
347
449
  }
@@ -354,7 +456,7 @@ function App() {
354
456
  const [feeds, setFeeds] = React.useState({});
355
457
  const sendRef = React.useRef(() => undefined);
356
458
  React.useEffect(() => {
357
- const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
459
+ const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
358
460
  const wsUrl = `${wsProtocol}//${window.location.host}/__castle/agent`;
359
461
  let ws = null;
360
462
  let alive = true;
@@ -368,11 +470,11 @@ function App() {
368
470
  outbox.push(data);
369
471
  };
370
472
  const connect = () => {
371
- if (!alive || document.visibilityState === 'hidden')
473
+ if (!alive || document.visibilityState === "hidden")
372
474
  return;
373
475
  const sock = new WebSocket(wsUrl);
374
476
  ws = sock;
375
- sock.addEventListener('open', () => {
477
+ sock.addEventListener("open", () => {
376
478
  retryMs = 500;
377
479
  while (outbox.length > 0 && sock.readyState === sock.OPEN) {
378
480
  const queued = outbox.shift();
@@ -380,7 +482,7 @@ function App() {
380
482
  sock.send(queued);
381
483
  }
382
484
  });
383
- sock.addEventListener('message', (event) => {
485
+ sock.addEventListener("message", (event) => {
384
486
  try {
385
487
  applyServerEvent(JSON.parse(String(event.data)), setMessages, setTasks, setFeeds);
386
488
  }
@@ -388,7 +490,7 @@ function App() {
388
490
  /* malformed frame */
389
491
  }
390
492
  });
391
- sock.addEventListener('close', () => {
493
+ sock.addEventListener("close", () => {
392
494
  if (ws !== sock)
393
495
  return;
394
496
  ws = null;
@@ -399,36 +501,37 @@ function App() {
399
501
  });
400
502
  };
401
503
  const onVisibility = () => {
402
- if (document.visibilityState !== 'hidden' && !ws)
504
+ if (document.visibilityState !== "hidden" && !ws)
403
505
  connect();
404
506
  };
405
507
  const onSetBackend = (event) => {
406
- const detail = event.detail;
508
+ const detail = event
509
+ .detail;
407
510
  if (!detail?.key || !detail.value)
408
511
  return;
409
- sendRef.current({ type: 'set-settings', [detail.key]: detail.value });
512
+ sendRef.current({ type: "set-settings", [detail.key]: detail.value });
410
513
  };
411
- document.addEventListener('visibilitychange', onVisibility);
412
- document.addEventListener('castle-set-backend', onSetBackend);
514
+ document.addEventListener("visibilitychange", onVisibility);
515
+ document.addEventListener("castle-set-backend", onSetBackend);
413
516
  connect();
414
517
  return () => {
415
518
  alive = false;
416
- document.removeEventListener('visibilitychange', onVisibility);
417
- document.removeEventListener('castle-set-backend', onSetBackend);
519
+ document.removeEventListener("visibilitychange", onVisibility);
520
+ document.removeEventListener("castle-set-backend", onSetBackend);
418
521
  ws?.close();
419
522
  };
420
523
  }, []);
421
524
  return (React.createElement(React.Fragment, null,
422
- React.createElement(TaskBoard, { tasks: tasks, feeds: feeds, onAck: (id, rejected) => sendRef.current({ type: 'task-ack', id, rejected }) }),
525
+ React.createElement(TaskBoard, { tasks: tasks, feeds: feeds, onAck: (id, rejected) => sendRef.current({ type: "task-ack", id, rejected }) }),
423
526
  React.createElement(MessageList, { messages: messages }),
424
- React.createElement(InputRow, { onSend: (text, images) => sendRef.current({ type: 'user-message', text, images }) })));
527
+ React.createElement(InputRow, { onSend: (text, images) => sendRef.current({ type: "user-message", text, images }) })));
425
528
  }
426
529
  initPanelChrome();
427
- const chatHost = document.getElementById('chat-host');
530
+ const chatHost = document.getElementById("chat-host");
428
531
  if (chatHost) {
429
532
  ReactDOM.createRoot(chatHost).render(React.createElement(App, null));
430
- if (document.body.classList.contains('term-open') &&
431
- !document.body.classList.contains('term-view')) {
533
+ if (document.body.classList.contains("term-open") &&
534
+ !document.body.classList.contains("term-view")) {
432
535
  window.setTimeout(focusChatInput, 0);
433
536
  }
434
537
  }
package/dist/ide.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import * as http from 'http';
2
- import { Duplex } from 'stream';
3
- import { type RawData } from 'ws';
1
+ import * as http from "http";
2
+ import { Duplex } from "stream";
3
+ import { type RawData } from "ws";
4
4
  export declare const IDE_ASSET_PREFIX = "/__castle/ide/";
5
5
  export declare const PTY_WS_PATH = "/__castle/pty";
6
6
  export declare const DECK_FOCUS_SCRIPT: string;