@try-works/dsh-recursive-mode 0.1.11 → 0.1.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/lib/client/board.d.ts +1 -26
- package/lib/client/derive.d.ts +11 -0
- package/lib/client/inspector.d.ts +2 -14
- package/lib/client/styles.d.ts +17 -0
- package/lib/client/theme.d.ts +20 -0
- package/lib/client.js +724 -278
- package/package.json +1 -1
- package/src/client/board.tsx +67 -97
- package/src/client/derive.ts +34 -0
- package/src/client/inspector.tsx +50 -50
- package/src/client/slots.ts +3 -0
- package/src/client/styles.ts +566 -0
- package/src/client/theme.ts +53 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@try-works/dsh-recursive-mode",
|
|
3
3
|
"description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + recursive_status tool",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
package/src/client/board.tsx
CHANGED
|
@@ -1,75 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Board overlay (SP2 R1 + run 08 R3 + run
|
|
3
|
-
* host-route projection
|
|
4
|
-
*
|
|
5
|
-
* mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
6
|
-
* when the snapshot is null (no recursive root / not loaded) or the current
|
|
7
|
-
* session is not on the recursive preset (the gate lives in slots.ts — here an
|
|
8
|
-
* empty projection with a null root is a no-op, not an "empty board").
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 15/16): kanban over the live
|
|
3
|
+
* host-route projection, class-based grid board with the Paper theme.
|
|
4
|
+
* Pure read-only renderer — no filesystem, no run-file scraping.
|
|
9
5
|
*
|
|
10
|
-
* Run
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer —
|
|
15
|
-
* without inline styles the mounted board was INVISIBLE (no position/size/z)
|
|
16
|
-
* and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
|
|
17
|
-
* (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
|
|
18
|
-
* backdrop, pointerEvents auto, and a visible close button.
|
|
6
|
+
* Run 16 (Paper): data-theme (default light), header theme toggle, workspace
|
|
7
|
+
* path + count chip, solid status pills. useBoardTheme is hoisted ABOVE the
|
|
8
|
+
* early returns (0.1.10 hook invariant).
|
|
19
9
|
*/
|
|
20
|
-
import { createElement
|
|
10
|
+
import { createElement } from 'react'
|
|
21
11
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts'
|
|
22
12
|
import type { LiveProjectionValue } from './contract.ts'
|
|
23
|
-
import { cardFacts, columnForRun, KANBAN_LANES } from './derive.ts'
|
|
13
|
+
import { cardFacts, columnForRun, KANBAN_LANES, cardPill, PILL_LABELS } from './derive.ts'
|
|
14
|
+
import { useBoardTheme, ThemeToggle } from './theme.ts'
|
|
24
15
|
import type { BoardSelection } from './open-state.ts'
|
|
25
16
|
|
|
26
|
-
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
27
|
-
export const overlayStyle: CSSProperties = {
|
|
28
|
-
position: 'fixed',
|
|
29
|
-
inset: 0,
|
|
30
|
-
background: 'rgba(15, 17, 21, 0.92)',
|
|
31
|
-
color: '#e6e6e6',
|
|
32
|
-
zIndex: 9999,
|
|
33
|
-
overflowY: 'auto',
|
|
34
|
-
padding: '24px 32px',
|
|
35
|
-
pointerEvents: 'auto',
|
|
36
|
-
fontFamily: 'system-ui, sans-serif',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const laneStyle: CSSProperties = { marginBottom: 24 }
|
|
40
|
-
const laneTitleStyle: CSSProperties = { fontSize: 14, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8, color: '#aaa' }
|
|
41
|
-
const cardStyle: CSSProperties = {
|
|
42
|
-
display: 'inline-block',
|
|
43
|
-
width: 280,
|
|
44
|
-
margin: '0 12px 12px 0',
|
|
45
|
-
padding: 12,
|
|
46
|
-
background: '#1f2329',
|
|
47
|
-
borderRadius: 8,
|
|
48
|
-
border: '1px solid #333',
|
|
49
|
-
cursor: 'pointer',
|
|
50
|
-
verticalAlign: 'top',
|
|
51
|
-
}
|
|
52
|
-
const cardHeadStyle: CSSProperties = { display: 'flex', justifyContent: 'space-between', marginBottom: 8 }
|
|
53
|
-
const badgeStyle: CSSProperties = { fontSize: 11, padding: '2px 6px', borderRadius: 4, background: '#333' }
|
|
54
|
-
const badgeTamperStyle: CSSProperties = { background: '#7a1f1f' }
|
|
55
|
-
const progressStyle: CSSProperties = { height: 6, background: '#333', borderRadius: 3, overflow: 'hidden' }
|
|
56
|
-
const progressFillStyle: CSSProperties = { background: '#4c9aff', height: '100%' }
|
|
57
|
-
const metaStyle: CSSProperties = { marginTop: 8, fontSize: 12, color: '#aaa', display: 'flex', justifyContent: 'space-between' }
|
|
58
|
-
const emptyStyle: CSSProperties = { padding: 40, textAlign: 'center', color: '#888' }
|
|
59
|
-
const closeStyle: CSSProperties = {
|
|
60
|
-
position: 'absolute',
|
|
61
|
-
top: 12,
|
|
62
|
-
right: 16,
|
|
63
|
-
background: '#1f2329',
|
|
64
|
-
color: '#e6e6e6',
|
|
65
|
-
border: '1px solid #444',
|
|
66
|
-
borderRadius: 6,
|
|
67
|
-
padding: '4px 12px',
|
|
68
|
-
cursor: 'pointer',
|
|
69
|
-
fontSize: 16,
|
|
70
|
-
zIndex: 1,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
17
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
74
18
|
export function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[] {
|
|
75
19
|
if (projection === undefined) return []
|
|
@@ -83,48 +27,74 @@ export function listRuns(projection: RecursiveProjection | undefined): Recursive
|
|
|
83
27
|
}
|
|
84
28
|
|
|
85
29
|
export interface BoardProps {
|
|
86
|
-
/** The live host-route snapshot (null = not loaded / no recursive root). */
|
|
87
30
|
snapshot: LiveProjectionValue | null
|
|
88
|
-
/** Opens the drill-down inspector for a run (R3). */
|
|
89
31
|
onOpenInspector?: (selection: BoardSelection) => void
|
|
90
|
-
/** Closes the overlay (run 12). */
|
|
91
32
|
onClose?: () => void
|
|
92
33
|
}
|
|
93
34
|
|
|
35
|
+
function closeButton(onClose: (() => void) | undefined) {
|
|
36
|
+
if (onClose === undefined) return null
|
|
37
|
+
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function solidPill(kind: ReturnType<typeof cardPill>) {
|
|
41
|
+
return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind])
|
|
42
|
+
}
|
|
43
|
+
|
|
94
44
|
export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
45
|
+
const { theme, toggle } = useBoardTheme()
|
|
95
46
|
if (snapshot === null || snapshot.root === null) return null
|
|
96
47
|
const runs = listRuns(snapshot.projection)
|
|
97
|
-
const
|
|
48
|
+
const workspacePath = snapshot.root
|
|
49
|
+
const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
|
|
98
50
|
if (runs.length === 0) {
|
|
99
|
-
return createElement('div', { className: 'rec-board', 'data-
|
|
100
|
-
|
|
101
|
-
|
|
51
|
+
return createElement('div', { className: 'rec-board', 'data-theme': theme, 'data-empty': true },
|
|
52
|
+
createElement('header', { className: 'rec-board-header' },
|
|
53
|
+
createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
|
|
54
|
+
createElement('span', { className: 'rec-board-path' }, workspacePath),
|
|
55
|
+
createElement('span', { className: 'rec-board-count' }, '0 runs'),
|
|
56
|
+
createElement(ThemeToggle, { theme, toggle }),
|
|
57
|
+
closeButton(onClose),
|
|
58
|
+
),
|
|
59
|
+
createElement('p', { className: 'rec-board-empty' }, 'No recursive runs in this workspace yet.'),
|
|
102
60
|
)
|
|
103
61
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
),
|
|
118
|
-
createElement('
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
62
|
+
return createElement('div', { className: 'rec-board', 'data-theme': theme },
|
|
63
|
+
createElement('header', { className: 'rec-board-header' },
|
|
64
|
+
createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
|
|
65
|
+
createElement('span', { className: 'rec-board-path' }, workspacePath),
|
|
66
|
+
createElement('span', { className: 'rec-board-count' }, runs.length + ' runs'),
|
|
67
|
+
createElement(ThemeToggle, { theme, toggle }),
|
|
68
|
+
closeButton(onClose),
|
|
69
|
+
),
|
|
70
|
+
createElement('div', { className: 'rec-columns' },
|
|
71
|
+
KANBAN_LANES.map((lane) => {
|
|
72
|
+
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id)
|
|
73
|
+
return createElement('section', { key: lane.id, className: 'rec-column' },
|
|
74
|
+
createElement('header', { className: 'rec-column-header' },
|
|
75
|
+
createElement('span', { className: 'rec-status-dot', 'data-status': laneRuns.length > 0 ? laneRuns[laneRuns.length - 1].state : 'new', 'aria-hidden': true }),
|
|
76
|
+
createElement('h3', { className: 'rec-column-title' }, lane.label),
|
|
77
|
+
createElement('span', { className: 'rec-column-count' }, String(laneRuns.length)),
|
|
78
|
+
),
|
|
79
|
+
createElement('div', { className: 'rec-cards' },
|
|
80
|
+
laneRuns.map((run) => {
|
|
81
|
+
const facts = cardFacts(run)
|
|
82
|
+
const pill = cardPill(run)
|
|
83
|
+
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', 'data-state': run.state, onClick: open(run) },
|
|
84
|
+
createElement('div', { className: 'rec-card-title' }, run.runId),
|
|
85
|
+
createElement('div', { className: 'rec-card-progress' },
|
|
86
|
+
createElement('div', { className: 'rec-card-progress-fill', style: { width: Math.round(facts.progress * 100) + '%' } }),
|
|
87
|
+
),
|
|
88
|
+
createElement('div', { className: 'rec-card-meta' },
|
|
89
|
+
createElement('span', { className: 'rec-card-phase' }, facts.currentPhase ?? 'no phases'),
|
|
90
|
+
solidPill(pill),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
}),
|
|
94
|
+
laneRuns.length === 0 && createElement('div', { className: 'rec-column-empty' }, '—'),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
129
99
|
)
|
|
130
100
|
}
|
package/src/client/derive.ts
CHANGED
|
@@ -187,6 +187,40 @@ export function cardFacts(card: RecursiveRunCard): RecursiveCardFacts {
|
|
|
187
187
|
* worktree root is part of the id so two runs with the same runId in
|
|
188
188
|
* different worktrees never collide — never "latest unfinished".
|
|
189
189
|
*/
|
|
190
|
+
/** Solid status-pill kinds (run 16 / Paper StatusPill). */
|
|
191
|
+
export type PillKind = 'locked' | 'in-progress' | 'paused' | 'blocked' | 'tampered' | 'advisory' | 'neutral'
|
|
192
|
+
|
|
193
|
+
/** Human pill label per kind (neutral renders an empty dash). */
|
|
194
|
+
export const PILL_LABELS: Record<PillKind, string> = {
|
|
195
|
+
locked: 'locked',
|
|
196
|
+
'in-progress': 'in progress',
|
|
197
|
+
paused: 'paused',
|
|
198
|
+
blocked: 'blocked',
|
|
199
|
+
tampered: 'tampered',
|
|
200
|
+
advisory: 'advisory',
|
|
201
|
+
neutral: '—',
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Solid-pill kind for a whole run card. Priority: tamper > fully-locked >
|
|
206
|
+
* run-state blocked > run-state paused > still in-progress.
|
|
207
|
+
*/
|
|
208
|
+
export function cardPill(card: RecursiveRunCard): PillKind {
|
|
209
|
+
const facts = cardFacts(card)
|
|
210
|
+
if (facts.tampered) return 'tampered'
|
|
211
|
+
if (facts.lockValidity === 'locked') return 'locked'
|
|
212
|
+
if (facts.state === 'blocked') return 'blocked'
|
|
213
|
+
if (facts.state === 'paused') return 'paused'
|
|
214
|
+
return 'in-progress'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Solid-pill kind for a phase row status string (LOCKED / DRAFT / '—'). */
|
|
218
|
+
export function phaseStatusPill(status: string): PillKind {
|
|
219
|
+
if (status === 'LOCKED') return 'locked'
|
|
220
|
+
if (status === '' || status === '—') return 'neutral'
|
|
221
|
+
return 'in-progress'
|
|
222
|
+
}
|
|
223
|
+
|
|
190
224
|
export function nodeKeyOf(runId: string, worktreeRoot: string): string {
|
|
191
225
|
return worktreeRoot + '\u0000' + runId
|
|
192
226
|
}
|
package/src/client/inspector.tsx
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (SP2 R1 + run
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
|
|
8
|
-
* unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
|
|
9
|
-
* styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
|
|
10
|
-
* close button.
|
|
2
|
+
* Drill-down inspector (SP2 R1 + run 15/16): centered modal-style detail panel
|
|
3
|
+
* (Paper .detail shape) with back + close, data-theme (default light), theme
|
|
4
|
+
* toggle, and solid status pills. All from the live host-route snapshot.
|
|
5
|
+
* useBoardTheme is hoisted ABOVE the not-found early return.
|
|
11
6
|
*/
|
|
12
|
-
import { createElement, type
|
|
7
|
+
import { createElement, type ReactNode } from 'react'
|
|
13
8
|
import type { LiveProjectionValue } from './contract.ts'
|
|
14
|
-
import { cardFacts, expandPhaseRows } from './derive.ts'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
const headStyle: CSSProperties = { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, borderBottom: '1px solid #333', paddingBottom: 12 }
|
|
18
|
-
const backStyle: CSSProperties = { background: '#1f2329', color: '#e6e6e6', border: '1px solid #444', borderRadius: 6, padding: '6px 12px', cursor: 'pointer' }
|
|
19
|
-
const closeStyle: CSSProperties = { background: '#1f2329', color: '#e6e6e6', border: '1px solid #444', borderRadius: 6, padding: '6px 12px', cursor: 'pointer', marginLeft: 'auto' }
|
|
20
|
-
const sectionStyle: CSSProperties = { marginBottom: 16 }
|
|
21
|
-
const rowStyle: CSSProperties = { display: 'flex', gap: 12, alignItems: 'center', padding: '6px 0', borderBottom: '1px solid #222', fontSize: 13 }
|
|
22
|
-
const phaseIdStyle: CSSProperties = { minWidth: 220, fontFamily: 'monospace', color: '#4c9aff' }
|
|
23
|
-
const phaseNameStyle: CSSProperties = { flex: 1, color: '#ccc' }
|
|
24
|
-
const badgeStyle: CSSProperties = { fontSize: 11, padding: '2px 6px', borderRadius: 4, background: '#333' }
|
|
25
|
-
const lockhashStyle: CSSProperties = { fontFamily: 'monospace', fontSize: 11, color: '#888' }
|
|
9
|
+
import { cardFacts, expandPhaseRows, cardPill, phaseStatusPill, PILL_LABELS } from './derive.ts'
|
|
10
|
+
import { useBoardTheme, ThemeToggle } from './theme.ts'
|
|
11
|
+
import type { BoardTheme } from './theme.ts'
|
|
26
12
|
|
|
27
13
|
export interface InspectorProps {
|
|
28
14
|
runId: string
|
|
29
15
|
worktreeRoot: string
|
|
30
16
|
snapshot: LiveProjectionValue | null
|
|
31
17
|
onBackToToolDetails: () => void
|
|
32
|
-
/** Closes the overlay (run 12). */
|
|
33
18
|
onClose?: () => void
|
|
34
19
|
}
|
|
35
20
|
|
|
21
|
+
function closeButton(onClose: (() => void) | undefined) {
|
|
22
|
+
if (onClose === undefined) return null
|
|
23
|
+
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function solidPill(kind: string) {
|
|
27
|
+
return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind as keyof typeof PILL_LABELS])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detailShell(runId: string, onBackToToolDetails: () => void, onClose: (() => void) | undefined, headerExtra: ReactNode, body: ReactNode, theme: BoardTheme, toggle: () => void) {
|
|
31
|
+
return createElement('div', { className: 'rec-inspector', 'data-theme': theme },
|
|
32
|
+
createElement('div', { className: 'rec-detail' },
|
|
33
|
+
createElement('div', { className: 'rec-detail-header' },
|
|
34
|
+
createElement('button', { type: 'button', className: 'rec-back', onClick: onBackToToolDetails }, '← Back'),
|
|
35
|
+
createElement('h2', { className: 'rec-detail-title' }, runId),
|
|
36
|
+
headerExtra,
|
|
37
|
+
createElement(ThemeToggle, { theme, toggle }),
|
|
38
|
+
closeButton(onClose),
|
|
39
|
+
),
|
|
40
|
+
createElement('div', { className: 'rec-detail-body' }, body),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps) {
|
|
37
|
-
const
|
|
46
|
+
const { theme, toggle } = useBoardTheme()
|
|
38
47
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
39
48
|
if (card === undefined) {
|
|
40
|
-
return
|
|
41
|
-
createElement('
|
|
42
|
-
|
|
43
|
-
close,
|
|
44
|
-
),
|
|
45
|
-
createElement('p', {}, 'Run not found: ' + runId),
|
|
49
|
+
return detailShell(runId, onBackToToolDetails, onClose, null,
|
|
50
|
+
createElement('p', { className: 'rec-detail-text' }, 'Run not found: ' + runId),
|
|
51
|
+
theme, toggle,
|
|
46
52
|
)
|
|
47
53
|
}
|
|
48
54
|
const facts = cardFacts(card)
|
|
49
55
|
const rows = expandPhaseRows(card)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
createElement('span', { className: 'rec-badge', style: badgeStyle }, row.status),
|
|
64
|
-
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash', style: lockhashStyle }, '#' + row.lockHash.slice(0, 8)),
|
|
65
|
-
)),
|
|
66
|
-
),
|
|
67
|
-
facts.gateBlocked && createElement('section', { className: 'rec-inspector-section', style: sectionStyle },
|
|
68
|
-
createElement('h3', { style: { fontSize: 13, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#aaa', marginBottom: 8 } }, 'Gate'),
|
|
69
|
-
createElement('p', {}, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
56
|
+
const pill = cardPill(card)
|
|
57
|
+
const headerExtra = solidPill(pill)
|
|
58
|
+
const body = createElement('section', { className: 'rec-detail-section' },
|
|
59
|
+
createElement('h3', {}, 'Phases'),
|
|
60
|
+
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row' },
|
|
61
|
+
createElement('span', { className: 'rec-phase-id' }, row.phase),
|
|
62
|
+
createElement('span', { className: 'rec-phase-name' }, row.fileName ?? '—'),
|
|
63
|
+
solidPill(phaseStatusPill(row.status)),
|
|
64
|
+
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)),
|
|
65
|
+
)),
|
|
66
|
+
facts.gateBlocked && createElement('div', { className: 'rec-detail-section' },
|
|
67
|
+
createElement('h3', {}, 'Gate'),
|
|
68
|
+
createElement('p', { className: 'rec-detail-text' }, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
70
69
|
),
|
|
71
70
|
)
|
|
71
|
+
return detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle)
|
|
72
72
|
}
|
package/src/client/slots.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { StatusStrip } from './strip.tsx'
|
|
|
25
25
|
import { RecursiveSettings } from './settings.tsx'
|
|
26
26
|
import { useLiveProjection } from './use-live.ts'
|
|
27
27
|
import { boardState, useBoardState } from './open-state.ts'
|
|
28
|
+
import { injectBoardStyles } from './styles.ts'
|
|
28
29
|
|
|
29
30
|
/** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
|
|
30
31
|
interface RootSlotProps {
|
|
@@ -73,6 +74,8 @@ export function RecursiveLauncherGate({ useSessions }: { useSessions: SnapshotSe
|
|
|
73
74
|
|
|
74
75
|
export function registerSlots(ctx: ClientContext): () => void {
|
|
75
76
|
const disposers: (() => void)[] = []
|
|
77
|
+
// Run 15: inject the one-shot theme-token stylesheet once per document (idempotent).
|
|
78
|
+
disposers.push(injectBoardStyles())
|
|
76
79
|
|
|
77
80
|
// Board launcher in the sidebar footer action list — OPENS the shared board store (run 08 R1).
|
|
78
81
|
// Run 11 (UX gate): the seat is root-scoped (visible in every session), but the board it
|