@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.
- package/lib/client/contract.d.ts +16 -13
- package/lib/client/index.d.ts +1 -1
- package/lib/client/slots.d.ts +37 -2
- package/lib/client.js +52 -36
- package/package.json +1 -1
- package/src/client/contract.ts +19 -22
- package/src/client/index.ts +1 -1
- package/src/client/slots.ts +56 -23
package/lib/client/contract.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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(
|
|
87
|
+
export declare function currentSessionCwd(state: SessionListStateLike): string;
|
package/lib/client/index.d.ts
CHANGED
|
@@ -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,
|
|
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). */
|
package/lib/client/slots.d.ts
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
|
|
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:
|
|
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(
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
23
|
-
|
|
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.
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
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
|
-
}, () =>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
4
|
+
"version": "0.1.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
package/src/client/contract.ts
CHANGED
|
@@ -33,23 +33,25 @@ export interface SessionSummaryRow {
|
|
|
33
33
|
agentPreset?: string
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
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
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
102
|
-
|
|
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
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -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,
|
|
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']
|
package/src/client/slots.ts
CHANGED
|
@@ -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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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,
|
|
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
|
|
29
|
+
/** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
|
|
27
30
|
interface RootSlotProps {
|
|
28
|
-
useSessions?:
|
|
29
|
-
useWorkspaces?:
|
|
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?:
|
|
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:
|
|
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
|
-
}, (
|
|
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:
|
|
132
|
+
function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
98
133
|
const board = useBoardState()
|
|
99
|
-
const list = useSessions
|
|
100
|
-
const
|
|
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(
|
|
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:
|
|
122
|
-
const list = useSessions
|
|
123
|
-
|
|
124
|
-
|
|
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 })
|