@oharness/server 0.9.2 → 0.10.0
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/i18n.js +356 -62
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/page/client/account.js +430 -23
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +20 -1
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/composer.js +709 -58
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/computer.d.ts +27 -0
- package/dist/page/client/computer.js +358 -0
- package/dist/page/client/computer.js.map +1 -0
- package/dist/page/client/dialogs.js +93 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +94 -7
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/files.js +86 -1
- package/dist/page/client/files.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +12 -6
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/routines.js +296 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +229 -51
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +273 -19
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/tasks.d.ts +8 -0
- package/dist/page/client/tasks.js +293 -0
- package/dist/page/client/tasks.js.map +1 -0
- package/dist/page/client/transcript.js +174 -6
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +1 -11
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +706 -104
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +161 -54
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +14 -0
- package/dist/prefs.js +8 -1
- package/dist/prefs.js.map +1 -1
- package/dist/remote-crypto.d.ts +78 -0
- package/dist/remote-crypto.js +216 -0
- package/dist/remote-crypto.js.map +1 -0
- package/dist/remote.d.ts +103 -0
- package/dist/remote.js +498 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +1291 -49
- package/dist/server.js.map +1 -1
- package/dist/starters.d.ts +92 -0
- package/dist/starters.js +213 -0
- package/dist/starters.js.map +1 -0
- package/dist/theme.js +5 -0
- package/dist/theme.js.map +1 -1
- package/dist/turn-shots.d.ts +27 -0
- package/dist/turn-shots.js +71 -0
- package/dist/turn-shots.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background tasks: their rows over the composer, and the sheet one opens
|
|
3
|
+
* into — what the subagent was told, what it did, what it said — with a way
|
|
4
|
+
* to stop it or tell it something.
|
|
5
|
+
*
|
|
6
|
+
* One part of the client script; see ./index.ts for how the parts join.
|
|
7
|
+
*/
|
|
8
|
+
export const TASKS = String.raw `
|
|
9
|
+
// --- background tasks -----------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const TASK_LIVE = new Set(["queued", "running"]);
|
|
12
|
+
/** How many task rows sit over the composer; the sheet lists every one. */
|
|
13
|
+
const TASK_ROWS = 4;
|
|
14
|
+
/** The task the sheet shows, while it is open. */
|
|
15
|
+
let taskOpen = null;
|
|
16
|
+
let taskSheet = null;
|
|
17
|
+
/** Bumped per paint, so a slow answer never draws over a newer one. */
|
|
18
|
+
let taskPaintSeq = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Running ones first, then the newest. Ended tasks stay listed, quieter, so
|
|
22
|
+
* what they said is a click away and they can still be sent a follow-up.
|
|
23
|
+
*/
|
|
24
|
+
function sortedTasks(tasks) {
|
|
25
|
+
return (tasks || []).slice().sort((a, b) =>
|
|
26
|
+
(Number(TASK_LIVE.has(b.status)) - Number(TASK_LIVE.has(a.status))) || (b.queuedAt - a.queuedAt));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The rows over the composer, drawn by renderQueued with the queue. */
|
|
30
|
+
function appendTaskRows(container, here) {
|
|
31
|
+
// Put-away tasks leave the list; the transcript's card and the sheet's
|
|
32
|
+
// chips still reach them.
|
|
33
|
+
const tasks = sortedTasks(here && here.tasks).filter((task) => !task.dismissed);
|
|
34
|
+
for (const task of tasks.slice(0, TASK_ROWS)) {
|
|
35
|
+
const row = el("div", "q task task-" + task.status);
|
|
36
|
+
row.appendChild(el("span", "tag", t("tasks.status." + task.status)));
|
|
37
|
+
const name = el("span", "n", task.description + (task.activity ? " · " + task.activity : ""));
|
|
38
|
+
row.appendChild(name);
|
|
39
|
+
const open = el("button", "go", t("tasks.open"));
|
|
40
|
+
open.type = "button";
|
|
41
|
+
open.addEventListener("click", () => openTask(activeId, task.id));
|
|
42
|
+
row.appendChild(open);
|
|
43
|
+
// One × in the same place, saying what it does: running, it stops the
|
|
44
|
+
// task; ended, it only takes the row away.
|
|
45
|
+
const live = TASK_LIVE.has(task.status);
|
|
46
|
+
const close = el("button", "x", "×");
|
|
47
|
+
close.type = "button";
|
|
48
|
+
close.title = t(live ? "tasks.stop" : "tasks.dismiss");
|
|
49
|
+
close.setAttribute("aria-label", close.title);
|
|
50
|
+
close.addEventListener("click", () => void (live ? stopTask(activeId, task.id) : dismissTask(activeId, task.id)));
|
|
51
|
+
row.appendChild(close);
|
|
52
|
+
name.addEventListener("click", () => openTask(activeId, task.id));
|
|
53
|
+
container.appendChild(row);
|
|
54
|
+
}
|
|
55
|
+
if (tasks.length > TASK_ROWS) {
|
|
56
|
+
const more = el("button", "q task-more", t("tasks.more", { n: tasks.length - TASK_ROWS }));
|
|
57
|
+
more.type = "button";
|
|
58
|
+
more.addEventListener("click", () => openTask(activeId, tasks[TASK_ROWS].id));
|
|
59
|
+
container.appendChild(more);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Background results read into the conversation, as the transcript shows them:
|
|
65
|
+
* one line per task saying how it ended, each a way into its sheet. Where it
|
|
66
|
+
* sits is where the model read them.
|
|
67
|
+
*/
|
|
68
|
+
function taskCard(tasks) {
|
|
69
|
+
const card = el("div", "task-card");
|
|
70
|
+
card.appendChild(el("div", "task-card-head", t("tasks.cardTitle")));
|
|
71
|
+
for (const task of tasks) {
|
|
72
|
+
const row = el("div", "task-card-row task-" + task.status);
|
|
73
|
+
row.appendChild(el("span", "tag", t("tasks.status." + task.status)));
|
|
74
|
+
row.appendChild(el("span", "n", task.description));
|
|
75
|
+
const open = el("button", "go", t("tasks.open"));
|
|
76
|
+
open.type = "button";
|
|
77
|
+
// The conversation this card is in, which need not be the one on screen.
|
|
78
|
+
const session = P.id;
|
|
79
|
+
open.addEventListener("click", () => openTask(session, task.id));
|
|
80
|
+
row.appendChild(open);
|
|
81
|
+
card.appendChild(row);
|
|
82
|
+
}
|
|
83
|
+
// In the transcript's column, so it lines up with the messages around it.
|
|
84
|
+
const row = el("div", "column task-card-row-wrap");
|
|
85
|
+
row.appendChild(card);
|
|
86
|
+
P.node.appendChild(row);
|
|
87
|
+
follow();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A conversation's tasks changed: redraw what shows them. */
|
|
91
|
+
function tasksChanged(session, tasks) {
|
|
92
|
+
pane(session).tasks = tasks || [];
|
|
93
|
+
if (session === activeId) renderQueued();
|
|
94
|
+
if (taskOpen && taskOpen.session === session) void paintTask();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function openTask(session, id) {
|
|
98
|
+
taskOpen = { session, id, stick: true };
|
|
99
|
+
ensureTaskSheet();
|
|
100
|
+
taskSheet.hidden = false;
|
|
101
|
+
void paintTask();
|
|
102
|
+
taskSheet.querySelector(".task-input").focus();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function closeTask() {
|
|
106
|
+
taskOpen = null;
|
|
107
|
+
if (taskSheet) taskSheet.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function dismissTask(session, id) {
|
|
111
|
+
const body = await api("/task", { session, id, action: "dismiss" });
|
|
112
|
+
if (body.error) addNotice(body.error, "notice err");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function stopTask(session, id) {
|
|
116
|
+
const body = await api("/task", { session, id, action: "stop" });
|
|
117
|
+
if (body.error) addNotice(body.error, "notice err");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ensureTaskSheet() {
|
|
121
|
+
if (taskSheet) return;
|
|
122
|
+
taskSheet = el("div");
|
|
123
|
+
taskSheet.id = "task-sheet";
|
|
124
|
+
taskSheet.hidden = true;
|
|
125
|
+
taskSheet.setAttribute("role", "dialog");
|
|
126
|
+
taskSheet.setAttribute("aria-label", t("tasks.title"));
|
|
127
|
+
|
|
128
|
+
const head = el("div", "task-head");
|
|
129
|
+
const titles = el("div", "task-titles");
|
|
130
|
+
titles.appendChild(el("div", "task-title"));
|
|
131
|
+
titles.appendChild(el("div", "task-meta"));
|
|
132
|
+
head.appendChild(titles);
|
|
133
|
+
const stop = el("button", "task-stop", t("tasks.stop"));
|
|
134
|
+
stop.type = "button";
|
|
135
|
+
stop.addEventListener("click", () => { if (taskOpen) void stopTask(taskOpen.session, taskOpen.id); });
|
|
136
|
+
head.appendChild(stop);
|
|
137
|
+
const close = el("button", "task-close", "×");
|
|
138
|
+
close.type = "button";
|
|
139
|
+
close.title = t("tasks.close");
|
|
140
|
+
close.setAttribute("aria-label", t("tasks.close"));
|
|
141
|
+
close.addEventListener("click", closeTask);
|
|
142
|
+
head.appendChild(close);
|
|
143
|
+
taskSheet.appendChild(head);
|
|
144
|
+
|
|
145
|
+
taskSheet.appendChild(el("div", "task-chips"));
|
|
146
|
+
const log = el("div", "task-log");
|
|
147
|
+
// Follow new lines only while the reader is at the bottom; scrolling up to
|
|
148
|
+
// read something earlier must not be undone by the next tool.
|
|
149
|
+
log.addEventListener("scroll", () => {
|
|
150
|
+
if (taskOpen) taskOpen.stick = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
|
|
151
|
+
});
|
|
152
|
+
taskSheet.appendChild(log);
|
|
153
|
+
|
|
154
|
+
// The box is the field and its send button together, as in the composer.
|
|
155
|
+
const form = el("form", "task-form");
|
|
156
|
+
const box = el("div", "task-box");
|
|
157
|
+
const input = el("textarea", "task-input");
|
|
158
|
+
input.rows = 2;
|
|
159
|
+
input.setAttribute("aria-label", t("tasks.messageLabel"));
|
|
160
|
+
const send = el("button", "primary task-send");
|
|
161
|
+
send.type = "submit";
|
|
162
|
+
send.title = t("tasks.send");
|
|
163
|
+
send.setAttribute("aria-label", t("tasks.send"));
|
|
164
|
+
send.appendChild(svgIcon("send"));
|
|
165
|
+
box.appendChild(input);
|
|
166
|
+
box.appendChild(send);
|
|
167
|
+
form.appendChild(box);
|
|
168
|
+
form.addEventListener("submit", (e) => {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
void sendTask(input);
|
|
171
|
+
});
|
|
172
|
+
input.addEventListener("keydown", (e) => {
|
|
173
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
void sendTask(input);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
taskSheet.appendChild(form);
|
|
179
|
+
|
|
180
|
+
taskSheet.addEventListener("keydown", (e) => {
|
|
181
|
+
if (e.key === "Escape") closeTask();
|
|
182
|
+
});
|
|
183
|
+
document.body.appendChild(taskSheet);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function sendTask(input) {
|
|
187
|
+
const text = input.value.trim();
|
|
188
|
+
if (!text || !taskOpen) return;
|
|
189
|
+
const { session, id } = taskOpen;
|
|
190
|
+
input.disabled = true;
|
|
191
|
+
const body = await api("/task", { session, id, action: "message", text });
|
|
192
|
+
input.disabled = false;
|
|
193
|
+
if (body.error) {
|
|
194
|
+
taskSheet.querySelector(".task-log").appendChild(el("div", "task-error", body.error));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
input.value = "";
|
|
198
|
+
input.focus();
|
|
199
|
+
if (taskOpen) taskOpen.stick = true;
|
|
200
|
+
void paintTask();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function taskElapsed(task) {
|
|
204
|
+
if (!task.startedAt) return "";
|
|
205
|
+
const seconds = Math.max(0, Math.round(((task.endedAt || Date.now()) - task.startedAt) / 1000));
|
|
206
|
+
return seconds < 60 ? seconds + "s" : Math.floor(seconds / 60) + "m" + (seconds % 60) + "s";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function paintTask() {
|
|
210
|
+
if (!taskOpen || !taskSheet) return;
|
|
211
|
+
const { session, id } = taskOpen;
|
|
212
|
+
const seq = ++taskPaintSeq;
|
|
213
|
+
const body = await getJson("/task?session=" + encodeURIComponent(session) + "&id=" + encodeURIComponent(id));
|
|
214
|
+
if (!taskOpen || taskOpen.id !== id || taskOpen.session !== session || seq !== taskPaintSeq) return;
|
|
215
|
+
const log = taskSheet.querySelector(".task-log");
|
|
216
|
+
// Tool rows the reader opened stay open across a redraw: a running task
|
|
217
|
+
// redraws on every step, and an expanded result snapping shut is unreadable.
|
|
218
|
+
// Only for the task already on screen: another task's rows are other rows.
|
|
219
|
+
const opened = new Set();
|
|
220
|
+
if (taskSheet.dataset.painted === id) {
|
|
221
|
+
log.querySelectorAll(".task-tool").forEach((node, index) => { if (node.open) opened.add(index); });
|
|
222
|
+
}
|
|
223
|
+
taskSheet.dataset.painted = id;
|
|
224
|
+
if (body.error) {
|
|
225
|
+
log.replaceChildren(el("div", "task-error", body.error));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const task = body.task;
|
|
229
|
+
|
|
230
|
+
taskSheet.querySelector(".task-title").textContent = task.description;
|
|
231
|
+
const meta = [t("tasks.status." + task.status), task.agentType, t("tasks.turns", { n: task.turns }), taskElapsed(task)];
|
|
232
|
+
if (task.activity) meta.push(task.activity);
|
|
233
|
+
taskSheet.querySelector(".task-meta").textContent = meta.filter(Boolean).join(" · ");
|
|
234
|
+
taskSheet.querySelector(".task-stop").hidden = !TASK_LIVE.has(task.status);
|
|
235
|
+
|
|
236
|
+
// Every task of this conversation, so a sheet opened on one reaches the rest.
|
|
237
|
+
// One row that scrolls sideways: rebuilt on every change, so it keeps
|
|
238
|
+
// where it was scrolled to, and brings the open task's chip into view.
|
|
239
|
+
const chips = taskSheet.querySelector(".task-chips");
|
|
240
|
+
const scrolled = chips.scrollLeft;
|
|
241
|
+
chips.replaceChildren();
|
|
242
|
+
const all = sortedTasks(pane(session).tasks);
|
|
243
|
+
chips.hidden = all.length < 2;
|
|
244
|
+
let current = null;
|
|
245
|
+
for (const other of all) {
|
|
246
|
+
const chip = el("button", "task-chip task-" + other.status + (other.id === id ? " on" : ""), other.description);
|
|
247
|
+
chip.type = "button";
|
|
248
|
+
chip.addEventListener("click", () => openTask(session, other.id));
|
|
249
|
+
chips.appendChild(chip);
|
|
250
|
+
if (other.id === id) current = chip;
|
|
251
|
+
}
|
|
252
|
+
chips.scrollLeft = scrolled;
|
|
253
|
+
if (current && (current.offsetLeft < chips.scrollLeft || current.offsetLeft + current.offsetWidth > chips.scrollLeft + chips.clientWidth)) {
|
|
254
|
+
chips.scrollLeft = current.offsetLeft - 16;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
log.replaceChildren();
|
|
258
|
+
for (const entry of body.transcript || []) {
|
|
259
|
+
if (entry.role === "tool") {
|
|
260
|
+
const tool = el("details", "task-tool" + (entry.isError ? " err" : ""));
|
|
261
|
+
const summary = el("summary");
|
|
262
|
+
summary.appendChild(el("span", "task-tool-name", entry.name));
|
|
263
|
+
summary.appendChild(el("span", "task-tool-input", entry.text));
|
|
264
|
+
tool.appendChild(summary);
|
|
265
|
+
if (entry.result) tool.appendChild(el("pre", "task-tool-result", entry.result));
|
|
266
|
+
if (opened.has(log.querySelectorAll(".task-tool").length)) tool.open = true;
|
|
267
|
+
log.appendChild(tool);
|
|
268
|
+
} else if (entry.role === "assistant") {
|
|
269
|
+
const msg = el("div", "task-msg assistant");
|
|
270
|
+
msg.appendChild(renderMarkdown(entry.text));
|
|
271
|
+
log.appendChild(msg);
|
|
272
|
+
} else {
|
|
273
|
+
log.appendChild(el("div", "task-msg user", entry.text));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!log.childElementCount) log.appendChild(el("div", "task-empty", t("tasks.empty")));
|
|
277
|
+
if (task.worktree) log.appendChild(el("div", "task-note", task.worktree));
|
|
278
|
+
if (task.error) log.appendChild(el("div", "task-error", task.error));
|
|
279
|
+
if (TASK_LIVE.has(task.status)) {
|
|
280
|
+
const working = el("div", "task-working");
|
|
281
|
+
working.appendChild(el("span", "spinner"));
|
|
282
|
+
log.appendChild(working);
|
|
283
|
+
}
|
|
284
|
+
if (taskOpen.stick) log.scrollTop = log.scrollHeight;
|
|
285
|
+
|
|
286
|
+
const input = taskSheet.querySelector(".task-input");
|
|
287
|
+
const stopped = task.status === "cancelled";
|
|
288
|
+
input.disabled = stopped;
|
|
289
|
+
taskSheet.querySelector(".task-send").disabled = stopped;
|
|
290
|
+
input.placeholder = t(stopped ? "tasks.placeholder.stopped" : TASK_LIVE.has(task.status) ? "tasks.placeholder.running" : "tasks.placeholder.ended");
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../../src/page/client/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4R9B,CAAC"}
|
|
@@ -88,6 +88,14 @@ log.addEventListener("scroll", () => {
|
|
|
88
88
|
jumpBtn.hidden = atBottom;
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
// The form's height, for the jump button to sit above (see #jump in style.ts).
|
|
92
|
+
{
|
|
93
|
+
const form = document.getElementById("composer");
|
|
94
|
+
new ResizeObserver(() => {
|
|
95
|
+
document.documentElement.style.setProperty("--form-h", form.offsetHeight + "px");
|
|
96
|
+
}).observe(form);
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
jumpBtn.addEventListener("click", () => {
|
|
92
100
|
atBottom = true;
|
|
93
101
|
jumpBtn.hidden = true;
|
|
@@ -108,13 +116,26 @@ function addUser(text, images) {
|
|
|
108
116
|
if (images && images.length) {
|
|
109
117
|
const strip = el("div", "shots");
|
|
110
118
|
for (const image of images) {
|
|
119
|
+
const src = image.src.startsWith("blob:") ? image.src : q(image.src);
|
|
120
|
+
if (image.kind === "video") {
|
|
121
|
+
// A clip plays where it was sent, with its own controls: an <img>
|
|
122
|
+
// pointed at a video drew a broken-image icon beside its name.
|
|
123
|
+
const video = document.createElement("video");
|
|
124
|
+
video.src = src;
|
|
125
|
+
video.controls = true;
|
|
126
|
+
video.preload = "metadata";
|
|
127
|
+
video.playsInline = true;
|
|
128
|
+
video.title = image.name || "";
|
|
129
|
+
strip.appendChild(video);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
111
132
|
const img = document.createElement("img");
|
|
112
|
-
img.src =
|
|
133
|
+
img.src = src;
|
|
113
134
|
img.alt = image.name || "";
|
|
114
135
|
img.loading = "lazy";
|
|
115
|
-
// A full-size screenshot in a chat bubble is unreadable; the
|
|
116
|
-
//
|
|
117
|
-
img.addEventListener("click", () =>
|
|
136
|
+
// A full-size screenshot in a chat bubble is unreadable; the preview is
|
|
137
|
+
// the same bytes at whatever size the window allows.
|
|
138
|
+
img.addEventListener("click", () => previewImage(img));
|
|
118
139
|
strip.appendChild(img);
|
|
119
140
|
}
|
|
120
141
|
bubble.appendChild(strip);
|
|
@@ -580,6 +601,7 @@ function finishTool(id, result, ms, isError) {
|
|
|
580
601
|
entry.body.appendChild(pre);
|
|
581
602
|
}
|
|
582
603
|
}
|
|
604
|
+
toolShots(entry.body, result);
|
|
583
605
|
if (isError) {
|
|
584
606
|
// A failed step is the one thing worth surfacing without a click.
|
|
585
607
|
entry.card.open = true;
|
|
@@ -590,6 +612,115 @@ function finishTool(id, result, ms, isError) {
|
|
|
590
612
|
follow();
|
|
591
613
|
}
|
|
592
614
|
|
|
615
|
+
/**
|
|
616
|
+
* What a tool saw: the screenshots it returned, as thumbnails in its card.
|
|
617
|
+
*
|
|
618
|
+
* The live pane shows the window as it is now; this is the record of what the
|
|
619
|
+
* agent looked at, for reading a turn back afterwards. It stays inside the
|
|
620
|
+
* folded card — a turn that takes twenty screenshots would otherwise bury its
|
|
621
|
+
* own reply — and a closed card fetches nothing.
|
|
622
|
+
*
|
|
623
|
+
* Live, the bytes arrive in the event and become a blob URL, which a click can
|
|
624
|
+
* open in a tab where a data: URL cannot. Replayed, each is an /attachment
|
|
625
|
+
* position, the same as a user's pasted image.
|
|
626
|
+
*/
|
|
627
|
+
function toolShots(body, result) {
|
|
628
|
+
const shots = (result || []).filter((part) => part.type === "image");
|
|
629
|
+
if (!shots.length) return;
|
|
630
|
+
const strip = el("div", "tool-shots");
|
|
631
|
+
for (const shot of shots) {
|
|
632
|
+
const img = document.createElement("img");
|
|
633
|
+
if (shot.src) {
|
|
634
|
+
img.src = q(shot.src);
|
|
635
|
+
} else if (shot.data) {
|
|
636
|
+
const bytes = Uint8Array.from(atob(shot.data), (c) => c.charCodeAt(0));
|
|
637
|
+
img.src = URL.createObjectURL(new Blob([bytes], { type: shot.mimeType || "image/jpeg" }));
|
|
638
|
+
} else {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
img.alt = "";
|
|
642
|
+
img.loading = "lazy";
|
|
643
|
+
img.addEventListener("click", () => previewImage(img));
|
|
644
|
+
strip.appendChild(img);
|
|
645
|
+
}
|
|
646
|
+
if (strip.childElementCount) body.appendChild(strip);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The windows a Computer use turn ended on, under its reply.
|
|
651
|
+
*
|
|
652
|
+
* The agent reads the accessibility tree rather than looking, so a turn in a
|
|
653
|
+
* native app often has no picture of its own; the host takes one of each
|
|
654
|
+
* window when the turn ends (see turn-shots.ts). Out in the turn rather than
|
|
655
|
+
* folded into a step, because it is the result — what the work left behind —
|
|
656
|
+
* and one per window at most. Above the fork and copy bar, which belongs last.
|
|
657
|
+
*/
|
|
658
|
+
function turnShots(wrap, shots) {
|
|
659
|
+
if (!wrap || !shots || !shots.length) return;
|
|
660
|
+
let strip = wrap.querySelector(":scope > .turn-shots");
|
|
661
|
+
if (!strip) {
|
|
662
|
+
strip = el("div", "turn-shots");
|
|
663
|
+
const bar = wrap.querySelector(":scope > .actions");
|
|
664
|
+
if (bar) wrap.insertBefore(strip, bar);
|
|
665
|
+
else wrap.appendChild(strip);
|
|
666
|
+
}
|
|
667
|
+
for (const shot of shots) {
|
|
668
|
+
const figure = el("figure", "turn-shot");
|
|
669
|
+
const img = document.createElement("img");
|
|
670
|
+
img.src = q(shot.src);
|
|
671
|
+
img.alt = shot.app || "";
|
|
672
|
+
img.loading = "lazy";
|
|
673
|
+
img.addEventListener("click", () => previewImage(img));
|
|
674
|
+
img.addEventListener("error", () => figure.remove());
|
|
675
|
+
figure.appendChild(img);
|
|
676
|
+
const caption = el("figcaption", "", shot.title ? shot.app + " — " + shot.title : shot.app);
|
|
677
|
+
caption.title = caption.textContent;
|
|
678
|
+
figure.appendChild(caption);
|
|
679
|
+
strip.appendChild(figure);
|
|
680
|
+
}
|
|
681
|
+
follow();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* An image at full size, over the page.
|
|
686
|
+
*
|
|
687
|
+
* It used to open in a new tab, which in the desktop app is a new window with
|
|
688
|
+
* no way back but closing it — and a blob URL there is a blank page. This
|
|
689
|
+
* stays in the conversation: Esc or a click anywhere closes it, and the arrow
|
|
690
|
+
* keys step through the other images in the same row, which for a turn of
|
|
691
|
+
* screenshots is the order the agent saw them in.
|
|
692
|
+
*/
|
|
693
|
+
function previewImage(img) {
|
|
694
|
+
const strip = img.closest(".turn-shots, .tool-shots, .shots, #tray") || img.parentElement;
|
|
695
|
+
const row = [...strip.querySelectorAll("img")];
|
|
696
|
+
let at = Math.max(0, row.indexOf(img));
|
|
697
|
+
const scrim = el("div", "img-preview");
|
|
698
|
+
const big = document.createElement("img");
|
|
699
|
+
big.alt = "";
|
|
700
|
+
const count = el("div", "img-preview-count");
|
|
701
|
+
scrim.append(big, count);
|
|
702
|
+
const show = () => {
|
|
703
|
+
big.src = row[at].src;
|
|
704
|
+
count.textContent = row.length > 1 ? at + 1 + " / " + row.length : "";
|
|
705
|
+
};
|
|
706
|
+
const close = () => {
|
|
707
|
+
scrim.remove();
|
|
708
|
+
document.removeEventListener("keydown", onKey, true);
|
|
709
|
+
};
|
|
710
|
+
const onKey = (event) => {
|
|
711
|
+
if (event.key === "Escape") close();
|
|
712
|
+
else if (event.key === "ArrowRight" && at < row.length - 1) { at++; show(); }
|
|
713
|
+
else if (event.key === "ArrowLeft" && at > 0) { at--; show(); }
|
|
714
|
+
else return;
|
|
715
|
+
event.preventDefault();
|
|
716
|
+
event.stopPropagation();
|
|
717
|
+
};
|
|
718
|
+
scrim.addEventListener("click", close);
|
|
719
|
+
document.addEventListener("keydown", onKey, true);
|
|
720
|
+
show();
|
|
721
|
+
document.body.appendChild(scrim);
|
|
722
|
+
}
|
|
723
|
+
|
|
593
724
|
/**
|
|
594
725
|
* A clip as a card rather than the browser's player.
|
|
595
726
|
*
|
|
@@ -772,12 +903,45 @@ function pane(id) {
|
|
|
772
903
|
const node = el("div", "pane");
|
|
773
904
|
node.hidden = true;
|
|
774
905
|
log.appendChild(node);
|
|
775
|
-
entry = { node, title: "", cwd: "", busy: false, queued: [], turn: null, streaming: null, tools: new Map(), thinking: null };
|
|
906
|
+
entry = { id, node, title: "", cwd: "", busy: false, queued: [], turn: null, streaming: null, tools: new Map(), thinking: null };
|
|
776
907
|
panes.set(id, entry);
|
|
777
908
|
}
|
|
778
909
|
return entry;
|
|
779
910
|
}
|
|
780
911
|
|
|
912
|
+
/**
|
|
913
|
+
* The rail page last opened, as a tab of its own after the sessions'.
|
|
914
|
+
*
|
|
915
|
+
* A page is a place beside the work rather than part of a session, so it gets
|
|
916
|
+
* its own tab instead of showing under the session's. There is one: opening
|
|
917
|
+
* another page takes its tab over, and closing it goes back to the session.
|
|
918
|
+
*/
|
|
919
|
+
let pageTab = null;
|
|
920
|
+
|
|
921
|
+
function pageTabButton(navId, on) {
|
|
922
|
+
const tab = el("button", "tab tab-page" + (on ? " on" : ""));
|
|
923
|
+
tab.type = "button";
|
|
924
|
+
tab.appendChild(navIcon(navId));
|
|
925
|
+
const label = document.querySelector("#" + navId + " > span[data-t]");
|
|
926
|
+
tab.appendChild(el("span", "label", label ? label.textContent : ""));
|
|
927
|
+
const close = el("button", "x");
|
|
928
|
+
close.appendChild(svgIcon("close"));
|
|
929
|
+
close.type = "button";
|
|
930
|
+
close.title = t("tabs.close");
|
|
931
|
+
close.setAttribute("aria-label", t("tabs.close"));
|
|
932
|
+
close.addEventListener("click", (e) => {
|
|
933
|
+
e.stopPropagation();
|
|
934
|
+
pageTab = null;
|
|
935
|
+
if (document.body.classList.contains("panelled")) setNav(null);
|
|
936
|
+
paintTabs();
|
|
937
|
+
});
|
|
938
|
+
tab.appendChild(close);
|
|
939
|
+
tab.addEventListener("click", () => {
|
|
940
|
+
if (!document.body.classList.contains("panelled")) document.getElementById(navId).click();
|
|
941
|
+
});
|
|
942
|
+
return tab;
|
|
943
|
+
}
|
|
944
|
+
|
|
781
945
|
function paintTabs() {
|
|
782
946
|
tabsEl.replaceChildren();
|
|
783
947
|
|
|
@@ -785,8 +949,9 @@ function paintTabs() {
|
|
|
785
949
|
// native title bar is gone, so the strip is the only place that names what
|
|
786
950
|
// is on screen, and a tab should read the same whether or not its
|
|
787
951
|
// neighbours share its tree.
|
|
952
|
+
const paged = document.body.classList.contains("panelled");
|
|
788
953
|
for (const [id, entry] of panes) {
|
|
789
|
-
const tab = el("button", "tab" + (id === activeId ? " on" : ""));
|
|
954
|
+
const tab = el("button", "tab" + (id === activeId && !paged ? " on" : ""));
|
|
790
955
|
tab.type = "button";
|
|
791
956
|
if (entry.busy) tab.appendChild(el("span", "dot"));
|
|
792
957
|
if (entry.cwd) tab.appendChild(el("span", "where", projectName(entry.cwd)));
|
|
@@ -812,6 +977,8 @@ function paintTabs() {
|
|
|
812
977
|
tabsEl.appendChild(tab);
|
|
813
978
|
}
|
|
814
979
|
|
|
980
|
+
if (pageTab) tabsEl.appendChild(pageTabButton(pageTab, paged));
|
|
981
|
+
|
|
815
982
|
const add = el("button");
|
|
816
983
|
add.appendChild(svgIcon("plus"));
|
|
817
984
|
add.id = "tab-add";
|
|
@@ -830,6 +997,7 @@ async function focusTab(id) {
|
|
|
830
997
|
paintEmpty(panes.get(id));
|
|
831
998
|
paintTabs();
|
|
832
999
|
browserActivateSession();
|
|
1000
|
+
void computerActivateSession();
|
|
833
1001
|
|
|
834
1002
|
const state = await getJson("/state?session=" + encodeURIComponent(id));
|
|
835
1003
|
if (!state.error) applyState(state);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../../src/page/client/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../../src/page/client/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAigCnC,CAAC"}
|
|
@@ -770,19 +770,10 @@ function usageQuery(path) {
|
|
|
770
770
|
: path + "?range=" + encodeURIComponent(usageRange);
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
-
/** The page's heading
|
|
773
|
+
/** The page's heading and, for an account, whose figures these are. */
|
|
774
774
|
function usageHead(panel) {
|
|
775
775
|
const head = el("div", "row-head");
|
|
776
776
|
head.appendChild(el("h2", null, t("usage.title")));
|
|
777
|
-
// Back to where this was opened from, because it was opened from a row in
|
|
778
|
-
// Settings and the sidebar row it lit is already showing as active — with
|
|
779
|
-
// no way out, the only exit is a sidebar click that looks like a no-op.
|
|
780
|
-
const back = el("button", "ghost", t("usage.back"));
|
|
781
|
-
back.type = "button";
|
|
782
|
-
back.addEventListener("click", () => {
|
|
783
|
-
setNav("nav-settings");
|
|
784
|
-
void showSettings();
|
|
785
|
-
});
|
|
786
777
|
/*
|
|
787
778
|
* Whose figures these are, as a small switch in the heading rather than a
|
|
788
779
|
* third row of tabs.
|
|
@@ -807,7 +798,6 @@ function usageHead(panel) {
|
|
|
807
798
|
}
|
|
808
799
|
actions.appendChild(box);
|
|
809
800
|
}
|
|
810
|
-
actions.appendChild(back);
|
|
811
801
|
head.appendChild(actions);
|
|
812
802
|
panel.appendChild(head);
|
|
813
803
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAkxC3B,CAAC"}
|