@oharness/server 0.12.0 → 0.14.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 +204 -0
- package/dist/i18n.js.map +1 -1
- package/dist/page/client/events.js +8 -3
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/panels.js +225 -1
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/routines.js +67 -4
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +8 -1
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +15 -2
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/tasks.js +153 -7
- package/dist/page/client/tasks.js.map +1 -1
- package/dist/page/client/usage.js +6 -1
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +90 -1
- package/dist/page/style.js.map +1 -1
- package/dist/server.js +58 -5
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -16,11 +16,135 @@ let taskOpen = null;
|
|
|
16
16
|
let taskSheet = null;
|
|
17
17
|
/** Bumped per paint, so a slow answer never draws over a newer one. */
|
|
18
18
|
let taskPaintSeq = 0;
|
|
19
|
+
const taskDrafts = new Map();
|
|
20
|
+
const taskSending = new Set();
|
|
21
|
+
const taskKey = (session, id) => JSON.stringify([session, id]);
|
|
22
|
+
function saveTaskDraft() {
|
|
23
|
+
if (taskOpen && taskSheet) taskDrafts.set(taskKey(taskOpen.session, taskOpen.id), taskSheet.querySelector(".task-input").value);
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Running ones first, then the newest. Ended tasks stay listed, quieter, so
|
|
22
28
|
* what they said is a click away and they can still be sent a follow-up.
|
|
23
29
|
*/
|
|
30
|
+
// Sidebar summaries deliberately do not allocate transcript panes.
|
|
31
|
+
const sidebarTasks = new Map();
|
|
32
|
+
let sidebarTaskRevision = 0;
|
|
33
|
+
let sessionListRequest = 0;
|
|
34
|
+
const taskTreeMore = new Set();
|
|
35
|
+
const TASK_TREE_ENDED = 3;
|
|
36
|
+
const taskTreeOpen = (() => {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(localStorage.getItem("oharness.taskTree") || "{}");
|
|
39
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
40
|
+
} catch { return {}; }
|
|
41
|
+
})();
|
|
42
|
+
function saveTaskTreeOpen() {
|
|
43
|
+
try { localStorage.setItem("oharness.taskTree", JSON.stringify(taskTreeOpen)); } catch {}
|
|
44
|
+
}
|
|
45
|
+
function sessionBlock(s, nested) {
|
|
46
|
+
const block = el("div", "session-block" + (nested ? " nested" : ""));
|
|
47
|
+
block.dataset.session = s.id;
|
|
48
|
+
const row = sessionRow(s, nested);
|
|
49
|
+
const toggle = el("button", "task-tree-toggle");
|
|
50
|
+
toggle.type = "button";
|
|
51
|
+
toggle.appendChild(svgIcon("chevronRight"));
|
|
52
|
+
const list = el("div", "task-tree");
|
|
53
|
+
list.id = "task-tree-" + s.id;
|
|
54
|
+
toggle.setAttribute("aria-controls", list.id);
|
|
55
|
+
toggle.addEventListener("click", (e) => {
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
taskTreeOpen[s.id] = toggle.getAttribute("aria-expanded") !== "true";
|
|
58
|
+
saveTaskTreeOpen();
|
|
59
|
+
paintTaskTree(block, s);
|
|
60
|
+
});
|
|
61
|
+
toggle.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
62
|
+
toggle.draggable = false;
|
|
63
|
+
// In the row's left inset, which every row in the list keeps, so titles
|
|
64
|
+
// start at one place whether or not their conversation has tasks.
|
|
65
|
+
row.prepend(toggle);
|
|
66
|
+
const count = el("span", "task-tree-count");
|
|
67
|
+
row.insertBefore(count, row.querySelector(".sub"));
|
|
68
|
+
block.append(row, list);
|
|
69
|
+
paintTaskTree(block, s);
|
|
70
|
+
return block;
|
|
71
|
+
}
|
|
72
|
+
function paintTaskTree(block, s) {
|
|
73
|
+
const tasks = sortedTasks((sidebarTasks.get(s.id) || {}).tasks).filter((task) => !task.dismissed);
|
|
74
|
+
const live = tasks.filter((task) => TASK_LIVE.has(task.status)).length;
|
|
75
|
+
// Only infer a default once live work appears in the current conversation.
|
|
76
|
+
if (!(s.id in taskTreeOpen) && s.id === activeId && live) {
|
|
77
|
+
taskTreeOpen[s.id] = true;
|
|
78
|
+
saveTaskTreeOpen();
|
|
79
|
+
}
|
|
80
|
+
const open = taskTreeOpen[s.id] === true;
|
|
81
|
+
const toggle = block.querySelector(".task-tree-toggle");
|
|
82
|
+
toggle.hidden = !tasks.length;
|
|
83
|
+
toggle.classList.toggle("open", open);
|
|
84
|
+
toggle.title = t(open ? "tasks.collapse" : "tasks.expand");
|
|
85
|
+
toggle.setAttribute("aria-label", toggle.title);
|
|
86
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
87
|
+
toggle.classList.toggle("quiet", !live);
|
|
88
|
+
// Only live work earns a badge; ended tasks would otherwise leave a number
|
|
89
|
+
// on every conversation that ever started one.
|
|
90
|
+
const count = block.querySelector(".task-tree-count");
|
|
91
|
+
count.hidden = !live;
|
|
92
|
+
count.textContent = t("tasks.liveCount", { n: live });
|
|
93
|
+
count.title = t("tasks.title");
|
|
94
|
+
const list = block.querySelector(".task-tree");
|
|
95
|
+
list.hidden = !open || !tasks.length;
|
|
96
|
+
if (list.hidden) return;
|
|
97
|
+
// Live tasks, then the few that ended last; older ones wait behind a button.
|
|
98
|
+
// Patch existing buttons so activity updates preserve keyboard focus.
|
|
99
|
+
const shown = taskTreeMore.has(s.id) ? tasks : tasks.slice(0, live + TASK_TREE_ENDED);
|
|
100
|
+
const keep = new Set(shown.map((task) => task.id));
|
|
101
|
+
for (const node of [...list.children]) {
|
|
102
|
+
if (!keep.has(node.dataset.task)) node.remove();
|
|
103
|
+
}
|
|
104
|
+
shown.forEach((task, index) => {
|
|
105
|
+
let button = [...list.children].find((node) => node.dataset.task === task.id);
|
|
106
|
+
if (!button) {
|
|
107
|
+
button = el("button", "task-tree-row");
|
|
108
|
+
button.type = "button";
|
|
109
|
+
button.dataset.task = task.id;
|
|
110
|
+
button.append(el("span", "task-tree-mark"), el("span", "task-tree-name"), el("span", "task-tree-status"));
|
|
111
|
+
button.addEventListener("click", async () => {
|
|
112
|
+
button.disabled = true;
|
|
113
|
+
try {
|
|
114
|
+
// Resume through the existing project-aware path, restoring task
|
|
115
|
+
// conversations before opening their input box.
|
|
116
|
+
if (activeId !== s.id) await openSession(s.id, s.cwd);
|
|
117
|
+
if (activeId === s.id) openTask(s.id, task.id);
|
|
118
|
+
} catch (err) { addNotice(String(err.message || err), "err"); }
|
|
119
|
+
finally { button.disabled = false; }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
button.className = "task-tree-row task-" + task.status;
|
|
123
|
+
button.classList.toggle("selected", !!taskOpen && taskOpen.session === s.id && taskOpen.id === task.id);
|
|
124
|
+
button.setAttribute("aria-pressed", String(!!taskOpen && taskOpen.session === s.id && taskOpen.id === task.id));
|
|
125
|
+
button.querySelector(".task-tree-name").textContent = task.description;
|
|
126
|
+
// A dot is the same shape running and done, so the word says which.
|
|
127
|
+
const status = t("tasks.status." + task.status);
|
|
128
|
+
button.querySelector(".task-tree-status").textContent = status;
|
|
129
|
+
button.setAttribute("aria-label", task.description + ", " + status);
|
|
130
|
+
button.title = task.description + " · " + status + (task.activity ? " · " + task.activity : "");
|
|
131
|
+
if (list.children[index] !== button) list.insertBefore(button, list.children[index] || null);
|
|
132
|
+
});
|
|
133
|
+
if (shown.length < tasks.length) {
|
|
134
|
+
const more = el("button", "task-tree-more", t("tasks.older", { n: tasks.length - shown.length }));
|
|
135
|
+
more.type = "button";
|
|
136
|
+
more.addEventListener("click", () => { taskTreeMore.add(s.id); paintTaskTree(block, s); });
|
|
137
|
+
list.appendChild(more);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function refreshTaskTrees(session) {
|
|
141
|
+
for (const block of sessionList.querySelectorAll(".session-block")) {
|
|
142
|
+
if (session && block.dataset.session !== session) continue;
|
|
143
|
+
const s = sessionRows.find((row) => row.id === block.dataset.session);
|
|
144
|
+
if (s) paintTaskTree(block, s);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
24
148
|
function sortedTasks(tasks) {
|
|
25
149
|
return (tasks || []).slice().sort((a, b) =>
|
|
26
150
|
(Number(TASK_LIVE.has(b.status)) - Number(TASK_LIVE.has(a.status))) || (b.queuedAt - a.queuedAt));
|
|
@@ -89,21 +213,30 @@ function taskCard(tasks) {
|
|
|
89
213
|
|
|
90
214
|
/** A conversation's tasks changed: redraw what shows them. */
|
|
91
215
|
function tasksChanged(session, tasks) {
|
|
92
|
-
|
|
216
|
+
sidebarTasks.set(session, { tasks: tasks || [], revision: ++sidebarTaskRevision });
|
|
217
|
+
refreshTaskTrees(session);
|
|
218
|
+
if (panes.has(session)) panes.get(session).tasks = tasks || [];
|
|
93
219
|
if (session === activeId) renderQueued();
|
|
94
220
|
if (taskOpen && taskOpen.session === session) void paintTask();
|
|
95
221
|
}
|
|
96
222
|
|
|
97
223
|
function openTask(session, id) {
|
|
224
|
+
saveTaskDraft();
|
|
98
225
|
taskOpen = { session, id, stick: true };
|
|
99
226
|
ensureTaskSheet();
|
|
227
|
+
taskSheet.querySelector(".task-input").value = taskDrafts.get(taskKey(session, id)) || "";
|
|
228
|
+
taskSheet.querySelector(".task-input").disabled = false;
|
|
229
|
+
taskSheet.querySelector(".task-send").disabled = true;
|
|
230
|
+
refreshTaskTrees();
|
|
100
231
|
taskSheet.hidden = false;
|
|
101
232
|
void paintTask();
|
|
102
233
|
taskSheet.querySelector(".task-input").focus();
|
|
103
234
|
}
|
|
104
235
|
|
|
105
236
|
function closeTask() {
|
|
237
|
+
saveTaskDraft();
|
|
106
238
|
taskOpen = null;
|
|
239
|
+
refreshTaskTrees();
|
|
107
240
|
if (taskSheet) taskSheet.hidden = true;
|
|
108
241
|
}
|
|
109
242
|
|
|
@@ -187,16 +320,29 @@ async function sendTask(input) {
|
|
|
187
320
|
const text = input.value.trim();
|
|
188
321
|
if (!text || !taskOpen) return;
|
|
189
322
|
const { session, id } = taskOpen;
|
|
323
|
+
const key = taskKey(session, id);
|
|
324
|
+
if (taskSending.has(key)) return;
|
|
325
|
+
const draft = input.value;
|
|
326
|
+
taskSending.add(key);
|
|
327
|
+
taskDrafts.set(key, draft);
|
|
190
328
|
input.disabled = true;
|
|
329
|
+
taskSheet.querySelector(".task-send").disabled = true;
|
|
191
330
|
const body = await api("/task", { session, id, action: "message", text });
|
|
331
|
+
taskSending.delete(key);
|
|
332
|
+
if (!body.error && taskDrafts.get(key) === draft) taskDrafts.delete(key);
|
|
333
|
+
// A response for A must never clear or disable the input now showing B.
|
|
334
|
+
if (!taskOpen || taskOpen.session !== session || taskOpen.id !== id) {
|
|
335
|
+
if (body.error) addNotice(body.error, "err");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
192
338
|
input.disabled = false;
|
|
193
339
|
if (body.error) {
|
|
194
340
|
taskSheet.querySelector(".task-log").appendChild(el("div", "task-error", body.error));
|
|
195
|
-
|
|
341
|
+
} else {
|
|
342
|
+
if (input.value === draft) input.value = "";
|
|
343
|
+
input.focus();
|
|
344
|
+
taskOpen.stick = true;
|
|
196
345
|
}
|
|
197
|
-
input.value = "";
|
|
198
|
-
input.focus();
|
|
199
|
-
if (taskOpen) taskOpen.stick = true;
|
|
200
346
|
void paintTask();
|
|
201
347
|
}
|
|
202
348
|
|
|
@@ -285,8 +431,8 @@ async function paintTask() {
|
|
|
285
431
|
|
|
286
432
|
const input = taskSheet.querySelector(".task-input");
|
|
287
433
|
const stopped = task.status === "cancelled";
|
|
288
|
-
input.disabled = stopped;
|
|
289
|
-
taskSheet.querySelector(".task-send").disabled =
|
|
434
|
+
input.disabled = stopped || taskSending.has(taskKey(session, id));
|
|
435
|
+
taskSheet.querySelector(".task-send").disabled = input.disabled;
|
|
290
436
|
input.placeholder = t(stopped ? "tasks.placeholder.stopped" : TASK_LIVE.has(task.status) ? "tasks.placeholder.running" : "tasks.placeholder.ended");
|
|
291
437
|
}
|
|
292
438
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../../src/page/client/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../../src/page/client/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8a9B,CAAC"}
|
|
@@ -987,7 +987,11 @@ const usageMoved = (row) => usageSent(row) + (row.output || 0);
|
|
|
987
987
|
function usageSessionRow(entry) {
|
|
988
988
|
const row = el("div", "row-card usage-session");
|
|
989
989
|
const line = el("div", "row-head");
|
|
990
|
-
|
|
990
|
+
// A day of background calls, not a conversation: named for what it was, and
|
|
991
|
+
// opening the page that explains it rather than a transcript.
|
|
992
|
+
const background = entry.background;
|
|
993
|
+
line.appendChild(el("span", "row-title", background ? t("usage.background." + background) : entry.title || t("session.untitled")));
|
|
994
|
+
if (background) line.appendChild(el("span", "tag", t("usage.background")));
|
|
991
995
|
if (entry.current) line.appendChild(el("span", "tag", t("usage.thisSession")));
|
|
992
996
|
line.appendChild(el("span", "muted small usage-amount", "$" + (entry.costUsd || 0).toFixed(4)));
|
|
993
997
|
row.appendChild(line);
|
|
@@ -999,6 +1003,7 @@ function usageSessionRow(entry) {
|
|
|
999
1003
|
row.tabIndex = 0;
|
|
1000
1004
|
row.setAttribute("role", "button");
|
|
1001
1005
|
const open = () => {
|
|
1006
|
+
if (background === "dreaming") { setNav("dreaming"); void showDreaming(); return; }
|
|
1002
1007
|
setNav(null);
|
|
1003
1008
|
if (panes.has(entry.id)) void focusTab(entry.id);
|
|
1004
1009
|
else void openSession(entry.id, entry.cwd);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuxC3B,CAAC"}
|
package/dist/page/style.js
CHANGED
|
@@ -218,6 +218,53 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
|
|
|
218
218
|
height:30px;padding:0 8px;border-radius:7px;cursor:pointer;
|
|
219
219
|
display:flex;align-items:center;gap:8px;
|
|
220
220
|
}
|
|
221
|
+
.session-block + .fold-row{margin-top:6px}
|
|
222
|
+
/*
|
|
223
|
+
* A session's background tasks. The chevron sits in the row's left inset —
|
|
224
|
+
* one every row in the list keeps, flat or nested — so titles share a left
|
|
225
|
+
* edge whether or not a row has tasks. The tasks hang under the title with no
|
|
226
|
+
* rule, each status mark under the title's first letter.
|
|
227
|
+
*
|
|
228
|
+
* The marks stay quiet: a dashed ring for work still waiting, and a dot for
|
|
229
|
+
* everything else — breathing while it runs, still once done, fainter when
|
|
230
|
+
* stopped. Only a failure takes a colour.
|
|
231
|
+
*/
|
|
232
|
+
.session-block .session{position:relative}
|
|
233
|
+
/* Two lit rows — the open one and the one under the pointer — must not
|
|
234
|
+
touch, or they read as one block. */
|
|
235
|
+
.session-block + .session-block{margin-top:2px}
|
|
236
|
+
.task-tree-row + .task-tree-row{margin-top:1px}
|
|
237
|
+
.session-block:not(.nested) .session,.show-more:not(.nested){padding-left:20px}
|
|
238
|
+
.task-tree-toggle{position:absolute;left:5px;top:50%;width:12px;height:16px;margin-top:-8px;display:grid;place-items:center;padding:0;border:0;background:none;color:var(--dim);cursor:pointer;opacity:.7}
|
|
239
|
+
.session-block.nested .task-tree-toggle{left:14px}
|
|
240
|
+
/* Above #side-panel's thinning of every 1.5 stroke, which these lose at this size. */
|
|
241
|
+
#side-panel .task-tree-toggle svg{width:10px;height:10px;stroke-width:1.75;transition:transform .15s ease}
|
|
242
|
+
.task-tree-toggle.open svg{transform:rotate(90deg)}
|
|
243
|
+
.task-tree-toggle:hover,.session:hover .task-tree-toggle{opacity:1;color:var(--fg)}
|
|
244
|
+
.task-tree-toggle.quiet{opacity:.6}
|
|
245
|
+
.task-tree-toggle:focus{outline:none}
|
|
246
|
+
.task-tree-count{flex:0 0 auto;font-size:10px;color:var(--dim)}
|
|
247
|
+
.task-tree{margin:0 0 4px 12px}
|
|
248
|
+
.session-block.nested .task-tree{margin-left:24px}
|
|
249
|
+
.task-tree-row,.task-tree-more{display:flex;align-items:center;gap:7px;width:calc(100% - 4px);height:26px;padding:0 8px;border:0;border-radius:7px;background:transparent;color:var(--dim);text-align:left;cursor:pointer;font:inherit;font-size:12px}
|
|
250
|
+
.task-tree-more{padding-left:29px}
|
|
251
|
+
.task-tree-row:hover,.task-tree-more:hover{background:var(--hover);color:var(--fg)}
|
|
252
|
+
.task-tree-row.selected{background:var(--sel);color:var(--fg)}
|
|
253
|
+
.task-tree-row.task-running,.task-tree-row.task-queued{color:var(--fg)}
|
|
254
|
+
.task-tree-mark{flex:0 0 14px;width:14px;height:14px;display:grid;place-items:center}
|
|
255
|
+
.task-tree-mark::after{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
|
|
256
|
+
.task-queued .task-tree-mark::after{width:10px;height:10px;box-sizing:border-box;background:none;border:1.25px dashed currentColor;opacity:.8}
|
|
257
|
+
.task-running .task-tree-mark::after{background:var(--fg);animation:task-breathe 1.6s ease-in-out infinite}
|
|
258
|
+
.task-completed .task-tree-mark::after{opacity:.7}
|
|
259
|
+
.task-cancelled .task-tree-mark::after{opacity:.35}
|
|
260
|
+
.task-failed .task-tree-mark::after{background:var(--err)}
|
|
261
|
+
@keyframes task-breathe{50%{opacity:.25;transform:scale(.8)}}
|
|
262
|
+
@media (prefers-reduced-motion:reduce){.task-running .task-tree-mark::after{animation:none}}
|
|
263
|
+
.task-tree-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
264
|
+
.task-tree-status{font-size:10px;flex:0 0 auto;opacity:.8}
|
|
265
|
+
.task-tree-row.task-running .task-tree-status{color:var(--fg);opacity:1}
|
|
266
|
+
.task-tree-toggle:focus-visible,.task-tree-row:focus-visible,.task-tree-more:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
267
|
+
.task-tree[hidden],.task-tree-toggle[hidden],.task-tree-count[hidden]{display:none}
|
|
221
268
|
.session:hover{background:var(--hover)}
|
|
222
269
|
.session.active{background:var(--sel)}
|
|
223
270
|
.session .title{
|
|
@@ -1586,6 +1633,36 @@ button.ghost.theme-apply:disabled{opacity:.4;cursor:default}
|
|
|
1586
1633
|
.memory-recent>div{padding:2px 0 2px 15px}
|
|
1587
1634
|
.memory-backup{display:flex;align-items:center;gap:12px;padding:8px 0 8px 15px}
|
|
1588
1635
|
.memory-backup-text{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
|
|
1636
|
+
.dream-card .row-head{align-items:center;gap:8px}
|
|
1637
|
+
.dream-head{display:flex;align-items:center;gap:10px}
|
|
1638
|
+
.dream-head h2{margin:0}
|
|
1639
|
+
.dream-spacer{flex:1 1 auto}
|
|
1640
|
+
.dream-auto{display:flex;align-items:center;gap:8px;color:var(--dim);cursor:pointer}
|
|
1641
|
+
.dream-auto-slot{display:inline-flex;align-items:center;flex:0 0 auto}
|
|
1642
|
+
.dream-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:14px 0 6px}
|
|
1643
|
+
.dream-metric{background:var(--panel);border-radius:10px;padding:10px 12px}
|
|
1644
|
+
.dream-metric-value{font-size:20px;font-weight:600;margin:2px 0}
|
|
1645
|
+
.dream-section{margin:20px 0 6px;font-size:14px}
|
|
1646
|
+
.dream-line{display:flex;align-items:center;gap:8px;overflow-wrap:anywhere}
|
|
1647
|
+
.dream-run>summary,.dream-change>summary{cursor:pointer;padding:8px 4px;border-radius:8px}
|
|
1648
|
+
.dream-run>summary:hover,.dream-change>summary:hover{background:var(--hover)}
|
|
1649
|
+
.dream-run{margin:2px 0}
|
|
1650
|
+
.dream-run>:not(summary){margin-left:18px}
|
|
1651
|
+
.dream-change{padding:4px 0}
|
|
1652
|
+
.dream-grow{flex:1 1 auto;min-width:0}
|
|
1653
|
+
.dream-when{flex:0 0 auto;min-width:96px;color:var(--dim);font-size:12px}
|
|
1654
|
+
.dream-small{font-size:12px;padding:2px 8px}
|
|
1655
|
+
.dream-diff{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin:6px 0}
|
|
1656
|
+
.memory-dream-heading{margin:14px 0 4px;font-size:13px;color:var(--dim);font-weight:600}
|
|
1657
|
+
.memory-dream-op{padding:6px 0 6px 15px}
|
|
1658
|
+
.memory-dream-op+.memory-dream-op{margin-top:4px}
|
|
1659
|
+
.memory-dream-op>summary{cursor:pointer;display:flex;gap:8px;align-items:baseline;overflow-wrap:anywhere}
|
|
1660
|
+
.memory-dream-tag{flex:0 0 auto;font-size:12px;padding:1px 6px;border-radius:6px;background:var(--field);color:var(--dim)}
|
|
1661
|
+
.memory-dream-tag.apply{color:var(--ok)}
|
|
1662
|
+
.memory-dream-tag.review{color:var(--warn)}
|
|
1663
|
+
.memory-dream-body{margin:4px 0 8px;background:var(--field);border-radius:8px;padding:8px 10px;font:inherit;font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
1664
|
+
.memory-dream-body.before{color:var(--dim)}
|
|
1665
|
+
.memory-dream-evidence{margin:4px 0 0;padding-left:18px;color:var(--dim)}
|
|
1589
1666
|
.memory-versions{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin:10px 0}
|
|
1590
1667
|
.memory-version h4{margin:0 0 6px}
|
|
1591
1668
|
.memory-version pre{margin:0;background:var(--field);border-radius:8px;padding:10px 12px;overflow-wrap:anywhere}
|
|
@@ -1648,6 +1725,13 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
|
|
|
1648
1725
|
title 30px in, and a body that started at the card's edge read as a
|
|
1649
1726
|
paragraph the row had dropped rather than the inside of it. */
|
|
1650
1727
|
.setting-body{padding:4px 14px 16px 30px}
|
|
1728
|
+
.setting-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}
|
|
1729
|
+
.setting-stat{background:var(--field);border-radius:10px;padding:10px 12px;min-width:0}
|
|
1730
|
+
.setting-stat-label{font-size:12px;color:var(--dim)}
|
|
1731
|
+
.setting-stat-value{font-size:15px;font-weight:600;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1732
|
+
.setting-stat-sub{font-size:12px;color:var(--dim);margin-top:2px;overflow-wrap:anywhere}
|
|
1733
|
+
.setting-stat.ok .setting-stat-value{color:var(--ok)}
|
|
1734
|
+
.setting-stat.warn .setting-stat-value{color:var(--warn)}
|
|
1651
1735
|
/* The shared .small breaks anywhere, which is right for a path and wrong for
|
|
1652
1736
|
a sentence: the hint here came back as "the trail is never s / ynced". The
|
|
1653
1737
|
prose breaks at spaces; the paths under it keep breaking anywhere. */
|
|
@@ -3533,11 +3617,16 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
|
|
|
3533
3617
|
/* An update question, in place of the OS's message box: the version is the
|
|
3534
3618
|
title, the release notes scroll if there are many, and Later stays quiet. */
|
|
3535
3619
|
.upd-dialog{width:min(440px,92vw);padding-bottom:16px}
|
|
3620
|
+
/* Notes get room to be read; the questions without them stay compact. */
|
|
3621
|
+
.upd-dialog:has(.upd-notes){width:min(560px,92vw)}
|
|
3536
3622
|
.upd-head{display:flex;align-items:center;gap:12px;padding:22px 24px 8px}
|
|
3537
3623
|
.upd-mark{flex:none;display:grid;place-items:center;width:30px;height:30px;border-radius:9px;background:var(--sel);color:var(--fg);font-size:15px;font-weight:600}
|
|
3538
3624
|
.upd-dialog.important .upd-mark{background:var(--accent);color:var(--bg)}
|
|
3539
3625
|
.upd-title{font-size:16px;font-weight:600;line-height:1.35}
|
|
3540
|
-
.upd-notes{margin:6px 24px 10px;padding:
|
|
3626
|
+
.upd-notes{margin:6px 24px 10px;padding:14px 16px;max-height:min(440px,55vh);overflow-y:auto;border-radius:10px;background:var(--hover);font-size:13px;line-height:1.55}
|
|
3627
|
+
/* release-notes.md is wrapped at 76 columns; a paragraph reflows to the dialog. */
|
|
3628
|
+
.upd-notes .md-p{white-space:normal}
|
|
3629
|
+
.upd-notes .md>:last-child{margin-bottom:0}
|
|
3541
3630
|
.upd-body{padding:0 24px;color:var(--dim);font-size:13.5px;line-height:1.5;word-break:break-word}
|
|
3542
3631
|
.upd-body.mono{font-size:12.5px}
|
|
3543
3632
|
.upd-dialog .dlg-actions{justify-content:flex-end;padding:18px 20px 4px}
|
package/dist/page/style.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA45HV,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { Harness } from "@oharness/engine/harness.ts";
|
|
8
|
+
import { BackgroundUsage } from "@oharness/engine/agent/background-usage.ts";
|
|
8
9
|
import { updateUserMcpConfig } from "@oharness/engine/config-files.ts";
|
|
9
10
|
import { connectorConfig, officialConnectors } from "@oharness/engine/mcp/registry.ts";
|
|
10
11
|
import { findForeignServer, foreignMcpServers } from "@oharness/engine/foreign.ts";
|
|
@@ -18,6 +19,7 @@ import { PERMISSION_MODES } from "@oharness/engine/agent/permissions.ts";
|
|
|
18
19
|
import { canDisableThinking, effectiveEffort, isEffortChoice, thinkingFor, } from "@oharness/engine/providers/types.ts";
|
|
19
20
|
import { isReadable } from "@oharness/engine/agent/workspace.ts";
|
|
20
21
|
import { BRANCH_TYPES, createWorktree, findWorktree, finishWorktree, listWorktrees, previewWorktree, worktreeStatus, } from "@oharness/engine/worktree/index.ts";
|
|
22
|
+
import { restoredTaskView } from "@oharness/engine/agent/background.ts";
|
|
21
23
|
import { within as insideAny } from "@oharness/engine/paths.ts";
|
|
22
24
|
import { visibleText } from "@oharness/engine/core/types.ts";
|
|
23
25
|
import { previewJson } from "@oharness/engine/util/json.ts";
|
|
@@ -305,6 +307,14 @@ export async function startWebServer(opts) {
|
|
|
305
307
|
equipThemes(opts.agent.tools);
|
|
306
308
|
const themeChanged = (state) => broadcast({ type: "theme", state });
|
|
307
309
|
const runners = new Map();
|
|
310
|
+
/** Sidebar rows of sessions that are not loaded, keyed by tasks-file stamp. */
|
|
311
|
+
const storedTaskRows = new Map();
|
|
312
|
+
const sidebarTaskRows = (views) => views
|
|
313
|
+
.filter((task) => !task.dismissed)
|
|
314
|
+
.map((task) => ({
|
|
315
|
+
id: task.id, description: task.description, status: task.status,
|
|
316
|
+
queuedAt: task.queuedAt, endedAt: task.endedAt, activity: task.activity,
|
|
317
|
+
}));
|
|
308
318
|
/** Agents whose background tasks are being watched; see watchTasks. */
|
|
309
319
|
const watchedTasks = new WeakSet();
|
|
310
320
|
// Written in the background; the page on that project swaps them in.
|
|
@@ -430,7 +440,6 @@ export async function startWebServer(opts) {
|
|
|
430
440
|
// browser is still the same one bound by Harness.agent(session).
|
|
431
441
|
initial.agent = opts.agent;
|
|
432
442
|
runners.set(opts.session.id, initial);
|
|
433
|
-
adoptTasks(initial);
|
|
434
443
|
let current = { session: opts.session, agent: opts.agent, harness: opts.harness };
|
|
435
444
|
/**
|
|
436
445
|
* Sign-ins waiting for their emailed code.
|
|
@@ -606,6 +615,8 @@ export async function startWebServer(opts) {
|
|
|
606
615
|
for (const client of clients)
|
|
607
616
|
client.write(payload);
|
|
608
617
|
};
|
|
618
|
+
// Restored tasks may resolve immediately, so the broadcaster must exist.
|
|
619
|
+
adoptTasks(initial);
|
|
609
620
|
const connectionSubscriptions = [];
|
|
610
621
|
function watchConnections(harness) {
|
|
611
622
|
connectionSubscriptions.push(harness.cloudConnectors.subscribe((card) => {
|
|
@@ -3011,13 +3022,39 @@ export async function startWebServer(opts) {
|
|
|
3011
3022
|
case "GET /sessions": {
|
|
3012
3023
|
const prefs = await readPrefs();
|
|
3013
3024
|
const pinned = new Set(prefs.pinnedSessions ?? []);
|
|
3025
|
+
const metas = await current.harness.listSessions(50);
|
|
3026
|
+
const taskRows = new Map();
|
|
3027
|
+
// Bound disk concurrency. Read stored views only: listing history must
|
|
3028
|
+
// never create agents, switch projects or deliver pending results.
|
|
3029
|
+
for (let offset = 0; offset < metas.length; offset += 5) {
|
|
3030
|
+
await Promise.all(metas.slice(offset, offset + 5).map(async (meta) => {
|
|
3031
|
+
const runner = runners.get(meta.id);
|
|
3032
|
+
if (runner) {
|
|
3033
|
+
taskRows.set(meta.id, sidebarTaskRows(runner.agent.backgroundTasks?.list() ?? []));
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
// The stored document carries every task's conversation; parse it
|
|
3037
|
+
// again only when it has been rewritten since the last listing.
|
|
3038
|
+
const stamp = await current.harness.store.tasksStamp(meta.id);
|
|
3039
|
+
const cached = storedTaskRows.get(meta.id);
|
|
3040
|
+
if (cached && cached.stamp === stamp)
|
|
3041
|
+
return void taskRows.set(meta.id, cached.rows);
|
|
3042
|
+
const rows = stamp === undefined ? [] : sidebarTaskRows((await current.harness.store.readTasks(meta.id)).flatMap((raw) => {
|
|
3043
|
+
const view = raw?.view;
|
|
3044
|
+
return view?.id ? [restoredTaskView(view)] : [];
|
|
3045
|
+
}));
|
|
3046
|
+
storedTaskRows.set(meta.id, { stamp, rows });
|
|
3047
|
+
taskRows.set(meta.id, rows);
|
|
3048
|
+
}));
|
|
3049
|
+
}
|
|
3014
3050
|
return send(res, 200, {
|
|
3015
3051
|
// What somebody arranged by hand travels beside the list rather than
|
|
3016
3052
|
// being applied to it here: the sidebar groups by project and hides
|
|
3017
3053
|
// the tail of each group, so it is the only side that knows which
|
|
3018
3054
|
// rows an order has to survive.
|
|
3019
3055
|
order: prefs.sessionOrder ?? {},
|
|
3020
|
-
sessions:
|
|
3056
|
+
sessions: metas.map((meta) => ({
|
|
3057
|
+
tasks: taskRows.get(meta.id) ?? [],
|
|
3021
3058
|
id: meta.id,
|
|
3022
3059
|
pinned: pinned.has(meta.id),
|
|
3023
3060
|
title: meta.title,
|
|
@@ -3085,7 +3122,8 @@ export async function startWebServer(opts) {
|
|
|
3085
3122
|
...row,
|
|
3086
3123
|
current: row.id === current.session.id,
|
|
3087
3124
|
})),
|
|
3088
|
-
|
|
3125
|
+
// Sessions, not rows: a day of background calls is listed but is not one.
|
|
3126
|
+
total: list.filter((row) => !row.background).length,
|
|
3089
3127
|
offset,
|
|
3090
3128
|
more: offset + limit < list.length,
|
|
3091
3129
|
truncated: stored.length >= USAGE_SESSIONS_SCAN,
|
|
@@ -3118,6 +3156,9 @@ export async function startWebServer(opts) {
|
|
|
3118
3156
|
return into;
|
|
3119
3157
|
};
|
|
3120
3158
|
const totals = list.reduce(add, { ...zero });
|
|
3159
|
+
// A background row is spending, not a session: it counts toward the
|
|
3160
|
+
// money and tokens, not toward how many sessions they came from.
|
|
3161
|
+
const sessionCount = list.filter((row) => !row.background).length;
|
|
3121
3162
|
// Per model, because that is the axis a bill is read along: the same
|
|
3122
3163
|
// work costs an order of magnitude more on one model than another, and
|
|
3123
3164
|
// the only way to see that is to have them side by side.
|
|
@@ -3125,7 +3166,8 @@ export async function startWebServer(opts) {
|
|
|
3125
3166
|
for (const row of list) {
|
|
3126
3167
|
const bucket = byModel.get(row.model)
|
|
3127
3168
|
?? { model: row.model, sessions: 0, lastUsedAt: 0, ...zero };
|
|
3128
|
-
|
|
3169
|
+
if (!row.background)
|
|
3170
|
+
bucket.sessions += 1;
|
|
3129
3171
|
bucket.lastUsedAt = Math.max(bucket.lastUsedAt, row.updatedAt);
|
|
3130
3172
|
byModel.set(row.model, add(bucket, row));
|
|
3131
3173
|
}
|
|
@@ -3138,7 +3180,7 @@ export async function startWebServer(opts) {
|
|
|
3138
3180
|
from: window.from,
|
|
3139
3181
|
to: window.to,
|
|
3140
3182
|
series: usageSeries(list, since || (list.length ? list[list.length - 1].updatedAt : Date.now()), until),
|
|
3141
|
-
totals: { ...totals, sessions:
|
|
3183
|
+
totals: { ...totals, sessions: sessionCount },
|
|
3142
3184
|
// Sorted by what was spent, not by name: the row worth seeing first
|
|
3143
3185
|
// is the expensive one.
|
|
3144
3186
|
models: [...byModel.values()].sort((a, b) => b.costUsd - a.costUsd),
|
|
@@ -3870,6 +3912,7 @@ export async function startWebServer(opts) {
|
|
|
3870
3912
|
await runners.get(id)?.agent.discardBackgroundTasks();
|
|
3871
3913
|
const forks = (await current.harness.store.children(id)).length;
|
|
3872
3914
|
const deleted = await current.harness.deleteSession(id);
|
|
3915
|
+
storedTaskRows.delete(id);
|
|
3873
3916
|
await removeTurnShots(id);
|
|
3874
3917
|
// A session nobody has spoken to has nothing on disk yet — the store
|
|
3875
3918
|
// writes on the first message — but it is still a session someone
|
|
@@ -5318,6 +5361,16 @@ export async function startWebServer(opts) {
|
|
|
5318
5361
|
...tokensOf(sess.usage),
|
|
5319
5362
|
});
|
|
5320
5363
|
}
|
|
5364
|
+
// Calls made outside any conversation (dreaming) have no transcript to sum;
|
|
5365
|
+
// they are recorded per day, purpose and model, and join the totals here.
|
|
5366
|
+
for (const record of await BackgroundUsage.shared.records().catch(() => [])) {
|
|
5367
|
+
const id = `background:${record.purpose}:${record.day}:${record.model}`;
|
|
5368
|
+
rows.set(id, {
|
|
5369
|
+
id, title: record.purpose, model: record.model, updatedAt: record.updatedAt, turns: 0,
|
|
5370
|
+
costUsd: record.costUsd, input: record.input, output: record.output, cacheRead: record.cacheRead, cacheWrite: record.cacheWrite,
|
|
5371
|
+
background: record.purpose,
|
|
5372
|
+
});
|
|
5373
|
+
}
|
|
5321
5374
|
return { stored, rows };
|
|
5322
5375
|
}
|
|
5323
5376
|
async function artifactCatalogue() {
|