@try-works/dsh-recursive-mode 0.1.8 → 0.1.10
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 +26 -1
- package/lib/client/inspector.d.ts +16 -1
- package/lib/client/slots.d.ts +18 -0
- package/lib/client.js +280 -31
- package/package.json +6 -4
- package/src/client/board.tsx +79 -21
- package/src/client/inspector.tsx +46 -21
- package/src/client/slots.ts +10 -5
package/lib/client/board.d.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
3
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
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").
|
|
9
|
+
*
|
|
10
|
+
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
11
|
+
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
import { type CSSProperties } from 'react';
|
|
1
21
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts';
|
|
2
22
|
import type { LiveProjectionValue } from './contract.ts';
|
|
3
23
|
import type { BoardSelection } from './open-state.ts';
|
|
24
|
+
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
25
|
+
export declare const overlayStyle: CSSProperties;
|
|
4
26
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
5
27
|
export declare function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[];
|
|
6
28
|
export interface BoardProps {
|
|
@@ -8,7 +30,10 @@ export interface BoardProps {
|
|
|
8
30
|
snapshot: LiveProjectionValue | null;
|
|
9
31
|
/** Opens the drill-down inspector for a run (R3). */
|
|
10
32
|
onOpenInspector?: (selection: BoardSelection) => void;
|
|
33
|
+
/** Closes the overlay (run 12). */
|
|
34
|
+
onClose?: () => void;
|
|
11
35
|
}
|
|
12
|
-
export declare function Board({ snapshot, onOpenInspector }: BoardProps): import("react").DetailedReactHTMLElement<{
|
|
36
|
+
export declare function Board({ snapshot, onOpenInspector, onClose }: BoardProps): import("react").DetailedReactHTMLElement<{
|
|
13
37
|
className: string;
|
|
38
|
+
style: CSSProperties;
|
|
14
39
|
}, HTMLElement> | null;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
3
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
import { type CSSProperties } from 'react';
|
|
1
13
|
import type { LiveProjectionValue } from './contract.ts';
|
|
2
14
|
export interface InspectorProps {
|
|
3
15
|
runId: string;
|
|
4
16
|
worktreeRoot: string;
|
|
5
17
|
snapshot: LiveProjectionValue | null;
|
|
6
18
|
onBackToToolDetails: () => void;
|
|
19
|
+
/** Closes the overlay (run 12). */
|
|
20
|
+
onClose?: () => void;
|
|
7
21
|
}
|
|
8
|
-
export declare function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }: InspectorProps): import("react").DetailedReactHTMLElement<{
|
|
22
|
+
export declare function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps): import("react").DetailedReactHTMLElement<{
|
|
9
23
|
className: string;
|
|
24
|
+
style: CSSProperties;
|
|
10
25
|
}, HTMLElement>;
|
package/lib/client/slots.d.ts
CHANGED
|
@@ -42,5 +42,23 @@ export declare function RecursiveLauncherGate({ useSessions }: {
|
|
|
42
42
|
useSessions: SnapshotSelectorHook<SessionListStateLike>;
|
|
43
43
|
}): ReactNode;
|
|
44
44
|
export declare function registerSlots(ctx: ClientContext): () => void;
|
|
45
|
+
/**
|
|
46
|
+
* Board overlay: subscribes to the shared board store, gates on open + recursive
|
|
47
|
+
* preset, and swaps board <-> inspector (run 08 R1/R3).
|
|
48
|
+
*
|
|
49
|
+
* Run 10: consumes the full SessionListStateLike through the identity selector
|
|
50
|
+
* (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
|
|
51
|
+
*/
|
|
52
|
+
export declare function RecursiveBoardOverlay({ useSessions }: {
|
|
53
|
+
useSessions: SnapshotSelectorHook<SessionListStateLike>;
|
|
54
|
+
}): ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* Strip gate: same preset gate, session-scoped sessionId; the strip renders
|
|
57
|
+
* nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
|
|
58
|
+
*/
|
|
59
|
+
export declare function RecursiveStripGate({ useSessions, sessionId }: {
|
|
60
|
+
useSessions: SnapshotSelectorHook<SessionListStateLike>;
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
}): ReactNode;
|
|
45
63
|
/** Export the gate helpers for tests. */
|
|
46
64
|
export { isRecursivePreset, currentSessionCwd };
|
package/lib/client.js
CHANGED
|
@@ -219,18 +219,102 @@ window.__ModuleLoader__.load({
|
|
|
219
219
|
//#endregion
|
|
220
220
|
//#region src/client/board.tsx
|
|
221
221
|
/**
|
|
222
|
-
* Board overlay (SP2 R1 + run 08 R3): the kanban view over the live
|
|
223
|
-
* projection. Pure read-only renderer — no filesystem, no run-file
|
|
224
|
-
* Cards are grouped by worktree root (the projection's outer key) and
|
|
225
|
-
* their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
226
|
-
* snapshot is null (no recursive root / not loaded) or the current
|
|
227
|
-
* not on the recursive preset (the gate lives in slots.ts — here an
|
|
228
|
-
* projection with a null root is a no-op, not an "empty board").
|
|
222
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
223
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
224
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
225
|
+
* mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
226
|
+
* when the snapshot is null (no recursive root / not loaded) or the current
|
|
227
|
+
* session is not on the recursive preset (the gate lives in slots.ts — here an
|
|
228
|
+
* empty projection with a null root is a no-op, not an "empty board").
|
|
229
229
|
*
|
|
230
230
|
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
231
231
|
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
232
232
|
* callback only selects a run for display; no mutation.
|
|
233
|
+
*
|
|
234
|
+
* Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer —
|
|
235
|
+
* without inline styles the mounted board was INVISIBLE (no position/size/z)
|
|
236
|
+
* and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
|
|
237
|
+
* (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
|
|
238
|
+
* backdrop, pointerEvents auto, and a visible close button.
|
|
233
239
|
*/
|
|
240
|
+
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
241
|
+
const overlayStyle = {
|
|
242
|
+
position: "fixed",
|
|
243
|
+
inset: 0,
|
|
244
|
+
background: "rgba(15, 17, 21, 0.92)",
|
|
245
|
+
color: "#e6e6e6",
|
|
246
|
+
zIndex: 9999,
|
|
247
|
+
overflowY: "auto",
|
|
248
|
+
padding: "24px 32px",
|
|
249
|
+
pointerEvents: "auto",
|
|
250
|
+
fontFamily: "system-ui, sans-serif"
|
|
251
|
+
};
|
|
252
|
+
const laneStyle = { marginBottom: 24 };
|
|
253
|
+
const laneTitleStyle = {
|
|
254
|
+
fontSize: 14,
|
|
255
|
+
textTransform: "uppercase",
|
|
256
|
+
letterSpacing: "0.08em",
|
|
257
|
+
marginBottom: 8,
|
|
258
|
+
color: "#aaa"
|
|
259
|
+
};
|
|
260
|
+
const cardStyle = {
|
|
261
|
+
display: "inline-block",
|
|
262
|
+
width: 280,
|
|
263
|
+
margin: "0 12px 12px 0",
|
|
264
|
+
padding: 12,
|
|
265
|
+
background: "#1f2329",
|
|
266
|
+
borderRadius: 8,
|
|
267
|
+
border: "1px solid #333",
|
|
268
|
+
cursor: "pointer",
|
|
269
|
+
verticalAlign: "top"
|
|
270
|
+
};
|
|
271
|
+
const cardHeadStyle = {
|
|
272
|
+
display: "flex",
|
|
273
|
+
justifyContent: "space-between",
|
|
274
|
+
marginBottom: 8
|
|
275
|
+
};
|
|
276
|
+
const badgeStyle$1 = {
|
|
277
|
+
fontSize: 11,
|
|
278
|
+
padding: "2px 6px",
|
|
279
|
+
borderRadius: 4,
|
|
280
|
+
background: "#333"
|
|
281
|
+
};
|
|
282
|
+
const badgeTamperStyle = { background: "#7a1f1f" };
|
|
283
|
+
const progressStyle = {
|
|
284
|
+
height: 6,
|
|
285
|
+
background: "#333",
|
|
286
|
+
borderRadius: 3,
|
|
287
|
+
overflow: "hidden"
|
|
288
|
+
};
|
|
289
|
+
const progressFillStyle = {
|
|
290
|
+
background: "#4c9aff",
|
|
291
|
+
height: "100%"
|
|
292
|
+
};
|
|
293
|
+
const metaStyle = {
|
|
294
|
+
marginTop: 8,
|
|
295
|
+
fontSize: 12,
|
|
296
|
+
color: "#aaa",
|
|
297
|
+
display: "flex",
|
|
298
|
+
justifyContent: "space-between"
|
|
299
|
+
};
|
|
300
|
+
const emptyStyle = {
|
|
301
|
+
padding: 40,
|
|
302
|
+
textAlign: "center",
|
|
303
|
+
color: "#888"
|
|
304
|
+
};
|
|
305
|
+
const closeStyle$1 = {
|
|
306
|
+
position: "absolute",
|
|
307
|
+
top: 12,
|
|
308
|
+
right: 16,
|
|
309
|
+
background: "#1f2329",
|
|
310
|
+
color: "#e6e6e6",
|
|
311
|
+
border: "1px solid #444",
|
|
312
|
+
borderRadius: 6,
|
|
313
|
+
padding: "4px 12px",
|
|
314
|
+
cursor: "pointer",
|
|
315
|
+
fontSize: 16,
|
|
316
|
+
zIndex: 1
|
|
317
|
+
};
|
|
234
318
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
235
319
|
function listRuns(projection) {
|
|
236
320
|
if (projection === void 0) return [];
|
|
@@ -238,57 +322,220 @@ window.__ModuleLoader__.load({
|
|
|
238
322
|
for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
|
|
239
323
|
return runs;
|
|
240
324
|
}
|
|
241
|
-
function Board({ snapshot, onOpenInspector }) {
|
|
325
|
+
function Board({ snapshot, onOpenInspector, onClose }) {
|
|
242
326
|
if (snapshot === null || snapshot.root === null) return null;
|
|
243
327
|
const runs = listRuns(snapshot.projection);
|
|
328
|
+
const close = onClose !== void 0 ? (0, react.createElement)("button", {
|
|
329
|
+
className: "rec-close",
|
|
330
|
+
style: closeStyle$1,
|
|
331
|
+
onClick: onClose,
|
|
332
|
+
title: "Close"
|
|
333
|
+
}, "×") : null;
|
|
244
334
|
if (runs.length === 0) return (0, react.createElement)("div", {
|
|
245
335
|
className: "rec-board",
|
|
246
|
-
"data-empty": true
|
|
247
|
-
|
|
336
|
+
"data-empty": true,
|
|
337
|
+
style: overlayStyle
|
|
338
|
+
}, close, (0, react.createElement)("p", {
|
|
339
|
+
className: "rec-board-empty",
|
|
340
|
+
style: emptyStyle
|
|
341
|
+
}, "No recursive runs in this workspace yet."));
|
|
248
342
|
const open = (run) => () => onOpenInspector?.({
|
|
249
343
|
worktreeRoot: run.worktreeRoot,
|
|
250
344
|
runId: run.runId
|
|
251
345
|
});
|
|
252
|
-
return (0, react.createElement)("div", {
|
|
346
|
+
return (0, react.createElement)("div", {
|
|
347
|
+
className: "rec-board",
|
|
348
|
+
style: overlayStyle
|
|
349
|
+
}, close, KANBAN_LANES.map((lane) => {
|
|
253
350
|
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
|
|
254
351
|
return (0, react.createElement)("section", {
|
|
255
352
|
key: lane.id,
|
|
256
|
-
className: "rec-lane"
|
|
257
|
-
|
|
353
|
+
className: "rec-lane",
|
|
354
|
+
style: laneStyle
|
|
355
|
+
}, (0, react.createElement)("h2", {
|
|
356
|
+
className: "rec-lane-title",
|
|
357
|
+
style: laneTitleStyle
|
|
358
|
+
}, lane.label), laneRuns.map((run) => {
|
|
258
359
|
const facts = cardFacts(run);
|
|
259
360
|
return (0, react.createElement)("article", {
|
|
260
361
|
key: run.worktreeRoot + "\0" + run.runId,
|
|
261
362
|
className: "rec-card",
|
|
363
|
+
style: cardStyle,
|
|
262
364
|
onClick: open(run)
|
|
263
|
-
}, (0, react.createElement)("div", {
|
|
365
|
+
}, (0, react.createElement)("div", {
|
|
366
|
+
className: "rec-card-head",
|
|
367
|
+
style: cardHeadStyle
|
|
368
|
+
}, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
|
|
264
369
|
className: "rec-badge rec-badge-tamper",
|
|
370
|
+
style: {
|
|
371
|
+
...badgeStyle$1,
|
|
372
|
+
...badgeTamperStyle
|
|
373
|
+
},
|
|
265
374
|
title: "tampered"
|
|
266
|
-
}, "!")), (0, react.createElement)("div", {
|
|
375
|
+
}, "!")), (0, react.createElement)("div", {
|
|
376
|
+
className: "rec-card-progress",
|
|
377
|
+
style: progressStyle
|
|
378
|
+
}, (0, react.createElement)("div", {
|
|
267
379
|
className: "rec-card-progress-fill",
|
|
268
|
-
style: {
|
|
269
|
-
|
|
380
|
+
style: {
|
|
381
|
+
...progressFillStyle,
|
|
382
|
+
width: Math.round(facts.progress * 100) + "%"
|
|
383
|
+
}
|
|
384
|
+
})), (0, react.createElement)("div", {
|
|
385
|
+
className: "rec-card-meta",
|
|
386
|
+
style: metaStyle
|
|
387
|
+
}, (0, react.createElement)("span", {}, facts.currentPhase ?? "no phases"), (0, react.createElement)("span", {
|
|
388
|
+
className: "rec-badge",
|
|
389
|
+
style: badgeStyle$1
|
|
390
|
+
}, facts.state)));
|
|
270
391
|
}));
|
|
271
392
|
}));
|
|
272
393
|
}
|
|
273
394
|
//#endregion
|
|
274
395
|
//#region src/client/inspector.tsx
|
|
275
396
|
/**
|
|
276
|
-
* Drill-down inspector (SP2 R1): the details-seat swap rendering one
|
|
277
|
-
* phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
278
|
-
* and tamper/gate facts — all from the live host-route snapshot, never
|
|
397
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
398
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
399
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
400
|
+
* disk.
|
|
401
|
+
*
|
|
402
|
+
* Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
|
|
403
|
+
* unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
|
|
404
|
+
* styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
|
|
405
|
+
* close button.
|
|
279
406
|
*/
|
|
280
|
-
|
|
407
|
+
const headStyle = {
|
|
408
|
+
display: "flex",
|
|
409
|
+
alignItems: "center",
|
|
410
|
+
gap: 12,
|
|
411
|
+
marginBottom: 16,
|
|
412
|
+
borderBottom: "1px solid #333",
|
|
413
|
+
paddingBottom: 12
|
|
414
|
+
};
|
|
415
|
+
const backStyle = {
|
|
416
|
+
background: "#1f2329",
|
|
417
|
+
color: "#e6e6e6",
|
|
418
|
+
border: "1px solid #444",
|
|
419
|
+
borderRadius: 6,
|
|
420
|
+
padding: "6px 12px",
|
|
421
|
+
cursor: "pointer"
|
|
422
|
+
};
|
|
423
|
+
const closeStyle = {
|
|
424
|
+
background: "#1f2329",
|
|
425
|
+
color: "#e6e6e6",
|
|
426
|
+
border: "1px solid #444",
|
|
427
|
+
borderRadius: 6,
|
|
428
|
+
padding: "6px 12px",
|
|
429
|
+
cursor: "pointer",
|
|
430
|
+
marginLeft: "auto"
|
|
431
|
+
};
|
|
432
|
+
const sectionStyle = { marginBottom: 16 };
|
|
433
|
+
const rowStyle = {
|
|
434
|
+
display: "flex",
|
|
435
|
+
gap: 12,
|
|
436
|
+
alignItems: "center",
|
|
437
|
+
padding: "6px 0",
|
|
438
|
+
borderBottom: "1px solid #222",
|
|
439
|
+
fontSize: 13
|
|
440
|
+
};
|
|
441
|
+
const phaseIdStyle = {
|
|
442
|
+
minWidth: 220,
|
|
443
|
+
fontFamily: "monospace",
|
|
444
|
+
color: "#4c9aff"
|
|
445
|
+
};
|
|
446
|
+
const phaseNameStyle = {
|
|
447
|
+
flex: 1,
|
|
448
|
+
color: "#ccc"
|
|
449
|
+
};
|
|
450
|
+
const badgeStyle = {
|
|
451
|
+
fontSize: 11,
|
|
452
|
+
padding: "2px 6px",
|
|
453
|
+
borderRadius: 4,
|
|
454
|
+
background: "#333"
|
|
455
|
+
};
|
|
456
|
+
const lockhashStyle = {
|
|
457
|
+
fontFamily: "monospace",
|
|
458
|
+
fontSize: 11,
|
|
459
|
+
color: "#888"
|
|
460
|
+
};
|
|
461
|
+
function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
|
|
462
|
+
const close = onClose !== void 0 ? (0, react.createElement)("button", {
|
|
463
|
+
className: "rec-close",
|
|
464
|
+
style: closeStyle,
|
|
465
|
+
onClick: onClose,
|
|
466
|
+
title: "Close"
|
|
467
|
+
}, "×") : null;
|
|
281
468
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId];
|
|
282
|
-
if (card === void 0) return (0, react.createElement)("div", {
|
|
469
|
+
if (card === void 0) return (0, react.createElement)("div", {
|
|
470
|
+
className: "rec-inspector",
|
|
471
|
+
style: overlayStyle
|
|
472
|
+
}, (0, react.createElement)("div", {
|
|
473
|
+
className: "rec-inspector-head",
|
|
474
|
+
style: headStyle
|
|
475
|
+
}, (0, react.createElement)("button", {
|
|
476
|
+
onClick: onBackToToolDetails,
|
|
477
|
+
className: "rec-back",
|
|
478
|
+
style: backStyle
|
|
479
|
+
}, "← back to tool details"), close), (0, react.createElement)("p", {}, "Run not found: " + runId));
|
|
283
480
|
const facts = cardFacts(card);
|
|
284
481
|
const rows = expandPhaseRows(card);
|
|
285
|
-
return (0, react.createElement)("div", {
|
|
482
|
+
return (0, react.createElement)("div", {
|
|
483
|
+
className: "rec-inspector",
|
|
484
|
+
style: overlayStyle
|
|
485
|
+
}, (0, react.createElement)("div", {
|
|
486
|
+
className: "rec-inspector-head",
|
|
487
|
+
style: headStyle
|
|
488
|
+
}, (0, react.createElement)("button", {
|
|
286
489
|
onClick: onBackToToolDetails,
|
|
287
|
-
className: "rec-back"
|
|
288
|
-
|
|
490
|
+
className: "rec-back",
|
|
491
|
+
style: backStyle
|
|
492
|
+
}, "← back to tool details"), (0, react.createElement)("h2", { style: {
|
|
493
|
+
margin: 0,
|
|
494
|
+
fontSize: 18
|
|
495
|
+
} }, runId), (0, react.createElement)("span", {
|
|
496
|
+
className: "rec-badge",
|
|
497
|
+
style: badgeStyle
|
|
498
|
+
}, facts.state), facts.tampered && (0, react.createElement)("span", {
|
|
499
|
+
className: "rec-badge rec-badge-tamper",
|
|
500
|
+
style: {
|
|
501
|
+
...badgeStyle,
|
|
502
|
+
background: "#7a1f1f"
|
|
503
|
+
}
|
|
504
|
+
}, "tampered"), close), (0, react.createElement)("section", {
|
|
505
|
+
className: "rec-inspector-section",
|
|
506
|
+
style: sectionStyle
|
|
507
|
+
}, (0, react.createElement)("h3", { style: {
|
|
508
|
+
fontSize: 13,
|
|
509
|
+
textTransform: "uppercase",
|
|
510
|
+
letterSpacing: "0.08em",
|
|
511
|
+
color: "#aaa",
|
|
512
|
+
marginBottom: 8
|
|
513
|
+
} }, "Phases"), rows.map((row) => (0, react.createElement)("div", {
|
|
289
514
|
key: row.phase,
|
|
290
|
-
className: "rec-phase-row"
|
|
291
|
-
|
|
515
|
+
className: "rec-phase-row",
|
|
516
|
+
style: rowStyle
|
|
517
|
+
}, (0, react.createElement)("span", {
|
|
518
|
+
className: "rec-phase-id",
|
|
519
|
+
style: phaseIdStyle
|
|
520
|
+
}, row.phase), (0, react.createElement)("span", {
|
|
521
|
+
className: "rec-phase-name",
|
|
522
|
+
style: phaseNameStyle
|
|
523
|
+
}, row.fileName ?? "—"), (0, react.createElement)("span", {
|
|
524
|
+
className: "rec-badge",
|
|
525
|
+
style: badgeStyle
|
|
526
|
+
}, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", {
|
|
527
|
+
className: "rec-lockhash",
|
|
528
|
+
style: lockhashStyle
|
|
529
|
+
}, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", {
|
|
530
|
+
className: "rec-inspector-section",
|
|
531
|
+
style: sectionStyle
|
|
532
|
+
}, (0, react.createElement)("h3", { style: {
|
|
533
|
+
fontSize: 13,
|
|
534
|
+
textTransform: "uppercase",
|
|
535
|
+
letterSpacing: "0.08em",
|
|
536
|
+
color: "#aaa",
|
|
537
|
+
marginBottom: 8
|
|
538
|
+
} }, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
|
|
292
539
|
}
|
|
293
540
|
//#endregion
|
|
294
541
|
//#region src/client/strip.tsx
|
|
@@ -597,21 +844,23 @@ window.__ModuleLoader__.load({
|
|
|
597
844
|
const board = useBoardState();
|
|
598
845
|
const list = useRecursiveSessions(useSessions);
|
|
599
846
|
const content = overlayContent(board, list);
|
|
600
|
-
if (content === "hidden") return null;
|
|
601
847
|
const cwd = currentSessionCwd(list);
|
|
602
848
|
const snapshot = useLiveProjection({
|
|
603
849
|
sessionId: list.current,
|
|
604
850
|
cwd
|
|
605
851
|
});
|
|
852
|
+
if (content === "hidden") return null;
|
|
606
853
|
if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
|
|
607
854
|
runId: board.selection.runId,
|
|
608
855
|
worktreeRoot: board.selection.worktreeRoot,
|
|
609
856
|
snapshot,
|
|
610
|
-
onBackToToolDetails: () => boardState.backToBoard()
|
|
857
|
+
onBackToToolDetails: () => boardState.backToBoard(),
|
|
858
|
+
onClose: () => boardState.close()
|
|
611
859
|
});
|
|
612
860
|
return (0, react.createElement)(Board, {
|
|
613
861
|
snapshot,
|
|
614
|
-
onOpenInspector: (sel) => boardState.openInspector(sel)
|
|
862
|
+
onOpenInspector: (sel) => boardState.openInspector(sel),
|
|
863
|
+
onClose: () => boardState.close()
|
|
615
864
|
});
|
|
616
865
|
}
|
|
617
866
|
/**
|
|
@@ -620,11 +869,11 @@ window.__ModuleLoader__.load({
|
|
|
620
869
|
*/
|
|
621
870
|
function RecursiveStripGate({ useSessions, sessionId }) {
|
|
622
871
|
const list = useRecursiveSessions(useSessions);
|
|
623
|
-
if (!isRecursivePreset(list)) return null;
|
|
624
872
|
const snapshot = useLiveProjection({
|
|
625
873
|
sessionId,
|
|
626
874
|
cwd: currentSessionCwd(list)
|
|
627
875
|
});
|
|
876
|
+
if (!isRecursivePreset(list)) return null;
|
|
628
877
|
return (0, react.createElement)(StatusStrip, { snapshot });
|
|
629
878
|
}
|
|
630
879
|
//#endregion
|
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.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.5",
|
|
49
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
|
|
50
|
-
"@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
|
|
51
48
|
"@deepseek-ai/dsh-session": "0.1.0-rc.5",
|
|
49
|
+
"@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
|
|
50
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
|
|
51
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.5",
|
|
52
52
|
"react": "^18.2.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
@@ -71,7 +71,9 @@
|
|
|
71
71
|
"@types/node": "^20.0.0",
|
|
72
72
|
"@types/react": "^18.2.0",
|
|
73
73
|
"@types/react-dom": "^18.2.0",
|
|
74
|
+
"@types/react-test-renderer": "18.3.0",
|
|
74
75
|
"react": "^18.2.0",
|
|
76
|
+
"react-test-renderer": "18.3.1",
|
|
75
77
|
"tsdown": "^0.22.2",
|
|
76
78
|
"tsx": "^4.19.0",
|
|
77
79
|
"typescript": "^5.6.0",
|
package/src/client/board.tsx
CHANGED
|
@@ -1,22 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Board overlay (SP2 R1 + run 08 R3): the kanban view over the live
|
|
3
|
-
* projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
-
* Cards are grouped by worktree root (the projection's outer key) and
|
|
5
|
-
* their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
6
|
-
* snapshot is null (no recursive root / not loaded) or the current
|
|
7
|
-
* not on the recursive preset (the gate lives in slots.ts — here an
|
|
8
|
-
* projection with a null root is a no-op, not an "empty board").
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
3
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
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").
|
|
9
9
|
*
|
|
10
10
|
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
11
11
|
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
12
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.
|
|
13
19
|
*/
|
|
14
|
-
import { createElement } from 'react'
|
|
20
|
+
import { createElement, type CSSProperties } from 'react'
|
|
15
21
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts'
|
|
16
22
|
import type { LiveProjectionValue } from './contract.ts'
|
|
17
23
|
import { cardFacts, columnForRun, KANBAN_LANES } from './derive.ts'
|
|
18
24
|
import type { BoardSelection } from './open-state.ts'
|
|
19
25
|
|
|
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
|
+
|
|
20
73
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
21
74
|
export function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[] {
|
|
22
75
|
if (projection === undefined) return []
|
|
@@ -34,35 +87,40 @@ export interface BoardProps {
|
|
|
34
87
|
snapshot: LiveProjectionValue | null
|
|
35
88
|
/** Opens the drill-down inspector for a run (R3). */
|
|
36
89
|
onOpenInspector?: (selection: BoardSelection) => void
|
|
90
|
+
/** Closes the overlay (run 12). */
|
|
91
|
+
onClose?: () => void
|
|
37
92
|
}
|
|
38
93
|
|
|
39
|
-
export function Board({ snapshot, onOpenInspector }: BoardProps) {
|
|
94
|
+
export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
40
95
|
if (snapshot === null || snapshot.root === null) return null
|
|
41
96
|
const runs = listRuns(snapshot.projection)
|
|
97
|
+
const close = onClose !== undefined ? createElement('button', { className: 'rec-close', style: closeStyle, onClick: onClose, title: 'Close' }, '×') : null
|
|
42
98
|
if (runs.length === 0) {
|
|
43
|
-
return createElement('div', { className: 'rec-board', 'data-empty': true },
|
|
44
|
-
|
|
99
|
+
return createElement('div', { className: 'rec-board', 'data-empty': true, style: overlayStyle },
|
|
100
|
+
close,
|
|
101
|
+
createElement('p', { className: 'rec-board-empty', style: emptyStyle }, 'No recursive runs in this workspace yet.'),
|
|
45
102
|
)
|
|
46
103
|
}
|
|
47
104
|
const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
|
|
48
|
-
return createElement('div', { className: 'rec-board' },
|
|
105
|
+
return createElement('div', { className: 'rec-board', style: overlayStyle },
|
|
106
|
+
close,
|
|
49
107
|
KANBAN_LANES.map((lane) => {
|
|
50
108
|
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id)
|
|
51
|
-
return createElement('section', { key: lane.id, className: 'rec-lane' },
|
|
52
|
-
createElement('h2', { className: 'rec-lane-title' }, lane.label),
|
|
109
|
+
return createElement('section', { key: lane.id, className: 'rec-lane', style: laneStyle },
|
|
110
|
+
createElement('h2', { className: 'rec-lane-title', style: laneTitleStyle }, lane.label),
|
|
53
111
|
laneRuns.map((run) => {
|
|
54
112
|
const facts = cardFacts(run)
|
|
55
|
-
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', onClick: open(run) },
|
|
56
|
-
createElement('div', { className: 'rec-card-head' },
|
|
113
|
+
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', style: cardStyle, onClick: open(run) },
|
|
114
|
+
createElement('div', { className: 'rec-card-head', style: cardHeadStyle },
|
|
57
115
|
createElement('strong', {}, run.runId),
|
|
58
|
-
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', title: 'tampered' }, '!'),
|
|
116
|
+
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', style: { ...badgeStyle, ...badgeTamperStyle }, title: 'tampered' }, '!'),
|
|
59
117
|
),
|
|
60
|
-
createElement('div', { className: 'rec-card-progress' },
|
|
61
|
-
createElement('div', { className: 'rec-card-progress-fill', style: { width: Math.round(facts.progress * 100) + '%' } }),
|
|
118
|
+
createElement('div', { className: 'rec-card-progress', style: progressStyle },
|
|
119
|
+
createElement('div', { className: 'rec-card-progress-fill', style: { ...progressFillStyle, width: Math.round(facts.progress * 100) + '%' } }),
|
|
62
120
|
),
|
|
63
|
-
createElement('div', { className: 'rec-card-meta' },
|
|
121
|
+
createElement('div', { className: 'rec-card-meta', style: metaStyle },
|
|
64
122
|
createElement('span', {}, facts.currentPhase ?? 'no phases'),
|
|
65
|
-
createElement('span', { className: 'rec-badge' }, facts.state),
|
|
123
|
+
createElement('span', { className: 'rec-badge', style: badgeStyle }, facts.state),
|
|
66
124
|
),
|
|
67
125
|
)
|
|
68
126
|
}),
|
package/src/client/inspector.tsx
CHANGED
|
@@ -1,46 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (SP2 R1): the details-seat swap rendering one
|
|
3
|
-
* phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
-
* and tamper/gate facts — all from the live host-route snapshot, never
|
|
2
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
3
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
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.
|
|
5
11
|
*/
|
|
6
|
-
import { createElement } from 'react'
|
|
12
|
+
import { createElement, type CSSProperties } from 'react'
|
|
7
13
|
import type { LiveProjectionValue } from './contract.ts'
|
|
8
14
|
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' }
|
|
9
26
|
|
|
10
27
|
export interface InspectorProps {
|
|
11
28
|
runId: string
|
|
12
29
|
worktreeRoot: string
|
|
13
30
|
snapshot: LiveProjectionValue | null
|
|
14
31
|
onBackToToolDetails: () => void
|
|
32
|
+
/** Closes the overlay (run 12). */
|
|
33
|
+
onClose?: () => void
|
|
15
34
|
}
|
|
16
35
|
|
|
17
|
-
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }: InspectorProps) {
|
|
36
|
+
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
|
|
18
38
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
19
39
|
if (card === undefined) {
|
|
20
|
-
return createElement('div', { className: 'rec-inspector' },
|
|
40
|
+
return createElement('div', { className: 'rec-inspector', style: overlayStyle },
|
|
41
|
+
createElement('div', { className: 'rec-inspector-head', style: headStyle },
|
|
42
|
+
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back', style: backStyle }, '← back to tool details'),
|
|
43
|
+
close,
|
|
44
|
+
),
|
|
21
45
|
createElement('p', {}, 'Run not found: ' + runId),
|
|
22
46
|
)
|
|
23
47
|
}
|
|
24
48
|
const facts = cardFacts(card)
|
|
25
49
|
const rows = expandPhaseRows(card)
|
|
26
|
-
return createElement('div', { className: 'rec-inspector' },
|
|
27
|
-
createElement('div', { className: 'rec-inspector-head' },
|
|
28
|
-
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back' }, '← back to tool details'),
|
|
29
|
-
createElement('h2', {}, runId),
|
|
30
|
-
createElement('span', { className: 'rec-badge' }, facts.state),
|
|
31
|
-
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper' }, 'tampered'),
|
|
50
|
+
return createElement('div', { className: 'rec-inspector', style: overlayStyle },
|
|
51
|
+
createElement('div', { className: 'rec-inspector-head', style: headStyle },
|
|
52
|
+
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back', style: backStyle }, '← back to tool details'),
|
|
53
|
+
createElement('h2', { style: { margin: 0, fontSize: 18 } }, runId),
|
|
54
|
+
createElement('span', { className: 'rec-badge', style: badgeStyle }, facts.state),
|
|
55
|
+
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', style: { ...badgeStyle, background: '#7a1f1f' } }, 'tampered'),
|
|
56
|
+
close,
|
|
32
57
|
),
|
|
33
|
-
createElement('section', { className: 'rec-inspector-section' },
|
|
34
|
-
createElement('h3', {}, 'Phases'),
|
|
35
|
-
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row' },
|
|
36
|
-
createElement('span', { className: 'rec-phase-id' }, row.phase),
|
|
37
|
-
createElement('span', { className: 'rec-phase-name' }, row.fileName ?? '—'),
|
|
38
|
-
createElement('span', { className: 'rec-badge' }, row.status),
|
|
39
|
-
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)),
|
|
58
|
+
createElement('section', { className: 'rec-inspector-section', style: sectionStyle },
|
|
59
|
+
createElement('h3', { style: { fontSize: 13, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#aaa', marginBottom: 8 } }, 'Phases'),
|
|
60
|
+
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row', style: rowStyle },
|
|
61
|
+
createElement('span', { className: 'rec-phase-id', style: phaseIdStyle }, row.phase),
|
|
62
|
+
createElement('span', { className: 'rec-phase-name', style: phaseNameStyle }, row.fileName ?? '—'),
|
|
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)),
|
|
40
65
|
)),
|
|
41
66
|
),
|
|
42
|
-
facts.gateBlocked && createElement('section', { className: 'rec-inspector-section' },
|
|
43
|
-
createElement('h3', {}, 'Gate'),
|
|
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'),
|
|
44
69
|
createElement('p', {}, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
45
70
|
),
|
|
46
71
|
)
|
package/src/client/slots.ts
CHANGED
|
@@ -129,35 +129,40 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
129
129
|
* Run 10: consumes the full SessionListStateLike through the identity selector
|
|
130
130
|
* (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
|
|
131
131
|
*/
|
|
132
|
-
function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
132
|
+
export function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
133
133
|
const board = useBoardState()
|
|
134
134
|
const list = useRecursiveSessions(useSessions)
|
|
135
135
|
const content = overlayContent(board, list)
|
|
136
|
-
if (content === 'hidden') return null
|
|
137
136
|
const cwd = currentSessionCwd(list)
|
|
138
137
|
const scope = { sessionId: list.current, cwd }
|
|
138
|
+
// Run 13 (LIVE BUG): ALWAYS call useLiveProjection — a conditional hook (after the
|
|
139
|
+
// hidden early return) changed the hook count across renders and threw
|
|
140
|
+
// 'Rendered more hooks than during the previous render'.
|
|
139
141
|
const snapshot = useLiveProjection(scope)
|
|
142
|
+
if (content === 'hidden') return null
|
|
140
143
|
if (content === 'inspector' && board.selection !== null) {
|
|
141
144
|
return createElement(Inspector, {
|
|
142
145
|
runId: board.selection.runId,
|
|
143
146
|
worktreeRoot: board.selection.worktreeRoot,
|
|
144
147
|
snapshot,
|
|
145
148
|
onBackToToolDetails: () => boardState.backToBoard(),
|
|
149
|
+
onClose: () => boardState.close(),
|
|
146
150
|
})
|
|
147
151
|
}
|
|
148
|
-
return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
|
|
152
|
+
return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
152
156
|
* Strip gate: same preset gate, session-scoped sessionId; the strip renders
|
|
153
157
|
* nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
|
|
154
158
|
*/
|
|
155
|
-
function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
|
|
159
|
+
export function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
|
|
156
160
|
const list = useRecursiveSessions(useSessions)
|
|
157
|
-
if (!isRecursivePreset(list)) return null
|
|
158
161
|
const cwd = currentSessionCwd(list)
|
|
159
162
|
const scope = { sessionId, cwd }
|
|
163
|
+
// Run 13: ALWAYS call useLiveProjection BEFORE the preset early return.
|
|
160
164
|
const snapshot = useLiveProjection(scope)
|
|
165
|
+
if (!isRecursivePreset(list)) return null
|
|
161
166
|
return createElement(StatusStrip, { snapshot })
|
|
162
167
|
}
|
|
163
168
|
|