pantheon-opencode 1.5.0-beta.12 → 1.5.0-beta.14
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/CHANGELOG.md +52 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/src/plugins/tui/dist/tui.d.ts +78 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +248 -326
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +267 -303
|
@@ -900,9 +900,9 @@ async function setupUsageBar(api: TuiPluginApi) {
|
|
|
900
900
|
* and the native `task()` tool ALSO spawns a child session with
|
|
901
901
|
* parentID = caller. A child WITHOUT a board report is a native
|
|
902
902
|
* task() child: childrenToDelegationEntries tags it source
|
|
903
|
-
* 'children-only'
|
|
903
|
+
* 'children-only' (distinct info color),
|
|
904
904
|
* so native task() work is visible in the panel alongside board
|
|
905
|
-
* delegates
|
|
905
|
+
* delegates — never filtered out, never duplicated
|
|
906
906
|
* with a board row for the same child (a child WITH a report keeps the
|
|
907
907
|
* md entry, source 'md').
|
|
908
908
|
*
|
|
@@ -984,7 +984,7 @@ export type DelegationEntry = {
|
|
|
984
984
|
// Reintroduce them once nested rows are actually produced.
|
|
985
985
|
/** Internal provenance used to keep a finalized md report authoritative.
|
|
986
986
|
* 'children-only' = a native task() child session with NO board report
|
|
987
|
-
* (
|
|
987
|
+
* (a native child row); 'md' = board report wins;
|
|
988
988
|
* 'board' = read from .pantheon/board/state.json (cross-session FSM). */
|
|
989
989
|
source?: 'child' | 'live' | 'md' | 'children-only' | 'board'
|
|
990
990
|
}
|
|
@@ -1172,7 +1172,7 @@ export async function readAllDelegationEntries(root: string): Promise<Delegation
|
|
|
1172
1172
|
|
|
1173
1173
|
/* ─── Job board channel (4th source: .pantheon/board/state.json) ────────
|
|
1174
1174
|
* `session.children` is scoped to the focused session, so a job running in
|
|
1175
|
-
* ANOTHER session is invisible to it (the "Delegations (0)"
|
|
1175
|
+
* ANOTHER session is invisible to it (the "Delegations (0)" symptom).
|
|
1176
1176
|
* The board state file is the only durable, cross-session source:
|
|
1177
1177
|
* FilePersistence (src/pantheon/file-persistence.ts) stores every
|
|
1178
1178
|
* BackgroundJobRecord there as a JSON array. Reads are fail-open — absent,
|
|
@@ -1302,35 +1302,49 @@ export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry)
|
|
|
1302
1302
|
}
|
|
1303
1303
|
|
|
1304
1304
|
/** Split the panel list: active jobs (running/retry, stale-marked) first,
|
|
1305
|
-
* then the most recent terminal reports
|
|
1306
|
-
*
|
|
1305
|
+
* then the most recent terminal reports; the remaining tail is collapsed
|
|
1306
|
+
* by the View into a single "… +N more" line. Pure — so the history-only panel (no sessionID) is testable
|
|
1307
1307
|
* without the TUI runtime. */
|
|
1308
1308
|
export function splitDelegationList(
|
|
1309
1309
|
all: readonly DelegationEntry[],
|
|
1310
1310
|
maxRecent = 8,
|
|
1311
1311
|
now = Date.now(),
|
|
1312
1312
|
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1313
|
-
): { active: DelegationEntry[]; recent: DelegationEntry[]
|
|
1313
|
+
): { active: DelegationEntry[]; recent: DelegationEntry[] } {
|
|
1314
1314
|
// Active = running OR retry OR the display-only stale-running. A stale row
|
|
1315
1315
|
// is still a live job (backend state unchanged), so it must never fall into
|
|
1316
|
-
// the terminal
|
|
1316
|
+
// the terminal tail — pushing a running delegation out hid the row
|
|
1317
1317
|
// the user launched.
|
|
1318
1318
|
const isActiveState = (st: DelegationEntry['state']): boolean =>
|
|
1319
1319
|
st === 'running' || st === 'retry' || st === 'stale-running'
|
|
1320
1320
|
const active = all
|
|
1321
1321
|
.filter((d) => isActiveState(d.state))
|
|
1322
1322
|
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1323
|
-
|
|
1323
|
+
.sort(compareDelegationEntries)
|
|
1324
|
+
const terminal = all
|
|
1325
|
+
.filter((d) => !isActiveState(d.state))
|
|
1326
|
+
.filter((d) => {
|
|
1327
|
+
// A missing/invalid finalized timestamp is not evidence that a report is
|
|
1328
|
+
// old. Keep it visible rather than accidentally hiding a terminal job.
|
|
1329
|
+
if (d.updatedAt === null || !Number.isFinite(d.updatedAt)) return true
|
|
1330
|
+
const age = now - d.updatedAt
|
|
1331
|
+
if (age < 0) return true
|
|
1332
|
+
const retention =
|
|
1333
|
+
delegationRowStatus(d.state) === 'failed'
|
|
1334
|
+
? DELEGATION_FAILED_RETENTION_MS
|
|
1335
|
+
: DELEGATION_DONE_RETENTION_MS
|
|
1336
|
+
return age < retention
|
|
1337
|
+
})
|
|
1338
|
+
.sort(compareDelegationEntries)
|
|
1324
1339
|
return {
|
|
1325
1340
|
active,
|
|
1326
1341
|
recent: terminal.slice(0, maxRecent),
|
|
1327
|
-
archived: terminal.slice(maxRecent),
|
|
1328
1342
|
}
|
|
1329
1343
|
}
|
|
1330
1344
|
|
|
1331
|
-
/**
|
|
1332
|
-
* existing tests): active jobs first, then the most
|
|
1333
|
-
* reports (capped)
|
|
1345
|
+
/** Live-first window used by {@link ceilingDelegationList} (kept for the
|
|
1346
|
+
* ceiling helper and existing tests): active jobs first, then the most
|
|
1347
|
+
* recent terminal reports (capped). Pure. */
|
|
1334
1348
|
export function visibleDelegationList(
|
|
1335
1349
|
all: readonly DelegationEntry[],
|
|
1336
1350
|
maxTerminal = 8,
|
|
@@ -1349,6 +1363,11 @@ export const STALE_RUNNING_THRESHOLD_MS = 30 * 60 * 1000
|
|
|
1349
1363
|
* display-only `stale-running` state. */
|
|
1350
1364
|
export const IDLE_SILENCE_MS = 60 * 1000
|
|
1351
1365
|
|
|
1366
|
+
/** Visual-only terminal retention windows. Reports remain on disk and in the
|
|
1367
|
+
* board; these constants only control which rows enter the TUI window. */
|
|
1368
|
+
export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000
|
|
1369
|
+
export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000
|
|
1370
|
+
|
|
1352
1371
|
/**
|
|
1353
1372
|
* Mark a running entry as `stale-running` if it has been running longer than
|
|
1354
1373
|
* the threshold AND has no recent activity (no `updatedAt` change in the last
|
|
@@ -1370,7 +1389,7 @@ export function markStaleIfRunning(
|
|
|
1370
1389
|
return { ...entry, state: 'stale-running' as const }
|
|
1371
1390
|
}
|
|
1372
1391
|
|
|
1373
|
-
/** Compact elapsed-time label: "
|
|
1392
|
+
/** Compact elapsed-time label, single unit only: "12s"/"3m"/"1h"/"2d" — ticks every
|
|
1374
1393
|
* second for running jobs. */
|
|
1375
1394
|
export function fmtElapsed(ms: number): string {
|
|
1376
1395
|
const total = Math.max(0, Math.floor(ms / 1000))
|
|
@@ -1378,9 +1397,9 @@ export function fmtElapsed(ms: number): string {
|
|
|
1378
1397
|
const hours = Math.floor((total % 86_400) / 3_600)
|
|
1379
1398
|
const minutes = Math.floor((total % 3_600) / 60)
|
|
1380
1399
|
const seconds = total % 60
|
|
1381
|
-
if (days > 0) return `${days}d
|
|
1382
|
-
if (hours > 0) return `${hours}h
|
|
1383
|
-
if (minutes > 0) return `${minutes}m
|
|
1400
|
+
if (days > 0) return `${days}d`
|
|
1401
|
+
if (hours > 0) return `${hours}h`
|
|
1402
|
+
if (minutes > 0) return `${minutes}m`
|
|
1384
1403
|
return `${seconds}s`
|
|
1385
1404
|
}
|
|
1386
1405
|
|
|
@@ -1451,38 +1470,14 @@ export function delegationSpinnerFrame(now: number): string {
|
|
|
1451
1470
|
* pre-dispatch tool part maps to `running` in {@link reduceDelegationToolPart}. */
|
|
1452
1471
|
export type DelegationDisplayState = DelegationEntry['state']
|
|
1453
1472
|
|
|
1454
|
-
/** Static glyph per display state. `running` shows its base spinner frame;
|
|
1455
|
-
* callers that animate must prefer {@link delegationStateMarker}. Unicode
|
|
1456
|
-
* geometric shapes only (no Nerd Font) — shape is an independent channel
|
|
1457
|
-
* from color, so rows stay legible without color. */
|
|
1458
|
-
export const DELEGATION_STATE_GLYPHS: Readonly<Record<DelegationDisplayState, string>> = {
|
|
1459
|
-
running: '\u280b', // ⠋ base frame of the animated spinner
|
|
1460
|
-
retry: '\u27f3', // ⟳ retrying
|
|
1461
|
-
'stale-running': '\u26a0', // ⚠ stale (display-only alert)
|
|
1462
|
-
completed: '\u2713', // ✓
|
|
1463
|
-
error: '\u2715', // ✕
|
|
1464
|
-
cancelled: '\u2212', // − minus: stopped, distinct from error's ✕
|
|
1465
|
-
startup_failed: '\u26a0', // ⚠ boot failed
|
|
1466
|
-
startup_unknown: '\u26a0', // ⚠ boot outcome unknown
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
export function delegationStateGlyph(state: DelegationDisplayState): string {
|
|
1470
|
-
return DELEGATION_STATE_GLYPHS[state]
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
/** Row marker (`<glyph> `) — the state channel. `running` animates through
|
|
1474
|
-
* {@link delegationSpinnerFrame} for the given tick; every other state is
|
|
1475
|
-
* static. Pure. */
|
|
1476
|
-
export function delegationStateMarker(state: DelegationDisplayState, now = Date.now()): string {
|
|
1477
|
-
const glyph = state === 'running' ? delegationSpinnerFrame(now) : delegationStateGlyph(state)
|
|
1478
|
-
return `${glyph} `
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
1473
|
/** Semantic tone mapped to the TUI theme at the row ({@link DelegationRow}).
|
|
1482
1474
|
* Kept separate + pure so the color channel is testable without booting the
|
|
1483
1475
|
* renderer. */
|
|
1484
1476
|
export type DelegationStateTone = 'warning' | 'error' | 'success' | 'muted'
|
|
1485
1477
|
|
|
1478
|
+
/** Color is the ONLY channel separating `stale-running` from a live `running`
|
|
1479
|
+
* row — the 4-glyph row design has no dedicated stale shape. Display-only;
|
|
1480
|
+
* no behavior change. */
|
|
1486
1481
|
export function delegationStateTone(state: DelegationDisplayState): DelegationStateTone {
|
|
1487
1482
|
switch (state) {
|
|
1488
1483
|
case 'running':
|
|
@@ -1490,6 +1485,7 @@ export function delegationStateTone(state: DelegationDisplayState): DelegationSt
|
|
|
1490
1485
|
case 'startup_unknown':
|
|
1491
1486
|
return 'warning'
|
|
1492
1487
|
case 'stale-running':
|
|
1488
|
+
return 'warning'
|
|
1493
1489
|
case 'error':
|
|
1494
1490
|
case 'startup_failed':
|
|
1495
1491
|
return 'error'
|
|
@@ -1518,10 +1514,6 @@ const latestToolActivity = new Map<string, ToolActivity>()
|
|
|
1518
1514
|
const MAX_TOOL_ACTIVITY_ENTRIES = 200
|
|
1519
1515
|
const TOOL_ACTIVITY_TTL_MS = 5 * 60 * 1000
|
|
1520
1516
|
|
|
1521
|
-
/** Archived-section pagination state — module-level so it survives sidebar
|
|
1522
|
-
* remounts (route changes) without leaking into the component tree. */
|
|
1523
|
-
const archivedUiState = { page: 0 }
|
|
1524
|
-
|
|
1525
1517
|
/** First usable summary string from common tool input fields. */
|
|
1526
1518
|
function summarizeToolInput(input: Record<string, unknown> | undefined): string {
|
|
1527
1519
|
if (!input) return ''
|
|
@@ -1611,6 +1603,17 @@ export function mergeChildDelegationSources(
|
|
|
1611
1603
|
)
|
|
1612
1604
|
continue
|
|
1613
1605
|
const incoming = toDelegationEntry(liveEntry)
|
|
1606
|
+
// A native task() live part never carries a board alias — it is a
|
|
1607
|
+
// children-only row by definition whether it lands on a new row or
|
|
1608
|
+
// upgrades an existing
|
|
1609
|
+
// children-only one. pantheon_delegate pending parts keep source 'live'.
|
|
1610
|
+
const isNativeLive = liveEntry.tool === 'task' && liveEntry.alias === null
|
|
1611
|
+
if (isNativeLive) {
|
|
1612
|
+
// Native task() rows carry no board alias — reuse the synthetic
|
|
1613
|
+
// 'native-task' marker so the single identity falls back to the agent.
|
|
1614
|
+
incoming.source = 'children-only'
|
|
1615
|
+
incoming.alias = 'native-task'
|
|
1616
|
+
}
|
|
1614
1617
|
const index =
|
|
1615
1618
|
(incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
|
|
1616
1619
|
(incoming.alias !== ''
|
|
@@ -1658,7 +1661,10 @@ export function mergeChildDelegationSources(
|
|
|
1658
1661
|
startedAt: Math.min(existing.startedAt, incoming.startedAt),
|
|
1659
1662
|
updatedAt: incoming.updatedAt,
|
|
1660
1663
|
read: incoming.read,
|
|
1661
|
-
|
|
1664
|
+
// A native task() live upgrade must not flip a children-only row to
|
|
1665
|
+
// 'live' (that would drop the children-only provenance); pantheon_delegate
|
|
1666
|
+
// upgrades still take source 'live'.
|
|
1667
|
+
source: isNativeLive ? existing.source : 'live',
|
|
1662
1668
|
}
|
|
1663
1669
|
}
|
|
1664
1670
|
|
|
@@ -1803,7 +1809,7 @@ export function parseDelegationToolPart(
|
|
|
1803
1809
|
// The alias/taskID only exist once the delegate tool COMPLETES (they are
|
|
1804
1810
|
// returned in its output); a running/pending part has neither. Native
|
|
1805
1811
|
// `task` parts never carry a board alias — the children channel supplies
|
|
1806
|
-
// the child session id
|
|
1812
|
+
// the child session id and the row's identity.
|
|
1807
1813
|
let alias: string | null = null
|
|
1808
1814
|
let taskID: string | null = null
|
|
1809
1815
|
if (status === 'completed') {
|
|
@@ -1877,7 +1883,7 @@ export function reduceDelegationToolPart(
|
|
|
1877
1883
|
// job launched (running). A COMPLETED delegate tool only means the job was
|
|
1878
1884
|
// registered — it stays running and we pick up alias + taskID from the
|
|
1879
1885
|
// output. Native `task` parts never yield a board alias, so their rows
|
|
1880
|
-
// stay
|
|
1886
|
+
// stay identity-less until the children channel resolves them.
|
|
1881
1887
|
const existing = map.get(parsed.callID)
|
|
1882
1888
|
if (parsed.status === 'error') {
|
|
1883
1889
|
if (existing !== undefined && existing.state === 'error' && existing.updatedAt === parsed.endAt)
|
|
@@ -1962,7 +1968,7 @@ export function removeDelegationEntry(
|
|
|
1962
1968
|
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1963
1969
|
* child session with parentID = caller — the same mechanism as
|
|
1964
1970
|
* pantheon_delegate — so its parts feed the live-map as the native signal
|
|
1965
|
-
* (rows
|
|
1971
|
+
* (rows come from the children channel). Pure w.r.t. I/O — used
|
|
1966
1972
|
* by the mount re-scan to re-seed the live map after compaction/attach. */
|
|
1967
1973
|
export function collectDelegationToolParts(
|
|
1968
1974
|
messages: readonly { id?: string; parts?: unknown[] }[] | undefined,
|
|
@@ -2135,70 +2141,20 @@ export function mergeBoardDelegationSources(
|
|
|
2135
2141
|
bySessionAlias.set(key(incoming.sessionID, incoming.alias), next)
|
|
2136
2142
|
continue
|
|
2137
2143
|
}
|
|
2138
|
-
out[index] =
|
|
2144
|
+
out[index] = {
|
|
2145
|
+
...incoming,
|
|
2146
|
+
// Session preservation: the board is authoritative for state/alias/agent,
|
|
2147
|
+
// but an EMPTY board sessionID (record without parentSessionID) must
|
|
2148
|
+
// never clobber a known session — otherwise a focused-session row would
|
|
2149
|
+
// lose its attribution. Keep the known session.
|
|
2150
|
+
sessionID: incoming.sessionID !== '' ? incoming.sessionID : (out[index]?.sessionID ?? ''),
|
|
2151
|
+
} // board wins — it is the persisted source of truth
|
|
2139
2152
|
}
|
|
2140
2153
|
|
|
2141
2154
|
out.sort(compareDelegationEntries)
|
|
2142
2155
|
return out
|
|
2143
2156
|
}
|
|
2144
2157
|
|
|
2145
|
-
/* ─── Delegation scope toggle ([Sessão]/[Tudo]) ───────────────────────── */
|
|
2146
|
-
|
|
2147
|
-
/** 'session' = focused session only (default); 'all' = every session the
|
|
2148
|
-
* board knows about. */
|
|
2149
|
-
export type DelegationScope = 'session' | 'all'
|
|
2150
|
-
|
|
2151
|
-
/** api.kv key holding the persisted scope (defaults to 'session'). */
|
|
2152
|
-
export const DELEGATION_SCOPE_KV_KEY = 'delegations.scope'
|
|
2153
|
-
|
|
2154
|
-
/** Structural subset of TuiKV — kept local so the pure helpers are testable
|
|
2155
|
-
* without the TUI runtime. */
|
|
2156
|
-
export type DelegationScopeKv = {
|
|
2157
|
-
get: (key: string, fallback?: unknown) => unknown
|
|
2158
|
-
set: (key: string, value: unknown) => void
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
/** Read the persisted scope; anything malformed/absent → 'session'. */
|
|
2162
|
-
export function readDelegationScope(kv: DelegationScopeKv | undefined): DelegationScope {
|
|
2163
|
-
try {
|
|
2164
|
-
const raw = kv?.get?.(DELEGATION_SCOPE_KV_KEY, 'session')
|
|
2165
|
-
return raw === 'all' ? 'all' : 'session'
|
|
2166
|
-
} catch {
|
|
2167
|
-
return 'session'
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
/** Persist the scope. Failure is non-fatal — the signal keeps it in memory. */
|
|
2172
|
-
export function writeDelegationScope(
|
|
2173
|
-
kv: DelegationScopeKv | undefined,
|
|
2174
|
-
scope: DelegationScope,
|
|
2175
|
-
): void {
|
|
2176
|
-
try {
|
|
2177
|
-
kv?.set?.(DELEGATION_SCOPE_KV_KEY, scope)
|
|
2178
|
-
} catch {
|
|
2179
|
-
/* in-memory only */
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
/** Keyboard/click toggle target. Pure. */
|
|
2184
|
-
export function nextDelegationScope(scope: DelegationScope): DelegationScope {
|
|
2185
|
-
return scope === 'session' ? 'all' : 'session'
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
/** Keep only the focused session's jobs ('session') or all of them ('all').
|
|
2189
|
-
* An empty sessionID marks a current-session child (the children channel is
|
|
2190
|
-
* session-scoped and only fills sessionID from a matching md report), so it
|
|
2191
|
-
* counts as the current session. With no resolved session there is nothing
|
|
2192
|
-
* to scope against — fail-open to the full list. Pure. */
|
|
2193
|
-
export function filterDelegationsByScope(
|
|
2194
|
-
entries: readonly DelegationEntry[],
|
|
2195
|
-
scope: DelegationScope,
|
|
2196
|
-
sessionID: string | null,
|
|
2197
|
-
): DelegationEntry[] {
|
|
2198
|
-
if (scope === 'all' || sessionID === null) return [...entries]
|
|
2199
|
-
return entries.filter((entry) => entry.sessionID === sessionID || entry.sessionID === '')
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
2158
|
/* ─── Children channel (primary source, delegations-sidebar pattern) ────
|
|
2203
2159
|
* `api.client.session.children` returns the child sessions of the current
|
|
2204
2160
|
* session — every pantheon_delegate spawns one (parentID = caller). The
|
|
@@ -2318,37 +2274,72 @@ export function childStatusToState(status: string | undefined): 'running' | 'com
|
|
|
2318
2274
|
return 'running' // busy or unknown
|
|
2319
2275
|
}
|
|
2320
2276
|
|
|
2321
|
-
/**
|
|
2322
|
-
* that split native task() work from pantheon_delegate jobs — the other is
|
|
2323
|
-
* {@link delegationIcon}):
|
|
2324
|
-
*
|
|
2325
|
-
* - `nat:<agent>` — a native task() child (source 'children-only', no board
|
|
2326
|
-
* report). The child session carries no board alias, so the agent IS the
|
|
2327
|
-
* identity.
|
|
2328
|
-
* - `pan:<alias>` — a pantheon_delegate board row (`pan:apo-1`), the same
|
|
2329
|
-
* alias the manager prints in pantheon_delegation_list.
|
|
2277
|
+
/** Status-only row model: ONE glyph + ONE identity per row.
|
|
2330
2278
|
*
|
|
2331
|
-
*
|
|
2332
|
-
* `
|
|
2333
|
-
|
|
2334
|
-
|
|
2279
|
+
* A row is
|
|
2280
|
+
* `{glyph} {alias:7} {elapsed:>5}` plus a muted description line. The short
|
|
2281
|
+
* alias (`apo-1`) is the single identity; rows without a board alias show
|
|
2282
|
+
* the agent instead (never both). */
|
|
2283
|
+
export type DelegationRowStatus = 'active' | 'done' | 'failed' | 'retry'
|
|
2284
|
+
|
|
2285
|
+
/** Map every FSM/display state to one of the 4 row kinds. reconciled never
|
|
2286
|
+
* reaches the panel (the board parser drops it) but reads as done;
|
|
2287
|
+
* cancelled reads as done; startup_failed reads as failed while
|
|
2288
|
+
* startup_unknown (no error known) reads as retry. Pure. */
|
|
2289
|
+
export function delegationRowStatus(state: DelegationDisplayState): DelegationRowStatus {
|
|
2290
|
+
switch (state) {
|
|
2291
|
+
case 'running':
|
|
2292
|
+
case 'stale-running':
|
|
2293
|
+
return 'active'
|
|
2294
|
+
case 'retry':
|
|
2295
|
+
case 'startup_unknown':
|
|
2296
|
+
return 'retry'
|
|
2297
|
+
case 'completed':
|
|
2298
|
+
case 'cancelled':
|
|
2299
|
+
return 'done'
|
|
2300
|
+
case 'error':
|
|
2301
|
+
case 'startup_failed':
|
|
2302
|
+
return 'failed'
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
/** The only 4 glyphs a row may show: animated active, done, failed, retry.
|
|
2307
|
+
* Shape is redundant with color (never the only signal). Pure. */
|
|
2308
|
+
export const DELEGATION_ROW_GLYPHS: Readonly<Record<DelegationRowStatus, string>> = {
|
|
2309
|
+
active: '⠋', // base frame of the animated spinner
|
|
2310
|
+
done: '✓',
|
|
2311
|
+
failed: '✕',
|
|
2312
|
+
retry: '⟳',
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
export function delegationRowGlyph(status: DelegationRowStatus): string {
|
|
2316
|
+
return DELEGATION_ROW_GLYPHS[status]
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/** Row marker (`<glyph> `) — the single state channel. `active` animates
|
|
2320
|
+
* through the 1s spinner for the given tick; every other kind is static.
|
|
2321
|
+
* Pure. */
|
|
2322
|
+
export function delegationRowMarker(state: DelegationDisplayState, now = Date.now()): string {
|
|
2323
|
+
const status = delegationRowStatus(state)
|
|
2324
|
+
const glyph = status === 'active' ? delegationSpinnerFrame(now) : delegationRowGlyph(status)
|
|
2325
|
+
return `${glyph} `
|
|
2335
2326
|
}
|
|
2336
2327
|
|
|
2337
|
-
/**
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
return entry.
|
|
2328
|
+
/** ONE identity per row: the short board alias (`apo-1`); a row without a
|
|
2329
|
+
* board alias (native task() child, alias `native-task`) shows the agent
|
|
2330
|
+
* instead — never alias + agent stacked. Pure. */
|
|
2331
|
+
export function delegationRowIdentity(entry: DelegationEntry): string {
|
|
2332
|
+
if (entry.alias === 'native-task' && entry.agent !== '') return entry.agent
|
|
2333
|
+
return entry.alias
|
|
2343
2334
|
}
|
|
2344
2335
|
|
|
2345
|
-
/**
|
|
2346
|
-
*
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2336
|
+
/** Fixed alias cell, pad-right + hard-truncate to `width` (default 7) so
|
|
2337
|
+
* identities line up across rows. Pure. */
|
|
2338
|
+
export const DELEGATION_ALIAS_WIDTH = 7
|
|
2339
|
+
|
|
2340
|
+
export function formatDelegationAlias(identity: string, width = DELEGATION_ALIAS_WIDTH): string {
|
|
2341
|
+
if (identity.length >= width) return identity.slice(0, width)
|
|
2342
|
+
return `${identity}${' '.repeat(width - identity.length)}`
|
|
2352
2343
|
}
|
|
2353
2344
|
|
|
2354
2345
|
/** Max description width on the row detail line — the old 180-char slice
|
|
@@ -2382,10 +2373,10 @@ export function truncateDelegationDescription(
|
|
|
2382
2373
|
return `${graphemes.slice(0, Math.max(0, max - 1)).join('')}\u2026`
|
|
2383
2374
|
}
|
|
2384
2375
|
|
|
2385
|
-
/** Fixed elapsed-time box: right-aligned to `width` (default
|
|
2386
|
-
* values line up across rows, e.g. `
|
|
2376
|
+
/** Fixed elapsed-time box: right-aligned to `width` (default 5) so elapsed
|
|
2377
|
+
* values line up across rows, e.g. ` 12s`. A label that already fills
|
|
2387
2378
|
* the box is returned untouched. Pure. */
|
|
2388
|
-
export const DELEGATION_ELAPSED_WIDTH =
|
|
2379
|
+
export const DELEGATION_ELAPSED_WIDTH = 5
|
|
2389
2380
|
|
|
2390
2381
|
export function formatDelegationElapsed(
|
|
2391
2382
|
entry: DelegationEntry,
|
|
@@ -2397,10 +2388,11 @@ export function formatDelegationElapsed(
|
|
|
2397
2388
|
return `${' '.repeat(width - label.length)}${label}`
|
|
2398
2389
|
}
|
|
2399
2390
|
|
|
2400
|
-
/**
|
|
2401
|
-
*
|
|
2402
|
-
|
|
2403
|
-
|
|
2391
|
+
/** Colored left half of a row line: `<marker><alias:7> ` — the state glyph
|
|
2392
|
+
* plus the single identity, padded so the muted elapsed column aligns. The
|
|
2393
|
+
* row renders this colored lead separately from the muted elapsed tail. Pure. */
|
|
2394
|
+
export function formatDelegationRowLead(entry: DelegationEntry, marker: string): string {
|
|
2395
|
+
return `${marker}${formatDelegationAlias(delegationRowIdentity(entry))} `
|
|
2404
2396
|
}
|
|
2405
2397
|
|
|
2406
2398
|
// Tree connectors (`isNestedDelegation` / `delegationTreePrefix`) were removed
|
|
@@ -2408,22 +2400,59 @@ export function formatDelegationRow(entry: DelegationEntry, marker: string): str
|
|
|
2408
2400
|
// ever actually nested and the connectors were never populated in production.
|
|
2409
2401
|
// BACKLOG: produce real grandchild rows first, then reintroduce them.
|
|
2410
2402
|
|
|
2411
|
-
/**
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2403
|
+
/** Visible-row ceiling for the panel: the remainder collapses into a single
|
|
2404
|
+
* "… +N more" line. Live rows render first, so a running job is never hidden
|
|
2405
|
+
* by the cap. */
|
|
2406
|
+
export const DELEGATION_VISIBLE_CEILING = 8
|
|
2407
|
+
|
|
2408
|
+
/** Header summary: `(N active · M done)` plus `· K failed` only when K > 0.
|
|
2409
|
+
* Active counts running/retry AND display-only stale-running (a stale row is
|
|
2410
|
+
* still a live job — it must never read as done); failed counts
|
|
2411
|
+
* error/startup_failed; cancelled reads as done. Pure. */
|
|
2414
2412
|
export function formatDelegationHeader(entries: readonly DelegationEntry[]): string {
|
|
2415
2413
|
let active = 0
|
|
2414
|
+
let done = 0
|
|
2415
|
+
let failed = 0
|
|
2416
2416
|
for (const e of entries) {
|
|
2417
|
-
|
|
2417
|
+
switch (delegationRowStatus(e.state)) {
|
|
2418
|
+
case 'active':
|
|
2419
|
+
case 'retry':
|
|
2420
|
+
active++
|
|
2421
|
+
break
|
|
2422
|
+
case 'failed':
|
|
2423
|
+
failed++
|
|
2424
|
+
break
|
|
2425
|
+
case 'done':
|
|
2426
|
+
done++
|
|
2427
|
+
break
|
|
2428
|
+
}
|
|
2418
2429
|
}
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
|
|
2430
|
+
const tail = failed > 0 ? ` · ${failed} failed` : ''
|
|
2431
|
+
return `(${active} active · ${done} done${tail})`
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/** Cap the panel: live rows first, then most-recent retained terminal rows, at
|
|
2435
|
+
* most `maxVisible` total. Hidden counts describe the retained render list,
|
|
2436
|
+
* not expired history. */
|
|
2437
|
+
export function ceilingDelegationList(
|
|
2438
|
+
all: readonly DelegationEntry[],
|
|
2439
|
+
maxVisible = DELEGATION_VISIBLE_CEILING,
|
|
2440
|
+
now = Date.now(),
|
|
2441
|
+
): { visible: DelegationEntry[]; hidden: number; hiddenActive: number; hiddenTerminal: number } {
|
|
2442
|
+
const { active, recent } = splitDelegationList(all, all.length, now)
|
|
2443
|
+
const visibleActive = active.slice(0, maxVisible)
|
|
2444
|
+
const visible = [
|
|
2445
|
+
...visibleActive,
|
|
2446
|
+
...recent.slice(0, Math.max(0, maxVisible - visibleActive.length)),
|
|
2447
|
+
]
|
|
2448
|
+
const hiddenActive = Math.max(0, active.length - visibleActive.length)
|
|
2449
|
+
const hiddenTerminal = Math.max(0, recent.length - (visible.length - visibleActive.length))
|
|
2450
|
+
return { visible, hidden: hiddenActive + hiddenTerminal, hiddenActive, hiddenTerminal }
|
|
2422
2451
|
}
|
|
2423
2452
|
|
|
2424
2453
|
/** Split a display list into native task() rows vs pantheon_delegate rows.
|
|
2425
2454
|
* Native = source 'children-only' (no board report); everything else counts
|
|
2426
|
-
* as pantheon. Pure — powers the
|
|
2455
|
+
* as pantheon. Pure — powers the hooks.log line. */
|
|
2427
2456
|
export function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
2428
2457
|
native: number
|
|
2429
2458
|
pantheon: number
|
|
@@ -2456,18 +2485,22 @@ export function formatPanelLogLine(
|
|
|
2456
2485
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
2457
2486
|
* child of the current session — pantheon_delegate OR the native `task()`
|
|
2458
2487
|
* tool — carries parentID = caller), so it gets source 'children-only',
|
|
2459
|
-
* the internal alias 'native-task'
|
|
2488
|
+
* the internal alias 'native-task' instead of a
|
|
2460
2489
|
* board alias. The 'task nativa' description fallback keeps the row
|
|
2461
2490
|
* non-empty when the child carries no title.
|
|
2462
2491
|
* Terminal md state wins over the derived state; a running md defers to
|
|
2463
2492
|
* the child's live status. A running child is NEVER archived — it always
|
|
2464
2493
|
* lands in the active split (splitDelegationList active = running/retry).
|
|
2494
|
+
* `parentSessionID` (the focused session id) is stamped on report-less
|
|
2495
|
+
* children so the row carries its origin session; omit it and they stay
|
|
2496
|
+
* unscoped ('').
|
|
2465
2497
|
* Sorted running-first (compareDelegationEntries).
|
|
2466
2498
|
* Pure — no I/O. */
|
|
2467
2499
|
export function childrenToDelegationEntries(
|
|
2468
2500
|
children: readonly ChildDelegationLike[] | undefined,
|
|
2469
2501
|
md: readonly DelegationEntry[],
|
|
2470
2502
|
now = Date.now(),
|
|
2503
|
+
parentSessionID = '',
|
|
2471
2504
|
): DelegationEntry[] {
|
|
2472
2505
|
const byTaskID = new Map<string, DelegationEntry>()
|
|
2473
2506
|
for (const m of md) {
|
|
@@ -2485,7 +2518,11 @@ export function childrenToDelegationEntries(
|
|
|
2485
2518
|
: childStatusToState(child.status)
|
|
2486
2519
|
out.push({
|
|
2487
2520
|
alias: mdEntry?.alias ?? 'native-task',
|
|
2488
|
-
|
|
2521
|
+
// A report-less child IS a child of the focused session
|
|
2522
|
+
// (session.children is session-scoped), so the caller-passed
|
|
2523
|
+
// parentSessionID is stamped on it as its origin session; an md
|
|
2524
|
+
// match always carries its own session (the report dir name).
|
|
2525
|
+
sessionID: mdEntry?.sessionID ?? parentSessionID,
|
|
2489
2526
|
taskID: child.id,
|
|
2490
2527
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
2491
2528
|
state,
|
|
@@ -2513,12 +2550,41 @@ export function childrenToDelegationEntries(
|
|
|
2513
2550
|
* server-valid session id ("ses...") ever reaches the router, so an
|
|
2514
2551
|
* unsubstituted "{sessionID}" placeholder can never be routed. */
|
|
2515
2552
|
export function navigateToDelegationSession(
|
|
2516
|
-
route:
|
|
2553
|
+
route:
|
|
2554
|
+
| {
|
|
2555
|
+
navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>
|
|
2556
|
+
}
|
|
2557
|
+
| undefined,
|
|
2517
2558
|
taskID: string | undefined,
|
|
2518
2559
|
): boolean {
|
|
2519
2560
|
if (typeof route?.navigate !== 'function' || !isValidSessionId(taskID)) return false
|
|
2520
|
-
|
|
2521
|
-
|
|
2561
|
+
try {
|
|
2562
|
+
const navigation = route.navigate('session', { sessionID: taskID })
|
|
2563
|
+
if (navigation !== undefined) {
|
|
2564
|
+
// A removed session can reject route.navigate for historical rows. That
|
|
2565
|
+
// rejection is expected and intentionally absorbed so it cannot break
|
|
2566
|
+
// the TUI; the no-op avoids logging sensitive session details.
|
|
2567
|
+
void Promise.resolve(navigation).catch(() => undefined)
|
|
2568
|
+
}
|
|
2569
|
+
return true
|
|
2570
|
+
} catch {
|
|
2571
|
+
// Synchronous router failures are also fail-open for an inert historical row.
|
|
2572
|
+
return false
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/** Build the mouse handler used by each delegation row. */
|
|
2577
|
+
export function createDelegationRowOpenHandler(
|
|
2578
|
+
route:
|
|
2579
|
+
| {
|
|
2580
|
+
navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>
|
|
2581
|
+
}
|
|
2582
|
+
| undefined,
|
|
2583
|
+
taskID: string | undefined,
|
|
2584
|
+
): () => void {
|
|
2585
|
+
return () => {
|
|
2586
|
+
navigateToDelegationSession(route, taskID)
|
|
2587
|
+
}
|
|
2522
2588
|
}
|
|
2523
2589
|
|
|
2524
2590
|
/* ─── Silence-by-default panel logger ──────────────────────
|
|
@@ -2624,7 +2690,7 @@ function SessionRow(props: { api: TuiPluginApi; session: any }) {
|
|
|
2624
2690
|
)
|
|
2625
2691
|
}
|
|
2626
2692
|
|
|
2627
|
-
/* ─── Delegation Row (single job from the children/md channels) ─ */
|
|
2693
|
+
/* ─── Delegation Row (single job from the children/md/board channels) ─ */
|
|
2628
2694
|
|
|
2629
2695
|
function DelegationRow(props: {
|
|
2630
2696
|
api: TuiPluginApi
|
|
@@ -2634,7 +2700,10 @@ function DelegationRow(props: {
|
|
|
2634
2700
|
}) {
|
|
2635
2701
|
const theme = () => props.api.theme.current
|
|
2636
2702
|
|
|
2637
|
-
|
|
2703
|
+
// Color is a SEPARATE channel from the glyph: the lead (glyph + identity)
|
|
2704
|
+
// carries the state tone, while elapsed + description stay muted so the
|
|
2705
|
+
// state reads at a glance without shouting.
|
|
2706
|
+
const tone = createMemo(() => {
|
|
2638
2707
|
const t = theme()
|
|
2639
2708
|
switch (delegationStateTone(props.job.state)) {
|
|
2640
2709
|
case 'warning':
|
|
@@ -2648,16 +2717,10 @@ function DelegationRow(props: {
|
|
|
2648
2717
|
}
|
|
2649
2718
|
})
|
|
2650
2719
|
|
|
2651
|
-
const marker = createMemo(() =>
|
|
2652
|
-
|
|
2653
|
-
// Two visual channels split native task() from pantheon_delegate: the tag
|
|
2654
|
-
// prefix (`nat:`/`pan:`, via delegationTag) and the glyph color — info
|
|
2655
|
-
// (cyan) for native, state color for board rows. The glyph shape (◇/◆) is
|
|
2656
|
-
// the third, color-independent channel.
|
|
2657
|
-
const tagColor = createMemo(() => (props.job.source === 'children-only' ? theme().info : color()))
|
|
2658
|
-
|
|
2659
|
-
const description = createMemo(() => truncateDelegationDescription(props.job.description))
|
|
2720
|
+
const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow))
|
|
2721
|
+
const lead = createMemo(() => formatDelegationRowLead(props.job, marker()))
|
|
2660
2722
|
const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
|
|
2723
|
+
const description = createMemo(() => truncateDelegationDescription(props.job.description))
|
|
2661
2724
|
|
|
2662
2725
|
// beta.6: live tool call the child is running RIGHT NOW ("↳ bash npm test"),
|
|
2663
2726
|
// fed by message.part.updated activity tracking. Null while idle/done.
|
|
@@ -2668,28 +2731,26 @@ function DelegationRow(props: {
|
|
|
2668
2731
|
// Click a row → navigate to the child session (route API guarded in the
|
|
2669
2732
|
// helper). Enter-key binding is left out (per-row keymap wiring is not
|
|
2670
2733
|
// worth the complexity in this sidebar); mouse is enabled in tui.json.
|
|
2671
|
-
const open = ()
|
|
2672
|
-
navigateToDelegationSession(props.api.route, props.job.taskID)
|
|
2673
|
-
}
|
|
2734
|
+
const open = createDelegationRowOpenHandler(props.api.route, props.job.taskID)
|
|
2674
2735
|
|
|
2675
2736
|
return (
|
|
2676
2737
|
<box onMouseDown={open}>
|
|
2738
|
+
{/* Line 1: colored lead (glyph + identity) + muted elapsed tail. */}
|
|
2677
2739
|
<box flexDirection="row">
|
|
2678
|
-
<text
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
<box flexDirection="row" justifyContent={description() !== '' ? 'space-between' : 'flex-end'}>
|
|
2683
|
-
<Show when={description() !== ''}>
|
|
2684
|
-
<text fg={theme().textMuted}>{description()}</text>
|
|
2685
|
-
</Show>
|
|
2686
|
-
<text fg={theme().textMuted}>{elapsed()}</text>
|
|
2740
|
+
<text>
|
|
2741
|
+
<span fg={tone()}>{lead()}</span>
|
|
2742
|
+
<span fg={theme().textMuted}>{elapsed()}</span>
|
|
2743
|
+
</text>
|
|
2687
2744
|
</box>
|
|
2745
|
+
{/* Line 2: the description, indented under the identity and muted. */}
|
|
2746
|
+
<Show when={description() !== ''}>
|
|
2747
|
+
<text fg={theme().textMuted}>{` ${description()}`}</text>
|
|
2748
|
+
</Show>
|
|
2688
2749
|
<Show when={activity()}>
|
|
2689
2750
|
{(a) => (
|
|
2690
2751
|
<text
|
|
2691
2752
|
fg={theme().textMuted}
|
|
2692
|
-
>{`
|
|
2753
|
+
>{` ↳ ${truncateDelegationDescription(`${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`)}`}</text>
|
|
2693
2754
|
)}
|
|
2694
2755
|
</Show>
|
|
2695
2756
|
</box>
|
|
@@ -2804,16 +2865,9 @@ function View(props: {
|
|
|
2804
2865
|
const projectRoot = createMemo(() => resolvePantheonRoot((props.api.state as any).path))
|
|
2805
2866
|
// Silence-by-default panel logger → the REAL <root>/.pantheon/logs/hooks.log.
|
|
2806
2867
|
const panelLog = createTuiLogger(projectRoot())
|
|
2807
|
-
//
|
|
2808
|
-
//
|
|
2809
|
-
//
|
|
2810
|
-
const [delegationScope, setDelegationScope] = createSignal<DelegationScope>(
|
|
2811
|
-
readDelegationScope(props.api.kv as unknown as DelegationScopeKv),
|
|
2812
|
-
)
|
|
2813
|
-
const applyDelegationScope = (scope: DelegationScope) => {
|
|
2814
|
-
setDelegationScope(scope)
|
|
2815
|
-
writeDelegationScope(props.api.kv as unknown as DelegationScopeKv, scope)
|
|
2816
|
-
}
|
|
2868
|
+
// Every job the board knows about is shown — no scope filter ("showing is
|
|
2869
|
+
// what matters"). Live-first ordering keeps running rows on top; the
|
|
2870
|
+
// ceiling collapses the rest into "… +N more".
|
|
2817
2871
|
// The rendered list is childDelegations (children enriched with md);
|
|
2818
2872
|
// there is no separate md-only signal — the md channel only feeds
|
|
2819
2873
|
// childrenToDelegationEntries below.
|
|
@@ -2901,13 +2955,10 @@ function View(props: {
|
|
|
2901
2955
|
}
|
|
2902
2956
|
}
|
|
2903
2957
|
const withStatus = children.map((c) => ({ ...c, status: resolveStatus(c.id) }))
|
|
2904
|
-
|
|
2905
|
-
//
|
|
2906
|
-
//
|
|
2907
|
-
|
|
2908
|
-
for (const entry of childEntries) {
|
|
2909
|
-
if (entry.sessionID === '') entry.sessionID = sessionID
|
|
2910
|
-
}
|
|
2958
|
+
// Report-less rows ARE children of the focused session
|
|
2959
|
+
// (session.children is session-scoped), so the resolved id is stamped on
|
|
2960
|
+
// the row as its origin session.
|
|
2961
|
+
const childEntries = childrenToDelegationEntries(withStatus, md, now(), sessionID)
|
|
2911
2962
|
for (const [k, v] of props.liveStore.map)
|
|
2912
2963
|
if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
|
|
2913
2964
|
props.liveStore.map.delete(k)
|
|
@@ -2942,33 +2993,15 @@ function View(props: {
|
|
|
2942
2993
|
})()
|
|
2943
2994
|
return delegationsInflight
|
|
2944
2995
|
}
|
|
2945
|
-
//
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
const scopedDelegations = createMemo(() =>
|
|
2951
|
-
filterDelegationsByScope(
|
|
2952
|
-
childDelegations(),
|
|
2953
|
-
delegationScope(),
|
|
2954
|
-
resolveCurrentSessionID({ sessionID: props.sessionID, api: props.api }),
|
|
2955
|
-
),
|
|
2996
|
+
// Live-first ceiling: at most DELEGATION_VISIBLE_CEILING rows render; the
|
|
2997
|
+
// remainder collapses into a single "… +N more" line. The header counts the
|
|
2998
|
+
// FULL board list — every session, no scope filter.
|
|
2999
|
+
const delegationCeiling = createMemo(() =>
|
|
3000
|
+
ceilingDelegationList(childDelegations(), DELEGATION_VISIBLE_CEILING, now()),
|
|
2956
3001
|
)
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
...delegationSplit().recent,
|
|
2961
|
-
])
|
|
2962
|
-
// Header summary over the FULL scoped list (active + recent + archived):
|
|
2963
|
-
// active vs done plus the native/pantheon breakdown, so a busy session reads
|
|
2964
|
-
// "Delegations (2 active · 9 done · nat:3 pan:8)".
|
|
2965
|
-
const delegationHeader = createMemo(() => formatDelegationHeader(scopedDelegations()))
|
|
2966
|
-
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2967
|
-
const clampArchivedPage = (page: number, pages: number) => {
|
|
2968
|
-
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
2969
|
-
archivedUiState.page = clamped
|
|
2970
|
-
setArchivedPage(clamped)
|
|
2971
|
-
}
|
|
3002
|
+
// Header counts the same retained rows shown below, so hidden/expired board
|
|
3003
|
+
// history cannot inflate the summary.
|
|
3004
|
+
const delegationHeader = createMemo(() => formatDelegationHeader(delegationCeiling().visible))
|
|
2972
3005
|
|
|
2973
3006
|
// ── Event subscriptions for live session/delegation updates ──
|
|
2974
3007
|
onMount(() => {
|
|
@@ -3047,29 +3080,6 @@ function View(props: {
|
|
|
3047
3080
|
/* re-scan unavailable — children API + 1s poll still cover it */
|
|
3048
3081
|
}
|
|
3049
3082
|
|
|
3050
|
-
// Keyboard shortcut for the scope toggle. `api.keymap` is an optional
|
|
3051
|
-
// peer (@opentui/keymap) — guard the duck-typed call so a degraded/missing
|
|
3052
|
-
// keymap never breaks the sidebar. The clickable [Sessão]/[Tudo] chips are
|
|
3053
|
-
// the always-available fallback.
|
|
3054
|
-
try {
|
|
3055
|
-
const keymap = (props.api as any)?.keymap
|
|
3056
|
-
if (typeof keymap?.registerLayer === 'function') {
|
|
3057
|
-
const off = keymap.registerLayer({
|
|
3058
|
-
priority: 1000,
|
|
3059
|
-
commands: [
|
|
3060
|
-
{
|
|
3061
|
-
name: 'pantheon.delegations.toggleScope',
|
|
3062
|
-
run: () => applyDelegationScope(nextDelegationScope(delegationScope())),
|
|
3063
|
-
},
|
|
3064
|
-
],
|
|
3065
|
-
bindings: [{ key: 'ctrl+shift+d', cmd: 'pantheon.delegations.toggleScope' }],
|
|
3066
|
-
})
|
|
3067
|
-
if (typeof off === 'function') cleanup.push(off)
|
|
3068
|
-
}
|
|
3069
|
-
} catch {
|
|
3070
|
-
/* keymap unavailable — the click toggle still works */
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
3083
|
onCleanup(() =>
|
|
3074
3084
|
cleanup.forEach((fn) => {
|
|
3075
3085
|
fn()
|
|
@@ -3136,31 +3146,13 @@ function View(props: {
|
|
|
3136
3146
|
<text fg={theme().text} attributes={1}>
|
|
3137
3147
|
{`${showDelegations() ? '▼' : '▶'} Delegations`}
|
|
3138
3148
|
</text>
|
|
3139
|
-
<text fg={theme().textMuted}>
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
fg={delegationScope() === 'session' ? theme().accent : theme().textMuted}
|
|
3143
|
-
attributes={delegationScope() === 'session' ? 1 : undefined}
|
|
3144
|
-
onMouseDown={(e: unknown) => {
|
|
3145
|
-
if (typeof e === 'object' && e !== null && 'stopPropagation' in e)
|
|
3146
|
-
(e as { stopPropagation: () => void }).stopPropagation()
|
|
3147
|
-
applyDelegationScope('session')
|
|
3148
|
-
}}
|
|
3149
|
-
>{`[Sessão]`}</text>
|
|
3150
|
-
<text fg={theme().textMuted}>{`/`}</text>
|
|
3151
|
-
<text
|
|
3152
|
-
fg={delegationScope() === 'all' ? theme().accent : theme().textMuted}
|
|
3153
|
-
attributes={delegationScope() === 'all' ? 1 : undefined}
|
|
3154
|
-
onMouseDown={(e: unknown) => {
|
|
3155
|
-
if (typeof e === 'object' && e !== null && 'stopPropagation' in e)
|
|
3156
|
-
(e as { stopPropagation: () => void }).stopPropagation()
|
|
3157
|
-
applyDelegationScope('all')
|
|
3158
|
-
}}
|
|
3159
|
-
>{`[Tudo]`}</text>
|
|
3149
|
+
<text fg={theme().textMuted}>
|
|
3150
|
+
{delegationCeiling().visible.length > 0 ? ` ${delegationHeader()}` : ' — idle'}
|
|
3151
|
+
</text>
|
|
3160
3152
|
</box>
|
|
3161
3153
|
<Show when={showDelegations()}>
|
|
3162
3154
|
<Show
|
|
3163
|
-
when={
|
|
3155
|
+
when={delegationCeiling().visible.length > 0}
|
|
3164
3156
|
fallback={
|
|
3165
3157
|
<box marginLeft={1}>
|
|
3166
3158
|
<text fg={theme().textMuted}>No delegations</text>
|
|
@@ -3168,7 +3160,7 @@ function View(props: {
|
|
|
3168
3160
|
}
|
|
3169
3161
|
>
|
|
3170
3162
|
<box marginLeft={1} flexDirection="column">
|
|
3171
|
-
<For each={
|
|
3163
|
+
<For each={delegationCeiling().visible}>
|
|
3172
3164
|
{(job) => (
|
|
3173
3165
|
<DelegationRow
|
|
3174
3166
|
api={props.api}
|
|
@@ -3178,40 +3170,12 @@ function View(props: {
|
|
|
3178
3170
|
/>
|
|
3179
3171
|
)}
|
|
3180
3172
|
</For>
|
|
3181
|
-
<Show when={
|
|
3182
|
-
{(
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
const slice = archived.slice(page * pageSize, page * pageSize + pageSize)
|
|
3188
|
-
return (
|
|
3189
|
-
<box flexDirection="column">
|
|
3190
|
-
<box flexDirection="row" marginLeft={0}>
|
|
3191
|
-
<text
|
|
3192
|
-
fg={theme().textMuted}
|
|
3193
|
-
onMouseDown={() => clampArchivedPage(page - 1, pages)}
|
|
3194
|
-
>{`\u2039 `}</text>
|
|
3195
|
-
<text fg={theme().textMuted}>{`Archived (${archived.length}) `}</text>
|
|
3196
|
-
<text fg={theme().textMuted}>{`${page + 1}/${pages}`}</text>
|
|
3197
|
-
<text
|
|
3198
|
-
fg={theme().textMuted}
|
|
3199
|
-
onMouseDown={() => clampArchivedPage(page + 1, pages)}
|
|
3200
|
-
>{` \u203a`}</text>
|
|
3201
|
-
</box>
|
|
3202
|
-
<For each={slice}>
|
|
3203
|
-
{(job) => (
|
|
3204
|
-
<DelegationRow
|
|
3205
|
-
api={props.api}
|
|
3206
|
-
job={job}
|
|
3207
|
-
now={now()}
|
|
3208
|
-
animationNow={animationNow()}
|
|
3209
|
-
/>
|
|
3210
|
-
)}
|
|
3211
|
-
</For>
|
|
3212
|
-
</box>
|
|
3213
|
-
)
|
|
3214
|
-
})()}
|
|
3173
|
+
<Show when={delegationCeiling().hidden > 0}>
|
|
3174
|
+
<text fg={theme().textMuted}>
|
|
3175
|
+
{delegationCeiling().hiddenActive > 0
|
|
3176
|
+
? `… +${delegationCeiling().hiddenActive} active`
|
|
3177
|
+
: `… +${delegationCeiling().hiddenTerminal} more`}
|
|
3178
|
+
</text>
|
|
3215
3179
|
</Show>
|
|
3216
3180
|
</box>
|
|
3217
3181
|
</Show>
|