@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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
291
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|