@oharness/server 0.9.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +500 -64
- package/dist/i18n.js.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +570 -27
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +55 -12
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +802 -59
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/computer.d.ts +27 -0
- package/dist/page/client/computer.js +358 -0
- package/dist/page/client/computer.js.map +1 -0
- package/dist/page/client/dialogs.js +168 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +119 -9
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/files.js +86 -1
- package/dist/page/client/files.js.map +1 -1
- package/dist/page/client/index.js +8 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +14 -10
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +432 -45
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +386 -104
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +278 -19
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/tasks.d.ts +8 -0
- package/dist/page/client/tasks.js +293 -0
- package/dist/page/client/tasks.js.map +1 -0
- package/dist/page/client/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +196 -8
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +1 -11
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +745 -76
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +94 -16
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +31 -0
- package/dist/prefs.js +13 -1
- package/dist/prefs.js.map +1 -1
- package/dist/remote-crypto.d.ts +78 -0
- package/dist/remote-crypto.js +216 -0
- package/dist/remote-crypto.js.map +1 -0
- package/dist/remote.d.ts +103 -0
- package/dist/remote.js +498 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +1738 -73
- package/dist/server.js.map +1 -1
- package/dist/starters.d.ts +92 -0
- package/dist/starters.js +213 -0
- package/dist/starters.js.map +1 -0
- package/dist/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +28 -2
- package/dist/terminal.js +45 -3
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/turn-shots.d.ts +43 -0
- package/dist/turn-shots.js +80 -0
- package/dist/turn-shots.js.map +1 -0
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -20,7 +20,8 @@ let currentCwd = "";
|
|
|
20
20
|
function formatTokens(n) {
|
|
21
21
|
if (n < 1000) return String(n);
|
|
22
22
|
if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0) + "k";
|
|
23
|
-
|
|
23
|
+
// "1M", not "1.0M": a model's window is usually a round number.
|
|
24
|
+
return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -44,10 +45,129 @@ function paintTokens(usage) {
|
|
|
44
45
|
: "";
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/*
|
|
49
|
+
* The context panel: what the ring stands for, opened by clicking it. The
|
|
50
|
+
* window split into what the model is given every time (system prompt, tool
|
|
51
|
+
* definitions), the conversation, and what is left; where automatic
|
|
52
|
+
* compaction fires; and a way to compact now instead of waiting for it.
|
|
53
|
+
*/
|
|
54
|
+
let ctxState = null;
|
|
55
|
+
let ctxPanel = null;
|
|
56
|
+
|
|
57
|
+
const CTX_PARTS = [
|
|
58
|
+
{ key: "system", label: "ctx.system", color: "var(--ctx-system)" },
|
|
59
|
+
{ key: "tools", label: "ctx.tools", color: "var(--ctx-tools)" },
|
|
60
|
+
{ key: "messages", label: "ctx.messages", color: "var(--ctx-messages)" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function paintCtxPanel() {
|
|
64
|
+
const c = ctxState;
|
|
65
|
+
const panel = ctxPanel;
|
|
66
|
+
panel.replaceChildren();
|
|
67
|
+
const pct = (n) => Math.round((n / c.window) * 1000) / 10;
|
|
68
|
+
|
|
69
|
+
const head = el("div", "ctx-head");
|
|
70
|
+
head.appendChild(el("span", "ctx-title", t("ctx.title")));
|
|
71
|
+
head.appendChild(el("span", "ctx-total", t("ctx.total", {
|
|
72
|
+
used: formatTokens(c.total),
|
|
73
|
+
total: formatTokens(c.window),
|
|
74
|
+
pct: Math.round((c.total / c.window) * 100),
|
|
75
|
+
})));
|
|
76
|
+
panel.appendChild(head);
|
|
77
|
+
|
|
78
|
+
// One bar, a segment per part, the empty rest as track. The tick marks where
|
|
79
|
+
// automatic compaction fires; it is a fill two pixels wide, not a rule.
|
|
80
|
+
const bar = el("div", "ctx-bar");
|
|
81
|
+
for (const part of CTX_PARTS) {
|
|
82
|
+
const seg = el("i");
|
|
83
|
+
seg.style.width = Math.min(100, pct(c[part.key])) + "%";
|
|
84
|
+
seg.style.background = part.color;
|
|
85
|
+
bar.appendChild(seg);
|
|
86
|
+
}
|
|
87
|
+
if (c.compactAt) {
|
|
88
|
+
const tick = el("b", "ctx-tick");
|
|
89
|
+
tick.style.left = Math.min(100, pct(c.compactAt)) + "%";
|
|
90
|
+
bar.appendChild(tick);
|
|
91
|
+
}
|
|
92
|
+
panel.appendChild(bar);
|
|
93
|
+
|
|
94
|
+
const rows = el("div", "ctx-rows");
|
|
95
|
+
const row = (color, label, value) => {
|
|
96
|
+
const r = el("div", "ctx-row");
|
|
97
|
+
const dot = el("span", "ctx-dot");
|
|
98
|
+
dot.style.background = color;
|
|
99
|
+
r.appendChild(dot);
|
|
100
|
+
r.appendChild(el("span", "ctx-label", label));
|
|
101
|
+
r.appendChild(el("span", "ctx-value", value));
|
|
102
|
+
rows.appendChild(r);
|
|
103
|
+
};
|
|
104
|
+
for (const part of CTX_PARTS) row(part.color, t(part.label), formatTokens(c[part.key]));
|
|
105
|
+
row("var(--track)", t("ctx.free"), formatTokens(Math.max(0, c.window - c.total)));
|
|
106
|
+
panel.appendChild(rows);
|
|
107
|
+
|
|
108
|
+
const notes = el("div", "ctx-notes");
|
|
109
|
+
notes.appendChild(el("div", "", c.compactAt
|
|
110
|
+
? t("ctx.autoAt", { n: formatTokens(c.compactAt) })
|
|
111
|
+
: t("ctx.autoOff")));
|
|
112
|
+
// Said plainly, because the two numbers can disagree by a lot: a provider's
|
|
113
|
+
// count of the prompt and a character-based guess at it.
|
|
114
|
+
notes.appendChild(el("div", "", t(c.measured ? "ctx.measured" : "ctx.estimated")));
|
|
115
|
+
panel.appendChild(notes);
|
|
116
|
+
|
|
117
|
+
const compact = el("button", "menu-item ctx-compact");
|
|
118
|
+
compact.type = "button";
|
|
119
|
+
compact.appendChild(el("span", "menu-label", t("ctx.compactNow")));
|
|
120
|
+
compact.addEventListener("click", () => { closeCtxPanel(); compactConversation(); });
|
|
121
|
+
panel.appendChild(compact);
|
|
122
|
+
placeCtxPanel();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function placeCtxPanel() {
|
|
126
|
+
const box = ctxRing.getBoundingClientRect();
|
|
127
|
+
// Above the ring and flush with its right edge: the ring sits at the right
|
|
128
|
+
// end of the line under the composer, with the box it belongs to above it.
|
|
129
|
+
ctxPanel.style.right = Math.max(8, window.innerWidth - box.right) + "px";
|
|
130
|
+
ctxPanel.style.bottom = (window.innerHeight - box.top + 8) + "px";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onCtxAway(event) {
|
|
134
|
+
if (!ctxPanel.contains(event.target) && !ctxRing.contains(event.target)) closeCtxPanel();
|
|
135
|
+
}
|
|
136
|
+
function onCtxKey(event) {
|
|
137
|
+
if (event.key === "Escape") { event.stopPropagation(); closeCtxPanel(); ctxRing.focus(); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function closeCtxPanel() {
|
|
141
|
+
if (!ctxPanel) return;
|
|
142
|
+
ctxPanel.remove();
|
|
143
|
+
ctxPanel = null;
|
|
144
|
+
ctxRing.setAttribute("aria-expanded", "false");
|
|
145
|
+
document.removeEventListener("mousedown", onCtxAway, true);
|
|
146
|
+
document.removeEventListener("keydown", onCtxKey, true);
|
|
147
|
+
window.removeEventListener("resize", placeCtxPanel);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ctxRing.addEventListener("click", () => {
|
|
151
|
+
if (ctxPanel) return closeCtxPanel();
|
|
152
|
+
if (!ctxState) return;
|
|
153
|
+
ctxPanel = el("div", "menu pop ctx-panel");
|
|
154
|
+
ctxPanel.setAttribute("role", "dialog");
|
|
155
|
+
ctxPanel.setAttribute("aria-label", t("ctx.title"));
|
|
156
|
+
document.body.appendChild(ctxPanel);
|
|
157
|
+
ctxRing.setAttribute("aria-expanded", "true");
|
|
158
|
+
paintCtxPanel();
|
|
159
|
+
document.addEventListener("mousedown", onCtxAway, true);
|
|
160
|
+
document.addEventListener("keydown", onCtxKey, true);
|
|
161
|
+
window.addEventListener("resize", placeCtxPanel);
|
|
162
|
+
});
|
|
163
|
+
|
|
47
164
|
function applyState(s) {
|
|
48
165
|
if (!s) return;
|
|
49
166
|
if (s.model) modelPicker.set(modelPicker.options || [], s.model);
|
|
50
167
|
if (s.mode) modePicker.set(MODES, s.mode);
|
|
168
|
+
// Kept per tab for the + menu's Plan mode row, which reads it.
|
|
169
|
+
if (s.sessionId && s.mode) pane(s.sessionId).mode = s.mode;
|
|
170
|
+
if (s.sessionId && Array.isArray(s.tasks)) tasksChanged(s.sessionId, s.tasks);
|
|
51
171
|
effortPicker.set(s);
|
|
52
172
|
if (s.cwd) {
|
|
53
173
|
// Kept so the approval dialog can show a path the way the user thinks of
|
|
@@ -60,22 +180,37 @@ function applyState(s) {
|
|
|
60
180
|
metaEl.textContent = s.costUsd > 0 ? "$" + s.costUsd.toFixed(4) : "";
|
|
61
181
|
paintTokens(s.usage);
|
|
62
182
|
const meta = document.getElementById("meta");
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
meta.classList.toggle("has-ctx",
|
|
68
|
-
if (
|
|
183
|
+
// Shown once the conversation has anything in it: how full the window is
|
|
184
|
+
// decides when earlier detail starts being summarized away, and that is
|
|
185
|
+
// worth knowing before it happens rather than only once it is close.
|
|
186
|
+
const hasCtx = typeof s.contextUsed === "number" && s.contextTokens > 0;
|
|
187
|
+
meta.classList.toggle("has-ctx", hasCtx);
|
|
188
|
+
if (hasCtx) {
|
|
69
189
|
const pct = Math.round(s.contextUsed * 100);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
190
|
+
// At least a sliver: an empty ring reads as a meter that is not working.
|
|
191
|
+
ctxRing.style.setProperty("--ctx-pct", String(Math.max(pct, 3)));
|
|
192
|
+
ctxRing.style.setProperty("--ctx-fill", pct >= 90 ? "var(--err)" : pct >= 70 ? "var(--warn)" : "var(--brand)");
|
|
193
|
+
const label = t("usage.contextOf", {
|
|
194
|
+
used: formatTokens(s.contextTokens),
|
|
195
|
+
total: formatTokens(s.contextWindow),
|
|
196
|
+
pct,
|
|
197
|
+
});
|
|
198
|
+
ctxRing.title = label;
|
|
199
|
+
ctxRing.setAttribute("aria-label", label);
|
|
73
200
|
}
|
|
201
|
+
// The panel reads the latest breakdown, and redraws if it is open, so a turn
|
|
202
|
+
// finishing under it is not shown with the numbers from before.
|
|
203
|
+
ctxState = hasCtx && s.context ? s.context : null;
|
|
204
|
+
if (ctxPanel) ctxState ? paintCtxPanel() : closeCtxPanel();
|
|
74
205
|
setBusy(Boolean(s.busy));
|
|
75
206
|
// Addressed by session: a reload and a tab switch both land here, and the
|
|
76
207
|
// pane this state describes is not always the one an event last touched.
|
|
77
208
|
if (s.sessionId) pane(s.sessionId).queued = s.queued || [];
|
|
209
|
+
if (s.sessionId) pane(s.sessionId).goal = s.goal || "";
|
|
210
|
+
// Absent outside the desktop app: no helper, no grants, no row to offer.
|
|
211
|
+
if (s.sessionId) pane(s.sessionId).computer = s.computerUse || null;
|
|
78
212
|
renderQueued();
|
|
213
|
+
renderAttachments();
|
|
79
214
|
// The state knows the session's name; a tab opened before it had one would
|
|
80
215
|
// otherwise keep the placeholder until the page reloaded.
|
|
81
216
|
if (s.sessionId && s.title && pane(s.sessionId).title !== s.title) {
|
|
@@ -201,16 +336,49 @@ function routineHead(count, open) {
|
|
|
201
336
|
return head;
|
|
202
337
|
}
|
|
203
338
|
|
|
339
|
+
/**
|
|
340
|
+
* The pin on a pinned row, which unpins it when clicked.
|
|
341
|
+
*
|
|
342
|
+
* It used to be a mark only, drawn inside the row's own button: clicking it
|
|
343
|
+
* folded the project or opened the session, and the way to unpin was hidden
|
|
344
|
+
* in the ⋯ menu while the thing that looked like the control did nothing of
|
|
345
|
+
* the kind. A span with a button's role rather than a <button>, because the
|
|
346
|
+
* project header it sits in is already a button and buttons do not nest.
|
|
347
|
+
*/
|
|
348
|
+
function pinMark(label, unpin) {
|
|
349
|
+
const mark = el("span", "pin-mark");
|
|
350
|
+
mark.appendChild(svgIcon("pin"));
|
|
351
|
+
mark.setAttribute("role", "button");
|
|
352
|
+
mark.tabIndex = 0;
|
|
353
|
+
mark.title = label;
|
|
354
|
+
mark.setAttribute("aria-label", label);
|
|
355
|
+
const act = (e) => {
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
e.stopPropagation();
|
|
358
|
+
unpin();
|
|
359
|
+
};
|
|
360
|
+
mark.addEventListener("click", act);
|
|
361
|
+
mark.addEventListener("keydown", (e) => {
|
|
362
|
+
if (e.key === "Enter" || e.key === " ") act(e);
|
|
363
|
+
});
|
|
364
|
+
// A drag that starts on the pin must not carry the row away with it.
|
|
365
|
+
mark.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
366
|
+
return mark;
|
|
367
|
+
}
|
|
368
|
+
|
|
204
369
|
/** A project header: a folder, a name, and how many sessions it holds. */
|
|
205
370
|
function projectHead(cwd, name, count, open) {
|
|
206
|
-
const
|
|
371
|
+
const wt = worktreeAt(cwd);
|
|
372
|
+
const head = el("button", "fold" + (open ? " open" : "") + (wt ? " wt" : ""));
|
|
207
373
|
head.type = "button";
|
|
208
374
|
// The folder itself says which state it is in — it is the only thing on the
|
|
209
|
-
// row that can, now that there is no arrow beside it.
|
|
210
|
-
|
|
375
|
+
// row that can, now that there is no arrow beside it. A worktree is a branch
|
|
376
|
+
// off the project above it, and draws as one.
|
|
377
|
+
head.appendChild(svgIcon(wt ? "worktree" : open ? "folderOpen" : "folder"));
|
|
211
378
|
head.appendChild(el("span", "fold-name", name));
|
|
379
|
+
if (wt) head.title = (wt.branch ? wt.branch + " · " : "") + wt.root;
|
|
212
380
|
if ((projects.find((project) => project.path === cwd) || {}).pinned) {
|
|
213
|
-
head.appendChild(
|
|
381
|
+
head.appendChild(pinMark(t("projects.unpin"), () => void togglePin("project", cwd, false)));
|
|
214
382
|
}
|
|
215
383
|
head.addEventListener("click", () => toggleProject(cwd, open));
|
|
216
384
|
|
|
@@ -271,7 +439,11 @@ function projectHead(cwd, name, count, open) {
|
|
|
271
439
|
} });
|
|
272
440
|
|
|
273
441
|
const pinned = (projects.find((project) => project.path === cwd) || {}).pinned === true;
|
|
442
|
+
const finish = wt
|
|
443
|
+
? [menuItem(t("worktree.finish"), { key: "w", run: () => void showWorktreeFinish(cwd, anchor, head) }), menuSep()]
|
|
444
|
+
: [];
|
|
274
445
|
const items = [
|
|
446
|
+
...finish,
|
|
275
447
|
menuItem(t("projects.open"), { key: "o", run: () => {
|
|
276
448
|
closeRowMenu();
|
|
277
449
|
void chooseProject(cwd);
|
|
@@ -288,6 +460,10 @@ function projectHead(cwd, name, count, open) {
|
|
|
288
460
|
closeRowMenu();
|
|
289
461
|
void openTerminalIn(cwd, true);
|
|
290
462
|
} }),
|
|
463
|
+
menuItem(t("projects.claudeCode"), { key: "c", run: () => {
|
|
464
|
+
closeRowMenu();
|
|
465
|
+
void openClaudeCode(cwd);
|
|
466
|
+
} }),
|
|
291
467
|
menuItem(t(IS_MAC ? "projects.revealMac" : IS_WIN ? "projects.revealWin" : "projects.reveal"), { key: "f", run: () => {
|
|
292
468
|
closeRowMenu();
|
|
293
469
|
void api("/reveal", { path: cwd }).then((body) => {
|
|
@@ -298,10 +474,11 @@ function projectHead(cwd, name, count, open) {
|
|
|
298
474
|
closeRowMenu();
|
|
299
475
|
exportProjectUi(cwd);
|
|
300
476
|
} }),
|
|
301
|
-
|
|
477
|
+
// A worktree's name is the one its branch and registry entry know it by.
|
|
478
|
+
...(wt ? [] : [menuItem(t("projects.rename"), { key: "n", run: () => {
|
|
302
479
|
closeRowMenu();
|
|
303
480
|
void renameProjectUi(cwd, name);
|
|
304
|
-
} }),
|
|
481
|
+
} })]),
|
|
305
482
|
];
|
|
306
483
|
// Forgetting a path is only offered where it can do something: this row
|
|
307
484
|
// stays on screen either way, because the sidebar groups by the tree a
|
|
@@ -491,7 +668,7 @@ function paintSessions() {
|
|
|
491
668
|
// recent session; the rest follow by how recently they were touched — and
|
|
492
669
|
// then a pin or a drag overrides both, because those were said out loud.
|
|
493
670
|
const pinnedProject = (cwd) => (projects.find((p) => p.path === cwd) || {}).pinned === true;
|
|
494
|
-
const
|
|
671
|
+
const sorted = arrange(
|
|
495
672
|
[...byProject.keys()].sort((a, b) => {
|
|
496
673
|
if (a === here) return -1;
|
|
497
674
|
if (b === here) return 1;
|
|
@@ -501,6 +678,23 @@ function paintSessions() {
|
|
|
501
678
|
projectOrder,
|
|
502
679
|
pinnedProject,
|
|
503
680
|
);
|
|
681
|
+
/*
|
|
682
|
+
* Worktrees sit directly under the project they came from, whatever their
|
|
683
|
+
* own recency — they are that project's parallel work, not projects of
|
|
684
|
+
* their own. One with no session yet still shows, so it can be finished.
|
|
685
|
+
*/
|
|
686
|
+
const order = sorted.filter((cwd) => !worktreeAt(cwd) || !sorted.includes(worktreeAt(cwd).parent));
|
|
687
|
+
for (const wt of worktrees) {
|
|
688
|
+
if (!byProject.has(wt.path)) byProject.set(wt.path, []);
|
|
689
|
+
const at = order.indexOf(wt.parent);
|
|
690
|
+
if (at === -1) {
|
|
691
|
+
if (!order.includes(wt.path) && byProject.get(wt.path).length) order.push(wt.path);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
let end = at + 1;
|
|
695
|
+
while (end < order.length && worktreeAt(order[end]) && worktreeAt(order[end]).parent === wt.parent) end++;
|
|
696
|
+
order.splice(end, 0, wt.path);
|
|
697
|
+
}
|
|
504
698
|
|
|
505
699
|
// Everything here belongs to the project already named at the top of the
|
|
506
700
|
// sidebar: a header repeating it is noise, and the rows do not need
|
|
@@ -741,6 +935,59 @@ function pointAnchor(e) {
|
|
|
741
935
|
return { getBoundingClientRect: () => rect };
|
|
742
936
|
}
|
|
743
937
|
|
|
938
|
+
/**
|
|
939
|
+
* The ways out of a worktree, in the menu it was asked from.
|
|
940
|
+
*
|
|
941
|
+
* Asked of the server each time rather than drawn from what the sidebar holds:
|
|
942
|
+
* whether there is anything to lose is the whole question, and the answer
|
|
943
|
+
* changes with every edit made in there.
|
|
944
|
+
*/
|
|
945
|
+
async function showWorktreeFinish(cwd, anchor, head) {
|
|
946
|
+
const body = await getJson("/worktree/status?path=" + encodeURIComponent(cwd));
|
|
947
|
+
if (body.error) return void addNotice(body.error, "notice err");
|
|
948
|
+
const wt = body.worktree;
|
|
949
|
+
const st = body.status;
|
|
950
|
+
const name = projectName(wt.root);
|
|
951
|
+
const parentName = projectName(wt.parent);
|
|
952
|
+
const facts = st.clean
|
|
953
|
+
? t("worktree.clean")
|
|
954
|
+
: [
|
|
955
|
+
st.commits ? t("worktree.commits", { n: st.commits, branch: wt.branch || "" }) : "",
|
|
956
|
+
st.changes.length ? t("worktree.changes", { n: st.changes.length }) : "",
|
|
957
|
+
].filter(Boolean).join(" · ");
|
|
958
|
+
const keys = { remove: "r", discard: "d", commit: "c", push: "p", merge: "m", apply: "a" };
|
|
959
|
+
const items = body.actions.map((action) =>
|
|
960
|
+
menuItem(t("worktree.action." + action, { name: parentName }), {
|
|
961
|
+
key: keys[action],
|
|
962
|
+
danger: action === "discard",
|
|
963
|
+
run: () => {
|
|
964
|
+
closeRowMenu();
|
|
965
|
+
void finishWorktreeUi(wt, action, name, st.clean);
|
|
966
|
+
},
|
|
967
|
+
}),
|
|
968
|
+
);
|
|
969
|
+
openRowMenu(anchor, head, [el("div", "menu-note", name + " — " + facts), ...items]);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function finishWorktreeUi(wt, action, name, clean) {
|
|
973
|
+
const body = { path: wt.path, action };
|
|
974
|
+
if (action === "commit") {
|
|
975
|
+
const message = await askText({ title: t("worktree.commitPrompt"), value: wt.task || "", ok: t("dialog.ok") });
|
|
976
|
+
if (!message) return;
|
|
977
|
+
body.message = message;
|
|
978
|
+
}
|
|
979
|
+
if (action === "discard" && !clean && !(await askConfirm({ body: t("worktree.discardConfirm", { name }), ok: t("dialog.discard"), danger: true }))) return;
|
|
980
|
+
const answer = await api("/worktree/finish", body, 5 * 60_000);
|
|
981
|
+
if (!answer || answer.error) return void addNotice((answer && answer.error) || t("worktree.failed"), "notice err");
|
|
982
|
+
addNotice(answer.result.message);
|
|
983
|
+
if (answer.result.url) window.open(answer.result.url, "_blank", "noopener");
|
|
984
|
+
// Its tabs and harness are gone with it; reloading drops every pane and
|
|
985
|
+
// path that pointed there at once, the way a rename does.
|
|
986
|
+
if (answer.result.removed) return void window.location.reload();
|
|
987
|
+
await loadProjects();
|
|
988
|
+
await loadSessions();
|
|
989
|
+
}
|
|
990
|
+
|
|
744
991
|
function openRowMenu(button, row, items) {
|
|
745
992
|
closeRowMenu();
|
|
746
993
|
rowMenu.replaceChildren(...items);
|
|
@@ -784,7 +1031,7 @@ function sessionRow(s, nested) {
|
|
|
784
1031
|
item.appendChild(el("div", "title", s.title || t("session.untitled")));
|
|
785
1032
|
// Outside the title rather than in it: that line ellipsises, and a mark that
|
|
786
1033
|
// vanishes with the end of a long name is not a mark.
|
|
787
|
-
if (s.pinned) item.appendChild(
|
|
1034
|
+
if (s.pinned) item.appendChild(pinMark(t("session.unpin"), () => void togglePin("session", s.id, false)));
|
|
788
1035
|
|
|
789
1036
|
draggable(item, {
|
|
790
1037
|
kind: "session",
|
|
@@ -1204,6 +1451,11 @@ function renderHistory(entries, into) {
|
|
|
1204
1451
|
P.turn = null;
|
|
1205
1452
|
P.streaming = null;
|
|
1206
1453
|
for (const entry of entries || []) {
|
|
1454
|
+
if (entry.tasks) {
|
|
1455
|
+
P.turn = null;
|
|
1456
|
+
taskCard(entry.tasks);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1207
1459
|
if (entry.role === "user") {
|
|
1208
1460
|
if (entry.text || (entry.images && entry.images.length)) {
|
|
1209
1461
|
addUser(entry.text || "", entry.images);
|
|
@@ -1224,7 +1476,8 @@ function renderHistory(entries, into) {
|
|
|
1224
1476
|
if (entry.text) assistantTurn().appendChild(renderMarkdown(entry.text));
|
|
1225
1477
|
for (const call of entry.tools || []) toolCard(call.id, call.name, call.name, call.input);
|
|
1226
1478
|
for (const result of entry.results || []) {
|
|
1227
|
-
|
|
1479
|
+
const shots = (result.images || []).map((image) => ({ type: "image", src: image.src }));
|
|
1480
|
+
finishTool(result.id, [{ type: "text", text: result.text }, ...shots], 0, result.isError);
|
|
1228
1481
|
}
|
|
1229
1482
|
// The cards, again. Live, these come from the "artifact" event, which a
|
|
1230
1483
|
// replay does not produce — so a resumed conversation had every message and
|
|
@@ -1237,6 +1490,8 @@ function renderHistory(entries, into) {
|
|
|
1237
1490
|
const card = replayedArtifactCard(call.input);
|
|
1238
1491
|
if (card) assistantTurn().appendChild(card);
|
|
1239
1492
|
}
|
|
1493
|
+
if (entry.shots) turnShots(assistantTurn(), entry.shots);
|
|
1494
|
+
if (entry.agent) agentReply(assistantTurn(), entry.agent);
|
|
1240
1495
|
if (P.turn) turnActions(P.turn, entry.keepMessages);
|
|
1241
1496
|
}
|
|
1242
1497
|
for (const card of connectionCards.values()) {
|
|
@@ -1254,7 +1509,11 @@ function renderHistory(entries, into) {
|
|
|
1254
1509
|
async function switchTo(body) {
|
|
1255
1510
|
const state = await api("/session", body);
|
|
1256
1511
|
if (state.error) return void addNotice(state.error, "err");
|
|
1512
|
+
await openState(state);
|
|
1513
|
+
}
|
|
1257
1514
|
|
|
1515
|
+
/** Show a session the server has just opened, as its own focused tab. */
|
|
1516
|
+
async function openState(state) {
|
|
1258
1517
|
// A tab per session, focused on arrival.
|
|
1259
1518
|
const record = pane(state.sessionId);
|
|
1260
1519
|
record.title = state.title || "";
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8/C9B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background tasks: their rows over the composer, and the sheet one opens
|
|
3
|
+
* into — what the subagent was told, what it did, what it said — with a way
|
|
4
|
+
* to stop it or tell it something.
|
|
5
|
+
*
|
|
6
|
+
* One part of the client script; see ./index.ts for how the parts join.
|
|
7
|
+
*/
|
|
8
|
+
export declare const TASKS: string;
|