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

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.
@@ -85,3 +85,32 @@ export interface LiveProjectionValue {
85
85
  export declare function isRecursivePreset(state: SessionListStateLike): boolean;
86
86
  /** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
87
87
  export declare function currentSessionCwd(state: SessionListStateLike): string;
88
+ /**
89
+ * A workspace row — the structural mirror of the REAL WorkspaceView the host
90
+ * injects. We consume only workspaceId/path/title/sessionIds.
91
+ */
92
+ export interface WorkspaceViewLike {
93
+ workspaceId: string;
94
+ path: string;
95
+ title: string;
96
+ sessionIds: string[];
97
+ }
98
+ /**
99
+ * The workspace-list snapshot — the structural subset of the REAL
100
+ * WorkspaceListState we consume (items + recentWorkspaceId). The real state
101
+ * also carries archivedSessionIds/state/phase/error/baselinesReady; those are
102
+ * irrelevant here and the real value satisfies this subset.
103
+ */
104
+ export interface WorkspaceListStateLike {
105
+ items: readonly WorkspaceViewLike[];
106
+ recentWorkspaceId?: string;
107
+ }
108
+ /**
109
+ * Resolve the CURRENT workspace path for a workspace-scoped board (run 14):
110
+ * the WORKSPACE owns the .recursive/ run-layer, not the session. Resolution:
111
+ * 1) recentWorkspaceId's item path (the workspace the user is viewing), else
112
+ * 2) the workspace whose sessionIds contains sessionList.current, else
113
+ * 3) the workspace whose path === currentSessionCwd(sessionList), else
114
+ * 4) '' (empty -> the route returns null root -> board renders nothing).
115
+ */
116
+ export declare function currentWorkspacePath(list: WorkspaceListStateLike, sessionList: SessionListStateLike): string;
@@ -20,8 +20,8 @@ export { useLiveProjection } from './use-live.ts';
20
20
  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
- export { isRecursivePreset, currentSessionCwd } from './contract.ts';
24
- export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts';
23
+ export { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts';
24
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, WorkspaceViewLike, WorkspaceListStateLike, 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). */
@@ -17,7 +17,7 @@
17
17
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
18
18
  */
19
19
  import { type ReactNode } from 'react';
20
- import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts';
20
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts';
21
21
  import { isRecursivePreset, currentSessionCwd } from './contract.ts';
22
22
  export type OverlayContent = 'hidden' | 'board' | 'inspector';
23
23
  /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
@@ -42,5 +42,24 @@ export declare function RecursiveLauncherGate({ useSessions }: {
42
42
  useSessions: SnapshotSelectorHook<SessionListStateLike>;
43
43
  }): ReactNode;
44
44
  export declare function registerSlots(ctx: ClientContext): () => void;
45
+ /**
46
+ * Board overlay: subscribes to the shared board store, gates on open + recursive
47
+ * preset, and swaps board <-> inspector (run 08 R1/R3).
48
+ *
49
+ * Run 10: consumes the full SessionListStateLike through the identity selector
50
+ * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
51
+ */
52
+ export declare function RecursiveBoardOverlay({ useSessions, useWorkspaces }: {
53
+ useSessions: SnapshotSelectorHook<SessionListStateLike>;
54
+ useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>;
55
+ }): ReactNode;
56
+ /**
57
+ * Strip gate: same preset gate, session-scoped sessionId; the strip renders
58
+ * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
59
+ */
60
+ export declare function RecursiveStripGate({ useSessions, sessionId }: {
61
+ useSessions: SnapshotSelectorHook<SessionListStateLike>;
62
+ sessionId?: string;
63
+ }): ReactNode;
45
64
  /** Export the gate helpers for tests. */
46
65
  export { isRecursivePreset, currentSessionCwd };
package/lib/client.js CHANGED
@@ -20,6 +20,31 @@ window.__ModuleLoader__.load({
20
20
  function currentSessionCwd(state) {
21
21
  return state.current === void 0 ? "" : state.byId[state.current]?.cwd ?? "";
22
22
  }
23
+ /**
24
+ * Resolve the CURRENT workspace path for a workspace-scoped board (run 14):
25
+ * the WORKSPACE owns the .recursive/ run-layer, not the session. Resolution:
26
+ * 1) recentWorkspaceId's item path (the workspace the user is viewing), else
27
+ * 2) the workspace whose sessionIds contains sessionList.current, else
28
+ * 3) the workspace whose path === currentSessionCwd(sessionList), else
29
+ * 4) '' (empty -> the route returns null root -> board renders nothing).
30
+ */
31
+ function currentWorkspacePath(list, sessionList) {
32
+ if (list.recentWorkspaceId !== void 0) {
33
+ const recent = list.items.find((w) => w.workspaceId === list.recentWorkspaceId);
34
+ if (recent !== void 0) return recent.path;
35
+ }
36
+ const current = sessionList.current;
37
+ if (current !== void 0) {
38
+ const bySession = list.items.find((w) => w.sessionIds.includes(current));
39
+ if (bySession !== void 0) return bySession.path;
40
+ }
41
+ const cwd = currentSessionCwd(sessionList);
42
+ if (cwd !== "") {
43
+ const byCwd = list.items.find((w) => w.path === cwd);
44
+ if (byCwd !== void 0) return byCwd.path;
45
+ }
46
+ return "";
47
+ }
23
48
  //#endregion
24
49
  //#region src/client/derive.ts
25
50
  const KANBAN_LANES = [
@@ -809,7 +834,10 @@ window.__ModuleLoader__.load({
809
834
  }, (props) => {
810
835
  const useSessions = props?.useSessions;
811
836
  if (useSessions === void 0) return null;
812
- return (0, react.createElement)(RecursiveBoardOverlay, { useSessions });
837
+ return (0, react.createElement)(RecursiveBoardOverlay, {
838
+ useSessions,
839
+ useWorkspaces: props?.useWorkspaces
840
+ });
813
841
  })));
814
842
  disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
815
843
  name: "conversation.input.dock",
@@ -840,16 +868,16 @@ window.__ModuleLoader__.load({
840
868
  * Run 10: consumes the full SessionListStateLike through the identity selector
841
869
  * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
842
870
  */
843
- function RecursiveBoardOverlay({ useSessions }) {
871
+ function RecursiveBoardOverlay({ useSessions, useWorkspaces }) {
844
872
  const board = useBoardState();
845
- const list = useRecursiveSessions(useSessions);
846
- const content = overlayContent(board, list);
873
+ const sessions = useRecursiveSessions(useSessions);
874
+ const workspaces = useWorkspaces !== void 0 ? useWorkspaces((s) => s) : {
875
+ items: [],
876
+ recentWorkspaceId: void 0
877
+ };
878
+ const content = overlayContent(board, sessions);
879
+ const snapshot = useLiveProjection({ cwd: currentWorkspacePath(workspaces, sessions) });
847
880
  if (content === "hidden") return null;
848
- const cwd = currentSessionCwd(list);
849
- const snapshot = useLiveProjection({
850
- sessionId: list.current,
851
- cwd
852
- });
853
881
  if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
854
882
  runId: board.selection.runId,
855
883
  worktreeRoot: board.selection.worktreeRoot,
@@ -869,11 +897,11 @@ window.__ModuleLoader__.load({
869
897
  */
870
898
  function RecursiveStripGate({ useSessions, sessionId }) {
871
899
  const list = useRecursiveSessions(useSessions);
872
- if (!isRecursivePreset(list)) return null;
873
900
  const snapshot = useLiveProjection({
874
901
  sessionId,
875
902
  cwd: currentSessionCwd(list)
876
903
  });
904
+ if (!isRecursivePreset(list)) return null;
877
905
  return (0, react.createElement)(StatusStrip, { snapshot });
878
906
  }
879
907
  //#endregion
@@ -910,6 +938,7 @@ window.__ModuleLoader__.load({
910
938
  exports.StatusStrip = StatusStrip;
911
939
  exports.apply = apply;
912
940
  exports.currentSessionCwd = currentSessionCwd;
941
+ exports.currentWorkspacePath = currentWorkspacePath;
913
942
  exports.fetchLiveState = fetchLiveState;
914
943
  exports.inject = inject;
915
944
  exports.isRecursivePreset = isRecursivePreset;
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.9",
4
+ "version": "0.1.11",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -45,10 +45,10 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@deepseek-ai/cordis": "^4.0.1",
48
- "@deepseek-ai/dsh-tools": "0.1.0-rc.5",
49
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
50
- "@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
51
48
  "@deepseek-ai/dsh-session": "0.1.0-rc.5",
49
+ "@deepseek-ai/dsh-session-projection": "0.1.0-rc.5",
50
+ "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.5",
51
+ "@deepseek-ai/dsh-tools": "0.1.0-rc.5",
52
52
  "react": "^18.2.0"
53
53
  },
54
54
  "devDependencies": {
@@ -71,7 +71,9 @@
71
71
  "@types/node": "^20.0.0",
72
72
  "@types/react": "^18.2.0",
73
73
  "@types/react-dom": "^18.2.0",
74
+ "@types/react-test-renderer": "18.3.0",
74
75
  "react": "^18.2.0",
76
+ "react-test-renderer": "18.3.1",
75
77
  "tsdown": "^0.22.2",
76
78
  "tsx": "^4.19.0",
77
79
  "typescript": "^5.6.0",
@@ -101,3 +101,51 @@ export function isRecursivePreset(state: SessionListStateLike): boolean {
101
101
  export function currentSessionCwd(state: SessionListStateLike): string {
102
102
  return state.current === undefined ? '' : (state.byId[state.current]?.cwd ?? '')
103
103
  }
104
+
105
+ /**
106
+ * A workspace row — the structural mirror of the REAL WorkspaceView the host
107
+ * injects. We consume only workspaceId/path/title/sessionIds.
108
+ */
109
+ export interface WorkspaceViewLike {
110
+ workspaceId: string
111
+ path: string
112
+ title: string
113
+ sessionIds: string[]
114
+ }
115
+
116
+ /**
117
+ * The workspace-list snapshot — the structural subset of the REAL
118
+ * WorkspaceListState we consume (items + recentWorkspaceId). The real state
119
+ * also carries archivedSessionIds/state/phase/error/baselinesReady; those are
120
+ * irrelevant here and the real value satisfies this subset.
121
+ */
122
+ export interface WorkspaceListStateLike {
123
+ items: readonly WorkspaceViewLike[]
124
+ recentWorkspaceId?: string
125
+ }
126
+
127
+ /**
128
+ * Resolve the CURRENT workspace path for a workspace-scoped board (run 14):
129
+ * the WORKSPACE owns the .recursive/ run-layer, not the session. Resolution:
130
+ * 1) recentWorkspaceId's item path (the workspace the user is viewing), else
131
+ * 2) the workspace whose sessionIds contains sessionList.current, else
132
+ * 3) the workspace whose path === currentSessionCwd(sessionList), else
133
+ * 4) '' (empty -> the route returns null root -> board renders nothing).
134
+ */
135
+ export function currentWorkspacePath(list: WorkspaceListStateLike, sessionList: SessionListStateLike): string {
136
+ if (list.recentWorkspaceId !== undefined) {
137
+ const recent = list.items.find((w) => w.workspaceId === list.recentWorkspaceId)
138
+ if (recent !== undefined) return recent.path
139
+ }
140
+ const current = sessionList.current
141
+ if (current !== undefined) {
142
+ const bySession = list.items.find((w) => w.sessionIds.includes(current))
143
+ if (bySession !== undefined) return bySession.path
144
+ }
145
+ const cwd = currentSessionCwd(sessionList)
146
+ if (cwd !== '') {
147
+ const byCwd = list.items.find((w) => w.path === cwd)
148
+ if (byCwd !== undefined) return byCwd.path
149
+ }
150
+ return ''
151
+ }
@@ -23,8 +23,8 @@ export { useLiveProjection } from './use-live.ts'
23
23
  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
- export { isRecursivePreset, currentSessionCwd } from './contract.ts'
27
- export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts'
26
+ export { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts'
27
+ export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, WorkspaceViewLike, WorkspaceListStateLike, 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']
@@ -17,8 +17,8 @@
17
17
  * READ-ONLY (R9): the client only GETs the live host route; no mutation.
18
18
  */
19
19
  import { createElement, type ReactNode } from 'react'
20
- import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts'
21
- import { isRecursivePreset, currentSessionCwd } from './contract.ts'
20
+ import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts'
21
+ import { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts'
22
22
  import { Board } from './board.tsx'
23
23
  import { Inspector } from './inspector.tsx'
24
24
  import { StatusStrip } from './strip.tsx'
@@ -29,7 +29,7 @@ import { boardState, useBoardState } from './open-state.ts'
29
29
  /** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
30
30
  interface RootSlotProps {
31
31
  useSessions?: SnapshotSelectorHook<SessionListStateLike>
32
- useWorkspaces?: SnapshotSelectorHook<unknown>
32
+ useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>
33
33
  }
34
34
 
35
35
  /** Structural standard-prop face for the session scope (sessionId + useSessions). */
@@ -97,7 +97,7 @@ export function registerSlots(ctx: ClientContext): () => void {
97
97
  }, (props: RootSlotProps) => {
98
98
  const useSessions = props?.useSessions
99
99
  if (useSessions === undefined) return null
100
- return createElement(RecursiveBoardOverlay, { useSessions })
100
+ return createElement(RecursiveBoardOverlay, { useSessions, useWorkspaces: props?.useWorkspaces })
101
101
  })))
102
102
 
103
103
  // Status strip in the composer input dock (session scope) — gated the same way.
@@ -129,14 +129,22 @@ export function registerSlots(ctx: ClientContext): () => void {
129
129
  * Run 10: consumes the full SessionListStateLike through the identity selector
130
130
  * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
131
131
  */
132
- function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
132
+ export function RecursiveBoardOverlay({ useSessions, useWorkspaces }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike> }): ReactNode {
133
133
  const board = useBoardState()
134
- const list = useRecursiveSessions(useSessions)
135
- const content = overlayContent(board, list)
136
- if (content === 'hidden') return null
137
- const cwd = currentSessionCwd(list)
138
- const scope = { sessionId: list.current, cwd }
134
+ const sessions = useRecursiveSessions(useSessions)
135
+ // Run 14 (USER-DIRECTED): the board keys on the WORKSPACE, not the session —
136
+ // the workspace owns the .recursive/ run-layer history. Hoist ALL hooks first.
137
+ const workspaces: WorkspaceListStateLike = useWorkspaces !== undefined
138
+ ? useWorkspaces((s) => s)
139
+ : { items: [], recentWorkspaceId: undefined }
140
+ const content = overlayContent(board, sessions)
141
+ const wsPath = currentWorkspacePath(workspaces, sessions)
142
+ const scope = { cwd: wsPath }
143
+ // Run 13 (LIVE BUG): ALWAYS call useLiveProjection — a conditional hook (after the
144
+ // hidden early return) changed the hook count across renders and threw
145
+ // 'Rendered more hooks than during the previous render'.
139
146
  const snapshot = useLiveProjection(scope)
147
+ if (content === 'hidden') return null
140
148
  if (content === 'inspector' && board.selection !== null) {
141
149
  return createElement(Inspector, {
142
150
  runId: board.selection.runId,
@@ -153,12 +161,13 @@ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorH
153
161
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
154
162
  * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
155
163
  */
156
- function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
164
+ export function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
157
165
  const list = useRecursiveSessions(useSessions)
158
- if (!isRecursivePreset(list)) return null
159
166
  const cwd = currentSessionCwd(list)
160
167
  const scope = { sessionId, cwd }
168
+ // Run 13: ALWAYS call useLiveProjection BEFORE the preset early return.
161
169
  const snapshot = useLiveProjection(scope)
170
+ if (!isRecursivePreset(list)) return null
162
171
  return createElement(StatusStrip, { snapshot })
163
172
  }
164
173