@try-works/dsh-recursive-mode 0.1.10 → 0.1.12
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 +0 -26
- package/lib/client/contract.d.ts +29 -0
- package/lib/client/index.d.ts +2 -2
- package/lib/client/inspector.d.ts +0 -14
- package/lib/client/slots.d.ts +3 -2
- package/lib/client/styles.d.ts +15 -0
- package/lib/client.js +506 -283
- package/package.json +1 -1
- package/src/client/board.tsx +53 -95
- package/src/client/contract.ts +48 -0
- package/src/client/index.ts +2 -2
- package/src/client/inspector.tsx +40 -49
- package/src/client/slots.ts +17 -9
- package/src/client/styles.ts +397 -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.12",
|
|
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,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Board overlay (SP2 R1 + run 08 R3 + run
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 15): the kanban view over the live
|
|
3
3
|
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
-
* scraping. Cards are grouped by worktree root
|
|
5
|
-
*
|
|
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").
|
|
4
|
+
* scraping. Cards are grouped by worktree root and mapped to their current-phase
|
|
5
|
+
* lane via derive.columnForRun.
|
|
9
6
|
*
|
|
10
|
-
* Run
|
|
11
|
-
*
|
|
12
|
-
* callback only selects a run for display; no mutation.
|
|
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.
|
|
7
|
+
* Run 15 (task-board redesign): class-based structure + theme-token stylesheet
|
|
8
|
+
* injected once via styles.ts (no inline styles — unlocks hover/grid/theme).
|
|
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
13
|
import { cardFacts, columnForRun, KANBAN_LANES } from './derive.ts'
|
|
24
14
|
import type { BoardSelection } from './open-state.ts'
|
|
25
15
|
|
|
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
16
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
74
17
|
export function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[] {
|
|
75
18
|
if (projection === undefined) return []
|
|
@@ -83,48 +26,63 @@ export function listRuns(projection: RecursiveProjection | undefined): Recursive
|
|
|
83
26
|
}
|
|
84
27
|
|
|
85
28
|
export interface BoardProps {
|
|
86
|
-
/** The live host-route snapshot (null = not loaded / no recursive root). */
|
|
87
29
|
snapshot: LiveProjectionValue | null
|
|
88
|
-
/** Opens the drill-down inspector for a run (R3). */
|
|
89
30
|
onOpenInspector?: (selection: BoardSelection) => void
|
|
90
|
-
/** Closes the overlay (run 12). */
|
|
91
31
|
onClose?: () => void
|
|
92
32
|
}
|
|
93
33
|
|
|
34
|
+
function closeButton(onClose: (() => void) | undefined) {
|
|
35
|
+
if (onClose === undefined) return null
|
|
36
|
+
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
37
|
+
}
|
|
38
|
+
|
|
94
39
|
export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
95
40
|
if (snapshot === null || snapshot.root === null) return null
|
|
96
41
|
const runs = listRuns(snapshot.projection)
|
|
97
|
-
const
|
|
42
|
+
const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
|
|
98
43
|
if (runs.length === 0) {
|
|
99
|
-
return createElement('div', { className: 'rec-board', 'data-empty': true
|
|
100
|
-
|
|
101
|
-
|
|
44
|
+
return createElement('div', { className: 'rec-board', 'data-empty': true },
|
|
45
|
+
createElement('header', { className: 'rec-board-header' },
|
|
46
|
+
createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
|
|
47
|
+
closeButton(onClose),
|
|
48
|
+
),
|
|
49
|
+
createElement('p', { className: 'rec-board-empty' }, 'No recursive runs in this workspace yet.'),
|
|
102
50
|
)
|
|
103
51
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
createElement('
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
52
|
+
return createElement('div', { className: 'rec-board' },
|
|
53
|
+
createElement('header', { className: 'rec-board-header' },
|
|
54
|
+
createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
|
|
55
|
+
createElement('span', { className: 'rec-board-count' }, String(runs.length)),
|
|
56
|
+
closeButton(onClose),
|
|
57
|
+
),
|
|
58
|
+
createElement('div', { className: 'rec-columns' },
|
|
59
|
+
KANBAN_LANES.map((lane) => {
|
|
60
|
+
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id)
|
|
61
|
+
return createElement('section', { key: lane.id, className: 'rec-column' },
|
|
62
|
+
createElement('header', { className: 'rec-column-header' },
|
|
63
|
+
createElement('span', { className: 'rec-status-dot', 'data-status': laneRuns.length > 0 ? laneRuns[laneRuns.length - 1].state : 'new', 'aria-hidden': true }),
|
|
64
|
+
createElement('h3', { className: 'rec-column-title' }, lane.label),
|
|
65
|
+
createElement('span', { className: 'rec-column-count' }, String(laneRuns.length)),
|
|
66
|
+
),
|
|
67
|
+
createElement('div', { className: 'rec-cards' },
|
|
68
|
+
laneRuns.map((run) => {
|
|
69
|
+
const facts = cardFacts(run)
|
|
70
|
+
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', 'data-state': run.state, onClick: open(run) },
|
|
71
|
+
createElement('div', { className: 'rec-card-title' }, run.runId),
|
|
72
|
+
createElement('div', { className: 'rec-card-progress' },
|
|
73
|
+
createElement('div', { className: 'rec-card-progress-fill', style: { width: Math.round(facts.progress * 100) + '%' } }),
|
|
74
|
+
),
|
|
75
|
+
createElement('div', { className: 'rec-card-meta' },
|
|
76
|
+
createElement('span', { className: 'rec-card-phase' }, facts.currentPhase ?? 'no phases'),
|
|
77
|
+
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', 'data-status': run.state, title: 'tampered' }, '!'),
|
|
78
|
+
createElement('span', { className: 'rec-badge', 'data-status': run.state }, run.state),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
}),
|
|
82
|
+
laneRuns.length === 0 && createElement('div', { className: 'rec-column-empty' }, '—'),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
129
87
|
)
|
|
130
88
|
}
|
package/src/client/contract.ts
CHANGED
|
@@ -101,3 +101,51 @@ export function isRecursivePreset(state: SessionListStateLike): boolean {
|
|
|
101
101
|
export function currentSessionCwd(state: SessionListStateLike): string {
|
|
102
102
|
return state.current === undefined ? '' : (state.byId[state.current]?.cwd ?? '')
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A workspace row — the structural mirror of the REAL WorkspaceView the host
|
|
107
|
+
* injects. We consume only workspaceId/path/title/sessionIds.
|
|
108
|
+
*/
|
|
109
|
+
export interface WorkspaceViewLike {
|
|
110
|
+
workspaceId: string
|
|
111
|
+
path: string
|
|
112
|
+
title: string
|
|
113
|
+
sessionIds: string[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The workspace-list snapshot — the structural subset of the REAL
|
|
118
|
+
* WorkspaceListState we consume (items + recentWorkspaceId). The real state
|
|
119
|
+
* also carries archivedSessionIds/state/phase/error/baselinesReady; those are
|
|
120
|
+
* irrelevant here and the real value satisfies this subset.
|
|
121
|
+
*/
|
|
122
|
+
export interface WorkspaceListStateLike {
|
|
123
|
+
items: readonly WorkspaceViewLike[]
|
|
124
|
+
recentWorkspaceId?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the CURRENT workspace path for a workspace-scoped board (run 14):
|
|
129
|
+
* the WORKSPACE owns the .recursive/ run-layer, not the session. Resolution:
|
|
130
|
+
* 1) recentWorkspaceId's item path (the workspace the user is viewing), else
|
|
131
|
+
* 2) the workspace whose sessionIds contains sessionList.current, else
|
|
132
|
+
* 3) the workspace whose path === currentSessionCwd(sessionList), else
|
|
133
|
+
* 4) '' (empty -> the route returns null root -> board renders nothing).
|
|
134
|
+
*/
|
|
135
|
+
export function currentWorkspacePath(list: WorkspaceListStateLike, sessionList: SessionListStateLike): string {
|
|
136
|
+
if (list.recentWorkspaceId !== undefined) {
|
|
137
|
+
const recent = list.items.find((w) => w.workspaceId === list.recentWorkspaceId)
|
|
138
|
+
if (recent !== undefined) return recent.path
|
|
139
|
+
}
|
|
140
|
+
const current = sessionList.current
|
|
141
|
+
if (current !== undefined) {
|
|
142
|
+
const bySession = list.items.find((w) => w.sessionIds.includes(current))
|
|
143
|
+
if (bySession !== undefined) return bySession.path
|
|
144
|
+
}
|
|
145
|
+
const cwd = currentSessionCwd(sessionList)
|
|
146
|
+
if (cwd !== '') {
|
|
147
|
+
const byCwd = list.items.find((w) => w.path === cwd)
|
|
148
|
+
if (byCwd !== undefined) return byCwd.path
|
|
149
|
+
}
|
|
150
|
+
return ''
|
|
151
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -23,8 +23,8 @@ export { useLiveProjection } from './use-live.ts'
|
|
|
23
23
|
export type { LiveProjectionSnapshot } from './use-live.ts'
|
|
24
24
|
export { fetchLiveState, subscribeLiveEvents } from './host-api.ts'
|
|
25
25
|
export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts'
|
|
26
|
-
export { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
27
|
-
export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts'
|
|
26
|
+
export { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts'
|
|
27
|
+
export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, WorkspaceViewLike, WorkspaceListStateLike, ClientContext } from './contract.ts'
|
|
28
28
|
|
|
29
29
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
30
30
|
export const inject = ['slots', 'sessions', 'workspaces', 'connection']
|
package/src/client/inspector.tsx
CHANGED
|
@@ -1,72 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (SP2 R1 + run
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* disk.
|
|
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): centered modal-style detail panel
|
|
3
|
+
* (task-board .detail shape) rendering one run's phase chain, lock badges, and
|
|
4
|
+
* tamper/gate facts — all from the live host-route snapshot, never disk.
|
|
11
5
|
*/
|
|
12
|
-
import { createElement, type
|
|
6
|
+
import { createElement, type ReactNode } from 'react'
|
|
13
7
|
import type { LiveProjectionValue } from './contract.ts'
|
|
14
8
|
import { cardFacts, expandPhaseRows } from './derive.ts'
|
|
15
|
-
import { overlayStyle } from './board.tsx'
|
|
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' }
|
|
26
9
|
|
|
27
10
|
export interface InspectorProps {
|
|
28
11
|
runId: string
|
|
29
12
|
worktreeRoot: string
|
|
30
13
|
snapshot: LiveProjectionValue | null
|
|
31
14
|
onBackToToolDetails: () => void
|
|
32
|
-
/** Closes the overlay (run 12). */
|
|
33
15
|
onClose?: () => void
|
|
34
16
|
}
|
|
35
17
|
|
|
18
|
+
function closeButton(onClose: (() => void) | undefined) {
|
|
19
|
+
if (onClose === undefined) return null
|
|
20
|
+
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detailShell(runId: string, onBackToToolDetails: () => void, onClose: (() => void) | undefined, headerExtra: ReactNode, body: ReactNode) {
|
|
24
|
+
return createElement('div', { className: 'rec-inspector' },
|
|
25
|
+
createElement('div', { className: 'rec-detail' },
|
|
26
|
+
createElement('div', { className: 'rec-detail-header' },
|
|
27
|
+
createElement('button', { type: 'button', className: 'rec-back', onClick: onBackToToolDetails }, '← Back'),
|
|
28
|
+
createElement('h2', { className: 'rec-detail-title' }, runId),
|
|
29
|
+
headerExtra,
|
|
30
|
+
closeButton(onClose),
|
|
31
|
+
),
|
|
32
|
+
createElement('div', { className: 'rec-detail-body' }, body),
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
36
37
|
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps) {
|
|
37
|
-
const close = onClose !== undefined ? createElement('button', { className: 'rec-close', style: closeStyle, onClick: onClose, title: 'Close' }, '×') : null
|
|
38
38
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
39
39
|
if (card === undefined) {
|
|
40
|
-
return
|
|
41
|
-
createElement('
|
|
42
|
-
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back', style: backStyle }, '← back to tool details'),
|
|
43
|
-
close,
|
|
44
|
-
),
|
|
45
|
-
createElement('p', {}, 'Run not found: ' + runId),
|
|
40
|
+
return detailShell(runId, onBackToToolDetails, onClose, null,
|
|
41
|
+
createElement('p', { className: 'rec-detail-text' }, 'Run not found: ' + runId),
|
|
46
42
|
)
|
|
47
43
|
}
|
|
48
44
|
const facts = cardFacts(card)
|
|
49
45
|
const rows = expandPhaseRows(card)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
createElement('
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 ?? '') + ')'),
|
|
46
|
+
const badge = createElement('span', { className: 'rec-badge', 'data-status': card.state }, facts.state)
|
|
47
|
+
const tamper = facts.tampered ? createElement('span', { className: 'rec-badge rec-badge-tamper', 'data-status': card.state }, 'tampered') : null
|
|
48
|
+
const headerExtra = createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, badge, tamper)
|
|
49
|
+
const body = createElement('section', { className: 'rec-detail-section' },
|
|
50
|
+
createElement('h3', {}, 'Phases'),
|
|
51
|
+
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row' },
|
|
52
|
+
createElement('span', { className: 'rec-phase-id' }, row.phase),
|
|
53
|
+
createElement('span', { className: 'rec-phase-name' }, row.fileName ?? '—'),
|
|
54
|
+
createElement('span', { className: 'rec-badge', 'data-status': row.status === 'LOCKED' ? 'complete' : 'active' }, row.status),
|
|
55
|
+
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)),
|
|
56
|
+
)),
|
|
57
|
+
facts.gateBlocked && createElement('div', { className: 'rec-detail-section' },
|
|
58
|
+
createElement('h3', {}, 'Gate'),
|
|
59
|
+
createElement('p', { className: 'rec-detail-text' }, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
70
60
|
),
|
|
71
61
|
)
|
|
62
|
+
return detailShell(runId, onBackToToolDetails, onClose, headerExtra, body)
|
|
72
63
|
}
|
package/src/client/slots.ts
CHANGED
|
@@ -17,19 +17,20 @@
|
|
|
17
17
|
* READ-ONLY (R9): the client only GETs the live host route; no mutation.
|
|
18
18
|
*/
|
|
19
19
|
import { createElement, type ReactNode } from 'react'
|
|
20
|
-
import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts'
|
|
21
|
-
import { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
20
|
+
import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts'
|
|
21
|
+
import { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts'
|
|
22
22
|
import { Board } from './board.tsx'
|
|
23
23
|
import { Inspector } from './inspector.tsx'
|
|
24
24
|
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 {
|
|
31
32
|
useSessions?: SnapshotSelectorHook<SessionListStateLike>
|
|
32
|
-
useWorkspaces?: SnapshotSelectorHook<
|
|
33
|
+
useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** Structural standard-prop face for the session scope (sessionId + useSessions). */
|
|
@@ -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
|
|
@@ -97,7 +100,7 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
97
100
|
}, (props: RootSlotProps) => {
|
|
98
101
|
const useSessions = props?.useSessions
|
|
99
102
|
if (useSessions === undefined) return null
|
|
100
|
-
return createElement(RecursiveBoardOverlay, { useSessions })
|
|
103
|
+
return createElement(RecursiveBoardOverlay, { useSessions, useWorkspaces: props?.useWorkspaces })
|
|
101
104
|
})))
|
|
102
105
|
|
|
103
106
|
// Status strip in the composer input dock (session scope) — gated the same way.
|
|
@@ -129,12 +132,17 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
129
132
|
* Run 10: consumes the full SessionListStateLike through the identity selector
|
|
130
133
|
* (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
|
|
131
134
|
*/
|
|
132
|
-
export function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
135
|
+
export function RecursiveBoardOverlay({ useSessions, useWorkspaces }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike> }): ReactNode {
|
|
133
136
|
const board = useBoardState()
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
137
|
+
const sessions = useRecursiveSessions(useSessions)
|
|
138
|
+
// Run 14 (USER-DIRECTED): the board keys on the WORKSPACE, not the session —
|
|
139
|
+
// the workspace owns the .recursive/ run-layer history. Hoist ALL hooks first.
|
|
140
|
+
const workspaces: WorkspaceListStateLike = useWorkspaces !== undefined
|
|
141
|
+
? useWorkspaces((s) => s)
|
|
142
|
+
: { items: [], recentWorkspaceId: undefined }
|
|
143
|
+
const content = overlayContent(board, sessions)
|
|
144
|
+
const wsPath = currentWorkspacePath(workspaces, sessions)
|
|
145
|
+
const scope = { cwd: wsPath }
|
|
138
146
|
// Run 13 (LIVE BUG): ALWAYS call useLiveProjection — a conditional hook (after the
|
|
139
147
|
// hidden early return) changed the hook count across renders and threw
|
|
140
148
|
// 'Rendered more hooks than during the previous render'.
|