@try-works/dsh-recursive-mode 0.1.6 → 0.1.8

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.
@@ -29,21 +29,24 @@ export interface SessionSummaryRow {
29
29
  /** Agent preset this session was composed from; absent when the deployment has none. */
30
30
  agentPreset?: string;
31
31
  }
32
- /** The session-list snapshot (mirrors SessionListState). */
32
+ /**
33
+ * The session-list snapshot (mirrors the REAL SessionListState). useSessions is
34
+ * a SnapshotSelectorHook over this state; the identity selector `(s) => s`
35
+ * yields the full value, which the gate reads directly (ids/byId/current).
36
+ */
33
37
  export interface SessionListStateLike {
34
38
  ids: string[];
35
39
  byId: Record<string, SessionSummaryRow>;
36
40
  current: string | undefined;
37
41
  }
38
- /** A snapshot store the client runtime hands out (mirrors SnapshotStore<T>). */
39
- export interface SnapshotStoreLike<T> {
40
- getSnapshot(): T;
41
- subscribe(fn: () => void): () => void;
42
- }
43
- /** The sessions service surface (mirrors ISessions.list). */
44
- export interface ISessionsLike {
45
- list: SnapshotStoreLike<SessionListStateLike>;
46
- }
42
+ /**
43
+ * The REAL selector-hook shape (ui-slots store.ts):
44
+ * SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?) => S
45
+ * The selector is REQUIRED — useSyncExternalStoreWithSelector needs it. Calling
46
+ * the hook with no selector passes sel=undefined and breaks the board mount.
47
+ * This is the run 10 live bug.
48
+ */
49
+ export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S;
47
50
  /** The slots registry surface the client injects into. */
48
51
  export interface ClientSlots {
49
52
  inject(seat: string, factory: (ctx: unknown) => () => void): () => void;
@@ -77,8 +80,8 @@ export interface LiveProjectionValue {
77
80
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
78
81
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
79
82
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
80
- * board, not a spinner.
83
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
81
84
  */
82
- export declare function isRecursivePreset(sessions: ISessionsLike): boolean;
85
+ export declare function isRecursivePreset(state: SessionListStateLike): boolean;
83
86
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
84
- export declare function currentSessionCwd(sessions: ISessionsLike): string;
87
+ export declare function currentSessionCwd(state: SessionListStateLike): string;
@@ -21,7 +21,7 @@ export type { LiveProjectionSnapshot } from './use-live.ts';
21
21
  export { fetchLiveState, subscribeLiveEvents } from './host-api.ts';
22
22
  export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts';
23
23
  export { isRecursivePreset, currentSessionCwd } from './contract.ts';
24
- export type { SessionSummaryRow, SessionListStateLike, ISessionsLike, ClientContext } from './contract.ts';
24
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts';
25
25
  /** Required services (fiber inject waiting — the runtime must be up first). */
26
26
  export declare const inject: string[];
27
27
  /** Browser-half plugin entry (R4 + SP2 R1). */
@@ -1,11 +1,46 @@
1
- import type { ClientContext, ISessionsLike } from './contract.ts';
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';
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';
4
23
  /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
5
24
  export declare function overlayContent(state: {
6
25
  open: boolean;
7
26
  selection: unknown;
8
- }, sessions: ISessionsLike): OverlayContent;
27
+ }, sessions: SessionListStateLike): OverlayContent;
28
+ /**
29
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
30
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
31
+ * Exported so the spec can assert the selector is actually passed.
32
+ */
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;
9
44
  export declare function registerSlots(ctx: ClientContext): () => void;
10
45
  /** Export the gate helpers for tests. */
11
46
  export { isRecursivePreset, currentSessionCwd };
