@try-works/dsh-recursive-mode 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client/board.d.ts +1 -0
- package/lib/client/contract.d.ts +9 -5
- package/lib/client/derive.d.ts +11 -0
- package/lib/client/inspector.d.ts +2 -0
- package/lib/client/styles.d.ts +9 -7
- package/lib/client/theme.d.ts +20 -0
- package/lib/client.js +436 -180
- package/package.json +1 -1
- package/src/client/board.tsx +24 -12
- package/src/client/contract.ts +13 -9
- package/src/client/derive.ts +34 -0
- package/src/client/inspector.tsx +20 -11
- package/src/client/styles.ts +300 -131
- 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.14",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
package/src/client/board.tsx
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Board overlay (SP2 R1 + run 08 R3 + run 15):
|
|
3
|
-
* host-route projection
|
|
4
|
-
*
|
|
5
|
-
* lane via derive.columnForRun.
|
|
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.
|
|
6
5
|
*
|
|
7
|
-
* Run
|
|
8
|
-
*
|
|
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).
|
|
9
9
|
*/
|
|
10
10
|
import { createElement } from 'react'
|
|
11
11
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts'
|
|
12
12
|
import type { LiveProjectionValue } from './contract.ts'
|
|
13
|
-
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'
|
|
14
15
|
import type { BoardSelection } from './open-state.ts'
|
|
15
16
|
|
|
16
17
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
@@ -36,23 +37,34 @@ function closeButton(onClose: (() => void) | undefined) {
|
|
|
36
37
|
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function solidPill(kind: ReturnType<typeof cardPill>) {
|
|
41
|
+
return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind])
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
45
|
+
const { theme, toggle } = useBoardTheme()
|
|
40
46
|
if (snapshot === null || snapshot.root === null) return null
|
|
41
47
|
const runs = listRuns(snapshot.projection)
|
|
48
|
+
const workspacePath = snapshot.root
|
|
42
49
|
const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
|
|
43
50
|
if (runs.length === 0) {
|
|
44
|
-
return createElement('div', { className: 'rec-board', 'data-empty': true },
|
|
51
|
+
return createElement('div', { className: 'rec-board', 'data-theme': theme, 'data-empty': true },
|
|
45
52
|
createElement('header', { className: 'rec-board-header' },
|
|
46
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 }),
|
|
47
57
|
closeButton(onClose),
|
|
48
58
|
),
|
|
49
59
|
createElement('p', { className: 'rec-board-empty' }, 'No recursive runs in this workspace yet.'),
|
|
50
60
|
)
|
|
51
61
|
}
|
|
52
|
-
return createElement('div', { className: 'rec-board' },
|
|
62
|
+
return createElement('div', { className: 'rec-board', 'data-theme': theme },
|
|
53
63
|
createElement('header', { className: 'rec-board-header' },
|
|
54
64
|
createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
|
|
55
|
-
createElement('span', { className: 'rec-board-
|
|
65
|
+
createElement('span', { className: 'rec-board-path' }, workspacePath),
|
|
66
|
+
createElement('span', { className: 'rec-board-count' }, runs.length + ' runs'),
|
|
67
|
+
createElement(ThemeToggle, { theme, toggle }),
|
|
56
68
|
closeButton(onClose),
|
|
57
69
|
),
|
|
58
70
|
createElement('div', { className: 'rec-columns' },
|
|
@@ -67,6 +79,7 @@ export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
|
67
79
|
createElement('div', { className: 'rec-cards' },
|
|
68
80
|
laneRuns.map((run) => {
|
|
69
81
|
const facts = cardFacts(run)
|
|
82
|
+
const pill = cardPill(run)
|
|
70
83
|
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', 'data-state': run.state, onClick: open(run) },
|
|
71
84
|
createElement('div', { className: 'rec-card-title' }, run.runId),
|
|
72
85
|
createElement('div', { className: 'rec-card-progress' },
|
|
@@ -74,8 +87,7 @@ export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
|
74
87
|
),
|
|
75
88
|
createElement('div', { className: 'rec-card-meta' },
|
|
76
89
|
createElement('span', { className: 'rec-card-phase' }, facts.currentPhase ?? 'no phases'),
|
|
77
|
-
|
|
78
|
-
createElement('span', { className: 'rec-badge', 'data-status': run.state }, run.state),
|
|
90
|
+
solidPill(pill),
|
|
79
91
|
),
|
|
80
92
|
)
|
|
81
93
|
}),
|
package/src/client/contract.ts
CHANGED
|
@@ -125,18 +125,18 @@ export interface WorkspaceListStateLike {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Resolve the CURRENT workspace path for a workspace-scoped board (run 14
|
|
129
|
-
* the WORKSPACE owns the .recursive/ run-layer, not
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
128
|
+
* Resolve the CURRENT workspace path for a workspace-scoped board (run 14,
|
|
129
|
+
* corrected run 16/0.1.14): the WORKSPACE owns the .recursive/ run-layer, not
|
|
130
|
+
* the session. Resolution mirrors the harness's startSession precedence
|
|
131
|
+
* (service.ts:183) — the CURRENT session's workspace wins over the
|
|
132
|
+
* recentWorkspaceId (which the harness derives as the most recently ACTIVE
|
|
133
|
+
* workspace by session updatedAt, NOT the workspace being viewed):
|
|
134
|
+
* 1) the workspace whose sessionIds contains sessionList.current, else
|
|
135
|
+
* 2) the workspace whose path === currentSessionCwd(sessionList), else
|
|
136
|
+
* 3) recentWorkspaceId's item path (fallback ONLY when no current session resolves), else
|
|
133
137
|
* 4) '' (empty -> the route returns null root -> board renders nothing).
|
|
134
138
|
*/
|
|
135
139
|
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
140
|
const current = sessionList.current
|
|
141
141
|
if (current !== undefined) {
|
|
142
142
|
const bySession = list.items.find((w) => w.sessionIds.includes(current))
|
|
@@ -147,5 +147,9 @@ export function currentWorkspacePath(list: WorkspaceListStateLike, sessionList:
|
|
|
147
147
|
const byCwd = list.items.find((w) => w.path === cwd)
|
|
148
148
|
if (byCwd !== undefined) return byCwd.path
|
|
149
149
|
}
|
|
150
|
+
if (list.recentWorkspaceId !== undefined) {
|
|
151
|
+
const recent = list.items.find((w) => w.workspaceId === list.recentWorkspaceId)
|
|
152
|
+
if (recent !== undefined) return recent.path
|
|
153
|
+
}
|
|
150
154
|
return ''
|
|
151
155
|
}
|
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,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (SP2 R1 + run 15): centered modal-style detail panel
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
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.
|
|
5
6
|
*/
|
|
6
7
|
import { createElement, type ReactNode } from 'react'
|
|
7
8
|
import type { LiveProjectionValue } from './contract.ts'
|
|
8
|
-
import { cardFacts, expandPhaseRows } from './derive.ts'
|
|
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'
|
|
9
12
|
|
|
10
13
|
export interface InspectorProps {
|
|
11
14
|
runId: string
|
|
@@ -20,13 +23,18 @@ function closeButton(onClose: (() => void) | undefined) {
|
|
|
20
23
|
return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function
|
|
24
|
-
return createElement('
|
|
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 },
|
|
25
32
|
createElement('div', { className: 'rec-detail' },
|
|
26
33
|
createElement('div', { className: 'rec-detail-header' },
|
|
27
34
|
createElement('button', { type: 'button', className: 'rec-back', onClick: onBackToToolDetails }, '← Back'),
|
|
28
35
|
createElement('h2', { className: 'rec-detail-title' }, runId),
|
|
29
36
|
headerExtra,
|
|
37
|
+
createElement(ThemeToggle, { theme, toggle }),
|
|
30
38
|
closeButton(onClose),
|
|
31
39
|
),
|
|
32
40
|
createElement('div', { className: 'rec-detail-body' }, body),
|
|
@@ -35,23 +43,24 @@ function detailShell(runId: string, onBackToToolDetails: () => void, onClose: ((
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps) {
|
|
46
|
+
const { theme, toggle } = useBoardTheme()
|
|
38
47
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
39
48
|
if (card === undefined) {
|
|
40
49
|
return detailShell(runId, onBackToToolDetails, onClose, null,
|
|
41
50
|
createElement('p', { className: 'rec-detail-text' }, 'Run not found: ' + runId),
|
|
51
|
+
theme, toggle,
|
|
42
52
|
)
|
|
43
53
|
}
|
|
44
54
|
const facts = cardFacts(card)
|
|
45
55
|
const rows = expandPhaseRows(card)
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const headerExtra = createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, badge, tamper)
|
|
56
|
+
const pill = cardPill(card)
|
|
57
|
+
const headerExtra = solidPill(pill)
|
|
49
58
|
const body = createElement('section', { className: 'rec-detail-section' },
|
|
50
59
|
createElement('h3', {}, 'Phases'),
|
|
51
60
|
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row' },
|
|
52
61
|
createElement('span', { className: 'rec-phase-id' }, row.phase),
|
|
53
62
|
createElement('span', { className: 'rec-phase-name' }, row.fileName ?? '—'),
|
|
54
|
-
|
|
63
|
+
solidPill(phaseStatusPill(row.status)),
|
|
55
64
|
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)),
|
|
56
65
|
)),
|
|
57
66
|
facts.gateBlocked && createElement('div', { className: 'rec-detail-section' },
|
|
@@ -59,5 +68,5 @@ export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails,
|
|
|
59
68
|
createElement('p', { className: 'rec-detail-text' }, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
60
69
|
),
|
|
61
70
|
)
|
|
62
|
-
return detailShell(runId, onBackToToolDetails, onClose, headerExtra, body)
|
|
71
|
+
return detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle)
|
|
63
72
|
}
|