@oharness/server 0.8.0 → 0.9.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.
@@ -0,0 +1,412 @@
1
+ /**
2
+ * The files pane — a project's tree beside the file picked from it — and the
3
+ * terminal that opens in the same folder.
4
+ *
5
+ * One part of the client script; see ./index.ts for how the parts join.
6
+ */
7
+ export const FILES = String.raw `
8
+ // --- files pane --------------------------------------------------------------
9
+ // Opened from a project's menu in the sidebar or from a session's tab, and
10
+ // always about one folder: the tree is fetched a directory at a time as it is
11
+ // unfolded, so a repository with a node_modules costs nothing until somebody
12
+ // actually walks into it.
13
+
14
+ const explorer = document.getElementById("explorer");
15
+ const explorerTitle = document.getElementById("explorer-title");
16
+ const explorerTree = document.getElementById("explorer-tree");
17
+ const explorerView = document.getElementById("explorer-view");
18
+
19
+ /** The project folder on show, absolute; null while the pane has never opened. */
20
+ let explorerCwd = null;
21
+ /** Each listed folder's children, keyed by path relative to the project. */
22
+ let explorerChildren = new Map();
23
+ /** Unfolded folders, remembered per project so a return finds them as left. */
24
+ const explorerOpenDirs = new Map();
25
+ /** The file on the right, relative to the project. */
26
+ let explorerSelected = null;
27
+ /** Markdown shows rendered unless somebody asked for the source. */
28
+ let explorerSource = false;
29
+
30
+ /** Past this a file is shown as plain text: the highlighter is one regex pass. */
31
+ const EXPLORER_HIGHLIGHT_MAX = 200 * 1024;
32
+ const EXPLORER_MARKDOWN = /\.(md|markdown|mdx)$/i;
33
+
34
+ function explorerIsOpen() {
35
+ return explorer.classList.contains("open");
36
+ }
37
+
38
+ function explorerDirs() {
39
+ let dirs = explorerOpenDirs.get(explorerCwd);
40
+ if (!dirs) explorerOpenDirs.set(explorerCwd, (dirs = new Set()));
41
+ return dirs;
42
+ }
43
+
44
+ /**
45
+ * Show one project's files.
46
+ *
47
+ * The same project asked for twice is a toggle, which is what the tab's button
48
+ * wants; the project menu passes force so that choosing "Files" never closes
49
+ * the thing it just asked to see.
50
+ */
51
+ async function openExplorer(cwd, force) {
52
+ if (!cwd) return;
53
+ if (explorerIsOpen() && explorerCwd === cwd && !force) return closeExplorer();
54
+ track("panel_opened", { panel: "files" });
55
+
56
+ // One column beside the transcript at a time: the review pane, the artifact
57
+ // drawer and the browser are all the same shape, and two of them open at
58
+ // once leaves the conversation a strip down the middle.
59
+ review.classList.remove("open");
60
+ shelf.classList.remove("open");
61
+ if (browserPane.classList.contains("open")) browserOpen(false);
62
+
63
+ if (explorerCwd !== cwd) {
64
+ explorerCwd = cwd;
65
+ explorerChildren = new Map();
66
+ explorerSelected = null;
67
+ explorerSource = false;
68
+ }
69
+ explorerTitle.textContent = projectName(cwd);
70
+ explorerTitle.title = cwd;
71
+ explorer.classList.add("open");
72
+ paintTabs();
73
+ await explorerLoad();
74
+ }
75
+
76
+ function closeExplorer() {
77
+ explorer.classList.remove("open");
78
+ paintTabs();
79
+ }
80
+
81
+ /** Keep the pane on the project of whichever tab is in front. */
82
+ function explorerFollow(cwd) {
83
+ if (explorerIsOpen() && cwd && cwd !== explorerCwd) void openExplorer(cwd, true);
84
+ }
85
+
86
+ /** Every folder on screen, fetched again: what the refresh button does. */
87
+ async function explorerLoad() {
88
+ const cwd = explorerCwd;
89
+ const dirs = ["", ...explorerDirs()];
90
+ const bodies = await Promise.all(dirs.map((dir) => explorerFetch(dir)));
91
+ if (cwd !== explorerCwd) return;
92
+ explorerChildren = new Map();
93
+ dirs.forEach((dir, index) => {
94
+ const body = bodies[index];
95
+ if (body.error) explorerDirs().delete(dir);
96
+ else explorerChildren.set(dir, body.entries || []);
97
+ });
98
+ paintExplorerTree();
99
+ if (explorerSelected) await explorerShow(explorerSelected);
100
+ else explorerView.replaceChildren(el("p", "muted explorer-hint", t("files.pick")));
101
+ }
102
+
103
+ function explorerFetch(dir) {
104
+ return getJson("/files/tree?cwd=" + encodeURIComponent(explorerCwd) + "&dir=" + encodeURIComponent(dir));
105
+ }
106
+
107
+ async function explorerToggleDir(dir) {
108
+ const dirs = explorerDirs();
109
+ if (dirs.has(dir)) {
110
+ dirs.delete(dir);
111
+ return paintExplorerTree();
112
+ }
113
+ dirs.add(dir);
114
+ if (!explorerChildren.has(dir)) {
115
+ const cwd = explorerCwd;
116
+ const body = await explorerFetch(dir);
117
+ if (cwd !== explorerCwd) return;
118
+ if (body.error) {
119
+ dirs.delete(dir);
120
+ return void addNotice(body.error, "err");
121
+ }
122
+ explorerChildren.set(dir, body.entries || []);
123
+ }
124
+ paintExplorerTree();
125
+ }
126
+
127
+ function paintExplorerTree() {
128
+ const scroll = explorerTree.scrollTop;
129
+ explorerTree.replaceChildren();
130
+ const root = explorerChildren.get("") || [];
131
+ if (!root.length) explorerTree.appendChild(el("p", "muted explorer-hint", t("files.empty")));
132
+ const dirs = explorerDirs();
133
+
134
+ const walk = (entries, depth) => {
135
+ for (const entry of entries) {
136
+ const row = el("button", "explorer-row" + (entry.path === explorerSelected ? " on" : ""));
137
+ row.type = "button";
138
+ row.setAttribute("role", "treeitem");
139
+ row.style.paddingLeft = 8 + depth * 14 + "px";
140
+ row.title = entry.path;
141
+ const open = entry.dir && dirs.has(entry.path);
142
+ if (entry.dir) row.setAttribute("aria-expanded", String(open));
143
+ const caret = el("span", "explorer-caret");
144
+ if (entry.dir) caret.appendChild(svgIcon(open ? "caret" : "chevronRight"));
145
+ row.appendChild(caret);
146
+ row.appendChild(svgIcon(entry.dir ? (open ? "folderOpen" : "folder") : "file"));
147
+ row.appendChild(el("span", "explorer-name", entry.name));
148
+ row.addEventListener("click", () => {
149
+ if (entry.dir) void explorerToggleDir(entry.path);
150
+ else void explorerShow(entry.path);
151
+ });
152
+ explorerTree.appendChild(row);
153
+ if (open) walk(explorerChildren.get(entry.path) || [], depth + 1);
154
+ }
155
+ };
156
+ walk(root, 0);
157
+ explorerTree.scrollTop = scroll;
158
+ }
159
+
160
+ function explorerSize(bytes) {
161
+ if (bytes < 1024) return bytes + " B";
162
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
163
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
164
+ }
165
+
166
+ async function explorerShow(file) {
167
+ const cwd = explorerCwd;
168
+ if (explorerSelected !== file) explorerSource = false;
169
+ explorerSelected = file;
170
+ for (const row of explorerTree.children) row.classList.toggle("on", row.title === file);
171
+
172
+ const body = await getJson("/files/read?cwd=" + encodeURIComponent(cwd) + "&path=" + encodeURIComponent(file));
173
+ if (cwd !== explorerCwd || file !== explorerSelected) return;
174
+ explorerView.replaceChildren();
175
+ if (body.error) return void explorerView.appendChild(el("p", "muted explorer-hint", body.error));
176
+
177
+ const bar = el("div", "explorer-bar");
178
+ bar.appendChild(el("span", "explorer-path mono", body.path));
179
+ bar.appendChild(el("span", "explorer-size", explorerSize(body.size)));
180
+
181
+ const markdown = body.kind === "text" && EXPLORER_MARKDOWN.test(body.path);
182
+ if (markdown) {
183
+ const flip = el("button", "explorer-btn", t(explorerSource ? "files.rendered" : "files.source"));
184
+ flip.type = "button";
185
+ flip.addEventListener("click", () => {
186
+ explorerSource = !explorerSource;
187
+ void explorerShow(file);
188
+ });
189
+ bar.appendChild(flip);
190
+ }
191
+ // A reference is a path relative to the session's own tree, so it is only
192
+ // offered when the tree on show is that one.
193
+ const active = activeId && panes.get(activeId);
194
+ if (active && active.cwd === cwd) {
195
+ const mention = el("button", "explorer-btn", t("files.mention"));
196
+ mention.type = "button";
197
+ mention.addEventListener("click", () => {
198
+ addRef("file", body.path);
199
+ input.focus();
200
+ });
201
+ bar.appendChild(mention);
202
+ }
203
+ explorerView.appendChild(bar);
204
+
205
+ const content = el("div", "explorer-content");
206
+ const raw = q("/files/raw?cwd=" + encodeURIComponent(cwd) + "&path=" + encodeURIComponent(body.path));
207
+ if (body.kind === "image") {
208
+ const img = el("img", "explorer-img");
209
+ img.alt = body.path;
210
+ img.src = raw;
211
+ content.appendChild(img);
212
+ } else if (body.kind === "video" || body.kind === "audio") {
213
+ const media = el(body.kind, "explorer-" + body.kind);
214
+ media.controls = true;
215
+ media.preload = "metadata";
216
+ media.src = raw;
217
+ content.appendChild(media);
218
+ } else if (body.kind === "pdf") {
219
+ content.classList.add("explorer-fill");
220
+ const frame = el("iframe", "explorer-pdf");
221
+ frame.title = body.path;
222
+ frame.src = raw;
223
+ content.appendChild(frame);
224
+ } else if (body.kind === "text" && markdown && !explorerSource) {
225
+ const md = renderMarkdown(body.content);
226
+ md.classList.add("explorer-md");
227
+ content.appendChild(md);
228
+ } else if (body.kind === "text") {
229
+ content.appendChild(explorerCode(body.content, body.path));
230
+ } else {
231
+ content.appendChild(el("p", "muted explorer-hint",
232
+ body.kind === "large" ? t("files.large", { size: explorerSize(body.size) }) : t("files.binary")));
233
+ }
234
+ explorerView.appendChild(content);
235
+ }
236
+
237
+ /** Source with a line-number gutter; the numbers are not part of a selection. */
238
+ function explorerCode(source, file) {
239
+ const text = source.replace(/\r\n?/g, "\n");
240
+ const lines = text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n");
241
+ const wrap = el("div", "explorer-code");
242
+ const gutter = el("pre", "explorer-gutter");
243
+ gutter.setAttribute("aria-hidden", "true");
244
+ gutter.textContent = lines.map((_, index) => index + 1).join("\n");
245
+ const pre = el("pre", "code explorer-src");
246
+ const ext = (file.match(/\.([^./]+)$/) || [])[1] || "";
247
+ if (text.length > EXPLORER_HIGHLIGHT_MAX) pre.textContent = text;
248
+ else pre.appendChild(highlight(text, ext));
249
+ wrap.appendChild(gutter);
250
+ wrap.appendChild(pre);
251
+ return wrap;
252
+ }
253
+
254
+ /** The tab's two ways into its tree: the files pane and a terminal. */
255
+ function tabProjectButtons(id, cwd) {
256
+ const files = el("button", "tab-tool");
257
+ files.type = "button";
258
+ files.title = t("files.open");
259
+ files.setAttribute("aria-label", t("files.open"));
260
+ files.classList.toggle("on", id === activeId && explorerIsOpen() && explorerCwd === cwd);
261
+ files.appendChild(svgIcon("folder"));
262
+ files.addEventListener("click", async (event) => {
263
+ event.stopPropagation();
264
+ if (id !== activeId) {
265
+ await focusTab(id);
266
+ return void openExplorer(cwd, true);
267
+ }
268
+ void openExplorer(cwd);
269
+ });
270
+
271
+ const term = el("button", "tab-tool");
272
+ term.type = "button";
273
+ term.title = t("files.terminalTab");
274
+ term.setAttribute("aria-label", t("files.terminalTab"));
275
+ term.appendChild(svgIcon("command"));
276
+ term.classList.toggle("on", id === activeId && terminalShownFor(cwd));
277
+ term.addEventListener("click", async (event) => {
278
+ event.stopPropagation();
279
+ if (id !== activeId) {
280
+ await focusTab(id);
281
+ return void openTerminalIn(cwd, true);
282
+ }
283
+ void openTerminalIn(cwd);
284
+ });
285
+ return [files, term];
286
+ }
287
+
288
+ document.getElementById("explorer-close").addEventListener("click", closeExplorer);
289
+ document.getElementById("explorer-refresh").addEventListener("click", () => void explorerLoad());
290
+ document.getElementById("explorer-terminal").addEventListener("click", () => void openTerminalIn(explorerCwd, true));
291
+
292
+ // --- dragging a size -----------------------------------------------------------
293
+
294
+ /**
295
+ * A handle that resizes something while it is dragged.
296
+ *
297
+ * Pointer events, so a finger drags it as well as a mouse; the move handler is
298
+ * given the distance from where the drag started and does its own clamping.
299
+ * Arrow keys nudge it for anyone not using a pointer at all.
300
+ */
301
+ function dragHandle(handle, opts) {
302
+ handle.addEventListener("pointerdown", (event) => {
303
+ if (event.button !== 0 || (opts.enabled && !opts.enabled())) return;
304
+ event.preventDefault();
305
+ const x0 = event.clientX;
306
+ const y0 = event.clientY;
307
+ const state = opts.start();
308
+ try { handle.setPointerCapture(event.pointerId); } catch { /* the window listeners keep it going */ }
309
+ document.body.classList.add("dragging", opts.axis === "x" ? "dragging-x" : "dragging-y");
310
+ const move = (next) => opts.move(next.clientX - x0, next.clientY - y0, state);
311
+ const done = () => {
312
+ window.removeEventListener("pointermove", move);
313
+ window.removeEventListener("pointerup", done);
314
+ window.removeEventListener("pointercancel", done);
315
+ document.body.classList.remove("dragging", "dragging-x", "dragging-y");
316
+ if (opts.end) opts.end();
317
+ };
318
+ window.addEventListener("pointermove", move);
319
+ window.addEventListener("pointerup", done);
320
+ window.addEventListener("pointercancel", done);
321
+ });
322
+ handle.addEventListener("keydown", (event) => {
323
+ const keys = opts.axis === "x" ? ["ArrowLeft", "ArrowRight"] : ["ArrowUp", "ArrowDown"];
324
+ if (!keys.includes(event.key) || (opts.enabled && !opts.enabled())) return;
325
+ event.preventDefault();
326
+ const state = opts.start();
327
+ const step = event.key === keys[0] ? -16 : 16;
328
+ opts.move(opts.axis === "x" ? step : 0, opts.axis === "x" ? 0 : step, state);
329
+ if (opts.end) opts.end();
330
+ });
331
+ if (opts.reset) handle.addEventListener("dblclick", opts.reset);
332
+ }
333
+
334
+ /** A remembered number, or undefined; storage can be missing or refuse. */
335
+ function storedSize(key) {
336
+ try {
337
+ const n = Number(localStorage.getItem(key));
338
+ return Number.isFinite(n) && n > 0 ? n : undefined;
339
+ } catch {
340
+ return undefined;
341
+ }
342
+ }
343
+ function storeSize(key, value) {
344
+ try {
345
+ if (value === undefined) localStorage.removeItem(key);
346
+ else localStorage.setItem(key, String(Math.round(value)));
347
+ } catch { /* the size still holds for this page */ }
348
+ }
349
+
350
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
351
+
352
+ // The pane's width, from its left edge. Desktop only: on a phone it is the
353
+ // whole screen.
354
+ const EXPLORER_WIDTH_KEY = "oharness.explorerWidth";
355
+ const EXPLORER_TREE_KEY = "oharness.explorerTreeWidth";
356
+ const EXPLORER_TREE_H_KEY = "oharness.explorerTreeHeight";
357
+
358
+ function explorerSetWidth(px) {
359
+ const split = document.getElementById("split").getBoundingClientRect().width;
360
+ const width = clamp(px, 360, split - 360);
361
+ explorer.style.setProperty("--explorer-width", width + "px");
362
+ return width;
363
+ }
364
+ function explorerSetTree(px) {
365
+ const body = document.getElementById("explorer-body").getBoundingClientRect();
366
+ if (isPhone()) {
367
+ const height = clamp(px, 80, body.height - 120);
368
+ explorer.style.setProperty("--explorer-tree-height", height + "px");
369
+ return height;
370
+ }
371
+ const width = clamp(px, 140, body.width - 200);
372
+ explorer.style.setProperty("--explorer-tree-width", width + "px");
373
+ return width;
374
+ }
375
+ {
376
+ const width = storedSize(EXPLORER_WIDTH_KEY);
377
+ if (width) explorer.style.setProperty("--explorer-width", width + "px");
378
+ const tree = storedSize(EXPLORER_TREE_KEY);
379
+ if (tree) explorer.style.setProperty("--explorer-tree-width", tree + "px");
380
+ const treeH = storedSize(EXPLORER_TREE_H_KEY);
381
+ if (treeH) explorer.style.setProperty("--explorer-tree-height", treeH + "px");
382
+ }
383
+
384
+ let explorerDragged;
385
+ dragHandle(document.getElementById("explorer-resizer"), {
386
+ axis: "x",
387
+ enabled: () => !isPhone(),
388
+ start: () => explorer.getBoundingClientRect().width,
389
+ move: (dx, dy, start) => { explorerDragged = explorerSetWidth(start - dx); },
390
+ end: () => storeSize(EXPLORER_WIDTH_KEY, explorerDragged),
391
+ reset: () => { explorer.style.removeProperty("--explorer-width"); storeSize(EXPLORER_WIDTH_KEY); },
392
+ });
393
+
394
+ const explorerSplit = document.getElementById("explorer-split");
395
+ let explorerTreeDragged;
396
+ // Beside the file on a desktop, above it on a phone: the same handle drags
397
+ // whichever way the two are stacked.
398
+ dragHandle(explorerSplit, {
399
+ get axis() { return isPhone() ? "y" : "x"; },
400
+ start: () => {
401
+ const rect = explorerTree.getBoundingClientRect();
402
+ return isPhone() ? rect.height : rect.width;
403
+ },
404
+ move: (dx, dy, start) => { explorerTreeDragged = explorerSetTree(start + (isPhone() ? dy : dx)); },
405
+ end: () => storeSize(isPhone() ? EXPLORER_TREE_H_KEY : EXPLORER_TREE_KEY, explorerTreeDragged),
406
+ reset: () => {
407
+ explorer.style.removeProperty(isPhone() ? "--explorer-tree-height" : "--explorer-tree-width");
408
+ storeSize(isPhone() ? EXPLORER_TREE_H_KEY : EXPLORER_TREE_KEY);
409
+ },
410
+ });
411
+ `;
412
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/page/client/files.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoZ9B,CAAC"}
@@ -11,6 +11,8 @@ import { ROUTINES } from "./routines.js";
11
11
  import { USAGE } from "./usage.js";
