@try-works/dsh-recursive-mode 0.1.7 → 0.1.9

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>;
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
3
+ * shell.overlay board, the conversation.input.dock status strip, and the
4
+ * settings.section page.
5
+ *
6
+ * Run 08: the launcher now OPENS the shared board store (onClick), and the
7
+ * shell.overlay entry renders the board gated on that store + the recursive
8
+ * preset, swapping to the drill-down inspector when a run is selected.
9
+ *
10
+ * Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
11
+ * thunk) — it is called WITH a selector, e.g. useSessions((s) => s) for the
12
+ * full SessionListStateLike. The old code called useSessions() with NO selector
13
+ * and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
14
+ * never saw the recursive preset and the board never mounted (launcher click
15
+ * did nothing). The board/strip now consume the SessionListStateLike directly.
16
+ *
17
+ * READ-ONLY (R9): the client only GETs the live host route; no mutation.
18
+ */
19
+ import { type ReactNode } from 'react';
1
20
  import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts';
2
21
  import { isRecursivePreset, currentSessionCwd } from './contract.ts';
3
22
  export type OverlayContent = 'hidden' | 'board' | 'inspector';
@@ -12,6 +31,16 @@ export declare function overlayContent(state: {
12
31
  * Exported so the spec can assert the selector is actually passed.
13
32
  */
14
33
  export declare function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike;
34
+ /**
35
+ * Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
36
+ * is root-scoped (the icon shows in every session otherwise), while the board it
37
+ * opens is recursive-preset-gated — in a code/other session the click was a dead
38
+ * no-op. Gating the launcher itself removes the trap: the icon appears exactly
39
+ * where clicking it opens the board.
40
+ */
41
+ export declare function RecursiveLauncherGate({ useSessions }: {
42
+ useSessions: SnapshotSelectorHook<SessionListStateLike>;
43
+ }): ReactNode;
15
44
  export declare function registerSlots(ctx: ClientContext): () => void;
16
45
  /** Export the gate helpers for tests. */
17
46
  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
@@ -528,6 +775,21 @@ window.__ModuleLoader__.load({
528
775
  function useRecursiveSessions(useSessions) {
529
776
  return useSessions((s) => s);
530
777
  }
778
+ /**
779
+ * Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
780
+ * is root-scoped (the icon shows in every session otherwise), while the board it
781
+ * opens is recursive-preset-gated — in a code/other session the click was a dead
782
+ * no-op. Gating the launcher itself removes the trap: the icon appears exactly
783
+ * where clicking it opens the board.
784
+ */
785
+ function RecursiveLauncherGate({ useSessions }) {
786
+ if (!isRecursivePreset(useRecursiveSessions(useSessions))) return null;
787
+ return (0, react.createElement)("button", {
788
+ className: "rec-launcher",
789
+ title: "Recursive runs",
790
+ onClick: () => boardState.openBoard()
791
+ }, "⧉");
792
+ }
531
793
  function registerSlots(ctx) {
532
794
  const disposers = [];
533
795
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
@@ -535,11 +797,11 @@ window.__ModuleLoader__.load({
535
797
  id: "recursive",
536
798
  order: 50,
537
799
  label: "Recursive runs"
538
- }, () => (0, react.createElement)("button", {
539
- className: "rec-launcher",
540
- title: "Recursive runs",
541
- onClick: () => boardState.openBoard()
542
- }, "⧉"))));
800
+ }, (props) => {
801
+ const useSessions = props?.useSessions;
802
+ if (useSessions === void 0) return null;
803
+ return (0, react.createElement)(RecursiveLauncherGate, { useSessions });
804
+ })));
543
805
  disposers.push(ctx.slots.inject("shell.overlay", () => ctx.slots.register({
544
806
  name: "shell.overlay",
545
807
  id: "recursive-board",
@@ -592,11 +854,13 @@ window.__ModuleLoader__.load({
592
854
  runId: board.selection.runId,
593
855
  worktreeRoot: board.selection.worktreeRoot,
594
856
  snapshot,
595
- onBackToToolDetails: () => boardState.backToBoard()
857
+ onBackToToolDetails: () => boardState.backToBoard(),
858
+ onClose: () => boardState.close()
596
859
  });
597
860
  return (0, react.createElement)(Board, {
598
861
  snapshot,
599
- onOpenInspector: (sel) => boardState.openInspector(sel)
862
+ onOpenInspector: (sel) => boardState.openInspector(sel),
863
+ onClose: () => boardState.close()
600
864
  });
601
865
  }
602
866
  /**
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.7",
4
+ "version": "0.1.9",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -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
  )
@@ -58,16 +58,36 @@ export function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionLi
58
58
  return useSessions((s) => s)
59
59
  }
60
60
 
61
+ /**
62
+ * Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
63
+ * is root-scoped (the icon shows in every session otherwise), while the board it
64
+ * opens is recursive-preset-gated — in a code/other session the click was a dead
65
+ * no-op. Gating the launcher itself removes the trap: the icon appears exactly
66
+ * where clicking it opens the board.
67
+ */
68
+ export function RecursiveLauncherGate({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
69
+ const list = useRecursiveSessions(useSessions)
70
+ if (!isRecursivePreset(list)) return null
71
+ return createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉')
72
+ }
73
+
61
74
  export function registerSlots(ctx: ClientContext): () => void {
62
75
  const disposers: (() => void)[] = []
63
76
 
64
77
  // Board launcher in the sidebar footer action list — OPENS the shared board store (run 08 R1).
78
+ // Run 11 (UX gate): the seat is root-scoped (visible in every session), but the board it
79
+ // opens is recursive-preset-gated. In a code/other session the icon was a dead click — a
80
+ // trap. The launcher itself now renders ONLY in recursive sessions, where it works.
65
81
  disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
66
82
  name: 'sidebar.footer.action',
67
83
  id: 'recursive',
68
84
  order: 50,
69
85
  label: 'Recursive runs',
70
- }, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉'))))
86
+ }, (props: RootSlotProps) => {
87
+ const useSessions = props?.useSessions
88
+ if (useSessions === undefined) return null
89
+ return createElement(RecursiveLauncherGate, { useSessions })
90
+ })))
71
91
 
72
92
  // Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
73
93
  disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
@@ -123,9 +143,10 @@ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorH
123
143
  worktreeRoot: board.selection.worktreeRoot,
124
144
  snapshot,
125
145
  onBackToToolDetails: () => boardState.backToBoard(),
146
+ onClose: () => boardState.close(),
126
147
  })
127
148
  }
128
- return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
149
+ return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
129
150
  }
130
151
 
131
152
  /**