@try-works/dsh-recursive-mode 0.1.16 → 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.
@@ -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,6 +345,11 @@ 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",
@@ -354,7 +359,7 @@ window.__ModuleLoader__.load({
354
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);
362
+ const runs = workspacePath !== void 0 && workspacePath !== "" && !sameWorkspacePath(workspacePath, snapshot.root) ? [] : listRuns(snapshot.projection);
358
363
  const headerPath = workspacePath ?? snapshot.root;
359
364
  const open = (run) => () => onOpenInspector?.({
360
365
  worktreeRoot: run.worktreeRoot,
@@ -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
@@ -1293,18 +1283,6 @@ window.__ModuleLoader__.load({
1293
1283
  useWorkspaces: props?.useWorkspaces
1294
1284
  });
1295
1285
  })));
1296
- disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
1297
- name: "conversation.input.dock",
1298
- id: "recursive-strip",
1299
- order: 20
1300
- }, (props) => {
1301
- const useSessions = props?.useSessions;
1302
- if (useSessions === void 0) return null;
1303
- return (0, react.createElement)(RecursiveStripGate, {
1304
- useSessions,
1305
- sessionId: props?.sessionId
1306
- });
1307
- })));
1308
1286
  disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
1309
1287
  name: "settings.section",
1310
1288
  id: "recursive",
@@ -1347,19 +1325,6 @@ window.__ModuleLoader__.load({
1347
1325
  onClose: () => boardState.close()
1348
1326
  });
1349
1327
  }
1350
- /**
1351
- * Strip gate: same preset gate, session-scoped sessionId; the strip renders
1352
- * nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
1353
- */
1354
- function RecursiveStripGate({ useSessions, sessionId }) {
1355
- const list = useRecursiveSessions(useSessions);
1356
- const snapshot = useLiveProjection({
1357
- sessionId,
1358
- cwd: currentSessionCwd(list)
1359
- });
1360
- if (!isRecursivePreset(list)) return null;
1361
- return (0, react.createElement)(StatusStrip, { snapshot });
1362
- }
1363
1328
  //#endregion
1364
1329
  //#region src/client/apply-guard.ts
1365
1330
  /** Claims the client apply slot. Returns true when this call won the slot. */
@@ -1391,7 +1356,6 @@ window.__ModuleLoader__.load({
1391
1356
  exports.Board = Board;
1392
1357
  exports.Inspector = Inspector;
1393
1358
  exports.RecursiveSettings = RecursiveSettings;
1394
- exports.StatusStrip = StatusStrip;
1395
1359
  exports.apply = apply;
1396
1360
  exports.currentSessionCwd = currentSessionCwd;
1397
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.16",
4
+ "version": "0.1.17",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -39,6 +39,12 @@ function closeButton(onClose: (() => void) | undefined) {
39
39
  return createElement('button', { type: 'button', className: 'rec-close', onClick: onClose, title: 'Close', 'aria-label': 'Close' }, '×')
40
40
  }
41
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
+
42
48
  function solidPill(kind: ReturnType<typeof cardPill>) {
43
49
  return createElement('span', { className: 'rec-pill', 'data-pill': kind }, PILL_LABELS[kind])
44
50
  }
@@ -46,7 +52,11 @@ function solidPill(kind: ReturnType<typeof cardPill>) {
46
52
  export function Board({ snapshot, workspacePath, onOpenInspector, onClose }: BoardProps) {
47
53
  const { theme, toggle } = useBoardTheme()
48
54
  if (snapshot === null || snapshot.root === null) return null
49
- const runs = listRuns(snapshot.projection)
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)
50
60
  const headerPath = workspacePath ?? snapshot.root
51
61
  const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
52
62
  if (runs.length === 0) {
@@ -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',
@@ -160,19 +141,5 @@ export function RecursiveBoardOverlay({ useSessions, useWorkspaces }: { useSessi
160
141
  return createElement(Board, { snapshot, workspacePath: wsPath, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
161
142
  }
162
143
 
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 })
175
- }
176
-
177
144
  /** Export the gate helpers for tests. */
178
- export { isRecursivePreset, currentSessionCwd }
145
+ export { isRecursivePreset }
@@ -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
- }