@try-works/dsh-recursive-mode 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -15
- package/lib/client/index.d.ts +3 -1
- package/lib/client/settings-view.d.ts +97 -0
- package/lib/client/settings.d.ts +25 -1
- package/lib/client.js +530 -11
- package/lib/enforcement.d.ts +37 -0
- package/lib/errors.d.ts +12 -0
- package/lib/goals-projection.d.ts +34 -4
- package/lib/index.js +594 -59
- package/lib/memory-feedback.d.ts +25 -2
- package/lib/recursive_ask.tool.d.ts +60 -0
- package/lib/recursive_init.tool.d.ts +15 -0
- package/lib/run-start.d.ts +55 -0
- package/lib/runtime.d.ts +137 -31
- package/package.json +1 -1
- package/scripts/test-recursive-mode-smoke.ts +5 -1
- package/src/client/index.ts +3 -1
- package/src/client/settings-view.ts +211 -0
- package/src/client/settings.tsx +194 -8
- package/src/client/slots.ts +15 -2
- package/src/client/styles.ts +237 -0
- package/src/enforcement.ts +74 -3
- package/src/errors.ts +12 -0
- package/src/goals-projection.ts +48 -7
- package/src/index.ts +56 -10
- package/src/memory-feedback.ts +50 -5
- package/src/phase-rules.ts +10 -1
- package/src/policy-globs.ts +25 -3
- package/src/policy-write.ts +1 -1
- package/src/recursive_ask.tool.ts +218 -17
- package/src/recursive_init.tool.ts +16 -1
- package/src/run-start.ts +123 -0
- package/src/runtime.ts +186 -14
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure display model for the Recursive settings panel (read-only report).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. The old panel rendered one sentence and no values — it
|
|
5
|
+
* claimed to report the host's configuration while reporting nothing. The data
|
|
6
|
+
* was already arriving: `host-api.ts` serves {root, projection, revision} on
|
|
7
|
+
* every fs change plus a 15s heartbeat, and `derive.ts` already derives board
|
|
8
|
+
* facts from the same projection. This module adds no data path; it is the same
|
|
9
|
+
* projection read for a different surface, and it is PURE (no React, no fs, no
|
|
10
|
+
* session window — the read-only-client + projection-over-files invariant, §11.9).
|
|
11
|
+
*
|
|
12
|
+
* THE ABSENCE RULE (`text()` + `SettingsRow.value === null`): a value is printed
|
|
13
|
+
* only when the projection carried it. A missing/blank value renders as
|
|
14
|
+
* `not reported` — never an empty row and never a plausible default. Note that
|
|
15
|
+
* `derive.cardFacts()` deliberately defaults a subagent's absent `status` to
|
|
16
|
+
* 'done'; this model does NOT, because a report that invents "done" is a report
|
|
17
|
+
* that lies. Required collection fields (`tampers`, `subagents`) that are present
|
|
18
|
+
* but empty print `none` — that IS their value; fields the card omits print
|
|
19
|
+
* `not reported`.
|
|
20
|
+
*/
|
|
21
|
+
import type { PhasePosition, RecursiveRunCard } from '../types.ts'
|
|
22
|
+
import type { LiveProjectionValue } from './contract.ts'
|
|
23
|
+
import { listRuns } from './board.tsx'
|
|
24
|
+
import { cardFacts, cardPill, expandPhaseRows, phaseStatusPill, type PillKind } from './derive.ts'
|
|
25
|
+
|
|
26
|
+
/** A label/value line. `value === null` means the projection carried NO value. */
|
|
27
|
+
export interface SettingsRow {
|
|
28
|
+
id: string
|
|
29
|
+
label: string
|
|
30
|
+
value: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One folded phase row, with the T21 single derived position printed verbatim. */
|
|
34
|
+
export interface SettingsPhaseRow {
|
|
35
|
+
phase: string
|
|
36
|
+
/** The phase doc filename, or null for a slot this run does not have (03.5). */
|
|
37
|
+
fileName: string | null
|
|
38
|
+
/** The raw folded status ('LOCKED' / 'DRAFT' / '—'). */
|
|
39
|
+
status: string
|
|
40
|
+
/** T21 single derived position; null when the producer carried none. */
|
|
41
|
+
position: PhasePosition | null
|
|
42
|
+
lockedAt: string | null
|
|
43
|
+
lockHash: string | null
|
|
44
|
+
present: boolean
|
|
45
|
+
pill: PillKind
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One subagent record: role/provider exactly as the host fold recorded them. */
|
|
49
|
+
export interface SettingsSubagentRow {
|
|
50
|
+
childId: string
|
|
51
|
+
role: string | null
|
|
52
|
+
provider: string | null
|
|
53
|
+
status: string | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One unresolved in-flight work item (T18). */
|
|
57
|
+
export interface SettingsPendingRow {
|
|
58
|
+
kind: string
|
|
59
|
+
delegationId: string
|
|
60
|
+
detail: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One run's report. */
|
|
64
|
+
export interface SettingsRunView {
|
|
65
|
+
runId: string
|
|
66
|
+
worktreeRoot: string
|
|
67
|
+
state: string
|
|
68
|
+
pill: PillKind
|
|
69
|
+
/** Scalar facts; a null value means the card did not carry that field. */
|
|
70
|
+
rows: SettingsRow[]
|
|
71
|
+
phases: SettingsPhaseRow[]
|
|
72
|
+
/** null when the card carried no `subagents` key; [] when it carried an empty one. */
|
|
73
|
+
subagents: SettingsSubagentRow[] | null
|
|
74
|
+
/** null when the card carried no `pendingWork` key; [] when it carried an empty one. */
|
|
75
|
+
pendingWork: SettingsPendingRow[] | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether the route has answered, and whether it resolved a recursive root. */
|
|
79
|
+
export type SettingsStatus = 'no-frame' | 'no-root' | 'connected'
|
|
80
|
+
|
|
81
|
+
/** The whole panel model. */
|
|
82
|
+
export interface SettingsView {
|
|
83
|
+
status: SettingsStatus
|
|
84
|
+
root: string | null
|
|
85
|
+
revision: number | null
|
|
86
|
+
runs: SettingsRunView[]
|
|
87
|
+
/**
|
|
88
|
+
* Route-level facts the payload does not carry AT ALL — the panel names them
|
|
89
|
+
* so a reader is never left to assume the client is hiding them.
|
|
90
|
+
*/
|
|
91
|
+
notCarried: SettingsRow[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The `/state` payload is {root, projection, revision} (host-api.ts /
|
|
96
|
+
* live-route.ts, both typed by LiveProjectionValue). Nothing else rides it, so
|
|
97
|
+
* anything else is reported as not carried rather than guessed.
|
|
98
|
+
*/
|
|
99
|
+
export const NOT_CARRIED: readonly SettingsRow[] = [
|
|
100
|
+
{ id: 'enforcement', label: 'Enforcement modes (preStep / toolGuards / tamper)', value: null },
|
|
101
|
+
{ id: 'router-defaults', label: 'Router defaults (defaults.subagent) and per-role routes', value: null },
|
|
102
|
+
{ id: 'scratch-format', label: 'Scratch format', value: null },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
/** A carried string, or null when the producer carried nothing to print. */
|
|
106
|
+
function text(value: string | undefined | null): string | null {
|
|
107
|
+
if (value === undefined || value === null) return null
|
|
108
|
+
return value.trim() === '' ? null : value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read a field the WIRE may omit even where the type calls it required (an
|
|
113
|
+
* older or partial producer). The comparison itself has to happen here: with
|
|
114
|
+
* `strict`, `card.subagents === undefined` is a no-overlap error at the call
|
|
115
|
+
* site, but a missing field must still read as "not reported", not crash.
|
|
116
|
+
*/
|
|
117
|
+
function carried<T>(value: T | undefined): T | undefined {
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `none` for a present-but-empty map, null for a map the card did not carry. */
|
|
122
|
+
function tamperValue(card: RecursiveRunCard): string | null {
|
|
123
|
+
const tampers = carried(card.tampers)
|
|
124
|
+
if (tampers === undefined) return null
|
|
125
|
+
const facts = Object.values(tampers)
|
|
126
|
+
if (facts.length === 0) return 'none'
|
|
127
|
+
return facts.map((f) => (text(f.path) ?? '') + ': ' + (text(f.reason) ?? 'no reason carried')).join(' · ')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Scalar facts for one run, in a fixed order. */
|
|
131
|
+
function runRows(card: RecursiveRunCard): SettingsRow[] {
|
|
132
|
+
const facts = cardFacts(card)
|
|
133
|
+
const gate = card.gateBlocked
|
|
134
|
+
return [
|
|
135
|
+
{ id: 'state', label: 'run state', value: text(card.state) },
|
|
136
|
+
{ id: 'state-reason', label: 'state reason', value: text(card.stateReason) },
|
|
137
|
+
{ id: 'lock', label: 'lock position', value: facts.lockValidity },
|
|
138
|
+
{ id: 'locked', label: 'locked phase groups', value: String(facts.lockedCount) + ' of ' + String(facts.totalPhases) + ' present' },
|
|
139
|
+
{ id: 'current-phase', label: 'current phase', value: text(facts.currentPhase) },
|
|
140
|
+
{
|
|
141
|
+
id: 'gate',
|
|
142
|
+
label: 'gate block',
|
|
143
|
+
value: gate === undefined
|
|
144
|
+
? null
|
|
145
|
+
: (text(gate.kind) ?? 'kind not carried') + ': ' + (gate.failures.length === 0 ? 'no failures listed' : gate.failures.join('; ')),
|
|
146
|
+
},
|
|
147
|
+
{ id: 'tampers', label: 'tamper facts', value: tamperValue(card) },
|
|
148
|
+
{ id: 'repo', label: 'repo', value: text(card.repo) },
|
|
149
|
+
{ id: 'template', label: 'template', value: text(card.template) },
|
|
150
|
+
{ id: 'merged', label: 'merged to repo root', value: text(card.mergedToRepoRoot) },
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The folded phase chain, with the card's own `position` read off the wire row. */
|
|
155
|
+
function phaseRows(card: RecursiveRunCard): SettingsPhaseRow[] {
|
|
156
|
+
return expandPhaseRows(card).map((row) => {
|
|
157
|
+
const wired = row.fileName === null ? undefined : card.phases[row.fileName]
|
|
158
|
+
return {
|
|
159
|
+
phase: row.phase,
|
|
160
|
+
fileName: row.fileName,
|
|
161
|
+
status: row.status,
|
|
162
|
+
position: wired?.position ?? null,
|
|
163
|
+
lockedAt: text(row.lockedAt),
|
|
164
|
+
lockHash: text(row.lockHash),
|
|
165
|
+
present: row.present,
|
|
166
|
+
pill: phaseStatusPill(row.status),
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One run card -> its report. */
|
|
172
|
+
export function settingsRunView(card: RecursiveRunCard): SettingsRunView {
|
|
173
|
+
const subagents = carried(card.subagents)
|
|
174
|
+
const pendingWork = carried(card.pendingWork)
|
|
175
|
+
return {
|
|
176
|
+
runId: card.runId,
|
|
177
|
+
worktreeRoot: card.worktreeRoot,
|
|
178
|
+
state: card.state,
|
|
179
|
+
pill: cardPill(card),
|
|
180
|
+
rows: runRows(card),
|
|
181
|
+
phases: phaseRows(card),
|
|
182
|
+
subagents: subagents === undefined
|
|
183
|
+
? null
|
|
184
|
+
: Object.values(subagents).map((s) => ({
|
|
185
|
+
childId: s.childId,
|
|
186
|
+
role: text(s.role),
|
|
187
|
+
provider: text(s.provider),
|
|
188
|
+
status: text(s.status),
|
|
189
|
+
})),
|
|
190
|
+
pendingWork: pendingWork === undefined
|
|
191
|
+
? null
|
|
192
|
+
: pendingWork.map((p) => ({ kind: p.kind, delegationId: p.delegationId, detail: p.detail })),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The panel model for one live snapshot (null = the route has not answered yet).
|
|
198
|
+
* Every run in the projection is reported; nothing is filtered or defaulted.
|
|
199
|
+
*/
|
|
200
|
+
export function settingsView(snapshot: LiveProjectionValue | null): SettingsView {
|
|
201
|
+
if (snapshot === null) {
|
|
202
|
+
return { status: 'no-frame', root: null, revision: null, runs: [], notCarried: [...NOT_CARRIED] }
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
status: snapshot.root === null ? 'no-root' : 'connected',
|
|
206
|
+
root: text(snapshot.root),
|
|
207
|
+
revision: typeof snapshot.revision === 'number' ? snapshot.revision : null,
|
|
208
|
+
runs: listRuns(snapshot.projection).map(settingsRunView),
|
|
209
|
+
notCarried: [...NOT_CARRIED],
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/client/settings.tsx
CHANGED
|
@@ -1,18 +1,204 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recursive settings page (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Recursive settings page (§11.8): a settings.section list entry that REPORTS the
|
|
3
|
+
* live projection the host route serves — the enforcement-free facts the wire
|
|
4
|
+
* actually carries (run state + lock position, the folded phase chain with its
|
|
5
|
+
* T21 position, tamper/gate/pending facts, subagent role+provider records).
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS PANEL IS NOT: it is not a config editor and not a second board. The
|
|
8
|
+
* client is read-only (§11.9): it GETs the host route and reads no files. Every
|
|
9
|
+
* value is printed verbatim from the projection; a field the payload does not
|
|
10
|
+
* carry is printed as `not reported`, and the route-level fields the payload can
|
|
11
|
+
* never carry (enforcement modes, router defaults, scratch format) are listed by
|
|
12
|
+
* name at the bottom so a reader is never left guessing what is missing.
|
|
13
|
+
*
|
|
14
|
+
* `RecursiveSettings` is the pure renderer (props in, tree out). The live seat is
|
|
15
|
+
* `RecursiveSettingsLive`, which resolves the workspace scope exactly like the
|
|
16
|
+
* board seats and hands the snapshot down — the same shape board.tsx and
|
|
17
|
+
* inspector.tsx consume.
|
|
5
18
|
*/
|
|
6
|
-
import { createElement } from 'react'
|
|
19
|
+
import { createElement, type ReactElement, type ReactNode } from 'react'
|
|
20
|
+
import { RECURSIVE_API_PREFIX } from '../live-route.ts'
|
|
21
|
+
import {
|
|
22
|
+
currentSessionCwd,
|
|
23
|
+
currentWorkspacePath,
|
|
24
|
+
type LiveProjectionValue,
|
|
25
|
+
type SessionListStateLike,
|
|
26
|
+
type SnapshotSelectorHook,
|
|
27
|
+
type WorkspaceListStateLike,
|
|
28
|
+
} from './contract.ts'
|
|
29
|
+
import { PILL_LABELS } from './derive.ts'
|
|
30
|
+
import { useLiveProjection } from './use-live.ts'
|
|
31
|
+
import {
|
|
32
|
+
settingsView,
|
|
33
|
+
type SettingsPendingRow,
|
|
34
|
+
type SettingsPhaseRow,
|
|
35
|
+
type SettingsRow,
|
|
36
|
+
type SettingsRunView,
|
|
37
|
+
type SettingsStatus,
|
|
38
|
+
type SettingsSubagentRow,
|
|
39
|
+
type SettingsView,
|
|
40
|
+
} from './settings-view.ts'
|
|
41
|
+
|
|
42
|
+
/** Rendered in place of every value the projection did not carry. */
|
|
43
|
+
export const ABSENT_LABEL = 'not reported'
|
|
7
44
|
|
|
8
45
|
export interface RecursiveSettingsProps {
|
|
9
46
|
close: () => void
|
|
47
|
+
/** The live host-route frame, or null while the first fetch is in flight. */
|
|
48
|
+
snapshot?: LiveProjectionValue | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The route-answer states, each said plainly instead of implied by an empty panel. */
|
|
52
|
+
const STATUS_TEXT: Record<SettingsStatus, string> = {
|
|
53
|
+
'no-frame': 'The host route has not answered for this workspace yet. Every value below is unknown — not a default.',
|
|
54
|
+
'no-root': 'The host route answered, but resolved no recursive root for this workspace (no .recursive/ run layer found).',
|
|
55
|
+
connected: 'Live.',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STATUS_VALUE: Record<SettingsStatus, string> = {
|
|
59
|
+
'no-frame': 'no frame yet',
|
|
60
|
+
'no-root': 'answered · no recursive root',
|
|
61
|
+
connected: 'connected',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A label/value report block (`dl` — the semantics of a definition list). */
|
|
65
|
+
function rows(className: string, items: readonly SettingsRow[]): ReactElement {
|
|
66
|
+
return createElement('dl', { className },
|
|
67
|
+
items.flatMap((row) => [
|
|
68
|
+
createElement('dt', { key: row.id + '-k', className: 'rec-settings-label' }, row.label),
|
|
69
|
+
createElement('dd', {
|
|
70
|
+
key: row.id + '-v',
|
|
71
|
+
className: row.value === null ? 'rec-settings-value rec-settings-absent' : 'rec-settings-value',
|
|
72
|
+
}, row.value ?? ABSENT_LABEL),
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The frame's own facts: what the route answered, and how much it carried. */
|
|
78
|
+
function sourceRows(view: SettingsView): SettingsRow[] {
|
|
79
|
+
return [
|
|
80
|
+
{ id: 'status', label: 'route status', value: STATUS_VALUE[view.status] },
|
|
81
|
+
{ id: 'root', label: 'workspace root', value: view.root },
|
|
82
|
+
{ id: 'revision', label: 'revision', value: view.revision === null ? null : String(view.revision) },
|
|
83
|
+
{ id: 'runs', label: 'runs in the projection', value: view.status === 'no-frame' ? null : String(view.runs.length) },
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function phaseNode(row: SettingsPhaseRow): ReactElement {
|
|
88
|
+
const lock: ReactNode[] = []
|
|
89
|
+
if (row.lockedAt !== null) lock.push(createElement('span', { key: 'at' }, 'locked at ' + row.lockedAt))
|
|
90
|
+
if (row.lockHash !== null) lock.push(createElement('code', { key: 'hash', className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)))
|
|
91
|
+
return createElement('div', { key: row.phase, className: 'rec-settings-phase' },
|
|
92
|
+
createElement('span', { className: 'rec-settings-phase-id' }, row.phase),
|
|
93
|
+
createElement('span', { className: 'rec-settings-phase-name' }, row.fileName ?? 'not present in this run'),
|
|
94
|
+
createElement('span', { className: 'rec-settings-phase-status' }, row.status),
|
|
95
|
+
createElement('span', {
|
|
96
|
+
className: row.position === null ? 'rec-settings-phase-pos rec-settings-absent' : 'rec-settings-phase-pos',
|
|
97
|
+
}, row.position ?? ABSENT_LABEL),
|
|
98
|
+
lock.length > 0 && createElement('span', { className: 'rec-settings-phase-lock' }, lock),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function subagentNode(s: SettingsSubagentRow): ReactElement {
|
|
103
|
+
return createElement('div', { key: s.childId, className: 'rec-settings-item' },
|
|
104
|
+
createElement('span', { className: 'rec-settings-item-id' }, s.childId),
|
|
105
|
+
createElement('span', { className: 'rec-settings-item-text' },
|
|
106
|
+
'role ' + (s.role ?? ABSENT_LABEL) + ' · provider ' + (s.provider ?? ABSENT_LABEL) + ' · status ' + (s.status ?? ABSENT_LABEL)),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pendingNode(p: SettingsPendingRow): ReactElement {
|
|
111
|
+
return createElement('div', { key: p.kind + '\u0000' + p.delegationId, className: 'rec-settings-item' },
|
|
112
|
+
createElement('span', { className: 'rec-settings-item-id' }, p.kind),
|
|
113
|
+
createElement('span', { className: 'rec-settings-item-text' }, p.delegationId + ' — ' + p.detail),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A collection field: null = the card carried no such key (not reported),
|
|
119
|
+
* [] = the card carried an empty one (that empty value IS the report).
|
|
120
|
+
*/
|
|
121
|
+
function collection<T>(items: readonly T[] | null, render: (item: T) => ReactElement, none: string): ReactElement {
|
|
122
|
+
if (items === null) {
|
|
123
|
+
return createElement('p', { className: 'rec-settings-value rec-settings-absent' }, ABSENT_LABEL + ' — the card carries no such field')
|
|
124
|
+
}
|
|
125
|
+
if (items.length === 0) return createElement('p', { className: 'rec-settings-value rec-settings-none' }, none)
|
|
126
|
+
return createElement('div', { className: 'rec-settings-items' }, items.map(render))
|
|
10
127
|
}
|
|
11
128
|
|
|
12
|
-
|
|
129
|
+
function runSection(run: SettingsRunView): ReactElement {
|
|
130
|
+
const present = run.phases.filter((p) => p.present).length
|
|
131
|
+
return createElement('section', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-settings-run' },
|
|
132
|
+
createElement('div', { className: 'rec-settings-run-header' },
|
|
133
|
+
createElement('h3', { className: 'rec-settings-run-title' }, run.runId),
|
|
134
|
+
createElement('span', { className: 'rec-settings-pill', 'data-pill': run.pill }, PILL_LABELS[run.pill]),
|
|
135
|
+
createElement('span', { className: 'rec-settings-run-root' }, run.worktreeRoot),
|
|
136
|
+
),
|
|
137
|
+
rows('rec-settings-rows', run.rows),
|
|
138
|
+
createElement('h4', { className: 'rec-settings-h4' }, 'Phases (' + String(present) + ' present)'),
|
|
139
|
+
createElement('p', { className: 'rec-settings-hint' },
|
|
140
|
+
'status is the folded artifact status; position is the host\'s single derived phase position (T21).'),
|
|
141
|
+
run.phases.length === 0
|
|
142
|
+
? createElement('p', { className: 'rec-settings-value rec-settings-none' }, 'none — the card carries no phase rows')
|
|
143
|
+
: createElement('div', { className: 'rec-settings-phases' }, run.phases.map(phaseNode)),
|
|
144
|
+
createElement('h4', { className: 'rec-settings-h4' }, 'Subagents (role and provider as the host recorded them)'),
|
|
145
|
+
collection(run.subagents, subagentNode, 'none — the card carries an empty subagent map'),
|
|
146
|
+
createElement('h4', { className: 'rec-settings-h4' }, 'Pending work (unresolved in-flight delegations)'),
|
|
147
|
+
collection(run.pendingWork, pendingNode, 'none — the card carries an empty pending set'),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The panel: a report of one live frame. Pure — snapshot in, tree out. */
|
|
152
|
+
export function RecursiveSettings({ close, snapshot = null }: RecursiveSettingsProps) {
|
|
153
|
+
const view = settingsView(snapshot)
|
|
13
154
|
return createElement('div', { className: 'rec-settings' },
|
|
14
|
-
createElement('
|
|
15
|
-
|
|
16
|
-
|
|
155
|
+
createElement('header', { className: 'rec-settings-header' },
|
|
156
|
+
createElement('h2', { className: 'rec-settings-title' }, 'Recursive'),
|
|
157
|
+
createElement('span', { className: 'rec-settings-tag' }, 'live projection · read-only'),
|
|
158
|
+
createElement('button', { type: 'button', className: 'rec-settings-close', onClick: close }, 'Close'),
|
|
159
|
+
),
|
|
160
|
+
createElement('p', { className: 'rec-settings-lede' },
|
|
161
|
+
'Read verbatim from the host route (GET ' + RECURSIVE_API_PREFIX + '/state + GET ' + RECURSIVE_API_PREFIX
|
|
162
|
+
+ '/events). This client writes nothing and reads no files; anything the projection does not carry is printed as "'
|
|
163
|
+
+ ABSENT_LABEL + '".'),
|
|
164
|
+
rows('rec-settings-source', sourceRows(view)),
|
|
165
|
+
view.status !== 'connected' && createElement('p', { className: 'rec-settings-hint' }, STATUS_TEXT[view.status]),
|
|
166
|
+
view.status === 'connected' && view.runs.length === 0
|
|
167
|
+
&& createElement('p', { className: 'rec-settings-value rec-settings-none' }, 'none — the projection carries no runs in this workspace'),
|
|
168
|
+
view.runs.map(runSection),
|
|
169
|
+
createElement('section', { className: 'rec-settings-notcarried' },
|
|
170
|
+
createElement('h3', { className: 'rec-settings-h3' }, 'Not carried by this route'),
|
|
171
|
+
createElement('p', { className: 'rec-settings-hint' },
|
|
172
|
+
'Configured on the host, but the payload carries only {root, projection, revision} — so this panel reports them as unknown rather than guessing.'),
|
|
173
|
+
rows('rec-settings-rows', view.notCarried),
|
|
174
|
+
),
|
|
17
175
|
)
|
|
18
176
|
}
|
|
177
|
+
|
|
178
|
+
/** The settings seat props: the root standard kit plus the shell's close affordance. */
|
|
179
|
+
export interface RecursiveSettingsSeatProps {
|
|
180
|
+
close: () => void
|
|
181
|
+
useSessions?: SnapshotSelectorHook<SessionListStateLike>
|
|
182
|
+
useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface RecursiveSettingsLiveProps {
|
|
186
|
+
close: () => void
|
|
187
|
+
useSessions: SnapshotSelectorHook<SessionListStateLike>
|
|
188
|
+
useWorkspaces: SnapshotSelectorHook<WorkspaceListStateLike>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The live seat: same scope resolution as the board seats (sessionId PRIMARY,
|
|
193
|
+
* workspace path as the cwd hint, session cwd while workspaces hydrate). Hooks
|
|
194
|
+
* are called unconditionally — the seat only renders this component when both
|
|
195
|
+
* selector hooks are present (slots.ts), so the hook order is fixed.
|
|
196
|
+
*/
|
|
197
|
+
export function RecursiveSettingsLive({ close, useSessions, useWorkspaces }: RecursiveSettingsLiveProps) {
|
|
198
|
+
const sessions = useSessions((s) => s)
|
|
199
|
+
const workspaces: WorkspaceListStateLike = useWorkspaces((s) => s) ?? { items: [], recentWorkspaceId: undefined }
|
|
200
|
+
const wsPath = currentWorkspacePath(workspaces, sessions)
|
|
201
|
+
const cwd = wsPath !== '' ? wsPath : currentSessionCwd(sessions)
|
|
202
|
+
const snapshot = useLiveProjection({ sessionId: sessions.current, cwd })
|
|
203
|
+
return createElement(RecursiveSettings, { close, snapshot })
|
|
204
|
+
}
|
package/src/client/slots.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, Workspa
|
|
|
21
21
|
import { isRecursivePreset, currentWorkspacePath } from './contract.ts'
|
|
22
22
|
import { Board } from './board.tsx'
|
|
23
23
|
import { Inspector } from './inspector.tsx'
|
|
24
|
-
import { RecursiveSettings } from './settings.tsx'
|
|
24
|
+
import { RecursiveSettings, RecursiveSettingsLive, type RecursiveSettingsSeatProps } from './settings.tsx'
|
|
25
25
|
import { useLiveProjection } from './use-live.ts'
|
|
26
26
|
import { boardState, useBoardState } from './open-state.ts'
|
|
27
27
|
import { injectBoardStyles } from './styles.ts'
|
|
@@ -139,12 +139,25 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
139
139
|
})))
|
|
140
140
|
|
|
141
141
|
// Settings section (root scope, always present; no gate — configuration is always available).
|
|
142
|
+
// The seat receives the shell's `close` PLUS the root standard kit (useSessions/useWorkspaces,
|
|
143
|
+
// scoped-slots standardProps), so the panel can subscribe to the SAME live route the board
|
|
144
|
+
// reads and report the projection. Without the kit the panel still renders — with every value
|
|
145
|
+
// reported as absent (never guessed), which is the honest degradation.
|
|
142
146
|
disposers.push(ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
143
147
|
name: 'settings.section',
|
|
144
148
|
id: 'recursive',
|
|
145
149
|
order: 90,
|
|
146
150
|
label: 'Recursive',
|
|
147
|
-
}, (props:
|
|
151
|
+
}, (props: RecursiveSettingsSeatProps) => {
|
|
152
|
+
if (props.useSessions === undefined || props.useWorkspaces === undefined) {
|
|
153
|
+
return createElement(RecursiveSettings, { close: props.close })
|
|
154
|
+
}
|
|
155
|
+
return createElement(RecursiveSettingsLive, {
|
|
156
|
+
close: props.close,
|
|
157
|
+
useSessions: props.useSessions,
|
|
158
|
+
useWorkspaces: props.useWorkspaces,
|
|
159
|
+
})
|
|
160
|
+
})))
|
|
148
161
|
|
|
149
162
|
return () => { for (const d of disposers) d() }
|
|
150
163
|
}
|