@crouton-kit/crouter 0.3.81 → 0.3.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +3 -3
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -3
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +16 -44
- package/dist/clients/attach/attach-cmd.d.ts +1 -2
- package/dist/clients/attach/attach-cmd.js +960 -961
- package/dist/clients/attach/attach-def.d.ts +2 -0
- package/dist/clients/attach/attach-def.js +93 -0
- package/dist/clients/attach/canvas-panels.d.ts +10 -3
- package/dist/clients/attach/canvas-panels.js +42 -16
- package/dist/clients/attach/chat-view.d.ts +20 -1
- package/dist/clients/attach/chat-view.js +85 -2
- package/dist/commands/__tests__/human.test.js +7 -22
- package/dist/commands/attention.js +31 -30
- package/dist/commands/human/prompts.js +51 -38
- package/dist/commands/human/queue.d.ts +1 -28
- package/dist/commands/human/queue.js +156 -520
- package/dist/commands/human/shared.d.ts +28 -68
- package/dist/commands/human/shared.js +37 -120
- package/dist/commands/human.js +13 -17
- package/dist/commands/node.js +4 -2
- package/dist/commands/surface.js +1 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +98 -0
- package/dist/commands/sys/setup-core.d.ts +22 -0
- package/dist/commands/sys/setup-core.js +62 -0
- package/dist/commands/sys/setup.js +8 -1
- package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
- package/dist/core/__tests__/human-deliver.test.js +332 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
- package/dist/core/__tests__/pid-identity-match.test.js +83 -0
- package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/attention.test.js +126 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.d.ts +16 -14
- package/dist/core/canvas/attention.js +34 -29
- package/dist/core/canvas/boot.d.ts +1 -0
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +3 -3
- package/dist/core/canvas/pid.d.ts +43 -17
- package/dist/core/canvas/pid.js +104 -35
- package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
- package/dist/core/canvas/remote-canvas-source.js +4 -2
- package/dist/core/canvas/render.d.ts +5 -5
- package/dist/core/canvas/render.js +14 -14
- package/dist/core/canvas/source.d.ts +2 -2
- package/dist/core/canvas/source.js +3 -3
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +3 -2
- package/dist/core/preview-registry.js +22 -29
- package/dist/core/profiles/select.js +4 -4
- package/dist/core/runtime/front-door.js +19 -6
- package/dist/core/runtime/package-health.d.ts +13 -0
- package/dist/core/runtime/package-health.js +18 -0
- package/dist/core/runtime/pi-vendored.js +15 -1
- package/dist/core/runtime/placement.d.ts +1 -10
- package/dist/core/runtime/placement.js +43 -26
- package/dist/core/runtime/resume.js +72 -4
- package/dist/core/runtime/session-list-cache.d.ts +22 -2
- package/dist/core/runtime/session-list-cache.js +92 -24
- package/dist/core/runtime/spawn.js +9 -0
- package/dist/core/spawn.d.ts +0 -59
- package/dist/core/spawn.js +5 -163
- package/dist/daemon/crtrd.js +7 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +2 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
- package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
- package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
- package/dist/core/__tests__/human-surface-target.test.js +0 -98
- package/dist/web-client/assets/index--md2ylfi.js +0 -78
- /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
package/dist/core/canvas/pid.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// placement.ts, and crtrd.ts into one (Phase 3 review reuse MINOR-1).
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { readKernelBootId } from './boot.js';
|
|
12
13
|
/** Is `pid` a ZOMBIE (exited, awaiting reap by its parent)? `kill(pid, 0)`
|
|
13
14
|
* reports a zombie as a perfectly normal alive process — its PID is still
|
|
14
15
|
* occupied in the process table — so callers relying on that signal-0 probe
|
|
@@ -60,13 +61,29 @@ export function isPidAlive(pid) {
|
|
|
60
61
|
}
|
|
61
62
|
return !isZombie(pid);
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
/** Matches the kernel `boot_id` UUID shape (`/proc/sys/kernel/random/boot_id`),
|
|
65
|
+
* used to tell a NEW-format identity base (a per-boot UUID) apart from a
|
|
66
|
+
* LEGACY one (a `ps lstart` wall-clock timestamp). A boot_id is a lowercase
|
|
67
|
+
* hex UUID; an `lstart` string is a human-readable date — the two never
|
|
68
|
+
* collide, so this cleanly classifies which comparison rule an identity's
|
|
69
|
+
* base obeys (see `identitiesMatch`). */
|
|
70
|
+
const BOOT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
71
|
+
/** Split a `composeIdentity` fingerprint (`<base>` or `<base>#<ticks>`, where
|
|
72
|
+
* `base` is a per-boot `boot_id` UUID on Linux-with-procfs or the portable
|
|
73
|
+
* `ps lstart` timestamp otherwise) into its coarse base and fine `ticks`
|
|
74
|
+
* (Linux-only jiffies) parts. `#` never appears in a `boot_id` or an `lstart`
|
|
75
|
+
* timestamp, so the first `#` unambiguously separates them. Only a NON-EMPTY,
|
|
76
|
+
* purely-numeric suffix counts as ticks (the shape `procStartTicks` and
|
|
77
|
+
* `composeIdentity` guarantee); a malformed or empty suffix (`A#`, `A#bad`,
|
|
78
|
+
* `A#1#2`) is treated as ABSENT ticks, so the base compare then applies — a
|
|
79
|
+
* garbage suffix must never be trusted as a fine discriminator (review
|
|
80
|
+
* Minor). */
|
|
67
81
|
function splitIdentity(identity) {
|
|
68
82
|
const i = identity.indexOf('#');
|
|
69
|
-
|
|
83
|
+
if (i === -1)
|
|
84
|
+
return { base: identity, ticks: undefined };
|
|
85
|
+
const suffix = identity.slice(i + 1);
|
|
86
|
+
return { base: identity.slice(0, i), ticks: /^\d+$/.test(suffix) ? suffix : undefined };
|
|
70
87
|
}
|
|
71
88
|
/** THE single precision-aware identity compare, used EVERYWHERE two
|
|
72
89
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
@@ -75,32 +92,60 @@ function splitIdentity(identity) {
|
|
|
75
92
|
* drift from this rule (review Major #1, Round 2 of the pid-reuse-liveness
|
|
76
93
|
* fix).
|
|
77
94
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* one capture and `lstart#ticks` on another (a transient `/proc` read
|
|
82
|
-
* failure, or capturing on a platform/container without procfs). A naive
|
|
83
|
-
* `===` then reads that as a reuse (different string) and reports `dead` for
|
|
84
|
-
* a broker that never actually died — the false-DEAD direction, which can
|
|
85
|
-
* make `reviveNode`'s double-launch guard wrongly relaunch a SECOND broker
|
|
86
|
-
* onto a still-live session.
|
|
95
|
+
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
96
|
+
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
97
|
+
* SECOND broker onto a still-live session):
|
|
87
98
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
99
|
+
* 1. `composeIdentity` appends the `#<ticks>` Linux discriminator only when
|
|
100
|
+
* `/proc/<pid>/stat` happens to be readable at capture time — so the SAME
|
|
101
|
+
* live process can be recorded plain `lstart` on one capture and
|
|
102
|
+
* `lstart#ticks` on another (a transient `/proc` read failure, or
|
|
103
|
+
* capturing on a platform/container without procfs). A naive `===` reads
|
|
104
|
+
* that as a reuse (different string) and reports `dead` for a broker that
|
|
105
|
+
* never died.
|
|
106
|
+
* 2. `lstart` is NOT stable for a live process on every host. It is derived
|
|
107
|
+
* from `/proc/stat` btime (wall-clock boot epoch) + starttime jiffies, so
|
|
108
|
+
* a clock correction / suspend-resume shifts btime and re-anchors the
|
|
109
|
+
* SAME live process's `lstart` by seconds between two captures. Observed
|
|
110
|
+
* on Blaxel unikraft guests: pid+ticks identical across probes, `lstart`
|
|
111
|
+
* drifting +81s — a base-first compare then reads the live home-node
|
|
112
|
+
* broker as reused/dead every cycle, driving an uncapped revive loop.
|
|
113
|
+
*
|
|
114
|
+
* A third hazard the base itself once carried: `ticks` (jiffies-since-boot)
|
|
115
|
+
* RESET on every boot and can repeat across boots, so ticks-alone is only
|
|
116
|
+
* safe WITHIN one boot. `composeIdentity` therefore boot-SCOPES the base on
|
|
117
|
+
* Linux-with-procfs — the base IS the kernel `boot_id` UUID — so a
|
|
118
|
+
* new-format identity is `<bootId>#<ticks>` and equality means same boot AND
|
|
119
|
+
* same process. This closes the cross-boot false match at the root, without
|
|
120
|
+
* plumbing boot reconciliation ahead of every caller.
|
|
121
|
+
*
|
|
122
|
+
* Rule: split both on `#`.
|
|
123
|
+
* - BOTH sides carry `#ticks` AND both bases are `boot_id` UUIDs
|
|
124
|
+
* (new-format vs new-format): match iff base AND ticks are equal. A
|
|
125
|
+
* different boot_id with equal ticks is NOT a match (the cross-boot
|
|
126
|
+
* collision).
|
|
127
|
+
* - BOTH sides carry `#ticks` but a base is NOT a boot_id UUID on EITHER
|
|
128
|
+
* side — a LEGACY `lstart#ticks` row recorded before this repo boot-scoped
|
|
129
|
+
* the base, or a Linux host that has procfs ticks but no readable boot_id:
|
|
130
|
+
* ticks DECIDE it alone (the prior landed semantics). This is a NARROW
|
|
131
|
+
* format-migration path so an in-place upgrade never false-kills a live
|
|
132
|
+
* broker whose row still holds `lstart#ticks`; it is NOT a general
|
|
133
|
+
* fallback — new-format vs new-format always demands base+ticks above.
|
|
134
|
+
* - EITHER side lacks a valid `#ticks` suffix (macOS / no procfs / transient
|
|
135
|
+
* `/proc` miss / malformed suffix): fall back to the coarse base —
|
|
136
|
+
* matching iff the bases agree (fail-open to same-process, matching this
|
|
137
|
+
* module's universal discipline: only a POSITIVE mismatch counts as reuse,
|
|
138
|
+
* never an absence of evidence). */
|
|
96
139
|
export function identitiesMatch(a, b) {
|
|
97
140
|
const ia = splitIdentity(a);
|
|
98
141
|
const ib = splitIdentity(b);
|
|
99
|
-
if (ia.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
if (ia.ticks !== undefined && ib.ticks !== undefined) {
|
|
143
|
+
if (BOOT_ID_RE.test(ia.base) && BOOT_ID_RE.test(ib.base)) {
|
|
144
|
+
return ia.base === ib.base && ia.ticks === ib.ticks;
|
|
145
|
+
}
|
|
146
|
+
return ia.ticks === ib.ticks; // legacy lstart#ticks (either side) → ticks-alone migration
|
|
147
|
+
}
|
|
148
|
+
return ia.base === ib.base;
|
|
104
149
|
}
|
|
105
150
|
export function recordedPidLiveness(pid, expectedIdentity) {
|
|
106
151
|
const debug = Boolean(process.env.CRTR_DEBUG_PID_LIVENESS);
|
|
@@ -226,16 +271,40 @@ function procStartTicks(pid) {
|
|
|
226
271
|
return null;
|
|
227
272
|
}
|
|
228
273
|
}
|
|
229
|
-
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
/** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
|
|
275
|
+
* whole lifetime of this process — it only changes on a kernel boot, which
|
|
276
|
+
* necessarily kills this process — so it is read ONCE and cached, rather than
|
|
277
|
+
* re-read for every row of a whole-process-table `ps` scan. `undefined` = not
|
|
278
|
+
* yet read; `null` = read and absent (non-Linux / no procfs). */
|
|
279
|
+
let cachedBootId;
|
|
280
|
+
function bootIdBase() {
|
|
281
|
+
if (cachedBootId === undefined)
|
|
282
|
+
cachedBootId = readKernelBootId();
|
|
283
|
+
return cachedBootId;
|
|
284
|
+
}
|
|
285
|
+
/** Compose the stable, BOOT-SCOPED process-identity fingerprint used
|
|
286
|
+
* everywhere in this module. ALWAYS used to build an identity string, never a
|
|
287
|
+
* raw field directly, so every identity captured anywhere (launch-time
|
|
288
|
+
* `recordPid`, the teardown snapshot, and the escalation-window re-check) is
|
|
289
|
+
* comparable apples-to-apples.
|
|
290
|
+
*
|
|
291
|
+
* Platform capability, not layered fallbacks — the base is the finest STABLE
|
|
292
|
+
* per-boot anchor available:
|
|
293
|
+
* - Linux-with-procfs (the production guest shape): `<bootId>#<ticks>` — the
|
|
294
|
+
* per-boot `boot_id` UUID plus the per-process jiffies-since-boot. Ticks
|
|
295
|
+
* reset per boot and can repeat, so scoping the base to the boot is what
|
|
296
|
+
* makes an equal-ticks compare safe ACROSS boots (see `identitiesMatch`).
|
|
297
|
+
* - Linux-with-procfs but no readable `boot_id` (if that combination
|
|
298
|
+
* exists): `<lstart>#<ticks>` — the prior landed behavior.
|
|
299
|
+
* - No procfs ticks (macOS / containers without procfs): `<lstart>` alone —
|
|
300
|
+
* unchanged coarse behavior. The base is only ever a `boot_id` when a
|
|
301
|
+
* `ticks` discriminator is ALSO present; a bare `boot_id` (shared by every
|
|
302
|
+
* process on the boot) would be a useless per-process identity. */
|
|
236
303
|
function composeIdentity(pid, lstart) {
|
|
237
304
|
const ticks = procStartTicks(pid);
|
|
238
|
-
|
|
305
|
+
if (ticks === null)
|
|
306
|
+
return lstart;
|
|
307
|
+
return `${bootIdBase() ?? lstart}#${ticks}`;
|
|
239
308
|
}
|
|
240
309
|
/** One combined `ps` probe over the WHOLE process table — `pid`, `ppid`, AND
|
|
241
310
|
* `lstart` per row — parsed into BOTH a ppid → children-pid[] map (for
|
|
@@ -57,6 +57,6 @@ export declare class RemoteCanvasSource implements CanvasSource {
|
|
|
57
57
|
subscriptionsOf(id: string): Promise<SubscriptionRef[]>;
|
|
58
58
|
subscribersOf(id: string): Promise<SubscriptionRef[]>;
|
|
59
59
|
view(root: string): Promise<string[]>;
|
|
60
|
-
|
|
60
|
+
ticketCountsForView(root: string): Promise<Record<string, number>>;
|
|
61
61
|
hasActiveLiveSubscription(id: string): Promise<boolean>;
|
|
62
62
|
}
|
|
@@ -12,7 +12,7 @@ const CACHE_TTL_MS = 1000;
|
|
|
12
12
|
// `execJson<T>` parses stdout and casts it to T with NO structural check — a
|
|
13
13
|
// remote subprocess that exits 0 with a wrong shape (e.g. `{}`, `{node:null}`)
|
|
14
14
|
// would otherwise pass through as a "valid" T. `fetchShow`/`listNodes`/
|
|
15
|
-
// `
|
|
15
|
+
// `ticketCountsForView` validate before caching/returning; on a shape mismatch
|
|
16
16
|
// they degrade exactly like the existing network-failure path (null/[]/{}),
|
|
17
17
|
// never a malformed object a caller might dereference.
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
@@ -206,7 +206,9 @@ export class RemoteCanvasSource {
|
|
|
206
206
|
this.viewCache.set(root, { value: out, at: Date.now() });
|
|
207
207
|
return out;
|
|
208
208
|
}
|
|
209
|
-
async
|
|
209
|
+
async ticketCountsForView(root) {
|
|
210
|
+
// The `canvas attention map` transport is unchanged; its `counts` values now
|
|
211
|
+
// mean all pending human tickets (deck/review/notify), not just asks.
|
|
210
212
|
const res = await this.execJson(['canvas', 'attention', 'map', '--view', root]);
|
|
211
213
|
return isPlainObject(res) && isPlainObject(res['counts']) ? res['counts'] : {};
|
|
212
214
|
}
|
|
@@ -126,16 +126,16 @@ export declare function dashboardRowsAll(): DashboardRow[];
|
|
|
126
126
|
export declare function dashboardRowsAllFromSource(source: CanvasSource): Promise<DashboardRow[]>;
|
|
127
127
|
/** Fold the cheap-boot row's deferred fields in for ONE row: the full label
|
|
128
128
|
* (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
|
|
129
|
-
* meta read + one telemetry read + one
|
|
130
|
-
* For a batch, prefer {@link enrichRows} — it scans each cwd's
|
|
129
|
+
* meta read + one telemetry read + one ticket scan. Idempotent (no-op once enriched).
|
|
130
|
+
* For a batch, prefer {@link enrichRows} — it scans each cwd's ticket inbox once. */
|
|
131
131
|
export declare function enrichRow(row: DashboardRow): DashboardRow;
|
|
132
132
|
/** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
|
|
133
|
-
* distinct cwd's
|
|
134
|
-
* Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
133
|
+
* distinct cwd's ticket inbox exactly ONCE (via `ticketCountsForNodes`) instead
|
|
134
|
+
* of per row. Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
135
135
|
export declare function enrichRows(rows: DashboardRow[]): void;
|
|
136
136
|
/** Telemetry (ctx tokens) is a local disk read, so it is suppressed to 0 for a
|
|
137
137
|
* remote source. Ask counts are suppressed to 0 UNLESS the caller supplies
|
|
138
|
-
* `remoteAsks` — a pre-fetched `
|
|
138
|
+
* `remoteAsks` — a pre-fetched `ticketCountsForView` map (one call per forest
|
|
139
139
|
* root; the caller's job, since the right root set is a browse/snapshot
|
|
140
140
|
* concern, not this per-row enrichment loop's). See `runBrowse`'s
|
|
141
141
|
* `remoteAskCounts`, fetched once per snapshot rebuild, not per flush. */
|
|
@@ -18,7 +18,7 @@ import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
|
|
|
18
18
|
import { RemoteCanvasSource } from './remote-canvas-source.js';
|
|
19
19
|
import { fullName } from './labels.js';
|
|
20
20
|
import { jobDir, contextDir } from './paths.js';
|
|
21
|
-
import {
|
|
21
|
+
import { countTickets, ticketCountsForNodes } from './attention.js';
|
|
22
22
|
import { isPidAlive } from './pid.js';
|
|
23
23
|
import { listFocuses } from './focuses.js';
|
|
24
24
|
import { resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
@@ -68,7 +68,7 @@ function nodeLine(nodeId, indent, connector) {
|
|
|
68
68
|
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
69
69
|
const tel = readNodeTelemetry(nodeId);
|
|
70
70
|
const ctx = fmtCtx(tel.tokens_in);
|
|
71
|
-
const asks =
|
|
71
|
+
const asks = countTickets(nodeId);
|
|
72
72
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
73
73
|
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
74
74
|
return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
|
|
@@ -119,7 +119,7 @@ export function renderTree(rootId) {
|
|
|
119
119
|
return `? <missing node: ${rootId}>`;
|
|
120
120
|
const tel = readNodeTelemetry(rootId);
|
|
121
121
|
const ctx = fmtCtx(tel.tokens_in);
|
|
122
|
-
const asks =
|
|
122
|
+
const asks = countTickets(rootId);
|
|
123
123
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
124
124
|
const hanging = hangingFor(node);
|
|
125
125
|
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
@@ -182,7 +182,7 @@ async function nodeLineFromSource(source, nodeId, indent, connector, remote) {
|
|
|
182
182
|
const hanging = remote ? null : hangingFor(node);
|
|
183
183
|
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
184
184
|
const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(nodeId).tokens_in);
|
|
185
|
-
const asks = remote ? 0 :
|
|
185
|
+
const asks = remote ? 0 : countTickets(nodeId);
|
|
186
186
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
187
187
|
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
188
188
|
return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
|
|
@@ -212,7 +212,7 @@ export async function renderTreeFromSource(source, rootId) {
|
|
|
212
212
|
if (node === null)
|
|
213
213
|
return `? <missing node: ${rootId}>`;
|
|
214
214
|
const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(rootId).tokens_in);
|
|
215
|
-
const asks = remote ? 0 :
|
|
215
|
+
const asks = remote ? 0 : countTickets(rootId);
|
|
216
216
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
217
217
|
const hanging = remote ? null : hangingFor(node);
|
|
218
218
|
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
@@ -457,7 +457,7 @@ export function dashboardRows(rootId) {
|
|
|
457
457
|
kind: node.kind,
|
|
458
458
|
mode: node.mode,
|
|
459
459
|
ctx_tokens: tel.tokens_in ?? 0,
|
|
460
|
-
asks:
|
|
460
|
+
asks: countTickets(id),
|
|
461
461
|
cwd: node.cwd,
|
|
462
462
|
created: node.created,
|
|
463
463
|
}];
|
|
@@ -542,8 +542,8 @@ export async function dashboardRowsAllFromSource(source) {
|
|
|
542
542
|
// ---------------------------------------------------------------------------
|
|
543
543
|
/** Fold the cheap-boot row's deferred fields in for ONE row: the full label
|
|
544
544
|
* (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
|
|
545
|
-
* meta read + one telemetry read + one
|
|
546
|
-
* For a batch, prefer {@link enrichRows} — it scans each cwd's
|
|
545
|
+
* meta read + one telemetry read + one ticket scan. Idempotent (no-op once enriched).
|
|
546
|
+
* For a batch, prefer {@link enrichRows} — it scans each cwd's ticket inbox once. */
|
|
547
547
|
export function enrichRow(row) {
|
|
548
548
|
if (row.enriched === true)
|
|
549
549
|
return row;
|
|
@@ -551,18 +551,18 @@ export function enrichRow(row) {
|
|
|
551
551
|
if (meta !== null)
|
|
552
552
|
row.name = fullName(meta);
|
|
553
553
|
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
554
|
-
row.asks =
|
|
554
|
+
row.asks = countTickets(row.node_id);
|
|
555
555
|
row.enriched = true;
|
|
556
556
|
return row;
|
|
557
557
|
}
|
|
558
558
|
/** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
|
|
559
|
-
* distinct cwd's
|
|
560
|
-
* Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
559
|
+
* distinct cwd's ticket inbox exactly ONCE (via `ticketCountsForNodes`) instead
|
|
560
|
+
* of per row. Use this for a whole viewport / the full forest; mutates each row in place. */
|
|
561
561
|
export function enrichRows(rows) {
|
|
562
562
|
const todo = rows.filter((r) => r.enriched !== true);
|
|
563
563
|
if (todo.length === 0)
|
|
564
564
|
return;
|
|
565
|
-
const asks =
|
|
565
|
+
const asks = ticketCountsForNodes(todo.map((r) => r.node_id));
|
|
566
566
|
for (const row of todo) {
|
|
567
567
|
const meta = getNode(row.node_id);
|
|
568
568
|
if (meta !== null)
|
|
@@ -574,7 +574,7 @@ export function enrichRows(rows) {
|
|
|
574
574
|
}
|
|
575
575
|
/** Telemetry (ctx tokens) is a local disk read, so it is suppressed to 0 for a
|
|
576
576
|
* remote source. Ask counts are suppressed to 0 UNLESS the caller supplies
|
|
577
|
-
* `remoteAsks` — a pre-fetched `
|
|
577
|
+
* `remoteAsks` — a pre-fetched `ticketCountsForView` map (one call per forest
|
|
578
578
|
* root; the caller's job, since the right root set is a browse/snapshot
|
|
579
579
|
* concern, not this per-row enrichment loop's). See `runBrowse`'s
|
|
580
580
|
* `remoteAskCounts`, fetched once per snapshot rebuild, not per flush. */
|
|
@@ -583,7 +583,7 @@ export async function enrichRowsFromSource(source, rows, remoteAsks) {
|
|
|
583
583
|
if (todo.length === 0)
|
|
584
584
|
return;
|
|
585
585
|
const remote = source instanceof RemoteCanvasSource;
|
|
586
|
-
const asks = remote ? (remoteAsks ?? {}) :
|
|
586
|
+
const asks = remote ? (remoteAsks ?? {}) : ticketCountsForNodes(todo.map((r) => r.node_id));
|
|
587
587
|
await Promise.all(todo.map(async (row) => {
|
|
588
588
|
const meta = await source.getNode(row.node_id);
|
|
589
589
|
if (meta !== null)
|
|
@@ -9,7 +9,7 @@ export interface CanvasSource {
|
|
|
9
9
|
subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
|
|
10
10
|
subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
|
|
11
11
|
view(root: string): Promise<string[]>;
|
|
12
|
-
|
|
12
|
+
ticketCountsForView(root: string): Promise<Record<string, number>>;
|
|
13
13
|
hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
|
|
14
14
|
}
|
|
15
15
|
export declare class LocalCanvasSource implements CanvasSource {
|
|
@@ -21,7 +21,7 @@ export declare class LocalCanvasSource implements CanvasSource {
|
|
|
21
21
|
subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
|
|
22
22
|
subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
|
|
23
23
|
view(root: string): Promise<string[]>;
|
|
24
|
-
|
|
24
|
+
ticketCountsForView(root: string): Promise<Record<string, number>>;
|
|
25
25
|
hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
|
|
26
26
|
}
|
|
27
27
|
export declare const localCanvasSource: CanvasSource;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNode, getRow, listNodes, subscriptionsOf, subscribersOf, view, hasActiveLiveSubscription, } from './canvas.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ticketCountsForNodes } from './attention.js';
|
|
3
3
|
import { readState } from '../config.js';
|
|
4
4
|
import { resolveRemoteCanvasTarget } from '../view/remote-canvas-target.js';
|
|
5
5
|
import { RemoteCanvasSource } from './remote-canvas-source.js';
|
|
@@ -22,8 +22,8 @@ export class LocalCanvasSource {
|
|
|
22
22
|
async view(root) {
|
|
23
23
|
return view(root);
|
|
24
24
|
}
|
|
25
|
-
async
|
|
26
|
-
return
|
|
25
|
+
async ticketCountsForView(root) {
|
|
26
|
+
return ticketCountsForNodes([root, ...view(root)]);
|
|
27
27
|
}
|
|
28
28
|
async hasActiveLiveSubscription(nodeId) {
|
|
29
29
|
return hasActiveLiveSubscription(nodeId);
|
|
@@ -18,7 +18,7 @@ const REQUIRED_IDS = [
|
|
|
18
18
|
...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
|
|
19
19
|
...ids('crtr.tmux.node', ['next', 'previous']),
|
|
20
20
|
...ids('crtr.tmux.view', ['next', 'previous']),
|
|
21
|
-
...ids('crtr.attach', ['detach', 'clear-or-detach', 'graph.toggle', 'model-ladder.next']),
|
|
21
|
+
...ids('crtr.attach', ['detach', 'clear-or-detach', 'graph.toggle', 'model-ladder.next', 'model-ladder.previous']),
|
|
22
22
|
'crtr.mode.cycle',
|
|
23
23
|
...ids('crtr.graph', ['down', 'up', 'first', 'last', 'collapse-or-manager', 'expand-or-child', 'focus', 'focus-manager', 'close-subtree', 'dismiss']),
|
|
24
24
|
...ids('crtr.graph.confirm', ['accept', 'cancel']),
|
|
@@ -7,8 +7,8 @@ type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child
|
|
|
7
7
|
type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin';
|
|
8
8
|
type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance';
|
|
9
9
|
type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
|
|
10
|
-
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'request-control' | 'release-control' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.settings.${'down' | 'up' | 'cell-previous' | 'cell-next' | 'refresh' | 'quit' | 'edit-or-submit' | 'cancel-edit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}`;
|
|
11
|
-
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
|
|
10
|
+
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'request-control' | 'release-control' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.settings.${'down' | 'up' | 'cell-previous' | 'cell-next' | 'refresh' | 'quit' | 'edit-or-submit' | 'cancel-edit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}`;
|
|
11
|
+
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.attach.model-ladder.previous">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
|
|
12
12
|
export declare const BINDING_IDS: ReadonlySet<string>;
|
|
13
13
|
export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
|
|
14
14
|
export {};
|
|
@@ -45,8 +45,9 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
45
45
|
b('crtr.attach.detach', 'Attach', 'Detach', 'Detach from the node.', ['ctrl+d'], 'host-global', ATTACH_CONTEXTS),
|
|
46
46
|
b('crtr.attach.clear-or-detach', 'Attach', 'Clear or detach', 'Clear input or detach when input is empty.', ['ctrl+c'], 'host-global', ATTACH_CONTEXTS),
|
|
47
47
|
b('crtr.attach.graph.toggle', 'Attach', 'Toggle graph', 'Open or close the graph overlay.', ['alt+g'], 'host-global', ATTACH_CONTEXTS),
|
|
48
|
-
b('crtr.attach.model-ladder.next', 'Attach', 'Next model ladder', 'Move to the next model ladder rung.', ['alt+
|
|
49
|
-
b('crtr.
|
|
48
|
+
b('crtr.attach.model-ladder.next', 'Attach', 'Next model ladder', 'Move to the next model ladder rung.', ['alt+m'], 'host-global', ATTACH_CONTEXTS),
|
|
49
|
+
b('crtr.attach.model-ladder.previous', 'Attach', 'Previous model ladder', 'Move to the previous model ladder rung.', ['alt+shift+m'], 'host-global', ATTACH_CONTEXTS),
|
|
50
|
+
b('crtr.mode.cycle', 'Attach', 'Cycle mode', 'Cycle the active agent mode.', ['alt+shift+p'], 'host-global', ATTACH_CONTEXTS),
|
|
50
51
|
b('crtr.graph.down', 'GRAPH', 'Move down', 'Move to the next graph row.', ['j'], 'terminal', ['graph']),
|
|
51
52
|
b('crtr.graph.up', 'GRAPH', 'Move up', 'Move to the previous graph row.', ['k'], 'terminal', ['graph']),
|
|
52
53
|
b('crtr.graph.first', 'GRAPH', 'First row', 'Move to the first graph row.', ['g'], 'terminal', ['graph']),
|
|
@@ -93,7 +93,6 @@ const LEAF_ICON_CP = {
|
|
|
93
93
|
'human notify': 0xf0f3,
|
|
94
94
|
'human show': 0xf108,
|
|
95
95
|
'human cancel': 0xf00d,
|
|
96
|
-
'human inbox': 0xf01c,
|
|
97
96
|
'human list': 0xf03a,
|
|
98
97
|
'human deck': 0xf0db,
|
|
99
98
|
'human resolve': 0xf00c,
|
|
@@ -1083,29 +1082,27 @@ function truncateLine(line, max) {
|
|
|
1083
1082
|
return line.length > max ? `${line.slice(0, max).trimEnd()}…` : line;
|
|
1084
1083
|
}
|
|
1085
1084
|
const ASK_TEASER_CHARS = 160;
|
|
1086
|
-
/** `crtr human ask` (humanAsk.run, src/commands/human/prompts.ts
|
|
1085
|
+
/** `crtr human ask` (humanAsk.run, src/commands/human/prompts.ts) returns
|
|
1087
1086
|
* `{job_id, dir, follow_up}` — `job_id` is always short (a node id) so it
|
|
1088
1087
|
* always bullets; `dir`/`follow_up` cross the prose threshold depending on
|
|
1089
|
-
* path length, so the shape is recognized by `job_id` plus
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1088
|
+
* path length, so the shape is recognized by `job_id` plus the single FIXED
|
|
1089
|
+
* QUEUED_FOLLOW_UP road-sign (`Queued for the human`, prompts.ts) appearing
|
|
1090
|
+
* anywhere in the text — never by assuming dir/follow_up's bullet vs. prose
|
|
1091
|
+
* form. Arrival is now always just durable queue state (no tmux/non-tmux
|
|
1092
|
+
* drained-vs-delivered fork), so the card collapses to one posted line. The
|
|
1093
|
+
* question text isn't in the output at all (only the bare-question invocation
|
|
1094
|
+
* echoes it), so it's recovered from argv: the first non-flag token after
|
|
1095
|
+
* `human ask`, skipping `--context-file`'s value — absent for a
|
|
1096
|
+
* `--context-file` deck invocation, which still gets the road-sign line with
|
|
1097
|
+
* no question teaser. Returns null when the road-sign is absent (an
|
|
1098
|
+
* error/unrecognized shape). */
|
|
1098
1099
|
function summarizeHumanAsk(text, tokens) {
|
|
1099
1100
|
const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
|
|
1100
1101
|
if (jobId === undefined)
|
|
1101
1102
|
return null;
|
|
1102
|
-
|
|
1103
|
-
const delivered = text.includes('no need to poll');
|
|
1104
|
-
if (!drained && !delivered)
|
|
1103
|
+
if (!text.includes('Queued for the human'))
|
|
1105
1104
|
return null;
|
|
1106
|
-
const lines =
|
|
1107
|
-
? [{ text: 'ask posted — drain via `human inbox`', tone: 'warning' }]
|
|
1108
|
-
: [{ text: 'ask posted → waiting on human', tone: 'accent' }];
|
|
1105
|
+
const lines = [{ text: 'ask posted → waiting on human', tone: 'accent' }];
|
|
1109
1106
|
const question = firstPositionalArg(tokens, 2, new Set(['--context-file']));
|
|
1110
1107
|
if (question !== undefined) {
|
|
1111
1108
|
const firstLine = truncateLine(question.split('\n')[0].trim(), ASK_TEASER_CHARS);
|
|
@@ -1114,26 +1111,22 @@ function summarizeHumanAsk(text, tokens) {
|
|
|
1114
1111
|
}
|
|
1115
1112
|
return lines;
|
|
1116
1113
|
}
|
|
1117
|
-
/** `crtr human review` (humanReview.run, src/commands/human/prompts.ts
|
|
1114
|
+
/** `crtr human review` (humanReview.run, src/commands/human/prompts.ts)
|
|
1118
1115
|
* returns `{job_id, dir, output, follow_up}` — same recognition strategy as
|
|
1119
|
-
* summarizeHumanAsk (job_id bullet +
|
|
1120
|
-
*
|
|
1121
|
-
* isn't in the output either, so its basename is recovered
|
|
1122
|
-
* file positional, skipping `--output`'s value). Returns null
|
|
1123
|
-
*
|
|
1116
|
+
* summarizeHumanAsk (job_id bullet + the single fixed QUEUED_FOLLOW_UP
|
|
1117
|
+
* road-sign, `Queued for the human`, which review shares with ask). The
|
|
1118
|
+
* reviewed file isn't in the output either, so its basename is recovered
|
|
1119
|
+
* from argv (the file positional, skipping `--output`'s value). Returns null
|
|
1120
|
+
* when the road-sign is absent. */
|
|
1124
1121
|
function summarizeHumanReview(text, tokens) {
|
|
1125
1122
|
const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
|
|
1126
1123
|
if (jobId === undefined)
|
|
1127
1124
|
return null;
|
|
1128
|
-
|
|
1129
|
-
const live = text.includes("live on the human's screen for anchored, line-by-line review");
|
|
1130
|
-
if (!drained && !live)
|
|
1125
|
+
if (!text.includes('Queued for the human'))
|
|
1131
1126
|
return null;
|
|
1132
1127
|
const file = firstPositionalArg(tokens, 2, new Set(['--output']));
|
|
1133
1128
|
const label = file !== undefined ? `review posted: ${basename(file)}` : 'review posted';
|
|
1134
|
-
return
|
|
1135
|
-
? [{ text: `${label} — drain via \`human inbox\``, tone: 'warning' }]
|
|
1136
|
-
: [{ text: `${label} → waiting on human`, tone: 'accent' }];
|
|
1129
|
+
return [{ text: `${label} → waiting on human`, tone: 'accent' }];
|
|
1137
1130
|
}
|
|
1138
1131
|
/** `crtr human list` (humanList.run, src/commands/human/queue.ts:283) has no
|
|
1139
1132
|
* custom `render`; the generic renderer bullets `- total: N` then an
|
|
@@ -297,10 +297,10 @@ function rootProfileMessage(cwd) {
|
|
|
297
297
|
* the session IS interactive. Ask on the controlling terminal — create a
|
|
298
298
|
* profile pointing at cwd, or proceed as root — and block until answered.
|
|
299
299
|
* Deliberately a minimal `readline` prompt, not `crtr human ask`: that bridge
|
|
300
|
-
* is asynchronous (
|
|
301
|
-
* and cannot gate pi's boot; the
|
|
302
|
-
*
|
|
303
|
-
*
|
|
300
|
+
* is asynchronous (registers an interaction with humanloop, then waits for the
|
|
301
|
+
* human to answer it from a separate inbox) and cannot gate pi's boot; the
|
|
302
|
+
* humanloop entry points (`human ask`/`review`) are built for a queued,
|
|
303
|
+
* asynchronously-answered interaction, not a two-choice gate on the same TTY pi
|
|
304
304
|
* is about to take over — a raw `readline` question is the actual minimal
|
|
305
305
|
* fit here. */
|
|
306
306
|
/** Explicit `--profile` names a profile whose manifest does NOT cover cwd.
|
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
// over the terminal and the process never returns.
|
|
21
21
|
import { existsSync, statSync } from 'node:fs';
|
|
22
22
|
import { resolve as resolvePath } from 'node:path';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import
|
|
23
|
+
// NOTE: the boot-path deps — ./spawn.js (bootRoot), ./resume.js (resumeRoot),
|
|
24
|
+
// ./package-health.js (missingPackageWarning + piAgentDir) — are DYNAMICALLY
|
|
25
|
+
// imported inside maybeBootRoot, NOT statically here. `cli.ts` calls
|
|
26
|
+
// maybeBootRoot on every invocation, and a static import chain that reaches the
|
|
27
|
+
// pi engine costs ~365ms of cold start that every leaf subcommand (`crtr memory
|
|
28
|
+
// list`, etc.) would pay despite never booting a root. The dynamic imports below
|
|
29
|
+
// are reached only after the early-return guards, i.e. only on a genuine
|
|
30
|
+
// front-door boot. Note we DELIBERATELY do NOT import `@earendil-works/pi-coding-agent`
|
|
31
|
+
// even on the boot path — the one thing we needed from it (getAgentDir) is
|
|
32
|
+
// reimplemented as package-health's `piAgentDir()` so the whole pi engine never
|
|
33
|
+
// loads in the FOREGROUND boot process (the broker loads it in parallel).
|
|
27
34
|
function isDir(p) {
|
|
28
35
|
try {
|
|
29
36
|
return existsSync(p) && statSync(p).isDirectory();
|
|
@@ -123,13 +130,19 @@ export async function maybeBootRoot(root, argv) {
|
|
|
123
130
|
return false;
|
|
124
131
|
}
|
|
125
132
|
// Unambiguous front-door launch → boot a resident root inline (exec pi in
|
|
126
|
-
// this terminal). Does not return.
|
|
133
|
+
// this terminal). Does not return. Load the heavy boot-path deps lazily now
|
|
134
|
+
// that we've committed to booting (see the import note at the top of file).
|
|
135
|
+
const [{ bootRoot }, { resumeRoot }, { missingPackageWarning, piAgentDir }] = await Promise.all([
|
|
136
|
+
import('./spawn.js'),
|
|
137
|
+
import('./resume.js'),
|
|
138
|
+
import('./package-health.js'),
|
|
139
|
+
]);
|
|
127
140
|
const args = parseRootArgs(tokens);
|
|
128
141
|
// Surface any stale/missing local-path pi package BEFORE the viewer takes over
|
|
129
142
|
// the terminal — pi discards a missing package silently, so this is the only
|
|
130
143
|
// moment the user sees why (e.g.) their ~/.claude commands vanished.
|
|
131
144
|
try {
|
|
132
|
-
const warn = missingPackageWarning(
|
|
145
|
+
const warn = missingPackageWarning(piAgentDir());
|
|
133
146
|
if (warn !== null)
|
|
134
147
|
process.stderr.write(`\n${warn}\n\n`);
|
|
135
148
|
}
|