codedeck 0.7.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 +49 -34
- 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 +8 -9
- 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.js +65 -42
- 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 +87 -0
- package/dist/core/claude-transcript.js.map +1 -1
- 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 +73 -50
- 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 +4 -1
- 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/daemon/build-id.js +30 -0
- package/dist/daemon/build-id.js.map +1 -0
- package/dist/daemon/daemon.js +1116 -767
- 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/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-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/pty.js +330 -5
- package/dist/open/pty.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 +33 -8
- package/dist/plugin/mods/agents/pane.ts +259 -136
- package/dist/plugin/mods/agents/types.ts +19 -3
- package/dist/plugin/statusline.sh +22 -34
- package/dist/store/database.js +5 -1
- package/dist/store/database.js.map +1 -1
- package/dist/store/sessions.js +55 -12
- package/dist/store/sessions.js.map +1 -1
- 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
|
@@ -100,8 +100,28 @@ function text(value: unknown): string {
|
|
|
100
100
|
return value;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
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 {
|
|
104
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;
|
|
105
125
|
return {
|
|
106
126
|
id: row.id,
|
|
107
127
|
status: row.status || EMPTY_CELL,
|
|
@@ -110,6 +130,10 @@ function toPaneRow(row: SessionRow): PaneRow {
|
|
|
110
130
|
effort: text(row.effort) || undefined,
|
|
111
131
|
name: row.name || EMPTY_CELL,
|
|
112
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,
|
|
113
137
|
};
|
|
114
138
|
}
|
|
115
139
|
|
|
@@ -118,7 +142,8 @@ function toPaneRow(row: SessionRow): PaneRow {
|
|
|
118
142
|
*
|
|
119
143
|
* Filters to `runId`, lifts the single `origin === "open"` row out as the
|
|
120
144
|
* orchestrator, orders the rest by `updatedAt` descending with a total order,
|
|
121
|
-
* 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
|
|
122
147
|
* `hidden` is 0; dropping rows to fit the pane is formatPane's decision.
|
|
123
148
|
*/
|
|
124
149
|
export function selectPane(rows: SessionRow[], runId: string, budget?: number): PaneSnapshot {
|
|
@@ -134,15 +159,17 @@ export function selectPane(rows: SessionRow[], runId: string, budget?: number):
|
|
|
134
159
|
agent: text(orchestratorRow.agent) || EMPTY_CELL,
|
|
135
160
|
model: text(orchestratorRow.model) || undefined,
|
|
136
161
|
effort: text(orchestratorRow.effort) || undefined,
|
|
162
|
+
role: text(orchestratorRow.role) || undefined,
|
|
137
163
|
}
|
|
138
164
|
: undefined;
|
|
139
165
|
const workers = matching.filter((row) => row.origin !== "open");
|
|
140
166
|
const ordered = [...workers].sort(compareRows);
|
|
141
167
|
const kept = ordered.slice(0, limit);
|
|
168
|
+
const keptIds = new Set(kept.map((row) => row.id));
|
|
142
169
|
return {
|
|
143
170
|
runId: text(runId),
|
|
144
171
|
orchestrator,
|
|
145
|
-
rows: kept.map(toPaneRow),
|
|
172
|
+
rows: kept.map((row) => toPaneRow(row, keptIds)),
|
|
146
173
|
hidden: ordered.length - kept.length,
|
|
147
174
|
total: matching.length,
|
|
148
175
|
};
|
|
@@ -170,6 +197,17 @@ function fit(value: string, width: number, fill = " "): string {
|
|
|
170
197
|
return (cut + "…").padEnd(width, fill);
|
|
171
198
|
}
|
|
172
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
|
+
|
|
173
211
|
function glyph(agent: unknown): string {
|
|
174
212
|
const key = text(agent);
|
|
175
213
|
return HARNESS_GLYPH[key] ?? "□";
|
|
@@ -212,6 +250,32 @@ function age(iso: unknown, now: number): string {
|
|
|
212
250
|
return `${Math.floor(minutes / 60)}h`;
|
|
213
251
|
}
|
|
214
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
|
+
|
|
215
279
|
function count(value: unknown): number {
|
|
216
280
|
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
217
281
|
}
|
|
@@ -235,22 +299,30 @@ function rowAge(iso: unknown): number {
|
|
|
235
299
|
return Number.isNaN(then) ? Number.NEGATIVE_INFINITY : then;
|
|
236
300
|
}
|
|
237
301
|
|
|
238
|
-
// One
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
// block's own lines; the assembled order adds any connector above it.
|
|
242
|
-
type Block = {
|
|
243
|
-
kind: "card" | "history";
|
|
244
|
-
size: number;
|
|
245
|
-
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 = {
|
|
246
305
|
id: string;
|
|
247
|
-
|
|
248
|
-
|
|
306
|
+
age: number;
|
|
307
|
+
body: (inner: number) => string[];
|
|
308
|
+
parent: Card | undefined;
|
|
309
|
+
children: Card[];
|
|
249
310
|
};
|
|
250
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
|
+
|
|
251
317
|
// One drawing pass, with the widths derived from the column budget. Kept inside
|
|
252
318
|
// its own function so formatPane can wrap the whole thing in one guard.
|
|
253
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.
|
|
254
326
|
function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined): string[] {
|
|
255
327
|
const source = (snapshot ?? {}) as Partial<PaneSnapshot>;
|
|
256
328
|
const rows = (Array.isArray(source.rows) ? source.rows : []).map(
|
|
@@ -258,7 +330,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
258
330
|
);
|
|
259
331
|
const orchestrator =
|
|
260
332
|
source.orchestrator && typeof source.orchestrator === "object"
|
|
261
|
-
? (source.orchestrator as Partial<{ agent: string; model: string; effort: string }>)
|
|
333
|
+
? (source.orchestrator as Partial<{ agent: string; model: string; effort: string; role: string }>)
|
|
262
334
|
: undefined;
|
|
263
335
|
const hidden = count(source.hidden);
|
|
264
336
|
const total = count(source.total);
|
|
@@ -267,23 +339,16 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
267
339
|
if (rows.length === 0 && orchestrator === undefined) return [];
|
|
268
340
|
|
|
269
341
|
const workerTotal = rows.length + hidden;
|
|
342
|
+
const isLive = (row: Partial<PaneRow>) => LIVE.has(row.status ?? "") || WAIT.has(row.status ?? "");
|
|
270
343
|
const working = rows.filter((row) => LIVE.has(row.status ?? "")).length;
|
|
271
344
|
const waiting = rows.filter((row) => WAIT.has(row.status ?? "")).length;
|
|
272
345
|
// The overflow is reported by count only, its statuses are not in the
|
|
273
346
|
// snapshot, so it is folded into "finished": the most recently touched
|
|
274
347
|
// sessions are the live ones and stay inside the budget.
|
|
275
348
|
const finished = Math.max(0, workerTotal - working - waiting);
|
|
276
|
-
const finishedRows = rows.filter(
|
|
277
|
-
(row) => !LIVE.has(row.status ?? "") && !WAIT.has(row.status ?? ""),
|
|
278
|
-
);
|
|
279
349
|
|
|
280
350
|
const W = Math.trunc(columns);
|
|
281
351
|
const IN = W - 2;
|
|
282
|
-
// Rule 8: the card box caps at MAX_CARD_COLUMNS no matter how wide the dock
|
|
283
|
-
// gets; the outer frame still spans the full width. The two-space inset the
|
|
284
|
-
// cards have always sat at inside the frame is kept.
|
|
285
|
-
const CARD = Math.min(IN - 4, MAX_CARD_COLUMNS);
|
|
286
|
-
const INNER = Math.max(0, CARD - 2);
|
|
287
352
|
const now = Date.now();
|
|
288
353
|
|
|
289
354
|
// Everything routed through frame* is exactly W wide.
|
|
@@ -293,90 +358,159 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
293
358
|
const frameTop = (label: string) =>
|
|
294
359
|
"┌" + fit(("─ " + label + " ").replace(CONTROL, " "), IN, "─") + "┐";
|
|
295
360
|
|
|
296
|
-
// A
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (orchestrator !== undefined) {
|
|
325
|
-
blocks.push(
|
|
326
|
-
cardBlock(Number.POSITIVE_INFINITY, "", [
|
|
327
|
-
` ${glyph(orchestrator.agent)} Orchestrator`,
|
|
328
|
-
` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
|
|
329
|
-
...(text(orchestrator.model).trim() || text(orchestrator.effort).trim()
|
|
330
|
-
? [detailLine(orchestrator.model, orchestrator.effort, "", "", INNER)]
|
|
331
|
-
: []),
|
|
332
|
-
]),
|
|
333
|
-
);
|
|
334
|
-
}
|
|
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
|
+
};
|
|
335
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>>();
|
|
336
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);
|
|
337
402
|
const status = row.status ?? "";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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: [] });
|
|
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);
|
|
351
447
|
}
|
|
352
448
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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));
|
|
452
|
+
|
|
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`];
|
|
356
459
|
for (const row of preview) {
|
|
357
460
|
const when = age(row.updatedAt, now);
|
|
358
|
-
|
|
359
|
-
|
|
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}`),
|
|
360
466
|
);
|
|
361
467
|
}
|
|
362
|
-
const older =
|
|
363
|
-
if (older > 0)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
kind: "history",
|
|
367
|
-
size: historyLines.length,
|
|
368
|
-
age: rowAge(preview[0]?.updatedAt),
|
|
369
|
-
id: "history",
|
|
370
|
-
count: finished,
|
|
371
|
-
build: (_connects, lead) => [...(lead ? [stem()] : []), ...historyLines.map(frameRow)],
|
|
372
|
-
});
|
|
373
|
-
}
|
|
468
|
+
const older = historyCount - preview.length;
|
|
469
|
+
if (older > 0) lines.push(` +${older} earlier`);
|
|
470
|
+
return lines;
|
|
471
|
+
};
|
|
374
472
|
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
const
|
|
379
|
-
|
|
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
|
+
};
|
|
380
514
|
|
|
381
515
|
// These frame lines are always drawn when the pane has content.
|
|
382
516
|
const head = [
|
|
@@ -386,56 +520,45 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
|
|
|
386
520
|
frameSep(),
|
|
387
521
|
frameRow(""),
|
|
388
522
|
];
|
|
389
|
-
const
|
|
390
|
-
const tail = [
|
|
391
|
-
frameSep(),
|
|
392
|
-
frameRow(harnessLegend),
|
|
393
|
-
frameBot(),
|
|
394
|
-
];
|
|
523
|
+
const tail = [frameSep(), frameRow(footerLegend(W)), frameBot()];
|
|
395
524
|
const fixed = head.length + tail.length;
|
|
396
|
-
// The hidden line keeps its own stem only when it hangs off a full card.
|
|
397
|
-
const hiddenSize = (list: Block[]): number =>
|
|
398
|
-
list.length > 0 && list[list.length - 1].kind === "card" ? 2 : 1;
|
|
399
525
|
|
|
400
|
-
|
|
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;
|
|
401
535
|
let dropped = 0;
|
|
402
|
-
let
|
|
536
|
+
let hiddenTotal = 0;
|
|
403
537
|
if (limit !== undefined) {
|
|
404
538
|
// Rule 7: the frame alone does not fit, so nothing is drawn at all.
|
|
405
539
|
if (limit < fixed) return [];
|
|
406
|
-
|
|
407
|
-
// first
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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;
|
|
421
556
|
}
|
|
422
557
|
// A hidden line is useful only when it does not displace a live card.
|
|
423
|
-
|
|
558
|
+
if (dropped > 0 && fixed + render(kept, withHistory, dropped).length <= limit) hiddenTotal = dropped;
|
|
424
559
|
}
|
|
425
560
|
|
|
426
|
-
|
|
427
|
-
// expose the statuses of rows that were cut before formatting.
|
|
428
|
-
const hiddenTotal = showHidden ? dropped : 0;
|
|
429
|
-
const hiddenRow = frameRow(` +${hiddenTotal} hidden agents`);
|
|
430
|
-
const out: string[] = [...head];
|
|
431
|
-
for (let i = 0; i < kept.length; i += 1) {
|
|
432
|
-
const connects = i < kept.length - 1 || hiddenTotal > 0;
|
|
433
|
-
out.push(...kept[i].build(connects, stemAbove(kept, i)));
|
|
434
|
-
}
|
|
435
|
-
if (hiddenTotal > 0) {
|
|
436
|
-
if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
|
|
437
|
-
else out.push(hiddenRow);
|
|
438
|
-
}
|
|
561
|
+
const out: string[] = [...head, ...render(kept, withHistory, hiddenTotal)];
|
|
439
562
|
if (limit !== undefined) {
|
|
440
563
|
const spare = limit - out.length - tail.length;
|
|
441
564
|
for (let i = 0; i < spare; i += 1) out.push(frameRow(""));
|
|
@@ -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,12 +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;
|
|
16
20
|
model?: string;
|
|
17
21
|
effort?: string;
|
|
18
22
|
status?: string;
|
|
19
23
|
updatedAt?: string;
|
|
24
|
+
createdAt?: string;
|
|
25
|
+
worktree?: string | null;
|
|
26
|
+
cwd?: string | null;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
@@ -32,13 +39,22 @@ export interface PaneRow {
|
|
|
32
39
|
effort?: string;
|
|
33
40
|
name: string;
|
|
34
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;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
/** Everything one drawing of the pane needs, already narrowed to one run. */
|
|
38
54
|
export interface PaneSnapshot {
|
|
39
55
|
runId: string;
|
|
40
56
|
/** The orchestrator's own row, the only one whose origin is "open". */
|
|
41
|
-
orchestrator: { agent: string; model?: string; effort?: string } | undefined;
|
|
57
|
+
orchestrator: { agent: string; model?: string; effort?: string; role?: string } | undefined;
|
|
42
58
|
rows: PaneRow[];
|
|
43
59
|
/** Rows that matched the run but fell outside the budget. */
|
|
44
60
|
hidden: number;
|