@oharness/server 0.9.3 → 0.11.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/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +500 -64
- package/dist/i18n.js.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +570 -27
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +55 -12
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +802 -59
- 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 +168 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +119 -9
- 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 +8 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +14 -10
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +432 -45
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +386 -104
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +278 -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/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +196 -8
- 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 +745 -76
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +94 -16
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +31 -0
- package/dist/prefs.js +13 -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 +1738 -73
- 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/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +28 -2
- package/dist/terminal.js +45 -3
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/turn-shots.d.ts +43 -0
- package/dist/turn-shots.js +80 -0
- package/dist/turn-shots.js.map +1 -0
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -22,55 +22,180 @@ async function newRoutine() {
|
|
|
22
22
|
input.setSelectionRange(input.value.length, input.value.length);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
/** A span of time as someone reads it: 42s, 3m 05s, 1h 12m. */
|
|
26
|
+
function runDuration(ms) {
|
|
27
|
+
const sec = Math.max(0, Math.round(ms / 1000));
|
|
28
|
+
if (sec < 60) return sec + "s";
|
|
29
|
+
const min = Math.floor(sec / 60);
|
|
30
|
+
if (min < 60) return min + "m " + String(sec % 60).padStart(2, "0") + "s";
|
|
31
|
+
return Math.floor(min / 60) + "h " + (min % 60) + "m";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The first line worth reading out of what a run said, or why it stopped. */
|
|
35
|
+
function runExcerpt(run) {
|
|
36
|
+
const text = run.error || run.output || "";
|
|
37
|
+
const line = text.split("\n").map((one) => one.replace(/^[#>*\-\s]+/, "").trim()).find(Boolean) || "";
|
|
38
|
+
return line.length > 160 ? line.slice(0, 159) + "…" : line;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
|
-
*
|
|
42
|
+
* What the page remembers across repaints: which histories are folded away
|
|
43
|
+
* and which runs are open. The panel is rebuilt whenever a run starts or
|
|
44
|
+
* ends, and losing what someone had opened each time would make it unusable
|
|
45
|
+
* while a routine is going.
|
|
46
|
+
*/
|
|
47
|
+
const foldedHistories = new Set();
|
|
48
|
+
const openRuns = new Set();
|
|
49
|
+
const runKey = (routine, run) => routine.name + "\0" + run.startedAt;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One run in the history: a status, when, how long, what it cost, and one
|
|
53
|
+
* line under it — what it is doing, while it runs; the start of its answer or
|
|
54
|
+
* its error, after. Clicking opens it in place to the whole of that text,
|
|
55
|
+
* with a way into its conversation.
|
|
56
|
+
*/
|
|
57
|
+
function routineRunRow(run, routine) {
|
|
58
|
+
const state = run.running ? "running" : run.skipped ? "skipped" : run.ok ? "ok" : "failed";
|
|
59
|
+
const key = runKey(routine, run);
|
|
60
|
+
const row = el("div", "routine-run " + state);
|
|
61
|
+
row.dataset.routine = routine.name;
|
|
62
|
+
const top = el("div", "routine-run-top");
|
|
63
|
+
top.appendChild(el("span", "run-status " + state, t("routines.status." + state)));
|
|
64
|
+
top.appendChild(el("span", null, new Date(run.startedAt).toLocaleString()));
|
|
65
|
+
const facts = el("span", "muted routine-run-facts");
|
|
66
|
+
if (run.running) {
|
|
67
|
+
const tick = () => {
|
|
68
|
+
if (!facts.isConnected && facts.dataset.painted) return clearInterval(timer);
|
|
69
|
+
facts.dataset.painted = "1";
|
|
70
|
+
const bits = [t("routines.elapsed", { d: runDuration(Date.now() - run.startedAt) })];
|
|
71
|
+
const turns = Number(row.dataset.turns || run.turns || 0);
|
|
72
|
+
if (turns) bits.push(t("routines.turns", { n: turns }));
|
|
73
|
+
facts.textContent = bits.join(" · ");
|
|
74
|
+
};
|
|
75
|
+
const timer = setInterval(tick, 1000);
|
|
76
|
+
tick();
|
|
77
|
+
} else if (!run.skipped) {
|
|
78
|
+
const bits = [runDuration(run.finishedAt - run.startedAt), t("routines.turns", { n: run.turns })];
|
|
79
|
+
if (run.costUsd) bits.push("$" + run.costUsd.toFixed(4));
|
|
80
|
+
if (run.catchUp) bits.push(t("routines.catchUp"));
|
|
81
|
+
facts.textContent = bits.join(" · ");
|
|
82
|
+
} else if (run.reason === "still-running") {
|
|
83
|
+
facts.textContent = t("routines.stillRunning");
|
|
84
|
+
}
|
|
85
|
+
top.appendChild(facts);
|
|
86
|
+
row.appendChild(top);
|
|
87
|
+
|
|
88
|
+
const full = run.running ? run.activity || "" : run.error || run.output || "";
|
|
89
|
+
const line = el("div", "routine-run-text" + (run.error ? " err" : " muted"),
|
|
90
|
+
run.running ? full || t("routines.starting") : runExcerpt(run));
|
|
91
|
+
row.appendChild(line);
|
|
92
|
+
if (!line.textContent) line.remove();
|
|
93
|
+
|
|
94
|
+
const detail = el("div", "routine-run-detail");
|
|
95
|
+
if (full && !run.running) {
|
|
96
|
+
// The answer as the conversation shows it; an error stays plain text.
|
|
97
|
+
const body = el("div", "routine-run-full" + (run.error ? " err" : " md"));
|
|
98
|
+
if (run.error) body.textContent = full;
|
|
99
|
+
else body.appendChild(renderMarkdown(full));
|
|
100
|
+
detail.appendChild(body);
|
|
101
|
+
}
|
|
102
|
+
if (run.sessionId) {
|
|
103
|
+
const open = el("button", "ghost small", t("routines.openRun"));
|
|
104
|
+
open.type = "button";
|
|
105
|
+
open.addEventListener("click", (event) => {
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
void openSession(run.sessionId, routine.cwd);
|
|
108
|
+
});
|
|
109
|
+
detail.appendChild(open);
|
|
110
|
+
}
|
|
111
|
+
if (!detail.childNodes.length) return row;
|
|
112
|
+
|
|
113
|
+
row.appendChild(detail);
|
|
114
|
+
row.tabIndex = 0;
|
|
115
|
+
row.setAttribute("role", "button");
|
|
116
|
+
const setOpen = (on) => {
|
|
117
|
+
row.classList.toggle("open", on);
|
|
118
|
+
row.setAttribute("aria-expanded", on ? "true" : "false");
|
|
119
|
+
if (on) openRuns.add(key); else openRuns.delete(key);
|
|
120
|
+
};
|
|
121
|
+
setOpen(openRuns.has(key));
|
|
122
|
+
row.addEventListener("click", (event) => {
|
|
123
|
+
// Selecting text in the full answer is reading it, not closing it.
|
|
124
|
+
if (String(getSelection() || "")) return;
|
|
125
|
+
if (event.target.closest(".routine-run-full") && row.classList.contains("open")) return;
|
|
126
|
+
setOpen(!row.classList.contains("open"));
|
|
127
|
+
});
|
|
128
|
+
row.addEventListener("keydown", (event) => {
|
|
129
|
+
if (event.target !== row || (event.key !== "Enter" && event.key !== " ")) return;
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
setOpen(!row.classList.contains("open"));
|
|
132
|
+
});
|
|
133
|
+
return row;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** How many runs a card shows before "Show all". */
|
|
137
|
+
const RUNS_SHOWN = 5;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A routine's runs, newest first, the one going now on top.
|
|
38
141
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* the
|
|
142
|
+
* Open unless folded: whether it ran, and what came of it, is what anyone
|
|
143
|
+
* opening this page wants to know. Repainted whenever a run starts or ends,
|
|
144
|
+
* and patched in place as a running one moves on, so a run started here is
|
|
145
|
+
* on the page at once and turns into its result when it finishes.
|
|
43
146
|
*/
|
|
44
147
|
function routineHistory(routine) {
|
|
45
|
-
const box = el("
|
|
46
|
-
|
|
148
|
+
const box = el("div", "routine-history");
|
|
149
|
+
const head = el("button", "routine-history-head muted small");
|
|
150
|
+
head.type = "button";
|
|
151
|
+
const label = el("span", null, t("routines.history"));
|
|
152
|
+
head.append(el("span", "chev", "▾"), label);
|
|
153
|
+
box.appendChild(head);
|
|
47
154
|
const list = el("div", "routine-runs");
|
|
48
155
|
box.appendChild(list);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
156
|
+
const fold = (on) => {
|
|
157
|
+
box.classList.toggle("folded", on);
|
|
158
|
+
head.setAttribute("aria-expanded", on ? "false" : "true");
|
|
159
|
+
if (on) foldedHistories.add(routine.name); else foldedHistories.delete(routine.name);
|
|
160
|
+
};
|
|
161
|
+
fold(foldedHistories.has(routine.name));
|
|
162
|
+
head.addEventListener("click", () => fold(!box.classList.contains("folded")));
|
|
163
|
+
|
|
164
|
+
list.appendChild(el("div", "muted small", t("routines.loading")));
|
|
165
|
+
void getJson("/routine/history?name=" + encodeURIComponent(routine.name)).then((body) => {
|
|
166
|
+
list.replaceChildren();
|
|
167
|
+
const runs = (body && body.runs) || [];
|
|
168
|
+
if (runs.length) label.textContent = t("routines.historyCount", { n: runs.filter((run) => !run.running).length });
|
|
169
|
+
if (!runs.length) return void list.appendChild(el("div", "muted small", t("routines.noRuns")));
|
|
170
|
+
const paint = (all) => {
|
|
54
171
|
list.replaceChildren();
|
|
55
|
-
const runs
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
line.appendChild(el("span", "muted", t("routines.stillRunning")));
|
|
62
|
-
}
|
|
63
|
-
if (run.sessionId) {
|
|
64
|
-
line.type = "button";
|
|
65
|
-
line.addEventListener("click", () => void openSession(run.sessionId, routine.cwd));
|
|
66
|
-
}
|
|
67
|
-
list.appendChild(line);
|
|
172
|
+
for (const run of all ? runs : runs.slice(0, RUNS_SHOWN)) list.appendChild(routineRunRow(run, routine));
|
|
173
|
+
if (runs.length > RUNS_SHOWN) {
|
|
174
|
+
const more = el("button", "ghost small routine-more", all ? t("routines.showFewer") : t("routines.showAll", { n: runs.length }));
|
|
175
|
+
more.type = "button";
|
|
176
|
+
more.addEventListener("click", () => paint(!all));
|
|
177
|
+
list.appendChild(more);
|
|
68
178
|
}
|
|
69
|
-
}
|
|
179
|
+
};
|
|
180
|
+
paint(false);
|
|
70
181
|
});
|
|
71
182
|
return box;
|
|
72
183
|
}
|
|
73
184
|
|
|
185
|
+
/** A running routine moved on: say so in its row, without a repaint. */
|
|
186
|
+
function paintRoutineActivity(event) {
|
|
187
|
+
for (const row of document.querySelectorAll(".routine-run.running")) {
|
|
188
|
+
if (row.dataset.routine !== event.name) continue;
|
|
189
|
+
row.dataset.turns = String(event.turns || 0);
|
|
190
|
+
let line = row.querySelector(".routine-run-text");
|
|
191
|
+
if (!line) {
|
|
192
|
+
line = el("div", "routine-run-text muted");
|
|
193
|
+
row.querySelector(".routine-run-top").after(line);
|
|
194
|
+
}
|
|
195
|
+
if (event.activity) line.textContent = event.activity;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
74
199
|
/**
|
|
75
200
|
* Scheduled runs, read from the scheduler rather than kept here.
|
|
76
201
|
*
|
|
@@ -103,6 +228,10 @@ async function showRoutines() {
|
|
|
103
228
|
const head = el("div", "row-head");
|
|
104
229
|
head.appendChild(el("span", "row-title", routine.name));
|
|
105
230
|
head.appendChild(el("span", "mono row-cron", routine.cron));
|
|
231
|
+
// What it runs on, and what it needs connected: the two things a run
|
|
232
|
+
// fails on without anyone there to fix them.
|
|
233
|
+
if (routine.model) head.appendChild(el("span", "muted small routine-where", routine.model.split("/").pop()));
|
|
234
|
+
if (routine.apps && routine.apps.length) head.appendChild(el("span", "muted small routine-where", routine.apps.join(", ")));
|
|
106
235
|
if (!routine.enabled) head.appendChild(el("span", "tag", t("routines.paused")));
|
|
107
236
|
// Where it runs. A routine is made in one project and runs there, from
|
|
108
237
|
// whichever project is open when its time comes, so the card says which.
|
|
@@ -112,8 +241,9 @@ async function showRoutines() {
|
|
|
112
241
|
head.appendChild(where);
|
|
113
242
|
}
|
|
114
243
|
|
|
115
|
-
const run = el("button", "ghost", t("routines.run"));
|
|
244
|
+
const run = el("button", "ghost", t(routine.running ? "routines.running" : "routines.run"));
|
|
116
245
|
run.type = "button";
|
|
246
|
+
run.disabled = Boolean(routine.running);
|
|
117
247
|
run.addEventListener("click", () => {
|
|
118
248
|
run.disabled = true;
|
|
119
249
|
run.textContent = t("routines.running");
|
|
@@ -145,8 +275,8 @@ async function showRoutines() {
|
|
|
145
275
|
|
|
146
276
|
const remove = el("button", "ghost", t("routines.remove"));
|
|
147
277
|
remove.type = "button";
|
|
148
|
-
remove.addEventListener("click", () => {
|
|
149
|
-
if (!
|
|
278
|
+
remove.addEventListener("click", async () => {
|
|
279
|
+
if (!(await askConfirm({ body: t("routines.removeConfirm", { name: routine.name }), ok: t("routines.remove"), danger: true }))) return;
|
|
150
280
|
remove.disabled = true;
|
|
151
281
|
void api("/routine/remove", { name: routine.name }).then((result) => {
|
|
152
282
|
pushNotice(result);
|
|
@@ -161,11 +291,9 @@ async function showRoutines() {
|
|
|
161
291
|
if (routine.enabled && routine.nextRunAt) {
|
|
162
292
|
bits.push(t("routines.next", { when: new Date(routine.nextRunAt).toLocaleString() }));
|
|
163
293
|
}
|
|
164
|
-
|
|
294
|
+
// The last run is the first line of the history below, so it is not
|
|
295
|
+
// repeated here.
|
|
165
296
|
if (bits.length) row.appendChild(el("div", "muted small", bits.join(" · ")));
|
|
166
|
-
if (routine.lastRun && routine.lastRun.error) {
|
|
167
|
-
row.appendChild(el("div", "err small", routine.lastRun.error));
|
|
168
|
-
}
|
|
169
297
|
row.appendChild(routineHistory(routine));
|
|
170
298
|
panel.appendChild(row);
|
|
171
299
|
}
|
|
@@ -187,7 +315,10 @@ async function showRoutines() {
|
|
|
187
315
|
* because a switch you have to open a row to reach is a switch that has been
|
|
188
316
|
* hidden rather than tidied.
|
|
189
317
|
*/
|
|
190
|
-
async function showSettings() {
|
|
318
|
+
async function showSettings(opts) {
|
|
319
|
+
// A row whose controls live inside it is reopened after a change redraws
|
|
320
|
+
// the page; called from a click handler, opts is the event, and opens none.
|
|
321
|
+
const opened = opts && typeof opts.open === "string" ? opts.open : undefined;
|
|
191
322
|
const settings = await getJson("/settings");
|
|
192
323
|
panelBody.replaceChildren();
|
|
193
324
|
if (settings.error) return void panelBody.appendChild(panelError(settings.error));
|
|
@@ -197,7 +328,7 @@ async function showSettings() {
|
|
|
197
328
|
|
|
198
329
|
// Rows are grouped by what they are about. Each group is filled as the page
|
|
199
330
|
// is built and drawn at the end under its heading, and only when it has rows.
|
|
200
|
-
const groups = { workspace: el("div"), sync: el("div"), memory: el("div"), activity: el("div"), advanced: el("div"), about: el("div") };
|
|
331
|
+
const groups = { workspace: el("div"), remote: el("div"), sync: el("div"), memory: el("div"), activity: el("div"), advanced: el("div"), about: el("div") };
|
|
201
332
|
let target = groups.workspace;
|
|
202
333
|
|
|
203
334
|
// A click on a button inside a summary toggles the disclosure as well as
|
|
@@ -311,6 +442,7 @@ async function showSettings() {
|
|
|
311
442
|
change.addEventListener("click", openProjectPicker);
|
|
312
443
|
field(t("settings.cwd"), settings.cwd || "—", t("settings.cwdHint"), change);
|
|
313
444
|
permissionsField(settings);
|
|
445
|
+
claudeAgentField(settings.claudeAgent || { on: false, available: false });
|
|
314
446
|
|
|
315
447
|
// A door, not a report: the numbers live on a page of their own, and the row
|
|
316
448
|
// here carries no total of its own because working one out means walking
|
|
@@ -460,6 +592,34 @@ async function showSettings() {
|
|
|
460
592
|
* and a "Show trail" you had to find after that was one click too many for
|
|
461
593
|
* the only thing anybody opens this row to see.
|
|
462
594
|
*/
|
|
595
|
+
/**
|
|
596
|
+
* Claude Agent: the user's own Claude Code, on their own Claude account, as
|
|
597
|
+
* a subagent and as @claude.
|
|
598
|
+
*
|
|
599
|
+
* Off until switched on here, because it spends someone's Claude plan and
|
|
600
|
+
* that is theirs to decide — the hint says so in so many words before the
|
|
601
|
+
* switch is flipped, not after. With no Claude Code on this machine the
|
|
602
|
+
* switch is shown but shut, and the row says where to get it.
|
|
603
|
+
*/
|
|
604
|
+
function claudeAgentField(status) {
|
|
605
|
+
const value = !status.available
|
|
606
|
+
? t("settings.claudeAgentMissingValue")
|
|
607
|
+
: t(status.on ? "settings.claudeAgentOnValue" : "settings.claudeAgentOffValue");
|
|
608
|
+
const body = field(
|
|
609
|
+
t("settings.claudeAgent"),
|
|
610
|
+
value,
|
|
611
|
+
t("settings.claudeAgentHint"),
|
|
612
|
+
toggle(status.on, t("settings.claudeAgent"), (next) => {
|
|
613
|
+
void api("/settings/claude-agent", { enabled: next }).then((result) => {
|
|
614
|
+
if (result && result.error) addNotice(result.error, "err");
|
|
615
|
+
void showSettings();
|
|
616
|
+
});
|
|
617
|
+
}, !status.available && !status.on),
|
|
618
|
+
);
|
|
619
|
+
if (!status.available) body.appendChild(el("div", "muted small", t("settings.claudeAgentMissing")));
|
|
620
|
+
else if (status.unavailable) body.appendChild(el("div", "muted small", status.unavailable));
|
|
621
|
+
}
|
|
622
|
+
|
|
463
623
|
function auditField(audit) {
|
|
464
624
|
const mine = audit.source !== "project";
|
|
465
625
|
|
|
@@ -664,7 +824,9 @@ async function showSettings() {
|
|
|
664
824
|
// a switch a browser tab cannot honour would be one that lies.
|
|
665
825
|
const keepAwake = await getJson("/keep-awake");
|
|
666
826
|
if (keepAwake && !keepAwake.error && keepAwake.available) {
|
|
667
|
-
|
|
827
|
+
// Set once, if ever: under Advanced, folded away with the other rows
|
|
828
|
+
// nobody comes to Settings to change.
|
|
829
|
+
target = groups.advanced;
|
|
668
830
|
field(
|
|
669
831
|
t("settings.keepAwake"),
|
|
670
832
|
t(keepAwake.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
@@ -674,6 +836,97 @@ async function showSettings() {
|
|
|
674
836
|
}),
|
|
675
837
|
);
|
|
676
838
|
}
|
|
839
|
+
// Remote control: a phone on this account driving this machine through the
|
|
840
|
+
// relay. Offered only where the server was given a relay to use, and off
|
|
841
|
+
// until somebody at the machine turns it on.
|
|
842
|
+
const remote = await getJson("/remote");
|
|
843
|
+
if (remote && !remote.error && remote.available) {
|
|
844
|
+
target = groups.remote;
|
|
845
|
+
const stateKey = (view) =>
|
|
846
|
+
view.state === "online" ? "online" : view.state === "connecting" ? "connecting" : view.state === "signed-out" ? "signedOut" : "off";
|
|
847
|
+
const body = field(
|
|
848
|
+
t("settings.remote"),
|
|
849
|
+
t("settings.remote." + stateKey(remote)),
|
|
850
|
+
t("settings.remoteHint"),
|
|
851
|
+
toggle(remote.on, t("settings.remote"), (next) => {
|
|
852
|
+
void api("/remote", { enabled: next }).then((result) => {
|
|
853
|
+
if (result && result.error) addNotice(result.error, "err");
|
|
854
|
+
void showSettings();
|
|
855
|
+
});
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
if (remote.keepsAwake) body.appendChild(el("div", "muted small", t("settings.remoteAwake")));
|
|
859
|
+
|
|
860
|
+
// Where it stands, said in a sentence: off, waiting on an account,
|
|
861
|
+
// reaching for the relay, or there.
|
|
862
|
+
const status = el("div", "setting-status small");
|
|
863
|
+
const dot = el("span", "status-dot");
|
|
864
|
+
const said = el("span");
|
|
865
|
+
status.append(dot, said);
|
|
866
|
+
body.appendChild(status);
|
|
867
|
+
let signIn = null;
|
|
868
|
+
if (typeof openConnect === "function") {
|
|
869
|
+
signIn = el("button", "ghost", t("settings.remoteSignIn"));
|
|
870
|
+
signIn.type = "button";
|
|
871
|
+
signIn.addEventListener("click", () => openConnect("official"));
|
|
872
|
+
body.appendChild(signIn);
|
|
873
|
+
}
|
|
874
|
+
const row = body.parentNode;
|
|
875
|
+
const shownValue = row.querySelector(".setting-value");
|
|
876
|
+
const paint = (view) => {
|
|
877
|
+
const key = stateKey(view);
|
|
878
|
+
dot.className = "status-dot" + (key === "online" ? " ok" : "");
|
|
879
|
+
said.textContent = t("settings.remoteStatus." + key);
|
|
880
|
+
if (shownValue) shownValue.textContent = t("settings.remote." + key);
|
|
881
|
+
if (signIn) signIn.hidden = key !== "signedOut";
|
|
882
|
+
};
|
|
883
|
+
paint(remote);
|
|
884
|
+
// Connecting and signing in finish on their own time, so the row looks
|
|
885
|
+
// again while it is on screen and not yet online.
|
|
886
|
+
if (remote.on) {
|
|
887
|
+
const timer = setInterval(() => {
|
|
888
|
+
if (!document.body.contains(row)) return clearInterval(timer);
|
|
889
|
+
void getJson("/remote").then((view) => {
|
|
890
|
+
if (!view || view.error || !document.body.contains(row)) return;
|
|
891
|
+
paint(view);
|
|
892
|
+
if (!view.on || view.state === "online") clearInterval(timer);
|
|
893
|
+
});
|
|
894
|
+
}, 3000);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// The phones allowed in so far. Forgetting one ends its connection now
|
|
898
|
+
// and sends it back to the Allow card next time.
|
|
899
|
+
const phones = el("div", "setting-list");
|
|
900
|
+
phones.appendChild(el("div", "muted small", t("settings.remotePhones")));
|
|
901
|
+
const devices = remote.devices || [];
|
|
902
|
+
if (!devices.length) phones.appendChild(el("div", "small", t("settings.remoteNoPhones")));
|
|
903
|
+
for (const device of devices) {
|
|
904
|
+
const line = el("div", "remote-phone small");
|
|
905
|
+
line.appendChild(el("span", "remote-phone-name", device.name || device.id));
|
|
906
|
+
if (device.pairedAt) {
|
|
907
|
+
line.appendChild(el("span", "muted", t("settings.remotePairedOn", { date: new Date(device.pairedAt).toLocaleDateString() })));
|
|
908
|
+
}
|
|
909
|
+
const forget = el("button", "ghost", t("settings.remoteForget"));
|
|
910
|
+
forget.type = "button";
|
|
911
|
+
forget.addEventListener("click", () => {
|
|
912
|
+
forget.disabled = true;
|
|
913
|
+
void api("/remote/revoke", { id: device.id }).then((result) => {
|
|
914
|
+
if (result && result.error) {
|
|
915
|
+
forget.disabled = false;
|
|
916
|
+
return void addNotice(result.error, "err");
|
|
917
|
+
}
|
|
918
|
+
addNotice(t("settings.remoteForgotten", { name: device.name || device.id }));
|
|
919
|
+
void showSettings();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
line.appendChild(forget);
|
|
923
|
+
phones.appendChild(line);
|
|
924
|
+
}
|
|
925
|
+
body.appendChild(phones);
|
|
926
|
+
}
|
|
927
|
+
// Computer use: only in the desktop app, which ships the helper. Read here,
|
|
928
|
+
// drawn after the browser row, its neighbour in kind.
|
|
929
|
+
const computer = await getJson("/computer-use/settings");
|
|
677
930
|
// Which browsers the agent may use. Three choices, so a segmented control
|
|
678
931
|
// rather than a switch; absent when the browser tools are off altogether,
|
|
679
932
|
// since then there is nothing for it to move.
|
|
@@ -707,6 +960,105 @@ async function showSettings() {
|
|
|
707
960
|
body.appendChild(status);
|
|
708
961
|
body.appendChild(extensionHelp(ext));
|
|
709
962
|
}
|
|
963
|
+
// Computer use, as one row: the row says how it gets switched on, and opens
|
|
964
|
+
// onto the three things a person owns about it — whether the agent may
|
|
965
|
+
// switch it on itself, whether it may ever take the real mouse and
|
|
966
|
+
// keyboard, and their own additions to the apps it refuses.
|
|
967
|
+
if (computer && !computer.error && computer.available) {
|
|
968
|
+
target = groups.workspace;
|
|
969
|
+
const enableModes = ["ask", "auto", "off"];
|
|
970
|
+
const enable = enableModes.includes(computer.enable) ? computer.enable : "ask";
|
|
971
|
+
const body = field(t("settings.computer"), t("settings.computerEnable." + enable), "");
|
|
972
|
+
const sub = (label, control, hint) => {
|
|
973
|
+
const part = el("div", "setting-part");
|
|
974
|
+
const line = el("div", "setting-part-line");
|
|
975
|
+
line.appendChild(el("div", "setting-part-name", label));
|
|
976
|
+
line.appendChild(control);
|
|
977
|
+
part.appendChild(line);
|
|
978
|
+
if (hint) part.appendChild(el("div", "muted small", hint));
|
|
979
|
+
body.appendChild(part);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const enableSeg = el("div", "seg setting-seg");
|
|
983
|
+
enableSeg.setAttribute("role", "radiogroup");
|
|
984
|
+
enableSeg.setAttribute("aria-label", t("settings.computerEnable"));
|
|
985
|
+
for (const mode of enableModes) {
|
|
986
|
+
const chosen = mode === enable;
|
|
987
|
+
const button = el("button", "seg-item" + (chosen ? " on" : ""), t("settings.computerEnable." + mode));
|
|
988
|
+
button.type = "button";
|
|
989
|
+
button.setAttribute("role", "radio");
|
|
990
|
+
button.setAttribute("aria-checked", chosen ? "true" : "false");
|
|
991
|
+
button.addEventListener("click", () => {
|
|
992
|
+
if (chosen) return;
|
|
993
|
+
enableSeg.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
994
|
+
void api("/computer-use/settings", { enable: mode }).then(() => showSettings({ open: "computer" }));
|
|
995
|
+
});
|
|
996
|
+
enableSeg.appendChild(button);
|
|
997
|
+
}
|
|
998
|
+
sub(t("settings.computerEnable"), enableSeg, t("settings.computerEnableHint." + enable));
|
|
999
|
+
|
|
1000
|
+
const takeover = computer.takeover !== "never";
|
|
1001
|
+
sub(
|
|
1002
|
+
t("settings.computerTakeover"),
|
|
1003
|
+
toggle(takeover, t("settings.computerTakeover"), (next) => {
|
|
1004
|
+
void api("/computer-use/settings", { takeover: next ? "ask" : "never" }).then(() => showSettings({ open: "computer" }));
|
|
1005
|
+
}),
|
|
1006
|
+
t("settings.computerTakeoverHint"),
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
const blocked = el("div", "setting-list setting-part");
|
|
1010
|
+
blocked.appendChild(el("div", "setting-part-name", t("settings.computerBlockedTitle")));
|
|
1011
|
+
blocked.appendChild(el("div", "muted small", t("settings.computerBlocked")));
|
|
1012
|
+
const box = el("textarea", "boxed");
|
|
1013
|
+
box.value = (computer.blockedApps || []).join("\n");
|
|
1014
|
+
box.rows = Math.max(2, (computer.blockedApps || []).length + 1);
|
|
1015
|
+
box.spellcheck = false;
|
|
1016
|
+
box.style.width = "100%";
|
|
1017
|
+
box.placeholder = t("settings.computerBlockedPlaceholder");
|
|
1018
|
+
blocked.appendChild(box);
|
|
1019
|
+
const save = el("button", "ghost", t("perms.save"));
|
|
1020
|
+
save.type = "button";
|
|
1021
|
+
save.addEventListener("click", () => {
|
|
1022
|
+
save.disabled = true;
|
|
1023
|
+
const apps = box.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1024
|
+
void api("/computer-use/settings", { blockedApps: apps }).then((result) => {
|
|
1025
|
+
save.disabled = false;
|
|
1026
|
+
if (result && result.error) return void addNotice(result.error, "err");
|
|
1027
|
+
addNotice(t("perms.saved"));
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
blocked.appendChild(save);
|
|
1031
|
+
body.appendChild(blocked);
|
|
1032
|
+
body.parentElement.dataset.row = "computer";
|
|
1033
|
+
if (opened === "computer") body.parentElement.open = true;
|
|
1034
|
+
}
|
|
1035
|
+
// One switch for both: the pictures of what a turn left in the browser and
|
|
1036
|
+
// on the computer, under its reply. Shown wherever either can run.
|
|
1037
|
+
const shots = await getJson("/turn-shots");
|
|
1038
|
+
if (shots && !shots.error && (browserModes.includes(settings.browserMode) || (computer && computer.available))) {
|
|
1039
|
+
target = groups.advanced;
|
|
1040
|
+
field(
|
|
1041
|
+
t("settings.turnShots"),
|
|
1042
|
+
t(shots.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
1043
|
+
t("settings.turnShotsHint"),
|
|
1044
|
+
toggle(shots.enabled, t("settings.turnShots"), (next) => {
|
|
1045
|
+
void api("/turn-shots", { enabled: next }).then(showSettings);
|
|
1046
|
+
}),
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
// The grey next message in the empty composer, written after each turn.
|
|
1050
|
+
const suggestions = await getJson("/suggestions");
|
|
1051
|
+
if (suggestions && !suggestions.error) {
|
|
1052
|
+
target = groups.workspace;
|
|
1053
|
+
field(
|
|
1054
|
+
t("settings.suggestions"),
|
|
1055
|
+
t(suggestions.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
1056
|
+
t("settings.suggestionsHint"),
|
|
1057
|
+
toggle(suggestions.enabled, t("settings.suggestions"), (next) => {
|
|
1058
|
+
void api("/suggestions", { enabled: next }).then(showSettings);
|
|
1059
|
+
}),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
710
1062
|
// The desktop app's version, with its release notes beside it. A row that
|
|
711
1063
|
// is always there, because the menu bar is not where people look and the
|
|
712
1064
|
// post-update hint is gone after one click.
|
|
@@ -726,6 +1078,7 @@ async function showSettings() {
|
|
|
726
1078
|
}
|
|
727
1079
|
const sections = [
|
|
728
1080
|
["settings.section.workspace", [groups.workspace]],
|
|
1081
|
+
["settings.section.remote", [groups.remote]],
|
|
729
1082
|
["settings.section.sync", [groups.sync, groups.memory]],
|
|
730
1083
|
["settings.section.activity", [groups.activity]],
|
|
731
1084
|
["settings.section.advanced", [groups.advanced]],
|
|
@@ -733,12 +1086,46 @@ async function showSettings() {
|
|
|
733
1086
|
];
|
|
734
1087
|
for (const [title, parts] of sections) {
|
|
735
1088
|
if (!parts.some((part) => part.childNodes.length)) continue;
|
|
1089
|
+
if (title === "settings.section.advanced") {
|
|
1090
|
+
panel.appendChild(advancedSection(parts));
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
736
1093
|
panel.appendChild(el("div", "section-title", t(title)));
|
|
737
1094
|
for (const part of parts) panel.appendChild(part);
|
|
738
1095
|
}
|
|
739
1096
|
panelBody.appendChild(panel);
|
|
740
1097
|
}
|
|
741
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Advanced: the rows nobody opens Settings to change, folded under their
|
|
1101
|
+
* heading so the ones people do are the page. Shut until opened, and then
|
|
1102
|
+
* open on every visit in this browser until shut again — a person who went
|
|
1103
|
+
* looking once is likely to go looking again.
|
|
1104
|
+
*/
|
|
1105
|
+
const ADVANCED_OPEN_KEY = "oh.settings.advancedOpen";
|
|
1106
|
+
|
|
1107
|
+
function advancedSection(parts) {
|
|
1108
|
+
const section = el("details", "settings-advanced");
|
|
1109
|
+
const head = el("summary", "section-title");
|
|
1110
|
+
head.appendChild(el("span", "chev"));
|
|
1111
|
+
head.appendChild(document.createTextNode(t("settings.section.advanced")));
|
|
1112
|
+
section.appendChild(head);
|
|
1113
|
+
for (const part of parts) section.appendChild(part);
|
|
1114
|
+
try {
|
|
1115
|
+
section.open = localStorage.getItem(ADVANCED_OPEN_KEY) === "1";
|
|
1116
|
+
} catch {
|
|
1117
|
+
// Storage refused: shut, as on a first visit.
|
|
1118
|
+
}
|
|
1119
|
+
section.addEventListener("toggle", () => {
|
|
1120
|
+
try {
|
|
1121
|
+
localStorage.setItem(ADVANCED_OPEN_KEY, section.open ? "1" : "0");
|
|
1122
|
+
} catch {
|
|
1123
|
+
// Not remembered, which costs one click next time.
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
return section;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
742
1129
|
/**
|
|
743
1130
|
* Where the money went, and on which model.
|
|
744
1131
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFA4xCgD,CAAC"}
|