@try-works/dsh-recursive-mode 0.1.16 → 0.1.18

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
@@ -576,7 +566,6 @@ window.__ModuleLoader__.load({
576
566
  setState(null);
577
567
  return;
578
568
  }
579
- setState(null);
580
569
  let disposed = false;
581
570
  const apply = (frame) => {
582
571
  if (!disposed) setState(frame);
@@ -1293,18 +1282,6 @@ window.__ModuleLoader__.load({
1293
1282
  useWorkspaces: props?.useWorkspaces
1294
1283
  });
1295
1284
  })));
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
1285
  disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
1309
1286
  name: "settings.section",
1310
1287
  id: "recursive",
@@ -1347,19 +1324,6 @@ window.__ModuleLoader__.load({
1347
1324
  onClose: () => boardState.close()
1348
1325
  });
1349
1326
  }
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
1327
  //#endregion
1364
1328
  //#region src/client/apply-guard.ts
1365
1329
  /** Claims the client apply slot. Returns true when this call won the slot. */
@@ -1391,7 +1355,6 @@ window.__ModuleLoader__.load({
1391
1355
  exports.Board = Board;
1392
1356
  exports.Inspector = Inspector;
1393
1357
  exports.RecursiveSettings = RecursiveSettings;
1394
- exports.StatusStrip = StatusStrip;
1395
1358
  exports.apply = apply;
1396
1359
  exports.currentSessionCwd = currentSessionCwd;
1397
1360
  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.18",
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 }
@@ -34,10 +34,11 @@ export function useLiveProjection(scope: LiveScope): LiveProjectionSnapshot {
34
34
  useEffect(() => {
35
35
  // Empty scope = no resolvable session row yet — render nothing, never fetch.
36
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
+ // 0.1.18 (LIVE BUG 5): do NOT null the snapshot on scope change. 0.1.16
38
+ // added board.tsx's synchronous stale-workspace guard (workspacePath vs
39
+ // async snapshot.root), which already suppresses stale cards; nulling here
40
+ // blanked the board on every session/workspace switch (the "delay then
41
+ // stop" symptom). Keep the previous snapshot until the fresh fetch resolves.
41
42
  let disposed = false
42
43
  const apply = (frame: LiveRecursiveFrame) => { if (!disposed) setState(frame) }
43
44
 
@@ -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
- }