@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
|
@@ -8,6 +8,11 @@ export const SHELL = String.raw `
|
|
|
8
8
|
|
|
9
9
|
const projectBtn = document.getElementById("project");
|
|
10
10
|
let projects = [];
|
|
11
|
+
/** The server's home directory, which the picker writes as ~. */
|
|
12
|
+
let projectHome = "";
|
|
13
|
+
/** Session directories that are worktrees, each naming the project it came from. */
|
|
14
|
+
let worktrees = [];
|
|
15
|
+
const worktreeAt = (cwd) => worktrees.find((one) => one.path === cwd);
|
|
11
16
|
|
|
12
17
|
/** The last path segment, which is what the directory is called. */
|
|
13
18
|
function projectName(dir) {
|
|
@@ -25,6 +30,7 @@ function paintProject() {
|
|
|
25
30
|
// it too, or it goes on advertising the tree the user just left.
|
|
26
31
|
const cwdEl = document.getElementById("empty-cwd");
|
|
27
32
|
if (cwdEl && active) cwdEl.textContent = active.name;
|
|
33
|
+
if (active) void paintStarters(active.path);
|
|
28
34
|
// A directory added while the chip was open belongs on its list.
|
|
29
35
|
refreshWorkspace();
|
|
30
36
|
}
|
|
@@ -50,6 +56,8 @@ function repaintPanel() {
|
|
|
50
56
|
async function loadProjects() {
|
|
51
57
|
const body = await getJson("/projects");
|
|
52
58
|
projects = body.projects || [];
|
|
59
|
+
projectHome = body.home || "";
|
|
60
|
+
worktrees = body.worktrees || [];
|
|
53
61
|
projectOrder = body.order || [];
|
|
54
62
|
paintProject();
|
|
55
63
|
// A project's pin and its place decide how the session list is grouped, so
|
|
@@ -59,9 +67,9 @@ async function loadProjects() {
|
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
async function renameProjectUi(cwd, name) {
|
|
62
|
-
const next =
|
|
63
|
-
if (!next || next
|
|
64
|
-
const body = await api("/project", { path: cwd, rename: next
|
|
70
|
+
const next = await askText({ title: t("projects.rename"), body: t("projects.renamePrompt"), value: name, ok: t("dialog.save") });
|
|
71
|
+
if (!next || next === name) return;
|
|
72
|
+
const body = await api("/project", { path: cwd, rename: next });
|
|
65
73
|
if (body && body.error) return void addNotice(body.error, "err");
|
|
66
74
|
addNotice(t("projects.renamed", { name: body.project.name }));
|
|
67
75
|
// Paths key tabs, local open/closed state and drag order. Reloading after
|
|
@@ -76,73 +84,124 @@ function exportProjectUi(cwd) {
|
|
|
76
84
|
link.click();
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/** A project's directory as people say it: under home, from ~. */
|
|
88
|
+
function projectDir(path) {
|
|
89
|
+
const home = projectHome.replace(/[\\/]+$/, "");
|
|
90
|
+
if (home && (path === home || path.startsWith(home + "/") || path.startsWith(home + "\\"))) return "~" + path.slice(home.length);
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function openProjectPicker() {
|
|
80
95
|
const dialog = document.getElementById("project-picker");
|
|
81
96
|
const field = document.getElementById("proj-search");
|
|
82
97
|
const listEl = document.getElementById("proj-list");
|
|
83
98
|
field.placeholder = t("projects.search");
|
|
84
99
|
|
|
85
|
-
|
|
100
|
+
// The row menu lives in <body>, which a modal dialog makes inert: while the
|
|
101
|
+
// picker is open the menu is moved inside it, and put back when it closes.
|
|
102
|
+
const menuHome = rowMenu.parentNode;
|
|
103
|
+
dialog.appendChild(rowMenu);
|
|
104
|
+
dialog.addEventListener("close", () => {
|
|
105
|
+
closeRowMenu();
|
|
106
|
+
menuHome.appendChild(rowMenu);
|
|
107
|
+
}, { once: true });
|
|
108
|
+
|
|
109
|
+
/** The rows that open a project, in order, and which one Enter opens. */
|
|
110
|
+
let rows = [];
|
|
111
|
+
let active = 0;
|
|
112
|
+
const light = (index, scroll) => {
|
|
113
|
+
if (!rows.length) return;
|
|
114
|
+
active = (index + rows.length) % rows.length;
|
|
115
|
+
rows.forEach((row, i) => row.parentNode.classList.toggle("active", i === active));
|
|
116
|
+
if (scroll) rows[active].scrollIntoView({ block: "nearest" });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const menuFor = (project, more, line) => {
|
|
120
|
+
const items = [
|
|
121
|
+
menuItem(t("projects.rename"), {
|
|
122
|
+
key: "r",
|
|
123
|
+
run: () => {
|
|
124
|
+
dialog.close();
|
|
125
|
+
void renameProjectUi(project.path, project.name);
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
menuItem(t("projects.export"), {
|
|
129
|
+
key: "e",
|
|
130
|
+
run: () => {
|
|
131
|
+
closeRowMenu();
|
|
132
|
+
exportProjectUi(project.path);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
];
|
|
136
|
+
// Not offered for the project this window is standing in: the next
|
|
137
|
+
// switch calls addProject and puts it straight back, so the control
|
|
138
|
+
// would appear to do nothing. Nothing recorded is lost either way —
|
|
139
|
+
// this forgets a path, and the sessions stay where they are.
|
|
140
|
+
if (!project.current) {
|
|
141
|
+
items.push(
|
|
142
|
+
menuSep(),
|
|
143
|
+
menuItem(t("projects.forget"), {
|
|
144
|
+
danger: true,
|
|
145
|
+
run: () => {
|
|
146
|
+
closeRowMenu();
|
|
147
|
+
void api("/project", { path: project.path, remove: true }).then(async (body) => {
|
|
148
|
+
if (body && body.error) return void addNotice(body.error, "err");
|
|
149
|
+
await loadProjects();
|
|
150
|
+
paint();
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
openRowMenu(more, line, items);
|
|
157
|
+
};
|
|
158
|
+
|
|
86
159
|
const paint = () => {
|
|
87
160
|
const query = field.value.trim().toLowerCase();
|
|
88
161
|
listEl.replaceChildren();
|
|
89
|
-
|
|
162
|
+
rows = [];
|
|
163
|
+
let here = 0;
|
|
90
164
|
for (const project of projects) {
|
|
91
165
|
if (query && !(project.name + " " + project.path).toLowerCase().includes(query)) continue;
|
|
166
|
+
const line = el("div", "proj-row" + (project.current ? " current" : ""));
|
|
92
167
|
const row = el("button", "proj-item");
|
|
93
168
|
row.type = "button";
|
|
169
|
+
row.title = project.path;
|
|
94
170
|
row.appendChild(el("span", "avatar", project.name.slice(0, 1).toUpperCase()));
|
|
95
|
-
|
|
171
|
+
const text = el("span", "proj-text");
|
|
172
|
+
text.appendChild(el("span", "path", project.name));
|
|
173
|
+
text.appendChild(el("span", "proj-dir", projectDir(project.path)));
|
|
174
|
+
row.appendChild(text);
|
|
96
175
|
if (project.missing) row.appendChild(el("span", "tag", t("projects.missing")));
|
|
97
|
-
|
|
176
|
+
const tick = el("span", "proj-check");
|
|
177
|
+
if (project.current) tick.appendChild(svgIcon("check"));
|
|
178
|
+
row.appendChild(tick);
|
|
98
179
|
row.addEventListener("click", () => {
|
|
99
180
|
dialog.close();
|
|
100
181
|
if (!project.current) void chooseProject(project.path);
|
|
101
182
|
});
|
|
102
|
-
|
|
103
|
-
|
|
183
|
+
const index = rows.length;
|
|
184
|
+
row.addEventListener("mousemove", () => { if (active !== index) light(index); });
|
|
104
185
|
line.appendChild(row);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
void renameProjectUi(project.path, project.name);
|
|
113
|
-
});
|
|
114
|
-
line.appendChild(rename);
|
|
115
|
-
const archive = el("button", "proj-forget", "↓");
|
|
116
|
-
archive.type = "button";
|
|
117
|
-
archive.title = t("projects.export");
|
|
118
|
-
archive.setAttribute("aria-label", t("projects.export") + ": " + project.name);
|
|
119
|
-
archive.addEventListener("click", (e) => {
|
|
186
|
+
|
|
187
|
+
const more = el("button", "proj-more");
|
|
188
|
+
more.type = "button";
|
|
189
|
+
more.title = t("session.more");
|
|
190
|
+
more.setAttribute("aria-label", t("session.more") + ": " + project.name);
|
|
191
|
+
more.appendChild(svgIcon("more"));
|
|
192
|
+
more.addEventListener("click", (e) => {
|
|
120
193
|
e.stopPropagation();
|
|
121
|
-
|
|
194
|
+
if (menuRow === line) return closeRowMenu();
|
|
195
|
+
menuFor(project, more, line);
|
|
122
196
|
});
|
|
123
|
-
line.appendChild(
|
|
124
|
-
// Not offered for the project this window is standing in: the next
|
|
125
|
-
// switch calls addProject and puts it straight back, so the control
|
|
126
|
-
// would appear to do nothing. Nothing recorded is lost either way —
|
|
127
|
-
// this forgets a path, and the sessions stay where they are.
|
|
128
|
-
if (!project.current) {
|
|
129
|
-
const forget = el("button", "proj-forget", "✕");
|
|
130
|
-
forget.type = "button";
|
|
131
|
-
forget.title = t("projects.forget");
|
|
132
|
-
forget.setAttribute("aria-label", t("projects.forget") + ": " + project.name);
|
|
133
|
-
forget.addEventListener("click", (e) => {
|
|
134
|
-
e.stopPropagation();
|
|
135
|
-
void api("/project", { path: project.path, remove: true }).then(async (body) => {
|
|
136
|
-
if (body && body.error) return void addNotice(body.error, "err");
|
|
137
|
-
await loadProjects();
|
|
138
|
-
paint();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
line.appendChild(forget);
|
|
142
|
-
}
|
|
197
|
+
line.appendChild(more);
|
|
143
198
|
listEl.appendChild(line);
|
|
144
|
-
|
|
199
|
+
if (project.current && !query) here = rows.length;
|
|
200
|
+
rows.push(row);
|
|
145
201
|
}
|
|
202
|
+
if (!rows.length) listEl.appendChild(el("div", "proj-empty", t("projects.none")));
|
|
203
|
+
// Searching starts from the first match; an empty query from where you are.
|
|
204
|
+
light(here, true);
|
|
146
205
|
};
|
|
147
206
|
|
|
148
207
|
const add = document.getElementById("proj-add");
|
|
@@ -153,20 +212,38 @@ function openProjectPicker() {
|
|
|
153
212
|
};
|
|
154
213
|
|
|
155
214
|
field.value = "";
|
|
156
|
-
field.oninput =
|
|
157
|
-
|
|
158
|
-
|
|
215
|
+
field.oninput = () => {
|
|
216
|
+
closeRowMenu();
|
|
217
|
+
paint();
|
|
218
|
+
};
|
|
219
|
+
// A list with a search box above it promises the keys of one: arrows move
|
|
220
|
+
// through what matched, Enter opens it, Escape shuts a menu before the picker.
|
|
159
221
|
field.onkeydown = (e) => {
|
|
160
|
-
if (e.
|
|
161
|
-
e.
|
|
162
|
-
|
|
222
|
+
if (e.isComposing) return;
|
|
223
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
closeRowMenu();
|
|
226
|
+
light(active + (e.key === "ArrowDown" ? 1 : -1), true);
|
|
227
|
+
} else if (e.key === "Enter" && rows[active]) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
rows[active].click();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
dialog.onkeydown = (e) => {
|
|
233
|
+
if (e.key === "Escape" && !rowMenu.hidden) {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
e.stopPropagation();
|
|
236
|
+
closeRowMenu();
|
|
237
|
+
field.focus();
|
|
238
|
+
}
|
|
163
239
|
};
|
|
164
240
|
paint();
|
|
165
241
|
dialog.showModal();
|
|
166
242
|
field.focus();
|
|
243
|
+
// Again now that it has a size: before showModal there is nothing to scroll.
|
|
244
|
+
light(active, true);
|
|
167
245
|
}
|
|
168
246
|
|
|
169
|
-
|
|
170
247
|
/**
|
|
171
248
|
* A directory browser, for hosts with no folder dialog.
|
|
172
249
|
*
|
|
@@ -294,6 +371,12 @@ const IS_WIN = /win/i.test(navigator.platform || navigator.userAgent || "");
|
|
|
294
371
|
/** The rows the current query left on screen, in the order they are drawn. */
|
|
295
372
|
let paletteRows = [];
|
|
296
373
|
let paletteCursor = 0;
|
|
374
|
+
/**
|
|
375
|
+
* Whether the palette was opened from the sidebar's "Search sessions": then it
|
|
376
|
+
* lists sessions and nothing else, and every one of them rather than the
|
|
377
|
+
* recent eight — the question asked was "where is that conversation".
|
|
378
|
+
*/
|
|
379
|
+
let paletteSessionsOnly = false;
|
|
297
380
|
|
|
298
381
|
/** A nav item's own icon, so the palette names things the way the sidebar does. */
|
|
299
382
|
function navIcon(id) {
|
|
@@ -315,6 +398,7 @@ const enLabel = (key) => (STRINGS.en && STRINGS.en[key]) || "";
|
|
|
315
398
|
function paletteItems() {
|
|
316
399
|
const items = [];
|
|
317
400
|
const query = paletteField.value.trim();
|
|
401
|
+
if (paletteSessionsOnly) return sessionItems(sessionRows);
|
|
318
402
|
const actions = t("palette.actions");
|
|
319
403
|
const here = (projects.find((project) => project.current) || {}).path || currentCwd;
|
|
320
404
|
|
|
@@ -342,6 +426,7 @@ function paletteItems() {
|
|
|
342
426
|
for (const [id, key] of [
|
|
343
427
|
["nav-artifacts", "nav.artifacts"],
|
|
344
428
|
["nav-sessions", "nav.sessions"],
|
|
429
|
+
["nav-usage", "nav.usage"],
|
|
345
430
|
["nav-models", "nav.models"],
|
|
346
431
|
["nav-routines", "nav.routines"],
|
|
347
432
|
["nav-settings", "nav.settings"],
|
|
@@ -359,21 +444,7 @@ function paletteItems() {
|
|
|
359
444
|
// Unfiltered, the list is a preview of the most recent work and the actions
|
|
360
445
|
// above it still have to be visible. Once there is a query, every session in
|
|
361
446
|
// every tree is fair game — that is what the field is for.
|
|
362
|
-
|
|
363
|
-
for (const session of query ? sessionRows : sessionRows.slice(0, 8)) {
|
|
364
|
-
items.push({
|
|
365
|
-
kind: "session",
|
|
366
|
-
group: sessions,
|
|
367
|
-
icon: () => svgIcon("chat"),
|
|
368
|
-
label: session.title || t("session.untitled"),
|
|
369
|
-
sub: [projectName(session.cwd || ""), ago(session.updatedAt)].filter(Boolean).join(" · "),
|
|
370
|
-
keywords: session.cwd || "",
|
|
371
|
-
run: () => {
|
|
372
|
-
if (session.current) return void setNav(null);
|
|
373
|
-
void openSession(session.id, session.cwd);
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
}
|
|
447
|
+
items.push(...sessionItems(query ? sessionRows : sessionRows.slice(0, 8)));
|
|
377
448
|
|
|
378
449
|
const named = t("palette.projects");
|
|
379
450
|
for (const project of query ? projects : projects.slice(0, 5)) {
|
|
@@ -394,6 +465,23 @@ function paletteItems() {
|
|
|
394
465
|
return items;
|
|
395
466
|
}
|
|
396
467
|
|
|
468
|
+
/** Palette rows for sessions, each reopening its own transcript. */
|
|
469
|
+
function sessionItems(rows) {
|
|
470
|
+
const group = t("palette.sessions");
|
|
471
|
+
return rows.map((session) => ({
|
|
472
|
+
kind: "session",
|
|
473
|
+
group,
|
|
474
|
+
icon: () => svgIcon("chat"),
|
|
475
|
+
label: session.title || t("session.untitled"),
|
|
476
|
+
sub: [projectName(session.cwd || ""), ago(session.updatedAt)].filter(Boolean).join(" · "),
|
|
477
|
+
keywords: session.cwd || "",
|
|
478
|
+
run: () => {
|
|
479
|
+
if (session.current) return void setNav(null);
|
|
480
|
+
void openSession(session.id, session.cwd);
|
|
481
|
+
},
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
|
|
397
485
|
/** Same word-by-word match the pickers use, over the row's hidden aliases too. */
|
|
398
486
|
const paletteMatch = (item, query) =>
|
|
399
487
|
matchesQuery(
|
|
@@ -454,12 +542,14 @@ function movePalette(step) {
|
|
|
454
542
|
if (active) active.scrollIntoView({ block: "nearest" });
|
|
455
543
|
}
|
|
456
544
|
|
|
457
|
-
function openPalette() {
|
|
545
|
+
function openPalette(sessionsOnly = false) {
|
|
458
546
|
// One modal at a time. A permission request is a question the agent is
|
|
459
547
|
// blocked on; a palette over it would answer neither.
|
|
460
548
|
if (document.querySelector("dialog[open]")) return;
|
|
461
549
|
closeAttachMenu();
|
|
462
550
|
closeRowMenu();
|
|
551
|
+
paletteSessionsOnly = sessionsOnly === true;
|
|
552
|
+
paletteField.placeholder = t(paletteSessionsOnly ? "palette.sessionsPlaceholder" : "palette.placeholder");
|
|
463
553
|
paletteField.value = "";
|
|
464
554
|
paletteCursor = 0;
|
|
465
555
|
paintPalette();
|
|
@@ -471,7 +561,7 @@ function openPalette() {
|
|
|
471
561
|
// The desktop shell reaches the palette through this: a menu accelerator is
|
|
472
562
|
// handled before the page ever sees the keystroke, so the menu item calls the
|
|
473
563
|
// same function the key would have.
|
|
474
|
-
window.openCommandPalette = openPalette;
|
|
564
|
+
window.openCommandPalette = () => openPalette();
|
|
475
565
|
|
|
476
566
|
paletteField.addEventListener("input", () => {
|
|
477
567
|
paletteCursor = 0;
|
|
@@ -520,7 +610,8 @@ document.addEventListener("keydown", (e) => {
|
|
|
520
610
|
openPalette();
|
|
521
611
|
});
|
|
522
612
|
|
|
523
|
-
document.getElementById("nav-search").addEventListener("click", openPalette);
|
|
613
|
+
document.getElementById("nav-search").addEventListener("click", () => openPalette());
|
|
614
|
+
document.getElementById("side-search").addEventListener("click", () => openPalette(true));
|
|
524
615
|
// The chord as this machine actually spells it, in the one place a mouse user
|
|
525
616
|
// will see it.
|
|
526
617
|
document.getElementById("palette-hint").textContent = IS_MAC ? "⌘K" : "Ctrl K";
|
|
@@ -586,41 +677,107 @@ rail.addEventListener("focusout", hideRailTip);
|
|
|
586
677
|
rail.addEventListener("click", hideRailTip);
|
|
587
678
|
|
|
588
679
|
/**
|
|
589
|
-
* What the empty state offers to do
|
|
680
|
+
* What the empty state offers to do.
|
|
590
681
|
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* "…prompt" string; the label is what fits on
|
|
596
|
-
* a chip, the prompt is the sentence the chip actually sends.
|
|
682
|
+
* The server decides which kind: a project with no memory gets a rotating pool
|
|
683
|
+
* of everyday offers, and one the agent has notes on gets three written from
|
|
684
|
+
* those notes (see ../../starters.ts). Each chip's label is what fits on it,
|
|
685
|
+
* and the sentence it actually sends rides along in data-p.
|
|
597
686
|
*/
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
687
|
+
const FIXED_STARTERS = ["trip", "email", "research", "savings", "japanTrip"];
|
|
688
|
+
const VISIBLE_STARTERS = 3;
|
|
689
|
+
const STARTER_ROTATION_MS = 6000;
|
|
690
|
+
/** Which project the chips on screen were drawn for, and the newest ask. */
|
|
691
|
+
let startersFor = "";
|
|
692
|
+
let startersAsk = 0;
|
|
693
|
+
let starterOffset = 0;
|
|
694
|
+
let starterTimer;
|
|
695
|
+
|
|
696
|
+
function starterChip(label, prompt) {
|
|
697
|
+
const button = el("button", "", label);
|
|
698
|
+
button.type = "button";
|
|
699
|
+
button.dataset.p = prompt;
|
|
700
|
+
// The label is a chip; the title is the sentence it will actually send.
|
|
701
|
+
button.title = prompt;
|
|
702
|
+
return button;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function animateStarters(box) {
|
|
706
|
+
box.classList.remove("fresh");
|
|
707
|
+
void box.offsetWidth;
|
|
708
|
+
box.classList.add("fresh");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function stopStarterRotation() {
|
|
712
|
+
clearInterval(starterTimer);
|
|
713
|
+
starterTimer = undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function drawFixedStarters(box, animate = false) {
|
|
717
|
+
const ids = Array.from({ length: VISIBLE_STARTERS }, (_, index) =>
|
|
718
|
+
FIXED_STARTERS[(starterOffset + index) % FIXED_STARTERS.length]);
|
|
719
|
+
box.replaceChildren(...ids.map((id) => starterChip(t("starter." + id), t("starter." + id + ".prompt"))));
|
|
720
|
+
if (animate) animateStarters(box);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function startStarterRotation(box) {
|
|
724
|
+
stopStarterRotation();
|
|
725
|
+
if (FIXED_STARTERS.length <= VISIBLE_STARTERS) return;
|
|
726
|
+
starterTimer = setInterval(() => {
|
|
727
|
+
// Do not move a choice out from under a mouse pointer or keyboard focus.
|
|
728
|
+
if (!box.offsetParent || box.matches(":hover") || box.matches(":focus-within")) return;
|
|
729
|
+
starterOffset = (starterOffset + 1) % FIXED_STARTERS.length;
|
|
730
|
+
drawFixedStarters(box, true);
|
|
731
|
+
}, STARTER_ROTATION_MS);
|
|
732
|
+
}
|
|
603
733
|
|
|
604
734
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
735
|
+
* Ask for the project's chips and draw them. Skipped when the project on
|
|
736
|
+
* screen has not changed, so a repaint of the project button does not pay for
|
|
737
|
+
* another round trip. The answer never waits on a model: a project whose set
|
|
738
|
+
* is not written yet gets the fixed three now, and the written set arrives
|
|
739
|
+
* later as a "starters" event (see startersArrived).
|
|
608
740
|
*/
|
|
609
|
-
function paintStarters() {
|
|
741
|
+
async function paintStarters(cwd) {
|
|
610
742
|
const box = document.getElementById("starters");
|
|
611
|
-
if (!box) return;
|
|
743
|
+
if (!box || !cwd || cwd === startersFor) return;
|
|
744
|
+
stopStarterRotation();
|
|
745
|
+
startersFor = cwd;
|
|
746
|
+
starterOffset = 0;
|
|
747
|
+
const ask = ++startersAsk;
|
|
612
748
|
box.replaceChildren();
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
749
|
+
let body;
|
|
750
|
+
try {
|
|
751
|
+
body = await getJson("/starters?cwd=" + encodeURIComponent(cwd) + "&locale=" + encodeURIComponent(LOCALE));
|
|
752
|
+
} catch {
|
|
753
|
+
body = { kind: "fixed" };
|
|
754
|
+
}
|
|
755
|
+
// A switch made while this was being written owns the box now.
|
|
756
|
+
if (ask !== startersAsk) return;
|
|
757
|
+
if (body && body.kind === "written" && Array.isArray(body.starters)) {
|
|
758
|
+
box.replaceChildren(...body.starters.map((item) => starterChip(item.label, item.prompt)));
|
|
759
|
+
} else {
|
|
760
|
+
drawFixedStarters(box);
|
|
761
|
+
startStarterRotation(box);
|
|
621
762
|
}
|
|
622
763
|
}
|
|
623
764
|
|
|
765
|
+
/**
|
|
766
|
+
* A set written in the background for a project, pushed when it lands.
|
|
767
|
+
*
|
|
768
|
+
* Swapped in only where it still fits: the same project and language on
|
|
769
|
+
* screen, and the empty state still showing — nobody's chip is replaced while
|
|
770
|
+
* they are reading a conversation, and the next visit reads it from disk.
|
|
771
|
+
*/
|
|
772
|
+
function startersArrived(event) {
|
|
773
|
+
const box = document.getElementById("starters");
|
|
774
|
+
if (!box || !event || event.cwd !== startersFor || event.locale !== LOCALE || !Array.isArray(event.starters)) return;
|
|
775
|
+
if (!box.offsetParent) return;
|
|
776
|
+
stopStarterRotation();
|
|
777
|
+
box.replaceChildren(...event.starters.map((item) => starterChip(item.label, item.prompt)));
|
|
778
|
+
animateStarters(box);
|
|
779
|
+
}
|
|
780
|
+
|
|
624
781
|
/**
|
|
625
782
|
* The desktop shell's update hint. The state lives in the main process, which
|
|
626
783
|
* decides whether a release gets a dialog or only this row; the page draws it
|
|
@@ -668,11 +825,128 @@ if (IS_DESKTOP && window.oharness.updates) {
|
|
|
668
825
|
// Asked once as well: a release found before this page loaded (or before a
|
|
669
826
|
// reopened window) was announced to nobody.
|
|
670
827
|
window.oharness.updates.state().then(paintUpdateHint, () => {});
|
|
828
|
+
if (window.oharness.updates.onPrompt) window.oharness.updates.onPrompt(showUpdatePrompt);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The update questions, asked in the app's own dialog rather than the OS's.
|
|
833
|
+
* The main process decides when to ask and what a button does; the page only
|
|
834
|
+
* words the question and hands back which button it was — 0 is the one that
|
|
835
|
+
* acts, and Escape answers -1, the same as Later.
|
|
836
|
+
*/
|
|
837
|
+
function showUpdatePrompt(prompt) {
|
|
838
|
+
const kind = prompt && prompt.kind;
|
|
839
|
+
const vars = { version: prompt.version || "", current: prompt.current || "" };
|
|
840
|
+
const dialog = el("dialog", "upd-dialog");
|
|
841
|
+
if (prompt.important) dialog.classList.add("important");
|
|
842
|
+
const head = el("div", "upd-head");
|
|
843
|
+
head.appendChild(el("span", "upd-mark", kind === "error" ? "!" : "↑"));
|
|
844
|
+
const title =
|
|
845
|
+
kind === "download"
|
|
846
|
+
? t(prompt.important ? "update.prompt.importantTitle" : "update.prompt.availableTitle", vars)
|
|
847
|
+
: kind === "restart"
|
|
848
|
+
? t("update.prompt.readyTitle", vars)
|
|
849
|
+
: kind === "current"
|
|
850
|
+
? t("update.prompt.currentTitle")
|
|
851
|
+
: kind === "error"
|
|
852
|
+
? t("update.prompt.errorTitle")
|
|
853
|
+
: t("update.prompt.devTitle");
|
|
854
|
+
head.appendChild(el("div", "upd-title", title));
|
|
855
|
+
dialog.appendChild(head);
|
|
856
|
+
|
|
857
|
+
if (kind === "download" && prompt.notes) dialog.appendChild(el("div", "upd-notes", prompt.notes));
|
|
858
|
+
const body =
|
|
859
|
+
kind === "download"
|
|
860
|
+
? t("update.prompt.availableBody", vars)
|
|
861
|
+
: kind === "restart"
|
|
862
|
+
? t("update.prompt.readyBody")
|
|
863
|
+
: kind === "current"
|
|
864
|
+
? t("update.prompt.currentBody", vars)
|
|
865
|
+
: kind === "error"
|
|
866
|
+
? prompt.detail || ""
|
|
867
|
+
: t("update.prompt.devBody");
|
|
868
|
+
if (body) dialog.appendChild(el("div", kind === "error" ? "upd-body mono" : "upd-body", body));
|
|
869
|
+
|
|
870
|
+
const actions = el("div", "dlg-actions");
|
|
871
|
+
const finish = (response) => {
|
|
872
|
+
window.oharness.updates.answer(prompt.id, response);
|
|
873
|
+
if (dialog.open) dialog.close();
|
|
874
|
+
dialog.remove();
|
|
875
|
+
};
|
|
876
|
+
let main;
|
|
877
|
+
let focus;
|
|
878
|
+
if (kind === "download" || kind === "restart") {
|
|
879
|
+
const later = el("button", "quiet", t("update.prompt.later"));
|
|
880
|
+
main = el("button", "approve", t(kind === "download" ? "update.prompt.download" : "update.prompt.restart"));
|
|
881
|
+
later.type = main.type = "button";
|
|
882
|
+
later.onclick = () => finish(1);
|
|
883
|
+
main.onclick = () => finish(0);
|
|
884
|
+
actions.append(later, main);
|
|
885
|
+
// Restarting ends whatever is running, so Enter must not do it by accident.
|
|
886
|
+
focus = kind === "restart" ? later : main;
|
|
887
|
+
} else {
|
|
888
|
+
main = el("button", "approve", t("update.prompt.ok"));
|
|
889
|
+
main.type = "button";
|
|
890
|
+
main.onclick = () => finish(0);
|
|
891
|
+
actions.append(main);
|
|
892
|
+
focus = main;
|
|
893
|
+
}
|
|
894
|
+
dialog.appendChild(actions);
|
|
895
|
+
dialog.addEventListener("cancel", (e) => {
|
|
896
|
+
e.preventDefault();
|
|
897
|
+
finish(-1);
|
|
898
|
+
});
|
|
899
|
+
document.body.appendChild(dialog);
|
|
900
|
+
dialog.showModal();
|
|
901
|
+
focus.focus();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* A prompt from a link on the website — oharness://new?prompt=… — opened as a
|
|
906
|
+
* new session with the text in the composer, and nothing more.
|
|
907
|
+
*
|
|
908
|
+
* The main process checked and cleaned the text (desktop/deep-link.js), but
|
|
909
|
+
* it came from whatever page the person was on, so this page treats it as
|
|
910
|
+
* someone else's suggestion: it is never sent, no permission is changed and
|
|
911
|
+
* no card is answered. It sits in the box until the person reads it and
|
|
912
|
+
* presses send, or deletes it. A new session rather than the open one, so the
|
|
913
|
+
* suggestion never lands in the middle of a conversation as if it belonged
|
|
914
|
+
* there; and a draft already in the box is only replaced when the person says
|
|
915
|
+
* so, because the box is shared by every tab and the draft may be the only
|
|
916
|
+
* copy of what they were writing.
|
|
917
|
+
*
|
|
918
|
+
* Not before boot has finished: switching sessions while the first one is
|
|
919
|
+
* still being opened leaves the two fighting over the page. Boot takes the
|
|
920
|
+
* one that launched the app at the end; the nudge covers every later click.
|
|
921
|
+
*/
|
|
922
|
+
let deepLinkReady = false;
|
|
923
|
+
|
|
924
|
+
async function takeDeepLink() {
|
|
925
|
+
if (!deepLinkReady || !IS_DESKTOP || !window.oharness.deepLink) return;
|
|
926
|
+
let text;
|
|
927
|
+
try {
|
|
928
|
+
text = await window.oharness.deepLink.take();
|
|
929
|
+
} catch {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (typeof text !== "string" || !text) return;
|
|
933
|
+
const draft = input.value.trim();
|
|
934
|
+
if (draft && draft !== text && !(await askConfirm({ body: t("deepLink.replaceDraft"), ok: t("dialog.replace") }))) return;
|
|
935
|
+
await switchTo({ action: "new" });
|
|
936
|
+
input.value = text;
|
|
937
|
+
// Called rather than an input event, which would also open the command
|
|
938
|
+
// menu for a prompt that starts with "/" — one Enter from running it.
|
|
939
|
+
autosize();
|
|
940
|
+
paintMirror();
|
|
941
|
+
input.focus();
|
|
942
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
943
|
+
}
|
|
944
|
+
if (IS_DESKTOP && window.oharness.deepLink) {
|
|
945
|
+
window.oharness.deepLink.onArrived(() => void takeDeepLink());
|
|
671
946
|
}
|
|
672
947
|
|
|
673
948
|
void (async () => {
|
|
674
949
|
applyStrings();
|
|
675
|
-
paintStarters();
|
|
676
950
|
// The language is here because it is the one thing that decides what to
|
|
677
951
|
// translate next, and the shell because a desktop window and a browser tab
|
|
678
952
|
// are different products to the person using them.
|
|
@@ -727,18 +1001,26 @@ void (async () => {
|
|
|
727
1001
|
await loadSidePanels();
|
|
728
1002
|
await loadProjects();
|
|
729
1003
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1004
|
+
// Wired here, after the session has loaded: a chip that submits before the
|
|
1005
|
+
// session it would submit to exists is worse than one that waits. Delegated,
|
|
1006
|
+
// because the chips are redrawn on every project switch. Absent when the tab
|
|
1007
|
+
// opened on a conversation: its first entry took the invitation off the
|
|
1008
|
+
// page, and a throw here stopped everything after it, the session list too.
|
|
1009
|
+
document.getElementById("starters")?.addEventListener("click", (event) => {
|
|
1010
|
+
const button = event.target.closest("#starters button");
|
|
1011
|
+
if (!button) return;
|
|
1012
|
+
stopStarterRotation();
|
|
1013
|
+
input.value = button.dataset.p;
|
|
1014
|
+
paintMirror();
|
|
1015
|
+
document.getElementById("composer").requestSubmit();
|
|
1016
|
+
});
|
|
737
1017
|
await loadSessions();
|
|
738
1018
|
await refreshRoutineCount();
|
|
739
1019
|
await refreshPluginPages();
|
|
740
1020
|
await refreshModelCount();
|
|
741
1021
|
await loadAccount();
|
|
742
1022
|
input.focus();
|
|
1023
|
+
deepLinkReady = true;
|
|
1024
|
+
await takeDeepLink();
|
|
743
1025
|
})();`;
|
|
744
1026
|
//# sourceMappingURL=shell.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2/BzB,CAAC"}
|