package/lib/client.js CHANGED
@@ -10,20 +10,15 @@ window.__ModuleLoader__.load({
10
10
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
11
11
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
12
12
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
13
- * board, not a spinner.
13
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
14
14
  */
15
- function isRecursivePreset(sessions) {
16
- const snapshot = sessions.list.getSnapshot();
17
- const current = snapshot.current;
18
- if (current === void 0) return false;
19
- return snapshot.byId[current]?.agentPreset === "recursive";
15
+ function isRecursivePreset(state) {
16
+ if (state.current === void 0) return false;
17
+ return state.byId[state.current]?.agentPreset === "recursive";
20
18
  }
21
19
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
22
- function currentSessionCwd(sessions) {
23
- const snapshot = sessions.list.getSnapshot();
24
- const current = snapshot.current;
25
- if (current === void 0) return "";
26
- return snapshot.byId[current]?.cwd ?? "";
20
+ function currentSessionCwd(state) {
21
+ return state.current === void 0 ? "" : state.byId[state.current]?.cwd ?? "";
27
22
  }
28
23
  //#endregion
29
24
  //#region src/client/derive.ts
@@ -507,11 +502,14 @@ window.__ModuleLoader__.load({
507
502
  *
508
503
  * Run 08: the launcher now OPENS the shared board store (onClick), and the
509
504
  * shell.overlay entry renders the board gated on that store + the recursive
510
- * preset, swapping to the drill-down inspector when a run is selected. The
511
- * board reads cwd from the ROOT-scope standard prop useSessions (the scoped-
512
- * slots root branch provides ONLY useSessions/useWorkspaces useProjection is
513
- * session-scope-only). The strip reads its sessionId from the session-scope
514
- * standard prop.
505
+ * preset, swapping to the drill-down inspector when a run is selected.
506
+ *
507
+ * Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
508
+ * thunk) it is called WITH a selector, e.g. useSessions((s) => s) for the
509
+ * full SessionListStateLike. The old code called useSessions() with NO selector
510
+ * and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
511
+ * never saw the recursive preset and the board never mounted (launcher click
512
+ * did nothing). The board/strip now consume the SessionListStateLike directly.
515
513
  *
516
514
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
517
515
  */
@@ -522,6 +520,29 @@ window.__ModuleLoader__.load({
522
520
  if (state.selection !== null) return "inspector";
523
521
  return "board";
524
522
  }
523
+ /**
524
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
525
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
526
+ * Exported so the spec can assert the selector is actually passed.
527
+ */
528
+ function useRecursiveSessions(useSessions) {
529
+ return useSessions((s) => s);
530
+ }
531
+ /**
532
+ * Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
533
+ * is root-scoped (the icon shows in every session otherwise), while the board it
534
+ * opens is recursive-preset-gated — in a code/other session the click was a dead
535
+ * no-op. Gating the launcher itself removes the trap: the icon appears exactly
536
+ * where clicking it opens the board.
537
+ */
538
+ function RecursiveLauncherGate({ useSessions }) {
539
+ if (!isRecursivePreset(useRecursiveSessions(useSessions))) return null;
540
+ return (0, react.createElement)("button", {
541
+ className: "rec-launcher",
542
+ title: "Recursive runs",
543
+ onClick: () => boardState.openBoard()
544
+ }, "⧉");
545
+ }
525
546
  function registerSlots(ctx) {
526
547
  const disposers = [];
527
548
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
@@ -529,11 +550,11 @@ window.__ModuleLoader__.load({
529
550
  id: "recursive",
530
551
  order: 50,
531
552
  label: "Recursive runs"
532
- }, () => (0, react.createElement)("button", {
533
- className: "rec-launcher",
534
- title: "Recursive runs",
535
- onClick: () => boardState.openBoard()
536
- }, "⧉"))));
553
+ }, (props) => {
554
+ const useSessions = props?.useSessions;
555
+ if (useSessions === void 0) return null;
556
+ return (0, react.createElement)(RecursiveLauncherGate, { useSessions });
557
+ })));
537
558
  disposers.push(ctx.slots.inject("shell.overlay", () => ctx.slots.register({
538
559
  name: "shell.overlay",
539
560
  id: "recursive-board",
@@ -568,17 +589,16 @@ window.__ModuleLoader__.load({
568
589
  /**
569
590
  * Board overlay: subscribes to the shared board store, gates on open + recursive
570
591
  * preset, and swaps board <-> inspector (run 08 R1/R3).
592
+ *
593
+ * Run 10: consumes the full SessionListStateLike through the identity selector
594
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
571
595
  */
572
596
  function RecursiveBoardOverlay({ useSessions }) {
573
597
  const board = useBoardState();
574
- const list = useSessions();
575
- const sessions = { list: {
576
- getSnapshot: () => list,
577
- subscribe: () => () => {}
578
- } };
579
- const content = overlayContent(board, sessions);
598
+ const list = useRecursiveSessions(useSessions);
599
+ const content = overlayContent(board, list);
580
600
  if (content === "hidden") return null;
581
- const cwd = currentSessionCwd(sessions);
601
+ const cwd = currentSessionCwd(list);
582
602
  const snapshot = useLiveProjection({
583
603
  sessionId: list.current,
584
604
  cwd
@@ -596,18 +616,14 @@ window.__ModuleLoader__.load({
596
616
  }
597
617
  /**
598
618
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
599
- * nothing for a non-recursive session.
619
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
600
620
  */
601
621
  function RecursiveStripGate({ useSessions, sessionId }) {
602
- const list = useSessions();
603
- const sessions = { list: {
604
- getSnapshot: () => list,
605
- subscribe: () => () => {}
606
- } };
607
- if (!isRecursivePreset(sessions)) return null;
622
+ const list = useRecursiveSessions(useSessions);
623
+ if (!isRecursivePreset(list)) return null;
608
624
  const snapshot = useLiveProjection({
609
625
  sessionId,
610
- cwd: currentSessionCwd(sessions)
626
+ cwd: currentSessionCwd(list)
611
627
  });
612
628
  return (0, react.createElement)(StatusStrip, { snapshot });
613
629
  }
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.6",
4
+ "version": "0.1.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -33,23 +33,25 @@ export interface SessionSummaryRow {
33
33
  agentPreset?: string
34
34
  }
35
35
 
36
- /** The session-list snapshot (mirrors SessionListState). */
36
+ /**
37
+ * The session-list snapshot (mirrors the REAL SessionListState). useSessions is
38
+ * a SnapshotSelectorHook over this state; the identity selector `(s) => s`
39
+ * yields the full value, which the gate reads directly (ids/byId/current).
40
+ */
37
41
  export interface SessionListStateLike {
38
42
  ids: string[]
39
43
  byId: Record<string, SessionSummaryRow>
40
44
  current: string | undefined
41
45
  }
42
46
 
43
- /** A snapshot store the client runtime hands out (mirrors SnapshotStore<T>). */
44
- export interface SnapshotStoreLike<T> {
45
- getSnapshot(): T
46
- subscribe(fn: () => void): () => void
47
- }
48
-
49
- /** The sessions service surface (mirrors ISessions.list). */
50
- export interface ISessionsLike {
51
- list: SnapshotStoreLike<SessionListStateLike>
52
- }
47
+ /**
48
+ * The REAL selector-hook shape (ui-slots store.ts):
49
+ * SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?) => S
50
+ * The selector is REQUIRED — useSyncExternalStoreWithSelector needs it. Calling
51
+ * the hook with no selector passes sel=undefined and breaks the board mount.
52
+ * This is the run 10 live bug.
53
+ */
54
+ export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S
53
55
 
54
56
  /** The slots registry surface the client injects into. */
55
57
  export interface ClientSlots {
@@ -88,19 +90,14 @@ export interface LiveProjectionValue {
88
90
  * The client-side preset gate (acceptance #3): the board/strip render ONLY when
89
91
  * the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
90
92
  * session (or a session whose preset is undefined) shows NOTHING — not an empty
91
- * board, not a spinner.
93
+ * board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
92
94
  */
93
- export function isRecursivePreset(sessions: ISessionsLike): boolean {
94
- const snapshot = sessions.list.getSnapshot()
95
- const current = snapshot.current
96
- if (current === undefined) return false
97
- return snapshot.byId[current]?.agentPreset === 'recursive'
95
+ export function isRecursivePreset(state: SessionListStateLike): boolean {
96
+ if (state.current === undefined) return false
97
+ return state.byId[state.current]?.agentPreset === 'recursive'
98
98
  }
99
99
 
100
100
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
101
- export function currentSessionCwd(sessions: ISessionsLike): string {
102
- const snapshot = sessions.list.getSnapshot()
103
- const current = snapshot.current
104
- if (current === undefined) return ''
105
- return snapshot.byId[current]?.cwd ?? ''
101
+ export function currentSessionCwd(state: SessionListStateLike): string {
102
+ return state.current === undefined ? '' : (state.byId[state.current]?.cwd ?? '')
106
103
  }
@@ -24,7 +24,7 @@ export type { LiveProjectionSnapshot } from './use-live.ts'
24
24
  export { fetchLiveState, subscribeLiveEvents } from './host-api.ts'
25
25
  export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts'
26
26
  export { isRecursivePreset, currentSessionCwd } from './contract.ts'
27
- export type { SessionSummaryRow, SessionListStateLike, ISessionsLike, ClientContext } from './contract.ts'
27
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts'
28
28
 
29
29
  /** Required services (fiber inject waiting — the runtime must be up first). */
30
30
  export const inject = ['slots', 'sessions', 'workspaces', 'connection']
@@ -5,16 +5,19 @@
5
5
  *
6
6
  * Run 08: the launcher now OPENS the shared board store (onClick), and the
7
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. The
9
- * board reads cwd from the ROOT-scope standard prop useSessions (the scoped-
10
- * slots root branch provides ONLY useSessions/useWorkspaces useProjection is
11
- * session-scope-only). The strip reads its sessionId from the session-scope
12
- * standard prop.
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.
13
16
  *
14
17
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
15
18
  */
16
19
  import { createElement, type ReactNode } from 'react'
17
- import type { ClientContext, ISessionsLike, SnapshotStoreLike, SessionListStateLike } from './contract.ts'
20
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts'
18
21
  import { isRecursivePreset, currentSessionCwd } from './contract.ts'
19
22
  import { Board } from './board.tsx'
20
23
  import { Inspector } from './inspector.tsx'
@@ -23,39 +26,68 @@ import { RecursiveSettings } from './settings.tsx'
23
26
  import { useLiveProjection } from './use-live.ts'
24
27
  import { boardState, useBoardState } from './open-state.ts'
25
28
 
26
- /** Structural standard-prop face for the root scope (useSessions only). */
29
+ /** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
27
30
  interface RootSlotProps {
28
- useSessions?: () => SessionListStateLike
29
- useWorkspaces?: () => unknown
31
+ useSessions?: SnapshotSelectorHook<SessionListStateLike>
32
+ useWorkspaces?: SnapshotSelectorHook<unknown>
30
33
  }
31
34
 
32
35
  /** Structural standard-prop face for the session scope (sessionId + useSessions). */
33
36
  interface SessionSlotProps {
34
37
  sessionId?: string
35
- useSessions?: () => SessionListStateLike
38
+ useSessions?: SnapshotSelectorHook<SessionListStateLike>
36
39
  useProjection?: unknown
37
40
  }
38
41
 
39
42
  export type OverlayContent = 'hidden' | 'board' | 'inspector'
40
43
 
41
44
  /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
42
- export function overlayContent(state: { open: boolean; selection: unknown }, sessions: ISessionsLike): OverlayContent {
45
+ export function overlayContent(state: { open: boolean; selection: unknown }, sessions: SessionListStateLike): OverlayContent {
43
46
  if (!state.open) return 'hidden'
44
47
  if (!isRecursivePreset(sessions)) return 'hidden'
45
48
  if (state.selection !== null) return 'inspector'
46
49
  return 'board'
47
50
  }
48
51
 
52
+ /**
53
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
54
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
55
+ * Exported so the spec can assert the selector is actually passed.
56
+ */
57
+ export function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike {
58
+ return useSessions((s) => s)
59
+ }
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
+
49
74
  export function registerSlots(ctx: ClientContext): () => void {
50
75
  const disposers: (() => void)[] = []
51
76
 
52
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.
53
81
  disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
54
82
  name: 'sidebar.footer.action',
55
83
  id: 'recursive',
56
84
  order: 50,
57
85
  label: 'Recursive runs',
58
- }, () => 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
+ })))
59
91
 
60
92
  // Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
61
93
  disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
@@ -93,14 +125,16 @@ export function registerSlots(ctx: ClientContext): () => void {
93
125
  /**
94
126
  * Board overlay: subscribes to the shared board store, gates on open + recursive
95
127
  * preset, and swaps board <-> inspector (run 08 R1/R3).
128
+ *
129
+ * Run 10: consumes the full SessionListStateLike through the identity selector
130
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
96
131
  */
97
- function RecursiveBoardOverlay({ useSessions }: { useSessions: () => SessionListStateLike }): ReactNode {
132
+ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
98
133
  const board = useBoardState()
99
- const list = useSessions()
100
- const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
101
- const content = overlayContent(board, sessions)
134
+ const list = useRecursiveSessions(useSessions)
135
+ const content = overlayContent(board, list)
102
136
  if (content === 'hidden') return null
103
- const cwd = currentSessionCwd(sessions)
137
+ const cwd = currentSessionCwd(list)
104
138
  const scope = { sessionId: list.current, cwd }
105
139
  const snapshot = useLiveProjection(scope)
106
140
  if (content === 'inspector' && board.selection !== null) {
@@ -116,13 +150,12 @@ function RecursiveBoardOverlay({ useSessions }: { useSessions: () => SessionList
116
150
 
117
151
  /**
118
152
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
119
- * nothing for a non-recursive session.
153
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
120
154
  */
121
- function RecursiveStripGate({ useSessions, sessionId }: { useSessions: () => SessionListStateLike; sessionId?: string }): ReactNode {
122
- const list = useSessions()
123
- const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
124
- if (!isRecursivePreset(sessions)) return null
125
- const cwd = currentSessionCwd(sessions)
155
+ function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
156
+ const list = useRecursiveSessions(useSessions)
157
+ if (!isRecursivePreset(list)) return null
158
+ const cwd = currentSessionCwd(list)
126
159
  const scope = { sessionId, cwd }
127
160
  const snapshot = useLiveProjection(scope)
128
161
  return createElement(StatusStrip, { snapshot })