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

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,17 @@
1
- import type { ClientContext, ISessionsLike } from './contract.ts';
1
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts';
2
2
  import { isRecursivePreset, currentSessionCwd } from './contract.ts';
3
3
  export type OverlayContent = 'hidden' | 'board' | 'inspector';
4
4
  /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
5
5
  export declare function overlayContent(state: {
6
6
  open: boolean;
7
7
  selection: unknown;
8
- }, sessions: ISessionsLike): OverlayContent;
8
+ }, sessions: SessionListStateLike): OverlayContent;
9
+ /**
10
+ * THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
11
+ * selector. The identity selector `(s) => s` yields the full SessionListStateLike.
12
+ * Exported so the spec can assert the selector is actually passed.
13
+ */
14
+ export declare function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike;
9
15
  export declare function registerSlots(ctx: ClientContext): () => void;
10
16
  /** Export the gate helpers for tests. */
11
17
  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,14 @@ 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
+ }
525
531
  function registerSlots(ctx) {
526
532
  const disposers = [];
527
533
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
@@ -568,17 +574,16 @@ window.__ModuleLoader__.load({
568
574
  /**
569
575
  * Board overlay: subscribes to the shared board store, gates on open + recursive
570
576
  * preset, and swaps board <-> inspector (run 08 R1/R3).
577
+ *
578
+ * Run 10: consumes the full SessionListStateLike through the identity selector
579
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
571
580
  */
572
581
  function RecursiveBoardOverlay({ useSessions }) {
573
582
  const board = useBoardState();
574
- const list = useSessions();
575
- const sessions = { list: {
576
- getSnapshot: () => list,
577
- subscribe: () => () => {}
578
- } };
579
- const content = overlayContent(board, sessions);
583
+ const list = useRecursiveSessions(useSessions);
584
+ const content = overlayContent(board, list);
580
585
  if (content === "hidden") return null;
581
- const cwd = currentSessionCwd(sessions);
586
+ const cwd = currentSessionCwd(list);
582
587
  const snapshot = useLiveProjection({
583
588
  sessionId: list.current,
584
589
  cwd
@@ -596,18 +601,14 @@ window.__ModuleLoader__.load({
596
601
  }
597
602
  /**
598
603
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
599
- * nothing for a non-recursive session.
604
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
600
605
  */
601
606
  function RecursiveStripGate({ useSessions, sessionId }) {
602
- const list = useSessions();
603
- const sessions = { list: {
604
- getSnapshot: () => list,
605
- subscribe: () => () => {}
606
- } };
607
- if (!isRecursivePreset(sessions)) return null;
607
+ const list = useRecursiveSessions(useSessions);
608
+ if (!isRecursivePreset(list)) return null;
608
609
  const snapshot = useLiveProjection({
609
610
  sessionId,
610
- cwd: currentSessionCwd(sessions)
611
+ cwd: currentSessionCwd(list)
611
612
  });
612
613
  return (0, react.createElement)(StatusStrip, { snapshot });
613
614
  }
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.7",
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,29 +26,38 @@ 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
+
49
61
  export function registerSlots(ctx: ClientContext): () => void {
50
62
  const disposers: (() => void)[] = []
51
63
 
@@ -93,14 +105,16 @@ export function registerSlots(ctx: ClientContext): () => void {
93
105
  /**
94
106
  * Board overlay: subscribes to the shared board store, gates on open + recursive
95
107
  * preset, and swaps board <-> inspector (run 08 R1/R3).
108
+ *
109
+ * Run 10: consumes the full SessionListStateLike through the identity selector
110
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
96
111
  */
97
- function RecursiveBoardOverlay({ useSessions }: { useSessions: () => SessionListStateLike }): ReactNode {
112
+ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
98
113
  const board = useBoardState()
99
- const list = useSessions()
100
- const sessions: ISessionsLike = { list: { getSnapshot: () => list, subscribe: () => () => {} } as SnapshotStoreLike<SessionListStateLike> }
101
- const content = overlayContent(board, sessions)
114
+ const list = useRecursiveSessions(useSessions)
115
+ const content = overlayContent(board, list)
102
116
  if (content === 'hidden') return null
103
- const cwd = currentSessionCwd(sessions)
117
+ const cwd = currentSessionCwd(list)
104
118
  const scope = { sessionId: list.current, cwd }
105
119
  const snapshot = useLiveProjection(scope)
106
120
  if (content === 'inspector' && board.selection !== null) {
@@ -116,13 +130,12 @@ function RecursiveBoardOverlay({ useSessions }: { useSessions: () => SessionList
116
130
 
117
131
  /**
118
132
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
119
- * nothing for a non-recursive session.
133
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
120
134
  */
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)
135
+ function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
136
+ const list = useRecursiveSessions(useSessions)
137
+ if (!isRecursivePreset(list)) return null
138
+ const cwd = currentSessionCwd(list)
126
139
  const scope = { sessionId, cwd }
127
140
  const snapshot = useLiveProjection(scope)
128
141
  return createElement(StatusStrip, { snapshot })