@oharness/server 0.7.1 → 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.
- package/dist/browser-pane.d.ts +48 -0
- package/dist/browser-pane.js +185 -0
- package/dist/browser-pane.js.map +1 -0
- package/dist/i18n.js +132 -0
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +20 -0
- package/dist/main.js.map +1 -1
- package/dist/page/client/account.d.ts +8 -0
- package/dist/page/client/account.js +1895 -0
- package/dist/page/client/account.js.map +1 -0
- package/dist/page/client/artifacts.d.ts +7 -0
- package/dist/page/client/artifacts.js +767 -0
- package/dist/page/client/artifacts.js.map +1 -0
- package/dist/page/client/base.d.ts +8 -0
- package/dist/page/client/base.js +400 -0
- package/dist/page/client/base.js.map +1 -0
- package/dist/page/client/browser.d.ts +21 -0
- package/dist/page/client/browser.js +904 -0
- package/dist/page/client/browser.js.map +1 -0
- package/dist/page/client/composer.d.ts +8 -0
- package/dist/page/client/composer.js +1694 -0
- package/dist/page/client/composer.js.map +1 -0
- package/dist/page/client/dialogs.d.ts +7 -0
- package/dist/page/client/dialogs.js +724 -0
- package/dist/page/client/dialogs.js.map +1 -0
- package/dist/page/client/events.d.ts +7 -0
- package/dist/page/client/events.js +288 -0
- package/dist/page/client/events.js.map +1 -0
- package/dist/page/client/index.d.ts +16 -0
- package/dist/page/client/index.js +46 -0
- package/dist/page/client/index.js.map +1 -0
- package/dist/page/client/panels.d.ts +7 -0
- package/dist/page/client/panels.js +659 -0
- package/dist/page/client/panels.js.map +1 -0
- package/dist/page/client/pickers.d.ts +7 -0
- package/dist/page/client/pickers.js +624 -0
- package/dist/page/client/pickers.js.map +1 -0
- package/dist/page/client/routines.d.ts +6 -0
- package/dist/page/client/routines.js +910 -0
- package/dist/page/client/routines.js.map +1 -0
- package/dist/page/client/shell.d.ts +6 -0
- package/dist/page/client/shell.js +690 -0
- package/dist/page/client/shell.js.map +1 -0
- package/dist/page/client/state.d.ts +7 -0
- package/dist/page/client/state.js +1275 -0
- package/dist/page/client/state.js.map +1 -0
- package/dist/page/client/transcript.d.ts +8 -0
- package/dist/page/client/transcript.js +699 -0
- package/dist/page/client/transcript.js.map +1 -0
- package/dist/page/client/usage.d.ts +7 -0
- package/dist/page/client/usage.js +954 -0
- package/dist/page/client/usage.js.map +1 -0
- package/dist/page/style.d.ts +1 -0
- package/dist/page/style.js +2793 -0
- package/dist/page/style.js.map +1 -0
- package/dist/page.js +62 -13871
- package/dist/page.js.map +1 -1
- package/dist/server.d.ts +24 -0
- package/dist/server.js +349 -22
- package/dist/server.js.map +1 -1
- 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"}
|