@khalilgharbaoui/opencode-claude-code-plugin 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +252 -49
- package/dist/index.js.map +1 -1
- package/dist/tui.js +751 -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" : "muted";
|
|
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,30 @@ 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
|
+
error: "error",
|
|
888
|
+
agent: "text",
|
|
889
|
+
description: "text",
|
|
890
|
+
muted: "muted",
|
|
891
|
+
effort: "effort",
|
|
892
|
+
"effort-faded": "effortFaded",
|
|
893
|
+
"bar-done": "barDone",
|
|
894
|
+
// Past every recorded turn: the warning colour, which the effort already takes.
|
|
895
|
+
"bar-over": "effort",
|
|
896
|
+
"bar-track": "barTrack",
|
|
897
|
+
fraction: "fraction",
|
|
898
|
+
estimate: "fraction",
|
|
899
|
+
"estimate-over": "effort",
|
|
900
|
+
tool: "tool",
|
|
901
|
+
cost: "cost",
|
|
902
|
+
tier: "tier",
|
|
903
|
+
reset: "reset"
|
|
904
|
+
};
|
|
431
905
|
function attempt(fn, fallback) {
|
|
432
906
|
try {
|
|
433
907
|
return fn();
|
|
@@ -447,74 +921,131 @@ function mix(a, b, weight) {
|
|
|
447
921
|
);
|
|
448
922
|
}
|
|
449
923
|
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
|
-
}
|
|
924
|
+
return colors[ROLE_COLOR[role] ?? "muted"];
|
|
467
925
|
}
|
|
926
|
+
var EMPTY_SOURCE = { messages: [], todos: [] };
|
|
927
|
+
var EMPTY_VIEW = { rows: [], lines: /* @__PURE__ */ new Map() };
|
|
468
928
|
function createSidebar(host) {
|
|
469
929
|
const [frame, setFrame] = createSignal(0);
|
|
470
930
|
const [now, setNow] = createSignal(Date.now());
|
|
471
931
|
const records = createSpawnRecordReader();
|
|
472
932
|
let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
|
|
933
|
+
const history = createTurnHistoryReader();
|
|
934
|
+
let historyMap = attempt(() => history.read(), /* @__PURE__ */ new Map());
|
|
473
935
|
const [recordVersion, setRecordVersion] = createSignal(0);
|
|
936
|
+
const watch = createStatusWatch();
|
|
937
|
+
const fills = createFillMemory();
|
|
474
938
|
const usageReader = createAccountUsageReader();
|
|
475
939
|
const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
|
|
940
|
+
const [known, setKnown] = createSignal(attempt(() => usageReader.known(), /* @__PURE__ */ new Map()));
|
|
476
941
|
const [usageNow, setUsageNow] = createSignal(Date.now());
|
|
477
942
|
const sectionsRunning = /* @__PURE__ */ new Set();
|
|
478
943
|
const sectionsShowing = /* @__PURE__ */ new Set();
|
|
479
944
|
const sectionsMounted = /* @__PURE__ */ new Set();
|
|
480
945
|
let spinner;
|
|
481
946
|
let refresh;
|
|
947
|
+
let refreshPeriod = 0;
|
|
948
|
+
const refreshTick = () => {
|
|
949
|
+
if (sectionsShowing.size > 0) {
|
|
950
|
+
const next = attempt(() => records.read(), recordMap);
|
|
951
|
+
const nextHistory = attempt(() => history.read(), historyMap);
|
|
952
|
+
if (next !== recordMap || nextHistory !== historyMap) {
|
|
953
|
+
recordMap = next;
|
|
954
|
+
historyMap = nextHistory;
|
|
955
|
+
setRecordVersion((value) => value + 1);
|
|
956
|
+
}
|
|
957
|
+
setNow(Date.now());
|
|
958
|
+
}
|
|
959
|
+
setUsage(attempt(() => usageReader.read(), usage()));
|
|
960
|
+
setKnown(attempt(() => usageReader.known(), known()));
|
|
961
|
+
const minute = Math.floor(Date.now() / 6e4) * 6e4;
|
|
962
|
+
if (minute !== usageNow()) setUsageNow(minute);
|
|
963
|
+
};
|
|
482
964
|
const syncClocks = () => {
|
|
483
965
|
if (sectionsRunning.size > 0 && !spinner) {
|
|
484
|
-
spinner = setInterval(() => setFrame((value) => (value + 1) %
|
|
966
|
+
spinner = setInterval(() => setFrame((value) => (value + 1) % FRAME_WRAP), SPINNER_INTERVAL_MS);
|
|
485
967
|
} else if (sectionsRunning.size === 0 && spinner) {
|
|
486
968
|
clearInterval(spinner);
|
|
487
969
|
spinner = void 0;
|
|
488
970
|
}
|
|
489
971
|
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);
|
|
972
|
+
const period = sectionsRunning.size > 0 ? PROGRESS_TICK_MS : REFRESH_INTERVAL_MS;
|
|
973
|
+
if (wanted && (!refresh || refreshPeriod !== period)) {
|
|
974
|
+
if (refresh) clearInterval(refresh);
|
|
975
|
+
refreshPeriod = period;
|
|
976
|
+
refresh = setInterval(refreshTick, period);
|
|
504
977
|
} else if (!wanted && refresh) {
|
|
505
978
|
clearInterval(refresh);
|
|
506
979
|
refresh = void 0;
|
|
980
|
+
refreshPeriod = 0;
|
|
507
981
|
}
|
|
508
982
|
};
|
|
509
|
-
const
|
|
983
|
+
const progressOf = (sessionID) => attempt(() => host.progress(sessionID), EMPTY_SOURCE);
|
|
984
|
+
const estimateFor = (sessionID, measure, view, agentFallback) => {
|
|
985
|
+
if (view.state !== "running") return view;
|
|
986
|
+
const record = recordMap.get(sessionID);
|
|
987
|
+
if (!record?.model) return view;
|
|
988
|
+
const samples = historyMap.get(turnHistoryKey(record.agent ?? agentFallback, record.model));
|
|
989
|
+
if (!samples) return view;
|
|
990
|
+
const estimate = estimateTurn(samples, { elapsedMs: view.elapsedMs, chars: measure.chars, effort: record.effort });
|
|
991
|
+
if (!estimate) return view;
|
|
992
|
+
const turn = measure.startedAt ?? 0;
|
|
993
|
+
return { ...view, estimate: { ...estimate, fill: fills.hold(sessionID, turn, estimate.fill) } };
|
|
994
|
+
};
|
|
995
|
+
const deriveView = (sessionID) => {
|
|
510
996
|
recordVersion();
|
|
511
|
-
|
|
512
|
-
|
|
997
|
+
now();
|
|
998
|
+
const clock = Date.now();
|
|
999
|
+
const gathered = host.gather(sessionID);
|
|
1000
|
+
const rows = deriveSubagentRows({
|
|
1001
|
+
...gathered,
|
|
513
1002
|
record: (id) => recordMap.get(id),
|
|
514
|
-
now:
|
|
1003
|
+
now: clock
|
|
1004
|
+
});
|
|
1005
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (const row of rows) {
|
|
1007
|
+
if (row.state !== "running" || !row.sessionID) continue;
|
|
1008
|
+
const measure2 = measureProgress(progressOf(row.sessionID));
|
|
1009
|
+
const line = rowProgress(measure2, { started: row.started, now: clock });
|
|
1010
|
+
lines.set(row.sessionID, estimateFor(row.sessionID, measure2, line, row.agent));
|
|
1011
|
+
}
|
|
1012
|
+
const status = gathered.status(sessionID);
|
|
1013
|
+
const memory = watch.observe(sessionID, status, clock);
|
|
1014
|
+
const measure = measureProgress(progressOf(sessionID));
|
|
1015
|
+
const session = sessionProgress(measure, {
|
|
1016
|
+
status,
|
|
1017
|
+
busySince: memory.busySince,
|
|
1018
|
+
stoppedAt: gathered.stoppedAt(sessionID) ?? memory.stoppedAt,
|
|
1019
|
+
now: clock
|
|
515
1020
|
});
|
|
1021
|
+
const progress = session ? estimateFor(sessionID, measure, session) : void 0;
|
|
1022
|
+
return { rows, lines, progress };
|
|
1023
|
+
};
|
|
1024
|
+
const styled = (segment) => {
|
|
1025
|
+
const span = createElement(segment.role === "fraction" ? "b" : "span");
|
|
1026
|
+
insertNode(span, createTextNode(segment.text));
|
|
1027
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1028
|
+
return span;
|
|
516
1029
|
};
|
|
517
|
-
const
|
|
1030
|
+
const buildProgressLine = (view, lead) => {
|
|
1031
|
+
const line = createElement("text");
|
|
1032
|
+
setProp(line, "wrapMode", "none");
|
|
1033
|
+
const layout = layoutProgressLine(view, lead, SIDEBAR_ROW_WIDTH);
|
|
1034
|
+
layout.lead.forEach((segment, index) => {
|
|
1035
|
+
if (index === 0 && lead === "glyph" && view.state === "running") {
|
|
1036
|
+
const span = createElement("span");
|
|
1037
|
+
insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length]);
|
|
1038
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1039
|
+
insertNode(line, span);
|
|
1040
|
+
} else {
|
|
1041
|
+
insertNode(line, styled(segment));
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
for (const segment of layout.bar) insertNode(line, styled(segment));
|
|
1045
|
+
for (const segment of layout.tail) insertNode(line, styled(segment));
|
|
1046
|
+
return line;
|
|
1047
|
+
};
|
|
1048
|
+
const buildRow = (row, progress) => {
|
|
518
1049
|
const box = createElement("box");
|
|
519
1050
|
const layout = layoutRow(row, SIDEBAR_ROW_WIDTH);
|
|
520
1051
|
const first = createElement("text");
|
|
@@ -533,14 +1064,10 @@ function createSidebar(host) {
|
|
|
533
1064
|
if (layout.second.length > 0) {
|
|
534
1065
|
const second = createElement("text");
|
|
535
1066
|
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
|
-
}
|
|
1067
|
+
for (const segment of layout.second) insertNode(second, styled(segment));
|
|
542
1068
|
insertNode(box, second);
|
|
543
1069
|
}
|
|
1070
|
+
if (progress) insertNode(box, attempt(() => buildProgressLine(progress, "indent"), createElement("box")));
|
|
544
1071
|
if (row.sessionID) {
|
|
545
1072
|
const sessionID = row.sessionID;
|
|
546
1073
|
setProp(box, "onMouseDown", () => attempt(() => host.navigate(sessionID), void 0));
|
|
@@ -557,7 +1084,16 @@ function createSidebar(host) {
|
|
|
557
1084
|
}
|
|
558
1085
|
return box;
|
|
559
1086
|
};
|
|
560
|
-
const
|
|
1087
|
+
const buildHeading = (label) => {
|
|
1088
|
+
const title = createElement("text");
|
|
1089
|
+
const bold = createElement("b");
|
|
1090
|
+
insertNode(bold, createTextNode(label));
|
|
1091
|
+
insertNode(title, bold);
|
|
1092
|
+
effect(() => setProp(title, "fg", host.colors().text));
|
|
1093
|
+
return title;
|
|
1094
|
+
};
|
|
1095
|
+
const buildSection = (view, expanded, setExpanded) => {
|
|
1096
|
+
const list = view.rows;
|
|
561
1097
|
const section = createElement("box");
|
|
562
1098
|
const header = createElement("box");
|
|
563
1099
|
setProp(header, "flexDirection", "row");
|
|
@@ -569,62 +1105,65 @@ function createSidebar(host) {
|
|
|
569
1105
|
effect(() => setProp(caret, "fg", host.colors().text));
|
|
570
1106
|
insertNode(header, caret);
|
|
571
1107
|
}
|
|
572
|
-
const title =
|
|
573
|
-
const bold = createElement("b");
|
|
574
|
-
insertNode(bold, createTextNode("Subagents"));
|
|
575
|
-
insertNode(title, bold);
|
|
1108
|
+
const title = buildHeading("Subagents");
|
|
576
1109
|
const summary = createElement("span");
|
|
577
1110
|
insert(summary, () => expanded() ? "" : ` (${collapsedSummary(list)})`);
|
|
578
1111
|
effect(() => setProp(summary, "style", { fg: host.colors().muted }));
|
|
579
1112
|
insertNode(title, summary);
|
|
580
|
-
effect(() => setProp(title, "fg", host.colors().text));
|
|
581
1113
|
insertNode(header, title);
|
|
582
1114
|
insertNode(section, header);
|
|
583
1115
|
const body = createElement("box");
|
|
584
|
-
insert(
|
|
1116
|
+
insert(
|
|
1117
|
+
body,
|
|
1118
|
+
() => expanded() || list.length <= 2 ? list.map((row) => buildRow(row, row.sessionID ? view.lines.get(row.sessionID) : void 0)) : null
|
|
1119
|
+
);
|
|
1120
|
+
insertNode(section, body);
|
|
1121
|
+
return section;
|
|
1122
|
+
};
|
|
1123
|
+
const buildProgress = (progress) => {
|
|
1124
|
+
const section = createElement("box");
|
|
1125
|
+
insertNode(section, buildHeading("Main Progress"));
|
|
1126
|
+
const body = createElement("box");
|
|
1127
|
+
insert(body, () => {
|
|
1128
|
+
const view = progress();
|
|
1129
|
+
return view ? attempt(() => buildProgressLine(view, "glyph"), null) : null;
|
|
1130
|
+
});
|
|
585
1131
|
insertNode(section, body);
|
|
586
1132
|
return section;
|
|
587
1133
|
};
|
|
588
1134
|
const buildAccounts = (lines) => {
|
|
589
1135
|
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);
|
|
1136
|
+
insertNode(section, buildHeading("Accounts"));
|
|
596
1137
|
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);
|
|
1138
|
+
const text3 = createElement("text");
|
|
1139
|
+
setProp(text3, "wrapMode", "none");
|
|
1140
|
+
for (const segment of line.segments) insertNode(text3, styled(segment));
|
|
1141
|
+
insertNode(section, text3);
|
|
606
1142
|
}
|
|
607
1143
|
return section;
|
|
608
1144
|
};
|
|
609
1145
|
const render = (sessionID) => {
|
|
610
1146
|
const root = createElement("box");
|
|
1147
|
+
const progressBox = createElement("box");
|
|
611
1148
|
const subagentsBox = createElement("box");
|
|
612
1149
|
const accountsBox = createElement("box");
|
|
1150
|
+
insertNode(root, progressBox);
|
|
613
1151
|
insertNode(root, subagentsBox);
|
|
614
1152
|
insertNode(root, accountsBox);
|
|
615
1153
|
const key = /* @__PURE__ */ Symbol("subagents-section");
|
|
616
1154
|
sectionsMounted.add(key);
|
|
617
1155
|
const [expanded, setExpanded] = createSignal(true);
|
|
618
|
-
const
|
|
1156
|
+
const view = createMemo(() => {
|
|
619
1157
|
const parentID = sessionID();
|
|
620
|
-
if (!parentID) return
|
|
621
|
-
return attempt(() =>
|
|
1158
|
+
if (!parentID) return EMPTY_VIEW;
|
|
1159
|
+
return attempt(() => deriveView(parentID), EMPTY_VIEW);
|
|
622
1160
|
});
|
|
623
1161
|
effect(() => {
|
|
624
|
-
const
|
|
625
|
-
|
|
1162
|
+
const { rows, progress } = view();
|
|
1163
|
+
const running = rows.some((row) => row.state === "running") || progress?.state === "running";
|
|
1164
|
+
if (running) sectionsRunning.add(key);
|
|
626
1165
|
else sectionsRunning.delete(key);
|
|
627
|
-
if (
|
|
1166
|
+
if (rows.length > 0 || progress) sectionsShowing.add(key);
|
|
628
1167
|
else sectionsShowing.delete(key);
|
|
629
1168
|
syncClocks();
|
|
630
1169
|
});
|
|
@@ -634,13 +1173,18 @@ function createSidebar(host) {
|
|
|
634
1173
|
sectionsMounted.delete(key);
|
|
635
1174
|
syncClocks();
|
|
636
1175
|
});
|
|
1176
|
+
const hasProgress = createMemo(() => view().progress !== void 0);
|
|
1177
|
+
insert(
|
|
1178
|
+
progressBox,
|
|
1179
|
+
() => hasProgress() ? attempt(() => buildProgress(() => view().progress), null) : null
|
|
1180
|
+
);
|
|
637
1181
|
insert(subagentsBox, () => {
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
640
|
-
return attempt(() => buildSection(
|
|
1182
|
+
const current = view();
|
|
1183
|
+
if (current.rows.length === 0) return null;
|
|
1184
|
+
return attempt(() => buildSection(current, expanded, setExpanded), null);
|
|
641
1185
|
});
|
|
642
1186
|
const accountLines = createMemo(
|
|
643
|
-
() => attempt(() => accountUsageLines(usage(), usageNow()), [])
|
|
1187
|
+
() => attempt(() => accountUsageLines(usage(), usageNow(), SIDEBAR_ROW_WIDTH, known()), [])
|
|
644
1188
|
);
|
|
645
1189
|
insert(accountsBox, () => {
|
|
646
1190
|
const lines = accountLines();
|
|
@@ -703,6 +1247,7 @@ function registerV1(api) {
|
|
|
703
1247
|
lastStatus.set(id, type);
|
|
704
1248
|
if (type === "idle" && previous && previous !== "idle") stoppedAt.set(id, Date.now());
|
|
705
1249
|
if (type !== "idle") stoppedAt.delete(id);
|
|
1250
|
+
bump();
|
|
706
1251
|
});
|
|
707
1252
|
const fetchChildren = (parentID) => {
|
|
708
1253
|
if (fetched.has(parentID)) return;
|
|
@@ -718,15 +1263,23 @@ function registerV1(api) {
|
|
|
718
1263
|
const host = {
|
|
719
1264
|
colors() {
|
|
720
1265
|
const theme = api.theme.current;
|
|
1266
|
+
const muted = theme.textMuted;
|
|
721
1267
|
return {
|
|
722
1268
|
text: theme.text,
|
|
723
|
-
muted
|
|
1269
|
+
muted,
|
|
724
1270
|
spinner: theme.accent,
|
|
725
|
-
done: mix(theme.success,
|
|
726
|
-
error: mix(theme.error,
|
|
727
|
-
effort: theme.warning ??
|
|
728
|
-
effortFaded: mix(theme.warning ??
|
|
729
|
-
hover: theme.backgroundElement
|
|
1271
|
+
done: mix(theme.success, muted, 0.45),
|
|
1272
|
+
error: mix(theme.error, muted, 0.35),
|
|
1273
|
+
effort: theme.warning ?? muted,
|
|
1274
|
+
effortFaded: mix(theme.warning ?? muted, muted, 0.45),
|
|
1275
|
+
hover: theme.backgroundElement,
|
|
1276
|
+
barDone: theme.success ?? theme.accent,
|
|
1277
|
+
barTrack: mix(muted, theme.backgroundPanel ?? theme.backgroundElement, 0.5),
|
|
1278
|
+
fraction: theme.text,
|
|
1279
|
+
tool: theme.info ?? theme.accent,
|
|
1280
|
+
cost: theme.secondary ?? theme.primary ?? muted,
|
|
1281
|
+
tier: theme.secondary ?? theme.primary ?? muted,
|
|
1282
|
+
reset: theme.info ?? theme.accent
|
|
730
1283
|
};
|
|
731
1284
|
},
|
|
732
1285
|
gather(parentID) {
|
|
@@ -779,9 +1332,31 @@ function registerV1(api) {
|
|
|
779
1332
|
if (type === "idle") return "idle";
|
|
780
1333
|
return "unknown";
|
|
781
1334
|
},
|
|
782
|
-
stoppedAt: (id) => stoppedAt.get(id)
|
|
1335
|
+
stoppedAt: (id) => stoppedAt.get(id),
|
|
1336
|
+
cost: (id) => attempt(() => api.state.session.get(id)?.cost, void 0)
|
|
783
1337
|
};
|
|
784
1338
|
},
|
|
1339
|
+
progress(sessionID) {
|
|
1340
|
+
const messages = [];
|
|
1341
|
+
const turn = turnMessages(api.state.session.messages(sessionID) ?? [], (message) => message.role === "user");
|
|
1342
|
+
for (const message of turn) {
|
|
1343
|
+
const tools = [];
|
|
1344
|
+
const texts = [];
|
|
1345
|
+
for (const part of api.state.part(message.id) ?? []) {
|
|
1346
|
+
if (message.role === "assistant" && part.type === "tool") tools.push({ name: part.tool, status: part.state?.status });
|
|
1347
|
+
if (message.role === "user" && part.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
1348
|
+
}
|
|
1349
|
+
messages.push({
|
|
1350
|
+
role: message.role,
|
|
1351
|
+
created: message.time?.created,
|
|
1352
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1353
|
+
tools,
|
|
1354
|
+
chars: message.role === "user" ? promptChars(texts) : void 0
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
const todos = attempt(() => api.state.session.todo?.(sessionID), void 0) ?? [];
|
|
1358
|
+
return { messages, todos: todos.map((todo) => ({ status: todo.status })) };
|
|
1359
|
+
},
|
|
785
1360
|
navigate(sessionID) {
|
|
786
1361
|
api.route.navigate("session", { sessionID });
|
|
787
1362
|
}
|
|
@@ -805,7 +1380,8 @@ function setupV2(context) {
|
|
|
805
1380
|
colors() {
|
|
806
1381
|
const theme = context.theme;
|
|
807
1382
|
const muted = theme.text.muted;
|
|
808
|
-
const
|
|
1383
|
+
const shade = context.themeMode === "light" ? "500" : "200";
|
|
1384
|
+
const accent = theme.hue.accent[shade] ?? theme.text.base;
|
|
809
1385
|
return {
|
|
810
1386
|
text: theme.text.base,
|
|
811
1387
|
muted,
|
|
@@ -814,7 +1390,14 @@ function setupV2(context) {
|
|
|
814
1390
|
error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),
|
|
815
1391
|
effort: theme.text.feedback.warning?.base ?? muted,
|
|
816
1392
|
effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),
|
|
817
|
-
hover: theme.background.raised.base
|
|
1393
|
+
hover: theme.background.raised.base,
|
|
1394
|
+
barDone: theme.text.feedback.success.base ?? accent,
|
|
1395
|
+
barTrack: mix(muted, theme.background.base ?? theme.background.raised.base, 0.5),
|
|
1396
|
+
fraction: theme.text.base,
|
|
1397
|
+
tool: theme.text.feedback.info?.base ?? accent,
|
|
1398
|
+
cost: theme.hue.interactive?.[shade] ?? accent,
|
|
1399
|
+
tier: theme.hue.interactive?.[shade] ?? accent,
|
|
1400
|
+
reset: theme.text.feedback.info?.base ?? accent
|
|
818
1401
|
};
|
|
819
1402
|
},
|
|
820
1403
|
gather(parentID) {
|
|
@@ -853,9 +1436,37 @@ function setupV2(context) {
|
|
|
853
1436
|
if (status === "idle") return "idle";
|
|
854
1437
|
return "unknown";
|
|
855
1438
|
},
|
|
856
|
-
stoppedAt: () => void 0
|
|
1439
|
+
stoppedAt: () => void 0,
|
|
1440
|
+
cost: (id) => attempt(() => context.data.session.cost?.(id), void 0)
|
|
857
1441
|
};
|
|
858
1442
|
},
|
|
1443
|
+
progress(sessionID) {
|
|
1444
|
+
const messages = [];
|
|
1445
|
+
const turn = turnMessages(
|
|
1446
|
+
context.data.session.message.list(sessionID) ?? [],
|
|
1447
|
+
(message) => message.type === "user" || message.type === "synthetic"
|
|
1448
|
+
);
|
|
1449
|
+
for (const message of turn) {
|
|
1450
|
+
const tools = [];
|
|
1451
|
+
const texts = [];
|
|
1452
|
+
for (const item of message.content ?? []) {
|
|
1453
|
+
if (message.type === "assistant" && item.type === "tool") tools.push({ name: item.name, status: item.state?.status });
|
|
1454
|
+
if (message.type === "user" && item.type === "text" && typeof item.text === "string") texts.push(item.text);
|
|
1455
|
+
}
|
|
1456
|
+
if (message.type === "synthetic" && message.text) texts.push(message.text);
|
|
1457
|
+
const starts = message.type === "user" || message.type === "synthetic";
|
|
1458
|
+
messages.push({
|
|
1459
|
+
// A synthetic message (a background child's end) prompts the parent
|
|
1460
|
+
// like a user message does: it starts a turn.
|
|
1461
|
+
role: message.type === "synthetic" ? "user" : message.type ?? "",
|
|
1462
|
+
created: message.time?.created,
|
|
1463
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1464
|
+
tools,
|
|
1465
|
+
chars: starts ? promptChars(texts) : void 0
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
return { messages, todos: [] };
|
|
1469
|
+
},
|
|
859
1470
|
navigate(sessionID) {
|
|
860
1471
|
context.ui.router.navigate({ type: "session", sessionID });
|
|
861
1472
|
}
|