@try-works/dsh-recursive-mode 0.1.7 → 0.1.9
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.
- package/lib/client/board.d.ts +26 -1
- package/lib/client/inspector.d.ts +16 -1
- package/lib/client/slots.d.ts +29 -0
- package/lib/client.js +298 -34
- package/package.json +1 -1
- package/src/client/board.tsx +79 -21
- package/src/client/inspector.tsx +46 -21
- package/src/client/slots.ts +23 -2
package/lib/client/board.d.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
3
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
5
|
+
* mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
6
|
+
* when the snapshot is null (no recursive root / not loaded) or the current
|
|
7
|
+
* session is not on the recursive preset (the gate lives in slots.ts — here an
|
|
8
|
+
* empty projection with a null root is a no-op, not an "empty board").
|
|
9
|
+
*
|
|
10
|
+
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
11
|
+
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
12
|
+
* callback only selects a run for display; no mutation.
|
|
13
|
+
*
|
|
14
|
+
* Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer —
|
|
15
|
+
* without inline styles the mounted board was INVISIBLE (no position/size/z)
|
|
16
|
+
* and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
|
|
17
|
+
* (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
|
|
18
|
+
* backdrop, pointerEvents auto, and a visible close button.
|
|
19
|
+
*/
|
|
20
|
+
import { type CSSProperties } from 'react';
|
|
1
21
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts';
|
|
2
22
|
import type { LiveProjectionValue } from './contract.ts';
|
|
3
23
|
import type { BoardSelection } from './open-state.ts';
|
|
24
|
+
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
25
|
+
export declare const overlayStyle: CSSProperties;
|
|
4
26
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
5
27
|
export declare function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[];
|
|
6
28
|
export interface BoardProps {
|
|
@@ -8,7 +30,10 @@ export interface BoardProps {
|
|
|
8
30
|
snapshot: LiveProjectionValue | null;
|
|
9
31
|
/** Opens the drill-down inspector for a run (R3). */
|
|
10
32
|
onOpenInspector?: (selection: BoardSelection) => void;
|
|
33
|
+
/** Closes the overlay (run 12). */
|
|
34
|
+
onClose?: () => void;
|
|
11
35
|
}
|
|
12
|
-
export declare function Board({ snapshot, onOpenInspector }: BoardProps): import("react").DetailedReactHTMLElement<{
|
|
36
|
+
export declare function Board({ snapshot, onOpenInspector, onClose }: BoardProps): import("react").DetailedReactHTMLElement<{
|
|
13
37
|
className: string;
|
|
38
|
+
style: CSSProperties;
|
|
14
39
|
}, HTMLElement> | null;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
3
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
5
|
+
* disk.
|
|
6
|
+
*
|
|
7
|
+
* Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
|
|
8
|
+
* unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
|
|
9
|
+
* styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
|
|
10
|
+
* close button.
|
|
11
|
+
*/
|
|
12
|
+
import { type CSSProperties } from 'react';
|
|
1
13
|
import type { LiveProjectionValue } from './contract.ts';
|
|
2
14
|
export interface InspectorProps {
|
|
3
15
|
runId: string;
|
|
4
16
|
worktreeRoot: string;
|
|
5
17
|
snapshot: LiveProjectionValue | null;
|
|
6
18
|
onBackToToolDetails: () => void;
|
|
19
|
+
/** Closes the overlay (run 12). */
|
|
20
|
+
onClose?: () => void;
|
|
7
21
|
}
|
|
8
|
-
export declare function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }: InspectorProps): import("react").DetailedReactHTMLElement<{
|
|
22
|
+
export declare function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps): import("react").DetailedReactHTMLElement<{
|
|
9
23
|
className: string;
|
|
24
|
+
style: CSSProperties;
|
|
10
25
|
}, HTMLElement>;
|
package/lib/client/slots.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
|
|
3
|
+
* shell.overlay board, the conversation.input.dock status strip, and the
|
|
4
|
+
* settings.section page.
|
|
5
|
+
*
|
|
6
|
+
* Run 08: the launcher now OPENS the shared board store (onClick), and the
|
|
7
|
+
* shell.overlay entry renders the board gated on that store + the recursive
|
|
8
|
+
* preset, swapping to the drill-down inspector when a run is selected.
|
|
9
|
+
*
|
|
10
|
+
* Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
|
|
11
|
+
* thunk) — it is called WITH a selector, e.g. useSessions((s) => s) for the
|
|
12
|
+
* full SessionListStateLike. The old code called useSessions() with NO selector
|
|
13
|
+
* and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
|
|
14
|
+
* never saw the recursive preset and the board never mounted (launcher click
|
|
15
|
+
* did nothing). The board/strip now consume the SessionListStateLike directly.
|
|
16
|
+
*
|
|
17
|
+
* READ-ONLY (R9): the client only GETs the live host route; no mutation.
|
|
18
|
+
*/
|
|
19
|
+
import { type ReactNode } from 'react';
|
|
1
20
|
import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts';
|
|
2
21
|
import { isRecursivePreset, currentSessionCwd } from './contract.ts';
|
|
3
22
|
export type OverlayContent = 'hidden' | 'board' | 'inspector';
|
|
@@ -12,6 +31,16 @@ export declare function overlayContent(state: {
|
|
|
12
31
|
* Exported so the spec can assert the selector is actually passed.
|
|
13
32
|
*/
|
|
14
33
|
export declare function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike;
|
|
34
|
+
/**
|
|
35
|
+
* Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
|
|
36
|
+
* is root-scoped (the icon shows in every session otherwise), while the board it
|
|
37
|
+
* opens is recursive-preset-gated — in a code/other session the click was a dead
|
|
38
|
+
* no-op. Gating the launcher itself removes the trap: the icon appears exactly
|
|
39
|
+
* where clicking it opens the board.
|
|
40
|
+
*/
|
|
41
|
+
export declare function RecursiveLauncherGate({ useSessions }: {
|
|
42
|
+
useSessions: SnapshotSelectorHook<SessionListStateLike>;
|
|
43
|
+
}): ReactNode;
|
|
15
44
|
export declare function registerSlots(ctx: ClientContext): () => void;
|
|
16
45
|
/** Export the gate helpers for tests. */
|
|
17
46
|
export { isRecursivePreset, currentSessionCwd };
|
package/lib/client.js
CHANGED
|
@@ -219,18 +219,102 @@ window.__ModuleLoader__.load({
|
|
|
219
219
|
//#endregion
|
|
220
220
|
//#region src/client/board.tsx
|
|
221
221
|
/**
|
|
222
|
-
* Board overlay (SP2 R1 + run 08 R3): the kanban view over the live
|
|
223
|
-
* projection. Pure read-only renderer — no filesystem, no run-file
|
|
224
|
-
* Cards are grouped by worktree root (the projection's outer key) and
|
|
225
|
-
* their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
226
|
-
* snapshot is null (no recursive root / not loaded) or the current
|
|
227
|
-
* not on the recursive preset (the gate lives in slots.ts — here an
|
|
228
|
-
* projection with a null root is a no-op, not an "empty board").
|
|
222
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
223
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
224
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
225
|
+
* mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
226
|
+
* when the snapshot is null (no recursive root / not loaded) or the current
|
|
227
|
+
* session is not on the recursive preset (the gate lives in slots.ts — here an
|
|
228
|
+
* empty projection with a null root is a no-op, not an "empty board").
|
|
229
229
|
*
|
|
230
230
|
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
231
231
|
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
232
232
|
* callback only selects a run for display; no mutation.
|
|
233
|
+
*
|
|
234
|
+
* Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer —
|
|
235
|
+
* without inline styles the mounted board was INVISIBLE (no position/size/z)
|
|
236
|
+
* and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
|
|
237
|
+
* (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
|
|
238
|
+
* backdrop, pointerEvents auto, and a visible close button.
|
|
233
239
|
*/
|
|
240
|
+
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
241
|
+
const overlayStyle = {
|
|
242
|
+
position: "fixed",
|
|
243
|
+
inset: 0,
|
|
244
|
+
background: "rgba(15, 17, 21, 0.92)",
|
|
245
|
+
color: "#e6e6e6",
|
|
246
|
+
zIndex: 9999,
|
|
247
|
+
overflowY: "auto",
|
|
248
|
+
padding: "24px 32px",
|
|
249
|
+
pointerEvents: "auto",
|
|
250
|
+
fontFamily: "system-ui, sans-serif"
|
|
251
|
+
};
|
|
252
|
+
const laneStyle = { marginBottom: 24 };
|
|
253
|
+
const laneTitleStyle = {
|
|
254
|
+
fontSize: 14,
|
|
255
|
+
textTransform: "uppercase",
|
|
256
|
+
letterSpacing: "0.08em",
|
|
257
|
+
marginBottom: 8,
|
|
258
|
+
color: "#aaa"
|
|
259
|
+
};
|
|
260
|
+
const cardStyle = {
|
|
261
|
+
display: "inline-block",
|
|
262
|
+
width: 280,
|
|
263
|
+
margin: "0 12px 12px 0",
|
|
264
|
+
padding: 12,
|
|
265
|
+
background: "#1f2329",
|
|
266
|
+
borderRadius: 8,
|
|
267
|
+
border: "1px solid #333",
|
|
268
|
+
cursor: "pointer",
|
|
269
|
+
verticalAlign: "top"
|
|
270
|
+
};
|
|
271
|
+
const cardHeadStyle = {
|
|
272
|
+
display: "flex",
|
|
273
|
+
justifyContent: "space-between",
|
|
274
|
+
marginBottom: 8
|
|
275
|
+
};
|
|
276
|
+
const badgeStyle$1 = {
|
|
277
|
+
fontSize: 11,
|
|
278
|
+
padding: "2px 6px",
|
|
279
|
+
borderRadius: 4,
|
|
280
|
+
background: "#333"
|
|
281
|
+
};
|
|
282
|
+
const badgeTamperStyle = { background: "#7a1f1f" };
|
|
283
|
+
const progressStyle = {
|
|
284
|
+
height: 6,
|
|
285
|
+
background: "#333",
|
|
286
|
+
borderRadius: 3,
|
|
287
|
+
overflow: "hidden"
|
|
288
|
+
};
|
|
289
|
+
const progressFillStyle = {
|
|
290
|
+
background: "#4c9aff",
|
|
291
|
+
height: "100%"
|
|
292
|
+
};
|
|
293
|
+
const metaStyle = {
|
|
294
|
+
marginTop: 8,
|
|
295
|
+
fontSize: 12,
|
|
296
|
+
color: "#aaa",
|
|
297
|
+
display: "flex",
|
|
298
|
+
justifyContent: "space-between"
|
|
299
|
+
};
|
|
300
|
+
const emptyStyle = {
|
|
301
|
+
padding: 40,
|
|
302
|
+
textAlign: "center",
|
|
303
|
+
color: "#888"
|
|
304
|
+
};
|
|
305
|
+
const closeStyle$1 = {
|
|
306
|
+
position: "absolute",
|
|
307
|
+
top: 12,
|
|
308
|
+
right: 16,
|
|
309
|
+
background: "#1f2329",
|
|
310
|
+
color: "#e6e6e6",
|
|
311
|
+
border: "1px solid #444",
|
|
312
|
+
borderRadius: 6,
|
|
313
|
+
padding: "4px 12px",
|
|
314
|
+
cursor: "pointer",
|
|
315
|
+
fontSize: 16,
|
|
316
|
+
zIndex: 1
|
|
317
|
+
};
|
|
234
318
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
235
319
|
function listRuns(projection) {
|
|
236
320
|
if (projection === void 0) return [];
|
|
@@ -238,57 +322,220 @@ window.__ModuleLoader__.load({
|
|
|
238
322
|
for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
|
|
239
323
|
return runs;
|
|
240
324
|
}
|
|
241
|
-
function Board({ snapshot, onOpenInspector }) {
|
|
325
|
+
function Board({ snapshot, onOpenInspector, onClose }) {
|
|
242
326
|
if (snapshot === null || snapshot.root === null) return null;
|
|
243
327
|
const runs = listRuns(snapshot.projection);
|
|
328
|
+
const close = onClose !== void 0 ? (0, react.createElement)("button", {
|
|
329
|
+
className: "rec-close",
|
|
330
|
+
style: closeStyle$1,
|
|
331
|
+
onClick: onClose,
|
|
332
|
+
title: "Close"
|
|
333
|
+
}, "×") : null;
|
|
244
334
|
if (runs.length === 0) return (0, react.createElement)("div", {
|
|
245
335
|
className: "rec-board",
|
|
246
|
-
"data-empty": true
|
|
247
|
-
|
|
336
|
+
"data-empty": true,
|
|
337
|
+
style: overlayStyle
|
|
338
|
+
}, close, (0, react.createElement)("p", {
|
|
339
|
+
className: "rec-board-empty",
|
|
340
|
+
style: emptyStyle
|
|
341
|
+
}, "No recursive runs in this workspace yet."));
|
|
248
342
|
const open = (run) => () => onOpenInspector?.({
|
|
249
343
|
worktreeRoot: run.worktreeRoot,
|
|
250
344
|
runId: run.runId
|
|
251
345
|
});
|
|
252
|
-
return (0, react.createElement)("div", {
|
|
346
|
+
return (0, react.createElement)("div", {
|
|
347
|
+
className: "rec-board",
|
|
348
|
+
style: overlayStyle
|
|
349
|
+
}, close, KANBAN_LANES.map((lane) => {
|
|
253
350
|
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
|
|
254
351
|
return (0, react.createElement)("section", {
|
|
255
352
|
key: lane.id,
|
|
256
|
-
className: "rec-lane"
|
|
257
|
-
|
|
353
|
+
className: "rec-lane",
|
|
354
|
+
style: laneStyle
|
|
355
|
+
}, (0, react.createElement)("h2", {
|
|
356
|
+
className: "rec-lane-title",
|
|
357
|
+
style: laneTitleStyle
|
|
358
|
+
}, lane.label), laneRuns.map((run) => {
|
|
258
359
|
const facts = cardFacts(run);
|
|
259
360
|
return (0, react.createElement)("article", {
|
|
260
361
|
key: run.worktreeRoot + "\0" + run.runId,
|
|
261
362
|
className: "rec-card",
|
|
363
|
+
style: cardStyle,
|
|
262
364
|
onClick: open(run)
|
|
263
|
-
}, (0, react.createElement)("div", {
|
|
365
|
+
}, (0, react.createElement)("div", {
|
|
366
|
+
className: "rec-card-head",
|
|
367
|
+
style: cardHeadStyle
|
|
368
|
+
}, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
|
|
264
369
|
className: "rec-badge rec-badge-tamper",
|
|
370
|
+
style: {
|
|
371
|
+
...badgeStyle$1,
|
|
372
|
+
...badgeTamperStyle
|
|
373
|
+
},
|
|
265
374
|
title: "tampered"
|
|
266
|
-
}, "!")), (0, react.createElement)("div", {
|
|
375
|
+
}, "!")), (0, react.createElement)("div", {
|
|
376
|
+
className: "rec-card-progress",
|
|
377
|
+
style: progressStyle
|
|
378
|
+
}, (0, react.createElement)("div", {
|
|
267
379
|
className: "rec-card-progress-fill",
|
|
268
|
-
style: {
|
|
269
|
-
|
|
380
|
+
style: {
|
|
381
|
+
...progressFillStyle,
|
|
382
|
+
width: Math.round(facts.progress * 100) + "%"
|
|
383
|
+
}
|
|
384
|
+
})), (0, react.createElement)("div", {
|
|
385
|
+
className: "rec-card-meta",
|
|
386
|
+
style: metaStyle
|
|
387
|
+
}, (0, react.createElement)("span", {}, facts.currentPhase ?? "no phases"), (0, react.createElement)("span", {
|
|
388
|
+
className: "rec-badge",
|
|
389
|
+
style: badgeStyle$1
|
|
390
|
+
}, facts.state)));
|
|
270
391
|
}));
|
|
271
392
|
}));
|
|
272
393
|
}
|
|
273
394
|
//#endregion
|
|
274
395
|
//#region src/client/inspector.tsx
|
|
275
396
|
/**
|
|
276
|
-
* Drill-down inspector (SP2 R1): the details-seat swap rendering one
|
|
277
|
-
* phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
278
|
-
* and tamper/gate facts — all from the live host-route snapshot, never
|
|
397
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
398
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
399
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
400
|
+
* disk.
|
|
401
|
+
*
|
|
402
|
+
* Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
|
|
403
|
+
* unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
|
|
404
|
+
* styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
|
|
405
|
+
* close button.
|
|
279
406
|
*/
|
|
280
|
-
|
|
407
|
+
const headStyle = {
|
|
408
|
+
display: "flex",
|
|
409
|
+
alignItems: "center",
|
|
410
|
+
gap: 12,
|
|
411
|
+
marginBottom: 16,
|
|
412
|
+
borderBottom: "1px solid #333",
|
|
413
|
+
paddingBottom: 12
|
|
414
|
+
};
|
|
415
|
+
const backStyle = {
|
|
416
|
+
background: "#1f2329",
|
|
417
|
+
color: "#e6e6e6",
|
|
418
|
+
border: "1px solid #444",
|
|
419
|
+
borderRadius: 6,
|
|
420
|
+
padding: "6px 12px",
|
|
421
|
+
cursor: "pointer"
|
|
422
|
+
};
|
|
423
|
+
const closeStyle = {
|
|
424
|
+
background: "#1f2329",
|
|
425
|
+
color: "#e6e6e6",
|
|
426
|
+
border: "1px solid #444",
|
|
427
|
+
borderRadius: 6,
|
|
428
|
+
padding: "6px 12px",
|
|
429
|
+
cursor: "pointer",
|
|
430
|
+
marginLeft: "auto"
|
|
431
|
+
};
|
|
432
|
+
const sectionStyle = { marginBottom: 16 };
|
|
433
|
+
const rowStyle = {
|
|
434
|
+
display: "flex",
|
|
435
|
+
gap: 12,
|
|
436
|
+
alignItems: "center",
|
|
437
|
+
padding: "6px 0",
|
|
438
|
+
borderBottom: "1px solid #222",
|
|
439
|
+
fontSize: 13
|
|
440
|
+
};
|
|
441
|
+
const phaseIdStyle = {
|
|
442
|
+
minWidth: 220,
|
|
443
|
+
fontFamily: "monospace",
|
|
444
|
+
color: "#4c9aff"
|
|
445
|
+
};
|
|
446
|
+
const phaseNameStyle = {
|
|
447
|
+
flex: 1,
|
|
448
|
+
color: "#ccc"
|
|
449
|
+
};
|
|
450
|
+
const badgeStyle = {
|
|
451
|
+
fontSize: 11,
|
|
452
|
+
padding: "2px 6px",
|
|
453
|
+
borderRadius: 4,
|
|
454
|
+
background: "#333"
|
|
455
|
+
};
|
|
456
|
+
const lockhashStyle = {
|
|
457
|
+
fontFamily: "monospace",
|
|
458
|
+
fontSize: 11,
|
|
459
|
+
color: "#888"
|
|
460
|
+
};
|
|
461
|
+
function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
|
|
462
|
+
const close = onClose !== void 0 ? (0, react.createElement)("button", {
|
|
463
|
+
className: "rec-close",
|
|
464
|
+
style: closeStyle,
|
|
465
|
+
onClick: onClose,
|
|
466
|
+
title: "Close"
|
|
467
|
+
}, "×") : null;
|
|
281
468
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId];
|
|
282
|
-
if (card === void 0) return (0, react.createElement)("div", {
|
|
469
|
+
if (card === void 0) return (0, react.createElement)("div", {
|
|
470
|
+
className: "rec-inspector",
|
|
471
|
+
style: overlayStyle
|
|
472
|
+
}, (0, react.createElement)("div", {
|
|
473
|
+
className: "rec-inspector-head",
|
|
474
|
+
style: headStyle
|
|
475
|
+
}, (0, react.createElement)("button", {
|
|
476
|
+
onClick: onBackToToolDetails,
|
|
477
|
+
className: "rec-back",
|
|
478
|
+
style: backStyle
|
|
479
|
+
}, "← back to tool details"), close), (0, react.createElement)("p", {}, "Run not found: " + runId));
|
|
283
480
|
const facts = cardFacts(card);
|
|
284
481
|
const rows = expandPhaseRows(card);
|
|
285
|
-
return (0, react.createElement)("div", {
|
|
482
|
+
return (0, react.createElement)("div", {
|
|
483
|
+
className: "rec-inspector",
|
|
484
|
+
style: overlayStyle
|
|
485
|
+
}, (0, react.createElement)("div", {
|
|
486
|
+
className: "rec-inspector-head",
|
|
487
|
+
style: headStyle
|
|
488
|
+
}, (0, react.createElement)("button", {
|
|
286
489
|
onClick: onBackToToolDetails,
|
|
287
|
-
className: "rec-back"
|
|
288
|
-
|
|
490
|
+
className: "rec-back",
|
|
491
|
+
style: backStyle
|
|
492
|
+
}, "← back to tool details"), (0, react.createElement)("h2", { style: {
|
|
493
|
+
margin: 0,
|
|
494
|
+
fontSize: 18
|
|
495
|
+
} }, runId), (0, react.createElement)("span", {
|
|
496
|
+
className: "rec-badge",
|
|
497
|
+
style: badgeStyle
|
|
498
|
+
}, facts.state), facts.tampered && (0, react.createElement)("span", {
|
|
499
|
+
className: "rec-badge rec-badge-tamper",
|
|
500
|
+
style: {
|
|
501
|
+
...badgeStyle,
|
|
502
|
+
background: "#7a1f1f"
|
|
503
|
+
}
|
|
504
|
+
}, "tampered"), close), (0, react.createElement)("section", {
|
|
505
|
+
className: "rec-inspector-section",
|
|
506
|
+
style: sectionStyle
|
|
507
|
+
}, (0, react.createElement)("h3", { style: {
|
|
508
|
+
fontSize: 13,
|
|
509
|
+
textTransform: "uppercase",
|
|
510
|
+
letterSpacing: "0.08em",
|
|
511
|
+
color: "#aaa",
|
|
512
|
+
marginBottom: 8
|
|
513
|
+
} }, "Phases"), rows.map((row) => (0, react.createElement)("div", {
|
|
289
514
|
key: row.phase,
|
|
290
|
-
className: "rec-phase-row"
|
|
291
|
-
|
|
515
|
+
className: "rec-phase-row",
|
|
516
|
+
style: rowStyle
|
|
517
|
+
}, (0, react.createElement)("span", {
|
|
518
|
+
className: "rec-phase-id",
|
|
519
|
+
style: phaseIdStyle
|
|
520
|
+
}, row.phase), (0, react.createElement)("span", {
|
|
521
|
+
className: "rec-phase-name",
|
|
522
|
+
style: phaseNameStyle
|
|
523
|
+
}, row.fileName ?? "—"), (0, react.createElement)("span", {
|
|
524
|
+
className: "rec-badge",
|
|
525
|
+
style: badgeStyle
|
|
526
|
+
}, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", {
|
|
527
|
+
className: "rec-lockhash",
|
|
528
|
+
style: lockhashStyle
|
|
529
|
+
}, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", {
|
|
530
|
+
className: "rec-inspector-section",
|
|
531
|
+
style: sectionStyle
|
|
532
|
+
}, (0, react.createElement)("h3", { style: {
|
|
533
|
+
fontSize: 13,
|
|
534
|
+
textTransform: "uppercase",
|
|
535
|
+
letterSpacing: "0.08em",
|
|
536
|
+
color: "#aaa",
|
|
537
|
+
marginBottom: 8
|
|
538
|
+
} }, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
|
|
292
539
|
}
|
|
293
540
|
//#endregion
|
|
294
541
|
//#region src/client/strip.tsx
|
|
@@ -528,6 +775,21 @@ window.__ModuleLoader__.load({
|
|
|
528
775
|
function useRecursiveSessions(useSessions) {
|
|
529
776
|
return useSessions((s) => s);
|
|
530
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
|
|
780
|
+
* is root-scoped (the icon shows in every session otherwise), while the board it
|
|
781
|
+
* opens is recursive-preset-gated — in a code/other session the click was a dead
|
|
782
|
+
* no-op. Gating the launcher itself removes the trap: the icon appears exactly
|
|
783
|
+
* where clicking it opens the board.
|
|
784
|
+
*/
|
|
785
|
+
function RecursiveLauncherGate({ useSessions }) {
|
|
786
|
+
if (!isRecursivePreset(useRecursiveSessions(useSessions))) return null;
|
|
787
|
+
return (0, react.createElement)("button", {
|
|
788
|
+
className: "rec-launcher",
|
|
789
|
+
title: "Recursive runs",
|
|
790
|
+
onClick: () => boardState.openBoard()
|
|
791
|
+
}, "⧉");
|
|
792
|
+
}
|
|
531
793
|
function registerSlots(ctx) {
|
|
532
794
|
const disposers = [];
|
|
533
795
|
disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
@@ -535,11 +797,11 @@ window.__ModuleLoader__.load({
|
|
|
535
797
|
id: "recursive",
|
|
536
798
|
order: 50,
|
|
537
799
|
label: "Recursive runs"
|
|
538
|
-
}, () =>
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
800
|
+
}, (props) => {
|
|
801
|
+
const useSessions = props?.useSessions;
|
|
802
|
+
if (useSessions === void 0) return null;
|
|
803
|
+
return (0, react.createElement)(RecursiveLauncherGate, { useSessions });
|
|
804
|
+
})));
|
|
543
805
|
disposers.push(ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
544
806
|
name: "shell.overlay",
|
|
545
807
|
id: "recursive-board",
|
|
@@ -592,11 +854,13 @@ window.__ModuleLoader__.load({
|
|
|
592
854
|
runId: board.selection.runId,
|
|
593
855
|
worktreeRoot: board.selection.worktreeRoot,
|
|
594
856
|
snapshot,
|
|
595
|
-
onBackToToolDetails: () => boardState.backToBoard()
|
|
857
|
+
onBackToToolDetails: () => boardState.backToBoard(),
|
|
858
|
+
onClose: () => boardState.close()
|
|
596
859
|
});
|
|
597
860
|
return (0, react.createElement)(Board, {
|
|
598
861
|
snapshot,
|
|
599
|
-
onOpenInspector: (sel) => boardState.openInspector(sel)
|
|
862
|
+
onOpenInspector: (sel) => boardState.openInspector(sel),
|
|
863
|
+
onClose: () => boardState.close()
|
|
600
864
|
});
|
|
601
865
|
}
|
|
602
866
|
/**
|
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.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
package/src/client/board.tsx
CHANGED
|
@@ -1,22 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Board overlay (SP2 R1 + run 08 R3): the kanban view over the live
|
|
3
|
-
* projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
-
* Cards are grouped by worktree root (the projection's outer key) and
|
|
5
|
-
* their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
6
|
-
* snapshot is null (no recursive root / not loaded) or the current
|
|
7
|
-
* not on the recursive preset (the gate lives in slots.ts — here an
|
|
8
|
-
* projection with a null root is a no-op, not an "empty board").
|
|
2
|
+
* Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
|
|
3
|
+
* host-route projection. Pure read-only renderer — no filesystem, no run-file
|
|
4
|
+
* scraping. Cards are grouped by worktree root (the projection's outer key) and
|
|
5
|
+
* mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
|
|
6
|
+
* when the snapshot is null (no recursive root / not loaded) or the current
|
|
7
|
+
* session is not on the recursive preset (the gate lives in slots.ts — here an
|
|
8
|
+
* empty projection with a null root is a no-op, not an "empty board").
|
|
9
9
|
*
|
|
10
10
|
* Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
|
|
11
11
|
* (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
|
|
12
12
|
* callback only selects a run for display; no mutation.
|
|
13
|
+
*
|
|
14
|
+
* Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer —
|
|
15
|
+
* without inline styles the mounted board was INVISIBLE (no position/size/z)
|
|
16
|
+
* and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
|
|
17
|
+
* (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
|
|
18
|
+
* backdrop, pointerEvents auto, and a visible close button.
|
|
13
19
|
*/
|
|
14
|
-
import { createElement } from 'react'
|
|
20
|
+
import { createElement, type CSSProperties } from 'react'
|
|
15
21
|
import type { RecursiveProjection, RecursiveRunCard } from '../types.ts'
|
|
16
22
|
import type { LiveProjectionValue } from './contract.ts'
|
|
17
23
|
import { cardFacts, columnForRun, KANBAN_LANES } from './derive.ts'
|
|
18
24
|
import type { BoardSelection } from './open-state.ts'
|
|
19
25
|
|
|
26
|
+
/** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
|
|
27
|
+
export const overlayStyle: CSSProperties = {
|
|
28
|
+
position: 'fixed',
|
|
29
|
+
inset: 0,
|
|
30
|
+
background: 'rgba(15, 17, 21, 0.92)',
|
|
31
|
+
color: '#e6e6e6',
|
|
32
|
+
zIndex: 9999,
|
|
33
|
+
overflowY: 'auto',
|
|
34
|
+
padding: '24px 32px',
|
|
35
|
+
pointerEvents: 'auto',
|
|
36
|
+
fontFamily: 'system-ui, sans-serif',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const laneStyle: CSSProperties = { marginBottom: 24 }
|
|
40
|
+
const laneTitleStyle: CSSProperties = { fontSize: 14, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8, color: '#aaa' }
|
|
41
|
+
const cardStyle: CSSProperties = {
|
|
42
|
+
display: 'inline-block',
|
|
43
|
+
width: 280,
|
|
44
|
+
margin: '0 12px 12px 0',
|
|
45
|
+
padding: 12,
|
|
46
|
+
background: '#1f2329',
|
|
47
|
+
borderRadius: 8,
|
|
48
|
+
border: '1px solid #333',
|
|
49
|
+
cursor: 'pointer',
|
|
50
|
+
verticalAlign: 'top',
|
|
51
|
+
}
|
|
52
|
+
const cardHeadStyle: CSSProperties = { display: 'flex', justifyContent: 'space-between', marginBottom: 8 }
|
|
53
|
+
const badgeStyle: CSSProperties = { fontSize: 11, padding: '2px 6px', borderRadius: 4, background: '#333' }
|
|
54
|
+
const badgeTamperStyle: CSSProperties = { background: '#7a1f1f' }
|
|
55
|
+
const progressStyle: CSSProperties = { height: 6, background: '#333', borderRadius: 3, overflow: 'hidden' }
|
|
56
|
+
const progressFillStyle: CSSProperties = { background: '#4c9aff', height: '100%' }
|
|
57
|
+
const metaStyle: CSSProperties = { marginTop: 8, fontSize: 12, color: '#aaa', display: 'flex', justifyContent: 'space-between' }
|
|
58
|
+
const emptyStyle: CSSProperties = { padding: 40, textAlign: 'center', color: '#888' }
|
|
59
|
+
const closeStyle: CSSProperties = {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: 12,
|
|
62
|
+
right: 16,
|
|
63
|
+
background: '#1f2329',
|
|
64
|
+
color: '#e6e6e6',
|
|
65
|
+
border: '1px solid #444',
|
|
66
|
+
borderRadius: 6,
|
|
67
|
+
padding: '4px 12px',
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
fontSize: 16,
|
|
70
|
+
zIndex: 1,
|
|
71
|
+
}
|
|
72
|
+
|
|
20
73
|
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
21
74
|
export function listRuns(projection: RecursiveProjection | undefined): RecursiveRunCard[] {
|
|
22
75
|
if (projection === undefined) return []
|
|
@@ -34,35 +87,40 @@ export interface BoardProps {
|
|
|
34
87
|
snapshot: LiveProjectionValue | null
|
|
35
88
|
/** Opens the drill-down inspector for a run (R3). */
|
|
36
89
|
onOpenInspector?: (selection: BoardSelection) => void
|
|
90
|
+
/** Closes the overlay (run 12). */
|
|
91
|
+
onClose?: () => void
|
|
37
92
|
}
|
|
38
93
|
|
|
39
|
-
export function Board({ snapshot, onOpenInspector }: BoardProps) {
|
|
94
|
+
export function Board({ snapshot, onOpenInspector, onClose }: BoardProps) {
|
|
40
95
|
if (snapshot === null || snapshot.root === null) return null
|
|
41
96
|
const runs = listRuns(snapshot.projection)
|
|
97
|
+
const close = onClose !== undefined ? createElement('button', { className: 'rec-close', style: closeStyle, onClick: onClose, title: 'Close' }, '×') : null
|
|
42
98
|
if (runs.length === 0) {
|
|
43
|
-
return createElement('div', { className: 'rec-board', 'data-empty': true },
|
|
44
|
-
|
|
99
|
+
return createElement('div', { className: 'rec-board', 'data-empty': true, style: overlayStyle },
|
|
100
|
+
close,
|
|
101
|
+
createElement('p', { className: 'rec-board-empty', style: emptyStyle }, 'No recursive runs in this workspace yet.'),
|
|
45
102
|
)
|
|
46
103
|
}
|
|
47
104
|
const open = (run: RecursiveRunCard) => () => onOpenInspector?.({ worktreeRoot: run.worktreeRoot, runId: run.runId })
|
|
48
|
-
return createElement('div', { className: 'rec-board' },
|
|
105
|
+
return createElement('div', { className: 'rec-board', style: overlayStyle },
|
|
106
|
+
close,
|
|
49
107
|
KANBAN_LANES.map((lane) => {
|
|
50
108
|
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id)
|
|
51
|
-
return createElement('section', { key: lane.id, className: 'rec-lane' },
|
|
52
|
-
createElement('h2', { className: 'rec-lane-title' }, lane.label),
|
|
109
|
+
return createElement('section', { key: lane.id, className: 'rec-lane', style: laneStyle },
|
|
110
|
+
createElement('h2', { className: 'rec-lane-title', style: laneTitleStyle }, lane.label),
|
|
53
111
|
laneRuns.map((run) => {
|
|
54
112
|
const facts = cardFacts(run)
|
|
55
|
-
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', onClick: open(run) },
|
|
56
|
-
createElement('div', { className: 'rec-card-head' },
|
|
113
|
+
return createElement('article', { key: run.worktreeRoot + '\u0000' + run.runId, className: 'rec-card', style: cardStyle, onClick: open(run) },
|
|
114
|
+
createElement('div', { className: 'rec-card-head', style: cardHeadStyle },
|
|
57
115
|
createElement('strong', {}, run.runId),
|
|
58
|
-
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', title: 'tampered' }, '!'),
|
|
116
|
+
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', style: { ...badgeStyle, ...badgeTamperStyle }, title: 'tampered' }, '!'),
|
|
59
117
|
),
|
|
60
|
-
createElement('div', { className: 'rec-card-progress' },
|
|
61
|
-
createElement('div', { className: 'rec-card-progress-fill', style: { width: Math.round(facts.progress * 100) + '%' } }),
|
|
118
|
+
createElement('div', { className: 'rec-card-progress', style: progressStyle },
|
|
119
|
+
createElement('div', { className: 'rec-card-progress-fill', style: { ...progressFillStyle, width: Math.round(facts.progress * 100) + '%' } }),
|
|
62
120
|
),
|
|
63
|
-
createElement('div', { className: 'rec-card-meta' },
|
|
121
|
+
createElement('div', { className: 'rec-card-meta', style: metaStyle },
|
|
64
122
|
createElement('span', {}, facts.currentPhase ?? 'no phases'),
|
|
65
|
-
createElement('span', { className: 'rec-badge' }, facts.state),
|
|
123
|
+
createElement('span', { className: 'rec-badge', style: badgeStyle }, facts.state),
|
|
66
124
|
),
|
|
67
125
|
)
|
|
68
126
|
}),
|
package/src/client/inspector.tsx
CHANGED
|
@@ -1,46 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drill-down inspector (SP2 R1): the details-seat swap rendering one
|
|
3
|
-
* phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
-
* and tamper/gate facts — all from the live host-route snapshot, never
|
|
2
|
+
* Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
|
|
3
|
+
* run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
|
|
4
|
+
* badges, and tamper/gate facts — all from the live host-route snapshot, never
|
|
5
|
+
* disk.
|
|
6
|
+
*
|
|
7
|
+
* Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
|
|
8
|
+
* unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
|
|
9
|
+
* styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
|
|
10
|
+
* close button.
|
|
5
11
|
*/
|
|
6
|
-
import { createElement } from 'react'
|
|
12
|
+
import { createElement, type CSSProperties } from 'react'
|
|
7
13
|
import type { LiveProjectionValue } from './contract.ts'
|
|
8
14
|
import { cardFacts, expandPhaseRows } from './derive.ts'
|
|
15
|
+
import { overlayStyle } from './board.tsx'
|
|
16
|
+
|
|
17
|
+
const headStyle: CSSProperties = { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, borderBottom: '1px solid #333', paddingBottom: 12 }
|
|
18
|
+
const backStyle: CSSProperties = { background: '#1f2329', color: '#e6e6e6', border: '1px solid #444', borderRadius: 6, padding: '6px 12px', cursor: 'pointer' }
|
|
19
|
+
const closeStyle: CSSProperties = { background: '#1f2329', color: '#e6e6e6', border: '1px solid #444', borderRadius: 6, padding: '6px 12px', cursor: 'pointer', marginLeft: 'auto' }
|
|
20
|
+
const sectionStyle: CSSProperties = { marginBottom: 16 }
|
|
21
|
+
const rowStyle: CSSProperties = { display: 'flex', gap: 12, alignItems: 'center', padding: '6px 0', borderBottom: '1px solid #222', fontSize: 13 }
|
|
22
|
+
const phaseIdStyle: CSSProperties = { minWidth: 220, fontFamily: 'monospace', color: '#4c9aff' }
|
|
23
|
+
const phaseNameStyle: CSSProperties = { flex: 1, color: '#ccc' }
|
|
24
|
+
const badgeStyle: CSSProperties = { fontSize: 11, padding: '2px 6px', borderRadius: 4, background: '#333' }
|
|
25
|
+
const lockhashStyle: CSSProperties = { fontFamily: 'monospace', fontSize: 11, color: '#888' }
|
|
9
26
|
|
|
10
27
|
export interface InspectorProps {
|
|
11
28
|
runId: string
|
|
12
29
|
worktreeRoot: string
|
|
13
30
|
snapshot: LiveProjectionValue | null
|
|
14
31
|
onBackToToolDetails: () => void
|
|
32
|
+
/** Closes the overlay (run 12). */
|
|
33
|
+
onClose?: () => void
|
|
15
34
|
}
|
|
16
35
|
|
|
17
|
-
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails }: InspectorProps) {
|
|
36
|
+
export function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }: InspectorProps) {
|
|
37
|
+
const close = onClose !== undefined ? createElement('button', { className: 'rec-close', style: closeStyle, onClick: onClose, title: 'Close' }, '×') : null
|
|
18
38
|
const card = snapshot?.projection?.[worktreeRoot]?.[runId]
|
|
19
39
|
if (card === undefined) {
|
|
20
|
-
return createElement('div', { className: 'rec-inspector' },
|
|
40
|
+
return createElement('div', { className: 'rec-inspector', style: overlayStyle },
|
|
41
|
+
createElement('div', { className: 'rec-inspector-head', style: headStyle },
|
|
42
|
+
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back', style: backStyle }, '← back to tool details'),
|
|
43
|
+
close,
|
|
44
|
+
),
|
|
21
45
|
createElement('p', {}, 'Run not found: ' + runId),
|
|
22
46
|
)
|
|
23
47
|
}
|
|
24
48
|
const facts = cardFacts(card)
|
|
25
49
|
const rows = expandPhaseRows(card)
|
|
26
|
-
return createElement('div', { className: 'rec-inspector' },
|
|
27
|
-
createElement('div', { className: 'rec-inspector-head' },
|
|
28
|
-
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back' }, '← back to tool details'),
|
|
29
|
-
createElement('h2', {}, runId),
|
|
30
|
-
createElement('span', { className: 'rec-badge' }, facts.state),
|
|
31
|
-
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper' }, 'tampered'),
|
|
50
|
+
return createElement('div', { className: 'rec-inspector', style: overlayStyle },
|
|
51
|
+
createElement('div', { className: 'rec-inspector-head', style: headStyle },
|
|
52
|
+
createElement('button', { onClick: onBackToToolDetails, className: 'rec-back', style: backStyle }, '← back to tool details'),
|
|
53
|
+
createElement('h2', { style: { margin: 0, fontSize: 18 } }, runId),
|
|
54
|
+
createElement('span', { className: 'rec-badge', style: badgeStyle }, facts.state),
|
|
55
|
+
facts.tampered && createElement('span', { className: 'rec-badge rec-badge-tamper', style: { ...badgeStyle, background: '#7a1f1f' } }, 'tampered'),
|
|
56
|
+
close,
|
|
32
57
|
),
|
|
33
|
-
createElement('section', { className: 'rec-inspector-section' },
|
|
34
|
-
createElement('h3', {}, 'Phases'),
|
|
35
|
-
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row' },
|
|
36
|
-
createElement('span', { className: 'rec-phase-id' }, row.phase),
|
|
37
|
-
createElement('span', { className: 'rec-phase-name' }, row.fileName ?? '—'),
|
|
38
|
-
createElement('span', { className: 'rec-badge' }, row.status),
|
|
39
|
-
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash' }, '#' + row.lockHash.slice(0, 8)),
|
|
58
|
+
createElement('section', { className: 'rec-inspector-section', style: sectionStyle },
|
|
59
|
+
createElement('h3', { style: { fontSize: 13, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#aaa', marginBottom: 8 } }, 'Phases'),
|
|
60
|
+
rows.map((row) => createElement('div', { key: row.phase, className: 'rec-phase-row', style: rowStyle },
|
|
61
|
+
createElement('span', { className: 'rec-phase-id', style: phaseIdStyle }, row.phase),
|
|
62
|
+
createElement('span', { className: 'rec-phase-name', style: phaseNameStyle }, row.fileName ?? '—'),
|
|
63
|
+
createElement('span', { className: 'rec-badge', style: badgeStyle }, row.status),
|
|
64
|
+
row.lockHash !== undefined && createElement('code', { className: 'rec-lockhash', style: lockhashStyle }, '#' + row.lockHash.slice(0, 8)),
|
|
40
65
|
)),
|
|
41
66
|
),
|
|
42
|
-
facts.gateBlocked && createElement('section', { className: 'rec-inspector-section' },
|
|
43
|
-
createElement('h3', {}, 'Gate'),
|
|
67
|
+
facts.gateBlocked && createElement('section', { className: 'rec-inspector-section', style: sectionStyle },
|
|
68
|
+
createElement('h3', { style: { fontSize: 13, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#aaa', marginBottom: 8 } }, 'Gate'),
|
|
44
69
|
createElement('p', {}, 'Blocked (' + (facts.gateKind ?? '') + ')'),
|
|
45
70
|
),
|
|
46
71
|
)
|
package/src/client/slots.ts
CHANGED
|
@@ -58,16 +58,36 @@ export function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionLi
|
|
|
58
58
|
return useSessions((s) => s)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Run 11 (UX gate): the ⧉ launcher renders ONLY in recursive sessions. The seat
|
|
63
|
+
* is root-scoped (the icon shows in every session otherwise), while the board it
|
|
64
|
+
* opens is recursive-preset-gated — in a code/other session the click was a dead
|
|
65
|
+
* no-op. Gating the launcher itself removes the trap: the icon appears exactly
|
|
66
|
+
* where clicking it opens the board.
|
|
67
|
+
*/
|
|
68
|
+
export function RecursiveLauncherGate({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
69
|
+
const list = useRecursiveSessions(useSessions)
|
|
70
|
+
if (!isRecursivePreset(list)) return null
|
|
71
|
+
return createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉')
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
export function registerSlots(ctx: ClientContext): () => void {
|
|
62
75
|
const disposers: (() => void)[] = []
|
|
63
76
|
|
|
64
77
|
// Board launcher in the sidebar footer action list — OPENS the shared board store (run 08 R1).
|
|
78
|
+
// Run 11 (UX gate): the seat is root-scoped (visible in every session), but the board it
|
|
79
|
+
// opens is recursive-preset-gated. In a code/other session the icon was a dead click — a
|
|
80
|
+
// trap. The launcher itself now renders ONLY in recursive sessions, where it works.
|
|
65
81
|
disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
66
82
|
name: 'sidebar.footer.action',
|
|
67
83
|
id: 'recursive',
|
|
68
84
|
order: 50,
|
|
69
85
|
label: 'Recursive runs',
|
|
70
|
-
}, (
|
|
86
|
+
}, (props: RootSlotProps) => {
|
|
87
|
+
const useSessions = props?.useSessions
|
|
88
|
+
if (useSessions === undefined) return null
|
|
89
|
+
return createElement(RecursiveLauncherGate, { useSessions })
|
|
90
|
+
})))
|
|
71
91
|
|
|
72
92
|
// Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
|
|
73
93
|
disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -123,9 +143,10 @@ function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorH
|
|
|
123
143
|
worktreeRoot: board.selection.worktreeRoot,
|
|
124
144
|
snapshot,
|
|
125
145
|
onBackToToolDetails: () => boardState.backToBoard(),
|
|
146
|
+
onClose: () => boardState.close(),
|
|
126
147
|
})
|
|
127
148
|
}
|
|
128
|
-
return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
|
|
149
|
+
return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel), onClose: () => boardState.close() })
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
/**
|