pantheon-opencode 1.5.0-beta.12 → 1.5.0-beta.13
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 +31 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/src/plugins/tui/dist/tui.d.ts +67 -76
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +215 -320
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +199 -296
|
@@ -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,18 +1302,18 @@ 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'
|
|
@@ -1324,13 +1324,12 @@ export function splitDelegationList(
|
|
|
1324
1324
|
return {
|
|
1325
1325
|
active,
|
|
1326
1326
|
recent: terminal.slice(0, maxRecent),
|
|
1327
|
-
archived: terminal.slice(maxRecent),
|
|
1328
1327
|
}
|
|
1329
1328
|
}
|
|
1330
1329
|
|
|
1331
|
-
/**
|
|
1332
|
-
* existing tests): active jobs first, then the most
|
|
1333
|
-
* reports (capped)
|
|
1330
|
+
/** Live-first window used by {@link ceilingDelegationList} (kept for the
|
|
1331
|
+
* ceiling helper and existing tests): active jobs first, then the most
|
|
1332
|
+
* recent terminal reports (capped). Pure. */
|
|
1334
1333
|
export function visibleDelegationList(
|
|
1335
1334
|
all: readonly DelegationEntry[],
|
|
1336
1335
|
maxTerminal = 8,
|
|
@@ -1370,7 +1369,7 @@ export function markStaleIfRunning(
|
|
|
1370
1369
|
return { ...entry, state: 'stale-running' as const }
|
|
1371
1370
|
}
|
|
1372
1371
|
|
|
1373
|
-
/** Compact elapsed-time label: "
|
|
1372
|
+
/** Compact elapsed-time label, single unit only: "12s"/"3m"/"1h"/"2d" — ticks every
|
|
1374
1373
|
* second for running jobs. */
|
|
1375
1374
|
export function fmtElapsed(ms: number): string {
|
|
1376
1375
|
const total = Math.max(0, Math.floor(ms / 1000))
|
|
@@ -1378,9 +1377,9 @@ export function fmtElapsed(ms: number): string {
|
|
|
1378
1377
|
const hours = Math.floor((total % 86_400) / 3_600)
|
|
1379
1378
|
const minutes = Math.floor((total % 3_600) / 60)
|
|
1380
1379
|
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
|
|
1380
|
+
if (days > 0) return `${days}d`
|
|
1381
|
+
if (hours > 0) return `${hours}h`
|
|
1382
|
+
if (minutes > 0) return `${minutes}m`
|
|
1384
1383
|
return `${seconds}s`
|
|
1385
1384
|
}
|
|
1386
1385
|
|
|
@@ -1451,38 +1450,14 @@ export function delegationSpinnerFrame(now: number): string {
|
|
|
1451
1450
|
* pre-dispatch tool part maps to `running` in {@link reduceDelegationToolPart}. */
|
|
1452
1451
|
export type DelegationDisplayState = DelegationEntry['state']
|
|
1453
1452
|
|
|
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
1453
|
/** Semantic tone mapped to the TUI theme at the row ({@link DelegationRow}).
|
|
1482
1454
|
* Kept separate + pure so the color channel is testable without booting the
|
|
1483
1455
|
* renderer. */
|
|
1484
1456
|
export type DelegationStateTone = 'warning' | 'error' | 'success' | 'muted'
|
|
1485
1457
|
|
|
1458
|
+
/** Color is the ONLY channel separating `stale-running` from a live `running`
|
|
1459
|
+
* row — the 4-glyph row design has no dedicated stale shape. Display-only;
|
|
1460
|
+
* no behavior change. */
|
|
1486
1461
|
export function delegationStateTone(state: DelegationDisplayState): DelegationStateTone {
|
|
1487
1462
|
switch (state) {
|
|
1488
1463
|
case 'running':
|
|
@@ -1518,10 +1493,6 @@ const latestToolActivity = new Map<string, ToolActivity>()
|
|
|
1518
1493
|
const MAX_TOOL_ACTIVITY_ENTRIES = 200
|
|
1519
1494
|
const TOOL_ACTIVITY_TTL_MS = 5 * 60 * 1000
|
|
1520
1495
|
|
|
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
1496
|
/** First usable summary string from common tool input fields. */
|
|
1526
1497
|
function summarizeToolInput(input: Record<string, unknown> | undefined): string {
|
|
1527
1498
|
if (!input) return ''
|
|
@@ -1611,6 +1582,17 @@ export function mergeChildDelegationSources(
|
|
|
1611
1582
|
)
|
|
1612
1583
|
continue
|
|
1613
1584
|
const incoming = toDelegationEntry(liveEntry)
|
|
1585
|
+
// A native task() live part never carries a board alias — it is a
|
|
1586
|
+
// children-only row by definition whether it lands on a new row or
|
|
1587
|
+
// upgrades an existing
|
|
1588
|
+
// children-only one. pantheon_delegate pending parts keep source 'live'.
|
|
1589
|
+
const isNativeLive = liveEntry.tool === 'task' && liveEntry.alias === null
|
|
1590
|
+
if (isNativeLive) {
|
|
1591
|
+
// Native task() rows carry no board alias — reuse the synthetic
|
|
1592
|
+
// 'native-task' marker so the single identity falls back to the agent.
|
|
1593
|
+
incoming.source = 'children-only'
|
|
1594
|
+
incoming.alias = 'native-task'
|
|
1595
|
+
}
|
|
1614
1596
|
const index =
|
|
1615
1597
|
(incoming.taskID !== undefined ? byTask.get(incoming.taskID) : undefined) ??
|
|
1616
1598
|
(incoming.alias !== ''
|
|
@@ -1658,7 +1640,10 @@ export function mergeChildDelegationSources(
|
|
|
1658
1640
|
startedAt: Math.min(existing.startedAt, incoming.startedAt),
|
|
1659
1641
|
updatedAt: incoming.updatedAt,
|
|
1660
1642
|
read: incoming.read,
|
|
1661
|
-
|
|
1643
|
+
// A native task() live upgrade must not flip a children-only row to
|
|
1644
|
+
// 'live' (that would drop the children-only provenance); pantheon_delegate
|
|
1645
|
+
// upgrades still take source 'live'.
|
|
1646
|
+
source: isNativeLive ? existing.source : 'live',
|
|
1662
1647
|
}
|
|
1663
1648
|
}
|
|
1664
1649
|
|
|
@@ -1803,7 +1788,7 @@ export function parseDelegationToolPart(
|
|
|
1803
1788
|
// The alias/taskID only exist once the delegate tool COMPLETES (they are
|
|
1804
1789
|
// returned in its output); a running/pending part has neither. Native
|
|
1805
1790
|
// `task` parts never carry a board alias — the children channel supplies
|
|
1806
|
-
// the child session id
|
|
1791
|
+
// the child session id and the row's identity.
|
|
1807
1792
|
let alias: string | null = null
|
|
1808
1793
|
let taskID: string | null = null
|
|
1809
1794
|
if (status === 'completed') {
|
|
@@ -1877,7 +1862,7 @@ export function reduceDelegationToolPart(
|
|
|
1877
1862
|
// job launched (running). A COMPLETED delegate tool only means the job was
|
|
1878
1863
|
// registered — it stays running and we pick up alias + taskID from the
|
|
1879
1864
|
// output. Native `task` parts never yield a board alias, so their rows
|
|
1880
|
-
// stay
|
|
1865
|
+
// stay identity-less until the children channel resolves them.
|
|
1881
1866
|
const existing = map.get(parsed.callID)
|
|
1882
1867
|
if (parsed.status === 'error') {
|
|
1883
1868
|
if (existing !== undefined && existing.state === 'error' && existing.updatedAt === parsed.endAt)
|
|
@@ -1962,7 +1947,7 @@ export function removeDelegationEntry(
|
|
|
1962
1947
|
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1963
1948
|
* child session with parentID = caller — the same mechanism as
|
|
1964
1949
|
* pantheon_delegate — so its parts feed the live-map as the native signal
|
|
1965
|
-
* (rows
|
|
1950
|
+
* (rows come from the children channel). Pure w.r.t. I/O — used
|
|
1966
1951
|
* by the mount re-scan to re-seed the live map after compaction/attach. */
|
|
1967
1952
|
export function collectDelegationToolParts(
|
|
1968
1953
|
messages: readonly { id?: string; parts?: unknown[] }[] | undefined,
|
|
@@ -2135,70 +2120,20 @@ export function mergeBoardDelegationSources(
|
|
|
2135
2120
|
bySessionAlias.set(key(incoming.sessionID, incoming.alias), next)
|
|
2136
2121
|
continue
|
|
2137
2122
|
}
|
|
2138
|
-
out[index] =
|
|
2123
|
+
out[index] = {
|
|
2124
|
+
...incoming,
|
|
2125
|
+
// Session preservation: the board is authoritative for state/alias/agent,
|
|
2126
|
+
// but an EMPTY board sessionID (record without parentSessionID) must
|
|
2127
|
+
// never clobber a known session — otherwise a focused-session row would
|
|
2128
|
+
// lose its attribution. Keep the known session.
|
|
2129
|
+
sessionID: incoming.sessionID !== '' ? incoming.sessionID : (out[index]?.sessionID ?? ''),
|
|
2130
|
+
} // board wins — it is the persisted source of truth
|
|
2139
2131
|
}
|
|
2140
2132
|
|
|
2141
2133
|
out.sort(compareDelegationEntries)
|
|
2142
2134
|
return out
|
|
2143
2135
|
}
|
|
2144
2136
|
|
|
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
2137
|
/* ─── Children channel (primary source, delegations-sidebar pattern) ────
|
|
2203
2138
|
* `api.client.session.children` returns the child sessions of the current
|
|
2204
2139
|
* session — every pantheon_delegate spawns one (parentID = caller). The
|
|
@@ -2318,37 +2253,72 @@ export function childStatusToState(status: string | undefined): 'running' | 'com
|
|
|
2318
2253
|
return 'running' // busy or unknown
|
|
2319
2254
|
}
|
|
2320
2255
|
|
|
2321
|
-
/**
|
|
2322
|
-
* that split native task() work from pantheon_delegate jobs — the other is
|
|
2323
|
-
* {@link delegationIcon}):
|
|
2256
|
+
/** Status-only row model: ONE glyph + ONE identity per row.
|
|
2324
2257
|
*
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
*
|
|
2333
|
-
|
|
2334
|
-
|
|
2258
|
+
* A row is
|
|
2259
|
+
* `{glyph} {alias:7} {elapsed:>5}` plus a muted description line. The short
|
|
2260
|
+
* alias (`apo-1`) is the single identity; rows without a board alias show
|
|
2261
|
+
* the agent instead (never both). */
|
|
2262
|
+
export type DelegationRowStatus = 'active' | 'done' | 'failed' | 'retry'
|
|
2263
|
+
|
|
2264
|
+
/** Map every FSM/display state to one of the 4 row kinds. reconciled never
|
|
2265
|
+
* reaches the panel (the board parser drops it) but reads as done;
|
|
2266
|
+
* cancelled reads as done; startup_failed reads as failed while
|
|
2267
|
+
* startup_unknown (no error known) reads as retry. Pure. */
|
|
2268
|
+
export function delegationRowStatus(state: DelegationDisplayState): DelegationRowStatus {
|
|
2269
|
+
switch (state) {
|
|
2270
|
+
case 'running':
|
|
2271
|
+
case 'stale-running':
|
|
2272
|
+
return 'active'
|
|
2273
|
+
case 'retry':
|
|
2274
|
+
case 'startup_unknown':
|
|
2275
|
+
return 'retry'
|
|
2276
|
+
case 'completed':
|
|
2277
|
+
case 'cancelled':
|
|
2278
|
+
return 'done'
|
|
2279
|
+
case 'error':
|
|
2280
|
+
case 'startup_failed':
|
|
2281
|
+
return 'failed'
|
|
2282
|
+
}
|
|
2335
2283
|
}
|
|
2336
2284
|
|
|
2337
|
-
/**
|
|
2338
|
-
*
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2285
|
+
/** The only 4 glyphs a row may show: animated active, done, failed, retry.
|
|
2286
|
+
* Shape is redundant with color (never the only signal). Pure. */
|
|
2287
|
+
export const DELEGATION_ROW_GLYPHS: Readonly<Record<DelegationRowStatus, string>> = {
|
|
2288
|
+
active: '⠋', // base frame of the animated spinner
|
|
2289
|
+
done: '✓',
|
|
2290
|
+
failed: '✕',
|
|
2291
|
+
retry: '⟳',
|
|
2343
2292
|
}
|
|
2344
2293
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2294
|
+
export function delegationRowGlyph(status: DelegationRowStatus): string {
|
|
2295
|
+
return DELEGATION_ROW_GLYPHS[status]
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/** Row marker (`<glyph> `) — the single state channel. `active` animates
|
|
2299
|
+
* through the 1s spinner for the given tick; every other kind is static.
|
|
2300
|
+
* Pure. */
|
|
2301
|
+
export function delegationRowMarker(state: DelegationDisplayState, now = Date.now()): string {
|
|
2302
|
+
const status = delegationRowStatus(state)
|
|
2303
|
+
const glyph = status === 'active' ? delegationSpinnerFrame(now) : delegationRowGlyph(status)
|
|
2304
|
+
return `${glyph} `
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** ONE identity per row: the short board alias (`apo-1`); a row without a
|
|
2308
|
+
* board alias (native task() child, alias `native-task`) shows the agent
|
|
2309
|
+
* instead — never alias + agent stacked. Pure. */
|
|
2310
|
+
export function delegationRowIdentity(entry: DelegationEntry): string {
|
|
2311
|
+
if (entry.alias === 'native-task' && entry.agent !== '') return entry.agent
|
|
2312
|
+
return entry.alias
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/** Fixed alias cell, pad-right + hard-truncate to `width` (default 7) so
|
|
2316
|
+
* identities line up across rows. Pure. */
|
|
2317
|
+
export const DELEGATION_ALIAS_WIDTH = 7
|
|
2318
|
+
|
|
2319
|
+
export function formatDelegationAlias(identity: string, width = DELEGATION_ALIAS_WIDTH): string {
|
|
2320
|
+
if (identity.length >= width) return identity.slice(0, width)
|
|
2321
|
+
return `${identity}${' '.repeat(width - identity.length)}`
|
|
2352
2322
|
}
|
|
2353
2323
|
|
|
2354
2324
|
/** Max description width on the row detail line — the old 180-char slice
|
|
@@ -2382,10 +2352,10 @@ export function truncateDelegationDescription(
|
|
|
2382
2352
|
return `${graphemes.slice(0, Math.max(0, max - 1)).join('')}\u2026`
|
|
2383
2353
|
}
|
|
2384
2354
|
|
|
2385
|
-
/** Fixed elapsed-time box: right-aligned to `width` (default
|
|
2386
|
-
* values line up across rows, e.g. `
|
|
2355
|
+
/** Fixed elapsed-time box: right-aligned to `width` (default 5) so elapsed
|
|
2356
|
+
* values line up across rows, e.g. ` 12s`. A label that already fills
|
|
2387
2357
|
* the box is returned untouched. Pure. */
|
|
2388
|
-
export const DELEGATION_ELAPSED_WIDTH =
|
|
2358
|
+
export const DELEGATION_ELAPSED_WIDTH = 5
|
|
2389
2359
|
|
|
2390
2360
|
export function formatDelegationElapsed(
|
|
2391
2361
|
entry: DelegationEntry,
|
|
@@ -2397,10 +2367,11 @@ export function formatDelegationElapsed(
|
|
|
2397
2367
|
return `${' '.repeat(width - label.length)}${label}`
|
|
2398
2368
|
}
|
|
2399
2369
|
|
|
2400
|
-
/**
|
|
2401
|
-
*
|
|
2402
|
-
|
|
2403
|
-
|
|
2370
|
+
/** Colored left half of a row line: `<marker><alias:7> ` — the state glyph
|
|
2371
|
+
* plus the single identity, padded so the muted elapsed column aligns. The
|
|
2372
|
+
* row renders this colored lead separately from the muted elapsed tail. Pure. */
|
|
2373
|
+
export function formatDelegationRowLead(entry: DelegationEntry, marker: string): string {
|
|
2374
|
+
return `${marker}${formatDelegationAlias(delegationRowIdentity(entry))} `
|
|
2404
2375
|
}
|
|
2405
2376
|
|
|
2406
2377
|
// Tree connectors (`isNestedDelegation` / `delegationTreePrefix`) were removed
|
|
@@ -2408,22 +2379,52 @@ export function formatDelegationRow(entry: DelegationEntry, marker: string): str
|
|
|
2408
2379
|
// ever actually nested and the connectors were never populated in production.
|
|
2409
2380
|
// BACKLOG: produce real grandchild rows first, then reintroduce them.
|
|
2410
2381
|
|
|
2411
|
-
/**
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2382
|
+
/** Visible-row ceiling for the panel: the remainder collapses into a single
|
|
2383
|
+
* "… +N more" line. Live rows render first, so a running job is never hidden
|
|
2384
|
+
* by the cap. */
|
|
2385
|
+
export const DELEGATION_VISIBLE_CEILING = 8
|
|
2386
|
+
|
|
2387
|
+
/** Header summary: `(N active · M done)` plus `· K failed` only when K > 0.
|
|
2388
|
+
* Active counts running/retry AND display-only stale-running (a stale row is
|
|
2389
|
+
* still a live job — it must never read as done); failed counts
|
|
2390
|
+
* error/startup_failed; cancelled reads as done. Pure. */
|
|
2414
2391
|
export function formatDelegationHeader(entries: readonly DelegationEntry[]): string {
|
|
2415
2392
|
let active = 0
|
|
2393
|
+
let done = 0
|
|
2394
|
+
let failed = 0
|
|
2416
2395
|
for (const e of entries) {
|
|
2417
|
-
|
|
2396
|
+
switch (delegationRowStatus(e.state)) {
|
|
2397
|
+
case 'active':
|
|
2398
|
+
case 'retry':
|
|
2399
|
+
active++
|
|
2400
|
+
break
|
|
2401
|
+
case 'failed':
|
|
2402
|
+
failed++
|
|
2403
|
+
break
|
|
2404
|
+
case 'done':
|
|
2405
|
+
done++
|
|
2406
|
+
break
|
|
2407
|
+
}
|
|
2418
2408
|
}
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
|
|
2409
|
+
const tail = failed > 0 ? ` · ${failed} failed` : ''
|
|
2410
|
+
return `(${active} active · ${done} done${tail})`
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/** Cap the panel: live rows first, then most-recent terminal rows, at most
|
|
2414
|
+
* `maxVisible` total. `hidden` is how many were dropped (rendered as
|
|
2415
|
+
* "… +N more"). Pure. */
|
|
2416
|
+
export function ceilingDelegationList(
|
|
2417
|
+
all: readonly DelegationEntry[],
|
|
2418
|
+
maxVisible = DELEGATION_VISIBLE_CEILING,
|
|
2419
|
+
now = Date.now(),
|
|
2420
|
+
): { visible: DelegationEntry[]; hidden: number } {
|
|
2421
|
+
const visible = visibleDelegationList(all, maxVisible, now).slice(0, maxVisible)
|
|
2422
|
+
return { visible, hidden: Math.max(0, all.length - visible.length) }
|
|
2422
2423
|
}
|
|
2423
2424
|
|
|
2424
2425
|
/** Split a display list into native task() rows vs pantheon_delegate rows.
|
|
2425
2426
|
* Native = source 'children-only' (no board report); everything else counts
|
|
2426
|
-
* as pantheon. Pure — powers the
|
|
2427
|
+
* as pantheon. Pure — powers the hooks.log line. */
|
|
2427
2428
|
export function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
2428
2429
|
native: number
|
|
2429
2430
|
pantheon: number
|
|
@@ -2456,18 +2457,22 @@ export function formatPanelLogLine(
|
|
|
2456
2457
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
2457
2458
|
* child of the current session — pantheon_delegate OR the native `task()`
|
|
2458
2459
|
* tool — carries parentID = caller), so it gets source 'children-only',
|
|
2459
|
-
* the internal alias 'native-task'
|
|
2460
|
+
* the internal alias 'native-task' instead of a
|
|
2460
2461
|
* board alias. The 'task nativa' description fallback keeps the row
|
|
2461
2462
|
* non-empty when the child carries no title.
|
|
2462
2463
|
* Terminal md state wins over the derived state; a running md defers to
|
|
2463
2464
|
* the child's live status. A running child is NEVER archived — it always
|
|
2464
2465
|
* lands in the active split (splitDelegationList active = running/retry).
|
|
2466
|
+
* `parentSessionID` (the focused session id) is stamped on report-less
|
|
2467
|
+
* children so the row carries its origin session; omit it and they stay
|
|
2468
|
+
* unscoped ('').
|
|
2465
2469
|
* Sorted running-first (compareDelegationEntries).
|
|
2466
2470
|
* Pure — no I/O. */
|
|
2467
2471
|
export function childrenToDelegationEntries(
|
|
2468
2472
|
children: readonly ChildDelegationLike[] | undefined,
|
|
2469
2473
|
md: readonly DelegationEntry[],
|
|
2470
2474
|
now = Date.now(),
|
|
2475
|
+
parentSessionID = '',
|
|
2471
2476
|
): DelegationEntry[] {
|
|
2472
2477
|
const byTaskID = new Map<string, DelegationEntry>()
|
|
2473
2478
|
for (const m of md) {
|
|
@@ -2485,7 +2490,11 @@ export function childrenToDelegationEntries(
|
|
|
2485
2490
|
: childStatusToState(child.status)
|
|
2486
2491
|
out.push({
|
|
2487
2492
|
alias: mdEntry?.alias ?? 'native-task',
|
|
2488
|
-
|
|
2493
|
+
// A report-less child IS a child of the focused session
|
|
2494
|
+
// (session.children is session-scoped), so the caller-passed
|
|
2495
|
+
// parentSessionID is stamped on it as its origin session; an md
|
|
2496
|
+
// match always carries its own session (the report dir name).
|
|
2497
|
+
sessionID: mdEntry?.sessionID ?? parentSessionID,
|
|
2489
2498
|
taskID: child.id,
|
|
2490
2499
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
2491
2500
|
state,
|
|
@@ -2624,7 +2633,7 @@ function SessionRow(props: { api: TuiPluginApi; session: any }) {
|
|
|
2624
2633
|
)
|
|
2625
2634
|
}
|
|
2626
2635
|
|
|
2627
|
-
/* ─── Delegation Row (single job from the children/md channels) ─ */
|
|
2636
|
+
/* ─── Delegation Row (single job from the children/md/board channels) ─ */
|
|
2628
2637
|
|
|
2629
2638
|
function DelegationRow(props: {
|
|
2630
2639
|
api: TuiPluginApi
|
|
@@ -2634,7 +2643,10 @@ function DelegationRow(props: {
|
|
|
2634
2643
|
}) {
|
|
2635
2644
|
const theme = () => props.api.theme.current
|
|
2636
2645
|
|
|
2637
|
-
|
|
2646
|
+
// Color is a SEPARATE channel from the glyph: the lead (glyph + identity)
|
|
2647
|
+
// carries the state tone, while elapsed + description stay muted so the
|
|
2648
|
+
// state reads at a glance without shouting.
|
|
2649
|
+
const tone = createMemo(() => {
|
|
2638
2650
|
const t = theme()
|
|
2639
2651
|
switch (delegationStateTone(props.job.state)) {
|
|
2640
2652
|
case 'warning':
|
|
@@ -2648,16 +2660,10 @@ function DelegationRow(props: {
|
|
|
2648
2660
|
}
|
|
2649
2661
|
})
|
|
2650
2662
|
|
|
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))
|
|
2663
|
+
const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow))
|
|
2664
|
+
const lead = createMemo(() => formatDelegationRowLead(props.job, marker()))
|
|
2660
2665
|
const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
|
|
2666
|
+
const description = createMemo(() => truncateDelegationDescription(props.job.description))
|
|
2661
2667
|
|
|
2662
2668
|
// beta.6: live tool call the child is running RIGHT NOW ("↳ bash npm test"),
|
|
2663
2669
|
// fed by message.part.updated activity tracking. Null while idle/done.
|
|
@@ -2674,22 +2680,22 @@ function DelegationRow(props: {
|
|
|
2674
2680
|
|
|
2675
2681
|
return (
|
|
2676
2682
|
<box onMouseDown={open}>
|
|
2683
|
+
{/* Line 1: colored lead (glyph + identity) + muted elapsed tail. */}
|
|
2677
2684
|
<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>
|
|
2685
|
+
<text>
|
|
2686
|
+
<span fg={tone()}>{lead()}</span>
|
|
2687
|
+
<span fg={theme().textMuted}>{elapsed()}</span>
|
|
2688
|
+
</text>
|
|
2687
2689
|
</box>
|
|
2690
|
+
{/* Line 2: the description, indented under the identity and muted. */}
|
|
2691
|
+
<Show when={description() !== ''}>
|
|
2692
|
+
<text fg={theme().textMuted}>{` ${description()}`}</text>
|
|
2693
|
+
</Show>
|
|
2688
2694
|
<Show when={activity()}>
|
|
2689
2695
|
{(a) => (
|
|
2690
2696
|
<text
|
|
2691
2697
|
fg={theme().textMuted}
|
|
2692
|
-
>{`
|
|
2698
|
+
>{` ↳ ${truncateDelegationDescription(`${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`)}`}</text>
|
|
2693
2699
|
)}
|
|
2694
2700
|
</Show>
|
|
2695
2701
|
</box>
|
|
@@ -2804,16 +2810,9 @@ function View(props: {
|
|
|
2804
2810
|
const projectRoot = createMemo(() => resolvePantheonRoot((props.api.state as any).path))
|
|
2805
2811
|
// Silence-by-default panel logger → the REAL <root>/.pantheon/logs/hooks.log.
|
|
2806
2812
|
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
|
-
}
|
|
2813
|
+
// Every job the board knows about is shown — no scope filter ("showing is
|
|
2814
|
+
// what matters"). Live-first ordering keeps running rows on top; the
|
|
2815
|
+
// ceiling collapses the rest into "… +N more".
|
|
2817
2816
|
// The rendered list is childDelegations (children enriched with md);
|
|
2818
2817
|
// there is no separate md-only signal — the md channel only feeds
|
|
2819
2818
|
// childrenToDelegationEntries below.
|
|
@@ -2901,13 +2900,10 @@ function View(props: {
|
|
|
2901
2900
|
}
|
|
2902
2901
|
}
|
|
2903
2902
|
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
|
-
}
|
|
2903
|
+
// Report-less rows ARE children of the focused session
|
|
2904
|
+
// (session.children is session-scoped), so the resolved id is stamped on
|
|
2905
|
+
// the row as its origin session.
|
|
2906
|
+
const childEntries = childrenToDelegationEntries(withStatus, md, now(), sessionID)
|
|
2911
2907
|
for (const [k, v] of props.liveStore.map)
|
|
2912
2908
|
if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 30_000)
|
|
2913
2909
|
props.liveStore.map.delete(k)
|
|
@@ -2942,33 +2938,13 @@ function View(props: {
|
|
|
2942
2938
|
})()
|
|
2943
2939
|
return delegationsInflight
|
|
2944
2940
|
}
|
|
2945
|
-
//
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
const scopedDelegations = createMemo(() =>
|
|
2951
|
-
filterDelegationsByScope(
|
|
2952
|
-
childDelegations(),
|
|
2953
|
-
delegationScope(),
|
|
2954
|
-
resolveCurrentSessionID({ sessionID: props.sessionID, api: props.api }),
|
|
2955
|
-
),
|
|
2941
|
+
// Live-first ceiling: at most DELEGATION_VISIBLE_CEILING rows render; the
|
|
2942
|
+
// remainder collapses into a single "… +N more" line. The header counts the
|
|
2943
|
+
// FULL board list — every session, no scope filter.
|
|
2944
|
+
const delegationCeiling = createMemo(() =>
|
|
2945
|
+
ceilingDelegationList(childDelegations(), DELEGATION_VISIBLE_CEILING, now()),
|
|
2956
2946
|
)
|
|
2957
|
-
const
|
|
2958
|
-
const visibleDelegations = createMemo(() => [
|
|
2959
|
-
...delegationSplit().active,
|
|
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
|
-
}
|
|
2947
|
+
const delegationHeader = createMemo(() => formatDelegationHeader(childDelegations()))
|
|
2972
2948
|
|
|
2973
2949
|
// ── Event subscriptions for live session/delegation updates ──
|
|
2974
2950
|
onMount(() => {
|
|
@@ -3047,29 +3023,6 @@ function View(props: {
|
|
|
3047
3023
|
/* re-scan unavailable — children API + 1s poll still cover it */
|
|
3048
3024
|
}
|
|
3049
3025
|
|
|
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
3026
|
onCleanup(() =>
|
|
3074
3027
|
cleanup.forEach((fn) => {
|
|
3075
3028
|
fn()
|
|
@@ -3136,31 +3089,13 @@ function View(props: {
|
|
|
3136
3089
|
<text fg={theme().text} attributes={1}>
|
|
3137
3090
|
{`${showDelegations() ? '▼' : '▶'} Delegations`}
|
|
3138
3091
|
</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>
|
|
3092
|
+
<text fg={theme().textMuted}>
|
|
3093
|
+
{childDelegations().length > 0 ? ` ${delegationHeader()}` : ' — idle'}
|
|
3094
|
+
</text>
|
|
3160
3095
|
</box>
|
|
3161
3096
|
<Show when={showDelegations()}>
|
|
3162
3097
|
<Show
|
|
3163
|
-
when={
|
|
3098
|
+
when={delegationCeiling().visible.length > 0}
|
|
3164
3099
|
fallback={
|
|
3165
3100
|
<box marginLeft={1}>
|
|
3166
3101
|
<text fg={theme().textMuted}>No delegations</text>
|
|
@@ -3168,7 +3103,7 @@ function View(props: {
|
|
|
3168
3103
|
}
|
|
3169
3104
|
>
|
|
3170
3105
|
<box marginLeft={1} flexDirection="column">
|
|
3171
|
-
<For each={
|
|
3106
|
+
<For each={delegationCeiling().visible}>
|
|
3172
3107
|
{(job) => (
|
|
3173
3108
|
<DelegationRow
|
|
3174
3109
|
api={props.api}
|
|
@@ -3178,40 +3113,8 @@ function View(props: {
|
|
|
3178
3113
|
/>
|
|
3179
3114
|
)}
|
|
3180
3115
|
</For>
|
|
3181
|
-
<Show when={
|
|
3182
|
-
{(()
|
|
3183
|
-
const archived = delegationSplit().archived
|
|
3184
|
-
const pageSize = 8
|
|
3185
|
-
const pages = Math.ceil(archived.length / pageSize)
|
|
3186
|
-
const page = Math.min(archivedPage(), pages - 1)
|
|
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
|
-
})()}
|
|
3116
|
+
<Show when={delegationCeiling().hidden > 0}>
|
|
3117
|
+
<text fg={theme().textMuted}>{`… +${delegationCeiling().hidden} more`}</text>
|
|
3215
3118
|
</Show>
|
|
3216
3119
|
</box>
|
|
3217
3120
|
</Show>
|