@oharness/server 0.9.3 → 0.10.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/i18n.js +354 -62
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/page/client/account.js +430 -23
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +20 -1
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/composer.js +693 -54
- 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 +93 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +94 -7
- 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 +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +10 -6
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/routines.js +296 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +187 -51
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +273 -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/transcript.js +137 -5
- 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 +568 -60
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +90 -11
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +14 -0
- package/dist/prefs.js +8 -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 +1291 -49
- 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/turn-shots.d.ts +27 -0
- package/dist/turn-shots.js +71 -0
- package/dist/turn-shots.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);
|
|
@@ -298,10 +470,11 @@ function projectHead(cwd, name, count, open) {
|
|
|
298
470
|
closeRowMenu();
|
|
299
471
|
exportProjectUi(cwd);
|
|
300
472
|
} }),
|
|
301
|
-
|
|
473
|
+
// A worktree's name is the one its branch and registry entry know it by.
|
|
474
|
+
...(wt ? [] : [menuItem(t("projects.rename"), { key: "n", run: () => {
|
|
302
475
|
closeRowMenu();
|
|
303
476
|
void renameProjectUi(cwd, name);
|
|
304
|
-
} }),
|
|
477
|
+
} })]),
|
|
305
478
|
];
|
|
306
479
|
// Forgetting a path is only offered where it can do something: this row
|
|
307
480
|
// stays on screen either way, because the sidebar groups by the tree a
|
|
@@ -491,7 +664,7 @@ function paintSessions() {
|
|
|
491
664
|
// recent session; the rest follow by how recently they were touched — and
|
|
492
665
|
// then a pin or a drag overrides both, because those were said out loud.
|
|
493
666
|
const pinnedProject = (cwd) => (projects.find((p) => p.path === cwd) || {}).pinned === true;
|
|
494
|
-
const
|
|
667
|
+
const sorted = arrange(
|
|
495
668
|
[...byProject.keys()].sort((a, b) => {
|
|
496
669
|
if (a === here) return -1;
|
|
497
670
|
if (b === here) return 1;
|
|
@@ -501,6 +674,23 @@ function paintSessions() {
|
|
|
501
674
|
projectOrder,
|
|
502
675
|
pinnedProject,
|
|
503
676
|
);
|
|
677
|
+
/*
|
|
678
|
+
* Worktrees sit directly under the project they came from, whatever their
|
|
679
|
+
* own recency — they are that project's parallel work, not projects of
|
|
680
|
+
* their own. One with no session yet still shows, so it can be finished.
|
|
681
|
+
*/
|
|
682
|
+
const order = sorted.filter((cwd) => !worktreeAt(cwd) || !sorted.includes(worktreeAt(cwd).parent));
|
|
683
|
+
for (const wt of worktrees) {
|
|
684
|
+
if (!byProject.has(wt.path)) byProject.set(wt.path, []);
|
|
685
|
+
const at = order.indexOf(wt.parent);
|
|
686
|
+
if (at === -1) {
|
|
687
|
+
if (!order.includes(wt.path) && byProject.get(wt.path).length) order.push(wt.path);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
let end = at + 1;
|
|
691
|
+
while (end < order.length && worktreeAt(order[end]) && worktreeAt(order[end]).parent === wt.parent) end++;
|
|
692
|
+
order.splice(end, 0, wt.path);
|
|
693
|
+
}
|
|
504
694
|
|
|
505
695
|
// Everything here belongs to the project already named at the top of the
|
|
506
696
|
// sidebar: a header repeating it is noise, and the rows do not need
|
|
@@ -741,6 +931,59 @@ function pointAnchor(e) {
|
|
|
741
931
|
return { getBoundingClientRect: () => rect };
|
|
742
932
|
}
|
|
743
933
|
|
|
934
|
+
/**
|
|
935
|
+
* The ways out of a worktree, in the menu it was asked from.
|
|
936
|
+
*
|
|
937
|
+
* Asked of the server each time rather than drawn from what the sidebar holds:
|
|
938
|
+
* whether there is anything to lose is the whole question, and the answer
|
|
939
|
+
* changes with every edit made in there.
|
|
940
|
+
*/
|
|
941
|
+
async function showWorktreeFinish(cwd, anchor, head) {
|
|
942
|
+
const body = await getJson("/worktree/status?path=" + encodeURIComponent(cwd));
|
|
943
|
+
if (body.error) return void addNotice(body.error, "notice err");
|
|
944
|
+
const wt = body.worktree;
|
|
945
|
+
const st = body.status;
|
|
946
|
+
const name = projectName(wt.root);
|
|
947
|
+
const parentName = projectName(wt.parent);
|
|
948
|
+
const facts = st.clean
|
|
949
|
+
? t("worktree.clean")
|
|
950
|
+
: [
|
|
951
|
+
st.commits ? t("worktree.commits", { n: st.commits, branch: wt.branch || "" }) : "",
|
|
952
|
+
st.changes.length ? t("worktree.changes", { n: st.changes.length }) : "",
|
|
953
|
+
].filter(Boolean).join(" · ");
|
|
954
|
+
const keys = { remove: "r", discard: "d", commit: "c", push: "p", merge: "m", apply: "a" };
|
|
955
|
+
const items = body.actions.map((action) =>
|
|
956
|
+
menuItem(t("worktree.action." + action, { name: parentName }), {
|
|
957
|
+
key: keys[action],
|
|
958
|
+
danger: action === "discard",
|
|
959
|
+
run: () => {
|
|
960
|
+
closeRowMenu();
|
|
961
|
+
void finishWorktreeUi(wt, action, name, st.clean);
|
|
962
|
+
},
|
|
963
|
+
}),
|
|
964
|
+
);
|
|
965
|
+
openRowMenu(anchor, head, [el("div", "menu-note", name + " — " + facts), ...items]);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function finishWorktreeUi(wt, action, name, clean) {
|
|
969
|
+
const body = { path: wt.path, action };
|
|
970
|
+
if (action === "commit") {
|
|
971
|
+
const message = window.prompt(t("worktree.commitPrompt"), wt.task || "");
|
|
972
|
+
if (!message) return;
|
|
973
|
+
body.message = message;
|
|
974
|
+
}
|
|
975
|
+
if (action === "discard" && !clean && !window.confirm(t("worktree.discardConfirm", { name }))) return;
|
|
976
|
+
const answer = await api("/worktree/finish", body, 5 * 60_000);
|
|
977
|
+
if (!answer || answer.error) return void addNotice((answer && answer.error) || t("worktree.failed"), "notice err");
|
|
978
|
+
addNotice(answer.result.message);
|
|
979
|
+
if (answer.result.url) window.open(answer.result.url, "_blank", "noopener");
|
|
980
|
+
// Its tabs and harness are gone with it; reloading drops every pane and
|
|
981
|
+
// path that pointed there at once, the way a rename does.
|
|
982
|
+
if (answer.result.removed) return void window.location.reload();
|
|
983
|
+
await loadProjects();
|
|
984
|
+
await loadSessions();
|
|
985
|
+
}
|
|
986
|
+
|
|
744
987
|
function openRowMenu(button, row, items) {
|
|
745
988
|
closeRowMenu();
|
|
746
989
|
rowMenu.replaceChildren(...items);
|
|
@@ -784,7 +1027,7 @@ function sessionRow(s, nested) {
|
|
|
784
1027
|
item.appendChild(el("div", "title", s.title || t("session.untitled")));
|
|
785
1028
|
// Outside the title rather than in it: that line ellipsises, and a mark that
|
|
786
1029
|
// vanishes with the end of a long name is not a mark.
|
|
787
|
-
if (s.pinned) item.appendChild(
|
|
1030
|
+
if (s.pinned) item.appendChild(pinMark(t("session.unpin"), () => void togglePin("session", s.id, false)));
|
|
788
1031
|
|
|
789
1032
|
draggable(item, {
|
|
790
1033
|
kind: "session",
|
|
@@ -1204,6 +1447,11 @@ function renderHistory(entries, into) {
|
|
|
1204
1447
|
P.turn = null;
|
|
1205
1448
|
P.streaming = null;
|
|
1206
1449
|
for (const entry of entries || []) {
|
|
1450
|
+
if (entry.tasks) {
|
|
1451
|
+
P.turn = null;
|
|
1452
|
+
taskCard(entry.tasks);
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1207
1455
|
if (entry.role === "user") {
|
|
1208
1456
|
if (entry.text || (entry.images && entry.images.length)) {
|
|
1209
1457
|
addUser(entry.text || "", entry.images);
|
|
@@ -1224,7 +1472,8 @@ function renderHistory(entries, into) {
|
|
|
1224
1472
|
if (entry.text) assistantTurn().appendChild(renderMarkdown(entry.text));
|
|
1225
1473
|
for (const call of entry.tools || []) toolCard(call.id, call.name, call.name, call.input);
|
|
1226
1474
|
for (const result of entry.results || []) {
|
|
1227
|
-
|
|
1475
|
+
const shots = (result.images || []).map((image) => ({ type: "image", src: image.src }));
|
|
1476
|
+
finishTool(result.id, [{ type: "text", text: result.text }, ...shots], 0, result.isError);
|
|
1228
1477
|
}
|
|
1229
1478
|
// The cards, again. Live, these come from the "artifact" event, which a
|
|
1230
1479
|
// replay does not produce — so a resumed conversation had every message and
|
|
@@ -1237,6 +1486,7 @@ function renderHistory(entries, into) {
|
|
|
1237
1486
|
const card = replayedArtifactCard(call.input);
|
|
1238
1487
|
if (card) assistantTurn().appendChild(card);
|
|
1239
1488
|
}
|
|
1489
|
+
if (entry.shots) turnShots(assistantTurn(), entry.shots);
|
|
1240
1490
|
if (P.turn) turnActions(P.turn, entry.keepMessages);
|
|
1241
1491
|
}
|
|
1242
1492
|
for (const card of connectionCards.values()) {
|
|
@@ -1254,7 +1504,11 @@ function renderHistory(entries, into) {
|
|
|
1254
1504
|
async function switchTo(body) {
|
|
1255
1505
|
const state = await api("/session", body);
|
|
1256
1506
|
if (state.error) return void addNotice(state.error, "err");
|
|
1507
|
+
await openState(state);
|
|
1508
|
+
}
|
|
1257
1509
|
|
|
1510
|
+
/** Show a session the server has just opened, as its own focused tab. */
|
|
1511
|
+
async function openState(state) {
|
|
1258
1512
|
// A tab per session, focused on arrival.
|
|
1259
1513
|
const record = pane(state.sessionId);
|
|
1260
1514
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAy/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;
|