@oharness/server 0.9.3 → 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.
Files changed (59) hide show
  1. package/dist/i18n.js +354 -62
  2. package/dist/i18n.js.map +1 -1
  3. package/dist/main.js +4 -0
  4. package/dist/main.js.map +1 -1
  5. package/dist/page/client/account.js +430 -23
  6. package/dist/page/client/account.js.map +1 -1
  7. package/dist/page/client/base.js +20 -1
  8. package/dist/page/client/base.js.map +1 -1
  9. package/dist/page/client/composer.js +693 -54
  10. package/dist/page/client/composer.js.map +1 -1
  11. package/dist/page/client/computer.d.ts +27 -0
  12. package/dist/page/client/computer.js +358 -0
  13. package/dist/page/client/computer.js.map +1 -0
  14. package/dist/page/client/dialogs.js +93 -6
  15. package/dist/page/client/dialogs.js.map +1 -1
  16. package/dist/page/client/events.js +94 -7
  17. package/dist/page/client/events.js.map +1 -1
  18. package/dist/page/client/files.js +86 -1
  19. package/dist/page/client/files.js.map +1 -1
  20. package/dist/page/client/index.js +4 -0
  21. package/dist/page/client/index.js.map +1 -1
  22. package/dist/page/client/panels.js +10 -6
  23. package/dist/page/client/panels.js.map +1 -1
  24. package/dist/page/client/routines.js +296 -41
  25. package/dist/page/client/routines.js.map +1 -1
  26. package/dist/page/client/shell.js +187 -51
  27. package/dist/page/client/shell.js.map +1 -1
  28. package/dist/page/client/state.js +273 -19
  29. package/dist/page/client/state.js.map +1 -1
  30. package/dist/page/client/tasks.d.ts +8 -0
  31. package/dist/page/client/tasks.js +293 -0
  32. package/dist/page/client/tasks.js.map +1 -0
  33. package/dist/page/client/transcript.js +137 -5
  34. package/dist/page/client/transcript.js.map +1 -1
  35. package/dist/page/client/usage.js +1 -11
  36. package/dist/page/client/usage.js.map +1 -1
  37. package/dist/page/style.js +568 -60
  38. package/dist/page/style.js.map +1 -1
  39. package/dist/page.js +90 -11
  40. package/dist/page.js.map +1 -1
  41. package/dist/prefs.d.ts +14 -0
  42. package/dist/prefs.js +8 -1
  43. package/dist/prefs.js.map +1 -1
  44. package/dist/remote-crypto.d.ts +78 -0
  45. package/dist/remote-crypto.js +216 -0
  46. package/dist/remote-crypto.js.map +1 -0
  47. package/dist/remote.d.ts +103 -0
  48. package/dist/remote.js +498 -0
  49. package/dist/remote.js.map +1 -0
  50. package/dist/server.d.ts +70 -0
  51. package/dist/server.js +1291 -49
  52. package/dist/server.js.map +1 -1
  53. package/dist/starters.d.ts +92 -0
  54. package/dist/starters.js +213 -0
  55. package/dist/starters.js.map +1 -0
  56. package/dist/turn-shots.d.ts +27 -0
  57. package/dist/turn-shots.js +71 -0
  58. package/dist/turn-shots.js.map +1 -0
  59. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"composer.js","sourceRoot":"","sources":["../../../src/page/client/composer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqrDjC,CAAC"}
