@try-works/dsh-recursive-mode 0.1.10 → 0.1.12

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.js CHANGED
@@ -20,6 +20,31 @@ window.__ModuleLoader__.load({
20
20
  function currentSessionCwd(state) {
21
21
  return state.current === void 0 ? "" : state.byId[state.current]?.cwd ?? "";
22
22
  }
23
+ /**
24
+ * Resolve the CURRENT workspace path for a workspace-scoped board (run 14):
25
+ * the WORKSPACE owns the .recursive/ run-layer, not the session. Resolution:
26
+ * 1) recentWorkspaceId's item path (the workspace the user is viewing), else
27
+ * 2) the workspace whose sessionIds contains sessionList.current, else
28
+ * 3) the workspace whose path === currentSessionCwd(sessionList), else
29
+ * 4) '' (empty -> the route returns null root -> board renders nothing).
30
+ */
31
+ function currentWorkspacePath(list, sessionList) {
32
+ if (list.recentWorkspaceId !== void 0) {
33
+ const recent = list.items.find((w) => w.workspaceId === list.recentWorkspaceId);
34
+ if (recent !== void 0) return recent.path;
35
+ }
36
+ const current = sessionList.current;
37
+ if (current !== void 0) {
38
+ const bySession = list.items.find((w) => w.sessionIds.includes(current));
39
+ if (bySession !== void 0) return bySession.path;
40
+ }
41
+ const cwd = currentSessionCwd(sessionList);
42
+ if (cwd !== "") {
43
+ const byCwd = list.items.find((w) => w.path === cwd);
44
+ if (byCwd !== void 0) return byCwd.path;
45
+ }
46
+ return "";
47
+ }
23
48
  //#endregion
24
49
  //#region src/client/derive.ts
25
50
  const KANBAN_LANES = [
@@ -219,102 +244,14 @@ window.__ModuleLoader__.load({
219
244
  //#endregion
220
245
  //#region src/client/board.tsx
221
246
  /**
222
- * Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
247
+ * Board overlay (SP2 R1 + run 08 R3 + run 15): the kanban view over the live
223
248
  * 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").
249
+ * scraping. Cards are grouped by worktree root and mapped to their current-phase
250
+ * lane via derive.columnForRun.
229
251
  *
230
- * Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
231
- * (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
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.
252
+ * Run 15 (task-board redesign): class-based structure + theme-token stylesheet
253
+ * injected once via styles.ts (no inline styles — unlocks hover/grid/theme).
239
254
  */
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
- };
318
255
  /** Flatten the worktree-grouped projection into a stable run list. */
319
256
  function listRuns(projection) {
320
257
  if (projection === void 0) return [];
@@ -322,220 +259,105 @@ window.__ModuleLoader__.load({
322
259
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
323
260
  return runs;
324
261
  }
262
+ function closeButton$1(onClose) {
263
+ if (onClose === void 0) return null;
264
+ return (0, react.createElement)("button", {
265
+ type: "button",
266
+ className: "rec-close",
267
+ onClick: onClose,
268
+ title: "Close",
269
+ "aria-label": "Close"
270
+ }, "×");
271
+ }
325
272
  function Board({ snapshot, onOpenInspector, onClose }) {
326
273
  if (snapshot === null || snapshot.root === null) return null;
327
274
  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;
334
- if (runs.length === 0) return (0, react.createElement)("div", {
335
- className: "rec-board",
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."));
342
275
  const open = (run) => () => onOpenInspector?.({
343
276
  worktreeRoot: run.worktreeRoot,
344
277
  runId: run.runId
345
278
  });
346
- return (0, react.createElement)("div", {
279
+ if (runs.length === 0) return (0, react.createElement)("div", {
347
280
  className: "rec-board",
348
- style: overlayStyle
349
- }, close, KANBAN_LANES.map((lane) => {
281
+ "data-empty": true
282
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
283
+ return (0, react.createElement)("div", { className: "rec-board" }, (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-count" }, String(runs.length)), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
350
284
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
351
285
  return (0, react.createElement)("section", {
352
286
  key: lane.id,
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) => {
287
+ className: "rec-column"
288
+ }, (0, react.createElement)("header", { className: "rec-column-header" }, (0, react.createElement)("span", {
289
+ className: "rec-status-dot",
290
+ "data-status": laneRuns.length > 0 ? laneRuns[laneRuns.length - 1].state : "new",
291
+ "aria-hidden": true
292
+ }), (0, react.createElement)("h3", { className: "rec-column-title" }, lane.label), (0, react.createElement)("span", { className: "rec-column-count" }, String(laneRuns.length))), (0, react.createElement)("div", { className: "rec-cards" }, laneRuns.map((run) => {
359
293
  const facts = cardFacts(run);
360
294
  return (0, react.createElement)("article", {
361
295
  key: run.worktreeRoot + "\0" + run.runId,
362
296
  className: "rec-card",
363
- style: cardStyle,
297
+ "data-state": run.state,
364
298
  onClick: open(run)
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", {
299
+ }, (0, react.createElement)("div", { className: "rec-card-title" }, run.runId), (0, react.createElement)("div", { className: "rec-card-progress" }, (0, react.createElement)("div", {
300
+ className: "rec-card-progress-fill",
301
+ style: { width: Math.round(facts.progress * 100) + "%" }
302
+ })), (0, react.createElement)("div", { className: "rec-card-meta" }, (0, react.createElement)("span", { className: "rec-card-phase" }, facts.currentPhase ?? "no phases"), facts.tampered && (0, react.createElement)("span", {
369
303
  className: "rec-badge rec-badge-tamper",
370
- style: {
371
- ...badgeStyle$1,
372
- ...badgeTamperStyle
373
- },
304
+ "data-status": run.state,
374
305
  title: "tampered"
375
- }, "!")), (0, react.createElement)("div", {
376
- className: "rec-card-progress",
377
- style: progressStyle
378
- }, (0, react.createElement)("div", {
379
- className: "rec-card-progress-fill",
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", {
306
+ }, "!"), (0, react.createElement)("span", {
388
307
  className: "rec-badge",
389
- style: badgeStyle$1
390
- }, facts.state)));
391
- }));
392
- }));
308
+ "data-status": run.state
309
+ }, run.state)));
310
+ }), laneRuns.length === 0 && (0, react.createElement)("div", { className: "rec-column-empty" }, "—")));
311
+ })));
393
312
  }
394
313
  //#endregion
395
314
  //#region src/client/inspector.tsx
396
315
  /**
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.
316
+ * Drill-down inspector (SP2 R1 + run 15): centered modal-style detail panel
317
+ * (task-board .detail shape) rendering one run's phase chain, lock badges, and
318
+ * tamper/gate facts — all from the live host-route snapshot, never disk.
406
319
  */
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", {
320
+ function closeButton(onClose) {
321
+ if (onClose === void 0) return null;
322
+ return (0, react.createElement)("button", {
323
+ type: "button",
463
324
  className: "rec-close",
464
- style: closeStyle,
465
325
  onClick: onClose,
466
- title: "Close"
467
- }, "×") : null;
468
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
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,
326
+ title: "Close",
327
+ "aria-label": "Close"
328
+ }, "×");
329
+ }
330
+ function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body) {
331
+ return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
332
+ type: "button",
477
333
  className: "rec-back",
478
- style: backStyle
479
- }, "← back to tool details"), close), (0, react.createElement)("p", {}, "Run not found: " + runId));
334
+ onClick: onBackToToolDetails
335
+ }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
336
+ }
337
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
338
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
339
+ if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId));
480
340
  const facts = cardFacts(card);
