@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.
@@ -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
- pane(session).tasks = tasks || [];
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
- return;
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 = stopped;
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4R9B,CAAC"}
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
- line.appendChild(el("span", "row-title", entry.title || t("session.untitled")));
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAkxC3B,CAAC"}
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"}
@@ -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:12px 14px;max-height:220px;overflow-y:auto;border-radius:10px;background:var(--hover);font-size:13px;line-height:1.55;white-space:pre-wrap}
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}
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm0HV,CAAC"}
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: (await current.harness.listSessions(50)).map((meta) => ({
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
- total: list.length,
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
- bucket.sessions += 1;
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: list.length },
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() {