codedeck 0.5.3 → 0.7.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/README.md +2 -1
- package/dist/cli/commands/open.js +94 -2
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/review.js +122 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/usage-backfill.js +149 -0
- package/dist/cli/commands/usage-backfill.js.map +1 -0
- package/dist/cli/commands/usage.js +49 -4
- package/dist/cli/commands/usage.js.map +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/claude-transcript.js +139 -0
- package/dist/core/claude-transcript.js.map +1 -0
- package/dist/core/pricing.js +14 -5
- package/dist/core/pricing.js.map +1 -1
- package/dist/core/run-usage.js +53 -10
- package/dist/core/run-usage.js.map +1 -1
- package/dist/core/usage-source.js +17 -0
- package/dist/core/usage-source.js.map +1 -0
- package/dist/daemon/daemon.js +216 -8
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ipc.js +4 -3
- package/dist/daemon/ipc.js.map +1 -1
- package/dist/drivers/opencode/driver.js +24 -2
- package/dist/drivers/opencode/driver.js.map +1 -1
- package/dist/drivers/session-driver.js +7 -1
- package/dist/drivers/session-driver.js.map +1 -1
- package/dist/open/launchers/claude.js +78 -2
- package/dist/open/launchers/claude.js.map +1 -1
- package/dist/open/link-watcher.js +44 -0
- package/dist/open/link-watcher.js.map +1 -0
- package/dist/open/runtime.js +15 -12
- package/dist/open/runtime.js.map +1 -1
- package/dist/plugin/hooks/pane-toggle.ts +13 -0
- package/dist/plugin/hooks/register.tsx +20 -29
- package/dist/plugin/hooks/session-id.sh +1 -1
- package/dist/plugin/mods/agents/pane.ts +67 -30
- package/dist/plugin/mods/agents/types.ts +5 -1
- package/dist/plugin/statusline.sh +40 -3
- package/dist/store/database.js +44 -0
- package/dist/store/database.js.map +1 -1
- package/dist/store/native-links.js +80 -0
- package/dist/store/native-links.js.map +1 -0
- package/dist/store/sessions.js +113 -41
- package/dist/store/sessions.js.map +1 -1
- package/dist/store/usage-ledger.js +153 -0
- package/dist/store/usage-ledger.js.map +1 -0
- package/dist/web/review-page.js +1 -2
- package/dist/web/review-page.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import type { Register } from "claude-code";
|
|
|
7
7
|
import { formatPane, paneButtonLabel, selectPane } from "../mods/agents/pane.js";
|
|
8
8
|
import { parseRows } from "../mods/agents/parse.js";
|
|
9
9
|
import type { PaneSnapshot } from "../mods/agents/types.js";
|
|
10
|
+
import { togglePane } from "./pane-toggle.js";
|
|
10
11
|
|
|
11
12
|
// Stable pane id: 1 to 64 letters, digits, "_" or "-". open carries it into
|
|
12
13
|
// e.requestId on the render event, which is how this module tells its own pane
|
|
@@ -42,6 +43,12 @@ type Engine$ = {
|
|
|
42
43
|
};
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
const toggleAgentsPane = async ($: Engine$, isOpen: boolean): Promise<boolean> =>
|
|
47
|
+
togglePane(isOpen, {
|
|
48
|
+
open: () => $.ui.open({ id: PANE_ID, side: "right" }),
|
|
49
|
+
close: () => $.ui.close({ id: PANE_ID }),
|
|
50
|
+
});
|
|
51
|
+
|
|
45
52
|
// Passing $ into a helper is allowed, verified. What the engine refuses is
|
|
46
53
|
// pulling a namespace off it: `const P = $.process` fails to load the module.
|
|
47
54
|
// So refresh takes $ as a parameter and the state register owns travels beside
|
|
@@ -118,20 +125,15 @@ export const register: Register = (on) => {
|
|
|
118
125
|
description: "toggle the codedeck agents pane",
|
|
119
126
|
immediate: true,
|
|
120
127
|
});
|
|
121
|
-
// Only
|
|
122
|
-
// loads this plugin by manual --plugin-dir or a global
|
|
123
|
-
// CODEDECK_RUN_ID, and
|
|
124
|
-
//
|
|
125
|
-
// for anyone who wants the pane anyway.
|
|
128
|
+
// Only show the button when there is actually a run behind the pane. A
|
|
129
|
+
// session that loads this plugin by manual --plugin-dir or a global
|
|
130
|
+
// install has no CODEDECK_RUN_ID, and its dock stays empty. /band remains
|
|
131
|
+
// available for anyone who wants to open it anyway.
|
|
126
132
|
const runId = await $.env.get("CODEDECK_RUN_ID");
|
|
127
133
|
state.hasRun = Boolean(runId);
|
|
128
134
|
if (runId) {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
await $.ui.open({ id: PANE_ID, side: "right" });
|
|
132
|
-
paneOpen = true;
|
|
133
|
-
// Populate before the first turn, or the pane stays blank through the
|
|
134
|
-
// whole opening exchange, which is exactly when the run is dispatched.
|
|
135
|
+
// Prime the button label without taking screen space when the session
|
|
136
|
+
// starts. The panel opens only on an explicit button press or /band.
|
|
135
137
|
void refresh($, state);
|
|
136
138
|
}
|
|
137
139
|
return next(e);
|
|
@@ -143,14 +145,9 @@ export const register: Register = (on) => {
|
|
|
143
145
|
// is the one call here not yet verified in a PTY session, paneOpen stays
|
|
144
146
|
// true and the next /band retries the close, instead of the flag and the
|
|
145
147
|
// pane desyncing for the rest of the session.
|
|
146
|
-
|
|
147
|
-
await $.ui.close({ id: PANE_ID });
|
|
148
|
-
paneOpen = false;
|
|
149
|
-
} else {
|
|
150
|
-
await $.ui.open({ id: PANE_ID, side: "right" });
|
|
151
|
-
paneOpen = true;
|
|
152
|
-
}
|
|
148
|
+
paneOpen = await toggleAgentsPane($, paneOpen);
|
|
153
149
|
await $.ui.invalidate("ui.render");
|
|
150
|
+
if (paneOpen) void refresh($, state);
|
|
154
151
|
return { text: paneOpen ? "agents pane open" : "agents pane closed" };
|
|
155
152
|
});
|
|
156
153
|
|
|
@@ -175,14 +172,9 @@ export const register: Register = (on) => {
|
|
|
175
172
|
// A $ captured from a past render does work, verified, but it outlives the
|
|
176
173
|
// event it came from and nothing promises how long.
|
|
177
174
|
if ((e as { element?: string }).element === BUTTON_KEY) {
|
|
178
|
-
await
|
|
179
|
-
// Opening an already open pane is a no-op, verified live, so the button
|
|
180
|
-
// never has to know the truth. Re-syncing the flag is the point: the
|
|
181
|
-
// pane's own close box closes it without firing a single hook, and after
|
|
182
|
-
// that one /band would toggle the wrong way.
|
|
183
|
-
paneOpen = true;
|
|
175
|
+
paneOpen = await toggleAgentsPane($, paneOpen);
|
|
184
176
|
await $.ui.invalidate("ui.render");
|
|
185
|
-
void refresh($, state);
|
|
177
|
+
if (paneOpen) void refresh($, state);
|
|
186
178
|
}
|
|
187
179
|
return await next(e);
|
|
188
180
|
});
|
|
@@ -195,10 +187,9 @@ export const register: Register = (on) => {
|
|
|
195
187
|
// wrong shape".
|
|
196
188
|
on("ui.render", async ($, e, next) => {
|
|
197
189
|
if (e.surface !== "terminal") return await next(e);
|
|
198
|
-
// The
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// is a run behind it.
|
|
190
|
+
// The pane can be closed from its own close box, which fires no hook.
|
|
191
|
+
// Keep one button above the prompt as the visible toggle for the state
|
|
192
|
+
// transitions this module owns.
|
|
202
193
|
if (e.component === "AbovePrompt") {
|
|
203
194
|
if (!state.hasRun) return await next(e);
|
|
204
195
|
const { Box, Button } = await $.ui.resolve(e, "Box", "Button");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Canvas layer of the orchestrator agents pane.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
3
|
+
// The run canvas, drawn as ASCII boxes for a docked terminal pane.
|
|
4
4
|
// Two pure functions behind one frozen interface: selectPane narrows the
|
|
5
5
|
// daemon's full session list to one run, formatPane turns that snapshot into
|
|
6
6
|
// the lines of the drawing and fits them to the height the pane was given.
|
|
@@ -47,24 +47,22 @@ function vocab(entries: Record<string, string>): Record<string, string> {
|
|
|
47
47
|
return Object.assign(Object.create(null) as Record<string, string>, entries);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const STATUS_DOT = vocab({
|
|
51
|
-
working: "●",
|
|
52
|
-
starting: "●",
|
|
53
|
-
needs_input: "◉",
|
|
54
|
-
});
|
|
55
50
|
const STATUS_WORD = vocab({
|
|
56
|
-
working: "Working
|
|
51
|
+
working: "Working",
|
|
57
52
|
starting: "Starting",
|
|
58
53
|
needs_input: "Waiting for you",
|
|
59
54
|
completed: "Completed",
|
|
60
55
|
stopped: "Paused",
|
|
61
56
|
failed: "Failed",
|
|
62
57
|
});
|
|
58
|
+
// Brand marks from the JetBrains Nerd Font and Omarchy icon font. The pane is
|
|
59
|
+
// a terminal surface, so these keep the identity of each harness without
|
|
60
|
+
// spending the width of its full name.
|
|
63
61
|
const HARNESS_GLYPH = vocab({
|
|
64
|
-
claude: "
|
|
65
|
-
opencode: "
|
|
66
|
-
codex: "
|
|
67
|
-
omp: "
|
|
62
|
+
claude: "\uEC82",
|
|
63
|
+
opencode: "\uE902",
|
|
64
|
+
codex: "\uEC81",
|
|
65
|
+
omp: "\uE903",
|
|
68
66
|
});
|
|
69
67
|
const HARNESS_LABEL = vocab({
|
|
70
68
|
claude: "Claude",
|
|
@@ -108,6 +106,8 @@ function toPaneRow(row: SessionRow): PaneRow {
|
|
|
108
106
|
id: row.id,
|
|
109
107
|
status: row.status || EMPTY_CELL,
|
|
110
108
|
agent: row.agent || EMPTY_CELL,
|
|
109
|
+
model: text(row.model) || undefined,
|
|
110
|
+
effort: text(row.effort) || undefined,
|
|
111
111
|
name: row.name || EMPTY_CELL,
|
|
112
112
|
updatedAt: iso,
|
|
113
113
|
};
|
|
@@ -130,7 +130,11 @@ export function selectPane(rows: SessionRow[], runId: string, budget?: number):
|
|
|
130
130
|
);
|
|
131
131
|
const orchestratorRow = matching.find((row) => row.origin === "open");
|
|
132
132
|
const orchestrator = orchestratorRow
|
|
133
|
-
? {
|
|
133
|
+
? {
|
|
134
|
+
agent: text(orchestratorRow.agent) || EMPTY_CELL,
|
|
135
|
+
model: text(orchestratorRow.model) || undefined,
|
|
136
|
+
effort: text(orchestratorRow.effort) || undefined,
|
|
137
|
+
}
|
|
134
138
|
: undefined;
|
|
135
139
|
const workers = matching.filter((row) => row.origin !== "open");
|
|
136
140
|
const ordered = [...workers].sort(compareRows);
|
|
@@ -176,20 +180,34 @@ function harnessLabel(agent: unknown): string {
|
|
|
176
180
|
return HARNESS_LABEL[key] ?? cell(agent);
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
function dot(status: unknown): string {
|
|
180
|
-
return STATUS_DOT[text(status)] ?? "○";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
183
|
function statusWord(status: unknown): string {
|
|
184
184
|
return STATUS_WORD[text(status)] ?? cell(status);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function footerLegend(columns: number): string {
|
|
188
|
+
const fullHarness =
|
|
189
|
+
" " +
|
|
190
|
+
glyph("claude") +
|
|
191
|
+
" Claude " +
|
|
192
|
+
glyph("opencode") +
|
|
193
|
+
" OpenCode " +
|
|
194
|
+
glyph("codex") +
|
|
195
|
+
" Codex " +
|
|
196
|
+
glyph("omp") +
|
|
197
|
+
" OMP";
|
|
198
|
+
if (fullHarness.length <= columns - 2) return fullHarness;
|
|
199
|
+
|
|
200
|
+
const iconHarness =
|
|
201
|
+
" " + glyph("claude") + " " + glyph("opencode") + " " + glyph("codex") + " " + glyph("omp");
|
|
202
|
+
return iconHarness;
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
function age(iso: unknown, now: number): string {
|
|
188
206
|
if (typeof iso !== "string") return "";
|
|
189
207
|
const then = Date.parse(iso);
|
|
190
208
|
if (Number.isNaN(then)) return "";
|
|
191
209
|
const minutes = Math.floor((now - then) / 60000);
|
|
192
|
-
if (minutes < 1) return "
|
|
210
|
+
if (minutes < 1) return "now";
|
|
193
211
|
if (minutes < 60) return `${minutes}m`;
|
|
194
212
|
return `${Math.floor(minutes / 60)}h`;
|
|
195
213
|
}
|
|
@@ -198,6 +216,16 @@ function count(value: unknown): number {
|
|
|
198
216
|
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
199
217
|
}
|
|
200
218
|
|
|
219
|
+
function detailLine(model: unknown, effort: unknown, status: string, when: string, width: number): string {
|
|
220
|
+
const state = status ? `${status}${when ? ` · ${when}` : ""}` : "";
|
|
221
|
+
const suffix = [text(effort).trim() ? `${cell(effort)} effort` : "", state]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(" · ");
|
|
224
|
+
const modelWidth = width - 3 - suffix.length - (suffix ? 3 : 0);
|
|
225
|
+
const modelText = text(model).trim() && modelWidth > 0 ? fit(cell(model), modelWidth).trimEnd() : "";
|
|
226
|
+
return ` ${[modelText, suffix].filter(Boolean).join(" · ")}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
201
229
|
// How old a row looks for eviction: an unparseable timestamp is the oldest
|
|
202
230
|
// possible age, matching how compareRows parks such rows at the end of the
|
|
203
231
|
// display. The orchestrator pins itself to +Infinity so it is the very last
|
|
@@ -230,7 +258,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
230
258
|
);
|
|
231
259
|
const orchestrator =
|
|
232
260
|
source.orchestrator && typeof source.orchestrator === "object"
|
|
233
|
-
? (source.orchestrator as Partial<{ agent: string }>)
|
|
261
|
+
? (source.orchestrator as Partial<{ agent: string; model: string; effort: string }>)
|
|
234
262
|
: undefined;
|
|
235
263
|
const hidden = count(source.hidden);
|
|
236
264
|
const total = count(source.total);
|
|
@@ -296,8 +324,11 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
296
324
|
if (orchestrator !== undefined) {
|
|
297
325
|
blocks.push(
|
|
298
326
|
cardBlock(Number.POSITIVE_INFINITY, "", [
|
|
299
|
-
` ${glyph(orchestrator.agent)}
|
|
327
|
+
` ${glyph(orchestrator.agent)} Orchestrator`,
|
|
300
328
|
` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
|
|
329
|
+
...(text(orchestrator.model).trim() || text(orchestrator.effort).trim()
|
|
330
|
+
? [detailLine(orchestrator.model, orchestrator.effort, "", "", INNER)]
|
|
331
|
+
: []),
|
|
301
332
|
]),
|
|
302
333
|
);
|
|
303
334
|
}
|
|
@@ -311,9 +342,9 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
311
342
|
const word = statusWord(status);
|
|
312
343
|
blocks.push(
|
|
313
344
|
cardBlock(rowAge(row.updatedAt), cell(row.id), [
|
|
314
|
-
` ${glyph(row.agent)} ${
|
|
345
|
+
` ${glyph(row.agent)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
|
|
315
346
|
` ${cell(row.name)}`,
|
|
316
|
-
|
|
347
|
+
detailLine(row.model, row.effort, word, when, INNER),
|
|
317
348
|
]),
|
|
318
349
|
);
|
|
319
350
|
}
|
|
@@ -347,8 +378,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
347
378
|
const stemAbove = (list: Block[], i: number): boolean =>
|
|
348
379
|
i > 0 && (list[i].kind === "card" || list[i - 1].kind === "card");
|
|
349
380
|
|
|
350
|
-
//
|
|
351
|
-
// they are drawn, and they are never the lines that the fit cuts.
|
|
381
|
+
// These frame lines are always drawn when the pane has content.
|
|
352
382
|
const head = [
|
|
353
383
|
frameTop(`Run canvas ${cell(source.runId)}`),
|
|
354
384
|
frameRow(` ${total} sessions · ${working} working`),
|
|
@@ -356,11 +386,10 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
356
386
|
frameSep(),
|
|
357
387
|
frameRow(""),
|
|
358
388
|
];
|
|
389
|
+
const harnessLegend = footerLegend(W);
|
|
359
390
|
const tail = [
|
|
360
|
-
frameRow(""),
|
|
361
391
|
frameSep(),
|
|
362
|
-
frameRow(
|
|
363
|
-
frameRow(" ▲ Claude ■ OpenCode ◆ Codex ⬟ OMP"),
|
|
392
|
+
frameRow(harnessLegend),
|
|
364
393
|
frameBot(),
|
|
365
394
|
];
|
|
366
395
|
const fixed = head.length + tail.length;
|
|
@@ -407,6 +436,10 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
407
436
|
if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
|
|
408
437
|
else out.push(hiddenRow);
|
|
409
438
|
}
|
|
439
|
+
if (limit !== undefined) {
|
|
440
|
+
const spare = limit - out.length - tail.length;
|
|
441
|
+
for (let i = 0; i < spare; i += 1) out.push(frameRow(""));
|
|
442
|
+
}
|
|
410
443
|
out.push(...tail);
|
|
411
444
|
return out;
|
|
412
445
|
}
|
|
@@ -443,10 +476,14 @@ export function paneButtonLabel(snapshot: PaneSnapshot | undefined): string {
|
|
|
443
476
|
if (!snapshot || typeof snapshot !== "object") return "agents";
|
|
444
477
|
try {
|
|
445
478
|
const rows = Array.isArray(snapshot.rows) ? snapshot.rows : [];
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
479
|
+
const workingRows = rows.filter((row) => LIVE.has(row?.status ?? ""));
|
|
480
|
+
if (workingRows.length > 0) {
|
|
481
|
+
return `${workingRows.length} working ${workingRows.map((row) => glyph(row?.agent)).join("")}`;
|
|
482
|
+
}
|
|
483
|
+
const waitingRows = rows.filter((row) => WAIT.has(row?.status ?? ""));
|
|
484
|
+
return waitingRows.length > 0
|
|
485
|
+
? `${waitingRows.length} waiting for you ${waitingRows.map((row) => glyph(row?.agent)).join("")}`
|
|
486
|
+
: "no active agents";
|
|
450
487
|
} catch {
|
|
451
488
|
return "agents";
|
|
452
489
|
}
|
|
@@ -13,6 +13,8 @@ export interface SessionRow {
|
|
|
13
13
|
origin?: string | null;
|
|
14
14
|
name?: string;
|
|
15
15
|
agent?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
effort?: string;
|
|
16
18
|
status?: string;
|
|
17
19
|
updatedAt?: string;
|
|
18
20
|
}
|
|
@@ -26,6 +28,8 @@ export interface PaneRow {
|
|
|
26
28
|
id: string;
|
|
27
29
|
status: string;
|
|
28
30
|
agent: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
effort?: string;
|
|
29
33
|
name: string;
|
|
30
34
|
updatedAt?: string;
|
|
31
35
|
}
|
|
@@ -34,7 +38,7 @@ export interface PaneRow {
|
|
|
34
38
|
export interface PaneSnapshot {
|
|
35
39
|
runId: string;
|
|
36
40
|
/** The orchestrator's own row, the only one whose origin is "open". */
|
|
37
|
-
orchestrator: { agent: string } | undefined;
|
|
41
|
+
orchestrator: { agent: string; model?: string; effort?: string } | undefined;
|
|
38
42
|
rows: PaneRow[];
|
|
39
43
|
/** Rows that matched the run but fell outside the budget. */
|
|
40
44
|
hidden: number;
|
|
@@ -181,8 +181,17 @@ const getRunUsage = () => {
|
|
|
181
181
|
const runId = text(process.env.CODEDECK_RUN_ID);
|
|
182
182
|
if (!runId) return undefined;
|
|
183
183
|
|
|
184
|
+
const args = ["usage", runId, "--json"];
|
|
185
|
+
const sessionId = payload.session_id;
|
|
186
|
+
const observeCost = nonNegativeNumber(payload.cost?.total_cost_usd);
|
|
187
|
+
if (
|
|
188
|
+
typeof sessionId === "string" &&
|
|
189
|
+
/^[0-9a-fA-F-]{8,}$/.test(sessionId) &&
|
|
190
|
+
observeCost !== undefined
|
|
191
|
+
) args.push("--observe", `${sessionId}=${observeCost}`);
|
|
192
|
+
|
|
184
193
|
try {
|
|
185
|
-
const output = execFileSync("codedeck",
|
|
194
|
+
const output = execFileSync("codedeck", args, {
|
|
186
195
|
encoding: "utf8",
|
|
187
196
|
stdio: ["ignore", "pipe", "ignore"],
|
|
188
197
|
timeout: 1000,
|
|
@@ -215,8 +224,30 @@ const getRunUsage = () => {
|
|
|
215
224
|
}
|
|
216
225
|
};
|
|
217
226
|
|
|
227
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
228
|
+
|
|
229
|
+
const getOrchestratorUsage = (usage) => {
|
|
230
|
+
const value = usage.orchestrator;
|
|
231
|
+
if (
|
|
232
|
+
!isObject(value) ||
|
|
233
|
+
nonNegativeNumber(value.costUsd) === undefined ||
|
|
234
|
+
typeof value.costComplete !== "boolean" ||
|
|
235
|
+
nonNegativeNumber(value.inputTokens) === undefined ||
|
|
236
|
+
nonNegativeNumber(value.outputTokens) === undefined ||
|
|
237
|
+
nonNegativeNumber(value.cachedTokens) === undefined ||
|
|
238
|
+
!Array.isArray(value.sources)
|
|
239
|
+
) return undefined;
|
|
240
|
+
if (!value.sources.every((source) =>
|
|
241
|
+
isObject(source) &&
|
|
242
|
+
typeof source.nativeId === "string" &&
|
|
243
|
+
nonNegativeNumber(source.costUsd) !== undefined
|
|
244
|
+
)) return undefined;
|
|
245
|
+
return value;
|
|
246
|
+
};
|
|
247
|
+
|
|
218
248
|
const local = localCost();
|
|
219
249
|
const runUsage = getRunUsage();
|
|
250
|
+
const orchestratorUsage = runUsage ? getOrchestratorUsage(runUsage) : undefined;
|
|
220
251
|
const workerTokens = runUsage
|
|
221
252
|
? runUsage.inputTokens + runUsage.outputTokens + runUsage.cachedTokens
|
|
222
253
|
: undefined;
|
|
@@ -227,8 +258,14 @@ const tokenField = () => {
|
|
|
227
258
|
|
|
228
259
|
const runField = () => {
|
|
229
260
|
if (!runUsage) return undefined;
|
|
230
|
-
const
|
|
231
|
-
|
|
261
|
+
const sourceCost = orchestratorUsage
|
|
262
|
+
? orchestratorUsage.sources.reduce(
|
|
263
|
+
(sum, source) => source.nativeId === payload.session_id ? sum : sum + source.costUsd,
|
|
264
|
+
0,
|
|
265
|
+
)
|
|
266
|
+
: 0;
|
|
267
|
+
const total = (local ?? 0) + runUsage.costUsd + sourceCost;
|
|
268
|
+
const incomplete = !runUsage.costComplete || Boolean(orchestratorUsage && !orchestratorUsage.costComplete);
|
|
232
269
|
if (!incomplete && total < COST_DISPLAY_THRESHOLD) return undefined;
|
|
233
270
|
return paint(MUTED, "run ") + costAmount(total, incomplete);
|
|
234
271
|
};
|
package/dist/store/database.js
CHANGED
|
@@ -89,9 +89,53 @@ export class Database {
|
|
|
89
89
|
active INTEGER NOT NULL DEFAULT 1
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
+
CREATE TABLE IF NOT EXISTS usage_sources (
|
|
93
|
+
source_key TEXT PRIMARY KEY,
|
|
94
|
+
cost REAL,
|
|
95
|
+
input_tokens INTEGER,
|
|
96
|
+
output_tokens INTEGER,
|
|
97
|
+
cached_tokens INTEGER,
|
|
98
|
+
updated_at TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS usage_attributions (
|
|
102
|
+
session_id TEXT NOT NULL,
|
|
103
|
+
source_key TEXT NOT NULL,
|
|
104
|
+
cost REAL,
|
|
105
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
106
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
108
|
+
PRIMARY KEY (session_id, source_key),
|
|
109
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
110
|
+
FOREIGN KEY (source_key) REFERENCES usage_sources(source_key) ON DELETE CASCADE
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS session_native_links (
|
|
114
|
+
session_id TEXT NOT NULL,
|
|
115
|
+
native_id TEXT NOT NULL,
|
|
116
|
+
linked_at TEXT NOT NULL,
|
|
117
|
+
reconciled_at TEXT,
|
|
118
|
+
state TEXT,
|
|
119
|
+
PRIMARY KEY (session_id, native_id),
|
|
120
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS usage_legacy (
|
|
124
|
+
native_id TEXT PRIMARY KEY,
|
|
125
|
+
ended_at TEXT NOT NULL,
|
|
126
|
+
cwd TEXT,
|
|
127
|
+
repository TEXT,
|
|
128
|
+
model TEXT,
|
|
129
|
+
cost REAL,
|
|
130
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
131
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0
|
|
133
|
+
);
|
|
134
|
+
|
|
92
135
|
CREATE INDEX IF NOT EXISTS idx_events_session_seq ON events(session_id, sequence);
|
|
93
136
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
94
137
|
CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at DESC);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_usage_attributions_source_key ON usage_attributions(source_key);
|
|
95
139
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_claims_active_session_path
|
|
96
140
|
ON claims(session_id, path_glob) WHERE active = 1;
|
|
97
141
|
`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAM1D,MAAM,OAAO,QAAQ;IACX,EAAE,CAAe;IACjB,MAAM,CAAS;IACf,QAAQ,CAAU;IAE1B,YAAY,MAAe,EAAE,OAAyB;QACpD,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,KAAK,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,UAAU,EAAE,CAAC;YACb,2BAA2B;YAC3B,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,CAAC,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC;gBACH,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;YAC9C,CAAC;YAAC,MAAM,CAAC;gBACP,gCAAgC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SessionStore } from "./sessions.js";
|
|
2
|
+
function toNativeLink(row) {
|
|
3
|
+
return {
|
|
4
|
+
sessionId: row.session_id,
|
|
5
|
+
nativeId: row.native_id,
|
|
6
|
+
linkedAt: row.linked_at,
|
|
7
|
+
reconciledAt: row.reconciled_at,
|
|
8
|
+
state: row.state,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export class NativeLinkStore {
|
|
12
|
+
db;
|
|
13
|
+
constructor(db) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
}
|
|
16
|
+
link(sessionId, nativeId) {
|
|
17
|
+
const previous = this.db.prepare(`
|
|
18
|
+
SELECT native_id FROM session_native_links
|
|
19
|
+
WHERE session_id = ? AND native_id <> ? AND reconciled_at IS NULL
|
|
20
|
+
ORDER BY linked_at, native_id
|
|
21
|
+
`).all(sessionId, nativeId).map((row) => row.native_id);
|
|
22
|
+
const result = this.db.prepare(`
|
|
23
|
+
INSERT INTO session_native_links (session_id, native_id, linked_at)
|
|
24
|
+
VALUES (?, ?, ?)
|
|
25
|
+
ON CONFLICT(session_id, native_id) DO NOTHING
|
|
26
|
+
`).run(sessionId, nativeId, new Date().toISOString());
|
|
27
|
+
return { created: Number(result.changes) > 0, previous };
|
|
28
|
+
}
|
|
29
|
+
unreconciled(sessionId) {
|
|
30
|
+
const rows = this.db.prepare(`
|
|
31
|
+
SELECT session_id, native_id, linked_at, reconciled_at, state
|
|
32
|
+
FROM session_native_links
|
|
33
|
+
WHERE session_id = ? AND reconciled_at IS NULL
|
|
34
|
+
ORDER BY linked_at, native_id
|
|
35
|
+
`).all(sessionId);
|
|
36
|
+
return rows.map(toNativeLink);
|
|
37
|
+
}
|
|
38
|
+
linksFor(sessionIds) {
|
|
39
|
+
if (sessionIds.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
42
|
+
const rows = this.db.prepare(`
|
|
43
|
+
SELECT session_id, native_id, linked_at, reconciled_at, state
|
|
44
|
+
FROM session_native_links
|
|
45
|
+
WHERE session_id IN (${placeholders})
|
|
46
|
+
ORDER BY session_id, linked_at, native_id
|
|
47
|
+
`).all(...sessionIds);
|
|
48
|
+
return rows.map(toNativeLink);
|
|
49
|
+
}
|
|
50
|
+
markReconciled(sessionId, nativeId, state) {
|
|
51
|
+
this.db.prepare(`
|
|
52
|
+
UPDATE session_native_links SET reconciled_at = ?, state = ?
|
|
53
|
+
WHERE session_id = ? AND native_id = ?
|
|
54
|
+
`).run(new Date().toISOString(), state, sessionId, nativeId);
|
|
55
|
+
}
|
|
56
|
+
staleOpenRows(isDead) {
|
|
57
|
+
const candidates = this.db.prepare(`
|
|
58
|
+
SELECT DISTINCT sessions.id, sessions.status
|
|
59
|
+
FROM sessions
|
|
60
|
+
INNER JOIN session_native_links
|
|
61
|
+
ON session_native_links.session_id = sessions.id
|
|
62
|
+
WHERE sessions.origin = 'open'
|
|
63
|
+
AND session_native_links.reconciled_at IS NULL
|
|
64
|
+
AND sessions.status IN ('interrupted', 'working')
|
|
65
|
+
ORDER BY sessions.created_at, sessions.id
|
|
66
|
+
`).all();
|
|
67
|
+
const sessions = new SessionStore(this.db);
|
|
68
|
+
const stale = [];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const session = sessions.get(candidate.id);
|
|
71
|
+
if (!session)
|
|
72
|
+
continue;
|
|
73
|
+
if (candidate.status === "interrupted" || (candidate.status === "working" && isDead(session))) {
|
|
74
|
+
stale.push(session);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return stale;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=native-links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-links.js","sourceRoot":"","sources":["../../src/store/native-links.ts"],"names":[],"mappings":"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"}
|