@try-works/dsh-recursive-mode 0.1.15 → 0.1.17

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.
@@ -5,10 +5,12 @@ import type { BoardSelection } from './open-state.ts';
5
5
  export declare function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[];
6
6
  export interface BoardProps {
7
7
  snapshot: LiveProjectionValue | null;
8
+ /** The authoritative current-workspace path (synchronous). snapshot.root is async/stale. */
9
+ workspacePath?: string;
8
10
  onOpenInspector?: (selection: BoardSelection) => void;
9
11
  onClose?: () => void;
10
12
  }
11
- export declare function Board({ snapshot, onOpenInspector, onClose }: BoardProps): import("react").DetailedReactHTMLElement<{
13
+ export declare function Board({ snapshot, workspacePath, onOpenInspector, onClose }: BoardProps): import("react").DetailedReactHTMLElement<{
12
14
  className: string;
13
15
  'data-theme': import("./theme.ts").BoardTheme;
14
16
  }, HTMLElement> | null;
@@ -14,7 +14,6 @@
14
14
  import type { ClientContext } from './contract.ts';
15
15
  export { Board, listRuns } from './board.tsx';
16
16
  export { Inspector } from './inspector.tsx';
17
- export { StatusStrip } from './strip.tsx';
18
17
  export { RecursiveSettings } from './settings.tsx';
19
18
  export { useLiveProjection } from './use-live.ts';
20
19
  export type { LiveProjectionSnapshot } from './use-live.ts';
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { type ReactNode } from 'react';
20
20
  import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts';
21
- import { isRecursivePreset, currentSessionCwd } from './contract.ts';
21
+ import { isRecursivePreset } 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. */
24
24
  export declare function overlayContent(state: {
@@ -53,13 +53,5 @@ export declare function RecursiveBoardOverlay({ useSessions, useWorkspaces }: {
53
53
  useSessions: SnapshotSelectorHook<SessionListStateLike>;
54
54
  useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>;
55
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;
64
56
  /** Export the gate helpers for tests. */
65
- export { isRecursivePreset, currentSessionCwd };
57
+ export { isRecursivePreset };
package/lib/client.js CHANGED
@@ -345,17 +345,22 @@ window.__ModuleLoader__.load({
345
345
  "aria-label": "Close"
346
346
  }, "×");
347
347
  }
348
+ /** Case/trailing-separator tolerant workspace-path equality (Windows host). */
349
+ function sameWorkspacePath(a, b) {
350
+ const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
351
+ return norm(a) === norm(b);
352
+ }
348
353
  function solidPill$1(kind) {
349
354
  return (0, react.createElement)("span", {
350
355
  className: "rec-pill",
351
356
  "data-pill": kind
352
357
  }, PILL_LABELS[kind]);
353
358
  }
354
- function Board({ snapshot, onOpenInspector, onClose }) {
359
+ function Board({ snapshot, workspacePath, onOpenInspector, onClose }) {
355
360
  const { theme, toggle } = useBoardTheme();
356
361
  if (snapshot === null || snapshot.root === null) return null;
357
- const runs = listRuns(snapshot.projection);
358
- const workspacePath = snapshot.root;
362
+ const runs = workspacePath !== void 0 && workspacePath !== "" && !sameWorkspacePath(workspacePath, snapshot.root) ? [] : listRuns(snapshot.projection);
363
+ const headerPath = workspacePath ?? snapshot.root;
359
364
  const open = (run) => () => onOpenInspector?.({
360
365
  worktreeRoot: run.worktreeRoot,
361
366
  runId: run.runId
@@ -364,14 +369,14 @@ window.__ModuleLoader__.load({
364
369
  className: "rec-board",
365
370
  "data-theme": theme,
366
371
  "data-empty": true
367
- }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, workspacePath), (0, react.createElement)("span", { className: "rec-board-count" }, "0 runs"), (0, react.createElement)(ThemeToggle, {
372
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, "0 runs"), (0, react.createElement)(ThemeToggle, {
368
373
  theme,
369
374
  toggle
370
375
  }), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
371
376
  return (0, react.createElement)("div", {
372
377
  className: "rec-board",
373
378
  "data-theme": theme
374
- }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, workspacePath), (0, react.createElement)("span", { className: "rec-board-count" }, runs.length + " runs"), (0, react.createElement)(ThemeToggle, {
379
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, runs.length + " runs"), (0, react.createElement)(ThemeToggle, {
375
380
  theme,
376
381
  toggle
377
382
  }), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
@@ -447,21 +452,6 @@ window.__ModuleLoader__.load({
447
452
  }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), solidPill(phaseStatusPill(row.status)), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8)))), facts.gateBlocked && (0, react.createElement)("div", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", { className: "rec-detail-text" }, "Blocked (" + (facts.gateKind ?? "") + ")"))), theme, toggle);
448
453
  }
449
454
  //#endregion
450
- //#region src/client/strip.tsx
451
- /**
452
- * Status strip (SP2 R1): a thin glance at the active run's phase, gate state,
453
- * and tamper badge, docked at conversation.input.dock. Renders NOTHING when no
454
- * run is current or the snapshot is null (the preset gate lives in slots.ts).
455
- */
456
- function StatusStrip({ snapshot }) {
457
- if (snapshot === null || snapshot.root === null) return null;
458
- const runs = listRuns(snapshot.projection);
459
- if (runs.length === 0) return null;
460
- const active = runs[runs.length - 1];
461
- const facts = cardFacts(active);
462
- return (0, react.createElement)("div", { className: "rec-strip" }, (0, react.createElement)("span", { className: "rec-strip-run" }, active.runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.currentPhase ?? "no phases"), facts.gateBlocked && (0, react.createElement)("span", { className: "rec-badge rec-badge-gate" }, "gate blocked"), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered"));
463
- }
464
- //#endregion
465
455
  //#region src/client/settings.tsx
466
456
  /**
467
457
  * Recursive settings page (Phase D R8, §11.8): a settings.section list entry
@@ -572,7 +562,11 @@ window.__ModuleLoader__.load({
572
562
  function useLiveProjection(scope) {
573
563
  const [state, setState] = (0, react.useState)(null);
574
564
  (0, react.useEffect)(() => {
575
- if (isEmpty(scope)) return;
565
+ if (isEmpty(scope)) {
566
+ setState(null);
567
+ return;
568
+ }
569
+ setState(null);
576
570
  let disposed = false;
577
571
  const apply = (frame) => {
578
572
  if (!disposed) setState(frame);
@@ -1289,18 +1283,6 @@ window.__ModuleLoader__.load({
1289
1283
  useWorkspaces: props?.useWorkspaces
1290
1284
  });
1291
1285
  })));
1292
- disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
1293
- name: "conversation.input.dock",
1294
- id: "recursive-strip",
1295
- order: 20
1296
- }, (props) => {
1297
- const useSessions = props?.useSessions;
1298
- if (useSessions === void 0) return null;
1299
- return (0, react.createElement)(RecursiveStripGate, {
1300
- useSessions,
1301
- sessionId: props?.sessionId
1302
- });
1303
- })));
1304
1286
  disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
1305
1287
  name: "settings.section",
1306
1288
  id: "recursive",
@@ -1326,7 +1308,8 @@ window.__ModuleLoader__.load({
1326
1308
  recentWorkspaceId: void 0
1327
1309
  };
1328
1310
  const content = overlayContent(board, sessions);
1329
- const snapshot = useLiveProjection({ cwd: currentWorkspacePath(workspaces, sessions) });
1311
+ const wsPath = currentWorkspacePath(workspaces, sessions);
1312
+ const snapshot = useLiveProjection({ cwd: wsPath });
1330
1313
  if (content === "hidden") return null;
1331
1314
  if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
1332
1315
  runId: board.selection.runId,
@@ -1337,23 +1320,11 @@ window.__ModuleLoader__.load({
1337
1320
  });
1338
1321
  return (0, react.createElement)(Board, {
1339
1322
  snapshot,
1323
+ workspacePath: wsPath,
1340
1324
  onOpenInspector: (sel) => boardState.openInspector(sel),
1341
1325
  onClose: () => boardState.close()
1342
1326
  });
1343
1327
  }
1344
- /**
1345
- * Strip gate: same preset gate, session-scoped sessionId; the strip renders
1346
- * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
1347
- */
1348
- function RecursiveStripGate({ useSessions, sessionId }) {
1349
- const list = useRecursiveSessions(useSessions);
1350
- const snapshot = useLiveProjection({
1351
- sessionId,
1352
- cwd: currentSessionCwd(list)
1353
- });
1354
- if (!isRecursivePreset(list)) return null;
1355
- return (0, react.createElement)(StatusStrip, { snapshot });
1356
- }
1357
1328
  //#endregion
1358
1329
  //#region src/client/apply-guard.ts
1359
1330
  /** Claims the client apply slot. Returns true when this call won the slot. */
@@ -1385,7 +1356,6 @@ window.__ModuleLoader__.load({
1385
1356
  exports.Board = Board;
1386
1357
  exports.Inspector = Inspector;
1387
1358
  exports.RecursiveSettings = RecursiveSettings;
1388
- exports.StatusStrip = StatusStrip;
1389
1359
  exports.apply = apply;
1390
1360
  exports.currentSessionCwd = currentSessionCwd;
1391
1361
  exports.currentWorkspacePath = currentWorkspacePath;
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.15",
4
+ "version": "0.1.17",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -28,6 +28,8 @@ export function listRuns(projection: RecursiveProjection | undefined): Recursive
28
28
 
29
29
  export interface BoardProps {
30
30
  snapshot: LiveProjectionValue | null
31
+ /** The authoritative current-workspace path (synchronous). snapshot.root is async/stale. */
32
+ workspacePath?: string
31
33
  onOpenInspector?: (selection: BoardSelection) => void
32
34
  onClose?: () => void
33
35
  }
@@ -37,21 +39,31 @@ function closeButton(onClose: (() => void) | undefined) {
37
39
  return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
38
40
  }
39
41
 
42
+ /** Case/trailing-separator tolerant workspace-path equality (Windows host). */
43
+ function sameWorkspacePath(a: string, b: string): boolean {
44
+ const norm = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
45
+ return norm(a) === norm(b)
46
+ }
47
+
40
48
  function solidPill(kind: ReturnType<typeof cardPill>) {
41
49
  return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind])
42
50
  }
43
51
 
44
- export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
52
+ export function Board({ snapshot, workspacePath, onOpenInspector, onClose }: BoardProps) {
45
53
  const { theme, toggle } = useBoardTheme()
46
54
  if (snapshot === null || snapshot.root === null) return null
47
- const runs = listRuns(snapshot.projection)
48
- const workspacePath = snapshot.root
55
+ // Run 16: when the synchronous workspace path differs from the async snapshot
56
+ // root, the projection is stale (from the previously-viewed workspace) — suppress
57
+ // its run cards so a workspace switch can never flash another workspace's runs.
58
+ const stale = workspacePath !== undefined && workspacePath !== '' && !sameWorkspacePath(workspacePath, snapshot.root)
59
+ const runs = stale ? [] : listRuns(snapshot.projection)
60
+ const headerPath = workspacePath ?? snapshot.root
49
61
  const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
50
62
  if (runs.length === 0) {
51
63
  return createElement('div', { className: 'rec-board', 'data-theme': theme, 'data-empty': true },
52
64
  createElement('header', { className: 'rec-board-header' },
53
65
  createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
54
- createElement('span', { className: 'rec-board-path' }, workspacePath),
66
+ createElement('span', { className: 'rec-board-path' }, headerPath),
55
67
  createElement('span', { className: 'rec-board-count' }, '0 runs'),
56
68
  createElement(ThemeToggle, { theme, toggle }),
57
69
  closeButton(onClose),
@@ -62,7 +74,7 @@ export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
62
74
  return createElement('div', { className: 'rec-board', 'data-theme': theme },
63
75
  createElement('header', { className: 'rec-board-header' },
64
76
  createElement('h2', { className: 'rec-board-title' }, 'Recursive runs'),
65
- createElement('span', { className: 'rec-board-path' }, workspacePath),
77
+ createElement('span', { className: 'rec-board-path' }, headerPath),
66
78
  createElement('span', { className: 'rec-board-count' }, runs.length + ' runs'),
67
79
  createElement(ThemeToggle, { theme, toggle }),
68
80
  closeButton(onClose),
@@ -17,7 +17,6 @@ import { claimClientApply, releaseClientApply } from './apply-guard.ts'
17
17
 
18
18
  export { Board, listRuns } from './board.tsx'
19
19
  export { Inspector } from './inspector.tsx'
20
- export { StatusStrip } from './strip.tsx'
21
20
  export { RecursiveSettings } from './settings.tsx'
22
21
  export { useLiveProjection } from './use-live.ts'
23
22
  export type { LiveProjectionSnapshot } from './use-live.ts'
@@ -18,10 +18,9 @@
18
18
  */
19
19
  import { createElement, type ReactNode } from 'react'
20
20
  import type { ClientContext, SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts'
21
- import { isRecursivePreset, currentSessionCwd, currentWorkspacePath } from './contract.ts'
21
+ import { isRecursivePreset, currentWorkspacePath } from './contract.ts'
22
22
  import { Board } from './board.tsx'
23
23
  import { Inspector } from './inspector.tsx'
24
- import { StatusStrip } from './strip.tsx'
25
24
  import { RecursiveSettings } from './settings.tsx'
26
25
  import { useLiveProjection } from './use-live.ts'
27
26
  import { boardState, useBoardState } from './open-state.ts'
@@ -33,13 +32,6 @@ interface RootSlotProps {
33
32
  useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>
34
33
  }
35
34
 
36
- /** Structural standard-prop face for the session scope (sessionId + useSessions). */
37
- interface SessionSlotProps {
38
- sessionId?: string
39
- useSessions?: SnapshotSelectorHook<SessionListStateLike>
40
- useProjection?: unknown
41
- }
42
-
43
35
  export type OverlayContent = 'hidden' | 'board' | 'inspector'
44
36
 
45
37
  /** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
@@ -103,17 +95,6 @@ export function registerSlots(ctx: ClientContext): () => void {
103
95
  return createElement(RecursiveBoardOverlay, { useSessions, useWorkspaces: props?.useWorkspaces })
104
96
  })))
105
97
 
106
- // Status strip in the composer input dock (session scope) — gated the same way.
107
- disposers.push(ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
108
- name: 'conversation.input.dock',
109
- id: 'recursive-strip',
110
- order: 20,
111
- }, (props: SessionSlotProps) => {
112
- const useSessions = props?.useSessions
113
- if (useSessions === undefined) return null
114
- return createElement(RecursiveStripGate, { useSessions, sessionId: props?.sessionId })
115
- })))
116
-
117
98
  // Settings section (root scope, always present; no gate — configuration is always available).
118
99
  disposers.push(ctx.slots.inject('settings.section', () => ctx.slots.register({
119
100
  name: 'settings.section',
@@ -157,22 +138,8 @@ export function RecursiveBoardOverlay({ useSessions, useWorkspaces }: { useSessi
157
138
  onClose: () => boardState.close(),
158
139
  })
159
140
  }
160
- return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
161
- }
162
-
163
- /**
164
- * Strip gate: same preset gate, session-scoped sessionId; the strip renders
165
- * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
166
- */
167
- export function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
168
- const list = useRecursiveSessions(useSessions)
169
- const cwd = currentSessionCwd(list)
170
- const scope = { sessionId, cwd }
171
- // Run 13: ALWAYS call useLiveProjection BEFORE the preset early return.
172
- const snapshot = useLiveProjection(scope)
173
- if (!isRecursivePreset(list)) return null
174
- return createElement(StatusStrip, { snapshot })
141
+ return createElement(Board, { snapshot, workspacePath: wsPath, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
175
142
  }
176
143
 
177
144
  /** Export the gate helpers for tests. */
178
- export { isRecursivePreset, currentSessionCwd }
145
+ export { isRecursivePreset }
@@ -33,7 +33,11 @@ export function useLiveProjection(scope: LiveScope): LiveProjectionSnapshot {
33
33
 
34
34
  useEffect(() => {
35
35
  // Empty scope = no resolvable session row yet — render nothing, never fetch.
36
- if (isEmpty(scope)) return
36
+ if (isEmpty(scope)) { setState(null); return }
37
+ // 0.1.16 (LIVE BUG 3): RESET the snapshot synchronously on scope change so
38
+ // the board never renders the PREVIOUS workspace's {root, projection} during
39
+ // a switch. The fresh fetch below re-populates state only when it resolves.
40
+ setState(null)
37
41
  let disposed = false
38
42
  const apply = (frame: LiveRecursiveFrame) => { if (!disposed) setState(frame) }
39
43
 
@@ -1,8 +0,0 @@
1
- import type { LiveProjectionValue } from './contract.ts';
2
- export interface StripProps {
3
- /** The live host-route snapshot (null = not loaded / no recursive root). */
4
- snapshot: LiveProjectionValue | null;
5
- }
6
- export declare function StatusStrip({ snapshot }: StripProps): import("react").DetailedReactHTMLElement<{
7
- className: string;
8
- }, HTMLElement> | null;
@@ -1,28 +0,0 @@
1
- /**
2
- * Status strip (SP2 R1): a thin glance at the active run's phase, gate state,
3
- * and tamper badge, docked at conversation.input.dock. Renders NOTHING when no
4
- * run is current or the snapshot is null (the preset gate lives in slots.ts).
5
- */
6
- import { createElement } from 'react'
7
- import { cardFacts } from './derive.ts'
8
- import { listRuns } from './board.tsx'
9
- import type { LiveProjectionValue } from './contract.ts'
10
-
11
- export interface StripProps {
12
- /** The live host-route snapshot (null = not loaded / no recursive root). */
13
- snapshot: LiveProjectionValue | null
14
- }
15
-
16
- export function StatusStrip({ snapshot }: StripProps) {
17
- if (snapshot === null || snapshot.root === null) return null
18
- const runs = listRuns(snapshot.projection)
19
- if (runs.length === 0) return null
20
- const active = runs[runs.length - 1]
21
- const facts = cardFacts(active)
22
- return createElement('div', { className: 'rec-strip' },
23
- createElement('span', { className: 'rec-strip-run' }, active.runId),
24
- createElement('span', { className: 'rec-badge' }, facts.currentPhase ?? 'no phases'),
25
- facts.gateBlocked && createElement('span', { className: 'rec-badge rec-badge-gate' }, 'gate blocked'),
26
- facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper' }, 'tampered'),
27
- )
28
- }