@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.
- package/dist/files.d.ts +40 -1
- package/dist/files.js +94 -3
- package/dist/files.js.map +1 -1
- package/dist/i18n.js +58 -0
- package/dist/i18n.js.map +1 -1
- package/dist/page/client/base.js +4 -0
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +3 -1
- package/dist/page/client/browser.js.map +1 -1
- package/dist/page/client/composer.js +20 -0
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/files.d.ts +7 -0
- package/dist/page/client/files.js +412 -0
- package/dist/page/client/files.js.map +1 -0
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/routines.js +19 -1
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +11 -1
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +12 -4
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.d.ts +7 -0
- package/dist/page/client/terminal.js +347 -0
- package/dist/page/client/terminal.js.map +1 -0
- package/dist/page/client/transcript.js +167 -0
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/style.js +226 -3
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +35 -1
- package/dist/page.js.map +1 -1
- package/dist/server.js +143 -1
- package/dist/server.js.map +1 -1
- package/dist/terminal.d.ts +34 -0
- package/dist/terminal.js +211 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +9 -8
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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.
|
|
279
|
+
menuItem(t(pinned ? "projects.unpin" : "projects.pin"), { key: "p", run: () => {
|
|
280
280
|
closeRowMenu();
|
|
281
|
-
void
|
|
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(
|
|
301
|
+
menuItem(t("projects.rename"), { key: "n", run: () => {
|
|
294
302
|
closeRowMenu();
|
|
295
|
-
void
|
|
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
|
|
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"}
|