@khalilgharbaoui/opencode-claude-code-plugin 0.56.0 → 0.57.1
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/index.js +252 -49
- package/dist/index.js.map +1 -1
- package/dist/tui.js +754 -140
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +20 -3
package/dist/tui.js
CHANGED
|
@@ -8,13 +8,14 @@ var FINISHED_TTL_MS = 10 * 6e4;
|
|
|
8
8
|
var FINISHED_KEEP = 3;
|
|
9
9
|
var STARTING_GRACE_MS = 1e4;
|
|
10
10
|
var SIDEBAR_ROW_WIDTH = 36;
|
|
11
|
+
var ROW_INDENT = " ";
|
|
11
12
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
12
13
|
var TITLE_SUFFIX = /\s*\(@([^)\s]+) subagent\)\s*$/;
|
|
13
14
|
function parseChildTitle(title) {
|
|
14
|
-
const
|
|
15
|
-
const match = TITLE_SUFFIX.exec(
|
|
16
|
-
if (!match) return { description:
|
|
17
|
-
return { description:
|
|
15
|
+
const text3 = (title ?? "").trim();
|
|
16
|
+
const match = TITLE_SUFFIX.exec(text3);
|
|
17
|
+
if (!match) return { description: text3 };
|
|
18
|
+
return { description: text3.slice(0, match.index).trim(), agent: match[1] };
|
|
18
19
|
}
|
|
19
20
|
function text(value) {
|
|
20
21
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -81,6 +82,12 @@ function subagentLabel(record, part) {
|
|
|
81
82
|
const account = record?.model ? text(record.account) : accountFromProvider(askedProvider);
|
|
82
83
|
return [model, effort, account].filter(Boolean).join(" \xB7 ");
|
|
83
84
|
}
|
|
85
|
+
function formatCost(value) {
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
|
|
87
|
+
if (value < 5e-3) return "<$0.01";
|
|
88
|
+
const digits = [2, 1, 0].map((places) => value.toFixed(places)).find((text3) => text3.length <= 4);
|
|
89
|
+
return `$${digits ?? value.toFixed(0)}`;
|
|
90
|
+
}
|
|
84
91
|
function truncate(value, width) {
|
|
85
92
|
const chars = Array.from(value.replace(/\s+/g, " ").trim());
|
|
86
93
|
if (width <= 0) return "";
|
|
@@ -157,6 +164,7 @@ function rowFor(input, child, parsed, part, completion) {
|
|
|
157
164
|
if (state !== "running" && finished === void 0) {
|
|
158
165
|
finished = input.stoppedAt(child.id) ?? child.updated ?? started;
|
|
159
166
|
}
|
|
167
|
+
const cost = input.cost?.(child.id);
|
|
160
168
|
return {
|
|
161
169
|
sessionID: child.id,
|
|
162
170
|
agent: parsed.agent ?? (part ? partAgent(part) : void 0) ?? text(child.agent) ?? "subagent",
|
|
@@ -165,6 +173,7 @@ function rowFor(input, child, parsed, part, completion) {
|
|
|
165
173
|
state,
|
|
166
174
|
label: subagentLabel(input.record(child.id), part),
|
|
167
175
|
effort: text(input.record(child.id)?.effort),
|
|
176
|
+
...cost !== void 0 ? { cost } : {},
|
|
168
177
|
started,
|
|
169
178
|
finished
|
|
170
179
|
};
|
|
@@ -193,15 +202,29 @@ function layoutRow(row, width = SIDEBAR_ROW_WIDTH, frame = 0) {
|
|
|
193
202
|
first.push({ text: marker, role: "muted" });
|
|
194
203
|
}
|
|
195
204
|
const second = [];
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
const content = width - ROW_INDENT.length;
|
|
206
|
+
const pieces = row.label ? truncate(row.label, content).split(" \xB7 ") : [];
|
|
207
|
+
const labelWidth = pieces.reduce((sum, piece) => sum + Array.from(piece).length, 0) + 3 * Math.max(0, pieces.length - 1);
|
|
208
|
+
const cost = formatCost(row.cost);
|
|
209
|
+
const costWidth = cost ? Array.from(cost).length : 0;
|
|
210
|
+
let joiner;
|
|
211
|
+
if (cost) {
|
|
212
|
+
if (pieces.length === 0) joiner = costWidth <= content ? "" : void 0;
|
|
213
|
+
else if (labelWidth + 3 + costWidth <= content) joiner = " \xB7 ";
|
|
214
|
+
else if (labelWidth + 1 + costWidth <= content) joiner = " ";
|
|
215
|
+
}
|
|
216
|
+
if (pieces.length > 0 || joiner !== void 0) {
|
|
198
217
|
const effortAt = row.effort ? pieces.indexOf(row.effort) : -1;
|
|
199
|
-
second.push({ text:
|
|
218
|
+
second.push({ text: ROW_INDENT, role: "muted" });
|
|
200
219
|
pieces.forEach((piece, index) => {
|
|
201
220
|
if (index > 0) second.push({ text: " \xB7 ", role: "muted" });
|
|
202
221
|
const role = index !== effortAt ? "muted" : finished ? "effort-faded" : "effort";
|
|
203
222
|
second.push({ text: piece, role });
|
|
204
223
|
});
|
|
224
|
+
if (cost && joiner !== void 0) {
|
|
225
|
+
if (joiner) second.push({ text: joiner, role: "muted" });
|
|
226
|
+
second.push({ text: cost, role: "cost" });
|
|
227
|
+
}
|
|
205
228
|
}
|
|
206
229
|
return { first, second };
|
|
207
230
|
}
|
|
@@ -210,6 +233,199 @@ function collapsedSummary(rows) {
|
|
|
210
233
|
return running > 0 ? `${running} running` : `${rows.length} done`;
|
|
211
234
|
}
|
|
212
235
|
|
|
236
|
+
// src/progress-line.ts
|
|
237
|
+
var BAR_WIDTH = 10;
|
|
238
|
+
var PROGRESS_LINGER_MS = 15e3;
|
|
239
|
+
var BAR_DONE = "\u25B0";
|
|
240
|
+
var BAR_TODO = "\u25B1";
|
|
241
|
+
function text2(value) {
|
|
242
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
243
|
+
}
|
|
244
|
+
function cells(value) {
|
|
245
|
+
return Array.from(value).length;
|
|
246
|
+
}
|
|
247
|
+
function turnMessages(messages, isUser) {
|
|
248
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
249
|
+
if (isUser(messages[index])) return index === 0 ? messages : messages.slice(index);
|
|
250
|
+
}
|
|
251
|
+
return messages;
|
|
252
|
+
}
|
|
253
|
+
function measureProgress(source) {
|
|
254
|
+
const messages = source.messages;
|
|
255
|
+
let turnStart = -1;
|
|
256
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
257
|
+
if (messages[index].role === "user") {
|
|
258
|
+
turnStart = index;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let calls = 0;
|
|
263
|
+
let tool;
|
|
264
|
+
let failed = false;
|
|
265
|
+
for (let index = turnStart + 1; index < messages.length; index += 1) {
|
|
266
|
+
const message = messages[index];
|
|
267
|
+
if (message.role !== "assistant") continue;
|
|
268
|
+
failed = message.error === true;
|
|
269
|
+
for (const call of message.tools) {
|
|
270
|
+
calls += 1;
|
|
271
|
+
const running = call.status === "running" || call.status === "pending";
|
|
272
|
+
tool = running ? text2(call.name) : void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
let total = 0;
|
|
276
|
+
let done = 0;
|
|
277
|
+
for (const todo of source.todos) {
|
|
278
|
+
if (todo.status === "cancelled") continue;
|
|
279
|
+
total += 1;
|
|
280
|
+
if (todo.status === "completed") done += 1;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
plan: total > 0 ? { done, total } : void 0,
|
|
284
|
+
startedAt: turnStart >= 0 ? messages[turnStart].created : void 0,
|
|
285
|
+
chars: turnStart >= 0 ? messages[turnStart].chars : void 0,
|
|
286
|
+
calls,
|
|
287
|
+
tool,
|
|
288
|
+
failed
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function sessionProgress(measure, input) {
|
|
292
|
+
const startedAt = measure.startedAt ?? input.busySince;
|
|
293
|
+
if (input.status === "busy") {
|
|
294
|
+
return {
|
|
295
|
+
state: "running",
|
|
296
|
+
plan: measure.plan,
|
|
297
|
+
elapsedMs: Math.max(0, input.now - (startedAt ?? input.now)),
|
|
298
|
+
calls: measure.calls,
|
|
299
|
+
tool: measure.tool
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (input.stoppedAt === void 0) return void 0;
|
|
303
|
+
if (startedAt !== void 0 && input.stoppedAt < startedAt) return void 0;
|
|
304
|
+
if (input.now - input.stoppedAt > PROGRESS_LINGER_MS) return void 0;
|
|
305
|
+
return {
|
|
306
|
+
state: measure.failed ? "error" : "done",
|
|
307
|
+
plan: measure.plan,
|
|
308
|
+
elapsedMs: Math.max(0, input.stoppedAt - (startedAt ?? input.stoppedAt)),
|
|
309
|
+
calls: measure.calls,
|
|
310
|
+
tool: void 0
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function rowProgress(measure, input) {
|
|
314
|
+
const startedAt = measure.startedAt ?? input.started;
|
|
315
|
+
return {
|
|
316
|
+
state: "running",
|
|
317
|
+
plan: measure.plan,
|
|
318
|
+
elapsedMs: Math.max(0, input.now - startedAt),
|
|
319
|
+
calls: measure.calls,
|
|
320
|
+
tool: measure.tool
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
var STATUS_WATCH_CAP = 256;
|
|
324
|
+
function createStatusWatch() {
|
|
325
|
+
const seen = /* @__PURE__ */ new Map();
|
|
326
|
+
return {
|
|
327
|
+
observe(sessionID, status, now) {
|
|
328
|
+
let entry = seen.get(sessionID);
|
|
329
|
+
if (!entry) {
|
|
330
|
+
entry = { status: "unknown" };
|
|
331
|
+
if (seen.size >= STATUS_WATCH_CAP) {
|
|
332
|
+
const oldest = seen.keys().next().value;
|
|
333
|
+
if (oldest !== void 0) seen.delete(oldest);
|
|
334
|
+
}
|
|
335
|
+
seen.set(sessionID, entry);
|
|
336
|
+
}
|
|
337
|
+
if (status === "busy" && entry.status !== "busy") {
|
|
338
|
+
entry.busySince = now;
|
|
339
|
+
entry.stoppedAt = void 0;
|
|
340
|
+
} else if (status === "idle" && entry.status === "busy") {
|
|
341
|
+
entry.stoppedAt = now;
|
|
342
|
+
}
|
|
343
|
+
if (status !== "unknown") entry.status = status;
|
|
344
|
+
return { busySince: entry.busySince, stoppedAt: entry.stoppedAt };
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function createFillMemory() {
|
|
349
|
+
const held = /* @__PURE__ */ new Map();
|
|
350
|
+
return {
|
|
351
|
+
hold(sessionID, turn, fill) {
|
|
352
|
+
const entry = held.get(sessionID);
|
|
353
|
+
if (entry && entry.turn === turn) {
|
|
354
|
+
entry.fill = Math.max(entry.fill, fill);
|
|
355
|
+
return entry.fill;
|
|
356
|
+
}
|
|
357
|
+
if (!entry && held.size >= STATUS_WATCH_CAP) {
|
|
358
|
+
const oldest = held.keys().next().value;
|
|
359
|
+
if (oldest !== void 0) held.delete(oldest);
|
|
360
|
+
}
|
|
361
|
+
held.set(sessionID, { turn, fill });
|
|
362
|
+
return fill;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function filledCells(plan, width = BAR_WIDTH) {
|
|
367
|
+
if (plan.total <= 0 || plan.done <= 0) return 0;
|
|
368
|
+
if (plan.done >= plan.total) return width;
|
|
369
|
+
return Math.min(width - 1, Math.max(1, Math.round(plan.done / plan.total * width)));
|
|
370
|
+
}
|
|
371
|
+
function estimateCells(estimate, width = BAR_WIDTH) {
|
|
372
|
+
if (estimate.over) return width;
|
|
373
|
+
return Math.min(width - 1, Math.max(0, Math.round(estimate.fill * width)));
|
|
374
|
+
}
|
|
375
|
+
function formatElapsed(ms) {
|
|
376
|
+
const seconds = Math.max(0, Math.floor(ms / 1e3));
|
|
377
|
+
if (seconds < 60) return `${seconds}s`;
|
|
378
|
+
const minutes = Math.floor(seconds / 60);
|
|
379
|
+
const pad2 = (value) => String(value).padStart(2, "0");
|
|
380
|
+
if (minutes < 60) return `${minutes}m${pad2(seconds % 60)}s`;
|
|
381
|
+
return `${Math.floor(minutes / 60)}h${pad2(minutes % 60)}m`;
|
|
382
|
+
}
|
|
383
|
+
function layoutProgressLine(view, lead, width = SIDEBAR_ROW_WIDTH, frame = 0) {
|
|
384
|
+
const ended = view.state !== "running";
|
|
385
|
+
const endedRole = view.state === "error" ? "error" : "done";
|
|
386
|
+
const leadSegments = lead === "indent" ? [{ text: ROW_INDENT, role: "muted" }] : [
|
|
387
|
+
view.state === "running" ? { text: SPINNER_FRAMES[frame % SPINNER_FRAMES.length], role: "spinner" } : { text: view.state === "done" ? "\u2713" : "\u2717", role: endedRole },
|
|
388
|
+
{ text: " ", role: "muted" }
|
|
389
|
+
];
|
|
390
|
+
const estimate = !ended && !view.plan ? view.estimate : void 0;
|
|
391
|
+
const filled = ended ? BAR_WIDTH : view.plan ? filledCells(view.plan) : estimate ? estimateCells(estimate) : void 0;
|
|
392
|
+
const litRole = ended ? endedRole : estimate?.over ? "bar-over" : "bar-done";
|
|
393
|
+
const bar = [];
|
|
394
|
+
if (filled !== void 0) {
|
|
395
|
+
for (let cell = 0; cell < BAR_WIDTH; cell += 1) {
|
|
396
|
+
bar.push(cell < filled ? { text: BAR_DONE, role: litRole } : { text: BAR_TODO, role: "bar-track" });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const required = [];
|
|
400
|
+
if (view.plan) {
|
|
401
|
+
required.push({ text: `${view.plan.done}/${view.plan.total}`, role: ended ? "muted" : "fraction" });
|
|
402
|
+
required.push({ text: formatElapsed(view.elapsedMs), role: "muted" });
|
|
403
|
+
} else if (estimate) {
|
|
404
|
+
required.push({
|
|
405
|
+
text: `${formatElapsed(view.elapsedMs)}/~${formatElapsed(estimate.totalMs)}`,
|
|
406
|
+
role: estimate.over ? "estimate-over" : "estimate"
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
required.push({ text: formatElapsed(view.elapsedMs), role: "muted" });
|
|
410
|
+
}
|
|
411
|
+
const calls = !view.plan && !estimate && view.calls > 0 ? { text: `${view.calls} call${view.calls === 1 ? "" : "s"}`, role: "muted" } : void 0;
|
|
412
|
+
const tool = !ended && view.tool ? { text: view.tool, role: "tool" } : void 0;
|
|
413
|
+
const budget = width - leadSegments.reduce((sum, segment) => sum + cells(segment.text), 0) - (bar.length > 0 ? BAR_WIDTH + 2 : 0);
|
|
414
|
+
const widthOf = (pieces2) => pieces2.reduce((sum, piece) => sum + cells(piece.text), 0) + 3 * Math.max(0, pieces2.length - 1);
|
|
415
|
+
let pieces = [...required, ...calls ? [calls] : [], ...tool ? [tool] : []];
|
|
416
|
+
if (widthOf(pieces) > budget && calls) pieces = [...required, ...tool ? [tool] : []];
|
|
417
|
+
if (widthOf(pieces) > budget && tool) {
|
|
418
|
+
const room = budget - widthOf(required) - 3;
|
|
419
|
+
pieces = room >= 2 ? [...required, { text: truncate(tool.text, room), role: "tool" }] : required;
|
|
420
|
+
}
|
|
421
|
+
const tail = bar.length > 0 ? [{ text: " ", role: "muted" }] : [];
|
|
422
|
+
pieces.forEach((piece, index) => {
|
|
423
|
+
if (index > 0) tail.push({ text: " \xB7 ", role: "muted" });
|
|
424
|
+
tail.push(piece);
|
|
425
|
+
});
|
|
426
|
+
return { lead: leadSegments, bar, tail };
|
|
427
|
+
}
|
|
428
|
+
|
|
213
429
|
// src/spawn-record-store.ts
|
|
214
430
|
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
215
431
|
import { homedir } from "os";
|
|
@@ -226,7 +442,7 @@ function isRecord(value) {
|
|
|
226
442
|
if (!record || typeof record !== "object" || Array.isArray(record)) return false;
|
|
227
443
|
if (typeof record.at !== "number" || !Number.isFinite(record.at)) return false;
|
|
228
444
|
const ok = (field) => field === void 0 || typeof field === "string";
|
|
229
|
-
if (!ok(record.model) || !ok(record.effort) || !ok(record.account)) return false;
|
|
445
|
+
if (!ok(record.model) || !ok(record.effort) || !ok(record.account) || !ok(record.agent)) return false;
|
|
230
446
|
return !!record.model;
|
|
231
447
|
}
|
|
232
448
|
function readEntries(file) {
|
|
@@ -242,6 +458,7 @@ function readEntries(file) {
|
|
|
242
458
|
model: record.model,
|
|
243
459
|
...record.effort ? { effort: record.effort } : {},
|
|
244
460
|
...record.account ? { account: record.account } : {},
|
|
461
|
+
...record.agent ? { agent: record.agent } : {},
|
|
245
462
|
at: record.at
|
|
246
463
|
});
|
|
247
464
|
}
|
|
@@ -271,20 +488,150 @@ function createSpawnRecordReader(file = spawnRecordStorePath) {
|
|
|
271
488
|
};
|
|
272
489
|
}
|
|
273
490
|
|
|
274
|
-
// src/
|
|
491
|
+
// src/turn-history.ts
|
|
275
492
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
276
493
|
import { homedir as homedir2 } from "os";
|
|
277
494
|
import { join as join2 } from "path";
|
|
495
|
+
var MAX_TURN_SAMPLES = 48;
|
|
496
|
+
var TURN_SAMPLE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
497
|
+
var MIN_TURN_SAMPLES = 3;
|
|
498
|
+
var CHARS_BANDWIDTH = Math.log(4);
|
|
499
|
+
var OTHER_EFFORT_WEIGHT = 0.5;
|
|
500
|
+
var CHARS_FLOOR = 50;
|
|
501
|
+
var pathOverride2 = null;
|
|
502
|
+
function turnHistoryStorePath() {
|
|
503
|
+
if (pathOverride2) return pathOverride2;
|
|
504
|
+
const stateRoot = process.env.XDG_STATE_HOME || join2(homedir2(), ".local", "state");
|
|
505
|
+
return join2(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
|
|
506
|
+
}
|
|
507
|
+
function turnHistoryKey(agent, model) {
|
|
508
|
+
return `${agent || "main"}/${model}`;
|
|
509
|
+
}
|
|
510
|
+
var INJECTED_BLOCKS = [
|
|
511
|
+
/<system-reminder>[\s\S]*?<\/system-reminder>/g,
|
|
512
|
+
/<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g,
|
|
513
|
+
/<dcp-message-id>[\s\S]*?<\/dcp-message-id>/g
|
|
514
|
+
];
|
|
515
|
+
function promptChars(texts) {
|
|
516
|
+
if (texts.length === 0) return void 0;
|
|
517
|
+
let total = 0;
|
|
518
|
+
for (const text3 of texts) {
|
|
519
|
+
let stripped = text3;
|
|
520
|
+
for (const block of INJECTED_BLOCKS) stripped = stripped.replace(block, "");
|
|
521
|
+
total += stripped.trim().length;
|
|
522
|
+
}
|
|
523
|
+
return total;
|
|
524
|
+
}
|
|
525
|
+
function isSample(value) {
|
|
526
|
+
const sample = value;
|
|
527
|
+
if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
|
|
528
|
+
if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
|
|
529
|
+
if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
|
|
530
|
+
if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
function cleanSample(sample) {
|
|
537
|
+
return {
|
|
538
|
+
ms: Math.round(sample.ms),
|
|
539
|
+
...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
|
|
540
|
+
...sample.effort ? { effort: sample.effort } : {},
|
|
541
|
+
at: sample.at
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function readEntries2(file, now) {
|
|
545
|
+
const out = /* @__PURE__ */ new Map();
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
548
|
+
const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
|
|
549
|
+
if (!entries || typeof entries !== "object") return out;
|
|
550
|
+
const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
|
|
551
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
552
|
+
if (!key || !Array.isArray(value)) continue;
|
|
553
|
+
const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
|
|
554
|
+
if (samples.length > 0) out.set(key, samples);
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
function createTurnHistoryReader(file = turnHistoryStorePath) {
|
|
561
|
+
let identity = "";
|
|
562
|
+
let cached = /* @__PURE__ */ new Map();
|
|
563
|
+
return {
|
|
564
|
+
read(now = Date.now()) {
|
|
565
|
+
const path = file();
|
|
566
|
+
let next = "";
|
|
567
|
+
try {
|
|
568
|
+
const stats = statSync2(path);
|
|
569
|
+
next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
570
|
+
} catch {
|
|
571
|
+
next = "missing";
|
|
572
|
+
}
|
|
573
|
+
if (next !== identity) {
|
|
574
|
+
identity = next;
|
|
575
|
+
cached = next === "missing" ? /* @__PURE__ */ new Map() : readEntries2(path, now);
|
|
576
|
+
}
|
|
577
|
+
return cached;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function weightedMedian(values, weights) {
|
|
582
|
+
const order = values.map((_, index) => index).sort((a, b) => values[a] - values[b]);
|
|
583
|
+
const total = order.reduce((sum, index) => sum + weights[index], 0);
|
|
584
|
+
let cumulative = 0;
|
|
585
|
+
for (const index of order) {
|
|
586
|
+
cumulative += weights[index];
|
|
587
|
+
if (cumulative >= total / 2) return values[index];
|
|
588
|
+
}
|
|
589
|
+
return values[order[order.length - 1]];
|
|
590
|
+
}
|
|
591
|
+
function charsWeight(a, b) {
|
|
592
|
+
if (a === void 0 || b === void 0) return 1;
|
|
593
|
+
const distance = Math.log((a + CHARS_FLOOR) / (b + CHARS_FLOOR)) / CHARS_BANDWIDTH;
|
|
594
|
+
return Math.exp(-0.5 * distance * distance);
|
|
595
|
+
}
|
|
596
|
+
function effortWeight(a, b) {
|
|
597
|
+
return a === void 0 || b === void 0 || a === b ? 1 : OTHER_EFFORT_WEIGHT;
|
|
598
|
+
}
|
|
599
|
+
function estimateTurn(samples, input) {
|
|
600
|
+
const usable = samples.filter((sample) => Number.isFinite(sample.ms) && sample.ms >= 0);
|
|
601
|
+
if (usable.length < MIN_TURN_SAMPLES) return void 0;
|
|
602
|
+
const elapsed = Math.max(0, input.elapsedMs);
|
|
603
|
+
const weights = usable.map(
|
|
604
|
+
(sample) => charsWeight(input.chars, sample.chars) * effortWeight(input.effort, sample.effort)
|
|
605
|
+
);
|
|
606
|
+
const survivors = usable.map((sample, index) => ({ ms: sample.ms, weight: weights[index] })).filter((sample) => sample.ms > elapsed);
|
|
607
|
+
if (survivors.length === 0) {
|
|
608
|
+
const usual = weightedMedian(usable.map((sample) => sample.ms), weights);
|
|
609
|
+
return { totalMs: usual, fill: 1, over: true };
|
|
610
|
+
}
|
|
611
|
+
const residual = weightedMedian(
|
|
612
|
+
survivors.map((sample) => sample.ms - elapsed),
|
|
613
|
+
survivors.map((sample) => sample.weight)
|
|
614
|
+
);
|
|
615
|
+
const totalMs = elapsed + residual;
|
|
616
|
+
return { totalMs, fill: totalMs > 0 ? elapsed / totalMs : 0, over: false };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/account-usage.ts
|
|
620
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
621
|
+
import { homedir as homedir3 } from "os";
|
|
622
|
+
import { join as join3 } from "path";
|
|
278
623
|
var UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
279
624
|
var USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
625
|
+
var MAX_USAGE_ACCOUNTS = 32;
|
|
280
626
|
var NAME = /^[a-z0-9_-]{1,64}$/;
|
|
281
627
|
var STATUS = /^[a-z_]{1,32}$/;
|
|
628
|
+
var TIER = /^[A-Za-z0-9][A-Za-z0-9 ]{0,23}$/;
|
|
282
629
|
var MAX_RESETS_TEXT = 80;
|
|
283
|
-
var
|
|
630
|
+
var pathOverride3 = null;
|
|
284
631
|
function accountUsageStorePath() {
|
|
285
|
-
if (
|
|
286
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
287
|
-
return
|
|
632
|
+
if (pathOverride3) return pathOverride3;
|
|
633
|
+
const stateRoot = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
|
|
634
|
+
return join3(stateRoot, "opencode-claude-code-plugin", "account-usage.json");
|
|
288
635
|
}
|
|
289
636
|
function finite(value) {
|
|
290
637
|
return typeof value === "number" && Number.isFinite(value);
|
|
@@ -311,11 +658,28 @@ function validEntry(value) {
|
|
|
311
658
|
noted
|
|
312
659
|
};
|
|
313
660
|
}
|
|
314
|
-
|
|
661
|
+
var emptyStore = () => ({ usage: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() });
|
|
662
|
+
function readKnown(parsed) {
|
|
315
663
|
const out = /* @__PURE__ */ new Map();
|
|
664
|
+
const known = parsed.known;
|
|
665
|
+
if (!known || typeof known !== "object" || Array.isArray(known)) return out;
|
|
666
|
+
for (const [account, raw] of Object.entries(known)) {
|
|
667
|
+
if (out.size >= MAX_USAGE_ACCOUNTS) break;
|
|
668
|
+
if (!NAME.test(account) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
669
|
+
const record = raw;
|
|
670
|
+
if (!finite(record.at)) continue;
|
|
671
|
+
const tier = typeof record.tier === "string" && TIER.test(record.tier) ? record.tier : void 0;
|
|
672
|
+
out.set(account, { ...tier ? { tier } : {}, at: record.at });
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
function parseStore(file, now) {
|
|
677
|
+
const out = emptyStore();
|
|
316
678
|
try {
|
|
317
|
-
const parsed = JSON.parse(
|
|
318
|
-
|
|
679
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
680
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return out;
|
|
681
|
+
out.known = readKnown(parsed);
|
|
682
|
+
const accounts = parsed.accounts;
|
|
319
683
|
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return out;
|
|
320
684
|
const cutoff = now - USAGE_READING_MAX_AGE_MS;
|
|
321
685
|
for (const [account, windows] of Object.entries(accounts)) {
|
|
@@ -326,7 +690,7 @@ function readStore(file, now) {
|
|
|
326
690
|
const entry = validEntry(raw);
|
|
327
691
|
if (entry && entry.observedAt >= cutoff) kept.set(window, entry);
|
|
328
692
|
}
|
|
329
|
-
if (kept.size > 0) out.set(account, kept);
|
|
693
|
+
if (kept.size > 0) out.usage.set(account, kept);
|
|
330
694
|
}
|
|
331
695
|
} catch {
|
|
332
696
|
}
|
|
@@ -341,28 +705,39 @@ function usagePercent(utilization) {
|
|
|
341
705
|
}
|
|
342
706
|
function createAccountUsageReader(file = accountUsageStorePath) {
|
|
343
707
|
let identity = "";
|
|
344
|
-
let
|
|
708
|
+
let store = emptyStore();
|
|
709
|
+
const refresh = () => {
|
|
710
|
+
const path = file();
|
|
711
|
+
let next = "";
|
|
712
|
+
try {
|
|
713
|
+
const stats = statSync3(path);
|
|
714
|
+
next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
715
|
+
} catch {
|
|
716
|
+
next = "missing";
|
|
717
|
+
}
|
|
718
|
+
if (next !== identity) {
|
|
719
|
+
identity = next;
|
|
720
|
+
store = next === "missing" ? emptyStore() : parseStore(path, Date.now());
|
|
721
|
+
}
|
|
722
|
+
};
|
|
345
723
|
return {
|
|
346
724
|
read() {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
if (next !== identity) {
|
|
356
|
-
identity = next;
|
|
357
|
-
usage = next === "missing" ? /* @__PURE__ */ new Map() : readStore(path, Date.now());
|
|
358
|
-
}
|
|
359
|
-
return usage;
|
|
725
|
+
refresh();
|
|
726
|
+
return store.usage;
|
|
727
|
+
},
|
|
728
|
+
/** The accounts registered at startup, with their tiers: the same map while the file is unchanged. */
|
|
729
|
+
known() {
|
|
730
|
+
refresh();
|
|
731
|
+
return store.known;
|
|
360
732
|
}
|
|
361
733
|
};
|
|
362
734
|
}
|
|
363
735
|
|
|
364
736
|
// src/account-sidebar.ts
|
|
365
737
|
var WARNING_PERCENT = 75;
|
|
738
|
+
var NO_READING_TEXT = "no reading yet";
|
|
739
|
+
var RESET_WORD = "resets ";
|
|
740
|
+
var SEPARATOR = " \xB7 ";
|
|
366
741
|
var SHORT_WINDOWS = {
|
|
367
742
|
five_hour: "5h",
|
|
368
743
|
seven_day: "7d",
|
|
@@ -375,14 +750,25 @@ function shortWindowName(window) {
|
|
|
375
750
|
if (window === "unknown") return "";
|
|
376
751
|
return SHORT_WINDOWS[window] ?? window.replace(/_/g, " ");
|
|
377
752
|
}
|
|
378
|
-
var
|
|
379
|
-
function
|
|
753
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
754
|
+
function sameDay(a, b) {
|
|
755
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
756
|
+
}
|
|
757
|
+
function formatResetClock(resetsAt, now, year = true) {
|
|
380
758
|
const when = new Date(resetsAt);
|
|
381
|
-
const today = new Date(now);
|
|
382
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
383
759
|
const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
384
|
-
|
|
385
|
-
|
|
760
|
+
if (sameDay(when, new Date(now))) return clock;
|
|
761
|
+
const date = `${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${clock}`;
|
|
762
|
+
return year ? `${when.getFullYear()}-${date}` : date;
|
|
763
|
+
}
|
|
764
|
+
function resetSpellings(entry, now) {
|
|
765
|
+
if (entry.resetsAt !== void 0) {
|
|
766
|
+
const full = formatResetClock(entry.resetsAt, now);
|
|
767
|
+
if (sameDay(new Date(entry.resetsAt), new Date(now))) return [`${RESET_WORD}${full}`];
|
|
768
|
+
return [`${RESET_WORD}${full}`, full, formatResetClock(entry.resetsAt, now, false)];
|
|
769
|
+
}
|
|
770
|
+
if (entry.resetsText) return [`${RESET_WORD}${entry.resetsText}`, entry.resetsText];
|
|
771
|
+
return [];
|
|
386
772
|
}
|
|
387
773
|
function rank(entry) {
|
|
388
774
|
if (entry.status === "rejected") return 1e3;
|
|
@@ -397,29 +783,94 @@ function pressingWindow(windows, now) {
|
|
|
397
783
|
}
|
|
398
784
|
return best;
|
|
399
785
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
786
|
+
var cells2 = (value) => Array.from(value).length;
|
|
787
|
+
var segmentsCells = (segments) => segments.reduce((sum, segment) => sum + cells2(segment.text), 0);
|
|
788
|
+
function levelRole(state) {
|
|
789
|
+
return state === "limited" ? "error" : state === "warning" ? "effort" : "ok";
|
|
790
|
+
}
|
|
791
|
+
function fitSegments(segments, budget) {
|
|
792
|
+
const out = [];
|
|
793
|
+
let used = 0;
|
|
794
|
+
for (const segment of segments) {
|
|
795
|
+
const length = cells2(segment.text);
|
|
796
|
+
if (used + length <= budget) {
|
|
797
|
+
out.push(segment);
|
|
798
|
+
used += length;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (segment.text !== SEPARATOR) {
|
|
802
|
+
const cut = truncate(segment.text, budget - used);
|
|
803
|
+
if (cut) out.push({ ...segment, text: cut });
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
function tailSegments(draft, spelling, withTier) {
|
|
810
|
+
if (draft.state === "none") {
|
|
811
|
+
const out2 = [];
|
|
812
|
+
if (withTier && draft.tier) out2.push({ text: SEPARATOR, role: "muted" }, { text: draft.tier, role: "tier" });
|
|
813
|
+
out2.push({ text: SEPARATOR, role: "muted" }, { text: NO_READING_TEXT, role: "muted" });
|
|
814
|
+
return out2;
|
|
815
|
+
}
|
|
816
|
+
const out = [
|
|
817
|
+
{ text: SEPARATOR, role: "muted" },
|
|
818
|
+
{ text: draft.level, role: levelRole(draft.state) }
|
|
819
|
+
];
|
|
820
|
+
const reset = draft.spellings[Math.min(spelling, draft.spellings.length - 1)];
|
|
821
|
+
if (reset) {
|
|
822
|
+
out.push({ text: SEPARATOR, role: "muted" });
|
|
823
|
+
if (reset.startsWith(RESET_WORD)) {
|
|
824
|
+
out.push({ text: RESET_WORD, role: "muted" }, { text: reset.slice(RESET_WORD.length), role: "reset" });
|
|
825
|
+
} else {
|
|
826
|
+
out.push({ text: reset, role: "reset" });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
function accountUsageLines(usage, now, width = SIDEBAR_ROW_WIDTH, known = /* @__PURE__ */ new Map()) {
|
|
832
|
+
const drafts = [];
|
|
833
|
+
const names = [.../* @__PURE__ */ new Set([...usage.keys(), ...known.keys()])].sort();
|
|
834
|
+
for (const account of names) {
|
|
835
|
+
const name = truncate(account, Math.max(1, Math.floor(width / 2)));
|
|
836
|
+
const windows = usage.get(account);
|
|
837
|
+
const pressing = windows ? pressingWindow(windows, now) : void 0;
|
|
838
|
+
if (!pressing) {
|
|
839
|
+
if (!known.has(account)) continue;
|
|
840
|
+
drafts.push({
|
|
841
|
+
account,
|
|
842
|
+
window: "",
|
|
843
|
+
state: "none",
|
|
844
|
+
name,
|
|
845
|
+
level: "",
|
|
846
|
+
tier: known.get(account)?.tier,
|
|
847
|
+
spellings: []
|
|
848
|
+
});
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
405
851
|
const [window, entry] = pressing;
|
|
406
852
|
const state = entry.status === "rejected" ? "limited" : entry.utilization !== null && usagePercent(entry.utilization) >= WARNING_PERCENT ? "warning" : "ok";
|
|
407
853
|
const short = shortWindowName(window);
|
|
408
854
|
const level = state === "limited" ? "limited" : entry.utilization === null ? `${short ? `${short} ` : ""}ok` : `${short ? `${short} ` : ""}${Math.round(usagePercent(entry.utilization))}%`;
|
|
409
|
-
|
|
410
|
-
const name = truncate(account, Math.max(1, Math.floor(width / 2)));
|
|
411
|
-
const rest = ` ${truncate(`\xB7 ${level}${resets}`, Math.max(0, width - Array.from(name).length - 1))}`;
|
|
412
|
-
lines.push({
|
|
413
|
-
account,
|
|
414
|
-
window,
|
|
415
|
-
state,
|
|
416
|
-
segments: [
|
|
417
|
-
{ text: name, role: "agent" },
|
|
418
|
-
{ text: rest, role: state === "limited" ? "error" : state === "warning" ? "effort" : "muted" }
|
|
419
|
-
]
|
|
420
|
-
});
|
|
855
|
+
drafts.push({ account, window, state, name, level, spellings: resetSpellings(entry, now) });
|
|
421
856
|
}
|
|
422
|
-
|
|
857
|
+
const budget = (draft) => Math.max(0, width - cells2(draft.name));
|
|
858
|
+
const spelt = drafts.filter((draft) => draft.state !== "none");
|
|
859
|
+
const last = Math.max(0, ...spelt.map((draft) => draft.spellings.length - 1));
|
|
860
|
+
let spelling = 0;
|
|
861
|
+
while (spelling < last && spelt.some((draft) => segmentsCells(tailSegments(draft, spelling, true)) > budget(draft))) {
|
|
862
|
+
spelling += 1;
|
|
863
|
+
}
|
|
864
|
+
return drafts.map((draft) => {
|
|
865
|
+
const full = tailSegments(draft, spelling, true);
|
|
866
|
+
const tail = segmentsCells(full) <= budget(draft) ? full : tailSegments(draft, spelling, false);
|
|
867
|
+
return {
|
|
868
|
+
account: draft.account,
|
|
869
|
+
window: draft.window,
|
|
870
|
+
state: draft.state,
|
|
871
|
+
segments: [{ text: draft.name, role: "agent" }, ...fitSegments(tail, budget(draft))]
|
|
872
|
+
};
|
|
873
|
+
});
|
|
423
874
|
}
|
|
424
875
|
|
|
425
876
|
// src/tui.ts
|
|
@@ -427,7 +878,31 @@ var SUBAGENTS_TUI_PLUGIN_ID = "opencode-claude-code-plugin.subagents";
|
|
|
427
878
|
var SECTION_ORDER = 900;
|
|
428
879
|
var SPINNER_INTERVAL_MS = 100;
|
|
429
880
|
var REFRESH_INTERVAL_MS = 1500;
|
|
881
|
+
var PROGRESS_TICK_MS = 1e3;
|
|
882
|
+
var FRAME_WRAP = SPINNER_FRAMES.length * 600;
|
|
430
883
|
var TASK_TOOLS = /* @__PURE__ */ new Set(["task", "subagent"]);
|
|
884
|
+
var ROLE_COLOR = {
|
|
885
|
+
spinner: "spinner",
|
|
886
|
+
done: "done",
|
|
887
|
+
ok: "ok",
|
|
888
|
+
error: "error",
|
|
889
|
+
agent: "text",
|
|
890
|
+
description: "text",
|
|
891
|
+
muted: "muted",
|
|
892
|
+
effort: "effort",
|
|
893
|
+
"effort-faded": "effortFaded",
|
|
894
|
+
"bar-done": "barDone",
|
|
895
|
+
// Past every recorded turn: the warning colour, which the effort already takes.
|
|
896
|
+
"bar-over": "effort",
|
|
897
|
+
"bar-track": "barTrack",
|
|
898
|
+
fraction: "fraction",
|
|
899
|
+
estimate: "fraction",
|
|
900
|
+
"estimate-over": "effort",
|
|
901
|
+
tool: "tool",
|
|
902
|
+
cost: "cost",
|
|
903
|
+
tier: "tier",
|
|
904
|
+
reset: "reset"
|
|
905
|
+
};
|
|
431
906
|
function attempt(fn, fallback) {
|
|
432
907
|
try {
|
|
433
908
|
return fn();
|
|
@@ -447,74 +922,131 @@ function mix(a, b, weight) {
|
|
|
447
922
|
);
|
|
448
923
|
}
|
|
449
924
|
function roleColor(colors, role) {
|
|
450
|
-
|
|
451
|
-
case "spinner":
|
|
452
|
-
return colors.spinner;
|
|
453
|
-
case "done":
|
|
454
|
-
return colors.done;
|
|
455
|
-
case "error":
|
|
456
|
-
return colors.error;
|
|
457
|
-
case "effort":
|
|
458
|
-
return colors.effort;
|
|
459
|
-
case "effort-faded":
|
|
460
|
-
return colors.effortFaded;
|
|
461
|
-
case "agent":
|
|
462
|
-
case "description":
|
|
463
|
-
return colors.text;
|
|
464
|
-
default:
|
|
465
|
-
return colors.muted;
|
|
466
|
-
}
|
|
925
|
+
return colors[ROLE_COLOR[role] ?? "muted"];
|
|
467
926
|
}
|
|
927
|
+
var EMPTY_SOURCE = { messages: [], todos: [] };
|
|
928
|
+
var EMPTY_VIEW = { rows: [], lines: /* @__PURE__ */ new Map() };
|
|
468
929
|
function createSidebar(host) {
|
|
469
930
|
const [frame, setFrame] = createSignal(0);
|
|
470
931
|
const [now, setNow] = createSignal(Date.now());
|
|
471
932
|
const records = createSpawnRecordReader();
|
|
472
933
|
let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
|
|
934
|
+
const history = createTurnHistoryReader();
|
|
935
|
+
let historyMap = attempt(() => history.read(), /* @__PURE__ */ new Map());
|
|
473
936
|
const [recordVersion, setRecordVersion] = createSignal(0);
|
|
937
|
+
const watch = createStatusWatch();
|
|
938
|
+
const fills = createFillMemory();
|
|
474
939
|
const usageReader = createAccountUsageReader();
|
|
475
940
|
const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
|
|
941
|
+
const [known, setKnown] = createSignal(attempt(() => usageReader.known(), /* @__PURE__ */ new Map()));
|
|
476
942
|
const [usageNow, setUsageNow] = createSignal(Date.now());
|
|
477
943
|
const sectionsRunning = /* @__PURE__ */ new Set();
|
|
478
944
|
const sectionsShowing = /* @__PURE__ */ new Set();
|
|
479
945
|
const sectionsMounted = /* @__PURE__ */ new Set();
|
|
480
946
|
let spinner;
|
|
481
947
|
let refresh;
|
|
948
|
+
let refreshPeriod = 0;
|
|
949
|
+
const refreshTick = () => {
|
|
950
|
+
if (sectionsShowing.size > 0) {
|
|
951
|
+
const next = attempt(() => records.read(), recordMap);
|
|
952
|
+
const nextHistory = attempt(() => history.read(), historyMap);
|
|
953
|
+
if (next !== recordMap || nextHistory !== historyMap) {
|
|
954
|
+
recordMap = next;
|
|
955
|
+
historyMap = nextHistory;
|
|
956
|
+
setRecordVersion((value) => value + 1);
|
|
957
|
+
}
|
|
958
|
+
setNow(Date.now());
|
|
959
|
+
}
|
|
960
|
+
setUsage(attempt(() => usageReader.read(), usage()));
|
|
961
|
+
setKnown(attempt(() => usageReader.known(), known()));
|
|
962
|
+
const minute = Math.floor(Date.now() / 6e4) * 6e4;
|
|
963
|
+
if (minute !== usageNow()) setUsageNow(minute);
|
|
964
|
+
};
|
|
482
965
|
const syncClocks = () => {
|
|
483
966
|
if (sectionsRunning.size > 0 && !spinner) {
|
|
484
|
-
spinner = setInterval(() => setFrame((value) => (value + 1) %
|
|
967
|
+
spinner = setInterval(() => setFrame((value) => (value + 1) % FRAME_WRAP), SPINNER_INTERVAL_MS);
|
|
485
968
|
} else if (sectionsRunning.size === 0 && spinner) {
|
|
486
969
|
clearInterval(spinner);
|
|
487
970
|
spinner = void 0;
|
|
488
971
|
}
|
|
489
972
|
const wanted = sectionsShowing.size > 0 || sectionsMounted.size > 0;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
recordMap = next;
|
|
496
|
-
setRecordVersion((value) => value + 1);
|
|
497
|
-
}
|
|
498
|
-
setNow(Date.now());
|
|
499
|
-
}
|
|
500
|
-
setUsage(attempt(() => usageReader.read(), usage()));
|
|
501
|
-
const minute = Math.floor(Date.now() / 6e4) * 6e4;
|
|
502
|
-
if (minute !== usageNow()) setUsageNow(minute);
|
|
503
|
-
}, REFRESH_INTERVAL_MS);
|
|
973
|
+
const period = sectionsRunning.size > 0 ? PROGRESS_TICK_MS : REFRESH_INTERVAL_MS;
|
|
974
|
+
if (wanted && (!refresh || refreshPeriod !== period)) {
|
|
975
|
+
if (refresh) clearInterval(refresh);
|
|
976
|
+
refreshPeriod = period;
|
|
977
|
+
refresh = setInterval(refreshTick, period);
|
|
504
978
|
} else if (!wanted && refresh) {
|
|
505
979
|
clearInterval(refresh);
|
|
506
980
|
refresh = void 0;
|
|
981
|
+
refreshPeriod = 0;
|
|
507
982
|
}
|
|
508
983
|
};
|
|
509
|
-
const
|
|
984
|
+
const progressOf = (sessionID) => attempt(() => host.progress(sessionID), EMPTY_SOURCE);
|
|
985
|
+
const estimateFor = (sessionID, measure, view, agentFallback) => {
|
|
986
|
+
if (view.state !== "running") return view;
|
|
987
|
+
const record = recordMap.get(sessionID);
|
|
988
|
+
if (!record?.model) return view;
|
|
989
|
+
const samples = historyMap.get(turnHistoryKey(record.agent ?? agentFallback, record.model));
|
|
990
|
+
if (!samples) return view;
|
|
991
|
+
const estimate = estimateTurn(samples, { elapsedMs: view.elapsedMs, chars: measure.chars, effort: record.effort });
|
|
992
|
+
if (!estimate) return view;
|
|
993
|
+
const turn = measure.startedAt ?? 0;
|
|
994
|
+
return { ...view, estimate: { ...estimate, fill: fills.hold(sessionID, turn, estimate.fill) } };
|
|
995
|
+
};
|
|
996
|
+
const deriveView = (sessionID) => {
|
|
510
997
|
recordVersion();
|
|
511
|
-
|
|
512
|
-
|
|
998
|
+
now();
|
|
999
|
+
const clock = Date.now();
|
|
1000
|
+
const gathered = host.gather(sessionID);
|
|
1001
|
+
const rows = deriveSubagentRows({
|
|
1002
|
+
...gathered,
|
|
513
1003
|
record: (id) => recordMap.get(id),
|
|
514
|
-
now:
|
|
1004
|
+
now: clock
|
|
1005
|
+
});
|
|
1006
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const row of rows) {
|
|
1008
|
+
if (row.state !== "running" || !row.sessionID) continue;
|
|
1009
|
+
const measure2 = measureProgress(progressOf(row.sessionID));
|
|
1010
|
+
const line = rowProgress(measure2, { started: row.started, now: clock });
|
|
1011
|
+
lines.set(row.sessionID, estimateFor(row.sessionID, measure2, line, row.agent));
|
|
1012
|
+
}
|
|
1013
|
+
const status = gathered.status(sessionID);
|
|
1014
|
+
const memory = watch.observe(sessionID, status, clock);
|
|
1015
|
+
const measure = measureProgress(progressOf(sessionID));
|
|
1016
|
+
const session = sessionProgress(measure, {
|
|
1017
|
+
status,
|
|
1018
|
+
busySince: memory.busySince,
|
|
1019
|
+
stoppedAt: gathered.stoppedAt(sessionID) ?? memory.stoppedAt,
|
|
1020
|
+
now: clock
|
|
515
1021
|
});
|
|
1022
|
+
const progress = session ? estimateFor(sessionID, measure, session) : void 0;
|
|
1023
|
+
return { rows, lines, progress };
|
|
1024
|
+
};
|
|
1025
|
+
const styled = (segment) => {
|
|
1026
|
+
const span = createElement(segment.role === "fraction" ? "b" : "span");
|
|
1027
|
+
insertNode(span, createTextNode(segment.text));
|
|
1028
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1029
|
+
return span;
|
|
516
1030
|
};
|
|
517
|
-
const
|
|
1031
|
+
const buildProgressLine = (view, lead) => {
|
|
1032
|
+
const line = createElement("text");
|
|
1033
|
+
setProp(line, "wrapMode", "none");
|
|
1034
|
+
const layout = layoutProgressLine(view, lead, SIDEBAR_ROW_WIDTH);
|
|
1035
|
+
layout.lead.forEach((segment, index) => {
|
|
1036
|
+
if (index === 0 && lead === "glyph" && view.state === "running") {
|
|
1037
|
+
const span = createElement("span");
|
|
1038
|
+
insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length]);
|
|
1039
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1040
|
+
insertNode(line, span);
|
|
1041
|
+
} else {
|
|
1042
|
+
insertNode(line, styled(segment));
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
for (const segment of layout.bar) insertNode(line, styled(segment));
|
|
1046
|
+
for (const segment of layout.tail) insertNode(line, styled(segment));
|
|
1047
|
+
return line;
|
|
1048
|
+
};
|
|
1049
|
+
const buildRow = (row, progress) => {
|
|
518
1050
|
const box = createElement("box");
|
|
519
1051
|
const layout = layoutRow(row, SIDEBAR_ROW_WIDTH);
|
|
520
1052
|
const first = createElement("text");
|
|
@@ -533,14 +1065,10 @@ function createSidebar(host) {
|
|
|
533
1065
|
if (layout.second.length > 0) {
|
|
534
1066
|
const second = createElement("text");
|
|
535
1067
|
setProp(second, "wrapMode", "none");
|
|
536
|
-
for (const segment of layout.second)
|
|
537
|
-
const span = createElement("span");
|
|
538
|
-
insertNode(span, createTextNode(segment.text));
|
|
539
|
-
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
540
|
-
insertNode(second, span);
|
|
541
|
-
}
|
|
1068
|
+
for (const segment of layout.second) insertNode(second, styled(segment));
|
|
542
1069
|
insertNode(box, second);
|
|
543
1070
|
}
|
|
1071
|
+
if (progress) insertNode(box, attempt(() => buildProgressLine(progress, "indent"), createElement("box")));
|
|
544
1072
|
if (row.sessionID) {
|
|
545
1073
|
const sessionID = row.sessionID;
|
|
546
1074
|
setProp(box, "onMouseDown", () => attempt(() => host.navigate(sessionID), void 0));
|
|
@@ -557,7 +1085,16 @@ function createSidebar(host) {
|
|
|
557
1085
|
}
|
|
558
1086
|
return box;
|
|
559
1087
|
};
|
|
560
|
-
const
|
|
1088
|
+
const buildHeading = (label) => {
|
|
1089
|
+
const title = createElement("text");
|
|
1090
|
+
const bold = createElement("b");
|
|
1091
|
+
insertNode(bold, createTextNode(label));
|
|
1092
|
+
insertNode(title, bold);
|
|
1093
|
+
effect(() => setProp(title, "fg", host.colors().text));
|
|
1094
|
+
return title;
|
|
1095
|
+
};
|
|
1096
|
+
const buildSection = (view, expanded, setExpanded) => {
|
|
1097
|
+
const list = view.rows;
|
|
561
1098
|
const section = createElement("box");
|
|
562
1099
|
const header = createElement("box");
|
|
563
1100
|
setProp(header, "flexDirection", "row");
|
|
@@ -569,62 +1106,65 @@ function createSidebar(host) {
|
|
|
569
1106
|
effect(() => setProp(caret, "fg", host.colors().text));
|
|
570
1107
|
insertNode(header, caret);
|
|
571
1108
|
}
|
|
572
|
-
const title =
|
|
573
|
-
const bold = createElement("b");
|
|
574
|
-
insertNode(bold, createTextNode("Subagents"));
|
|
575
|
-
insertNode(title, bold);
|
|
1109
|
+
const title = buildHeading("Subagents");
|
|
576
1110
|
const summary = createElement("span");
|
|
577
1111
|
insert(summary, () => expanded() ? "" : ` (${collapsedSummary(list)})`);
|
|
578
1112
|
effect(() => setProp(summary, "style", { fg: host.colors().muted }));
|
|
579
1113
|
insertNode(title, summary);
|
|
580
|
-
effect(() => setProp(title, "fg", host.colors().text));
|
|
581
1114
|
insertNode(header, title);
|
|
582
1115
|
insertNode(section, header);
|
|
583
1116
|
const body = createElement("box");
|
|
584
|
-
insert(
|
|
1117
|
+
insert(
|
|
1118
|
+
body,
|
|
1119
|
+
() => expanded() || list.length <= 2 ? list.map((row) => buildRow(row, row.sessionID ? view.lines.get(row.sessionID) : void 0)) : null
|
|
1120
|
+
);
|
|
1121
|
+
insertNode(section, body);
|
|
1122
|
+
return section;
|
|
1123
|
+
};
|
|
1124
|
+
const buildProgress = (progress) => {
|
|
1125
|
+
const section = createElement("box");
|
|
1126
|
+
insertNode(section, buildHeading("Main Progress"));
|
|
1127
|
+
const body = createElement("box");
|
|
1128
|
+
insert(body, () => {
|
|
1129
|
+
const view = progress();
|
|
1130
|
+
return view ? attempt(() => buildProgressLine(view, "glyph"), null) : null;
|
|
1131
|
+
});
|
|
585
1132
|
insertNode(section, body);
|
|
586
1133
|
return section;
|
|
587
1134
|
};
|
|
588
1135
|
const buildAccounts = (lines) => {
|
|
589
1136
|
const section = createElement("box");
|
|
590
|
-
|
|
591
|
-
const bold = createElement("b");
|
|
592
|
-
insertNode(bold, createTextNode("Accounts"));
|
|
593
|
-
insertNode(title, bold);
|
|
594
|
-
effect(() => setProp(title, "fg", host.colors().text));
|
|
595
|
-
insertNode(section, title);
|
|
1137
|
+
insertNode(section, buildHeading("Accounts"));
|
|
596
1138
|
for (const line of lines) {
|
|
597
|
-
const
|
|
598
|
-
setProp(
|
|
599
|
-
for (const segment of line.segments)
|
|
600
|
-
|
|
601
|
-
insertNode(span, createTextNode(segment.text));
|
|
602
|
-
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
603
|
-
insertNode(text2, span);
|
|
604
|
-
}
|
|
605
|
-
insertNode(section, text2);
|
|
1139
|
+
const text3 = createElement("text");
|
|
1140
|
+
setProp(text3, "wrapMode", "none");
|
|
1141
|
+
for (const segment of line.segments) insertNode(text3, styled(segment));
|
|
1142
|
+
insertNode(section, text3);
|
|
606
1143
|
}
|
|
607
1144
|
return section;
|
|
608
1145
|
};
|
|
609
1146
|
const render = (sessionID) => {
|
|
610
1147
|
const root = createElement("box");
|
|
1148
|
+
const progressBox = createElement("box");
|
|
611
1149
|
const subagentsBox = createElement("box");
|
|
612
1150
|
const accountsBox = createElement("box");
|
|
1151
|
+
insertNode(root, progressBox);
|
|
613
1152
|
insertNode(root, subagentsBox);
|
|
614
1153
|
insertNode(root, accountsBox);
|
|
615
1154
|
const key = /* @__PURE__ */ Symbol("subagents-section");
|
|
616
1155
|
sectionsMounted.add(key);
|
|
617
1156
|
const [expanded, setExpanded] = createSignal(true);
|
|
618
|
-
const
|
|
1157
|
+
const view = createMemo(() => {
|
|
619
1158
|
const parentID = sessionID();
|
|
620
|
-
if (!parentID) return
|
|
621
|
-
return attempt(() =>
|
|
1159
|
+
if (!parentID) return EMPTY_VIEW;
|
|
1160
|
+
return attempt(() => deriveView(parentID), EMPTY_VIEW);
|
|
622
1161
|
});
|
|
623
1162
|
effect(() => {
|
|
624
|
-
const
|
|
625
|
-
|
|
1163
|
+
const { rows, progress } = view();
|
|
1164
|
+
const running = rows.some((row) => row.state === "running") || progress?.state === "running";
|
|
1165
|
+
if (running) sectionsRunning.add(key);
|
|
626
1166
|
else sectionsRunning.delete(key);
|
|
627
|
-
if (
|
|
1167
|
+
if (rows.length > 0 || progress) sectionsShowing.add(key);
|
|
628
1168
|
else sectionsShowing.delete(key);
|
|
629
1169
|
syncClocks();
|
|
630
1170
|
});
|
|
@@ -634,13 +1174,18 @@ function createSidebar(host) {
|
|
|
634
1174
|
sectionsMounted.delete(key);
|
|
635
1175
|
syncClocks();
|
|
636
1176
|
});
|
|
1177
|
+
const hasProgress = createMemo(() => view().progress !== void 0);
|
|
1178
|
+
insert(
|
|
1179
|
+
progressBox,
|
|
1180
|
+
() => hasProgress() ? attempt(() => buildProgress(() => view().progress), null) : null
|
|
1181
|
+
);
|
|
637
1182
|
insert(subagentsBox, () => {
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
640
|
-
return attempt(() => buildSection(
|
|
1183
|
+
const current = view();
|
|
1184
|
+
if (current.rows.length === 0) return null;
|
|
1185
|
+
return attempt(() => buildSection(current, expanded, setExpanded), null);
|
|
641
1186
|
});
|
|
642
1187
|
const accountLines = createMemo(
|
|
643
|
-
() => attempt(() => accountUsageLines(usage(), usageNow()), [])
|
|
1188
|
+
() => attempt(() => accountUsageLines(usage(), usageNow(), SIDEBAR_ROW_WIDTH, known()), [])
|
|
644
1189
|
);
|
|
645
1190
|
insert(accountsBox, () => {
|
|
646
1191
|
const lines = accountLines();
|
|
@@ -703,6 +1248,7 @@ function registerV1(api) {
|
|
|
703
1248
|
lastStatus.set(id, type);
|
|
704
1249
|
if (type === "idle" && previous && previous !== "idle") stoppedAt.set(id, Date.now());
|
|
705
1250
|
if (type !== "idle") stoppedAt.delete(id);
|
|
1251
|
+
bump();
|
|
706
1252
|
});
|
|
707
1253
|
const fetchChildren = (parentID) => {
|
|
708
1254
|
if (fetched.has(parentID)) return;
|
|
@@ -718,15 +1264,24 @@ function registerV1(api) {
|
|
|
718
1264
|
const host = {
|
|
719
1265
|
colors() {
|
|
720
1266
|
const theme = api.theme.current;
|
|
1267
|
+
const muted = theme.textMuted;
|
|
721
1268
|
return {
|
|
722
1269
|
text: theme.text,
|
|
723
|
-
muted
|
|
1270
|
+
muted,
|
|
724
1271
|
spinner: theme.accent,
|
|
725
|
-
done: mix(theme.success,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1272
|
+
done: mix(theme.success, muted, 0.45),
|
|
1273
|
+
ok: theme.success ?? theme.accent,
|
|
1274
|
+
error: mix(theme.error, muted, 0.35),
|
|
1275
|
+
effort: theme.warning ?? muted,
|
|
1276
|
+
effortFaded: mix(theme.warning ?? muted, muted, 0.45),
|
|
1277
|
+
hover: theme.backgroundElement,
|
|
1278
|
+
barDone: theme.success ?? theme.accent,
|
|
1279
|
+
barTrack: mix(muted, theme.backgroundPanel ?? theme.backgroundElement, 0.5),
|
|
1280
|
+
fraction: theme.text,
|
|
1281
|
+
tool: theme.info ?? theme.accent,
|
|
1282
|
+
cost: theme.secondary ?? theme.primary ?? muted,
|
|
1283
|
+
tier: theme.secondary ?? theme.primary ?? muted,
|
|
1284
|
+
reset: theme.info ?? theme.accent
|
|
730
1285
|
};
|
|
731
1286
|
},
|
|
732
1287
|
gather(parentID) {
|
|
@@ -779,9 +1334,31 @@ function registerV1(api) {
|
|
|
779
1334
|
if (type === "idle") return "idle";
|
|
780
1335
|
return "unknown";
|
|
781
1336
|
},
|
|
782
|
-
stoppedAt: (id) => stoppedAt.get(id)
|
|
1337
|
+
stoppedAt: (id) => stoppedAt.get(id),
|
|
1338
|
+
cost: (id) => attempt(() => api.state.session.get(id)?.cost, void 0)
|
|
783
1339
|
};
|
|
784
1340
|
},
|
|
1341
|
+
progress(sessionID) {
|
|
1342
|
+
const messages = [];
|
|
1343
|
+
const turn = turnMessages(api.state.session.messages(sessionID) ?? [], (message) => message.role === "user");
|
|
1344
|
+
for (const message of turn) {
|
|
1345
|
+
const tools = [];
|
|
1346
|
+
const texts = [];
|
|
1347
|
+
for (const part of api.state.part(message.id) ?? []) {
|
|
1348
|
+
if (message.role === "assistant" && part.type === "tool") tools.push({ name: part.tool, status: part.state?.status });
|
|
1349
|
+
if (message.role === "user" && part.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
1350
|
+
}
|
|
1351
|
+
messages.push({
|
|
1352
|
+
role: message.role,
|
|
1353
|
+
created: message.time?.created,
|
|
1354
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1355
|
+
tools,
|
|
1356
|
+
chars: message.role === "user" ? promptChars(texts) : void 0
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
const todos = attempt(() => api.state.session.todo?.(sessionID), void 0) ?? [];
|
|
1360
|
+
return { messages, todos: todos.map((todo) => ({ status: todo.status })) };
|
|
1361
|
+
},
|
|
785
1362
|
navigate(sessionID) {
|
|
786
1363
|
api.route.navigate("session", { sessionID });
|
|
787
1364
|
}
|
|
@@ -805,16 +1382,25 @@ function setupV2(context) {
|
|
|
805
1382
|
colors() {
|
|
806
1383
|
const theme = context.theme;
|
|
807
1384
|
const muted = theme.text.muted;
|
|
808
|
-
const
|
|
1385
|
+
const shade = context.themeMode === "light" ? "500" : "200";
|
|
1386
|
+
const accent = theme.hue.accent[shade] ?? theme.text.base;
|
|
809
1387
|
return {
|
|
810
1388
|
text: theme.text.base,
|
|
811
1389
|
muted,
|
|
812
1390
|
spinner: accent,
|
|
813
1391
|
done: mix(theme.text.feedback.success.muted ?? theme.text.feedback.success.base, muted, 0.45),
|
|
1392
|
+
ok: theme.text.feedback.success.base ?? accent,
|
|
814
1393
|
error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),
|
|
815
1394
|
effort: theme.text.feedback.warning?.base ?? muted,
|
|
816
1395
|
effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),
|
|
817
|
-
hover: theme.background.raised.base
|
|
1396
|
+
hover: theme.background.raised.base,
|
|
1397
|
+
barDone: theme.text.feedback.success.base ?? accent,
|
|
1398
|
+
barTrack: mix(muted, theme.background.base ?? theme.background.raised.base, 0.5),
|
|
1399
|
+
fraction: theme.text.base,
|
|
1400
|
+
tool: theme.text.feedback.info?.base ?? accent,
|
|
1401
|
+
cost: theme.hue.interactive?.[shade] ?? accent,
|
|
1402
|
+
tier: theme.hue.interactive?.[shade] ?? accent,
|
|
1403
|
+
reset: theme.text.feedback.info?.base ?? accent
|
|
818
1404
|
};
|
|
819
1405
|
},
|
|
820
1406
|
gather(parentID) {
|
|
@@ -853,9 +1439,37 @@ function setupV2(context) {
|
|
|
853
1439
|
if (status === "idle") return "idle";
|
|
854
1440
|
return "unknown";
|
|
855
1441
|
},
|
|
856
|
-
stoppedAt: () => void 0
|
|
1442
|
+
stoppedAt: () => void 0,
|
|
1443
|
+
cost: (id) => attempt(() => context.data.session.cost?.(id), void 0)
|
|
857
1444
|
};
|
|
858
1445
|
},
|
|
1446
|
+
progress(sessionID) {
|
|
1447
|
+
const messages = [];
|
|
1448
|
+
const turn = turnMessages(
|
|
1449
|
+
context.data.session.message.list(sessionID) ?? [],
|
|
1450
|
+
(message) => message.type === "user" || message.type === "synthetic"
|
|
1451
|
+
);
|
|
1452
|
+
for (const message of turn) {
|
|
1453
|
+
const tools = [];
|
|
1454
|
+
const texts = [];
|
|
1455
|
+
for (const item of message.content ?? []) {
|
|
1456
|
+
if (message.type === "assistant" && item.type === "tool") tools.push({ name: item.name, status: item.state?.status });
|
|
1457
|
+
if (message.type === "user" && item.type === "text" && typeof item.text === "string") texts.push(item.text);
|
|
1458
|
+
}
|
|
1459
|
+
if (message.type === "synthetic" && message.text) texts.push(message.text);
|
|
1460
|
+
const starts = message.type === "user" || message.type === "synthetic";
|
|
1461
|
+
messages.push({
|
|
1462
|
+
// A synthetic message (a background child's end) prompts the parent
|
|
1463
|
+
// like a user message does: it starts a turn.
|
|
1464
|
+
role: message.type === "synthetic" ? "user" : message.type ?? "",
|
|
1465
|
+
created: message.time?.created,
|
|
1466
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1467
|
+
tools,
|
|
1468
|
+
chars: starts ? promptChars(texts) : void 0
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return { messages, todos: [] };
|
|
1472
|
+
},
|
|
859
1473
|
navigate(sessionID) {
|
|
860
1474
|
context.ui.router.navigate({ type: "session", sessionID });
|
|
861
1475
|
}
|