priiisk 0.1.19 → 0.1.21

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.
@@ -2,15 +2,16 @@ import { createRequire as __priiiskCreateRequire } from "node:module";
2
2
  const require = __priiiskCreateRequire(import.meta.url);
3
3
  import {
4
4
  CampAskState,
5
- get2 as get,
6
- make2 as make3,
5
+ CampWorkerState,
6
+ get2,
7
+ make2 as make4,
7
8
  modify,
8
9
  modifyEffect,
9
- set,
10
+ set as set2,
10
11
  tagsExhaustive,
11
12
  type,
12
- update
13
- } from "./chunk-5TXANNSW.js";
13
+ update as update2
14
+ } from "./chunk-U2FODXMR.js";
14
15
  import {
15
16
  Tag,
16
17
  TaggedError,
@@ -18,36 +19,44 @@ import {
18
19
  _void,
19
20
  acquireRelease,
20
21
  addFinalizer2 as addFinalizer,
21
- as2 as as,
22
+ as,
22
23
  catchAll,
23
- catchAllCause2 as catchAllCause,
24
+ catchAllCause,
24
25
  currentTimeMillis,
26
+ equipmentDescriptionLines,
25
27
  fail4 as fail,
26
- flatMap4 as flatMap,
28
+ flatMap3 as flatMap,
27
29
  forEach,
28
- forever2 as forever,
30
+ forever,
29
31
  forkScoped,
30
32
  gen,
33
+ get4 as get,
34
+ getAndSet,
31
35
  ignore,
32
36
  isInterruptedOnly,
33
37
  isSuccess,
34
- make12 as make2,
35
- make7 as make,
38
+ make14 as make3,
39
+ make6 as make,
40
+ make7 as make2,
36
41
  makeRuntime,
42
+ millis,
43
+ never,
37
44
  pretty,
38
45
  run2 as run,
39
46
  runForEach,
40
47
  runFork,
41
48
  scoped,
49
+ set2 as set,
42
50
  sleep,
43
- succeed3 as succeed,
44
- succeed4 as succeed2,
45
- suspend2 as suspend,
46
- sync2 as sync,
51
+ succeed2 as succeed,
52
+ succeed3 as succeed2,
53
+ suspend,
54
+ sync,
47
55
  taggedEnum,
48
- tap2 as tap,
49
- zipRight2 as zipRight
50
- } from "./chunk-26WAMIP2.js";
56
+ tap,
57
+ update,
58
+ zipRight
59
+ } from "./chunk-DPNPQG2D.js";
51
60
  import "./chunk-7UPFIIZD.js";
52
61
 
53
62
  // packages/host-ui/src/backend/campUiBackend.ts
