codedeck 0.6.0 → 0.7.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/README.md +50 -35
- package/dist/cli/commands/claims.js +3 -2
- package/dist/cli/commands/claims.js.map +1 -1
- package/dist/cli/commands/doctor.js +8 -40
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/open.js +102 -11
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/ps.js +6 -6
- package/dist/cli/commands/ps.js.map +1 -1
- package/dist/cli/commands/review.js +36 -58
- package/dist/cli/commands/review.js.map +1 -1
- package/dist/cli/commands/run.js +27 -27
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/setup.js +96 -314
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +2 -1
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/ui.js +61 -0
- package/dist/cli/commands/ui.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 +113 -45
- package/dist/cli/commands/usage.js.map +1 -1
- package/dist/cli/commands/wait.js +3 -2
- package/dist/cli/commands/wait.js.map +1 -1
- package/dist/cli/index.js +55 -39
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/web-launch.js +126 -0
- package/dist/cli/web-launch.js.map +1 -0
- package/dist/config/config.js +0 -70
- package/dist/config/config.js.map +1 -1
- package/dist/config/paths.js +1 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/setup.js +248 -0
- package/dist/config/setup.js.map +1 -0
- package/dist/config/web-host.js +31 -0
- package/dist/config/web-host.js.map +1 -0
- package/dist/config/web-port.js +24 -0
- package/dist/config/web-port.js.map +1 -0
- package/dist/core/claude-transcript.js +226 -0
- package/dist/core/claude-transcript.js.map +1 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/models.js +4 -1
- package/dist/core/models.js.map +1 -1
- package/dist/core/pricing.js +87 -55
- package/dist/core/pricing.js.map +1 -1
- package/dist/core/roles.js +6 -5
- package/dist/core/roles.js.map +1 -1
- package/dist/core/run-skills.js +114 -0
- package/dist/core/run-skills.js.map +1 -0
- package/dist/core/run-usage.js +56 -10
- package/dist/core/run-usage.js.map +1 -1
- package/dist/core/session.js +6 -16
- package/dist/core/session.js.map +1 -1
- package/dist/core/usage-query.js +41 -0
- package/dist/core/usage-query.js.map +1 -0
- package/dist/core/usage-source.js +17 -0
- package/dist/core/usage-source.js.map +1 -0
- package/dist/daemon/build-id.js +30 -0
- package/dist/daemon/build-id.js.map +1 -0
- package/dist/daemon/daemon.js +1257 -700
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/instance-lock.js +48 -0
- package/dist/daemon/instance-lock.js.map +1 -0
- package/dist/daemon/ipc.js +4 -3
- package/dist/daemon/ipc.js.map +1 -1
- package/dist/daemon/web-supervisor.js +251 -0
- package/dist/daemon/web-supervisor.js.map +1 -0
- package/dist/drivers/claude/driver.js +1 -0
- package/dist/drivers/claude/driver.js.map +1 -1
- package/dist/drivers/codex/driver.js +14 -3
- package/dist/drivers/codex/driver.js.map +1 -1
- package/dist/drivers/codex/host-skills.js +57 -0
- package/dist/drivers/codex/host-skills.js.map +1 -0
- package/dist/drivers/opencode/driver.js +6 -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/drivers/session-runtime.js +2 -0
- package/dist/drivers/session-runtime.js.map +1 -1
- package/dist/drivers/terminal.js +15 -0
- package/dist/drivers/terminal.js.map +1 -1
- package/dist/git/diff.js +29 -4
- package/dist/git/diff.js.map +1 -1
- package/dist/open/contract.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/pty.js +330 -5
- package/dist/open/pty.js.map +1 -1
- package/dist/open/runtime.js +15 -12
- package/dist/open/runtime.js.map +1 -1
- package/dist/plugin/agents/general.md +33 -6
- package/dist/plugin/hooks/pane-ticker.ts +102 -0
- package/dist/plugin/hooks/register.tsx +39 -18
- package/dist/plugin/hooks/session-id.sh +1 -1
- package/dist/plugin/mods/agents/pane.ts +285 -156
- package/dist/plugin/mods/agents/types.ts +23 -3
- package/dist/plugin/statusline.sh +57 -32
- package/dist/store/database.js +49 -1
- 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 +166 -51
- 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/utils/process.js +2 -16
- package/dist/utils/process.js.map +1 -1
- package/dist/web/brand.js +59 -0
- package/dist/web/brand.js.map +1 -0
- package/dist/web/child.js +67 -0
- package/dist/web/child.js.map +1 -0
- package/dist/web/home-page.js +48 -0
- package/dist/web/home-page.js.map +1 -0
- package/dist/web/review-page.js +12 -5
- package/dist/web/review-page.js.map +1 -1
- package/dist/web/security.js +170 -0
- package/dist/web/security.js.map +1 -0
- package/dist/web/server.js +198 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/setup-icons.js +37 -0
- package/dist/web/setup-icons.js.map +1 -0
- package/dist/web/setup-page.js +1795 -0
- package/dist/web/setup-page.js.map +1 -0
- package/dist/web/setup-routes.js +475 -0
- package/dist/web/setup-routes.js.map +1 -0
- package/dist/web/usage-page.js +609 -0
- package/dist/web/usage-page.js.map +1 -0
- package/dist/web/usage-routes.js +88 -0
- package/dist/web/usage-routes.js.map +1 -0
- package/dist/web/web-token.js +56 -0
- package/dist/web/web-token.js.map +1 -0
- package/package.json +1 -1
- package/dist/plugin/prompts/_partials/commit.md +0 -145
- package/dist/plugin/prompts/_partials/pr-writer.md +0 -186
|
@@ -47,13 +47,8 @@ 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",
|
|
@@ -105,14 +100,40 @@ function text(value: unknown): string {
|
|
|
105
100
|
return value;
|
|
106
101
|
}
|
|
107
102
|
|
|
108
|
-
function
|
|
103
|
+
function displaySessionId(value: unknown): string {
|
|
104
|
+
// Mirrors SESSION_ID_DISPLAY_LENGTH in src/core/session.ts.
|
|
105
|
+
return text(value).slice(0, 8);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Shorten a leading home directory without relying on a process global. */
|
|
109
|
+
export function displayPath(path: string): string {
|
|
110
|
+
return path.replace(/^\/(?:home|Users)\/[^/]+(?=\/|$)/, "~");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// `workers` is the set of ids the snapshot keeps: a parent outside it (the
|
|
114
|
+
// run root, a row cut by the budget, a legacy row with no parent) leaves
|
|
115
|
+
// parentId undefined, which draws the card under the root.
|
|
116
|
+
function toPaneRow(row: SessionRow, workers: ReadonlySet<string>): PaneRow {
|
|
109
117
|
const iso = typeof row.updatedAt === "string" ? row.updatedAt : undefined;
|
|
118
|
+
const parent = text(row.parentId);
|
|
119
|
+
const path =
|
|
120
|
+
typeof row.worktree === "string" && row.worktree !== ""
|
|
121
|
+
? row.worktree
|
|
122
|
+
: typeof row.cwd === "string" && row.cwd !== ""
|
|
123
|
+
? row.cwd
|
|
124
|
+
: undefined;
|
|
110
125
|
return {
|
|
111
126
|
id: row.id,
|
|
112
127
|
status: row.status || EMPTY_CELL,
|
|
113
128
|
agent: row.agent || EMPTY_CELL,
|
|
129
|
+
model: text(row.model) || undefined,
|
|
130
|
+
effort: text(row.effort) || undefined,
|
|
114
131
|
name: row.name || EMPTY_CELL,
|
|
115
132
|
updatedAt: iso,
|
|
133
|
+
createdAt: typeof row.createdAt === "string" ? row.createdAt : undefined,
|
|
134
|
+
parentId: parent !== "" && parent !== row.id && workers.has(parent) ? parent : undefined,
|
|
135
|
+
role: text(row.role) || undefined,
|
|
136
|
+
path,
|
|
116
137
|
};
|
|
117
138
|
}
|
|
118
139
|
|
|
@@ -121,7 +142,8 @@ function toPaneRow(row: SessionRow): PaneRow {
|
|
|
121
142
|
*
|
|
122
143
|
* Filters to `runId`, lifts the single `origin === "open"` row out as the
|
|
123
144
|
* orchestrator, orders the rest by `updatedAt` descending with a total order,
|
|
124
|
-
* and cuts to `budget`.
|
|
145
|
+
* and cuts to `budget`. Each kept row carries the id of the kept row that
|
|
146
|
+
* dispatched it, so formatPane can draw who started whom. With no explicit budget every matched row is kept and
|
|
125
147
|
* `hidden` is 0; dropping rows to fit the pane is formatPane's decision.
|
|
126
148
|
*/
|
|
127
149
|
export function selectPane(rows: SessionRow[], runId: string, budget?: number): PaneSnapshot {
|
|
@@ -133,15 +155,21 @@ export function selectPane(rows: SessionRow[], runId: string, budget?: number):
|
|
|
133
155
|
);
|
|
134
156
|
const orchestratorRow = matching.find((row) => row.origin === "open");
|
|
135
157
|
const orchestrator = orchestratorRow
|
|
136
|
-
? {
|
|
158
|
+
? {
|
|
159
|
+
agent: text(orchestratorRow.agent) || EMPTY_CELL,
|
|
160
|
+
model: text(orchestratorRow.model) || undefined,
|
|
161
|
+
effort: text(orchestratorRow.effort) || undefined,
|
|
162
|
+
role: text(orchestratorRow.role) || undefined,
|
|
163
|
+
}
|
|
137
164
|
: undefined;
|
|
138
165
|
const workers = matching.filter((row) => row.origin !== "open");
|
|
139
166
|
const ordered = [...workers].sort(compareRows);
|
|
140
167
|
const kept = ordered.slice(0, limit);
|
|
168
|
+
const keptIds = new Set(kept.map((row) => row.id));
|
|
141
169
|
return {
|
|
142
170
|
runId: text(runId),
|
|
143
171
|
orchestrator,
|
|
144
|
-
rows: kept.map(toPaneRow),
|
|
172
|
+
rows: kept.map((row) => toPaneRow(row, keptIds)),
|
|
145
173
|
hidden: ordered.length - kept.length,
|
|
146
174
|
total: matching.length,
|
|
147
175
|
};
|
|
@@ -169,6 +197,17 @@ function fit(value: string, width: number, fill = " "): string {
|
|
|
169
197
|
return (cut + "…").padEnd(width, fill);
|
|
170
198
|
}
|
|
171
199
|
|
|
200
|
+
// Keep the tail of a value, never leave half a surrogate pair after the marker,
|
|
201
|
+
// and pad to the exact width used by the pane.
|
|
202
|
+
function fitStart(value: string, width: number, fill = " "): string {
|
|
203
|
+
if (width <= 0) return "";
|
|
204
|
+
if (value.length <= width) return value.padEnd(width, fill);
|
|
205
|
+
let start = value.length - (width - 1);
|
|
206
|
+
const first = value.charCodeAt(start);
|
|
207
|
+
if (first >= 0xdc00 && first <= 0xdfff) start += 1;
|
|
208
|
+
return ("…" + value.slice(start)).padEnd(width, fill);
|
|
209
|
+
}
|
|
210
|
+
|
|
172
211
|
function glyph(agent: unknown): string {
|
|
173
212
|
const key = text(agent);
|
|
174
213
|
return HARNESS_GLYPH[key] ?? "□";
|
|
@@ -179,16 +218,11 @@ function harnessLabel(agent: unknown): string {
|
|
|
179
218
|
return HARNESS_LABEL[key] ?? cell(agent);
|
|
180
219
|
}
|
|
181
220
|
|
|
182
|
-
function dot(status: unknown): string {
|
|
183
|
-
return STATUS_DOT[text(status)] ?? "○";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
221
|
function statusWord(status: unknown): string {
|
|
187
222
|
return STATUS_WORD[text(status)] ?? cell(status);
|
|
188
223
|
}
|
|
189
224
|
|
|
190
|
-
function footerLegend(columns: number):
|
|
191
|
-
const fullStatus = " ● working ◉ waiting ○ finished";
|
|
225
|
+
function footerLegend(columns: number): string {
|
|
192
226
|
const fullHarness =
|
|
193
227
|
" " +
|
|
194
228
|
glyph("claude") +
|
|
@@ -199,15 +233,11 @@ function footerLegend(columns: number): [string, string] {
|
|
|
199
233
|
" Codex " +
|
|
200
234
|
glyph("omp") +
|
|
201
235
|
" OMP";
|
|
202
|
-
if (
|
|
203
|
-
return [fullStatus, fullHarness];
|
|
204
|
-
}
|
|
236
|
+
if (fullHarness.length <= columns - 2) return fullHarness;
|
|
205
237
|
|
|
206
|
-
const compactStatus = " ● wk ◉ wait ○ done";
|
|
207
238
|
const iconHarness =
|
|
208
239
|
" " + glyph("claude") + " " + glyph("opencode") + " " + glyph("codex") + " " + glyph("omp");
|
|
209
|
-
|
|
210
|
-
return [" ● ◉ ○", iconHarness];
|
|
240
|
+
return iconHarness;
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
function age(iso: unknown, now: number): string {
|
|
@@ -215,15 +245,51 @@ function age(iso: unknown, now: number): string {
|
|
|
215
245
|
const then = Date.parse(iso);
|
|
216
246
|
if (Number.isNaN(then)) return "";
|
|
217
247
|
const minutes = Math.floor((now - then) / 60000);
|
|
218
|
-
if (minutes < 1) return "
|
|
248
|
+
if (minutes < 1) return "now";
|
|
219
249
|
if (minutes < 60) return `${minutes}m`;
|
|
220
250
|
return `${Math.floor(minutes / 60)}h`;
|
|
221
251
|
}
|
|
222
252
|
|
|
253
|
+
/** Stable elapsed-time text for a card detail line. */
|
|
254
|
+
export function elapsed(ms: number): string {
|
|
255
|
+
if (!Number.isFinite(ms) || ms < 0) return "0s";
|
|
256
|
+
const seconds = Math.floor(ms / 1000);
|
|
257
|
+
if (seconds < 60) return `${seconds}s`;
|
|
258
|
+
const minutes = Math.floor(seconds / 60);
|
|
259
|
+
const paddedSeconds = String(seconds % 60).padStart(2, "0");
|
|
260
|
+
if (minutes < 60) return `${minutes}m ${paddedSeconds}s`;
|
|
261
|
+
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}m ${paddedSeconds}s`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function cardElapsed(row: Partial<PaneRow>, now: number): string {
|
|
265
|
+
if (typeof row.createdAt !== "string") return "";
|
|
266
|
+
const createdAt = Date.parse(row.createdAt);
|
|
267
|
+
if (!Number.isFinite(createdAt)) return "";
|
|
268
|
+
|
|
269
|
+
const live = LIVE.has(row.status ?? "") || WAIT.has(row.status ?? "");
|
|
270
|
+
const endAt = live
|
|
271
|
+
? now
|
|
272
|
+
: typeof row.updatedAt === "string"
|
|
273
|
+
? Date.parse(row.updatedAt)
|
|
274
|
+
: Number.NaN;
|
|
275
|
+
if (!Number.isFinite(endAt)) return "";
|
|
276
|
+
return elapsed(endAt - createdAt);
|
|
277
|
+
}
|
|
278
|
+
|
|
223
279
|
function count(value: unknown): number {
|
|
224
280
|
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
225
281
|
}
|
|
226
282
|
|
|
283
|
+
function detailLine(model: unknown, effort: unknown, status: string, when: string, width: number): string {
|
|
284
|
+
const state = status ? `${status}${when ? ` · ${when}` : ""}` : "";
|
|
285
|
+
const suffix = [text(effort).trim() ? `${cell(effort)} effort` : "", state]
|
|
286
|
+
.filter(Boolean)
|
|
287
|
+
.join(" · ");
|
|
288
|
+
const modelWidth = width - 3 - suffix.length - (suffix ? 3 : 0);
|
|
289
|
+
const modelText = text(model).trim() && modelWidth > 0 ? fit(cell(model), modelWidth).trimEnd() : "";
|
|
290
|
+
return ` ${[modelText, suffix].filter(Boolean).join(" · ")}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
227
293
|
// How old a row looks for eviction: an unparseable timestamp is the oldest
|
|
228
294
|
// possible age, matching how compareRows parks such rows at the end of the
|
|
229
295
|
// display. The orchestrator pins itself to +Infinity so it is the very last
|
|
@@ -233,22 +299,30 @@ function rowAge(iso: unknown): number {
|
|
|
233
299
|
return Number.isNaN(then) ? Number.NEGATIVE_INFINITY : then;
|
|
234
300
|
}
|
|
235
301
|
|
|
236
|
-
// One
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
// block's own lines; the assembled order adds any connector above it.
|
|
240
|
-
type Block = {
|
|
241
|
-
kind: "card" | "history";
|
|
242
|
-
size: number;
|
|
243
|
-
age: number;
|
|
302
|
+
// One card of the lineage tree. `children` holds only the cards drawn under
|
|
303
|
+
// it; finished leaves go to the history section instead of the tree.
|
|
304
|
+
type Card = {
|
|
244
305
|
id: string;
|
|
245
|
-
|
|
246
|
-
|
|
306
|
+
age: number;
|
|
307
|
+
body: (inner: number) => string[];
|
|
308
|
+
parent: Card | undefined;
|
|
309
|
+
children: Card[];
|
|
247
310
|
};
|
|
248
311
|
|
|
312
|
+
function roleLabel(role: unknown): string {
|
|
313
|
+
const clean = cell(role);
|
|
314
|
+
return clean === EMPTY_CELL ? "" : clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
249
317
|
// One drawing pass, with the widths derived from the column budget. Kept inside
|
|
250
318
|
// its own function so formatPane can wrap the whole thing in one guard.
|
|
251
319
|
// `limit` is the usable body height in rows, or undefined for unbounded.
|
|
320
|
+
//
|
|
321
|
+
// The body is a tree: every card hangs from the session that dispatched it,
|
|
322
|
+
// so a reviewer a worker started is drawn under that worker rather than under
|
|
323
|
+
// the run root. A child card joins its parent through an elbow on the card's
|
|
324
|
+
// first body line; the parent's bottom edge carries a tee where the trunk
|
|
325
|
+
// leaves it.
|
|
252
326
|
function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined): string[] {
|
|
253
327
|
const source = (snapshot ?? {}) as Partial<PaneSnapshot>;
|
|
254
328
|
const rows = (Array.isArray(source.rows) ? source.rows : []).map(
|
|
@@ -256,7 +330,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
256
330
|
);
|
|
257
331
|
const orchestrator =
|
|
258
332
|
source.orchestrator && typeof source.orchestrator === "object"
|
|
259
|
-
? (source.orchestrator as Partial<{ agent: string }>)
|
|
333
|
+
? (source.orchestrator as Partial<{ agent: string; model: string; effort: string; role: string }>)
|
|
260
334
|
: undefined;
|
|
261
335
|
const hidden = count(source.hidden);
|
|
262
336
|
const total = count(source.total);
|
|
@@ -265,23 +339,16 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
265
339
|
if (rows.length === 0 && orchestrator === undefined) return [];
|
|
266
340
|
|
|
267
341
|
const workerTotal = rows.length + hidden;
|
|
342
|
+
const isLive = (row: Partial<PaneRow>) => LIVE.has(row.status ?? "") || WAIT.has(row.status ?? "");
|
|
268
343
|
const working = rows.filter((row) => LIVE.has(row.status ?? "")).length;
|
|
269
344
|
const waiting = rows.filter((row) => WAIT.has(row.status ?? "")).length;
|
|
270
345
|
// The overflow is reported by count only, its statuses are not in the
|
|
271
346
|
// snapshot, so it is folded into "finished": the most recently touched
|
|
272
347
|
// sessions are the live ones and stay inside the budget.
|
|
273
348
|
const finished = Math.max(0, workerTotal - working - waiting);
|
|
274
|
-
const finishedRows = rows.filter(
|
|
275
|
-
(row) => !LIVE.has(row.status ?? "") && !WAIT.has(row.status ?? ""),
|
|
276
|
-
);
|
|
277
349
|
|
|
278
350
|
const W = Math.trunc(columns);
|
|
279
351
|
const IN = W - 2;
|
|
280
|
-
// Rule 8: the card box caps at MAX_CARD_COLUMNS no matter how wide the dock
|
|
281
|
-
// gets; the outer frame still spans the full width. The two-space inset the
|
|
282
|
-
// cards have always sat at inside the frame is kept.
|
|
283
|
-
const CARD = Math.min(IN - 4, MAX_CARD_COLUMNS);
|
|
284
|
-
const INNER = Math.max(0, CARD - 2);
|
|
285
352
|
const now = Date.now();
|
|
286
353
|
|
|
287
354
|
// Everything routed through frame* is exactly W wide.
|
|
@@ -291,90 +358,161 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
291
358
|
const frameTop = (label: string) =>
|
|
292
359
|
"┌" + fit(("─ " + label + " ").replace(CONTROL, " "), IN, "─") + "┐";
|
|
293
360
|
|
|
294
|
-
// A
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (orchestrator !== undefined) {
|
|
323
|
-
blocks.push(
|
|
324
|
-
cardBlock(Number.POSITIVE_INFINITY, "", [
|
|
325
|
-
` ${glyph(orchestrator.agent)} ● Orchestrator`,
|
|
326
|
-
` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
|
|
327
|
-
]),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
361
|
+
// Lineage. A row whose parent is not in the snapshot hangs from the root;
|
|
362
|
+
// a parent chain that loops is cut there, so every row is reachable once.
|
|
363
|
+
const byId = new Map<string, Partial<PaneRow>>();
|
|
364
|
+
for (const row of rows) if (typeof row.id === "string" && !byId.has(row.id)) byId.set(row.id, row);
|
|
365
|
+
const declared = (row: Partial<PaneRow>): Partial<PaneRow> | undefined => {
|
|
366
|
+
const id = typeof row.parentId === "string" ? row.parentId : undefined;
|
|
367
|
+
return id !== undefined && id !== row.id ? byId.get(id) : undefined;
|
|
368
|
+
};
|
|
369
|
+
// A row on a parent loop hangs from the root instead.
|
|
370
|
+
const onLoop = (row: Partial<PaneRow>): boolean => {
|
|
371
|
+
const seen = new Set<Partial<PaneRow>>();
|
|
372
|
+
for (let up = declared(row); up !== undefined && !seen.has(up); up = declared(up)) {
|
|
373
|
+
if (up === row) return true;
|
|
374
|
+
seen.add(up);
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
};
|
|
378
|
+
const parentOf = (row: Partial<PaneRow>): Partial<PaneRow> | undefined =>
|
|
379
|
+
onLoop(row) ? undefined : declared(row);
|
|
380
|
+
const ancestors = (row: Partial<PaneRow>): Partial<PaneRow>[] => {
|
|
381
|
+
const chain: Partial<PaneRow>[] = [];
|
|
382
|
+
const seen = new Set<Partial<PaneRow>>([row]);
|
|
383
|
+
for (let up = parentOf(row); up !== undefined && !seen.has(up); up = parentOf(up)) {
|
|
384
|
+
seen.add(up);
|
|
385
|
+
chain.push(up);
|
|
386
|
+
}
|
|
387
|
+
return chain;
|
|
388
|
+
};
|
|
330
389
|
|
|
390
|
+
// A card for every live row and for every ancestor of one, finished or
|
|
391
|
+
// not, so a live reviewer never floats free of the worker that started it.
|
|
392
|
+
const carded = new Set<Partial<PaneRow>>();
|
|
331
393
|
for (const row of rows) {
|
|
394
|
+
if (!isLive(row)) continue;
|
|
395
|
+
carded.add(row);
|
|
396
|
+
for (const up of ancestors(row)) carded.add(up);
|
|
397
|
+
}
|
|
398
|
+
const historyRows = rows.filter((row) => !carded.has(row));
|
|
399
|
+
|
|
400
|
+
const workerBody = (row: Partial<PaneRow>) => (inner: number): string[] => {
|
|
401
|
+
const role = roleLabel(row.role);
|
|
332
402
|
const status = row.status ?? "";
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
403
|
+
const path =
|
|
404
|
+
typeof row.path === "string" && row.path !== ""
|
|
405
|
+
? " " + fitStart(displayPath(cell(row.path)), Math.max(0, inner - 3))
|
|
406
|
+
: undefined;
|
|
407
|
+
return [
|
|
408
|
+
` ${glyph(row.agent)} ${cell(displaySessionId(row.id))} ${role ? `${role} · ` : ""}${harnessLabel(row.agent)}`,
|
|
409
|
+
` ${cell(row.name)}`,
|
|
410
|
+
detailLine(row.model, row.effort, statusWord(status), cardElapsed(row, now), inner),
|
|
411
|
+
...(path === undefined ? [] : [path]),
|
|
412
|
+
];
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const root: Card | undefined =
|
|
416
|
+
orchestrator === undefined
|
|
417
|
+
? undefined
|
|
418
|
+
: {
|
|
419
|
+
id: "",
|
|
420
|
+
age: Number.POSITIVE_INFINITY,
|
|
421
|
+
parent: undefined,
|
|
422
|
+
children: [],
|
|
423
|
+
body: (inner) => [
|
|
424
|
+
` ${glyph(orchestrator.agent)} ${roleLabel(orchestrator.role) || "Orchestrator"}`,
|
|
425
|
+
` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
|
|
426
|
+
...(text(orchestrator.model).trim() || text(orchestrator.effort).trim()
|
|
427
|
+
? [detailLine(orchestrator.model, orchestrator.effort, "", "", inner)]
|
|
428
|
+
: []),
|
|
429
|
+
],
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Rows arrive newest first, so siblings keep that order.
|
|
433
|
+
const cards = new Map<Partial<PaneRow>, Card>();
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
if (!carded.has(row)) continue;
|
|
436
|
+
cards.set(row, { id: cell(row.id), age: rowAge(row.updatedAt), body: workerBody(row), parent: undefined, children: [] });
|
|
346
437
|
}
|
|
438
|
+
const tops: Card[] = root ? [root] : [];
|
|
439
|
+
for (const row of rows) {
|
|
440
|
+
const card = cards.get(row);
|
|
441
|
+
if (!card) continue;
|
|
442
|
+
const chain = ancestors(row);
|
|
443
|
+
const up = chain.length > 0 ? cards.get(chain[0]) : undefined;
|
|
444
|
+
card.parent = up ?? root;
|
|
445
|
+
if (card.parent) card.parent.children.push(card);
|
|
446
|
+
else tops.push(card);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Card width at a depth: capped at MAX_CARD_COLUMNS, shrinking as the tree
|
|
450
|
+
// indents, never so narrow the frame cannot clip it cleanly.
|
|
451
|
+
const cardWidth = (depth: number) => Math.max(10, Math.min(IN - 4 - 4 * depth, MAX_CARD_COLUMNS));
|
|
347
452
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
453
|
+
// A finished worker kept as a card for a live child is not history.
|
|
454
|
+
const historyCount = historyRows.length + hidden;
|
|
455
|
+
const historyLines = (): string[] => {
|
|
456
|
+
if (historyCount === 0) return [];
|
|
457
|
+
const preview = historyRows.slice(0, HISTORY_PREVIEW_ROWS);
|
|
458
|
+
const lines = [` ─ HISTORY · ${historyCount} finished`];
|
|
351
459
|
for (const row of preview) {
|
|
352
460
|
const when = age(row.updatedAt, now);
|
|
353
|
-
|
|
354
|
-
|
|
461
|
+
const up = parentOf(row);
|
|
462
|
+
lines.push(
|
|
463
|
+
` ${glyph(row.agent)} ${cell(displaySessionId(row.id))} ${cell(row.name)}` +
|
|
464
|
+
(up ? ` ← ${cell(displaySessionId(up.id))}` : "") +
|
|
465
|
+
(when === "" ? "" : ` · ${when}`),
|
|
355
466
|
);
|
|
356
467
|
}
|
|
357
|
-
const older =
|
|
358
|
-
if (older > 0)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
kind: "history",
|
|
362
|
-
size: historyLines.length,
|
|
363
|
-
age: rowAge(preview[0]?.updatedAt),
|
|
364
|
-
id: "history",
|
|
365
|
-
count: finished,
|
|
366
|
-
build: (_connects, lead) => [...(lead ? [stem()] : []), ...historyLines.map(frameRow)],
|
|
367
|
-
});
|
|
368
|
-
}
|
|
468
|
+
const older = historyCount - preview.length;
|
|
469
|
+
if (older > 0) lines.push(` +${older} earlier`);
|
|
470
|
+
return lines;
|
|
471
|
+
};
|
|
369
472
|
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
|
|
473
|
+
// Draw the kept part of the tree. `kept` holds the cards still on screen;
|
|
474
|
+
// an evicted card's children were evicted before it, so the tree stays
|
|
475
|
+
// connected.
|
|
476
|
+
const render = (kept: Set<Card>, withHistory: boolean, hiddenTotal: number): string[] => {
|
|
477
|
+
const out: string[] = [];
|
|
478
|
+
// `rails[k]` says whether the ancestor at depth k+1 has a later sibling,
|
|
479
|
+
// which keeps its vertical rail running past this card.
|
|
480
|
+
const walk = (card: Card, depth: number, rails: boolean[], last: boolean) => {
|
|
481
|
+
const width = cardWidth(depth);
|
|
482
|
+
const inner = width - 2;
|
|
483
|
+
const shown = card.children.filter((child) => kept.has(child));
|
|
484
|
+
const lines = [
|
|
485
|
+
"┌" + "─".repeat(inner) + "┐",
|
|
486
|
+
...card.body(inner).map((line) => "│" + fit(line, inner) + "│"),
|
|
487
|
+
shown.length > 0
|
|
488
|
+
? "└─┬" + "─".repeat(Math.max(0, inner - 2)) + "┘"
|
|
489
|
+
: "└" + "─".repeat(inner) + "┘",
|
|
490
|
+
];
|
|
491
|
+
lines.forEach((line, i) => {
|
|
492
|
+
let prefix = rails.map((rail) => (rail ? " │ " : " ")).join("");
|
|
493
|
+
let body = line;
|
|
494
|
+
if (depth > 0) {
|
|
495
|
+
if (i === 0) prefix += " │ ";
|
|
496
|
+
else if (i === 1) {
|
|
497
|
+
prefix += last ? " └─" : " ├─";
|
|
498
|
+
body = "┤" + line.slice(1);
|
|
499
|
+
} else prefix += last ? " " : " │ ";
|
|
500
|
+
}
|
|
501
|
+
out.push(frameRow(" " + prefix + body));
|
|
502
|
+
});
|
|
503
|
+
shown.forEach((child, i) =>
|
|
504
|
+
walk(child, depth + 1, depth > 0 ? [...rails, !last] : rails, i === shown.length - 1),
|
|
505
|
+
);
|
|
506
|
+
};
|
|
507
|
+
for (const top of tops) if (kept.has(top)) walk(top, 0, [], true);
|
|
508
|
+
const history = withHistory ? historyLines() : [];
|
|
509
|
+
if (history.length > 0 && out.length > 0) out.push(frameRow(""));
|
|
510
|
+
out.push(...history.map(frameRow));
|
|
511
|
+
if (hiddenTotal > 0) out.push(frameRow(` +${hiddenTotal} hidden agents`));
|
|
512
|
+
return out;
|
|
513
|
+
};
|
|
375
514
|
|
|
376
|
-
//
|
|
377
|
-
// they are drawn, and they are never the lines that the fit cuts.
|
|
515
|
+
// These frame lines are always drawn when the pane has content.
|
|
378
516
|
const head = [
|
|
379
517
|
frameTop(`Run canvas ${cell(source.runId)}`),
|
|
380
518
|
frameRow(` ${total} sessions · ${working} working`),
|
|
@@ -382,57 +520,48 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
382
520
|
frameSep(),
|
|
383
521
|
frameRow(""),
|
|
384
522
|
];
|
|
385
|
-
const [
|
|
386
|
-
const tail = [
|
|
387
|
-
frameRow(""),
|
|
388
|
-
frameSep(),
|
|
389
|
-
frameRow(statusLegend),
|
|
390
|
-
frameRow(harnessLegend),
|
|
391
|
-
frameBot(),
|
|
392
|
-
];
|
|
523
|
+
const tail = [frameSep(), frameRow(footerLegend(W)), frameBot()];
|
|
393
524
|
const fixed = head.length + tail.length;
|
|
394
|
-
// The hidden line keeps its own stem only when it hangs off a full card.
|
|
395
|
-
const hiddenSize = (list: Block[]): number =>
|
|
396
|
-
list.length > 0 && list[list.length - 1].kind === "card" ? 2 : 1;
|
|
397
525
|
|
|
398
|
-
|
|
526
|
+
const all = new Set<Card>();
|
|
527
|
+
const collect = (card: Card) => {
|
|
528
|
+
all.add(card);
|
|
529
|
+
card.children.forEach(collect);
|
|
530
|
+
};
|
|
531
|
+
tops.forEach(collect);
|
|
532
|
+
|
|
533
|
+
let kept = all;
|
|
534
|
+
let withHistory = true;
|
|
399
535
|
let dropped = 0;
|
|
400
|
-
let
|
|
536
|
+
let hiddenTotal = 0;
|
|
401
537
|
if (limit !== undefined) {
|
|
402
538
|
// Rule 7: the frame alone does not fit, so nothing is drawn at all.
|
|
403
539
|
if (limit < fixed) return [];
|
|
404
|
-
|
|
405
|
-
// first
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
540
|
+
const needed = () => fixed + render(kept, withHistory, 0).length;
|
|
541
|
+
// History leaves first, then cards oldest first, and only a card with no
|
|
542
|
+
// child left on screen, so the tree never loses a link. The root carries
|
|
543
|
+
// +Infinity and is the last to go. The hidden count follows sessions,
|
|
544
|
+
// not rendered lines.
|
|
545
|
+
if (needed() > limit && historyLines().length > 0) {
|
|
546
|
+
withHistory = false;
|
|
547
|
+
dropped += historyCount;
|
|
548
|
+
}
|
|
549
|
+
kept = new Set(all);
|
|
550
|
+
while (needed() > limit && kept.size > 0) {
|
|
551
|
+
const leaves = [...kept].filter((card) => !card.children.some((child) => kept.has(child)));
|
|
552
|
+
leaves.sort((a, b) => a.age - b.age || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
553
|
+
const victim = leaves[0];
|
|
554
|
+
kept.delete(victim);
|
|
555
|
+
if (victim !== root) dropped += 1;
|
|
419
556
|
}
|
|
420
557
|
// A hidden line is useful only when it does not displace a live card.
|
|
421
|
-
|
|
558
|
+
if (dropped > 0 && fixed + render(kept, withHistory, dropped).length <= limit) hiddenTotal = dropped;
|
|
422
559
|
}
|
|
423
560
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const out: string[] = [...head];
|
|
429
|
-
for (let i = 0; i < kept.length; i += 1) {
|
|
430
|
-
const connects = i < kept.length - 1 || hiddenTotal > 0;
|
|
431
|
-
out.push(...kept[i].build(connects, stemAbove(kept, i)));
|
|
432
|
-
}
|
|
433
|
-
if (hiddenTotal > 0) {
|
|
434
|
-
if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
|
|
435
|
-
else out.push(hiddenRow);
|
|
561
|
+
const out: string[] = [...head, ...render(kept, withHistory, hiddenTotal)];
|
|
562
|
+
if (limit !== undefined) {
|
|
563
|
+
const spare = limit - out.length - tail.length;
|
|
564
|
+
for (let i = 0; i < spare; i += 1) out.push(frameRow(""));
|
|
436
565
|
}
|
|
437
566
|
out.push(...tail);
|
|
438
567
|
return out;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Shared shapes for the orchestrator agents band.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Parse, select and format share this interface. The optional directory fields
|
|
4
|
+
// are a deliberate exception to the frozen interface, per the agents-pane path
|
|
5
|
+
// decision, so a worker's worktree or cwd can reach its card.
|
|
5
6
|
//
|
|
6
7
|
// SessionRow mirrors one element of `codedeck ps --all --json`. Every field
|
|
7
8
|
// past `id` is optional on purpose: the band reads a CLI it does not version
|
|
@@ -11,10 +12,18 @@ export interface SessionRow {
|
|
|
11
12
|
id: string;
|
|
12
13
|
runId?: string;
|
|
13
14
|
origin?: string | null;
|
|
15
|
+
/** Session that dispatched this one; absent on rows older than lineage. */
|
|
16
|
+
parentId?: string | null;
|
|
17
|
+
role?: string | null;
|
|
14
18
|
name?: string;
|
|
15
19
|
agent?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
effort?: string;
|
|
16
22
|
status?: string;
|
|
17
23
|
updatedAt?: string;
|
|
24
|
+
createdAt?: string;
|
|
25
|
+
worktree?: string | null;
|
|
26
|
+
cwd?: string | null;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
/**
|
|
@@ -26,15 +35,26 @@ export interface PaneRow {
|
|
|
26
35
|
id: string;
|
|
27
36
|
status: string;
|
|
28
37
|
agent: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
effort?: string;
|
|
29
40
|
name: string;
|
|
30
41
|
updatedAt?: string;
|
|
42
|
+
createdAt?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Id of the card this one hangs from, or undefined when it hangs from the
|
|
45
|
+
* run root. A parent outside the run (or a legacy row with none) is folded
|
|
46
|
+
* onto the root so every row stays reachable.
|
|
47
|
+
*/
|
|
48
|
+
parentId?: string;
|
|
49
|
+
role?: string;
|
|
50
|
+
path?: string;
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
/** Everything one drawing of the pane needs, already narrowed to one run. */
|
|
34
54
|
export interface PaneSnapshot {
|
|
35
55
|
runId: string;
|
|
36
56
|
/** The orchestrator's own row, the only one whose origin is "open". */
|
|
37
|
-
orchestrator: { agent: string } | undefined;
|
|
57
|
+
orchestrator: { agent: string; model?: string; effort?: string; role?: string } | undefined;
|
|
38
58
|
rows: PaneRow[];
|
|
39
59
|
/** Rows that matched the run but fell outside the budget. */
|
|
40
60
|
hidden: number;
|