@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.
@@ -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>;
@@ -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 host-route
223
- * projection. Pure read-only renderer — no filesystem, no run-file scraping.
224
- * Cards are grouped by worktree root (the projection's outer key) and mapped to
225
- * their current-phase lane via derive.columnForRun. Renders NOTHING when the
226
- * snapshot is null (no recursive root / not loaded) or the current session is
227
- * not on the recursive preset (the gate lives in slots.ts — here an empty
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
- }, (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
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", { className: "rec-board" }, KANBAN_LANES.map((lane) => {
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
- }, (0, react.createElement)("h2", { className: "rec-lane-title" }, lane.label), laneRuns.map((run) => {
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", { className: "rec-card-head" }, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
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", { className: "rec-card-progress" }, (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: { width: Math.round(facts.progress * 100) + "%" }
269
- })), (0, react.createElement)("div", { className: "rec-card-meta" }, (0, react.createElement)("span", {}, facts.currentPhase ?? "no phases"), (0, react.createElement)("span", { className: "rec-badge" }, facts.state)));
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 run's
277
- * phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock badges,
278
- * and tamper/gate facts — all from the live host-route snapshot, never disk.
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
- function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }) {
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", { className: "rec-inspector" }, (0, react.createElement)("p", {}, "Run not found: " + runId));
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", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-inspector-head" }, (0, react.createElement)("button", {
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
- }, "← back to tool details"), (0, react.createElement)("h2", {}, runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.state), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered")), (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
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
- }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", { className: "rec-badge" }, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
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.8",
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",
@@ -1,22 +1,75 @@
1
1
  /**
2
- * Board overlay (SP2 R1 + run 08 R3): the kanban view over the live host-route
3
- * projection. Pure read-only renderer — no filesystem, no run-file scraping.
4
- * Cards are grouped by worktree root (the projection's outer key) and mapped to
5
- * their current-phase lane via derive.columnForRun. Renders NOTHING when the
6
- * snapshot is null (no recursive root / not loaded) or the current session is
7
- * not on the recursive preset (the gate lives in slots.ts — here an empty
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
- createElement('p', { className: 'rec-board-empty' }, 'No recursive runs in this workspace yet.'),
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
  }),
@@ -1,46 +1,71 @@
1
1
  /**
2
- * Drill-down inspector (SP2 R1): the details-seat swap rendering one run's
3
- * phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock badges,
4
- * and tamper/gate facts — all from the live host-route snapshot, never disk.
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
  )
@@ -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