@@ -76,6 +85,7 @@ var makeInitialCampUiState = (snapshot, presentationNow = 0) => ({
76
85
  screen: CampUiScreen.camp,
77
86
  focusedSlotId: "roster",
78
87
  detailVisible: false,
88
+ inventoryVisible: false,
79
89
  presentationNow,
80
90
  drafts: /* @__PURE__ */ new Map(),
81
91
  scrollOffsets: /* @__PURE__ */ new Map(),
@@ -139,8 +149,8 @@ var updateCampUiStringSet = (current, value, included) => {
139
149
  var CampUiControllerError = class extends TaggedError("CampUiControllerError") {
140
150
  };
141
151
  var makeCampUiStore = (initial, presentationNow = 0) => gen(function* () {
142
- const modelRef = yield* make3(makeCampUiModel(initial, presentationNow));
143
- const updateState = (mutate) => update(modelRef, (model) => ({ ...model, state: mutate(model.state) }));
152
+ const modelRef = yield* make4(makeCampUiModel(initial, presentationNow));
153
+ const updateState = (mutate) => update2(modelRef, (model) => ({ ...model, state: mutate(model.state) }));
144
154
  const controller = {
145
155
  selectWorker: (workerId) => modifyEffect(
146
156
  modelRef,
@@ -201,6 +211,10 @@ var makeCampUiStore = (initial, presentationNow = 0) => gen(function* () {
201
211
  if (state.detailVisible === detailVisible) return state;
202
212
  return { ...state, detailVisible };
203
213
  }),
214
+ setInventoryVisible: (inventoryVisible) => updateState((state) => {
215
+ if (state.inventoryVisible === inventoryVisible) return state;
216
+ return { ...state, inventoryVisible };
217
+ }),
204
218
  setDetailBlockExpanded: (blockId, expanded) => updateState((state) => ({
205
219
  ...state,
206
220
  expandedDetailBlockIds: updateCampUiStringSet(
@@ -224,7 +238,7 @@ var makeCampUiStore = (initial, presentationNow = 0) => gen(function* () {
224
238
  })
225
239
  };
226
240
  return {
227
- current: get(modelRef),
241
+ current: get2(modelRef),
228
242
  changes: modelRef.changes,
229
243
  updateSnapshot: (snapshot) => modify(modelRef, (current) => {
230
244
  const stale = current.snapshot.generation === snapshot.generation && snapshot.revision <= current.snapshot.revision;
@@ -238,9 +252,16 @@ var makeCampUiStore = (initial, presentationNow = 0) => gen(function* () {
238
252
  var makeCampUiComponentGroup = (components) => {
239
253
  const inputComponents = components.some((component) => component.handleInput !== void 0);
240
254
  const pointerComponents = components.some((component) => component.handlePointer !== void 0);
255
+ const sizingComponents = components.some((component) => component.preferredRows !== void 0);
241
256
  const invalidatingComponents = components.some((component) => component.invalidate !== void 0);
242
257
  return {
243
258
  render: (viewport) => components.flatMap((component) => component.render(viewport)),
259
+ ...sizingComponents ? {
260
+ preferredRows: (width) => components.reduce(
261
+ (total, component) => total + (component.preferredRows?.(width) ?? 0),
262
+ 0
263
+ )
264
+ } : {},
244
265
  ...components.some((component) => component.focusable === true) ? { focusable: true } : {},
245
266
  ...inputComponents ? {
246
267
  handleInput: (input) => [...components].reverse().some((component) => component.handleInput?.(input) === true)
@@ -268,14 +289,14 @@ var emptyCampUiComponent = {
268
289
  // packages/host-ui/src/composition/campUiSlots.ts
269
290
  var CampUiSlot = {
270
291
  campHeader: "camp-header",
271
- seat: "seat",
292
+ activity: "activity",
293
+ equipment: "equipment",
272
294
  resources: "resources",
273
295
  roster: "roster",
274
296
  sessionHeader: "session-header",
275
297
  transcript: "transcript",
276
298
  composer: "composer",
277
- campFooter: "camp-footer",
278
- sessionFooter: "session-footer",
299
+ footer: "footer",
279
300
  overlay: "overlay"
280
301
  };
281
302
  var CampUiContributionStrategy = {
@@ -440,6 +461,7 @@ var makeCampUiIntentHandler = (options) => type().pipe(
440
461
  }),
441
462
  SelectWorker: ({ workerId }) => options.controller.selectWorker(workerId),
442
463
  SetDraft: ({ workerId, draft }) => options.controller.setDraft(workerId, draft),
464
+ SetInventoryVisible: ({ visible }) => options.controller.setInventoryVisible(visible),
443
465
  SetScrollOffset: ({ workerId, offset }) => options.controller.setScrollOffset(workerId, offset),
444
466
  SetTranscriptVisible: ({ visible }) => options.controller.setDetailVisible(visible),
445
467
  Steer: ({ workerId, message }) => message.trim() === "" ? _void : options.backend.steer({ workerId, message }).pipe(
@@ -502,13 +524,16 @@ var findCampUiSpatialFocusTarget = (frame, currentSlotId, focusableSlotIds, dire
502
524
  // packages/host-ui/src/layout/campUiLayoutPolicy.ts
503
525
  var defaultCampUiLayoutPolicy = {
504
526
  leftMinimumWidth: 28,
505
- leftMaximumWidth: 40,
506
- sessionMinimumWidth: 52,
527
+ rightMinimumWidth: 28,
528
+ leftTrackWidthThreshold: 82,
529
+ rightTrackWidthThreshold: 112,
530
+ trackGapWidth: 1,
507
531
  wideMinimumHeight: 18,
508
- leftRatio: 0.3,
532
+ campHeaderHeight: 1,
509
533
  footerHeight: 3,
510
534
  sessionHeaderHeight: 1,
511
- composerHeight: 3
535
+ composerMinimumHeight: 1,
536
+ composerMaximumHeight: 4
512
537
  };
513
538
 
514
539
  // packages/host-ui/src/layout/campUiSizing.ts
@@ -516,17 +541,86 @@ var CampUiSectionSizeKind = {
516
541
  fixed: "fixed",
517
542
  grow: "grow"
518
543
  };
519
- var rows = (value) => Math.max(0, Math.floor(value));
544
+ var CampUiTrackSizeKind = {
545
+ fixed: "fixed",
546
+ min: "min",
547
+ fill: "fill"
548
+ };
549
+ var units = (value) => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
550
+ var allocateCampUiTrackColumns = (availableColumns, sizes) => {
551
+ const width = units(availableColumns);
552
+ const allocated = sizes.map(() => 0);
553
+ const visible = sizes.map(
554
+ (size) => size.collapseBelow === void 0 || width >= units(size.collapseBelow)
555
+ );
556
+ const minimumIndexes = [];
557
+ const fillIndexes = [];
558
+ let remaining = width;
559
+ for (const [index, size] of sizes.entries()) {
560
+ if (visible[index] !== true) continue;
561
+ if (size.kind === CampUiTrackSizeKind.min) {
562
+ minimumIndexes.push(index);
563
+ continue;
564
+ }
565
+ if (size.kind === CampUiTrackSizeKind.fill) {
566
+ fillIndexes.push(index);
567
+ continue;
568
+ }
569
+ const trackWidth = Math.min(remaining, units(size.columns));
570
+ allocated[index] = trackWidth;
571
+ remaining -= trackWidth;
572
+ }
573
+ const maximumMinimum = Math.max(
574
+ 0,
575
+ ...minimumIndexes.map((index) => {
576
+ const size = sizes[index];
577
+ return size?.kind === CampUiTrackSizeKind.min ? units(size.minimumColumns) : 0;
578
+ })
579
+ );
580
+ for (let column = 0; column < maximumMinimum && remaining > 0; column += 1) {
581
+ for (const index of minimumIndexes) {
582
+ const size = sizes[index];
583
+ if (size?.kind !== CampUiTrackSizeKind.min || column >= units(size.minimumColumns)) continue;
584
+ allocated[index] = (allocated[index] ?? 0) + 1;
585
+ remaining -= 1;
586
+ if (remaining === 0) break;
587
+ }
588
+ }
589
+ const weighted = fillIndexes.flatMap((index) => {
590
+ const size = sizes[index];
591
+ const weight = size?.kind === CampUiTrackSizeKind.fill ? Math.max(0, size.weight) : 0;
592
+ return weight > 0 ? [{ index, weight }] : [];
593
+ });
594
+ const totalWeight = weighted.reduce((total, item) => total + item.weight, 0);
595
+ if (remaining > 0 && totalWeight > 0) {
596
+ const distributable = remaining;
597
+ const shares = weighted.map((item) => {
598
+ const exact = distributable * item.weight / totalWeight;
599
+ return { ...item, columns: Math.floor(exact), fraction: exact - Math.floor(exact) };
600
+ });
601
+ for (const share of shares) allocated[share.index] = share.columns;
602
+ let roundingRemainder = distributable - shares.reduce((total, item) => total + item.columns, 0);
603
+ const remainderOrder = [...shares].sort(
604
+ (left, right) => right.fraction - left.fraction || left.index - right.index
605
+ );
606
+ for (const share of remainderOrder) {
607
+ if (roundingRemainder === 0) break;
608
+ allocated[share.index] = (allocated[share.index] ?? 0) + 1;
609
+ roundingRemainder -= 1;
610
+ }
611
+ }
612
+ return allocated;
613
+ };
520
614
  var allocateCampUiSectionRows = (availableRows, sizes) => {
521
615
  const allocated = sizes.map(() => 0);
522
- let remaining = rows(availableRows);
616
+ let remaining = units(availableRows);
523
617
  const growIndexes = [];
524
618
  for (const [index, size] of sizes.entries()) {
525
619
  if (size.kind === CampUiSectionSizeKind.grow) {
526
620
  growIndexes.push(index);
527
621
  continue;
528
622
  }
529
- const height = Math.min(remaining, rows(size.rows));
623
+ const height = Math.min(remaining, units(size.rows));
530
624
  allocated[index] = height;
531
625
  remaining -= height;
532
626
  }
@@ -534,13 +628,13 @@ var allocateCampUiSectionRows = (availableRows, sizes) => {
534
628
  0,
535
629
  ...growIndexes.map((index) => {
536
630
  const size = sizes[index];
537
- return size?.kind === CampUiSectionSizeKind.grow ? rows(size.minimumRows ?? 0) : 0;
631
+ return size?.kind === CampUiSectionSizeKind.grow ? units(size.minimumRows ?? 0) : 0;
538
632
  })
539
633
  );
540
634
  for (let minimumRow = 0; minimumRow < maximumMinimum && remaining > 0; minimumRow += 1) {
541
635
  for (const index of growIndexes) {
542
636
  const size = sizes[index];
543
- if (size?.kind !== CampUiSectionSizeKind.grow || minimumRow >= rows(size.minimumRows ?? 0)) {
637
+ if (size?.kind !== CampUiSectionSizeKind.grow || minimumRow >= units(size.minimumRows ?? 0)) {
544
638
  continue;
545
639
  }
546
640
  allocated[index] = (allocated[index] ?? 0) + 1;
@@ -566,10 +660,14 @@ var allocateCampUiSectionRows = (availableRows, sizes) => {
566
660
  return allocated;
567
661
  };
568
662
  var defaultCampUiLeftSectionSizes = {
569
- camp: { kind: CampUiSectionSizeKind.fixed, rows: 4 },
663
+ camp: { kind: CampUiSectionSizeKind.fixed, rows: 3 },
570
664
  resources: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 },
571
665
  workers: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 }
572
666
  };
667
+ var defaultCampUiRightSectionSizes = {
668
+ activity: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 2 },
669
+ equipment: { kind: CampUiSectionSizeKind.fixed, rows: 7 }
670
+ };
573
671
 
574
672
  // packages/host-ui/src/layout/campUiLayout.ts
575
673
  var CampUiLayoutMode = {
@@ -578,31 +676,47 @@ var CampUiLayoutMode = {
578
676
  };
579
677
  var region = (slotId, x, y, width, height) => width <= 0 || height <= 0 ? void 0 : { slotId, x, y, width, height };
580
678
  var compact = (items) => items.filter((item) => item !== void 0);
581
- var fixedRows = (available, requested) => Math.min(Math.max(available, 0), Math.max(requested, 0));
679
+ var units2 = (value) => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
680
+ var fixedRows = (available, requested) => Math.min(units2(available), units2(requested));
582
681
  var leftSectionHeights = (bodyHeight, slots) => {
583
682
  const size = (slotId, fallback) => slots?.get(slotId)?.sectionSize ?? fallback;
584
683
  return allocateCampUiSectionRows(bodyHeight, [
585
684
  size(CampUiSlot.campHeader, defaultCampUiLeftSectionSizes.camp),
586
- size(CampUiSlot.resources, defaultCampUiLeftSectionSizes.resources),
587
685
  size(CampUiSlot.roster, defaultCampUiLeftSectionSizes.workers)
588
686
  ]);
589
687
  };
688
+ var rightSectionHeights = (bodyHeight, width, slots) => {
689
+ const minimumActivityRows = Math.min(units2(bodyHeight), 2);
690
+ const fallback = defaultCampUiRightSectionSizes.equipment.rows;
691
+ const preferred = units2(
692
+ slots?.get(CampUiSlot.equipment)?.preferredRows?.(units2(width)) ?? fallback
693
+ );
694
+ const equipmentHeight = fixedRows(Math.max(0, bodyHeight - minimumActivityRows), preferred);
695
+ return [Math.max(0, bodyHeight - equipmentHeight), equipmentHeight];
696
+ };
697
+ var composerHeight = (availableHeight, width, policy, slots) => {
698
+ const maximum = units2(policy.composerMaximumHeight);
699
+ const minimum = Math.min(maximum, units2(policy.composerMinimumHeight));
700
+ const preferred = units2(
701
+ slots?.get(CampUiSlot.composer)?.preferredRows?.(units2(width)) ?? minimum
702
+ );
703
+ return fixedRows(availableHeight, Math.max(minimum, Math.min(maximum, preferred)));
704
+ };
590
705
  var makeNarrowFrame = (viewport, state, policy, slots) => {
591
706
  const footerHeight = fixedRows(viewport.height, policy.footerHeight);
592
707
  const bodyHeight = Math.max(0, viewport.height - footerHeight);
593
708
  const footerY = bodyHeight;
594
709
  const workerScreen = state.screen === CampUiScreen.worker && state.selectedWorkerId !== void 0;
595
- const [campHeight = 0, resourcesHeight = 0, rosterHeight = 0] = workerScreen ? [] : leftSectionHeights(bodyHeight, slots);
710
+ const [campHeight = 0, rosterHeight = 0] = workerScreen ? [] : leftSectionHeights(bodyHeight, slots);
596
711
  const sessionHeaderHeight = workerScreen ? fixedRows(bodyHeight, policy.sessionHeaderHeight) : 0;
597
- const composerHeight = workerScreen ? fixedRows(bodyHeight - sessionHeaderHeight, policy.composerHeight) : 0;
598
- const transcriptHeight = workerScreen ? bodyHeight - sessionHeaderHeight - composerHeight : 0;
712
+ const inputHeight = workerScreen ? composerHeight(bodyHeight - sessionHeaderHeight, viewport.width, policy, slots) : 0;
713
+ const transcriptHeight = workerScreen ? bodyHeight - sessionHeaderHeight - inputHeight : 0;
599
714
  return {
600
715
  mode: CampUiLayoutMode.narrow,
601
716
  dividers: [],
602
717
  regions: compact([
603
718
  region(CampUiSlot.campHeader, 0, 0, viewport.width, campHeight),
604
- region(CampUiSlot.resources, 0, campHeight, viewport.width, resourcesHeight),
605
- region(CampUiSlot.roster, 0, campHeight + resourcesHeight, viewport.width, rosterHeight),
719
+ region(CampUiSlot.roster, 0, campHeight, viewport.width, rosterHeight),
606
720
  region(CampUiSlot.sessionHeader, 0, 0, viewport.width, sessionHeaderHeight),
607
721
  region(CampUiSlot.transcript, 0, sessionHeaderHeight, viewport.width, transcriptHeight),
608
722
  region(
@@ -610,57 +724,79 @@ var makeNarrowFrame = (viewport, state, policy, slots) => {
610
724
  0,
611
725
  sessionHeaderHeight + transcriptHeight,
612
726
  viewport.width,
613
- composerHeight
727
+ inputHeight
614
728
  ),
615
- region(
616
- workerScreen ? CampUiSlot.sessionFooter : CampUiSlot.campFooter,
617
- 0,
618
- footerY,
619
- viewport.width,
620
- footerHeight
621
- )
729
+ region(CampUiSlot.footer, 0, footerY, viewport.width, footerHeight)
622
730
  ])
623
731
  };
624
732
  };
625
733
  var makeWideFrame = (viewport, policy, slots) => {
626
- const footerHeight = fixedRows(viewport.height, policy.footerHeight);
627
- const bodyHeight = Math.max(0, viewport.height - footerHeight);
628
- const leftWidth = Math.max(
629
- policy.leftMinimumWidth,
630
- Math.min(
631
- policy.leftMaximumWidth,
632
- viewport.width - policy.sessionMinimumWidth - 1,
633
- Math.floor(viewport.width * policy.leftRatio)
634
- )
635
- );
636
- const rightX = leftWidth + 1;
637
- const rightWidth = viewport.width - rightX;
638
- const [campHeight = 0, resourcesHeight = 0, rosterHeight = 0] = leftSectionHeights(
734
+ const campHeaderHeight = fixedRows(viewport.height, policy.campHeaderHeight);
735
+ const footerHeight = fixedRows(viewport.height - campHeaderHeight, policy.footerHeight);
736
+ const bodyHeight = Math.max(0, viewport.height - campHeaderHeight - footerHeight);
737
+ const footerY = campHeaderHeight + bodyHeight;
738
+ const [leftWidth = 0, leftGap = 0, centerWidth = 0, rightGap = 0, rightWidth = 0] = allocateCampUiTrackColumns(viewport.width, [
739
+ {
740
+ kind: CampUiTrackSizeKind.min,
741
+ minimumColumns: policy.leftMinimumWidth,
742
+ collapseBelow: policy.leftTrackWidthThreshold
743
+ },
744
+ {
745
+ kind: CampUiTrackSizeKind.fixed,
746
+ columns: policy.trackGapWidth,
747
+ collapseBelow: policy.leftTrackWidthThreshold
748
+ },
749
+ { kind: CampUiTrackSizeKind.fill, weight: 1 },
750
+ {
751
+ kind: CampUiTrackSizeKind.fixed,
752
+ columns: policy.trackGapWidth,
753
+ collapseBelow: policy.rightTrackWidthThreshold
754
+ },
755
+ {
756
+ kind: CampUiTrackSizeKind.min,
757
+ minimumColumns: policy.rightMinimumWidth,
758
+ collapseBelow: policy.rightTrackWidthThreshold
759
+ }
760
+ ]);
761
+ const centerX2 = leftWidth + leftGap;
762
+ const rightX = centerX2 + centerWidth + rightGap;
763
+ const [activityHeight = 0, equipmentHeight = 0] = rightSectionHeights(
639
764
  bodyHeight,
765
+ rightWidth,
640
766
  slots
641
767
  );
642
768
  const sessionHeaderHeight = fixedRows(bodyHeight, policy.sessionHeaderHeight);
643
- const composerHeight = fixedRows(bodyHeight - sessionHeaderHeight, policy.composerHeight);
644
- const transcriptHeight = bodyHeight - sessionHeaderHeight - composerHeight;
645
- const footerY = bodyHeight;
769
+ const inputHeight = composerHeight(bodyHeight - sessionHeaderHeight, centerWidth, policy, slots);
770
+ const transcriptHeight = bodyHeight - sessionHeaderHeight - inputHeight;
771
+ const bodyY = campHeaderHeight;
646
772
  return {
647
773
  mode: CampUiLayoutMode.wide,
648
- dividers: viewport.height === 0 ? [] : [{ x: leftWidth, y: 0, height: viewport.height }],
774
+ dividers: [],
649
775
  regions: compact([
650
- region(CampUiSlot.campHeader, 0, 0, leftWidth, campHeight),
651
- region(CampUiSlot.resources, 0, campHeight, leftWidth, resourcesHeight),
652
- region(CampUiSlot.roster, 0, campHeight + resourcesHeight, leftWidth, rosterHeight),
653
- region(CampUiSlot.sessionHeader, rightX, 0, rightWidth, sessionHeaderHeight),
654
- region(CampUiSlot.transcript, rightX, sessionHeaderHeight, rightWidth, transcriptHeight),
776
+ region(CampUiSlot.campHeader, 0, 0, viewport.width, campHeaderHeight),
777
+ region(CampUiSlot.roster, 0, bodyY, leftWidth, bodyHeight),
778
+ region(CampUiSlot.sessionHeader, centerX2, bodyY, centerWidth, sessionHeaderHeight),
779
+ region(
780
+ CampUiSlot.transcript,
781
+ centerX2,
782
+ bodyY + sessionHeaderHeight,
783
+ centerWidth,
784
+ transcriptHeight
785
+ ),
655
786
  region(
656
787
  CampUiSlot.composer,
657
- rightX,
658
- sessionHeaderHeight + transcriptHeight,
659
- rightWidth,
660
- composerHeight
788
+ centerX2,
789
+ bodyY + sessionHeaderHeight + transcriptHeight,
790
+ centerWidth,
791
+ inputHeight
661
792
  ),
662
- region(CampUiSlot.campFooter, 0, footerY, leftWidth, footerHeight),
663
- region(CampUiSlot.sessionFooter, rightX, footerY, rightWidth, footerHeight)
793
+ region(CampUiSlot.activity, rightX, bodyY, rightWidth, activityHeight),
794
+ region(CampUiSlot.equipment, rightX, bodyY + activityHeight, rightWidth, equipmentHeight),
795
+ /*
796
+ * One line across the whole width: two footers pinned to the corners split
797
+ * along the track boundary and repeated what the worker cards already say.
798
+ */
799
+ region(CampUiSlot.footer, 0, footerY, viewport.width, footerHeight)
664
800
  ])
665
801
  };
666
802
  };
@@ -668,11 +804,10 @@ var makeDefaultCampUiLayout = (policy = defaultCampUiLayoutPolicy) => ({
668
804
  id: "default",
669
805
  resolve: (viewport, state, slots) => {
670
806
  const normalized = {
671
- width: Math.max(0, viewport.width),
672
- height: Math.max(0, viewport.height)
807
+ width: units2(viewport.width),
808
+ height: units2(viewport.height)
673
809
  };
674
- const wideMinimumWidth = policy.leftMinimumWidth + 1 + policy.sessionMinimumWidth;
675
- return normalized.width >= wideMinimumWidth && normalized.height >= policy.wideMinimumHeight ? makeWideFrame(normalized, policy, slots) : makeNarrowFrame(normalized, state, policy, slots);
810
+ return normalized.height >= policy.wideMinimumHeight ? makeWideFrame(normalized, policy, slots) : makeNarrowFrame(normalized, state, policy, slots);
676
811
  }
677
812
  });
678
813
 
@@ -850,7 +985,7 @@ var fallbackWorkerPresentation = {
850
985
  glyph: CampUiGlyph.neutral
851
986
  };
852
987
  var attentionStates = /* @__PURE__ */ new Set(["waiting_for_orchestrator", "recovering", "retrying"]);
853
- var renderCampUiWorkerState = (state, options) => {
988
+ var resolveCampUiWorkerState = (state, options) => {
854
989
  const base = workerStatePresentation.get(state) ?? fallbackWorkerPresentation;
855
990
  const terminal = state === "completed" || state === "closed";
856
991
  const streamingLifts = !terminal && options.streaming && !attentionStates.has(state);
@@ -859,7 +994,11 @@ var renderCampUiWorkerState = (state, options) => {
859
994
  tone: CampUiTone.attention,
860
995
  glyph: CampUiGlyph.attention
861
996
  } : streamingLifts ? workerStatePresentation.get("running") : base;
862
- const resolved = presentation ?? fallbackWorkerPresentation;
997
+ return presentation ?? fallbackWorkerPresentation;
998
+ };
999
+ var campUiWorkerStateLabel = (state, options) => resolveCampUiWorkerState(state, options).label;
1000
+ var renderCampUiWorkerState = (state, options) => {
1001
+ const resolved = resolveCampUiWorkerState(state, options);
863
1002
  return renderCampUiStatus(resolved.label, resolved.tone, resolved.glyph);
864
1003
  };
865
1004
  var renderSelectedCampUiRow = (line, width) => {
@@ -927,44 +1066,44 @@ var toneByState = {
927
1066
  [CampAskState.expired]: CampUiTone.error,
928
1067
  [CampAskState.interrupted]: CampUiTone.error
929
1068
  };
1069
+ var campUiAskDetailBlock = (ask, model, width) => {
1070
+ const blockId = campUiAskBlockId(ask.askId);
1071
+ const mode = ask.blocking ? "blocking" : "non-blocking";
1072
+ const stateLabel = {
1073
+ [CampAskState.pending]: `pending \xB7 ${formatCampUiDuration(
1074
+ model.state.presentationNow - ask.openedAt
1075
+ )}`,
1076
+ [CampAskState.answered]: "answered",
1077
+ [CampAskState.expired]: "expired",
1078
+ [CampAskState.interrupted]: "interrupted"
1079
+ }[ask.state];
1080
+ const summary = {
1081
+ [CampAskState.pending]: `Awaiting orchestrator reply \xB7 ${ask.askId}`,
1082
+ [CampAskState.answered]: `${ask.answerSummary ?? "Answered"} \xB7 ${ask.askId}`,
1083
+ [CampAskState.expired]: `Expired \xB7 ${ask.askId}`,
1084
+ [CampAskState.interrupted]: `Interrupted by host restart \xB7 ${ask.askId}`
1085
+ }[ask.state];
1086
+ return {
1087
+ id: blockId,
1088
+ lines: renderCampUiDirectedBlock(
1089
+ {
1090
+ label: `Ask \xB7 to ${ask.controllerId} \xB7 ${mode} \xB7 ${stateLabel}`,
1091
+ markdown: ask.question,
1092
+ summary,
1093
+ tone: toneByState[ask.state],
1094
+ expanded: model.state.expandedDetailBlockIds.has(blockId),
1095
+ ...ask.state === CampAskState.answered ? { collapsedBodyLines: 0, showToggleHint: false } : {}
1096
+ },
1097
+ width
1098
+ ),
1099
+ toggleable: ask.state !== CampAskState.answered
1100
+ };
1101
+ };
1102
+ var isCampUiPendingAsk = (ask) => ask.state === CampAskState.pending;
930
1103
  var campUiPendingAskBlockRenderer = {
931
1104
  id: "pending-asks",
932
1105
  order: 100,
933
- render: ({ model, worker, width }) => {
934
- return worker.asks.map((ask) => {
935
- const blockId = campUiAskBlockId(ask.askId);
936
- const mode = ask.blocking ? "blocking" : "non-blocking";
937
- const stateLabel = {
938
- [CampAskState.pending]: `pending \xB7 ${formatCampUiDuration(
939
- model.state.presentationNow - ask.openedAt
940
- )}`,
941
- [CampAskState.answered]: "answered",
942
- [CampAskState.expired]: "expired",
943
- [CampAskState.interrupted]: "interrupted"
944
- }[ask.state];
945
- const summary = {
946
- [CampAskState.pending]: `Awaiting orchestrator reply \xB7 ${ask.askId}`,
947
- [CampAskState.answered]: `${ask.answerSummary ?? "Answered"} \xB7 ${ask.askId}`,
948
- [CampAskState.expired]: `Expired \xB7 ${ask.askId}`,
949
- [CampAskState.interrupted]: `Interrupted by host restart \xB7 ${ask.askId}`
950
- }[ask.state];
951
- return {
952
- id: blockId,
953
- lines: renderCampUiDirectedBlock(
954
- {
955
- label: `Ask \xB7 to ${ask.controllerId} \xB7 ${mode} \xB7 ${stateLabel}`,
956
- markdown: ask.question,
957
- summary,
958
- tone: toneByState[ask.state],
959
- expanded: model.state.expandedDetailBlockIds.has(blockId),
960
- ...ask.state === CampAskState.answered ? { collapsedBodyLines: 0, showToggleHint: false } : {}
961
- },
962
- width
963
- ),
964
- toggleable: ask.state !== CampAskState.answered
965
- };
966
- });
967
- }
1106
+ render: ({ model, worker, width }) => worker.asks.filter(isCampUiPendingAsk).map((ask) => campUiAskDetailBlock(ask, model, width))
968
1107
  };
969
1108
 
970
1109
  // packages/host-ui/src/pi/blocks/campUiAssistantMessage.ts
@@ -1053,7 +1192,7 @@ var campUiResultSignature = (result) => result === void 0 ? "pending" : campUiMe
1053
1192
 
1054
1193
  // packages/host-ui/src/pi/blocks/campUiComposer.ts
1055
1194
  import { getMarkdownTheme as getMarkdownTheme3, getSelectListTheme as getSelectListTheme4 } from "@earendil-works/pi-coding-agent";
1056
- import { Editor, Key, matchesKey } from "@earendil-works/pi-tui";
1195
+ import { CURSOR_MARKER, Editor, Key, matchesKey } from "@earendil-works/pi-tui";
1057
1196
  var makeCampUiComposerComponent = (context, tui) => {
1058
1197
  const markdownTheme = getMarkdownTheme3();
1059
1198
  const selectTheme = getSelectListTheme4();
@@ -1100,20 +1239,40 @@ var makeCampUiComposerComponent = (context, tui) => {
1100
1239
  editors.set(workerId, created);
1101
1240
  return created;
1102
1241
  };
1242
+ const renderSelectedEditor = (width) => {
1243
+ const model = context.readModel();
1244
+ const workerId = model.state.selectedWorkerId;
1245
+ const entry = selectedEditor();
1246
+ if (workerId === void 0 || entry === void 0) return void 0;
1247
+ const { editor } = entry;
1248
+ const focused = model.state.focusedSlotId === CampUiSlot.composer;
1249
+ editor.focused = focused;
1250
+ editor.borderColor = focused ? selectTheme.selectedPrefix : markdownTheme.codeBlockBorder;
1251
+ entry.setDraft(model.state.drafts.get(workerId) ?? "");
1252
+ editor.focused = true;
1253
+ let lines;
1254
+ try {
1255
+ lines = editor.render(width).slice(1, -1);
1256
+ } finally {
1257
+ editor.focused = focused;
1258
+ }
1259
+ const cursorRow = lines.findIndex((line) => line.includes(CURSOR_MARKER));
1260
+ return {
1261
+ cursorRow,
1262
+ lines: lines.map((line) => focused ? line : line.replaceAll(CURSOR_MARKER, ""))
1263
+ };
1264
+ };
1103
1265
  return {
1104
1266
  focusable: true,
1267
+ preferredRows: (width) => renderSelectedEditor(width)?.lines.length ?? 1,
1105
1268
  render: (viewport) => {
1106
- const model = context.readModel();
1107
- const workerId = model.state.selectedWorkerId;
1108
- const entry = selectedEditor();
1109
- if (workerId === void 0 || entry === void 0) {
1110
- return [selectTheme.description("worker not selected")];
1111
- }
1112
- const { editor } = entry;
1113
- editor.focused = model.state.focusedSlotId === CampUiSlot.composer;
1114
- editor.borderColor = editor.focused ? selectTheme.selectedPrefix : markdownTheme.codeBlockBorder;
1115
- entry.setDraft(model.state.drafts.get(workerId) ?? "");
1116
- return editor.render(viewport.width).slice(0, viewport.height);
1269
+ const rendered = renderSelectedEditor(viewport.width);
1270
+ if (rendered === void 0) return [selectTheme.description("worker not selected")];
1271
+ const start = rendered.cursorRow < 0 ? 0 : Math.min(
1272
+ Math.max(0, rendered.lines.length - viewport.height),
1273
+ Math.max(0, rendered.cursorRow - viewport.height + 1)
1274
+ );
1275
+ return rendered.lines.slice(start, start + viewport.height);
1117
1276
  },
1118
1277
  handleInput: (input) => {
1119
1278
  const entry = selectedEditor();
@@ -1147,50 +1306,12 @@ var makeCampUiComposerComponent = (context, tui) => {
1147
1306
  import { getSelectListTheme as getSelectListTheme8 } from "@earendil-works/pi-coding-agent";
1148
1307
  import { Key as Key2, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
1149
1308
 
1150
- // packages/host-ui/src/pi/blocks/campUiEquipmentBlock.ts
1151
- import { getMarkdownTheme as getMarkdownTheme4, UserMessageComponent } from "@earendil-works/pi-coding-agent";
1152
- var escapeMarkdown = (value) => value.replaceAll(/([\\`*_[\]{}()#+.!|>])/gu, "\\$1");
1153
- var renderNames = (names) => names.length === 0 ? "none" : names.map(escapeMarkdown).join(", ");
1154
- var renderMcpServers = (equipment) => renderNames(
1155
- equipment.mcp.serverNames.map((serverName) => {
1156
- const toolNames = equipment.mcp.toolNamesByServer?.[serverName];
1157
- return toolNames === void 0 ? serverName : `${serverName} (${toolNames.join(", ")})`;
1158
- })
1159
- );
1160
- var equipmentMarkdown = (equipment) => {
1161
- const role = equipment.role ?? equipment.preset ?? "unknown";
1162
- const tools = [.../* @__PURE__ */ new Set([...equipment.builtinTools, ...equipment.customToolNames])].sort();
1163
- const lines = [
1164
- `**Equipment \xB7 ${escapeMarkdown(role)}**`,
1165
- "",
1166
- `- **Tools:** ${renderNames(tools)}`,
1167
- `- **MCP:** ${renderMcpServers(equipment)}`,
1168
- `- **Skills:** ${renderNames(equipment.skillNames)}`,
1169
- `- **Access:** ${equipment.readOnly ? "read-only" : "read/write"}`
1170
- ];
1171
- return lines.join("\n");
1172
- };
1173
- var campUiEquipmentBlockRenderer = {
1174
- id: "role-equipment",
1175
- order: -100,
1176
- render: ({ worker, width }) => [
1177
- {
1178
- id: "role-equipment",
1179
- lines: new UserMessageComponent(
1180
- equipmentMarkdown(worker.snapshot.worker.equipment),
1181
- getMarkdownTheme4(),
1182
- 0
1183
- ).render(width)
1184
- }
1185
- ]
1186
- };
1187
-
1188
1309
  // packages/host-ui/src/pi/blocks/campUiTranscriptBlocks.ts
1189
- import { getMarkdownTheme as getMarkdownTheme7, UserMessageComponent as UserMessageComponent2 } from "@earendil-works/pi-coding-agent";
1310
+ import { getMarkdownTheme as getMarkdownTheme6, UserMessageComponent } from "@earendil-works/pi-coding-agent";
1190
1311
  import { Markdown as Markdown4 } from "@earendil-works/pi-tui";
1191
1312
 
1192
1313
  // packages/host-ui/src/pi/shared/campUiPanel.ts
1193
- import { getMarkdownTheme as getMarkdownTheme5, getSelectListTheme as getSelectListTheme5 } from "@earendil-works/pi-coding-agent";
1314
+ import { getMarkdownTheme as getMarkdownTheme4, getSelectListTheme as getSelectListTheme5 } from "@earendil-works/pi-coding-agent";
1194
1315
  import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
1195
1316
  var rule = (start, label, width, borderColor, labelColor) => {
1196
1317
  const prefix = `${borderColor(`${start}\u2500 `)}${labelColor(label)}${borderColor(" ")}`;
@@ -1199,7 +1320,7 @@ var rule = (start, label, width, borderColor, labelColor) => {
1199
1320
  };
1200
1321
  var renderCampUiPanel = (label, body, width) => {
1201
1322
  if (width <= 0) return [];
1202
- const borderColor = getMarkdownTheme5().codeBlockBorder;
1323
+ const borderColor = getMarkdownTheme4().codeBlockBorder;
1203
1324
  const labelColor = getSelectListTheme5().selectedText;
1204
1325
  if (width === 1) {
1205
1326
  return [borderColor("\u2502"), ...body.map(() => borderColor("\u2502")), borderColor("\u2570")];
@@ -1339,7 +1460,7 @@ var renderCampUiExploredGroup = (reads, width, expanded) => {
1339
1460
 
1340
1461
  // packages/host-ui/src/pi/blocks/campUiToolResult.ts
1341
1462
  import {
1342
- getMarkdownTheme as getMarkdownTheme6,
1463
+ getMarkdownTheme as getMarkdownTheme5,
1343
1464
  getSelectListTheme as getSelectListTheme7,
1344
1465
  truncateToVisualLines
1345
1466
  } from "@earendil-works/pi-coding-agent";
@@ -1414,13 +1535,13 @@ var renderCampUiToolResult = (result, width, expanded) => {
1414
1535
  ]);
1415
1536
  }
1416
1537
  const content = toolResultContent(result, true);
1417
- const lines = content === "" ? [] : new Markdown3(content, 0, 0, getMarkdownTheme6()).render(boundedWidth);
1538
+ const lines = content === "" ? [] : new Markdown3(content, 0, 0, getMarkdownTheme5()).render(boundedWidth);
1418
1539
  return keep([
1419
1540
  ...lines,
1420
1541
  ...result.errorMessage === void 0 ? [] : [getSelectListTheme7().noMatch(`Error: ${result.errorMessage}`)],
1421
1542
  ...result.toolResultDetails === void 0 ? [] : [
1422
1543
  getSelectListTheme7().description("Details"),
1423
- ...new Markdown3(jsonText(result.toolResultDetails), 0, 0, getMarkdownTheme6()).render(
1544
+ ...new Markdown3(jsonText(result.toolResultDetails), 0, 0, getMarkdownTheme5()).render(
1424
1545
  boundedWidth
1425
1546
  )
1426
1547
  ],
@@ -1467,7 +1588,7 @@ var renderMessage = (message, width) => {
1467
1588
  ] : [];
1468
1589
  return [
1469
1590
  ...attribution,
1470
- ...new UserMessageComponent2(markdown2, getMarkdownTheme7(), 0).render(width)
1591
+ ...new UserMessageComponent(markdown2, getMarkdownTheme6(), 0).render(width)
1471
1592
  ];
1472
1593
  }
1473
1594
  if (message.role === "assistant") {
@@ -1481,9 +1602,9 @@ var renderMessage = (message, width) => {
1481
1602
  const suffix = message.errorMessage === void 0 ? "" : `
1482
1603
 
1483
1604
  Error: ${message.errorMessage}`;
1484
- return new Markdown4(`${prefix}${markdown}${suffix}`, 0, 0, getMarkdownTheme7()).render(width);
1605
+ return new Markdown4(`${prefix}${markdown}${suffix}`, 0, 0, getMarkdownTheme6()).render(width);
1485
1606
  };
1486
- var renderMarkdown = (markdown, width) => new Markdown4(markdown, 0, 0, getMarkdownTheme7()).render(Math.max(1, width));
1607
+ var renderMarkdown = (markdown, width) => new Markdown4(markdown, 0, 0, getMarkdownTheme6()).render(Math.max(1, width));
1487
1608
  var firstStringArgument = (parameters, names) => {
1488
1609
  for (const name of names) {
1489
1610
  const value = parameters[name];
@@ -1532,7 +1653,7 @@ var exploredRead = (toolCall, path, result) => {
1532
1653
  ...errorMessage === void 0 ? {} : { errorMessage }
1533
1654
  };
1534
1655
  };
1535
- var renderTranscript = (transcript, width, expandedDetailBlockIds) => {
1656
+ var renderTranscript = (transcript, width, expandedDetailBlockIds, settledAsks) => {
1536
1657
  const results = /* @__PURE__ */ new Map();
1537
1658
  const callIds = /* @__PURE__ */ new Set();
1538
1659
  for (const message of transcript) {
@@ -1559,7 +1680,19 @@ var renderTranscript = (transcript, width, expandedDetailBlockIds) => {
1559
1680
  });
1560
1681
  group = [];
1561
1682
  };
1683
+ let nextAsk = 0;
1684
+ const flushAsksUntil = (timestamp) => {
1685
+ if (timestamp === void 0) return;
1686
+ while (nextAsk < settledAsks.length) {
1687
+ const pending = settledAsks[nextAsk];
1688
+ if (pending === void 0 || pending.at > timestamp) return;
1689
+ closeGroup();
1690
+ blocks.push(pending.block);
1691
+ nextAsk += 1;
1692
+ }
1693
+ };
1562
1694
  transcript.forEach((message, index) => {
1695
+ flushAsksUntil(message.timestamp);
1563
1696
  const messageId = message.id ?? String(index);
1564
1697
  if (message.role === "toolResult") {
1565
1698
  if (message.toolCallId !== void 0 && callIds.has(message.toolCallId)) return;
@@ -1606,21 +1739,26 @@ var renderTranscript = (transcript, width, expandedDetailBlockIds) => {
1606
1739
  }
1607
1740
  });
1608
1741
  closeGroup();
1742
+ for (const remaining of settledAsks.slice(nextAsk)) blocks.push(remaining.block);
1609
1743
  return blocks;
1610
1744
  };
1611
1745
  var campUiTranscriptBlockRenderer = {
1612
1746
  id: "transcript",
1613
1747
  order: 0,
1614
- render: ({ model, worker, width }) => renderTranscript(
1615
- worker.snapshot.session?.transcript ?? [],
1616
- width,
1617
- model.state.expandedDetailBlockIds
1618
- )
1748
+ render: (context) => {
1749
+ const { model, worker, width } = context;
1750
+ const settledAsks = worker.asks.filter((ask) => !isCampUiPendingAsk(ask)).map((ask) => ({ at: ask.openedAt, block: campUiAskDetailBlock(ask, model, width) })).sort((left, right) => left.at - right.at);
1751
+ return renderTranscript(
1752
+ worker.snapshot.session?.transcript ?? [],
1753
+ width,
1754
+ model.state.expandedDetailBlockIds,
1755
+ settledAsks
1756
+ );
1757
+ }
1619
1758
  };
1620
1759
 
1621
1760
  // packages/host-ui/src/pi/blocks/campUiDetail.ts
1622
1761
  var defaultCampUiDetailBlockRenderers = [
1623
- campUiEquipmentBlockRenderer,
1624
1762
  campUiTranscriptBlockRenderer,
1625
1763
  campUiPendingAskBlockRenderer
1626
1764
  ];
@@ -1759,12 +1897,261 @@ var makeCampUiDetailComponent = (context, registry = defaultCampUiDetailBlockRen
1759
1897
  };
1760
1898
  };
1761
1899
 
1762
- // packages/host-ui/src/pi/layout/campUiChrome.ts
1900
+ // packages/host-ui/src/pi/layout/campUiActivity.ts
1763
1901
  import { getSelectListTheme as getSelectListTheme9 } from "@earendil-works/pi-coding-agent";
1902
+ import { sliceByColumn, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
1903
+ var CampUiActivityAccess = {
1904
+ read: "read",
1905
+ edit: "edit",
1906
+ write: "write"
1907
+ };
1908
+ var accessByTool = /* @__PURE__ */ new Map([
1909
+ ["read", CampUiActivityAccess.read],
1910
+ ["edit", CampUiActivityAccess.edit],
1911
+ ["write", CampUiActivityAccess.write]
1912
+ ]);
1913
+ var accessRank = {
1914
+ read: 0,
1915
+ edit: 1,
1916
+ write: 2
1917
+ };
1918
+ var filePathArgument = (parameters) => {
1919
+ for (const name of ["path", "file_path"]) {
1920
+ const value = parameters[name];
1921
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
1922
+ }
1923
+ return void 0;
1924
+ };
1925
+ var splitPath2 = (path) => {
1926
+ const separator = path.lastIndexOf("/");
1927
+ if (separator < 0) return { directory: ".", name: path };
1928
+ if (separator === 0) return { directory: "/", name: path.slice(1) || path };
1929
+ return { directory: path.slice(0, separator), name: path.slice(separator + 1) || path };
1930
+ };
1931
+ var collectCampUiActivityTree = (transcript) => {
1932
+ const directories = /* @__PURE__ */ new Map();
1933
+ const nodes = /* @__PURE__ */ new Map();
1934
+ for (const message of transcript) {
1935
+ for (const content of message.content) {
1936
+ if (content.kind !== "tool-call") continue;
1937
+ const access = accessByTool.get(content.toolName);
1938
+ if (access === void 0) continue;
1939
+ const path = filePathArgument(content.arguments);
1940
+ if (path === void 0) continue;
1941
+ const existing = nodes.get(path);
1942
+ if (existing !== void 0) {
1943
+ existing.count += 1;
1944
+ if (accessRank[access] > accessRank[existing.access]) existing.access = access;
1945
+ continue;
1946
+ }
1947
+ const { directory, name } = splitPath2(path);
1948
+ const node = { path, directory, name, access, count: 1 };
1949
+ nodes.set(path, node);
1950
+ const group = directories.get(directory) ?? { path: directory, nodes: [] };
1951
+ if (!directories.has(directory)) directories.set(directory, group);
1952
+ group.nodes.push(node);
1953
+ }
1954
+ }
1955
+ return [...directories.values()].map((directory) => ({
1956
+ path: directory.path,
1957
+ nodes: directory.nodes.map((node) => ({ ...node }))
1958
+ }));
1959
+ };
1960
+ var CAMP_UI_ACTIVITY_MAX_VISIBLE_PATHS = 12;
1961
+ var ACTIVITY_COLUMNS_PER_VISIBLE_PATH = 4;
1962
+ var flattenedNodes = (tree) => tree.flatMap((directory) => directory.nodes);
1963
+ var renderedRowCount = (tree, selected, hidden) => {
1964
+ const directories = tree.filter(
1965
+ (directory) => directory.nodes.some((node) => selected.has(node))
1966
+ ).length;
1967
+ return selected.size + directories + (hidden > 0 ? 1 : 0);
1968
+ };
1969
+ var selectVisibleNodes2 = (tree, width, height) => {
1970
+ const nodes = flattenedNodes(tree);
1971
+ const selected = /* @__PURE__ */ new Set();
1972
+ const widthBudget = Math.max(
1973
+ 1,
1974
+ Math.floor(Math.max(0, width) / ACTIVITY_COLUMNS_PER_VISIBLE_PATH)
1975
+ );
1976
+ const nodeBudget = Math.min(CAMP_UI_ACTIVITY_MAX_VISIBLE_PATHS, widthBudget);
1977
+ const mutations = nodes.filter((node) => node.access !== CampUiActivityAccess.read);
1978
+ const reads = nodes.filter((node) => node.access === CampUiActivityAccess.read);
1979
+ const addVisible = (node) => {
1980
+ if (selected.size >= nodeBudget) return;
1981
+ const candidate = new Set(selected).add(node);
1982
+ const hidden = nodes.length - candidate.size;
1983
+ if (renderedRowCount(tree, candidate, hidden) <= Math.max(0, height)) selected.add(node);
1984
+ };
1985
+ for (const node of mutations) addVisible(node);
1986
+ if (mutations.some((node) => !selected.has(node))) return selected;
1987
+ for (const node of reads) addVisible(node);
1988
+ return selected;
1989
+ };
1990
+ var shortenTail = (value, width) => {
1991
+ const available = Math.max(0, Math.floor(width));
1992
+ const valueWidth = visibleWidth3(value);
1993
+ if (valueWidth <= available) return value;
1994
+ if (available <= 0) return "";
1995
+ if (available === 1) return "\u2026";
1996
+ return `\u2026${sliceByColumn(value, valueWidth - available + 1, available - 1, true)}`;
1997
+ };
1998
+ var accessPresentation = {
1999
+ read: { label: "read", glyph: "r", tone: CampUiTone.neutral },
2000
+ edit: { label: "edited", glyph: "~", tone: CampUiTone.attention },
2001
+ write: { label: "created", glyph: "+", tone: CampUiTone.success }
2002
+ };
2003
+ var nodeLine2 = (node, last, width) => {
2004
+ const theme = getSelectListTheme9();
2005
+ const presentation = accessPresentation[node.access];
2006
+ const count = node.count > 1 ? ` \xD7${String(node.count)}` : "";
2007
+ const suffix = ` \xB7 ${presentation.label}${count}`;
2008
+ const branch = last ? "\u2514\u2500" : "\u251C\u2500";
2009
+ const nameWidth = Math.max(1, width - visibleWidth3(`${branch} ${presentation.glyph} ${suffix}`));
2010
+ const name = shortenTail(node.name, nameWidth);
2011
+ return fitCampUiLine(
2012
+ `${theme.description(branch)} ${renderCampUiStatus(name, presentation.tone, presentation.glyph)}${theme.description(suffix)}`,
2013
+ width
2014
+ );
2015
+ };
2016
+ var hiddenSummary = (nodes) => {
2017
+ const parts = Object.values(CampUiActivityAccess).flatMap((access) => {
2018
+ const count = nodes.filter((node) => node.access === access).length;
2019
+ if (count === 0) return [];
2020
+ const label = accessPresentation[access].label;
2021
+ return [`${String(count)} ${label} ${count === 1 ? "path" : "paths"}`];
2022
+ });
2023
+ return `... ${parts.join(" \xB7 ")} hidden`;
2024
+ };
2025
+ var renderCampUiActivityTree = (transcript, width, height) => {
2026
+ const theme = getSelectListTheme9();
2027
+ const tree = collectCampUiActivityTree(transcript);
2028
+ const nodes = flattenedNodes(tree);
2029
+ if (height <= 0) return [];
2030
+ if (nodes.length === 0) return [fitCampUiLine(theme.description("no file activity"), width)];
2031
+ const selected = selectVisibleNodes2(tree, width, height);
2032
+ const lines = [];
2033
+ for (const directory of tree) {
2034
+ const visible = directory.nodes.filter((node) => selected.has(node));
2035
+ if (visible.length === 0) continue;
2036
+ lines.push(fitCampUiLine(theme.description(shortenTail(directory.path, width)), width));
2037
+ visible.forEach((node, index) => {
2038
+ lines.push(nodeLine2(node, index === visible.length - 1, width));
2039
+ });
2040
+ }
2041
+ const hidden = nodes.filter((node) => !selected.has(node));
2042
+ if (hidden.length > 0) {
2043
+ lines.push(fitCampUiLine(theme.description(hiddenSummary(hidden)), width));
2044
+ }
2045
+ return lines;
2046
+ };
2047
+ var makeCampUiActivity = (context) => ({
2048
+ render: (viewport) => {
2049
+ const model = context.readModel();
2050
+ const workerId = model.state.selectedWorkerId;
2051
+ const worker = workerId === void 0 ? void 0 : findCampUiWorker(model.snapshot, workerId);
2052
+ return renderCampUiActivityTree(
2053
+ worker?.snapshot.session?.transcript ?? [],
2054
+ viewport.width,
2055
+ viewport.height
2056
+ );
2057
+ }
2058
+ });
2059
+
2060
+ // packages/host-ui/src/pi/layout/campUiChrome.ts
2061
+ import { getSelectListTheme as getSelectListTheme10 } from "@earendil-works/pi-coding-agent";
2062
+ import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
2063
+
2064
+ // packages/host-ui/src/pi/shared/campUiPulse.ts
2065
+ var BRAILLE_BASE = 10240;
2066
+ var DOT_BITS = [
2067
+ [1, 2, 4, 64],
2068
+ [8, 16, 32, 128]
2069
+ ];
2070
+ var ROWS_PER_CELL = 4;
2071
+ var LINES = 2;
2072
+ var LEVELS = ROWS_PER_CELL * LINES;
2073
+ var SAMPLES_PER_COLUMN = 2;
2074
+ var EMPTY_CELL = String.fromCharCode(BRAILLE_BASE);
2075
+ var emptyPulse = (columns) => {
2076
+ const blank = EMPTY_CELL.repeat(Math.max(0, columns));
2077
+ return [blank, blank];
2078
+ };
2079
+ var renderCampUiPulse = (samples, options) => {
2080
+ const columns = Math.max(0, Math.floor(options.columns));
2081
+ if (columns === 0 || samples.length === 0) return emptyPulse(columns);
2082
+ const window = samples.slice(-columns * SAMPLES_PER_COLUMN);
2083
+ const peak = options.ceiling ?? Math.max(...window);
2084
+ const scale = peak > 0 ? peak : 1;
2085
+ const cells = [new Array(columns).fill(0), new Array(columns).fill(0)];
2086
+ const offset = columns * SAMPLES_PER_COLUMN - window.length;
2087
+ window.forEach((value, index) => {
2088
+ const position = offset + index;
2089
+ const column = Math.floor(position / SAMPLES_PER_COLUMN);
2090
+ const half = position % SAMPLES_PER_COLUMN;
2091
+ const height = Math.max(0, Math.min(LEVELS - 1, Math.round(value / scale * (LEVELS - 1))));
2092
+ const row = LEVELS - 1 - height;
2093
+ const line = Math.floor(row / ROWS_PER_CELL);
2094
+ const bits = DOT_BITS[half]?.[row % ROWS_PER_CELL];
2095
+ const target = cells[line];
2096
+ if (bits === void 0 || target === void 0) return;
2097
+ target[column] = (target[column] ?? 0) | bits;
2098
+ });
2099
+ const [upper, lower] = cells.map(
2100
+ (row) => row.map((bits) => String.fromCharCode(BRAILLE_BASE + bits)).join("")
2101
+ );
2102
+ return [upper ?? "", lower ?? ""];
2103
+ };
2104
+
2105
+ // packages/host-ui/src/pi/shared/campUiTone.ts
2106
+ var CampUiLevel = {
2107
+ /** An ordinary card: present, but not asking to be read. */
2108
+ resting: "resting",
2109
+ /** The cursor is on this card; its session is not open. */
2110
+ focused: "focused",
2111
+ /** The open card, whose session fills the centre. */
2112
+ selected: "selected"
2113
+ };
2114
+ var roleColors = {
2115
+ foreman: { r: 122, g: 192, b: 140 },
2116
+ wright: { r: 196, g: 96, b: 108 },
2117
+ assayer: { r: 214, g: 168, b: 96 },
2118
+ prospector: { r: 120, g: 178, b: 214 }
2119
+ };
2120
+ var UNKNOWN_ROLE = { r: 178, g: 178, b: 190 };
2121
+ var CampUiInk = {
2122
+ primary: { r: 216, g: 216, b: 228 },
2123
+ secondary: { r: 182, g: 182, b: 196 },
2124
+ muted: { r: 154, g: 154, b: 168 }
2125
+ };
2126
+ var levelSteps = {
2127
+ [CampUiLevel.selected]: { saturation: 1, brightness: 1 },
2128
+ [CampUiLevel.focused]: { saturation: 0.72, brightness: 0.96 },
2129
+ [CampUiLevel.resting]: { saturation: 0.45, brightness: 0.9 }
2130
+ };
2131
+ var luminance = ({ r, g, b }) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
2132
+ var FOREGROUND_RESET = "\x1B[39m";
2133
+ var step = (color, level) => {
2134
+ const { saturation, brightness } = levelSteps[level];
2135
+ const grey = luminance(color);
2136
+ const blend = (channel) => Math.max(0, Math.min(255, Math.round((grey + (channel - grey) * saturation) * brightness)));
2137
+ return { r: blend(color.r), g: blend(color.g), b: blend(color.b) };
2138
+ };
2139
+ var foreground = (color) => `\x1B[38;2;${String(color.r)};${String(color.g)};${String(color.b)}m`;
2140
+ var campUiRoleColor = (role) => role === void 0 ? UNKNOWN_ROLE : roleColors[role] ?? UNKNOWN_ROLE;
2141
+ var campUiText = (text, color, level) => `${foreground(step(color, level))}${text}${FOREGROUND_RESET}`;
2142
+ var campUiPulseText = (pulse, level, roleColor) => campUiText(pulse, level === CampUiLevel.selected ? roleColor : CampUiInk.secondary, level);
2143
+ var levelMarkers = {
2144
+ [CampUiLevel.selected]: "\u25CF",
2145
+ [CampUiLevel.focused]: "\u25CB",
2146
+ [CampUiLevel.resting]: " "
2147
+ };
2148
+ var campUiLevelMarkerText = (level, color) => level === CampUiLevel.resting ? levelMarkers[level] : campUiText(levelMarkers[level], color, level);
2149
+
2150
+ // packages/host-ui/src/pi/layout/campUiChrome.ts
1764
2151
  var makeCampUiCampHeader = (context) => ({
1765
2152
  render: (viewport) => {
1766
2153
  const snapshot = context.readModel().snapshot;
1767
- const theme = getSelectListTheme9();
2154
+ const theme = getSelectListTheme10();
1768
2155
  const mode = snapshot.placement === "external" ? "agentless" : "foreman";
1769
2156
  return [
1770
2157
  fitCampUiLine(theme.description(snapshot.displayName), viewport.width),
@@ -1775,55 +2162,158 @@ var makeCampUiCampHeader = (context) => ({
1775
2162
  ];
1776
2163
  }
1777
2164
  });
1778
- var makeCampUiSeat = (context) => ({
1779
- render: (viewport) => {
1780
- const snapshot = context.readModel().snapshot;
1781
- const theme = getSelectListTheme9();
1782
- const external = snapshot.placement === "external";
1783
- const controller = snapshot.controllerConnected ? renderCampUiStatus("live", CampUiTone.success, CampUiGlyph.live) : renderCampUiStatus("lost", CampUiTone.error);
1784
- return [
1785
- fitCampUiLine(
1786
- external ? `${renderCampUiStatus("controller", CampUiTone.external)} ${theme.description("\xB7 external \xB7")} ${controller}` : `${renderCampUiStatus("foreman", CampUiTone.neutral)} ${theme.description("\xB7 session not reported \xB7")} ${controller}`,
1787
- viewport.width
1788
- )
1789
- ];
2165
+ var PULSE_WINDOW_MS = 12e4;
2166
+ var PULSE_CEILING = 4;
2167
+ var PULSE_SAMPLES_PER_COLUMN = 2;
2168
+ var rosterWidths = (width) => {
2169
+ const right = Math.min(17, Math.max(0, width - 17));
2170
+ const left = Math.min(11, Math.max(0, width - right - 6));
2171
+ return { left, pulse: Math.max(0, width - left - right - 4), right };
2172
+ };
2173
+ var rightColumn = (text, width) => {
2174
+ const fitted = truncateToWidth4(text, width);
2175
+ return `${" ".repeat(Math.max(0, width - visibleWidth4(fitted)))}${fitted}`;
2176
+ };
2177
+ var pulseSamples = (timestamps, sampleCount, now) => {
2178
+ if (sampleCount <= 0) return [];
2179
+ const samples = new Array(sampleCount).fill(0);
2180
+ const windowStart = now - PULSE_WINDOW_MS;
2181
+ const interval = PULSE_WINDOW_MS / sampleCount;
2182
+ for (const timestamp of timestamps) {
2183
+ if (timestamp < windowStart || timestamp > now) continue;
2184
+ const index = Math.min(sampleCount - 1, Math.floor((timestamp - windowStart) / interval));
2185
+ samples[index] = (samples[index] ?? 0) + 1;
1790
2186
  }
1791
- });
2187
+ return samples;
2188
+ };
2189
+ var elapsedText = (updatedAt, now) => {
2190
+ const seconds = Math.floor(Math.max(0, now - updatedAt) / 1e3);
2191
+ if (seconds < 60) return `${String(seconds)}s`;
2192
+ const minutes = Math.floor(seconds / 60);
2193
+ if (minutes < 60) return `${String(minutes)}m`;
2194
+ const hours = Math.floor(minutes / 60);
2195
+ if (hours < 24) return `${String(hours)}h`;
2196
+ return `${String(Math.floor(hours / 24))}d`;
2197
+ };
2198
+ var percentText = (percent) => percent === null || percent === void 0 ? "\u2014" : `${percent.toFixed(0)}%`;
2199
+ var usageText = (entry, width) => {
2200
+ const usage = entry.snapshot.session?.usage;
2201
+ const context = percentText(usage?.context?.percent);
2202
+ const cache = usage?.latestCacheHitRate === void 0 ? usage === void 0 || usage.tokens.total === 0 ? "n/a" : "n/r" : percentText(usage.latestCacheHitRate);
2203
+ const cost = usage === void 0 ? "\u2014" : `$${usage.cost.toFixed(3)}`;
2204
+ const full = `${context} ${cache} ${cost}`;
2205
+ if (visibleWidth4(full) <= width || usage === void 0) return full;
2206
+ const compactCost = `$${usage.cost.toFixed(2).replace(/^0/u, "")}`;
2207
+ return `${context}${cache}${compactCost}`;
2208
+ };
2209
+ var stateText = (state, duration, width) => {
2210
+ const full = `${state} ${duration}`;
2211
+ return visibleWidth4(full) <= width ? full : state;
2212
+ };
2213
+ var modelText = (entry) => {
2214
+ const model = entry.snapshot.session?.usage?.model ?? entry.snapshot.worker.equipment.model;
2215
+ if (model === void 0) return "model unknown";
2216
+ return model.split("/").at(-1) ?? model;
2217
+ };
2218
+ var cardLine = (marker, left, pulse, right, widths, width) => fitCampUiLine(
2219
+ `${marker} ${fitCampUiLine(left, widths.left)} ${fitCampUiLine(pulse, widths.pulse)} ${rightColumn(right, widths.right)}`,
2220
+ width
2221
+ );
2222
+ var renderExternalControllerCard = (controllerConnected, width) => {
2223
+ const level = CampUiLevel.resting;
2224
+ const color = campUiRoleColor("foreman");
2225
+ const widths = rosterWidths(width);
2226
+ const neutral = (text) => campUiText(text, CampUiInk.secondary, level);
2227
+ return [
2228
+ cardLine(
2229
+ campUiLevelMarkerText(level, color),
2230
+ campUiText("controller", color, level),
2231
+ "",
2232
+ neutral("ext"),
2233
+ widths,
2234
+ width
2235
+ ),
2236
+ cardLine(
2237
+ " ",
2238
+ "",
2239
+ "",
2240
+ neutral(controllerConnected ? "lease live" : "lease lost"),
2241
+ widths,
2242
+ width
2243
+ )
2244
+ ];
2245
+ };
2246
+ var renderWorkerCard = (entry, selectedWorkerId, detailVisible, now, width) => {
2247
+ const worker = entry.snapshot.worker;
2248
+ const session = entry.snapshot.session;
2249
+ const level = worker.id !== selectedWorkerId ? CampUiLevel.resting : detailVisible ? CampUiLevel.selected : CampUiLevel.focused;
2250
+ const roleColor = campUiRoleColor(worker.equipment.role);
2251
+ const widths = rosterWidths(width);
2252
+ const timestamps = (session?.transcript ?? []).flatMap(
2253
+ (message) => message.timestamp === void 0 ? [] : [message.timestamp]
2254
+ );
2255
+ const pulse = renderCampUiPulse(
2256
+ pulseSamples(timestamps, widths.pulse * PULSE_SAMPLES_PER_COLUMN, now),
2257
+ { columns: widths.pulse, ceiling: PULSE_CEILING }
2258
+ );
2259
+ const pendingAsks = entry.asks.filter((ask) => ask.state === "pending").length;
2260
+ const state = campUiWorkerStateLabel(worker.state, {
2261
+ failed: entry.error !== void 0,
2262
+ pendingAsks,
2263
+ streaming: session?.isStreaming === true
2264
+ });
2265
+ const neutral = (text) => campUiText(text, CampUiInk.secondary, level);
2266
+ const name = worker.alias ?? worker.id.slice(0, 8);
2267
+ return [
2268
+ cardLine(
2269
+ campUiLevelMarkerText(level, roleColor),
2270
+ campUiText(name, roleColor, level),
2271
+ campUiPulseText(pulse[0], level, roleColor),
2272
+ neutral(modelText(entry)),
2273
+ widths,
2274
+ width
2275
+ ),
2276
+ cardLine(
2277
+ " ",
2278
+ neutral(stateText(state, elapsedText(worker.updatedAt, now), widths.left)),
2279
+ campUiPulseText(pulse[1], level, roleColor),
2280
+ neutral(usageText(entry, widths.right)),
2281
+ widths,
2282
+ width
2283
+ )
2284
+ ];
2285
+ };
1792
2286
  var makeCampUiRoster = (context) => ({
1793
2287
  focusable: true,
1794
2288
  render: (viewport) => {
1795
2289
  const model = context.readModel();
1796
- const theme = getSelectListTheme9();
1797
- const focused = model.state.focusedSlotId === CampUiSlot.roster;
1798
- if (model.snapshot.workers.length === 0) {
1799
- return [theme.description("No workers")];
2290
+ const external = model.snapshot.placement === CampUiPlacement.external;
2291
+ if (model.snapshot.workers.length === 0 && !external) {
2292
+ return [getSelectListTheme10().description("No workers")];
1800
2293
  }
1801
- const workers = model.snapshot.workers.map((entry) => {
1802
- const worker = entry.snapshot.worker;
1803
- const selected = worker.id === model.state.selectedWorkerId;
1804
- const name = worker.alias ?? worker.id.slice(0, 8);
1805
- const session = entry.snapshot.session;
1806
- const active = uniqueCampUiNames([
1807
- ...session?.activeToolNames ?? [],
1808
- ...session?.pendingToolNames ?? []
1809
- ]);
1810
- const pendingAsks = entry.asks.filter((ask) => ask.state === "pending").length;
1811
- const tool = active.length === 0 ? "" : styleCampUiTone(CampUiTone.active, ` ${active.slice(0, 2).join(", ")}`);
1812
- const label = selected ? theme.selectedText(name) : name;
1813
- const role = theme.description(`(${worker.equipment.role ?? "unknown"})`);
1814
- const status = renderCampUiWorkerState(worker.state, {
1815
- failed: entry.error !== void 0,
1816
- pendingAsks,
1817
- streaming: session?.isStreaming === true
1818
- });
1819
- const line = `${status} ${label} ${role}${tool}`;
1820
- return selected && focused ? renderSelectedCampUiRow(line, viewport.width) : fitCampUiLine(line, viewport.width);
2294
+ const controller = external ? renderExternalControllerCard(model.snapshot.controllerConnected, viewport.width) : [];
2295
+ const workers = model.snapshot.workers.flatMap((entry) => {
2296
+ const candidateNow = model.state.presentationNow;
2297
+ const now = Number.isFinite(candidateNow) ? candidateNow : entry.snapshot.worker.updatedAt;
2298
+ return renderWorkerCard(
2299
+ entry,
2300
+ model.state.selectedWorkerId,
2301
+ model.state.detailVisible,
2302
+ now,
2303
+ viewport.width
2304
+ );
1821
2305
  });
1822
- return workers;
2306
+ return [...controller, ...workers];
1823
2307
  },
1824
2308
  handlePointer: (pointer) => {
1825
2309
  if (pointer.action !== "press" || pointer.button !== "left") return [];
1826
- const workerId = context.readModel().snapshot.workers[pointer.y]?.snapshot.worker.id;
2310
+ const model = context.readModel();
2311
+ const controllerRows = model.snapshot.placement === CampUiPlacement.external ? 2 : 0;
2312
+ if (pointer.y < controllerRows) {
2313
+ return [CampUiIntents.FocusSlot({ slotId: CampUiSlot.roster })];
2314
+ }
2315
+ const workerIndex = Math.floor((pointer.y - controllerRows) / 2);
2316
+ const workerId = model.snapshot.workers[workerIndex]?.snapshot.worker.id;
1827
2317
  if (workerId === void 0) return [];
1828
2318
  return [
1829
2319
  CampUiIntents.FocusSlot({ slotId: CampUiSlot.roster }),
@@ -1834,7 +2324,7 @@ var makeCampUiRoster = (context) => ({
1834
2324
  var makeCampUiSessionHeader = (context) => ({
1835
2325
  render: (viewport) => {
1836
2326
  const model = context.readModel();
1837
- const theme = getSelectListTheme9();
2327
+ const theme = getSelectListTheme10();
1838
2328
  const workerId = model.state.selectedWorkerId;
1839
2329
  if (workerId === void 0) {
1840
2330
  return [fitCampUiLine(theme.description("Select a worker"), viewport.width)];
@@ -1855,29 +2345,63 @@ var makeCampUiSessionHeader = (context) => ({
1855
2345
  ] : []
1856
2346
  });
1857
2347
 
1858
- // packages/host-ui/src/pi/layout/campUiFooter.ts
1859
- import { getMarkdownTheme as getMarkdownTheme8, getSelectListTheme as getSelectListTheme10 } from "@earendil-works/pi-coding-agent";
1860
- var aggregateCampUsage = (workers) => {
1861
- const usages = workers.flatMap(
1862
- (worker) => worker.snapshot.session?.usage === void 0 ? [] : [worker.snapshot.session.usage]
2348
+ // packages/host-ui/src/pi/layout/campUiEquipment.ts
2349
+ import { getSelectListTheme as getSelectListTheme11 } from "@earendil-works/pi-coding-agent";
2350
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
2351
+ var fullEquipmentLines = (equipment, revision, width) => {
2352
+ if (width <= 0) return [];
2353
+ const theme = getSelectListTheme11();
2354
+ return equipmentDescriptionLines(equipment, revision).flatMap(
2355
+ (line) => wrapTextWithAnsi(theme.description(line), width).map(
2356
+ (wrapped) => fitCampUiLine(wrapped, width)
2357
+ )
2358
+ );
2359
+ };
2360
+ var renderCampUiEquipment = (equipment, revision, width, height) => {
2361
+ const lines = fullEquipmentLines(equipment, revision, width);
2362
+ const available = Math.max(0, Math.floor(height));
2363
+ if (available === 0 || lines.length === 0) return [];
2364
+ if (lines.length <= available) return lines;
2365
+ const shown = lines.slice(0, Math.max(0, available - 1));
2366
+ const hidden = lines.length - shown.length;
2367
+ const summary = getSelectListTheme11().description(
2368
+ `... ${String(hidden)} equipment ${hidden === 1 ? "line" : "lines"} hidden`
1863
2369
  );
2370
+ return [...shown, fitCampUiLine(summary, width)];
2371
+ };
2372
+ var makeCampUiEquipment = (context) => {
2373
+ const selectedWorker = () => {
2374
+ const model = context.readModel();
2375
+ const workerId = model.state.selectedWorkerId;
2376
+ return workerId === void 0 ? void 0 : findCampUiWorker(model.snapshot, workerId);
2377
+ };
2378
+ const fullLines = (width) => {
2379
+ const worker = selectedWorker();
2380
+ return worker === void 0 ? [fitCampUiLine(getSelectListTheme11().description("no worker selected"), width)] : fullEquipmentLines(
2381
+ worker.snapshot.worker.equipment,
2382
+ worker.snapshot.worker.equipmentVersion,
2383
+ width
2384
+ );
2385
+ };
1864
2386
  return {
1865
- cost: usages.reduce((total, usage) => total + usage.cost, 0),
1866
- reportedWorkers: usages.length,
1867
- tokens: usages.reduce((total, usage) => total + usage.tokens.total, 0),
1868
- totalWorkers: workers.length
2387
+ preferredRows: (width) => fullLines(width).length,
2388
+ render: (viewport) => {
2389
+ const worker = selectedWorker();
2390
+ if (worker === void 0) return fullLines(viewport.width).slice(0, viewport.height);
2391
+ return renderCampUiEquipment(
2392
+ worker.snapshot.worker.equipment,
2393
+ worker.snapshot.worker.equipmentVersion,
2394
+ viewport.width,
2395
+ viewport.height
2396
+ );
2397
+ }
1869
2398
  };
1870
2399
  };
1871
- var aggregateUsageText = (usage) => {
1872
- const partial = usage.reportedWorkers < usage.totalWorkers;
1873
- const coverage = partial ? ` \xB7 ${usage.reportedWorkers}/${usage.totalWorkers} reported` : "";
1874
- if (usage.totalWorkers > 0 && usage.reportedWorkers === 0) {
1875
- return `tokens unknown \xB7 cost unknown${coverage}`;
1876
- }
1877
- const lowerBound = partial ? "\u2265" : "";
1878
- return `\u2193 ${lowerBound}${compactNumber(usage.tokens)} tokens \xB7 ${lowerBound}$${usage.cost.toFixed(3)}${coverage}`;
1879
- };
1880
- var footerDivider = (width) => fitCampUiLine(getMarkdownTheme8().codeBlockBorder("\u2500".repeat(width)), width);
2400
+
2401
+ // packages/host-ui/src/pi/layout/campUiFooter.ts
2402
+ import { getMarkdownTheme as getMarkdownTheme7, getSelectListTheme as getSelectListTheme12 } from "@earendil-works/pi-coding-agent";
2403
+ import { sliceByColumn as sliceByColumn2, visibleWidth as visibleWidth5 } from "@earendil-works/pi-tui";
2404
+ var footerDivider = (width) => fitCampUiLine(getMarkdownTheme7().codeBlockBorder("\u2500".repeat(width)), width);
1881
2405
  var retryText = (retryAt, attempt, now) => {
1882
2406
  if (retryAt === void 0) return attempt === void 0 ? "" : `attempt ${String(attempt)}`;
1883
2407
  const seconds = Math.max(0, Math.ceil((retryAt - now) / 1e3));
@@ -1888,56 +2412,73 @@ var diagnosticText = (value) => {
1888
2412
  const normalized = value?.replaceAll(/\s+/gu, " ").trim();
1889
2413
  return normalized === "" ? void 0 : normalized;
1890
2414
  };
1891
- var makeCampUiCampFooter = (context) => ({
2415
+ var workerCountText = (workers) => {
2416
+ if (workers.length === 0) return "no workers";
2417
+ const working = workers.filter(
2418
+ (worker) => worker.snapshot.worker.state === CampWorkerState.running
2419
+ ).length;
2420
+ const waiting = workers.filter((worker) => worker.asks.some(isPendingAsk)).length;
2421
+ const parts = [
2422
+ `${String(workers.length)} ${workers.length === 1 ? "worker" : "workers"}`,
2423
+ ...working === 0 ? [] : [`${String(working)} working`],
2424
+ ...waiting === 0 ? [] : [`${String(waiting)} waiting`]
2425
+ ];
2426
+ return parts.join(" \xB7 ");
2427
+ };
2428
+ var isPendingAsk = (ask) => ask.state === "pending";
2429
+ var shortenPath = (value, width) => {
2430
+ const available = Math.max(0, Math.floor(width));
2431
+ if (available <= 0) return "";
2432
+ if (visibleWidth5(value) <= available) return value;
2433
+ if (available === 1) return "\u2026";
2434
+ const kept = available - 1;
2435
+ return `\u2026${sliceByColumn2(value, visibleWidth5(value) - kept, kept, true)}`;
2436
+ };
2437
+ var makeCampUiFooter = (context) => ({
1892
2438
  render: (viewport) => {
1893
2439
  const model = context.readModel();
1894
- const theme = getSelectListTheme10();
1895
- const health = model.snapshot.health;
1896
- const usage = aggregateUsageText(aggregateCampUsage(model.snapshot.workers));
2440
+ const theme = getSelectListTheme12();
2441
+ const snapshot = model.snapshot;
2442
+ const health = snapshot.health;
1897
2443
  const actionError = diagnosticText(model.state.lastActionError);
1898
2444
  const healthSummary = diagnosticText(health.message);
1899
2445
  const healthMessage = health.code === void 0 ? healthSummary : healthSummary === void 0 ? health.code : `${health.code} \xB7 ${healthSummary}`;
1900
2446
  const healthTone = health.status === CampUiHealthStatus.failed ? CampUiTone.error : CampUiTone.attention;
1901
- const diagnostic = actionError !== void 0 ? `${renderCampUiStatus("action error", CampUiTone.error)} ${theme.description(`\xB7 ${actionError}`)}` : healthMessage !== void 0 ? styleCampUiTone(healthTone, healthMessage) : health.status === CampUiHealthStatus.ready ? "" : renderCampUiStatus(health.status, healthTone);
1902
2447
  const retry = retryText(health.retryAt, health.attempt, model.state.presentationNow);
1903
- return [
1904
- footerDivider(viewport.width),
1905
- joinCampUiColumns(diagnostic, theme.description(usage), viewport.width),
1906
- fitCampUiLine(retry === "" ? "" : theme.description(retry), viewport.width)
2448
+ const diagnostic = actionError !== void 0 ? `${renderCampUiStatus("action error", CampUiTone.error)} ${theme.description(`\xB7 ${actionError}`)}` : healthMessage !== void 0 ? styleCampUiTone(healthTone, healthMessage) : health.status === CampUiHealthStatus.ready ? "" : renderCampUiStatus(health.status, healthTone);
2449
+ const branch = snapshot.projectBranch === null ? "" : ` \xB7 ${snapshot.projectBranch}`;
2450
+ const version = `v${snapshot.hostVersion}`;
2451
+ const candidates = [
2452
+ { right: [workerCountText(snapshot.workers), version, retry], branch },
2453
+ { right: [workerCountText(snapshot.workers), version], branch },
2454
+ { right: [version], branch },
2455
+ { right: [version], branch: "" }
1907
2456
  ];
1908
- }
1909
- });
1910
- var makeCampUiSessionFooter = (context) => ({
1911
- render: (viewport) => {
1912
- const model = context.readModel();
1913
- const theme = getSelectListTheme10();
1914
- const workerId = model.state.selectedWorkerId;
1915
- const worker = workerId === void 0 ? void 0 : findCampUiWorker(model.snapshot, workerId);
1916
- const usage = worker?.snapshot.session?.usage;
1917
- const tokenText = usage === void 0 ? "tokens unknown" : `\u2193 ${compactNumber(usage.tokens.total)} tokens`;
1918
- const costText = usage === void 0 ? "cost unknown" : `$${usage.cost.toFixed(3)}`;
1919
- const cacheText = usage?.latestCacheHitRate === void 0 ? usage === void 0 || usage.tokens.total === 0 ? "cache n/a" : "cache not reported" : `cache ${usage.latestCacheHitRate.toFixed(0)}%`;
1920
- const contextText = usage?.context?.percent === null || usage?.context?.percent === void 0 ? "context unknown" : `context ${usage.context.percent.toFixed(0)}%`;
1921
- const modelText = usage?.model ?? worker?.snapshot.worker.equipment.model ?? "model unknown";
1922
- const thinking = worker?.snapshot.session?.thinkingLevel ?? worker?.snapshot.worker.equipment.thinkingLevel ?? "unknown";
1923
- const identity = worker === void 0 ? theme.description("worker not selected") : `${theme.selectedText(worker.snapshot.worker.alias ?? worker.snapshot.worker.id.slice(0, 8))} ${theme.description(`\xB7 ${worker.snapshot.worker.cwd}`)}`;
1924
- const branch = worker === void 0 || model.snapshot.projectBranch === null ? "" : theme.description(`branch ${model.snapshot.projectBranch}`);
2457
+ const MINIMUM_PATH_COLUMNS = 8;
2458
+ const chosen = candidates.find((candidate) => {
2459
+ const rightWidth = visibleWidth5(candidate.right.filter((part) => part !== "").join(" \xB7 "));
2460
+ const room = viewport.width - rightWidth - 3 - visibleWidth5(candidate.branch);
2461
+ return room >= MINIMUM_PATH_COLUMNS;
2462
+ }) ?? candidates[candidates.length - 1];
2463
+ const right = (chosen ?? candidates[0])?.right.filter((part) => part !== "").join(" \xB7 ") ?? "";
2464
+ const chosenBranch = chosen?.branch ?? "";
2465
+ const leftBudget = Math.max(0, viewport.width - visibleWidth5(right) - 3);
2466
+ const path = shortenPath(
2467
+ snapshot.projectScope,
2468
+ Math.max(0, leftBudget - visibleWidth5(chosenBranch))
2469
+ );
2470
+ const left = diagnostic === "" ? theme.description(`${path}${chosenBranch}`) : `${diagnostic} ${theme.description(`\xB7 ${path}${chosenBranch}`)}`;
1925
2471
  return [
1926
2472
  footerDivider(viewport.width),
1927
- branch === "" ? fitCampUiLine(identity, viewport.width) : joinCampUiColumns(identity, branch, viewport.width),
1928
- joinCampUiColumns(
1929
- theme.description(`${tokenText} \xB7 ${costText} \xB7 ${cacheText} \xB7 ${contextText}`),
1930
- theme.description(`${modelText} \xB7 effort ${thinking}`),
1931
- viewport.width
1932
- )
2473
+ joinCampUiColumns(left, theme.description(right), viewport.width)
1933
2474
  ];
1934
2475
  }
1935
2476
  });
1936
2477
 
1937
2478
  // packages/host-ui/src/pi/layout/campUiResources.ts
1938
- import { getSelectListTheme as getSelectListTheme11 } from "@earendil-works/pi-coding-agent";
2479
+ import { getSelectListTheme as getSelectListTheme13 } from "@earendil-works/pi-coding-agent";
1939
2480
  var renderResourceTree = (label, value, tone, width) => {
1940
- const theme = getSelectListTheme11();
2481
+ const theme = getSelectListTheme13();
1941
2482
  const lines = [
1942
2483
  theme.description(label),
1943
2484
  `${theme.description("\u2514\u2500")} ${renderCampUiStatus(value, tone)}`
@@ -1945,6 +2486,7 @@ var renderResourceTree = (label, value, tone, width) => {
1945
2486
  return lines.map((line) => fitCampUiLine(line, width));
1946
2487
  };
1947
2488
  var makeCampUiResources = (context) => ({
2489
+ preferredRows: () => 10,
1948
2490
  render: (viewport) => {
1949
2491
  const model = context.readModel();
1950
2492
  const snapshot = model.snapshot;
@@ -1978,27 +2520,29 @@ var makeCampUiResources = (context) => ({
1978
2520
  ...missingMcpHealth ? ["health not reported"] : []
1979
2521
  ];
1980
2522
  const mcpText = !equipmentReported ? "not reported" : mcpNames.length === 0 ? "none configured" : `${mcpNames.join(", ")}${mcpNotes.length === 0 ? "" : ` \xB7 ${mcpNotes.join(", ")}`}`;
1981
- return [
2523
+ const contentWidth = Math.max(1, viewport.width - 2);
2524
+ const body = [
1982
2525
  ...renderResourceTree(
1983
2526
  "Context",
1984
2527
  resourceText(inventory.contextFiles),
1985
2528
  inventory.contextFiles.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1986
- viewport.width
2529
+ contentWidth
1987
2530
  ),
1988
2531
  ...renderResourceTree(
1989
2532
  "Extensions",
1990
2533
  resourceText(inventory.extensions),
1991
2534
  inventory.extensions.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1992
- viewport.width
2535
+ contentWidth
1993
2536
  ),
1994
2537
  ...renderResourceTree(
1995
2538
  "Skills",
1996
2539
  resourceText(inventory.skills),
1997
2540
  inventory.skills.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1998
- viewport.width
2541
+ contentWidth
1999
2542
  ),
2000
- ...renderResourceTree("MCP", mcpText, mcpTone, viewport.width)
2543
+ ...renderResourceTree("MCP", mcpText, mcpTone, contentWidth)
2001
2544
  ];
2545
+ return renderCampUiPanel("Camp inventory \xB7 Esc to close", body, viewport.width);
2002
2546
  }
2003
2547
  });
2004
2548
 
@@ -2010,14 +2554,14 @@ var makeDefaultCampUiContributions = (tui, detailBlocks) => [
2010
2554
  mount: (context) => succeed2(makeCampUiCampHeader(context))
2011
2555
  },
2012
2556
  {
2013
- id: "default-seat",
2014
- slotId: CampUiSlot.seat,
2015
- mount: (context) => succeed2(makeCampUiSeat(context))
2557
+ id: "default-activity",
2558
+ slotId: CampUiSlot.activity,
2559
+ mount: (context) => succeed2(makeCampUiActivity(context))
2016
2560
  },
2017
2561
  {
2018
- id: "default-resources",
2019
- slotId: CampUiSlot.resources,
2020
- mount: (context) => succeed2(makeCampUiResources(context))
2562
+ id: "default-equipment",
2563
+ slotId: CampUiSlot.equipment,
2564
+ mount: (context) => succeed2(makeCampUiEquipment(context))
2021
2565
  },
2022
2566
  {
2023
2567
  id: "default-roster",
@@ -2040,29 +2584,29 @@ var makeDefaultCampUiContributions = (tui, detailBlocks) => [
2040
2584
  mount: (context) => succeed2(makeCampUiComposerComponent(context, tui))
2041
2585
  },
2042
2586
  {
2043
- id: "default-camp-footer",
2044
- slotId: CampUiSlot.campFooter,
2045
- mount: (context) => succeed2(makeCampUiCampFooter(context))
2587
+ id: "default-footer",
2588
+ slotId: CampUiSlot.footer,
2589
+ mount: (context) => succeed2(makeCampUiFooter(context))
2046
2590
  },
2047
2591
  {
2048
- id: "default-session-footer",
2049
- slotId: CampUiSlot.sessionFooter,
2050
- mount: (context) => succeed2(makeCampUiSessionFooter(context))
2592
+ id: "default-inventory-overlay",
2593
+ slotId: CampUiSlot.overlay,
2594
+ mount: (context) => succeed2(makeCampUiResources(context))
2051
2595
  }
2052
2596
  ];
2053
2597
 
2054
2598
  // packages/host-ui/src/pi/layout/campUiSection.ts
2055
- import { getMarkdownTheme as getMarkdownTheme9, getSelectListTheme as getSelectListTheme12 } from "@earendil-works/pi-coding-agent";
2056
- import { visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
2599
+ import { getMarkdownTheme as getMarkdownTheme8, getSelectListTheme as getSelectListTheme14 } from "@earendil-works/pi-coding-agent";
2600
+ import { visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
2057
2601
  var renderHeader = (header, width) => {
2058
- const theme = getMarkdownTheme9();
2059
- const selectTheme = getSelectListTheme12();
2602
+ const theme = getMarkdownTheme8();
2603
+ const selectTheme = getSelectListTheme14();
2060
2604
  const rule2 = header.focused === true ? selectTheme.selectedPrefix : theme.codeBlockBorder;
2061
2605
  const title = header.focused === true ? selectTheme.selectedText : theme.heading;
2062
2606
  const ruleCharacter = header.focused === true ? "\u2501" : "\u2500";
2063
2607
  const detail = header.detail === void 0 ? "" : ` ${selectTheme.description(header.detail)}`;
2064
2608
  const label = `${rule2(ruleCharacter.repeat(2))} ${title(header.title)}${detail} `;
2065
- const ruleWidth = Math.max(0, width - visibleWidth3(label));
2609
+ const ruleWidth = Math.max(0, width - visibleWidth6(label));
2066
2610
  return fitCampUiLine(`${label}${rule2(ruleCharacter.repeat(ruleWidth))}`, width);
2067
2611
  };
2068
2612
  var makeCampUiSection = (options) => {
@@ -2074,6 +2618,7 @@ var makeCampUiSection = (options) => {
2074
2618
  const disposableComponents = components.some((component) => component.dispose !== void 0);
2075
2619
  return {
2076
2620
  sectionSize: options.size,
2621
+ ...options.preferredRows === void 0 ? {} : { preferredRows: options.preferredRows },
2077
2622
  ...components.some((component) => component.focusable === true) ? { focusable: true } : {},
2078
2623
  render: (viewport) => {
2079
2624
  if (viewport.height <= 0) {
@@ -2139,28 +2684,38 @@ var decorateDefaultCampUiSections = (context, slots) => {
2139
2684
  {
2140
2685
  component: component(CampUiSlot.campHeader),
2141
2686
  size: { kind: CampUiSectionSizeKind.fixed, rows: 2 }
2142
- },
2143
- {
2144
- component: component(CampUiSlot.seat),
2145
- size: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 }
2146
2687
  }
2147
2688
  ]
2148
2689
  })
2149
2690
  );
2150
- decorated.delete(CampUiSlot.seat);
2151
2691
  decorated.set(
2152
- CampUiSlot.resources,
2692
+ CampUiSlot.activity,
2153
2693
  makeCampUiSection({
2154
- header: () => ({ title: "Resources" }),
2155
- size: defaultCampUiLeftSectionSizes.resources,
2694
+ header: () => ({ title: "Activity" }),
2695
+ size: defaultCampUiRightSectionSizes.activity,
2156
2696
  children: [
2157
2697
  {
2158
- component: component(CampUiSlot.resources),
2698
+ component: component(CampUiSlot.activity),
2159
2699
  size: { kind: CampUiSectionSizeKind.grow, weight: 1 }
2160
2700
  }
2161
2701
  ]
2162
2702
  })
2163
2703
  );
2704
+ const equipment = component(CampUiSlot.equipment);
2705
+ decorated.set(
2706
+ CampUiSlot.equipment,
2707
+ makeCampUiSection({
2708
+ header: () => ({ title: "Equipment" }),
2709
+ size: defaultCampUiRightSectionSizes.equipment,
2710
+ children: [
2711
+ {
2712
+ component: equipment,
2713
+ size: { kind: CampUiSectionSizeKind.grow, weight: 1 }
2714
+ }
2715
+ ],
2716
+ preferredRows: (width) => 1 + (equipment.preferredRows?.(width) ?? defaultCampUiRightSectionSizes.equipment.rows - 1)
2717
+ })
2718
+ );
2164
2719
  decorated.set(
2165
2720
  CampUiSlot.roster,
2166
2721
  makeCampUiSection({
@@ -2186,7 +2741,7 @@ import { initTheme } from "@earendil-works/pi-coding-agent";
2186
2741
  import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
2187
2742
 
2188
2743
  // packages/host-ui/src/pi/piCampUiRoot.ts
2189
- import { getMarkdownTheme as getMarkdownTheme10 } from "@earendil-works/pi-coding-agent";
2744
+ import { getMarkdownTheme as getMarkdownTheme9 } from "@earendil-works/pi-coding-agent";
2190
2745
  import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
2191
2746
 
2192
2747
  // packages/host-ui/src/pi/runtime/campUiPointerInput.ts
@@ -2315,7 +2870,7 @@ var PiCampUiRoot = class {
2315
2870
  segments.push({
2316
2871
  x: divider.x,
2317
2872
  width: 1,
2318
- text: getMarkdownTheme10().codeBlockBorder("\u2502")
2873
+ text: getMarkdownTheme9().codeBlockBorder("\u2502")
2319
2874
  });
2320
2875
  }
2321
2876
  }
@@ -2362,6 +2917,14 @@ var PiCampUiRoot = class {
2362
2917
  if (focusedSlotId !== model.state.focusedSlotId) {
2363
2918
  this.context.emit(CampUiIntents.FocusSlot({ slotId: focusedSlotId }));
2364
2919
  }
2920
+ if (model.state.inventoryVisible && matchesKey3(input, Key3.escape)) {
2921
+ this.context.emit(CampUiIntents.SetInventoryVisible({ visible: false }));
2922
+ return;
2923
+ }
2924
+ if (focusedSlotId !== CampUiSlot.composer && input === "i") {
2925
+ this.context.emit(CampUiIntents.SetInventoryVisible({ visible: true }));
2926
+ return;
2927
+ }
2365
2928
  if (this.slots.get(focusedSlotId)?.handleInput?.(input) === true) return;
2366
2929
  if (matchesKey3(input, Key3.escape)) {
2367
2930
  if (focusedSlotId === CampUiSlot.composer) {
@@ -2447,19 +3010,67 @@ var PiCampUiRoot = class {
2447
3010
  }
2448
3011
  };
2449
3012
 
3013
+ // packages/host-ui/src/pi/runtime/campUiFrameScheduler.ts
3014
+ var DEFAULT_MINIMUM_INTERVAL_MS = 1e3;
3015
+ var DEFAULT_MAXIMUM_INTERVAL_MS = 8e3;
3016
+ var DEFAULT_EXPENSIVE_FRAME_MS = 120;
3017
+ var CAMP_UI_REDUCED_MOTION_ENV = "PRIIISK_REDUCED_MOTION";
3018
+ var campUiReducedMotionFromEnv = (environment) => {
3019
+ const value = environment[CAMP_UI_REDUCED_MOTION_ENV]?.trim().toLowerCase();
3020
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false";
3021
+ };
3022
+ var makeCampUiFrameScheduler = (options = {}) => gen(function* () {
3023
+ const minimum = Math.max(
3024
+ 1,
3025
+ Math.floor(options.minimumIntervalMs ?? DEFAULT_MINIMUM_INTERVAL_MS)
3026
+ );
3027
+ const maximum = Math.max(
3028
+ minimum,
3029
+ Math.floor(options.maximumIntervalMs ?? DEFAULT_MAXIMUM_INTERVAL_MS)
3030
+ );
3031
+ const expensive = Math.max(
3032
+ 1,
3033
+ Math.floor(options.expensiveFrameMs ?? DEFAULT_EXPENSIVE_FRAME_MS)
3034
+ );
3035
+ const reducedMotion = options.reducedMotion === true;
3036
+ const pending = yield* make(false);
3037
+ const interval = yield* make(minimum);
3038
+ return {
3039
+ request: reducedMotion ? _void : set(pending, true),
3040
+ intervalMs: get(interval),
3041
+ run: (frame) => reducedMotion ? never : forever(
3042
+ gen(function* () {
3043
+ yield* sleep(millis(yield* get(interval)));
3044
+ const requested = yield* getAndSet(pending, false);
3045
+ if (!requested) return;
3046
+ const startedAt = yield* currentTimeMillis;
3047
+ yield* frame;
3048
+ const cost = (yield* currentTimeMillis) - startedAt;
3049
+ yield* update(
3050
+ interval,
3051
+ (current) => cost > expensive ? Math.min(maximum, current * 2) : Math.max(minimum, Math.floor(current / 2))
3052
+ );
3053
+ })
3054
+ )
3055
+ };
3056
+ });
3057
+
2450
3058
  // packages/host-ui/src/pi/runtime/campUiPresentationClock.ts
2451
3059
  var hasPendingCampUiAsk = (model) => model.snapshot.workers.some(
2452
3060
  (worker) => worker.asks.some((ask) => ask.state === CampAskState.pending)
2453
3061
  );
2454
- var runCampUiPresentationClock = (readModel, controller) => forever(
2455
- sleep("1 second").pipe(
2456
- zipRight(
2457
- suspend(
2458
- () => hasPendingCampUiAsk(readModel()) ? currentTimeMillis.pipe(flatMap(controller.setPresentationNow)) : _void
2459
- )
2460
- )
2461
- )
2462
- );
3062
+ var runCampUiPresentationClock = (readModel, controller, scheduler) => gen(function* () {
3063
+ const frames = scheduler ?? (yield* makeCampUiFrameScheduler({
3064
+ reducedMotion: campUiReducedMotionFromEnv(process.env)
3065
+ }));
3066
+ const advance = suspend(
3067
+ () => hasPendingCampUiAsk(readModel()) ? currentTimeMillis.pipe(flatMap(controller.setPresentationNow)) : _void
3068
+ );
3069
+ return yield* zipRight(
3070
+ frames.request,
3071
+ frames.run(zipRight(advance, frames.request))
3072
+ );
3073
+ });
2463
3074
 
2464
3075
  // packages/host-ui/src/pi/runtime/campUiSubscriptionRetry.ts
2465
3076
  var CAMP_UI_SUBSCRIPTION_MAX_RETRIES = 4;
@@ -2515,16 +3126,16 @@ var makeCampUiTerminalModes = (terminal) => {
2515
3126
  var closedOnPurpose = (exit) => isSuccess(exit) || isInterruptedOnly(exit.cause);
2516
3127
  var mountPiCampUi = (options) => gen(function* () {
2517
3128
  const startedAt = yield* currentTimeMillis;
2518
- const healthRef = yield* make3({
3129
+ const healthRef = yield* make4({
2519
3130
  status: CampUiRuntimeStatus.starting,
2520
3131
  operation: "ui.mount",
2521
3132
  updatedAt: startedAt
2522
3133
  });
2523
- const setHealth = (health) => get(healthRef).pipe(
3134
+ const setHealth = (health) => get2(healthRef).pipe(
2524
3135
  flatMap(
2525
3136
  (current2) => sameCampUiRuntimeHealth(current2, { ...health, updatedAt: current2.updatedAt }) ? _void : currentTimeMillis.pipe(
2526
3137
  flatMap(
2527
- (updatedAt) => set(healthRef, { ...health, updatedAt })
3138
+ (updatedAt) => set2(healthRef, { ...health, updatedAt })
2528
3139
  )
2529
3140
  )
2530
3141
  )
@@ -2577,6 +3188,22 @@ var mountPiCampUi = (options) => gen(function* () {
2577
3188
  );
2578
3189
  tui.addChild(root);
2579
3190
  tui.setFocus(root);
3191
+ const inventory = slots.get(CampUiSlot.overlay);
3192
+ const inventoryOverlay = inventory === void 0 ? void 0 : tui.showOverlay(
3193
+ {
3194
+ render: (width) => [
3195
+ ...inventory.render({ width, height: Math.max(0, terminal.rows) })
3196
+ ],
3197
+ invalidate: () => inventory.invalidate?.()
3198
+ },
3199
+ {
3200
+ width: "60%",
3201
+ minWidth: 36,
3202
+ margin: 1,
3203
+ visible: () => current.state.inventoryVisible,
3204
+ nonCapturing: true
3205
+ }
3206
+ );
2580
3207
  yield* sync(() => {
2581
3208
  terminal.setTitle(initial.displayName);
2582
3209
  terminal.clearScreen();
@@ -2593,6 +3220,9 @@ var mountPiCampUi = (options) => gen(function* () {
2593
3220
  )
2594
3221
  )
2595
3222
  );
3223
+ if (inventoryOverlay !== void 0) {
3224
+ yield* addFinalizer(() => sync(() => inventoryOverlay.hide()));
3225
+ }
2596
3226
  yield* forkScoped(
2597
3227
  gen(function* () {
2598
3228
  for (let attempt = 0; attempt < 25; attempt += 1) {
@@ -2601,7 +3231,7 @@ var mountPiCampUi = (options) => gen(function* () {
2601
3231
  }
2602
3232
  })
2603
3233
  );
2604
- const fibers = yield* make2();
3234
+ const fibers = yield* make3();
2605
3235
  yield* run(
2606
3236
  fibers,
2607
3237
  store.changes.pipe(
@@ -2665,12 +3295,12 @@ var mountPiCampUi = (options) => gen(function* () {
2665
3295
  return {
2666
3296
  controller: store.controller,
2667
3297
  tui,
2668
- health: { current: get(healthRef), changes: healthRef.changes }
3298
+ health: { current: get2(healthRef), changes: healthRef.changes }
2669
3299
  };
2670
3300
  });
2671
3301
 
2672
3302
  // packages/host-ui/src/pi/run-selector/campRunSelector.ts
2673
- import { getMarkdownTheme as getMarkdownTheme11, getSelectListTheme as getSelectListTheme13, initTheme as initTheme2 } from "@earendil-works/pi-coding-agent";
3303
+ import { getMarkdownTheme as getMarkdownTheme10, getSelectListTheme as getSelectListTheme15, initTheme as initTheme2 } from "@earendil-works/pi-coding-agent";
2674
3304
  import {
2675
3305
  Key as Key4,
2676
3306
  matchesKey as matchesKey4,
@@ -2692,8 +3322,8 @@ var PiCampRunSelectorRoot = class {
2692
3322
  render(width) {
2693
3323
  const height = Math.max(0, this.getHeight());
2694
3324
  const viewportWidth = Math.max(0, width - 1);
2695
- const markdown = getMarkdownTheme11();
2696
- const selection = getSelectListTheme13();
3325
+ const markdown = getMarkdownTheme10();
3326
+ const selection = getSelectListTheme15();
2697
3327
  const header = `${markdown.codeBlockBorder("\u2500\u2500")} ${markdown.heading("Resume camp")} ${markdown.codeBlockBorder("\u2500".repeat(Math.max(0, viewportWidth - 16)))}`;
2698
3328
  const available = Math.max(0, height - 2);
2699
3329
  const capacity = Math.max(1, Math.floor(available / 2));
@@ -2702,7 +3332,7 @@ var PiCampRunSelectorRoot = class {
2702
3332
  Math.max(0, this.items.length - capacity)
2703
3333
  );
2704
3334
  const visibleItems = this.items.slice(start, start + capacity);
2705
- const rows2 = visibleItems.flatMap((item, index) => {
3335
+ const rows = visibleItems.flatMap((item, index) => {
2706
3336
  const selected = start + index === this.selectedIndex;
2707
3337
  const prefix = selected ? selection.selectedPrefix("\u25CF ") : " ";
2708
3338
  const title = selected ? selection.selectedText(item.runId) : item.runId;
@@ -2714,7 +3344,7 @@ var PiCampRunSelectorRoot = class {
2714
3344
  fitCampUiLine(` ${details}`, viewportWidth)
2715
3345
  ];
2716
3346
  });
2717
- const content = [fitCampUiLine(header, viewportWidth), "", ...rows2].slice(0, height);
3347
+ const content = [fitCampUiLine(header, viewportWidth), "", ...rows].slice(0, height);
2718
3348
  return [...content, ...Array.from({ length: Math.max(0, height - content.length) }, () => "")];
2719
3349
  }
2720
3350
  handleInput(input) {
@@ -2737,7 +3367,7 @@ var PiCampRunSelectorRoot = class {
2737
3367
  var selectCampRun = (options) => scoped(
2738
3368
  gen(function* () {
2739
3369
  yield* sync(() => initTheme2(void 0, false));
2740
- const selected = yield* make();
3370
+ const selected = yield* make2();
2741
3371
  const terminal = options.terminal ?? new ProcessTerminal2();
2742
3372
  const modes = makeCampUiTerminalModes(terminal);
2743
3373
  yield* acquireRelease(
@@ -2771,12 +3401,15 @@ var selectCampRun = (options) => scoped(
2771
3401
  // packages/host-ui/src/index.ts
2772
3402
  var PACKAGE = "@priiisk/host-ui";
2773
3403
  export {
3404
+ CAMP_UI_ACTIVITY_MAX_VISIBLE_PATHS,
2774
3405
  CAMP_UI_BASH_PREVIEW_LINES,
2775
3406
  CAMP_UI_DEFAULT_TOOL_PREVIEW_LINES,
2776
3407
  CAMP_UI_EXPLORED_COLLAPSED_PATHS,
2777
3408
  CAMP_UI_EXPLORED_TOOL_NAMES,
3409
+ CAMP_UI_REDUCED_MOTION_ENV,
2778
3410
  CAMP_UI_SUBSCRIPTION_BASE_DELAY_MS,
2779
3411
  CAMP_UI_SUBSCRIPTION_MAX_RETRIES,
3412
+ CampUiActivityAccess,
2780
3413
  CampUiAssistantMessage,
2781
3414
  CampUiBackend,
2782
3415
  CampUiBackendError,
@@ -2795,24 +3428,29 @@ export {
2795
3428
  CampUiSectionSizeKind,
2796
3429
  CampUiSlot,
2797
3430
  CampUiTone,
3431
+ CampUiTrackSizeKind,
2798
3432
  DEFAULT_DIRECTED_BLOCK_BODY_LINES,
2799
3433
  PACKAGE,
2800
3434
  PiCampRunSelectorRoot,
2801
3435
  PiCampUiRoot,
2802
3436
  allocateCampUiSectionRows,
3437
+ allocateCampUiTrackColumns,
2803
3438
  cachedCampUiBlock,
2804
3439
  campUiAskBlockId,
3440
+ campUiAskDetailBlock,
2805
3441
  campUiBlockCacheSize,
2806
- campUiEquipmentBlockRenderer,
2807
3442
  campUiExploredReadPath,
2808
3443
  campUiExploredReadRange,
2809
3444
  campUiMessageSignature,
2810
3445
  campUiPendingAskBlockRenderer,
2811
3446
  campUiPendingAskTargets,
3447
+ campUiReducedMotionFromEnv,
2812
3448
  campUiResultSignature,
2813
3449
  campUiSubscriptionRetryDelay,
2814
3450
  campUiToolResultCacheSize,
2815
3451
  campUiTranscriptBlockRenderer,
3452
+ campUiWorkerStateLabel,
3453
+ collectCampUiActivityTree,
2816
3454
  compactNumber,
2817
3455
  composeCampUiDetailBlocks,
2818
3456
  composeCampUiSlots,
@@ -2820,6 +3458,7 @@ export {
2820
3458
  defaultCampUiDetailBlockRenderers,
2821
3459
  defaultCampUiLayoutPolicy,
2822
3460
  defaultCampUiLeftSectionSizes,
3461
+ defaultCampUiRightSectionSizes,
2823
3462
  emptyCampUiComponent,
2824
3463
  findCampUiSpatialFocusTarget,
2825
3464
  findCampUiWorker,
@@ -2829,14 +3468,19 @@ export {
2829
3468
  hasCampUiTextContent,
2830
3469
  hasPendingCampUiAsk,
2831
3470
  isCampUiExploredTool,
3471
+ isCampUiPendingAsk,
2832
3472
  joinCampUiColumns,
2833
3473
  jsonText,
3474
+ makeCampUiActivity,
2834
3475
  makeCampUiComponentGroup,
2835
3476
  makeCampUiComposerComponent,
2836
3477
  makeCampUiDetailComponent,
3478
+ makeCampUiEquipment,
3479
+ makeCampUiFrameScheduler,
2837
3480
  makeCampUiIntentHandler,
2838
3481
  makeCampUiIntentRouter,
2839
3482
  makeCampUiModel,
3483
+ makeCampUiResources,
2840
3484
  makeCampUiSection,
2841
3485
  makeCampUiStore,
2842
3486
  makeDefaultCampUiContributions,
@@ -2844,9 +3488,11 @@ export {
2844
3488
  makeInitialCampUiState,
2845
3489
  mountPiCampUi,
2846
3490
  nextCampUiPendingAskTarget,
3491
+ renderCampUiActivityTree,
2847
3492
  renderCampUiDetailBlockLayout,
2848
3493
  renderCampUiDetailBlocks,
2849
3494
  renderCampUiDirectedBlock,
3495
+ renderCampUiEquipment,
2850
3496
  renderCampUiExploredGroup,
2851
3497
  renderCampUiPanel,
2852
3498
  renderCampUiState,