@oharness/server 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/browser-pane.d.ts +48 -0
  2. package/dist/browser-pane.js +185 -0
  3. package/dist/browser-pane.js.map +1 -0
  4. package/dist/i18n.js +144 -0
  5. package/dist/i18n.js.map +1 -1
  6. package/dist/main.js +20 -0
  7. package/dist/main.js.map +1 -1
  8. package/dist/page/client/account.d.ts +8 -0
  9. package/dist/page/client/account.js +1895 -0
  10. package/dist/page/client/account.js.map +1 -0
  11. package/dist/page/client/artifacts.d.ts +7 -0
  12. package/dist/page/client/artifacts.js +767 -0
  13. package/dist/page/client/artifacts.js.map +1 -0
  14. package/dist/page/client/base.d.ts +8 -0
  15. package/dist/page/client/base.js +400 -0
  16. package/dist/page/client/base.js.map +1 -0
  17. package/dist/page/client/browser.d.ts +21 -0
  18. package/dist/page/client/browser.js +904 -0
  19. package/dist/page/client/browser.js.map +1 -0
  20. package/dist/page/client/composer.d.ts +8 -0
  21. package/dist/page/client/composer.js +1694 -0
  22. package/dist/page/client/composer.js.map +1 -0
  23. package/dist/page/client/dialogs.d.ts +7 -0
  24. package/dist/page/client/dialogs.js +724 -0
  25. package/dist/page/client/dialogs.js.map +1 -0
  26. package/dist/page/client/events.d.ts +7 -0
  27. package/dist/page/client/events.js +288 -0
  28. package/dist/page/client/events.js.map +1 -0
  29. package/dist/page/client/index.d.ts +16 -0
  30. package/dist/page/client/index.js +46 -0
  31. package/dist/page/client/index.js.map +1 -0
  32. package/dist/page/client/panels.d.ts +7 -0
  33. package/dist/page/client/panels.js +659 -0
  34. package/dist/page/client/panels.js.map +1 -0
  35. package/dist/page/client/pickers.d.ts +7 -0
  36. package/dist/page/client/pickers.js +624 -0
  37. package/dist/page/client/pickers.js.map +1 -0
  38. package/dist/page/client/routines.d.ts +6 -0
  39. package/dist/page/client/routines.js +910 -0
  40. package/dist/page/client/routines.js.map +1 -0
  41. package/dist/page/client/shell.d.ts +6 -0
  42. package/dist/page/client/shell.js +690 -0
  43. package/dist/page/client/shell.js.map +1 -0
  44. package/dist/page/client/state.d.ts +7 -0
  45. package/dist/page/client/state.js +1275 -0
  46. package/dist/page/client/state.js.map +1 -0
  47. package/dist/page/client/transcript.d.ts +8 -0
  48. package/dist/page/client/transcript.js +699 -0
  49. package/dist/page/client/transcript.js.map +1 -0
  50. package/dist/page/client/usage.d.ts +7 -0
  51. package/dist/page/client/usage.js +954 -0
  52. package/dist/page/client/usage.js.map +1 -0
  53. package/dist/page/style.d.ts +1 -0
  54. package/dist/page/style.js +2793 -0
  55. package/dist/page/style.js.map +1 -0
  56. package/dist/page.js +70 -13583
  57. package/dist/page.js.map +1 -1
  58. package/dist/server.d.ts +24 -0
  59. package/dist/server.js +511 -25
  60. package/dist/server.js.map +1 -1
  61. package/package.json +2 -2
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Projects, the command palette, and the boot sequence that starts it all.
3
+ *
4
+ * One part of the client script; see ./index.ts for how the parts join.
5
+ */
6
+ export const SHELL = String.raw `
7
+ // --- projects --------------------------------------------------------------
8
+
9
+ const projectBtn = document.getElementById("project");
10
+ let projects = [];
11
+
12
+ /** The last path segment, which is what the directory is called. */
13
+ function projectName(dir) {
14
+ const parts = String(dir).replace(/\/+$/, "").split("/");
15
+ return parts[parts.length - 1] || dir;
16
+ }
17
+
18
+ function paintProject() {
19
+ const active = projects.find((project) => project.current);
20
+ document.getElementById("project-name").textContent = active ? active.name : "—";
21
+ document.getElementById("project-avatar").textContent = active
22
+ ? active.name.slice(0, 1).toUpperCase()
23
+ : "?";
24
+ // The invitation names the project it is connected to; a switch has to reach
25
+ // it too, or it goes on advertising the tree the user just left.
26
+ const cwdEl = document.getElementById("empty-cwd");
27
+ if (cwdEl && active) cwdEl.textContent = active.name;
28
+ // A directory added while the chip was open belongs on its list.
29
+ refreshWorkspace();
30
+ }
31
+
32
+ /** Repaint whichever panel is on screen, if one is. */
33
+ function repaintPanel() {
34
+ void refreshPluginPages();
35
+ if (panelView.hidden) return;
36
+ // Off what was opened, not off which sidebar row is lit: a sub-panel lights
37
+ // the row it was reached from, so asking the sidebar would answer "settings"
38
+ // for a page that is not settings and quietly navigate away from it.
39
+ if (shownPanel === "nav-artifacts") void showAllArtifacts();
40
+ else if (shownPanel === "nav-models") void showModels();
41
+ else if (shownPanel === "nav-routines") void showRoutines();
42
+ else if (shownPanel === "nav-settings") void showSettings();
43
+ else if (shownPanel === "usage") void showUsage();
44
+ else if (shownPanel === "audit") void showAudit();
45
+ else if (shownPanel === "permissions") void showPermissions();
46
+ else if (shownPanel?.startsWith("nav-plugin-")) void showMemoryPage(shownPanel.slice("nav-plugin-".length));
47
+ }
48
+
49
+ async function loadProjects() {
50
+ const body = await getJson("/projects");
51
+ projects = body.projects || [];
52
+ projectOrder = body.order || [];
53
+ paintProject();
54
+ // A project's pin and its place decide how the session list is grouped, so
55
+ // the two are painted together rather than waiting for the next thing that
56
+ // happens to reload sessions.
57
+ if (sessionRows.length) paintSessions();
58
+ }
59
+
60
+ async function renameProjectUi(cwd, name) {
61
+ const next = window.prompt(t("projects.renamePrompt"), name);
62
+ if (!next || next.trim() === name) return;
63
+ const body = await api("/project", { path: cwd, rename: next.trim() });
64
+ if (body && body.error) return void addNotice(body.error, "err");
65
+ addNotice(t("projects.renamed", { name: body.project.name }));
66
+ // Paths key tabs, local open/closed state and drag order. Reloading after
67
+ // the server has rebuilt the renamed harness drops every stale path at once.
68
+ window.location.reload();
69
+ }
70
+
71
+ function exportProjectUi(cwd) {
72
+ const link = document.createElement("a");
73
+ link.href = q("/project/export?path=" + encodeURIComponent(cwd));
74
+ link.download = "";
75
+ link.click();
76
+ }
77
+
78
+ function openProjectPicker() {
79
+ const dialog = document.getElementById("project-picker");
80
+ const field = document.getElementById("proj-search");
81
+ const listEl = document.getElementById("proj-list");
82
+ field.placeholder = t("projects.search");
83
+
84
+ let first = null;
85
+ const paint = () => {
86
+ const query = field.value.trim().toLowerCase();
87
+ listEl.replaceChildren();
88
+ first = null;
89
+ for (const project of projects) {
90
+ if (query && !(project.name + " " + project.path).toLowerCase().includes(query)) continue;
91
+ const row = el("button", "proj-item");
92
+ row.type = "button";
93
+ row.appendChild(el("span", "avatar", project.name.slice(0, 1).toUpperCase()));
94
+ row.appendChild(el("span", "path", project.name));
95
+ if (project.missing) row.appendChild(el("span", "tag", t("projects.missing")));
96
+ if (project.current) row.appendChild(el("span", "muted", "✓"));
97
+ row.addEventListener("click", () => {
98
+ dialog.close();
99
+ if (!project.current) void chooseProject(project.path);
100
+ });
101
+
102
+ const line = el("div", "proj-row");
103
+ line.appendChild(row);
104
+ const rename = el("button", "proj-forget", "✎");
105
+ rename.type = "button";
106
+ rename.title = t("projects.rename");
107
+ rename.setAttribute("aria-label", t("projects.rename") + ": " + project.name);
108
+ rename.addEventListener("click", (e) => {
109
+ e.stopPropagation();
110
+ dialog.close();
111
+ void renameProjectUi(project.path, project.name);
112
+ });
113
+ line.appendChild(rename);
114
+ const archive = el("button", "proj-forget", "↓");
115
+ archive.type = "button";
116
+ archive.title = t("projects.export");
117
+ archive.setAttribute("aria-label", t("projects.export") + ": " + project.name);
118
+ archive.addEventListener("click", (e) => {
119
+ e.stopPropagation();
120
+ exportProjectUi(project.path);
121
+ });
122
+ line.appendChild(archive);
123
+ // Not offered for the project this window is standing in: the next
124
+ // switch calls addProject and puts it straight back, so the control
125
+ // would appear to do nothing. Nothing recorded is lost either way —
126
+ // this forgets a path, and the sessions stay where they are.
127
+ if (!project.current) {
128
+ const forget = el("button", "proj-forget", "✕");
129
+ forget.type = "button";
130
+ forget.title = t("projects.forget");
131
+ forget.setAttribute("aria-label", t("projects.forget") + ": " + project.name);
132
+ forget.addEventListener("click", (e) => {
133
+ e.stopPropagation();
134
+ void api("/project", { path: project.path, remove: true }).then(async (body) => {
135
+ if (body && body.error) return void addNotice(body.error, "err");
136
+ await loadProjects();
137
+ paint();
138
+ });
139
+ });
140
+ line.appendChild(forget);
141
+ }
142
+ listEl.appendChild(line);
143
+ first = first || row;
144
+ }
145
+ };
146
+
147
+ const add = document.getElementById("proj-add");
148
+ add.replaceChildren(el("span", null, "+"), el("span", null, t("projects.add")));
149
+ add.onclick = () => {
150
+ dialog.close();
151
+ void pickFolder((path) => chooseProject(path));
152
+ };
153
+
154
+ field.value = "";
155
+ field.oninput = paint;
156
+ // Typing enough of the name and pressing Enter is what a list with a search
157
+ // box above it promises; without this the field swallows the key.
158
+ field.onkeydown = (e) => {
159
+ if (e.key !== "Enter" || !first) return;
160
+ e.preventDefault();
161
+ first.click();
162
+ };
163
+ paint();
164
+ dialog.showModal();
165
+ field.focus();
166
+ }
167
+
168
+
169
+ /**
170
+ * A directory browser, for hosts with no folder dialog.
171
+ *
172
+ * Lists directories from the server because the page cannot see the disk. Only
173
+ * names and only directories: this navigates to a project, it is not a file
174
+ * manager.
175
+ */
176
+ function openBrowser(start, opts) {
177
+ // Whoever opened it decides what a chosen directory is for: a project to
178
+ // open, or the tree a session is about to start in.
179
+ const onPick = (opts && opts.onPick) || ((path) => chooseProject(path));
180
+ const dialog = document.getElementById("browse");
181
+ const listEl = document.getElementById("browse-list");
182
+ const hereEl = document.getElementById("browse-here");
183
+ let at = start || "";
184
+
185
+ const paint = async () => {
186
+ const body = await getJson("/fs" + (at ? "?path=" + encodeURIComponent(at) : ""));
187
+ if (body.error) return void addNotice(body.error, "err");
188
+ at = body.path;
189
+ hereEl.textContent = body.path;
190
+ listEl.replaceChildren();
191
+
192
+ if (body.parent) {
193
+ const up = el("button", "proj-item");
194
+ up.type = "button";
195
+ up.appendChild(el("span", "avatar", "↑"));
196
+ up.appendChild(el("span", "path", t("projects.up")));
197
+ up.addEventListener("click", () => {
198
+ at = body.parent;
199
+ void paint();
200
+ });
201
+ listEl.appendChild(up);
202
+ }
203
+
204
+ for (const entry of body.entries || []) {
205
+ const row = el("button", "proj-item");
206
+ row.type = "button";
207
+ row.appendChild(el("span", "avatar", entry.name.slice(0, 1).toUpperCase()));
208
+ row.appendChild(el("span", "path", entry.name));
209
+ row.addEventListener("click", () => {
210
+ at = entry.path;
211
+ void paint();
212
+ });
213
+ listEl.appendChild(row);
214
+ }
215
+ };
216
+
217
+ const pick = document.getElementById("browse-choose");
218
+ pick.textContent = t("projects.choose");
219
+ pick.onclick = () => {
220
+ dialog.close();
221
+ void onPick(at);
222
+ };
223
+ void paint();
224
+ dialog.showModal();
225
+ }
226
+
227
+ async function chooseProject(path) {
228
+ const state = await api("/project", { path });
229
+ if (state && state.error) return void addNotice(state.error, "err");
230
+ await adoptSession(state);
231
+ }
232
+
233
+ /**
234
+ * Take over the session a project switch landed on.
235
+ *
236
+ * A switch replaces every open tab: the old sessions belong to a different
237
+ * tree, and keeping them would offer transcripts whose files are no longer the
238
+ * ones on screen. The new session has to be adopted rather than left implicit —
239
+ * leaving activeId null pointed the page at a pane that had just been removed,
240
+ * so the first reply after a project switch rendered into nothing.
241
+ */
242
+ async function adoptSession(state) {
243
+ for (const entry of panes.values()) entry.node.remove();
244
+ panes.clear();
245
+
246
+ const record = pane(state.sessionId);
247
+ record.title = state.title || "";
248
+ activeId = state.sessionId;
249
+ P = record;
250
+ record.node.hidden = false;
251
+ paintTabs();
252
+
253
+ const { entries } = await getJson("/history?session=" + encodeURIComponent(state.sessionId));
254
+ renderHistory(entries, record);
255
+ applyState(state);
256
+
257
+ await loadProjects();
258
+ await loadSessions();
259
+ await loadSidePanels();
260
+ // Settings reports the working directory, so the page that offered the
261
+ // switch has to show its result rather than the path that is no longer used.
262
+ repaintPanel();
263
+ }
264
+
265
+ projectBtn.addEventListener("click", openProjectPicker);
266
+
267
+ // --- the command palette ---------------------------------------------------
268
+
269
+ /**
270
+ * One keystroke to what the sidebar is for: start a session, reopen one,
271
+ * change project.
272
+ *
273
+ * Everything here is already reachable by mouse. What it adds is reach: the
274
+ * sidebar previews five sessions per project and folds the rest away, so the
275
+ * transcript from two days ago costs an unfold, a scroll and a read. Typing
276
+ * three letters of its title is the same answer without the hunt.
277
+ */
278
+ const paletteDialog = document.getElementById("palette");
279
+ const paletteField = document.getElementById("cmdk-search");
280
+ const paletteListEl = document.getElementById("cmdk-list");
281
+
282
+ /**
283
+ * Cmd on a Mac, Ctrl everywhere else — deliberately not both.
284
+ *
285
+ * Ctrl-K inside a text field on macOS deletes to the end of the line, and the
286
+ * composer is a text field the user is usually sitting in when they reach for
287
+ * this. Claiming that key would break an editing shortcut to duplicate one
288
+ * the same keyboard already has.
289
+ */
290
+ const IS_MAC = /mac/i.test(navigator.platform || navigator.userAgent || "");
291
+ const IS_WIN = /win/i.test(navigator.platform || navigator.userAgent || "");
292
+
293
+ /** The rows the current query left on screen, in the order they are drawn. */
294
+ let paletteRows = [];
295
+ let paletteCursor = 0;
296
+
297
+ /** A nav item's own icon, so the palette names things the way the sidebar does. */
298
+ function navIcon(id) {
299
+ const source = document.querySelector("#" + id + " .ico");
300
+ const copy = source ? source.cloneNode(true) : svgIcon("file");
301
+ copy.setAttribute("class", "ico-sm");
302
+ return copy;
303
+ }
304
+
305
+ /**
306
+ * The English label for a key.
307
+ *
308
+ * A translated UI still answers to the English word: "settings" finds 设置.
309
+ * The alternative is a second set of strings that exist only to be searched.
310
+ */
311
+ const enLabel = (key) => (STRINGS.en && STRINGS.en[key]) || "";
312
+
313
+ /** Everything the palette can offer, in the order it offers it. */
314
+ function paletteItems() {
315
+ const items = [];
316
+ const query = paletteField.value.trim();
317
+ const actions = t("palette.actions");
318
+ const here = (projects.find((project) => project.current) || {}).path || currentCwd;
319
+
320
+ items.push({
321
+ kind: "new",
322
+ group: actions,
323
+ icon: () => navIcon("new"),
324
+ label: t("nav.new"),
325
+ // Where it will run, because "new session" does not say in which tree and
326
+ // the answer is the one thing about it that changes.
327
+ sub: here ? projectName(here) : "",
328
+ keywords: enLabel("nav.new"),
329
+ run: () => void switchTo({ action: "new" }),
330
+ });
331
+ items.push({
332
+ kind: "switch",
333
+ group: actions,
334
+ icon: () => svgIcon("folder"),
335
+ label: t("palette.switchProject"),
336
+ keywords: enLabel("palette.switchProject"),
337
+ run: () => openProjectPicker(),
338
+ });
339
+ // Clicked rather than reimplemented: each nav item already knows how to open
340
+ // its own panel, and a second copy of that wiring here would drift.
341
+ for (const [id, key] of [
342
+ ["nav-artifacts", "nav.artifacts"],
343
+ ["nav-models", "nav.models"],
344
+ ["nav-routines", "nav.routines"],
345
+ ["nav-settings", "nav.settings"],
346
+ ]) {
347
+ items.push({
348
+ kind: "panel",
349
+ group: actions,
350
+ icon: () => navIcon(id),
351
+ label: t(key),
352
+ keywords: enLabel(key),
353
+ run: () => document.getElementById(id).click(),
354
+ });
355
+ }
356
+
357
+ // Unfiltered, the list is a preview of the most recent work and the actions
358
+ // above it still have to be visible. Once there is a query, every session in
359
+ // every tree is fair game — that is what the field is for.
360
+ const sessions = t("palette.sessions");
361
+ for (const session of query ? sessionRows : sessionRows.slice(0, 8)) {
362
+ items.push({
363
+ kind: "session",
364
+ group: sessions,
365
+ icon: () => svgIcon("chat"),
366
+ label: session.title || t("session.untitled"),
367
+ sub: [projectName(session.cwd || ""), ago(session.updatedAt)].filter(Boolean).join(" · "),
368
+ keywords: session.cwd || "",
369
+ run: () => {
370
+ if (session.current) return void setNav(null);
371
+ void openSession(session.id, session.cwd);
372
+ },
373
+ });
374
+ }
375
+
376
+ const named = t("palette.projects");
377
+ for (const project of query ? projects : projects.slice(0, 5)) {
378
+ items.push({
379
+ kind: "project",
380
+ group: named,
381
+ icon: () => svgIcon("folder"),
382
+ label: project.name,
383
+ sub: project.missing ? t("projects.missing") : "",
384
+ // The path, so two projects with the same last segment can be told apart
385
+ // by typing the part of the path that differs.
386
+ keywords: project.path,
387
+ run: () => {
388
+ if (!project.current) void chooseProject(project.path);
389
+ },
390
+ });
391
+ }
392
+ return items;
393
+ }
394
+
395
+ /** Same word-by-word match the pickers use, over the row's hidden aliases too. */
396
+ const paletteMatch = (item, query) =>
397
+ matchesQuery(
398
+ { label: item.label, group: item.group, sub: (item.sub || "") + " " + (item.keywords || "") },
399
+ query,
400
+ );
401
+
402
+ function paintPalette() {
403
+ const query = paletteField.value.trim();
404
+ paletteRows = paletteItems().filter((item) => paletteMatch(item, query));
405
+ if (paletteCursor >= paletteRows.length) paletteCursor = Math.max(0, paletteRows.length - 1);
406
+
407
+ paletteListEl.replaceChildren();
408
+ let lastGroup = null;
409
+ paletteRows.forEach((item, i) => {
410
+ if (item.group !== lastGroup) {
411
+ lastGroup = item.group;
412
+ paletteListEl.appendChild(el("div", "cmdk-group", item.group));
413
+ }
414
+ const row = el("button", "cmdk-item" + (i === paletteCursor ? " cursor" : ""));
415
+ row.type = "button";
416
+ row.appendChild(item.icon());
417
+ row.appendChild(el("span", "cmdk-label", item.label));
418
+ if (item.sub) row.appendChild(el("span", "cmdk-sub", item.sub));
419
+ // Moving the mouse over a row makes it the row Enter answers. Two separate
420
+ // highlights — one under the pointer, one under the keyboard — leave the
421
+ // user guessing which of them the next key is talking to.
422
+ row.addEventListener("mousemove", () => {
423
+ if (paletteCursor === i) return;
424
+ paletteCursor = i;
425
+ for (const other of paletteListEl.querySelectorAll(".cmdk-item.cursor")) {
426
+ other.classList.remove("cursor");
427
+ }
428
+ row.classList.add("cursor");
429
+ });
430
+ row.addEventListener("click", () => runPalette(item));
431
+ paletteListEl.appendChild(row);
432
+ });
433
+
434
+ if (!paletteRows.length) paletteListEl.appendChild(el("div", "cmdk-empty", t("menu.none")));
435
+ }
436
+
437
+ function runPalette(item) {
438
+ // Closed first: the row may open a dialog of its own, and two modals deep is
439
+ // one more than anything here needs.
440
+ paletteDialog.close();
441
+ // Which kind of row got used, never its title or its path — the point of the
442
+ // palette is which of the three it is actually for.
443
+ track("palette_run", { kind: item.kind });
444
+ item.run();
445
+ }
446
+
447
+ function movePalette(step) {
448
+ if (!paletteRows.length) return;
449
+ paletteCursor = (paletteCursor + step + paletteRows.length) % paletteRows.length;
450
+ paintPalette();
451
+ const active = paletteListEl.querySelector(".cursor");
452
+ if (active) active.scrollIntoView({ block: "nearest" });
453
+ }
454
+
455
+ function openPalette() {
456
+ // One modal at a time. A permission request is a question the agent is
457
+ // blocked on; a palette over it would answer neither.
458
+ if (document.querySelector("dialog[open]")) return;
459
+ closeAttachMenu();
460
+ closeRowMenu();
461
+ paletteField.value = "";
462
+ paletteCursor = 0;
463
+ paintPalette();
464
+ paletteDialog.showModal();
465
+ paletteField.focus();
466
+ track("palette_opened", { shell: IS_DESKTOP ? "desktop" : "browser" });
467
+ }
468
+
469
+ // The desktop shell reaches the palette through this: a menu accelerator is
470
+ // handled before the page ever sees the keystroke, so the menu item calls the
471
+ // same function the key would have.
472
+ window.openCommandPalette = openPalette;
473
+
474
+ paletteField.addEventListener("input", () => {
475
+ paletteCursor = 0;
476
+ paintPalette();
477
+ // The list keeps its scroll offset across a repaint, which leaves the first
478
+ // results of a new query above the visible window.
479
+ paletteListEl.scrollTop = 0;
480
+ });
481
+
482
+ paletteField.addEventListener("keydown", (e) => {
483
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
484
+ e.preventDefault();
485
+ return void movePalette(e.key === "ArrowDown" ? 1 : -1);
486
+ }
487
+ if (e.key !== "Enter") return;
488
+ e.preventDefault();
489
+ const picked = paletteRows[paletteCursor];
490
+ if (picked) runPalette(picked);
491
+ });
492
+
493
+ // A dialog's backdrop is the dialog element itself; a click on a row never
494
+ // reaches here.
495
+ paletteDialog.addEventListener("click", (e) => {
496
+ if (e.target === paletteDialog) paletteDialog.close();
497
+ });
498
+
499
+ paletteDialog.addEventListener("close", () => {
500
+ // A dialog hands focus back to whatever opened it, so closing this one left
501
+ // a focus ring sitting on the sidebar's Search button — and a ⌘K typed into
502
+ // the composer was never "opened from" there at all. The composer is where
503
+ // the next keystroke was going in either case.
504
+ if (!panelView.hidden) return;
505
+ input.focus();
506
+ });
507
+
508
+ document.addEventListener("keydown", (e) => {
509
+ // e.code as well as e.key: on a non-Latin layout the key is whatever letter
510
+ // that layout puts there, and the shortcut is the position on the keyboard.
511
+ if (e.code !== "KeyK" && String(e.key).toLowerCase() !== "k") return;
512
+ if (IS_MAC ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return;
513
+ if (e.altKey || e.shiftKey) return;
514
+ // Chrome gives Cmd-K to the address bar, so the page has to say no. Every
515
+ // app that has this shortcut does the same.
516
+ e.preventDefault();
517
+ if (paletteDialog.open) return void paletteDialog.close();
518
+ openPalette();
519
+ });
520
+
521
+ document.getElementById("nav-search").addEventListener("click", openPalette);
522
+ // The chord as this machine actually spells it, in the one place a mouse user
523
+ // will see it.
524
+ document.getElementById("palette-hint").textContent = IS_MAC ? "⌘K" : "Ctrl K";
525
+
526
+
527
+ // --- boot -----------------------------------------------------------------
528
+
529
+ /** Fill every element that declared a string key. Runs before anything draws. */
530
+ function applyStrings() {
531
+ for (const node of document.querySelectorAll("[data-t]")) node.textContent = ts(node.dataset.t);
532
+ for (const node of document.querySelectorAll("[data-t-placeholder]")) {
533
+ node.placeholder = ts(node.dataset.tPlaceholder);
534
+ }
535
+ for (const node of document.querySelectorAll("[data-t-title]")) {
536
+ node.title = ts(node.dataset.tTitle);
537
+ }
538
+ for (const node of document.querySelectorAll("[data-t-aria-label]")) {
539
+ node.setAttribute("aria-label", ts(node.dataset.tAriaLabel));
540
+ }
541
+ document.documentElement.lang = LOCALE;
542
+ }
543
+
544
+ /**
545
+ * What the empty state offers to do, grouped by the kind of work it is.
546
+ *
547
+ * One chip is drawn from each group, so the three on screen always span
548
+ * writing code, getting through the day's work, and taking the thing to
549
+ * market — three variations on "read my repo" would teach a new person that
550
+ * this is a code-reading tool and nothing else. Each id names a label and a
551
+ * "…prompt" string; the label is what fits on
552
+ * a chip, the prompt is the sentence the chip actually sends.
553
+ */
554
+ const STARTERS = [
555
+ ["explain", "bugs", "tests", "review", "faster"],
556
+ ["plan", "docs", "digest", "automate", "backlog"],
557
+ ["landing", "launch", "pitch", "position", "changelog"],
558
+ ];
559
+
560
+ /**
561
+ * Draw one chip per group. The sentence rides along in data-p, and the click
562
+ * is wired at the end of boot with the rest of them: a chip that submits before
563
+ * the session it would submit to has loaded is worse than one that waits.
564
+ */
565
+ function paintStarters() {
566
+ const box = document.getElementById("starters");
567
+ if (!box) return;
568
+ box.replaceChildren();
569
+ for (const group of STARTERS) {
570
+ const id = group[Math.floor(Math.random() * group.length)];
571
+ const button = el("button", "", t("starter." + id));
572
+ button.type = "button";
573
+ button.dataset.p = t("starter." + id + ".prompt");
574
+ // The label is a chip; the title is the sentence it will actually send.
575
+ button.title = button.dataset.p;
576
+ box.appendChild(button);
577
+ }
578
+ }
579
+
580
+ /**
581
+ * The desktop shell's update hint. The state lives in the main process, which
582
+ * decides whether a release gets a dialog or only this row; the page draws it
583
+ * and passes the click back, where the right question is asked.
584
+ */
585
+ function paintUpdateHint(state) {
586
+ const box = document.getElementById("update-hint");
587
+ if (!box) return;
588
+ const status = state && state.status;
589
+ if (status !== "available" && status !== "downloading" && status !== "ready" && status !== "updated") {
590
+ box.hidden = true;
591
+ box.textContent = "";
592
+ return;
593
+ }
594
+ box.hidden = false;
595
+ box.classList.toggle("important", Boolean(state.important));
596
+ const button = document.createElement("button");
597
+ button.type = "button";
598
+ button.disabled = status === "downloading";
599
+ if (state.important) button.title = t("update.important");
600
+ const dot = document.createElement("span");
601
+ dot.className = "dot";
602
+ const label = document.createElement("span");
603
+ label.className = "label";
604
+ label.textContent =
605
+ status === "updated"
606
+ ? t("update.updated", { version: state.version })
607
+ : status === "ready"
608
+ ? t("update.ready")
609
+ : status === "downloading"
610
+ ? t("update.downloading", { version: state.version })
611
+ : t("update.available", { version: state.version });
612
+ button.append(dot, label);
613
+ if (status === "downloading") {
614
+ const pct = document.createElement("span");
615
+ pct.className = "pct";
616
+ pct.textContent = (state.percent || 0) + "%";
617
+ button.append(pct);
618
+ }
619
+ button.addEventListener("click", () => window.oharness.updates.act());
620
+ box.replaceChildren(button);
621
+ }
622
+ if (IS_DESKTOP && window.oharness.updates) {
623
+ window.oharness.updates.onState(paintUpdateHint);
624
+ // Asked once as well: a release found before this page loaded (or before a
625
+ // reopened window) was announced to nobody.
626
+ window.oharness.updates.state().then(paintUpdateHint, () => {});
627
+ }
628
+
629
+ void (async () => {
630
+ applyStrings();
631
+ paintStarters();
632
+ // The language is here because it is the one thing that decides what to
633
+ // translate next, and the shell because a desktop window and a browser tab
634
+ // are different products to the person using them.
635
+ track("app_opened", { locale: LOCALE, shell: IS_DESKTOP ? "desktop" : "browser" });
636
+ // After the first track call, not before: the queue exists so that boot does
637
+ // not have to wait on a round trip to find out whether anyone is listening.
638
+ void startAnalytics();
639
+ const state = await getJson("/state");
640
+ if (state.error) {
641
+ // Nothing else will work either; saying so beats an empty shell.
642
+ P = pane("boot");
643
+ P.node.hidden = false;
644
+ P.node.replaceChildren(panelError(state.error));
645
+ return;
646
+ }
647
+
648
+ // Reopen whatever the server still has running, so a reload does not silently
649
+ // abandon turns that are still streaming in other tabs.
650
+ const opened = await getJson("/open");
651
+ const list = (opened.open || []).length ? opened.open : [{ id: state.sessionId, current: true }];
652
+ for (const entry of list) {
653
+ const record = pane(entry.id);
654
+ record.title = entry.title || record.title;
655
+ record.cwd = entry.cwd || record.cwd;
656
+ record.busy = Boolean(entry.busy);
657
+ }
658
+ activeId = (list.find((entry) => entry.current) || list[0]).id;
659
+ for (const [id, record] of panes) record.node.hidden = id !== activeId;
660
+ paintTabs();
661
+
662
+ P = pane(activeId);
663
+ await loadModels();
664
+ applyState(state);
665
+ const cwdEl = document.getElementById("empty-cwd");
666
+ if (cwdEl) cwdEl.textContent = projectName(state.cwd || "");
667
+
668
+ for (const [id, record] of panes) {
669
+ const { entries } = await getJson("/history?session=" + encodeURIComponent(id));
670
+ renderHistory(entries, record);
671
+ }
672
+ P = pane(activeId);
673
+ await loadSidePanels();
674
+ await loadProjects();
675
+
676
+ for (const button of document.querySelectorAll("#starters button")) {
677
+ button.addEventListener("click", () => {
678
+ input.value = button.dataset.p;
679
+ paintMirror();
680
+ document.getElementById("composer").requestSubmit();
681
+ });
682
+ }
683
+ await loadSessions();
684
+ await refreshRoutineCount();
685
+ await refreshPluginPages();
686
+ await refreshModelCount();
687
+ await loadAccount();
688
+ input.focus();
689
+ })();`;
690
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2qBzB,CAAC"}