1
+ {"version":3,"file":"composer.js","sourceRoot":"","sources":["../../../src/page/client/composer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAozEjC,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The Computer card: the window the agent is working in, live, floating over
3
+ * the conversation with the agent's cursor on it.
4
+ *
5
+ * Computer use works in the background, in windows behind the one the person
6
+ * is using or on another Space. The cursor overlay on screen can only point at
7
+ * what is visible, so without this the work is invisible.
8
+ *
9
+ * A card rather than a column. It used to be a side pane, and a pane that
10
+ * takes a third of the window the moment the agent touches an app is one
11
+ * people close — after which nothing showed the work at all. The card sits in
12
+ * the corner of the conversation while a turn is working in a window, can be
13
+ * enlarged, dragged, or folded to a pill, and goes when the turn ends: the
14
+ * picture of where it ended is under the reply by then (see turnShots).
15
+ *
16
+ * The picture is a live capture of that one window, straight from Chromium
17
+ * (the desktop app's getUserMedia on a window source), not screenshots from
18
+ * the computer tools: each of those replaces the snapshot the agent's element
19
+ * references belong to, so polling them would keep invalidating the agent's
20
+ * work. It uses the Screen Recording grant OHarness already has.
21
+ *
22
+ * Presentation only. Nothing here is model input, and the card never drives
23
+ * the window; the person's own mouse does that.
24
+ *
25
+ * One part of the client script; see ./index.ts for how the parts join.
26
+ */
27
+ export declare const COMPUTER: string;
@@ -0,0 +1,358 @@
1
+ /**
2
+ * The Computer card: the window the agent is working in, live, floating over
3
+ * the conversation with the agent's cursor on it.
4
+ *
5
+ * Computer use works in the background, in windows behind the one the person
6
+ * is using or on another Space. The cursor overlay on screen can only point at
7
+ * what is visible, so without this the work is invisible.
8
+ *
9
+ * A card rather than a column. It used to be a side pane, and a pane that
10
+ * takes a third of the window the moment the agent touches an app is one
11
+ * people close — after which nothing showed the work at all. The card sits in
12
+ * the corner of the conversation while a turn is working in a window, can be
13
+ * enlarged, dragged, or folded to a pill, and goes when the turn ends: the
14
+ * picture of where it ended is under the reply by then (see turnShots).
15
+ *
16
+ * The picture is a live capture of that one window, straight from Chromium
17
+ * (the desktop app's getUserMedia on a window source), not screenshots from
18
+ * the computer tools: each of those replaces the snapshot the agent's element
19
+ * references belong to, so polling them would keep invalidating the agent's
20
+ * work. It uses the Screen Recording grant OHarness already has.
21
+ *
22
+ * Presentation only. Nothing here is model input, and the card never drives
23
+ * the window; the person's own mouse does that.
24
+ *
25
+ * One part of the client script; see ./index.ts for how the parts join.
26
+ */
27
+ export const COMPUTER = String.raw `
28
+ const computerFloat = document.getElementById("computer-float");
29
+ const computerBody = document.getElementById("computer-body");
30
+ const computerWhere = document.getElementById("computer-where");
31
+ const computerLive = document.getElementById("computer-live");
32
+ const computerStopBtn = document.getElementById("computer-stop");
33
+ const computerGrowBtn = document.getElementById("computer-grow");
34
+ const computerHideBtn = document.getElementById("computer-hide");
35
+ const computerPill = document.getElementById("computer-pill");
36
+ const computerPillWhere = document.getElementById("computer-pill-where");
37
+
38
+ /**
39
+ * Per conversation: its last action, whether its turn is working in a window
40
+ * right now, and whether the person folded the card to a pill for it.
41
+ */
42
+ const computerViews = new Map();
43
+ let computerStream = null;
44
+ let computerStreamWindow = "";
45
+ let computerVideo = null;
46
+ let computerCursor = null;
47
+ let computerEmpty = null;
48
+ let computerLiveTimer = null;
49
+
50
+ function computerView(id) {
51
+ let view = computerViews.get(id);
52
+ if (!view) {
53
+ view = { event: null, live: false, folded: false };
54
+ computerViews.set(id, view);
55
+ }
56
+ return view;
57
+ }
58
+
59
+ function computerIsOpen() {
60
+ return !computerFloat.hidden;
61
+ }
62
+
63
+ /**
64
+ * Show the active conversation's card, its pill, or neither.
65
+ *
66
+ * The card is for a turn that is working in a window now. Folded, it is a
67
+ * pill that says where, one click from the card again. Anything else — no
68
+ * window yet, the turn over, another conversation — and both are gone.
69
+ */
70
+ function computerSync() {
71
+ const view = activeId && computerViews.get(activeId);
72
+ const hasWindow = Boolean(view && view.event && view.event.window);
73
+ const show = Boolean(IS_DESKTOP && !IS_EMBED && view && view.live && hasWindow);
74
+ const card = show && !view.folded;
75
+ computerFloat.hidden = !card;
76
+ computerPill.hidden = !(show && view.folded);
77
+ if (card) computerPaint();
78
+ else computerStopStream();
79
+ if (show) computerPillWhere.textContent = computerWhereText(view.event);
80
+ }
81
+
82
+ function computerWhereText(event) {
83
+ return (event.app || "") + (event.title ? " — " + event.title : "");
84
+ }
85
+
86
+ function computerStopStream() {
87
+ if (computerStream) for (const track of computerStream.getTracks()) track.stop();
88
+ computerStream = null;
89
+ computerStreamWindow = "";
90
+ if (computerVideo) computerVideo.srcObject = null;
91
+ }
92
+
93
+ function computerEnsureBody() {
94
+ if (computerVideo) return;
95
+ computerVideo = el("video", "computer-video");
96
+ computerVideo.muted = true;
97
+ computerVideo.autoplay = true;
98
+ computerVideo.playsInline = true;
99
+ computerCursor = el("div", "computer-cursor");
100
+ computerCursor.innerHTML = '<svg viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2l15 7-6.5 2L9 18z"/></svg>';
101
+ computerEmpty = el("div", "computer-empty");
102
+ computerBody.replaceChildren(computerVideo, computerCursor, computerEmpty);
103
+ computerVideo.addEventListener("loadedmetadata", () => {
104
+ // The card takes the window's own shape, so there are no bands beside a
105
+ // tall window or above a wide one.
106
+ if (computerVideo.videoWidth && computerVideo.videoHeight) {
107
+ computerBody.style.aspectRatio = computerVideo.videoWidth + " / " + computerVideo.videoHeight;
108
+ }
109
+ computerPlaceCursor();
110
+ });
111
+ new ResizeObserver(() => computerPlaceCursor()).observe(computerBody);
112
+ }
113
+
114
+ /**
115
+ * Capture one window as a video. Electron takes the same source ids its
116
+ * desktopCapturer lists, and a window's is its native id: the CGWindowID on
117
+ * macOS and the HWND on Windows, which is what the computer tools report.
118
+ */
119
+ async function computerAttach(windowId) {
120
+ if (computerStreamWindow === windowId && computerStream) return;
121
+ computerStopStream();
122
+ computerStreamWindow = windowId;
123
+ try {
124
+ const stream = await navigator.mediaDevices.getUserMedia({
125
+ audio: false,
126
+ video: {
127
+ mandatory: {
128
+ chromeMediaSource: "desktop",
129
+ chromeMediaSourceId: "window:" + windowId + ":0",
130
+ maxWidth: 1600,
131
+ maxHeight: 1200,
132
+ maxFrameRate: 12,
133
+ },
134
+ },
135
+ });
136
+ // The agent moved to another window, or the card went, while this one was being opened.
137
+ if (computerStreamWindow !== windowId || !computerIsOpen()) {
138
+ for (const track of stream.getTracks()) track.stop();
139
+ return;
140
+ }
141
+ computerStream = stream;
142
+ computerVideo.srcObject = stream;
143
+ computerEmpty.textContent = "";
144
+ computerEmpty.hidden = true;
145
+ // A closed or minimised window ends its track; say so instead of freezing.
146
+ for (const track of stream.getVideoTracks()) {
147
+ track.addEventListener("ended", () => {
148
+ if (computerStreamWindow !== windowId) return;
149
+ computerStream = null;
150
+ computerStreamWindow = "";
151
+ computerShowEmpty(t("computer.windowGone"));
152
+ });
153
+ }
154
+ } catch (err) {
155
+ computerStream = null;
156
+ computerShowEmpty(t("computer.noStream"));
157
+ }
158
+ }
159
+
160
+ function computerShowEmpty(text) {
161
+ computerEnsureBody();
162
+ computerEmpty.textContent = text;
163
+ computerEmpty.hidden = false;
164
+ computerCursor.classList.remove("on");
165
+ }
166
+
167
+ /**
168
+ * Put the agent's cursor where it acted, on the picture.
169
+ *
170
+ * The event carries a screen point and where the window was on screen; the
171
+ * video is that window, drawn letterboxed inside the card. Two proportions
172
+ * and an offset, and no need to know the display's scale on either side.
173
+ */
174
+ function computerPlaceCursor(ripple) {
175
+ const view = activeId && computerViews.get(activeId);
176
+ const event = view && view.event;
177
+ if (!computerVideo || !event || !event.windowFrame || !computerVideo.videoWidth) {
178
+ if (computerCursor) computerCursor.classList.remove("on");
179
+ return;
180
+ }
181
+ const point = event.point || (event.frame ? { x: event.frame.x + event.frame.w / 2, y: event.frame.y + event.frame.h / 2 } : null);
182
+ if (!point) return void computerCursor.classList.remove("on");
183
+ const wf = event.windowFrame;
184
+ const box = computerBody.getBoundingClientRect();
185
+ const scale = Math.min(box.width / computerVideo.videoWidth, box.height / computerVideo.videoHeight);
186
+ const drawnW = computerVideo.videoWidth * scale;
187
+ const drawnH = computerVideo.videoHeight * scale;
188
+ const left = (box.width - drawnW) / 2 + ((point.x - wf.x) / wf.w) * drawnW;
189
+ const top = (box.height - drawnH) / 2 + ((point.y - wf.y) / wf.h) * drawnH;
190
+ computerCursor.style.transform = "translate(" + left + "px," + top + "px)";
191
+ computerCursor.classList.add("on");
192
+ if (ripple) {
193
+ const ring = el("div", "computer-ring");
194
+ ring.style.left = left + "px";
195
+ ring.style.top = top + "px";
196
+ computerBody.appendChild(ring);
197
+ setTimeout(() => ring.remove(), 600);
198
+ }
199
+ }
200
+
201
+ function computerPaint() {
202
+ computerEnsureBody();
203
+ const view = activeId && computerViews.get(activeId);
204
+ const event = view && view.event;
205
+ if (!event || !event.window) return void computerShowEmpty("");
206
+ computerWhere.textContent = computerWhereText(event);
207
+ computerWhere.title = computerWhere.textContent;
208
+ void computerAttach(event.window);
209
+ computerPlaceCursor(false);
210
+ }
211
+
212
+ /**
213
+ * One action by one conversation.
214
+ *
215
+ * The first action in a window brings the card up; the end of the turn (idle)
216
+ * or the person stopping it (release) takes it away. Taking control is a
217
+ * banner on screen, not a change of window, so it moves nothing here.
218
+ */
219
+ function computerEvent(session, event) {
220
+ const view = computerView(session);
221
+ if (event.kind === "idle" || event.kind === "release") {
222
+ view.live = false;
223
+ if (session === activeId) computerSync();
224
+ return;
225
+ }
226
+ if (event.kind === "control") return;
227
+ const previous = view.event;
228
+ view.event = { ...event, window: event.window || (previous && previous.window), windowFrame: event.windowFrame || (previous && previous.windowFrame), title: event.title || (event.window && previous && previous.window === event.window ? previous.title : undefined) };
229
+ view.live = true;
230
+ if (session !== activeId) return;
231
+ computerSync();
232
+ if (!computerIsOpen()) return;
233
+ computerPlaceCursor(event.kind === "click" || event.kind === "type" || event.kind === "key");
234
+ computerLive.hidden = false;
235
+ if (computerLiveTimer) clearTimeout(computerLiveTimer);
236
+ computerLiveTimer = setTimeout(() => { computerLive.hidden = true; }, 6000);
237
+ }
238
+
239
+ /** Follow the active tab: its card if its turn is working in a window, else nothing. */
240
+ async function computerActivateSession() {
241
+ if (!activeId) return computerSync();
242
+ const view = computerView(activeId);
243
+ if (!view.event) {
244
+ const answer = await getJson("/computer-use/view?session=" + encodeURIComponent(activeId)).catch(() => null);
245
+ // The last thing it did, as the server kept it: a turn still working in a
246
+ // window was left mid-action, one that finished was left idle.
247
+ if (answer && answer.event) {
248
+ view.event = answer.event;
249
+ view.live = answer.event.kind !== "idle" && answer.event.kind !== "release";
250
+ }
251
+ }
252
+ computerSync();
253
+ }
254
+
255
+ /** From the composer's Computer use chip: the card again, if a turn is working in a window. */
256
+ function computerShow() {
257
+ if (!activeId) return;
258
+ computerView(activeId).folded = false;
259
+ computerSync();
260
+ }
261
+
262
+ // --- the card's own controls ------------------------------------------------
263
+
264
+ computerStopBtn.addEventListener("click", () => {
265
+ if (!activeId) return;
266
+ track("computer_stop", {});
267
+ void api("/computer-use/stop", { session: activeId });
268
+ });
269
+
270
+ // Fold to the pill, for this conversation. The pill brings it back.
271
+ computerHideBtn.addEventListener("click", () => {
272
+ if (!activeId) return;
273
+ computerView(activeId).folded = true;
274
+ computerSync();
275
+ });
276
+ computerPill.addEventListener("click", computerShow);
277
+
278
+ // Larger, for reading the window rather than glancing at it; the same button
279
+ // brings it back to the corner.
280
+ computerGrowBtn.addEventListener("click", () => {
281
+ const big = computerFloat.classList.toggle("big");
282
+ computerGrowBtn.setAttribute("aria-pressed", String(big));
283
+ computerKeepInside();
284
+ computerPlaceCursor(false);
285
+ });
286
+
287
+ /**
288
+ * Dragged from anywhere on it — it is all picture now, with no bar to hold —
289
+ * over the conversation, and remembered for the next time it comes up, in
290
+ * this browser only, which is where a corner that suits this screen belongs.
291
+ * Double-clicking it puts it back.
292
+ */
293
+ const COMPUTER_SPOT_KEY = "oh.computerSpot";
294
+ function computerKeepInside() {
295
+ const area = computerFloat.parentElement.getBoundingClientRect();
296
+ const box = computerFloat.getBoundingClientRect();
297
+ if (!computerFloat.style.left) return;
298
+ const left = Math.max(8, Math.min(parseFloat(computerFloat.style.left), area.width - box.width - 8));
299
+ const top = Math.max(8, Math.min(parseFloat(computerFloat.style.top), area.height - box.height - 8));
300
+ computerFloat.style.left = left + "px";
301
+ computerFloat.style.top = top + "px";
302
+ }
303
+ function computerPlace(spot) {
304
+ if (spot && typeof spot.left === "number" && typeof spot.top === "number") {
305
+ computerFloat.style.left = spot.left + "px";
306
+ computerFloat.style.top = spot.top + "px";
307
+ computerFloat.style.right = "auto";
308
+ } else {
309
+ computerFloat.style.left = "";
310
+ computerFloat.style.top = "";
311
+ computerFloat.style.right = "";
312
+ }
313
+ }
314
+ try {
315
+ computerPlace(JSON.parse(localStorage.getItem(COMPUTER_SPOT_KEY) || "null"));
316
+ } catch {
317
+ // No storage here: it starts in the corner.
318
+ }
319
+ computerFloat.addEventListener("pointerdown", (event) => {
320
+ if (event.button !== 0 || event.target.closest("button")) return;
321
+ const area = computerFloat.parentElement.getBoundingClientRect();
322
+ const box = computerFloat.getBoundingClientRect();
323
+ const dx = event.clientX - box.left;
324
+ const dy = event.clientY - box.top;
325
+ computerFloat.setPointerCapture(event.pointerId);
326
+ computerFloat.classList.add("dragging");
327
+ const move = (e) => {
328
+ const left = Math.max(8, Math.min(e.clientX - area.left - dx, area.width - box.width - 8));
329
+ const top = Math.max(8, Math.min(e.clientY - area.top - dy, area.height - box.height - 8));
330
+ computerPlace({ left, top });
331
+ };
332
+ const up = () => {
333
+ computerFloat.removeEventListener("pointermove", move);
334
+ computerFloat.removeEventListener("pointerup", up);
335
+ computerFloat.classList.remove("dragging");
336
+ try {
337
+ localStorage.setItem(COMPUTER_SPOT_KEY, JSON.stringify({ left: parseFloat(computerFloat.style.left), top: parseFloat(computerFloat.style.top) }));
338
+ } catch {
339
+ // Remembering is a convenience.
340
+ }
341
+ };
342
+ computerFloat.addEventListener("pointermove", move);
343
+ computerFloat.addEventListener("pointerup", up);
344
+ });
345
+ computerFloat.addEventListener("dblclick", (event) => {
346
+ if (event.target.closest("button")) return;
347
+ computerPlace(null);
348
+ try {
349
+ localStorage.removeItem(COMPUTER_SPOT_KEY);
350
+ } catch {
351
+ // Nothing was kept.
352
+ }
353
+ });
354
+ window.addEventListener("resize", () => {
355
+ if (computerIsOpen()) computerKeepInside();
356
+ });
357
+ `;
358
+ //# sourceMappingURL=computer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computer.js","sourceRoot":"","sources":["../../../src/page/client/computer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0UjC,CAAC"}
@@ -180,7 +180,10 @@ function paintAskHead(view, request) {
180
180
  // Which rule made the harness ask belongs with the other metadata, and in
181
181
  // the page's own language rather than the engine's English sentence.
182
182
  const rule = request.rule ? t("ask.rule", { rule: request.rule }) : request.reason;
183
- meta.textContent = view.meta.concat(rule ? [rule] : []).filter(Boolean).join(" · ");
183
+ // A subagent's card is shown in the conversation that started it; this is
184
+ // what says the call is the subagent's and not that conversation's own.
185
+ const from = request.from ? [t("ask.from", { from: request.from })] : [];
186
+ meta.textContent = from.concat(view.meta, rule ? [rule] : []).filter(Boolean).join(" · ");
184
187
  meta.hidden = meta.textContent === "";
185
188
  }
186
189
 
@@ -235,7 +238,29 @@ function renderAskBody() {
235
238
  askMore.hidden = askExpand.hidden && askRaw.hidden;
236
239
  }
237
240
 
241
+ /**
242
+ * Close events still owed for closes this page made itself.
243
+ *
244
+ * A dialog's close event arrives after close() — and not at all while the
245
+ * page is hidden, which is exactly when a background task's card tends to
246
+ * come in. So a close made here does its own clean-up at once and moves on to
247
+ * the next card, and its event, whenever it comes, is only counted off: taken
248
+ * for a dismissal it would refuse whichever card is up by then.
249
+ */
250
+ let askOwnCloses = 0;
251
+ /** Answered here; a replay of one from a reconnecting stream is not asked again. */
252
+ const askAnswered = new Set();
253
+
254
+ function closeAsk() {
255
+ if (!ask.open) return;
256
+ askOwnCloses++;
257
+ ask.close();
258
+ askState = null;
259
+ setTimeout(nextAsk, 0);
260
+ }
261
+
238
262
  function showAsk(event) {
263
+ if (!event.id || askAnswered.has(event.id)) return;
239
264
  if (pendingId === event.id || askQueue.some((queued) => queued.id === event.id)) return;
240
265
  if (pendingId) return void askQueue.push(event);
241
266
  pendingId = event.id;
@@ -258,7 +283,13 @@ function showAsk(event) {
258
283
  // The palette is a convenience; this is a turn waiting on an answer. Two
259
284
  // stacked modals would leave the deeper one unreachable behind the newer.
260
285
  if (paletteDialog.open) paletteDialog.close();
261
- if (!ask.open) ask.showModal();
286
+ // A dialog closed a moment ago may not be ready to open again; one more try
287
+ // shortly after rather than a card that never appears.
288
+ try {
289
+ if (!ask.open) ask.showModal();
290
+ } catch {
291
+ setTimeout(() => { if (!ask.open && pendingId) ask.showModal(); }, 50);
292
+ }
262
293
  ask.querySelector('[data-answer="once"]').focus();
263
294
  }
264
295
 
@@ -273,7 +304,7 @@ function withdrawAsk(id) {
273
304
  if (queued >= 0) askQueue.splice(queued, 1);
274
305
  if (pendingId !== id) return;
275
306
  pendingId = null;
276
- ask.close();
307
+ closeAsk();
277
308
  }
278
309
 
279
310
  askExpand.addEventListener("click", () => {
@@ -286,9 +317,18 @@ askRaw.addEventListener("click", () => {
286
317
  renderAskBody();
287
318
  });
288
319
 
320
+ // Escape: a dismissal is a refusal, decided at the key rather than when the
321
+ // close event gets round to it. Approval has to be an explicit act.
322
+ ask.addEventListener("cancel", (event) => {
323
+ event.preventDefault();
324
+ if (pendingId) answer(false, null);
325
+ closeAsk();
326
+ });
327
+
289
328
  ask.addEventListener("close", () => {
290
- // Escape, the backdrop, anything other than a button: a dismissal is a
291
- // refusal. Approval has to be an explicit act.
329
+ // One this page made: already dealt with in closeAsk.
330
+ if (askOwnCloses > 0) return void askOwnCloses--;
331
+ // Closed some other way: still a refusal.
292
332
  if (pendingId) answer(false, null);
293
333
  askState = null;
294
334
  // After the close has finished: showModal on a dialog still closing throws.
@@ -308,7 +348,7 @@ for (const button of ask.querySelectorAll("[data-answer]")) {
308
348
  button.addEventListener("click", () => {
309
349
  const choice = button.dataset.answer;
310
350
  answer(choice !== "no", choice === "session" || choice === "project" ? choice : null);
311
- ask.close();
351
+ closeAsk();
312
352
  });
313
353
 
314
354
  // What a scope actually costs, spelled out before it is chosen rather than
@@ -331,6 +371,7 @@ function answer(allow, remember) {
331
371
  const id = pendingId;
332
372
  pendingId = null;
333
373
  if (!id) return;
374
+ askAnswered.add(id);
334
375
  // Which way the answer went and how far it was remembered — never what was
335
376
  // being asked about, which is a command line or a path.
336
377
  track("permission_answered", { allow: allow, remember: remember || "once" });
@@ -720,5 +761,51 @@ function questionSettled(entry) {
720
761
  return nodes;
721
762
  }
722
763
 
764
+
765
+ /**
766
+ * A phone asking to drive this machine (remote.ts). Built on demand rather
767
+ * than declared in the page: most windows never see one. Both screens show
768
+ * the same six digits; the person compares them before allowing.
769
+ */
770
+ const pairDialogs = new Map();
771
+
772
+ function showPairing(event) {
773
+ if (pairDialogs.has(event.id)) return;
774
+ const dialog = el("dialog", "pair-dialog");
775
+ dialog.appendChild(el("div", "pair-title", t("remote.pair.title", { name: event.device && event.device.name || "" })));
776
+ dialog.appendChild(el("div", "pair-body", t("remote.pair.body")));
777
+ const code = String(event.code || "");
778
+ dialog.appendChild(el("div", "pair-code mono", code.slice(0, 3) + " " + code.slice(3)));
779
+ const actions = el("div", "dlg-actions");
780
+ const deny = el("button", "quiet", t("remote.pair.deny"));
781
+ const allow = el("button", "approve", t("remote.pair.allow"));
782
+ deny.type = allow.type = "button";
783
+ const answer = (value) => {
784
+ deny.disabled = allow.disabled = true;
785
+ void api("/remote/pair", { id: event.id, allow: value }).then(() => closePairing(event.id));
786
+ };
787
+ deny.onclick = () => answer(false);
788
+ allow.onclick = () => answer(true);
789
+ // Escape is a no, never a silent dismissal that leaves the phone waiting.
790
+ dialog.addEventListener("cancel", (e) => {
791
+ e.preventDefault();
792
+ answer(false);
793
+ });
794
+ actions.append(deny, allow);
795
+ dialog.appendChild(actions);
796
+ document.body.appendChild(dialog);
797
+ pairDialogs.set(event.id, dialog);
798
+ dialog.showModal();
799
+ deny.focus();
800
+ }
801
+
802
+ function closePairing(id) {
803
+ const dialog = pairDialogs.get(id);
804
+ if (!dialog) return;
805
+ pairDialogs.delete(id);
806
+ if (dialog.open) dialog.close();
807
+ dialog.remove();
808
+ }
809
+
723
810
  `;
724
811
  //# sourceMappingURL=dialogs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialogs.js","sourceRoot":"","sources":["../../../src/page/client/dialogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4sBhC,CAAC"}
1
+ {"version":3,"file":"dialogs.js","sourceRoot":"","sources":["../../../src/page/client/dialogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmyBhC,CAAC"}