12
12
  import { PANELS } from "./panels.js";
13
13
  import { BROWSER } from "./browser.js";
14
+ import { FILES } from "./files.js";
15
+ import { TERMINAL } from "./terminal.js";
14
16
  import { SHELL } from "./shell.js";
15
17
  /**
16
18
  * The client script, assembled.
@@ -40,6 +42,8 @@ export const APP_SCRIPT = [
40
42
  ROUTINES,
41
43
  USAGE,
42
44
  PANELS,
45
+ FILES,
46
+ TERMINAL,
43
47
  BROWSER,
44
48
  SHELL,
45
49
  ].join("") + "\n";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/page/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GACrB;IACA,IAAI;IACJ,OAAO;IACP,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,MAAM;IACN,OAAO;IACP,KAAK;CACJ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/page/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GACrB;IACA,IAAI;IACJ,OAAO;IACP,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,MAAM;IACN,KAAK;IACL,QAAQ;IACR,OAAO;IACP,KAAK;CACJ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC"}
@@ -197,7 +197,7 @@ async function showSettings() {
197
197
 
198
198
  // Rows are grouped by what they are about. Each group is filled as the page
199
199
  // 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") };
200
+ const groups = { workspace: el("div"), sync: el("div"), memory: el("div"), activity: el("div"), advanced: el("div"), about: el("div") };
201
201
  let target = groups.workspace;
202
202
 
203
203
  // A click on a button inside a summary toggles the disclosure as well as
@@ -707,11 +707,29 @@ async function showSettings() {
707
707
  body.appendChild(status);
708
708
  body.appendChild(extensionHelp(ext));
709
709
  }
710
+ // The desktop app's version, with its release notes beside it. A row that
711
+ // is always there, because the menu bar is not where people look and the
712
+ // post-update hint is gone after one click.
713
+ const desktopApp = IS_DESKTOP && window.oharness.app;
714
+ if (desktopApp && desktopApp.version) {
715
+ target = groups.about;
716
+ const notes = el("button", "ghost", t("settings.releaseNotes"));
717
+ notes.type = "button";
718
+ notes.addEventListener("click", () => desktopApp.releaseNotes());
719
+ let check = null;
720
+ if (desktopApp.checkForUpdates) {
721
+ check = el("button", "ghost", t("settings.checkUpdates"));
722
+ check.type = "button";
723
+ check.addEventListener("click", () => desktopApp.checkForUpdates());
724
+ }
725
+ field(t("settings.version"), desktopApp.version, t("settings.releaseNotesHint"), check, notes);
726
+ }
710
727
  const sections = [
711
728
  ["settings.section.workspace", [groups.workspace]],
712
729
  ["settings.section.sync", [groups.sync, groups.memory]],
713
730
  ["settings.section.activity", [groups.activity]],
714
731
  ["settings.section.advanced", [groups.advanced]],
732
+ ["settings.section.about", [groups.about]],
715
733
  ];
716
734
  for (const [title, parts] of sections) {
717
735
  if (!parts.some((part) => part.childNodes.length)) continue;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFAu4BgD,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFAy5BgD,CAAC"}
@@ -648,7 +648,13 @@ void (async () => {
648
648
  // Reopen whatever the server still has running, so a reload does not silently
649
649
  // abandon turns that are still streaming in other tabs.
650
650
  const opened = await getJson("/open");
651
- const list = (opened.open || []).length ? opened.open : [{ id: state.sessionId, current: true }];
651
+ // With nothing open yet — a server just started — the one tab is the
652
+ // current session, and it needs its tree like any other: without it the tab
653
+ // lost its project name, its files and terminal buttons, and anything that
654
+ // reads a file from the project.
655
+ const list = (opened.open || []).length
656
+ ? opened.open
657
+ : [{ id: state.sessionId, title: state.title, cwd: state.cwd, current: true }];
652
658
  for (const entry of list) {
653
659
  const record = pane(entry.id);
654
660
  record.title = entry.title || record.title;
@@ -662,6 +668,10 @@ void (async () => {
662
668
  P = pane(activeId);
663
669
  await loadModels();
664
670
  applyState(state);
671
+ if (!P.cwd && state.cwd && state.sessionId === activeId) {
672
+ P.cwd = state.cwd;
673
+ paintTabs();
674
+ }
665
675
  const cwdEl = document.getElementById("empty-cwd");
666
676
  if (cwdEl) cwdEl.textContent = projectName(state.cwd || "");
667
677
 
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2qBzB,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAqrBzB,CAAC"}
@@ -276,9 +276,17 @@ function projectHead(cwd, name, count, open) {
276
276
  closeRowMenu();
277
277
  void chooseProject(cwd);
278
278
  } }),
279
- menuItem(t("projects.rename"), { key: "n", run: () => {
279
+ menuItem(t(pinned ? "projects.unpin" : "projects.pin"), { key: "p", run: () => {
280
280
  closeRowMenu();
281
- void renameProjectUi(cwd, name);
281
+ void togglePin("project", cwd, !pinned);
282
+ } }),
283
+ menuItem(t("projects.files"), { key: "i", run: () => {
284
+ closeRowMenu();
285
+ void openExplorer(cwd, true);
286
+ } }),
287
+ menuItem(t("projects.terminal"), { key: "t", run: () => {
288
+ closeRowMenu();
289
+ void openTerminalIn(cwd, true);
282
290
  } }),
283
291
  menuItem(t(IS_MAC ? "projects.revealMac" : IS_WIN ? "projects.revealWin" : "projects.reveal"), { key: "f", run: () => {
284
292
  closeRowMenu();
@@ -290,9 +298,9 @@ function projectHead(cwd, name, count, open) {
290
298
  closeRowMenu();
291
299
  exportProjectUi(cwd);
292
300
  } }),
293
- menuItem(t(pinned ? "projects.unpin" : "projects.pin"), { key: "p", run: () => {
301
+ menuItem(t("projects.rename"), { key: "n", run: () => {
294
302
  closeRowMenu();
295
- void togglePin("project", cwd, !pinned);
303
+ void renameProjectUi(cwd, name);
296
304
  } }),
297
305
  ];
298
306
  // Forgetting a path is only offered where it can do something: this row
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/page/client/state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmvC9B,CAAC"}
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/page/client/state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2vC9B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The embedded terminal: a real shell per project, drawn by xterm.js in a
3
+ * panel under the conversation.
4
+ *
5
+ * One part of the client script; see ./index.ts for how the parts join.
6
+ */
7
+ export declare const TERMINAL: string;