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.
- package/dist/agent-prompts.js +3 -2
- package/dist/agent.d.ts +4 -4
- package/dist/agent.js +342 -239
- package/dist/api.d.ts +9 -0
- package/dist/api.js +1 -1
- package/dist/castle-host/host.d.ts +47 -0
- package/dist/castle-host/host.js +373 -0
- package/dist/chat-client.js +199 -96
- package/dist/ide.d.ts +3 -3
- package/dist/ide.js +92 -64
- package/dist/serve.js +42 -1
- package/kits/basic-2d/CLAUDE.md +4 -3
- package/kits/basic-2d/behaviors/Drawing.jsx +5 -2
- package/kits/basic-2d/drawings/default.drawing +70 -0
- package/package.json +5 -2
package/dist/chat-client.js
CHANGED
|
@@ -11,26 +11,26 @@
|
|
|
11
11
|
// because the popover lives outside the React root.
|
|
12
12
|
function applyBackendSegs(settings) {
|
|
13
13
|
const segs = [
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
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(
|
|
22
|
+
button.classList.toggle("active", (button.dataset.backend ?? button.dataset.model) === value);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const TERMINAL_TASK_STATUSES = [
|
|
27
|
-
const ATTACHMENT_URL_PREFIX =
|
|
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(
|
|
42
|
+
document.getElementById("chat-input")?.focus();
|
|
43
43
|
}
|
|
44
44
|
function initPanelChrome() {
|
|
45
|
-
const viewButtons = Array.from(document.querySelectorAll(
|
|
45
|
+
const viewButtons = Array.from(document.querySelectorAll("#panel-view-seg button"));
|
|
46
46
|
const applyView = (view) => {
|
|
47
|
-
document.body.classList.toggle(
|
|
47
|
+
document.body.classList.toggle("term-view", view === "terminal");
|
|
48
48
|
for (const button of viewButtons) {
|
|
49
|
-
button.classList.toggle(
|
|
49
|
+
button.classList.toggle("active", button.dataset.view === view);
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
|
-
localStorage.setItem(
|
|
52
|
+
localStorage.setItem("castle-panel-view", view);
|
|
53
53
|
}
|
|
54
54
|
catch {
|
|
55
55
|
/* storage disabled */
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
-
document.addEventListener(
|
|
59
|
-
const detail = event
|
|
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 ===
|
|
63
|
+
const view = detail.view === "terminal" ? "terminal" : "chat";
|
|
63
64
|
applyView(view);
|
|
64
|
-
if (view ===
|
|
65
|
+
if (view === "chat" && detail.focus)
|
|
65
66
|
focusChatInput();
|
|
66
67
|
});
|
|
67
68
|
for (const button of viewButtons) {
|
|
68
|
-
button.addEventListener(
|
|
69
|
-
const view = button.dataset.view ===
|
|
70
|
-
document.dispatchEvent(new CustomEvent(
|
|
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 =
|
|
74
|
+
let storedView = "chat";
|
|
74
75
|
try {
|
|
75
|
-
storedView =
|
|
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(
|
|
88
|
+
document.getElementById("term-toggle")?.addEventListener("click", () => {
|
|
85
89
|
window.setTimeout(() => {
|
|
86
|
-
if (document.body.classList.contains(
|
|
87
|
-
!document.body.classList.contains(
|
|
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(
|
|
96
|
+
const progressButtons = Array.from(document.querySelectorAll("#chat-progress-seg button"));
|
|
93
97
|
const applyProgressStyle = (style) => {
|
|
94
|
-
document.body.classList.toggle(
|
|
98
|
+
document.body.classList.toggle("progress-bars", style === "bar");
|
|
95
99
|
for (const button of progressButtons) {
|
|
96
|
-
button.classList.toggle(
|
|
100
|
+
button.classList.toggle("active", button.dataset.progress === style);
|
|
97
101
|
}
|
|
98
102
|
try {
|
|
99
|
-
localStorage.setItem(
|
|
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(
|
|
107
|
-
applyProgressStyle(button.dataset.progress ===
|
|
110
|
+
button.addEventListener("click", () => {
|
|
111
|
+
applyProgressStyle(button.dataset.progress === "bar" ? "bar" : "pie");
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
|
-
let storedStyle =
|
|
114
|
+
let storedStyle = "pie";
|
|
111
115
|
try {
|
|
112
|
-
storedStyle =
|
|
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
|
-
[
|
|
122
|
-
[
|
|
123
|
-
[
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
157
|
+
else if (ev.type === "task-feed" && ev.id) {
|
|
151
158
|
const id = ev.id;
|
|
152
|
-
const entry = ev.entry ??
|
|
153
|
-
setFeeds((prev) => ({
|
|
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 ===
|
|
165
|
+
else if (ev.type === "settings" && ev.settings) {
|
|
156
166
|
applyBackendSegs(ev.settings);
|
|
157
167
|
}
|
|
158
|
-
else if (ev.type ===
|
|
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 ===
|
|
163
|
-
setMessages((prev) => prev.map((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 ===
|
|
166
|
-
setMessages((prev) => prev.map((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 ===
|
|
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 ??
|
|
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 ===
|
|
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
|
|
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) =>
|
|
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 ===
|
|
213
|
-
|
|
214
|
-
|
|
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 ?
|
|
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
|
|
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 &&
|
|
227
|
-
|
|
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 ===
|
|
337
|
+
if (msg.role === "assistant" &&
|
|
338
|
+
msg.status !== "streaming" &&
|
|
339
|
+
!msg.text.trim()) {
|
|
243
340
|
return null;
|
|
244
341
|
}
|
|
245
|
-
if (msg.role ===
|
|
342
|
+
if (msg.role === "log") {
|
|
246
343
|
return React.createElement("div", { className: "msg msg-log" }, msg.text);
|
|
247
344
|
}
|
|
248
|
-
if (msg.role ===
|
|
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 = [
|
|
254
|
-
if (msg.status ===
|
|
255
|
-
classes.push(
|
|
256
|
-
if (msg.status ===
|
|
257
|
-
classes.push(
|
|
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 ===
|
|
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 =
|
|
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 =
|
|
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(
|
|
400
|
+
if (!file.type.startsWith("image/"))
|
|
302
401
|
continue;
|
|
303
402
|
const reader = new FileReader();
|
|
304
403
|
reader.onload = () => {
|
|
305
|
-
if (typeof reader.result ===
|
|
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 =
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
473
|
+
if (!alive || document.visibilityState === "hidden")
|
|
372
474
|
return;
|
|
373
475
|
const sock = new WebSocket(wsUrl);
|
|
374
476
|
ws = sock;
|
|
375
|
-
sock.addEventListener(
|
|
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(
|
|
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(
|
|
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 !==
|
|
504
|
+
if (document.visibilityState !== "hidden" && !ws)
|
|
403
505
|
connect();
|
|
404
506
|
};
|
|
405
507
|
const onSetBackend = (event) => {
|
|
406
|
-
const detail = event
|
|
508
|
+
const detail = event
|
|
509
|
+
.detail;
|
|
407
510
|
if (!detail?.key || !detail.value)
|
|
408
511
|
return;
|
|
409
|
-
sendRef.current({ type:
|
|
512
|
+
sendRef.current({ type: "set-settings", [detail.key]: detail.value });
|
|
410
513
|
};
|
|
411
|
-
document.addEventListener(
|
|
412
|
-
document.addEventListener(
|
|
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(
|
|
417
|
-
document.removeEventListener(
|
|
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:
|
|
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:
|
|
527
|
+
React.createElement(InputRow, { onSend: (text, images) => sendRef.current({ type: "user-message", text, images }) })));
|
|
425
528
|
}
|
|
426
529
|
initPanelChrome();
|
|
427
|
-
const chatHost = document.getElementById(
|
|
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(
|
|
431
|
-
!document.body.classList.contains(
|
|
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
|
|
2
|
-
import { Duplex } from
|
|
3
|
-
import { type RawData } from
|
|
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;
|