481
341
  const rows = expandPhaseRows(card);
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", {
489
- onClick: onBackToToolDetails,
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", {
342
+ const badge = (0, react.createElement)("span", {
496
343
  className: "rec-badge",
497
- style: badgeStyle
498
- }, facts.state), facts.tampered && (0, react.createElement)("span", {
344
+ "data-status": card.state
345
+ }, facts.state);
346
+ const tamper = facts.tampered ? (0, react.createElement)("span", {
499
347
  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", {
348
+ "data-status": card.state
349
+ }, "tampered") : null;
350
+ return detailShell(runId, onBackToToolDetails, onClose, (0, react.createElement)("span", { style: {
351
+ display: "flex",
352
+ alignItems: "center",
353
+ gap: 6
354
+ } }, badge, tamper), (0, react.createElement)("section", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
514
355
  key: row.phase,
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", {
356
+ className: "rec-phase-row"
357
+ }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", {
524
358
  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 ?? "") + ")")));
359
+ "data-status": row.status === "LOCKED" ? "complete" : "active"
360
+ }, 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 ?? "") + ")"))));
539
361
  }
540
362
  //#endregion
541
363
  //#region src/client/strip.tsx
@@ -741,6 +563,402 @@ window.__ModuleLoader__.load({
741
563
  return (0, react.useSyncExternalStore)(store.subscribe, store.get, store.get);
742
564
  }
743
565
  //#endregion
566
+ //#region src/client/styles.ts
567
+ /**
568
+ * One-shot board/inspector stylesheet injection (run 15). The client bundle is a
569
+ * plain CJS closure with NO CSS pipeline (tsdown has no CSS handling), so we
570
+ * inject a single <style data-dsh-recursive> element into document.head and
571
+ * style via class names + the shell's --dsw-* theme tokens. That unlocks hover,
572
+ * grid layout, and light/dark/skin theme following — impossible with inline
573
+ * styles. Idempotent: guarded by document.querySelector, so every registration
574
+ * reuses the one element; the returned disposer removes it.
575
+ */
576
+ const BOARD_CSS = `
577
+ /* dsh-recursive board + inspector (theme-token driven, .rec-* scoped) */
578
+ .rec-board {
579
+ position: fixed;
580
+ inset: 0;
581
+ z-index: 9999;
582
+ background: var(--dsw-alias-bg-base);
583
+ color: var(--dsw-alias-label-primary);
584
+ font-family: var(--dsw-font-family);
585
+ display: flex;
586
+ flex-direction: column;
587
+ box-sizing: border-box;
588
+ padding: 14px 16px 16px;
589
+ gap: 12px;
590
+ pointer-events: auto;
591
+ overflow: hidden;
592
+ }
593
+
594
+ .rec-board-header {
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 10px;
598
+ flex: none;
599
+ }
600
+
601
+ .rec-board-title {
602
+ margin: 0;
603
+ font-size: 16px;
604
+ font-weight: 700;
605
+ color: var(--dsw-alias-label-primary);
606
+ white-space: nowrap;
607
+ }
608
+
609
+ .rec-board-count {
610
+ flex: none;
611
+ font-size: 12px;
612
+ color: var(--dsw-alias-label-tertiary);
613
+ background: var(--dsw-alias-interactive-bg-hover);
614
+ border-radius: 999px;
615
+ padding: 1px 8px;
616
+ }
617
+
618
+ .rec-board-empty {
619
+ margin: 0;
620
+ padding: 40px;
621
+ text-align: center;
622
+ color: var(--dsw-alias-label-tertiary);
623
+ }
624
+
625
+ .rec-close {
626
+ position: absolute;
627
+ top: 14px;
628
+ right: 16px;
629
+ z-index: 1;
630
+ display: inline-flex;
631
+ align-items: center;
632
+ justify-content: center;
633
+ width: 26px;
634
+ height: 26px;
635
+ padding: 0;
636
+ background: transparent;
637
+ border: 1px solid var(--dsw-alias-border-l2);
638
+ border-radius: 6px;
639
+ color: var(--dsw-alias-label-secondary);
640
+ cursor: pointer;
641
+ font-size: 15px;
642
+ line-height: 1;
643
+ }
644
+
645
+ .rec-close:hover {
646
+ background: var(--dsw-alias-interactive-bg-hover);
647
+ color: var(--dsw-alias-label-primary);
648
+ }
649
+
650
+ .rec-columns {
651
+ display: grid;
652
+ grid-auto-flow: column;
653
+ grid-auto-columns: minmax(240px, 1fr);
654
+ gap: 12px;
655
+ flex: 1;
656
+ min-height: 0;
657
+ overflow-x: auto;
658
+ overflow-y: hidden;
659
+ overscroll-behavior-inline: contain;
660
+ padding-bottom: 6px;
661
+ scrollbar-color: var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);
662
+ scrollbar-width: thin;
663
+ }
664
+
665
+ .rec-columns::-webkit-scrollbar { height: 10px; }
666
+ .rec-columns::-webkit-scrollbar-track { background: var(--dsw-alias-interactive-bg-hover); border-radius: 999px; }
667
+ .rec-columns::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l3); background-clip: content-box; border: 2px solid transparent; border-radius: 999px; }
668
+ .rec-columns::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-border-l3); background-clip: content-box; }
669
+
670
+ .rec-column {
671
+ display: flex;
672
+ flex-direction: column;
673
+ min-height: 0;
674
+ background: var(--dsw-alias-bg-layer-2);
675
+ border: 1px solid var(--dsw-alias-border-l1);
676
+ border-radius: 12px;
677
+ overflow: hidden;
678
+ }
679
+
680
+ .rec-column-header {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 6px;
684
+ padding: 10px 12px;
685
+ flex: none;
686
+ }
687
+
688
+ .rec-column-title {
689
+ margin: 0;
690
+ flex: 1;
691
+ font-size: 13px;
692
+ font-weight: 700;
693
+ color: var(--dsw-alias-label-primary);
694
+ overflow: hidden;
695
+ text-overflow: ellipsis;
696
+ white-space: nowrap;
697
+ }
698
+
699
+ .rec-column-count {
700
+ flex: none;
701
+ font-size: 12px;
702
+ color: var(--dsw-alias-label-tertiary);
703
+ background: var(--dsw-alias-interactive-bg-hover);
704
+ border-radius: 999px;
705
+ padding: 1px 8px;
706
+ }
707
+
708
+ .rec-status-dot {
709
+ width: 8px;
710
+ height: 8px;
711
+ border-radius: 50%;
712
+ flex: none;
713
+ }
714
+
715
+ .rec-status-dot[data-status='new'] { background: var(--dsw-alias-label-tertiary); }
716
+ .rec-status-dot[data-status='active'] { background: var(--dsw-alias-state-business-primary); }
717
+ .rec-status-dot[data-status='paused'] { background: var(--dsw-alias-state-warn-primary); }
718
+ .rec-status-dot[data-status='blocked'] { background: var(--dsw-alias-state-error-primary); }
719
+ .rec-status-dot[data-status='complete'] { background: var(--dsw-alias-state-success-primary); }
720
+
721
+ .rec-cards {
722
+ display: flex;
723
+ flex-direction: column;
724
+ gap: 8px;
725
+ padding: 2px 8px 10px;
726
+ overflow-y: auto;
727
+ flex: 1;
728
+ min-height: 0;
729
+ }
730
+
731
+ .rec-column-empty {
732
+ padding: 24px 8px;
733
+ text-align: center;
734
+ font-size: 12px;
735
+ color: var(--dsw-alias-label-tertiary);
736
+ }
737
+
738
+ .rec-card {
739
+ display: flex;
740
+ flex-direction: column;
741
+ gap: 6px;
742
+ padding: 10px 12px;
743
+ text-align: left;
744
+ background: var(--dsw-alias-bg-base);
745
+ border: 1px solid var(--dsw-alias-border-l2);
746
+ border-radius: 10px;
747
+ cursor: pointer;
748
+ color: var(--dsw-alias-label-primary);
749
+ font-family: inherit;
750
+ transition: box-shadow 120ms ease, border-color 120ms ease, transform 120ms ease;
751
+ }
752
+
753
+ .rec-card:hover {
754
+ box-shadow: var(--dsw-shadow-lv2);
755
+ border-color: var(--dsw-alias-border-l3);
756
+ transform: translateY(-1px);
757
+ }
758
+
759
+ .rec-card:active {
760
+ box-shadow: var(--dsw-shadow-lv1);
761
+ transform: translateY(0);
762
+ }
763
+
764
+ .rec-card[data-state='active'] { border-color: var(--dsw-alias-state-business-primary); }
765
+ .rec-card[data-state='paused'] { border-color: var(--dsw-alias-state-warn-primary); }
766
+ .rec-card[data-state='blocked'] { border-color: var(--dsw-alias-state-error-primary); }
767
+ .rec-card[data-state='complete'] { border-color: var(--dsw-alias-state-success-primary); }
768
+
769
+ .rec-card-title {
770
+ font-size: 13px;
771
+ font-weight: 600;
772
+ line-height: 1.35;
773
+ overflow: hidden;
774
+ display: -webkit-box;
775
+ -webkit-line-clamp: 2;
776
+ -webkit-box-orient: vertical;
777
+ }
778
+
779
+ .rec-card-progress {
780
+ height: 6px;
781
+ background: var(--dsw-alias-interactive-bg-hover);
782
+ border-radius: 3px;
783
+ overflow: hidden;
784
+ }
785
+
786
+ .rec-card-progress-fill {
787
+ height: 100%;
788
+ background: var(--dsw-alias-state-business-primary);
789
+ }
790
+
791
+ .rec-card-meta {
792
+ display: flex;
793
+ align-items: center;
794
+ gap: 8px;
795
+ font-size: 11px;
796
+ color: var(--dsw-alias-label-tertiary);
797
+ }
798
+
799
+ .rec-card-phase {
800
+ flex: 1;
801
+ overflow: hidden;
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+
806
+ .rec-badge {
807
+ flex: none;
808
+ padding: 2px 10px;
809
+ font-size: 12px;
810
+ border-radius: 999px;
811
+ border: 1px solid var(--dsw-alias-border-l2);
812
+ color: var(--dsw-alias-label-secondary);
813
+ }
814
+
815
+ .rec-badge[data-status='active'] { color: var(--dsw-alias-state-business-primary); border-color: var(--dsw-alias-state-business-primary); }
816
+ .rec-badge[data-status='paused'] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
817
+ .rec-badge[data-status='blocked'] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
818
+ .rec-badge[data-status='complete'] { color: var(--dsw-alias-state-success-primary); border-color: var(--dsw-alias-state-success-primary); }
819
+ .rec-badge[data-status='new'] { color: var(--dsw-alias-label-tertiary); }
820
+
821
+ .rec-badge-tamper {
822
+ color: var(--dsw-alias-state-error-primary);
823
+ border-color: var(--dsw-alias-state-error-primary);
824
+ }
825
+
826
+ /* inspector: centered modal-style detail */
827
+ .rec-inspector {
828
+ position: fixed;
829
+ inset: 0;
830
+ z-index: 9999;
831
+ display: flex;
832
+ align-items: center;
833
+ justify-content: center;
834
+ background: var(--dsw-alias-bg-mask-1);
835
+ pointer-events: auto;
836
+ }
837
+
838
+ .rec-detail {
839
+ display: flex;
840
+ flex-direction: column;
841
+ width: min(640px, calc(100vw - 48px));
842
+ max-height: calc(100vh - 80px);
843
+ background: var(--dsw-alias-bg-base);
844
+ border: 1px solid var(--dsw-alias-border-l2);
845
+ border-radius: 14px;
846
+ box-shadow: var(--dsw-shadow-lv3);
847
+ color: var(--dsw-alias-label-primary);
848
+ overflow: hidden;
849
+ }
850
+
851
+ .rec-detail-header {
852
+ display: flex;
853
+ align-items: center;
854
+ gap: 10px;
855
+ padding: 14px 18px;
856
+ border-bottom: 1px solid var(--dsw-alias-separator-primary);
857
+ flex: none;
858
+ }
859
+
860
+ .rec-detail-title {
861
+ margin: 0;
862
+ flex: 1;
863
+ font-size: 15px;
864
+ font-weight: 700;
865
+ overflow-wrap: anywhere;
866
+ }
867
+
868
+ .rec-back {
869
+ display: inline-flex;
870
+ align-items: center;
871
+ gap: 4px;
872
+ padding: 5px 12px;
873
+ font-size: 12px;
874
+ color: var(--dsw-alias-label-primary);
875
+ background: transparent;
876
+ border: 1px solid var(--dsw-alias-border-l2);
877
+ border-radius: 8px;
878
+ cursor: pointer;
879
+ white-space: nowrap;
880
+ }
881
+
882
+ .rec-back:hover {
883
+ background: var(--dsw-alias-interactive-bg-hover);
884
+ }
885
+
886
+ .rec-detail-body {
887
+ padding: 14px 18px;
888
+ overflow-y: auto;
889
+ display: flex;
890
+ flex-direction: column;
891
+ gap: 16px;
892
+ flex: 1;
893
+ }
894
+
895
+ .rec-detail-section {
896
+ display: flex;
897
+ flex-direction: column;
898
+ gap: 6px;
899
+ }
900
+
901
+ .rec-detail-section h3 {
902
+ margin: 0;
903
+ font-size: 12px;
904
+ font-weight: 700;
905
+ color: var(--dsw-alias-label-tertiary);
906
+ }
907
+
908
+ .rec-phase-row {
909
+ display: flex;
910
+ align-items: center;
911
+ gap: 10px;
912
+ padding: 8px 10px;
913
+ border: 1px solid var(--dsw-alias-border-l1);
914
+ border-radius: 8px;
915
+ font-size: 13px;
916
+ }
917
+
918
+ .rec-phase-id {
919
+ min-width: 48px;
920
+ font-family: var(--dsw-font-markdown-code-block-small);
921
+ color: var(--dsw-alias-state-business-primary);
922
+ }
923
+
924
+ .rec-phase-name {
925
+ flex: 1;
926
+ color: var(--dsw-alias-label-primary);
927
+ overflow: hidden;
928
+ text-overflow: ellipsis;
929
+ white-space: nowrap;
930
+ }
931
+
932
+ .rec-lockhash {
933
+ font-family: var(--dsw-font-markdown-code-block-small);
934
+ font-size: 11px;
935
+ color: var(--dsw-alias-label-tertiary);
936
+ }
937
+
938
+ @media (prefers-reduced-motion: reduce) {
939
+ .rec-card, .rec-back, .rec-close { transition: none; }
940
+ }
941
+ `;
942
+ /**
943
+ * Inject the board/inspector stylesheet once. Idempotent: if a <style
944
+ * data-dsh-recursive> already exists, returns a disposer that does nothing
945
+ * (the first injector owns the element). Returns a disposer that removes it.
946
+ */
947
+ function injectBoardStyles() {
948
+ if (typeof document === "undefined") return () => {};
949
+ if (document.querySelector("style[data-dsh-recursive]") !== null) return () => {};
950
+ const style = document.createElement("style");
951
+ style.setAttribute("data-dsh-recursive", "");
952
+ style.textContent = BOARD_CSS;
953
+ document.head.appendChild(style);
954
+ let disposed = false;
955
+ return () => {
956
+ if (disposed) return;
957
+ disposed = true;
958
+ style.remove();
959
+ };
960
+ }
961
+ //#endregion
744
962
  //#region src/client/slots.ts
745
963
  /**
746
964
  * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
@@ -792,6 +1010,7 @@ window.__ModuleLoader__.load({
792
1010
  }
793
1011
  function registerSlots(ctx) {
794
1012
  const disposers = [];
1013
+ disposers.push(injectBoardStyles());
795
1014
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
796
1015
  name: "sidebar.footer.action",
797
1016
  id: "recursive",
@@ -809,7 +1028,10 @@ window.__ModuleLoader__.load({
809
1028
  }, (props) => {
810
1029
  const useSessions = props?.useSessions;
811
1030
  if (useSessions === void 0) return null;
812
- return (0, react.createElement)(RecursiveBoardOverlay, { useSessions });
1031
+ return (0, react.createElement)(RecursiveBoardOverlay, {
1032
+ useSessions,
1033
+ useWorkspaces: props?.useWorkspaces
1034
+ });
813
1035
  })));
814
1036
  disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
815
1037
  name: "conversation.input.dock",
@@ -840,15 +1062,15 @@ window.__ModuleLoader__.load({
840
1062
  * Run 10: consumes the full SessionListStateLike through the identity selector
841
1063
  * (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
842
1064
  */
843
- function RecursiveBoardOverlay({ useSessions }) {
1065
+ function RecursiveBoardOverlay({ useSessions, useWorkspaces }) {
844
1066
  const board = useBoardState();
845
- const list = useRecursiveSessions(useSessions);
846
- const content = overlayContent(board, list);
847
- const cwd = currentSessionCwd(list);
848
- const snapshot = useLiveProjection({
849
- sessionId: list.current,
850
- cwd
851
- });
1067
+ const sessions = useRecursiveSessions(useSessions);
1068
+ const workspaces = useWorkspaces !== void 0 ? useWorkspaces((s) => s) : {
1069
+ items: [],
1070
+ recentWorkspaceId: void 0
1071
+ };
1072
+ const content = overlayContent(board, sessions);
1073
+ const snapshot = useLiveProjection({ cwd: currentWorkspacePath(workspaces, sessions) });
852
1074
  if (content === "hidden") return null;
853
1075
  if (content === "inspector" && board.selection !== null) return (0, react.createElement)(Inspector, {
854
1076
  runId: board.selection.runId,
@@ -910,6 +1132,7 @@ window.__ModuleLoader__.load({
910
1132
  exports.StatusStrip = StatusStrip;
911
1133
  exports.apply = apply;
912
1134
  exports.currentSessionCwd = currentSessionCwd;
1135
+ exports.currentWorkspacePath = currentWorkspacePath;
913
1136
  exports.fetchLiveState = fetchLiveState;
914
1137
  exports.inject = inject;
915
1138
  exports.isRecursivePreset = isRecursivePreset;