@try-works/dsh-recursive-mode 0.1.10 → 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. */
@@ -49,8 +49,9 @@ export declare function registerSlots(ctx: ClientContext): () => void;
49
49
  * Run 10: consumes the full SessionListStateLike through the identity selector
50
50
  * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
51
51
  */
52
- export declare function RecursiveBoardOverlay({ useSessions }: {
52
+ export declare function RecursiveBoardOverlay({ useSessions, useWorkspaces }: {
53
53
  useSessions: SnapshotSelectorHook<SessionListStateLike>;
54
+ useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>;
54
55
  }): ReactNode;
55
56
  /**
56
57
  * Strip gate: same preset gate, session-scoped sessionId; the strip renders
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,15 +868,15 @@ 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);
847
- const cwd = currentSessionCwd(list);
848
- const snapshot = useLiveProjection({
849
- sessionId: list.current,
850
- cwd
851
- });
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) });
852
880
  if (content === "hidden") return null;
853
881
  if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
854
882
  runId: board.selection.runId,
@@ -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.10",
4
+ "version": "0.1.11",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -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,12 +129,17 @@ 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
- export 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
- const cwd = currentSessionCwd(list)
137
- 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 }
138
143
  // Run 13 (LIVE BUG): ALWAYS call useLiveProjection — a conditional hook (after the
139
144
  // hidden early return) changed the hook count across renders and threw
140
145
  // 'Rendered more hooks than during the previous render'.