priiisk 0.1.1

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.
@@ -0,0 +1,2547 @@
1
+ import { createRequire as __priiiskCreateRequire } from "node:module";
2
+ const require = __priiiskCreateRequire(import.meta.url);
3
+ import {
4
+ CampAskState,
5
+ Cause_exports,
6
+ Clock_exports,
7
+ Context_exports,
8
+ Data_exports,
9
+ Deferred_exports,
10
+ Effect_exports,
11
+ FiberSet_exports,
12
+ Match_exports,
13
+ Stream_exports,
14
+ SubscriptionRef_exports
15
+ } from "./chunk-KFSFN6L5.js";
16
+
17
+ // packages/host-ui/src/backend/campUiBackend.ts
18
+ var CampUiBackendError = class extends Data_exports.TaggedError("CampUiBackendError") {
19
+ };
20
+ var CampUiBackend = class extends Context_exports.Tag("priiisk/CampUiBackend")() {
21
+ };
22
+
23
+ // packages/host-ui/src/model/campUiModel.ts
24
+ var CampUiPlacement = {
25
+ external: "external",
26
+ embedded: "embedded"
27
+ };
28
+ var CampUiHealthStatus = {
29
+ ready: "ready",
30
+ degraded: "degraded",
31
+ failed: "failed"
32
+ };
33
+ var CampUiScreen = {
34
+ camp: "camp",
35
+ worker: "worker"
36
+ };
37
+ var campUiAskBlockId = (askId) => `ask:${askId}`;
38
+ var makeInitialCampUiState = (snapshot, presentationNow = 0) => ({
39
+ ...snapshot.workers[0] === void 0 ? {} : { selectedWorkerId: snapshot.workers[0].snapshot.worker.id },
40
+ screen: CampUiScreen.camp,
41
+ focusedSlotId: "roster",
42
+ detailVisible: false,
43
+ presentationNow,
44
+ drafts: /* @__PURE__ */ new Map(),
45
+ scrollOffsets: /* @__PURE__ */ new Map(),
46
+ expandedDetailBlockIds: /* @__PURE__ */ new Set()
47
+ });
48
+ var makeCampUiModel = (snapshot, presentationNow = 0) => ({
49
+ snapshot,
50
+ state: makeInitialCampUiState(snapshot, presentationNow)
51
+ });
52
+ var findCampUiWorker = (snapshot, workerId) => snapshot.workers.find((candidate) => candidate.snapshot.worker.id === workerId);
53
+
54
+ // packages/host-ui/src/backend/campUiReconcile.ts
55
+ var retainWorkerEntries = (entries, workerIds) => new Map([...entries].filter(([workerId]) => workerIds.has(workerId)));
56
+ var reconcileCampUiState = (current, snapshot) => {
57
+ if (current.snapshot.generation !== snapshot.generation) {
58
+ return makeInitialCampUiState(snapshot, current.state.presentationNow);
59
+ }
60
+ const workerIds = new Set(snapshot.workers.map((worker) => worker.snapshot.worker.id));
61
+ const previousId = current.state.selectedWorkerId;
62
+ const previousIndex = current.snapshot.workers.findIndex(
63
+ (worker) => worker.snapshot.worker.id === previousId
64
+ );
65
+ const fallbackIndex = Math.min(Math.max(previousIndex, 0), snapshot.workers.length - 1);
66
+ const selectedWorkerId = previousId !== void 0 && workerIds.has(previousId) ? previousId : snapshot.workers[fallbackIndex]?.snapshot.worker.id;
67
+ const {
68
+ selectedWorkerId: _selectedWorkerId,
69
+ focusedDetailBlockId: previousFocusedBlockId,
70
+ ...stableState
71
+ } = current.state;
72
+ const askBlockIds = new Set(
73
+ snapshot.workers.flatMap((worker) => worker.asks.map((ask) => campUiAskBlockId(ask.askId)))
74
+ );
75
+ const expandedDetailBlockIds = new Set(
76
+ [...current.state.expandedDetailBlockIds].filter(
77
+ (blockId) => !blockId.startsWith("ask:") || askBlockIds.has(blockId)
78
+ )
79
+ );
80
+ const focusedDetailBlockId = previousFocusedBlockId !== void 0 && (!previousFocusedBlockId.startsWith("ask:") || askBlockIds.has(previousFocusedBlockId)) ? previousFocusedBlockId : void 0;
81
+ return {
82
+ ...stableState,
83
+ ...selectedWorkerId === void 0 ? {} : { selectedWorkerId },
84
+ ...focusedDetailBlockId === void 0 ? {} : { focusedDetailBlockId },
85
+ screen: selectedWorkerId === void 0 ? CampUiScreen.camp : current.state.screen,
86
+ focusedSlotId: selectedWorkerId === void 0 ? "roster" : current.state.focusedSlotId,
87
+ drafts: retainWorkerEntries(current.state.drafts, workerIds),
88
+ scrollOffsets: retainWorkerEntries(current.state.scrollOffsets, workerIds),
89
+ expandedDetailBlockIds
90
+ };
91
+ };
92
+
93
+ // packages/host-ui/src/backend/campUiStoreState.ts
94
+ var updateCampUiWorkerMap = (current, workerId, value) => new Map(current).set(workerId, value);
95
+ var updateCampUiStringSet = (current, value, included) => {
96
+ const next = new Set(current);
97
+ if (included) next.add(value);
98
+ else next.delete(value);
99
+ return next;
100
+ };
101
+
102
+ // packages/host-ui/src/backend/campUiStore.ts
103
+ var CampUiControllerError = class extends Data_exports.TaggedError("CampUiControllerError") {
104
+ };
105
+ var makeCampUiStore = (initial, presentationNow = 0) => Effect_exports.gen(function* () {
106
+ const modelRef = yield* SubscriptionRef_exports.make(makeCampUiModel(initial, presentationNow));
107
+ const updateState = (mutate) => SubscriptionRef_exports.update(modelRef, (model) => ({ ...model, state: mutate(model.state) }));
108
+ const controller = {
109
+ selectWorker: (workerId) => SubscriptionRef_exports.modifyEffect(
110
+ modelRef,
111
+ (model) => findCampUiWorker(model.snapshot, workerId) === void 0 ? Effect_exports.fail(
112
+ new CampUiControllerError({
113
+ operation: "campUi.selectWorker",
114
+ code: "worker_not_found",
115
+ workerId
116
+ })
117
+ ) : Effect_exports.succeed([
118
+ void 0,
119
+ {
120
+ ...model,
121
+ state: { ...model.state, selectedWorkerId: workerId }
122
+ }
123
+ ])
124
+ ),
125
+ openWorker: (workerId) => SubscriptionRef_exports.modifyEffect(
126
+ modelRef,
127
+ (model) => findCampUiWorker(model.snapshot, workerId) === void 0 ? Effect_exports.fail(
128
+ new CampUiControllerError({
129
+ operation: "campUi.openWorker",
130
+ code: "worker_not_found",
131
+ workerId
132
+ })
133
+ ) : Effect_exports.succeed([
134
+ void 0,
135
+ {
136
+ ...model,
137
+ state: {
138
+ ...model.state,
139
+ selectedWorkerId: workerId,
140
+ screen: CampUiScreen.worker
141
+ }
142
+ }
143
+ ])
144
+ ),
145
+ activateScreen: (screen) => SubscriptionRef_exports.modifyEffect(
146
+ modelRef,
147
+ (model) => screen === CampUiScreen.worker && model.state.selectedWorkerId === void 0 ? Effect_exports.fail(
148
+ new CampUiControllerError({
149
+ operation: "campUi.activateScreen",
150
+ code: "worker_not_selected"
151
+ })
152
+ ) : Effect_exports.succeed([void 0, { ...model, state: { ...model.state, screen } }])
153
+ ),
154
+ focusDetailBlock: (focusedDetailBlockId) => updateState((state) => {
155
+ if (focusedDetailBlockId !== void 0) return { ...state, focusedDetailBlockId };
156
+ const { focusedDetailBlockId: _focusedDetailBlockId, ...rest } = state;
157
+ return rest;
158
+ }),
159
+ focusSlot: (focusedSlotId) => updateState((state) => ({ ...state, focusedSlotId })),
160
+ setDraft: (workerId, draft) => updateState((state) => ({
161
+ ...state,
162
+ drafts: updateCampUiWorkerMap(state.drafts, workerId, draft)
163
+ })),
164
+ setDetailVisible: (detailVisible) => updateState((state) => {
165
+ if (state.detailVisible === detailVisible) return state;
166
+ return { ...state, detailVisible };
167
+ }),
168
+ setDetailBlockExpanded: (blockId, expanded) => updateState((state) => ({
169
+ ...state,
170
+ expandedDetailBlockIds: updateCampUiStringSet(
171
+ state.expandedDetailBlockIds,
172
+ blockId,
173
+ expanded
174
+ )
175
+ })),
176
+ setPresentationNow: (now) => updateState((state) => {
177
+ const presentationNow2 = Math.max(0, Math.floor(now));
178
+ return presentationNow2 === state.presentationNow ? state : { ...state, presentationNow: presentationNow2 };
179
+ }),
180
+ setScrollOffset: (workerId, offset) => updateState((state) => ({
181
+ ...state,
182
+ scrollOffsets: updateCampUiWorkerMap(state.scrollOffsets, workerId, Math.max(0, offset))
183
+ })),
184
+ setActionError: (message) => updateState((state) => {
185
+ if (message !== void 0) return { ...state, lastActionError: message };
186
+ const { lastActionError: _lastActionError, ...rest } = state;
187
+ return rest;
188
+ })
189
+ };
190
+ return {
191
+ current: SubscriptionRef_exports.get(modelRef),
192
+ changes: modelRef.changes,
193
+ updateSnapshot: (snapshot) => SubscriptionRef_exports.modify(modelRef, (current) => {
194
+ const stale = current.snapshot.generation === snapshot.generation && snapshot.revision <= current.snapshot.revision;
195
+ return stale ? [false, current] : [true, { snapshot, state: reconcileCampUiState(current, snapshot) }];
196
+ }),
197
+ controller
198
+ };
199
+ });
200
+
201
+ // packages/host-ui/src/composition/campUiComponent.ts
202
+ var makeCampUiComponentGroup = (components) => {
203
+ const inputComponents = components.some((component) => component.handleInput !== void 0);
204
+ const pointerComponents = components.some((component) => component.handlePointer !== void 0);
205
+ const invalidatingComponents = components.some((component) => component.invalidate !== void 0);
206
+ return {
207
+ render: (viewport) => components.flatMap((component) => component.render(viewport)),
208
+ ...components.some((component) => component.focusable === true) ? { focusable: true } : {},
209
+ ...inputComponents ? {
210
+ handleInput: (input) => [...components].reverse().some((component) => component.handleInput?.(input) === true)
211
+ } : {},
212
+ ...pointerComponents ? {
213
+ handlePointer: (pointer) => {
214
+ for (const component of [...components].reverse()) {
215
+ const intents = component.handlePointer?.(pointer) ?? [];
216
+ if (intents.length > 0) return intents;
217
+ }
218
+ return [];
219
+ }
220
+ } : {},
221
+ ...invalidatingComponents ? {
222
+ invalidate: () => {
223
+ for (const component of components) component.invalidate?.();
224
+ }
225
+ } : {}
226
+ };
227
+ };
228
+ var emptyCampUiComponent = {
229
+ render: () => []
230
+ };
231
+
232
+ // packages/host-ui/src/composition/campUiSlots.ts
233
+ var CampUiSlot = {
234
+ campHeader: "camp-header",
235
+ seat: "seat",
236
+ resources: "resources",
237
+ roster: "roster",
238
+ sessionHeader: "session-header",
239
+ transcript: "transcript",
240
+ composer: "composer",
241
+ campFooter: "camp-footer",
242
+ sessionFooter: "session-footer",
243
+ overlay: "overlay"
244
+ };
245
+ var CampUiContributionStrategy = {
246
+ append: "append",
247
+ replace: "replace",
248
+ decorate: "decorate"
249
+ };
250
+ var CampUiCompositionError = class extends Data_exports.TaggedError("CampUiCompositionError") {
251
+ };
252
+ var orderContributions = (contributions) => [...contributions].sort(
253
+ (left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id)
254
+ );
255
+ var registerDispose = (component) => component.dispose === void 0 ? Effect_exports.succeed(component) : Effect_exports.addFinalizer(() => component.dispose).pipe(
256
+ Effect_exports.as(component)
257
+ );
258
+ var validateContributions = (defaults, contributions) => {
259
+ const ids = [...defaults, ...contributions].map((contribution) => contribution.id);
260
+ const duplicateId = ids.find((id, index) => ids.indexOf(id) !== index);
261
+ if (duplicateId !== void 0) {
262
+ return Effect_exports.fail(
263
+ new CampUiCompositionError({ code: "duplicate_id", details: { id: duplicateId } })
264
+ );
265
+ }
266
+ const replacementSlots = contributions.filter((contribution) => contribution.strategy === CampUiContributionStrategy.replace).map((contribution) => contribution.slotId);
267
+ const duplicateSlot = replacementSlots.find(
268
+ (slotId, index) => replacementSlots.indexOf(slotId) !== index
269
+ );
270
+ return duplicateSlot === void 0 ? Effect_exports.void : Effect_exports.fail(
271
+ new CampUiCompositionError({
272
+ code: "multiple_replacements",
273
+ details: { slotId: duplicateSlot }
274
+ })
275
+ );
276
+ };
277
+ var composeCampUiSlots = (context, defaults, contributions) => Effect_exports.gen(function* () {
278
+ yield* validateContributions(defaults, contributions);
279
+ const slotIds = [...new Set([...defaults, ...contributions].map((item) => item.slotId))];
280
+ const entries = yield* Effect_exports.forEach(
281
+ slotIds,
282
+ (slotId) => Effect_exports.gen(function* () {
283
+ const replacement = contributions.find(
284
+ (item) => item.slotId === slotId && item.strategy === CampUiContributionStrategy.replace
285
+ );
286
+ const base = replacement === void 0 ? defaults.filter((item) => item.slotId === slotId) : [];
287
+ const appended = contributions.filter(
288
+ (item) => item.slotId === slotId && item.strategy === CampUiContributionStrategy.append
289
+ );
290
+ const contentFactories = orderContributions([
291
+ ...base,
292
+ ...replacement === void 0 ? [] : [replacement],
293
+ ...appended
294
+ ]);
295
+ const components = yield* Effect_exports.forEach(
296
+ contentFactories,
297
+ (item) => item.mount(context).pipe(Effect_exports.flatMap(registerDispose))
298
+ );
299
+ let content = components.length === 0 ? emptyCampUiComponent : makeCampUiComponentGroup(components);
300
+ const decorators = orderContributions(
301
+ contributions.filter(
302
+ (item) => item.slotId === slotId && item.strategy === CampUiContributionStrategy.decorate
303
+ )
304
+ );
305
+ for (const decorator of decorators) {
306
+ content = yield* decorator.decorate(context, content).pipe(Effect_exports.flatMap(registerDispose));
307
+ }
308
+ return [slotId, content];
309
+ })
310
+ );
311
+ return new Map(entries);
312
+ });
313
+
314
+ // packages/host-ui/src/composition/campUiDetailBlocks.ts
315
+ var byOrder = (left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id);
316
+ var composeCampUiDetailBlocks = (defaults, contributions) => {
317
+ const renderers = [...defaults, ...contributions];
318
+ const ids = renderers.map((renderer) => renderer.id);
319
+ const duplicateId = ids.find((id, index) => ids.indexOf(id) !== index);
320
+ return duplicateId === void 0 ? Effect_exports.succeed(renderers.sort(byOrder)) : Effect_exports.fail(
321
+ new CampUiCompositionError({
322
+ code: "duplicate_id",
323
+ details: { area: "detail-block", id: duplicateId }
324
+ })
325
+ );
326
+ };
327
+ var renderCampUiDetailBlocks = (registry, context) => renderCampUiDetailBlockLayout(registry, context).lines;
328
+ var renderCampUiDetailBlockLayout = (registry, context) => {
329
+ const lines = [];
330
+ const ranges = [];
331
+ for (const renderer of registry) {
332
+ for (const block of renderer.render(context)) {
333
+ const start = lines.length;
334
+ lines.push(...block.lines, "");
335
+ ranges.push({
336
+ id: block.id,
337
+ start,
338
+ end: start + block.lines.length,
339
+ toggleable: block.toggleable === true
340
+ });
341
+ }
342
+ }
343
+ return { lines, ranges };
344
+ };
345
+
346
+ // packages/host-ui/src/model/campUiAttention.ts
347
+ var campUiPendingAskTargets = (snapshot) => snapshot.workers.flatMap(
348
+ (worker, workerIndex) => worker.asks.filter((ask) => ask.state === CampAskState.pending).map((ask) => ({
349
+ askId: ask.askId,
350
+ blockId: campUiAskBlockId(ask.askId),
351
+ openedAt: ask.openedAt,
352
+ workerId: worker.snapshot.worker.id,
353
+ workerIndex
354
+ }))
355
+ ).sort(
356
+ (left, right) => left.openedAt - right.openedAt || left.workerIndex - right.workerIndex || left.askId.localeCompare(right.askId)
357
+ );
358
+ var nextCampUiPendingAskTarget = (snapshot, focusedBlockId) => {
359
+ const targets = campUiPendingAskTargets(snapshot);
360
+ if (targets.length === 0) return void 0;
361
+ const current = targets.findIndex((target) => target.blockId === focusedBlockId);
362
+ return targets[current < 0 ? 0 : (current + 1) % targets.length];
363
+ };
364
+
365
+ // packages/host-ui/src/interaction/campUiIntents.ts
366
+ var CampUiIntents = Data_exports.taggedEnum();
367
+ var makeCampUiIntentHandler = (options) => Match_exports.type().pipe(
368
+ Match_exports.tagsExhaustive({
369
+ ActivateScreen: ({ screen }) => options.controller.activateScreen(screen),
370
+ FocusDetailBlock: ({ blockId }) => options.controller.focusDetailBlock(blockId),
371
+ FocusSlot: ({ slotId }) => options.controller.focusSlot(slotId),
372
+ JumpToWorkerTail: ({ workerId }) => options.controller.focusDetailBlock(void 0).pipe(Effect_exports.zipRight(options.controller.setScrollOffset(workerId, 0))),
373
+ NextPendingAsk: () => Effect_exports.suspend(() => {
374
+ const model = options.readModel();
375
+ const target = nextCampUiPendingAskTarget(
376
+ model.snapshot,
377
+ model.state.focusedDetailBlockId
378
+ );
379
+ return target === void 0 ? Effect_exports.void : options.controller.openWorker(target.workerId).pipe(
380
+ Effect_exports.zipRight(options.controller.setDetailBlockExpanded(target.blockId, true)),
381
+ Effect_exports.zipRight(options.controller.focusDetailBlock(target.blockId)),
382
+ Effect_exports.zipRight(options.controller.focusSlot(CampUiSlot.transcript))
383
+ );
384
+ }),
385
+ OpenWorker: ({ workerId }) => options.controller.openWorker(workerId),
386
+ ScrollWorker: ({ workerId, delta }) => Effect_exports.suspend(() => {
387
+ const offset = options.readModel().state.scrollOffsets.get(workerId) ?? 0;
388
+ return options.controller.focusDetailBlock(void 0).pipe(
389
+ Effect_exports.zipRight(
390
+ options.controller.setScrollOffset(workerId, Math.max(0, offset + delta))
391
+ )
392
+ );
393
+ }),
394
+ SelectRelativeWorker: ({ delta }) => Effect_exports.suspend(() => {
395
+ const model = options.readModel();
396
+ const workers = model.snapshot.workers;
397
+ if (workers.length === 0) return Effect_exports.void;
398
+ const current = workers.findIndex(
399
+ (worker) => worker.snapshot.worker.id === model.state.selectedWorkerId
400
+ );
401
+ const index = current < 0 ? 0 : Math.min(workers.length - 1, Math.max(0, current + delta));
402
+ const workerId = workers[index]?.snapshot.worker.id;
403
+ return workerId === void 0 ? Effect_exports.void : options.controller.selectWorker(workerId);
404
+ }),
405
+ SelectWorker: ({ workerId }) => options.controller.selectWorker(workerId),
406
+ SetDraft: ({ workerId, draft }) => options.controller.setDraft(workerId, draft),
407
+ SetScrollOffset: ({ workerId, offset }) => options.controller.setScrollOffset(workerId, offset),
408
+ SetTranscriptVisible: ({ visible }) => options.controller.setDetailVisible(visible),
409
+ Steer: ({ workerId, message }) => message.trim() === "" ? Effect_exports.void : options.backend.steer({ workerId, message }).pipe(
410
+ Effect_exports.tap(() => options.controller.setActionError(void 0)),
411
+ Effect_exports.tap(() => options.controller.setDraft(workerId, "")),
412
+ Effect_exports.catchAll(
413
+ (cause) => options.controller.setDraft(workerId, message).pipe(Effect_exports.zipRight(options.controller.setActionError(cause.message)))
414
+ )
415
+ ),
416
+ ToggleDetailBlock: ({ blockId }) => Effect_exports.suspend(
417
+ () => options.controller.setDetailBlockExpanded(
418
+ blockId,
419
+ !options.readModel().state.expandedDetailBlockIds.has(blockId)
420
+ )
421
+ )
422
+ })
423
+ );
424
+ var makeCampUiIntentRouter = (options) => {
425
+ const handle = makeCampUiIntentHandler(options);
426
+ return { emit: (intent) => options.dispatch(handle(intent)) };
427
+ };
428
+
429
+ // packages/host-ui/src/interaction/campUiSpatialFocus.ts
430
+ var CampUiFocusDirection = {
431
+ down: "down",
432
+ left: "left",
433
+ right: "right",
434
+ up: "up"
435
+ };
436
+ var centerX = (region2) => region2.x + region2.width / 2;
437
+ var centerY = (region2) => region2.y + region2.height / 2;
438
+ var distanceToInterval = (value, start, end) => value < start ? start - value : value > end ? value - end : 0;
439
+ var scoreCandidate = (current, candidate, direction, order) => {
440
+ const horizontalDelta = centerX(candidate) - centerX(current);
441
+ const verticalDelta = centerY(candidate) - centerY(current);
442
+ const horizontal = direction === CampUiFocusDirection.left || direction === CampUiFocusDirection.right;
443
+ const directionDelta = horizontal ? horizontalDelta : verticalDelta;
444
+ const expectedSign = direction === CampUiFocusDirection.right || direction === CampUiFocusDirection.down ? 1 : -1;
445
+ if (directionDelta * expectedSign <= 0) return void 0;
446
+ const primaryDistance = horizontal ? direction === CampUiFocusDirection.right ? Math.max(0, candidate.x - (current.x + current.width)) : Math.max(0, current.x - (candidate.x + candidate.width)) : direction === CampUiFocusDirection.down ? Math.max(0, candidate.y - (current.y + current.height)) : Math.max(0, current.y - (candidate.y + candidate.height));
447
+ const perpendicularDistance = horizontal ? distanceToInterval(centerY(current), candidate.y, candidate.y + candidate.height) : distanceToInterval(centerX(current), candidate.x, candidate.x + candidate.width);
448
+ return {
449
+ slotId: candidate.slotId,
450
+ primaryDistance,
451
+ perpendicularDistance,
452
+ centerDistance: Math.abs(directionDelta),
453
+ order
454
+ };
455
+ };
456
+ var compareCandidates = (left, right) => left.primaryDistance - right.primaryDistance || left.perpendicularDistance - right.perpendicularDistance || left.centerDistance - right.centerDistance || left.order - right.order;
457
+ var findCampUiSpatialFocusTarget = (frame, currentSlotId, focusableSlotIds, direction) => {
458
+ if (!focusableSlotIds.has(currentSlotId)) return void 0;
459
+ const current = frame?.regions.find((region2) => region2.slotId === currentSlotId);
460
+ if (current === void 0) return void 0;
461
+ return frame?.regions.map(
462
+ (candidate, order) => candidate.slotId === currentSlotId || !focusableSlotIds.has(candidate.slotId) ? void 0 : scoreCandidate(current, candidate, direction, order)
463
+ ).filter((candidate) => candidate !== void 0).sort(compareCandidates)[0]?.slotId;
464
+ };
465
+
466
+ // packages/host-ui/src/layout/campUiLayoutPolicy.ts
467
+ var defaultCampUiLayoutPolicy = {
468
+ leftMinimumWidth: 28,
469
+ leftMaximumWidth: 40,
470
+ sessionMinimumWidth: 52,
471
+ wideMinimumHeight: 18,
472
+ leftRatio: 0.3,
473
+ footerHeight: 3,
474
+ sessionHeaderHeight: 1,
475
+ composerHeight: 3
476
+ };
477
+
478
+ // packages/host-ui/src/layout/campUiSizing.ts
479
+ var CampUiSectionSizeKind = {
480
+ fixed: "fixed",
481
+ grow: "grow"
482
+ };
483
+ var rows = (value) => Math.max(0, Math.floor(value));
484
+ var allocateCampUiSectionRows = (availableRows, sizes) => {
485
+ const allocated = sizes.map(() => 0);
486
+ let remaining = rows(availableRows);
487
+ const growIndexes = [];
488
+ for (const [index, size] of sizes.entries()) {
489
+ if (size.kind === CampUiSectionSizeKind.grow) {
490
+ growIndexes.push(index);
491
+ continue;
492
+ }
493
+ const height = Math.min(remaining, rows(size.rows));
494
+ allocated[index] = height;
495
+ remaining -= height;
496
+ }
497
+ const maximumMinimum = Math.max(
498
+ 0,
499
+ ...growIndexes.map((index) => {
500
+ const size = sizes[index];
501
+ return size?.kind === CampUiSectionSizeKind.grow ? rows(size.minimumRows ?? 0) : 0;
502
+ })
503
+ );
504
+ for (let minimumRow = 0; minimumRow < maximumMinimum && remaining > 0; minimumRow += 1) {
505
+ for (const index of growIndexes) {
506
+ const size = sizes[index];
507
+ if (size?.kind !== CampUiSectionSizeKind.grow || minimumRow >= rows(size.minimumRows ?? 0)) {
508
+ continue;
509
+ }
510
+ allocated[index] = (allocated[index] ?? 0) + 1;
511
+ remaining -= 1;
512
+ if (remaining === 0) break;
513
+ }
514
+ }
515
+ let remainingWeight = growIndexes.reduce((total, index) => {
516
+ const size = sizes[index];
517
+ return total + (size?.kind === CampUiSectionSizeKind.grow ? Math.max(0, size.weight) : 0);
518
+ }, 0);
519
+ for (const [position, index] of growIndexes.entries()) {
520
+ if (remaining === 0) break;
521
+ const size = sizes[index];
522
+ if (size?.kind !== CampUiSectionSizeKind.grow) continue;
523
+ const weight = Math.max(0, size.weight);
524
+ const last = position === growIndexes.length - 1;
525
+ const height = last || remainingWeight <= 0 ? remaining : Math.floor(remaining * weight / remainingWeight);
526
+ allocated[index] = (allocated[index] ?? 0) + height;
527
+ remaining -= height;
528
+ remainingWeight -= weight;
529
+ }
530
+ return allocated;
531
+ };
532
+ var defaultCampUiLeftSectionSizes = {
533
+ camp: { kind: CampUiSectionSizeKind.fixed, rows: 4 },
534
+ resources: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 },
535
+ workers: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 }
536
+ };
537
+
538
+ // packages/host-ui/src/layout/campUiLayout.ts
539
+ var CampUiLayoutMode = {
540
+ wide: "wide",
541
+ narrow: "narrow"
542
+ };
543
+ var region = (slotId, x, y, width, height) => width <= 0 || height <= 0 ? void 0 : { slotId, x, y, width, height };
544
+ var compact = (items) => items.filter((item) => item !== void 0);
545
+ var fixedRows = (available, requested) => Math.min(Math.max(available, 0), Math.max(requested, 0));
546
+ var leftSectionHeights = (bodyHeight, slots) => {
547
+ const size = (slotId, fallback) => slots?.get(slotId)?.sectionSize ?? fallback;
548
+ return allocateCampUiSectionRows(bodyHeight, [
549
+ size(CampUiSlot.campHeader, defaultCampUiLeftSectionSizes.camp),
550
+ size(CampUiSlot.resources, defaultCampUiLeftSectionSizes.resources),
551
+ size(CampUiSlot.roster, defaultCampUiLeftSectionSizes.workers)
552
+ ]);
553
+ };
554
+ var makeNarrowFrame = (viewport, state, policy, slots) => {
555
+ const footerHeight = fixedRows(viewport.height, policy.footerHeight);
556
+ const bodyHeight = Math.max(0, viewport.height - footerHeight);
557
+ const footerY = bodyHeight;
558
+ const workerScreen = state.screen === CampUiScreen.worker && state.selectedWorkerId !== void 0;
559
+ const [campHeight = 0, resourcesHeight = 0, rosterHeight = 0] = workerScreen ? [] : leftSectionHeights(bodyHeight, slots);
560
+ const sessionHeaderHeight = workerScreen ? fixedRows(bodyHeight, policy.sessionHeaderHeight) : 0;
561
+ const composerHeight = workerScreen ? fixedRows(bodyHeight - sessionHeaderHeight, policy.composerHeight) : 0;
562
+ const transcriptHeight = workerScreen ? bodyHeight - sessionHeaderHeight - composerHeight : 0;
563
+ return {
564
+ mode: CampUiLayoutMode.narrow,
565
+ dividers: [],
566
+ regions: compact([
567
+ region(CampUiSlot.campHeader, 0, 0, viewport.width, campHeight),
568
+ region(CampUiSlot.resources, 0, campHeight, viewport.width, resourcesHeight),
569
+ region(CampUiSlot.roster, 0, campHeight + resourcesHeight, viewport.width, rosterHeight),
570
+ region(CampUiSlot.sessionHeader, 0, 0, viewport.width, sessionHeaderHeight),
571
+ region(CampUiSlot.transcript, 0, sessionHeaderHeight, viewport.width, transcriptHeight),
572
+ region(
573
+ CampUiSlot.composer,
574
+ 0,
575
+ sessionHeaderHeight + transcriptHeight,
576
+ viewport.width,
577
+ composerHeight
578
+ ),
579
+ region(
580
+ workerScreen ? CampUiSlot.sessionFooter : CampUiSlot.campFooter,
581
+ 0,
582
+ footerY,
583
+ viewport.width,
584
+ footerHeight
585
+ )
586
+ ])
587
+ };
588
+ };
589
+ var makeWideFrame = (viewport, policy, slots) => {
590
+ const footerHeight = fixedRows(viewport.height, policy.footerHeight);
591
+ const bodyHeight = Math.max(0, viewport.height - footerHeight);
592
+ const leftWidth = Math.max(
593
+ policy.leftMinimumWidth,
594
+ Math.min(
595
+ policy.leftMaximumWidth,
596
+ viewport.width - policy.sessionMinimumWidth - 1,
597
+ Math.floor(viewport.width * policy.leftRatio)
598
+ )
599
+ );
600
+ const rightX = leftWidth + 1;
601
+ const rightWidth = viewport.width - rightX;
602
+ const [campHeight = 0, resourcesHeight = 0, rosterHeight = 0] = leftSectionHeights(
603
+ bodyHeight,
604
+ slots
605
+ );
606
+ const sessionHeaderHeight = fixedRows(bodyHeight, policy.sessionHeaderHeight);
607
+ const composerHeight = fixedRows(bodyHeight - sessionHeaderHeight, policy.composerHeight);
608
+ const transcriptHeight = bodyHeight - sessionHeaderHeight - composerHeight;
609
+ const footerY = bodyHeight;
610
+ return {
611
+ mode: CampUiLayoutMode.wide,
612
+ dividers: viewport.height === 0 ? [] : [{ x: leftWidth, y: 0, height: viewport.height }],
613
+ regions: compact([
614
+ region(CampUiSlot.campHeader, 0, 0, leftWidth, campHeight),
615
+ region(CampUiSlot.resources, 0, campHeight, leftWidth, resourcesHeight),
616
+ region(CampUiSlot.roster, 0, campHeight + resourcesHeight, leftWidth, rosterHeight),
617
+ region(CampUiSlot.sessionHeader, rightX, 0, rightWidth, sessionHeaderHeight),
618
+ region(CampUiSlot.transcript, rightX, sessionHeaderHeight, rightWidth, transcriptHeight),
619
+ region(
620
+ CampUiSlot.composer,
621
+ rightX,
622
+ sessionHeaderHeight + transcriptHeight,
623
+ rightWidth,
624
+ composerHeight
625
+ ),
626
+ region(CampUiSlot.campFooter, 0, footerY, leftWidth, footerHeight),
627
+ region(CampUiSlot.sessionFooter, rightX, footerY, rightWidth, footerHeight)
628
+ ])
629
+ };
630
+ };
631
+ var makeDefaultCampUiLayout = (policy = defaultCampUiLayoutPolicy) => ({
632
+ id: "default",
633
+ resolve: (viewport, state, slots) => {
634
+ const normalized = {
635
+ width: Math.max(0, viewport.width),
636
+ height: Math.max(0, viewport.height)
637
+ };
638
+ const wideMinimumWidth = policy.leftMinimumWidth + 1 + policy.sessionMinimumWidth;
639
+ return normalized.width >= wideMinimumWidth && normalized.height >= policy.wideMinimumHeight ? makeWideFrame(normalized, policy, slots) : makeNarrowFrame(normalized, state, policy, slots);
640
+ }
641
+ });
642
+
643
+ // packages/host-ui/src/model/campUiRuntimeHealth.ts
644
+ var CampUiRuntimeStatus = {
645
+ starting: "starting",
646
+ ready: "ready",
647
+ retrying: "retrying",
648
+ failed: "failed",
649
+ stopped: "stopped"
650
+ };
651
+
652
+ // packages/host-ui/src/pi/shared/campUiVisual.ts
653
+ import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
654
+
655
+ // packages/host-ui/src/pi/shared/campUiRenderText.ts
656
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
657
+ var piShellIntegrationMarkers = [
658
+ "\x1B]133;A\x07",
659
+ "\x1B]133;B\x07",
660
+ "\x1B]133;C\x07"
661
+ ];
662
+ var stripPiShellIntegration = (line) => piShellIntegrationMarkers.reduce((current, marker) => current.replaceAll(marker, ""), line);
663
+ var hasCampUiTextContent = (line) => {
664
+ let index = 0;
665
+ while (index < line.length) {
666
+ if (line.charCodeAt(index) !== 27) {
667
+ if ((line[index] ?? "").trim() !== "") return true;
668
+ index += 1;
669
+ continue;
670
+ }
671
+ const marker = line[index + 1];
672
+ if (marker === "[") {
673
+ index += 2;
674
+ while (index < line.length) {
675
+ const code = line.charCodeAt(index);
676
+ index += 1;
677
+ if (code >= 64 && code <= 126) break;
678
+ }
679
+ continue;
680
+ }
681
+ if (marker === "]") {
682
+ index += 2;
683
+ while (index < line.length) {
684
+ if (line.charCodeAt(index) === 7) {
685
+ index += 1;
686
+ break;
687
+ }
688
+ if (line.charCodeAt(index) === 27 && line[index + 1] === "\\") {
689
+ index += 2;
690
+ break;
691
+ }
692
+ index += 1;
693
+ }
694
+ continue;
695
+ }
696
+ index += Math.min(2, line.length - index);
697
+ }
698
+ return false;
699
+ };
700
+ var fitCampUiLine = (line, width) => {
701
+ if (width <= 0) return "";
702
+ const truncated = truncateToWidth(stripPiShellIntegration(line), width);
703
+ return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
704
+ };
705
+ var fitCampUiLines = (lines, width, height) => Array.from(
706
+ { length: Math.max(0, height) },
707
+ (_, index) => fitCampUiLine(lines[index] ?? "", width)
708
+ );
709
+ var joinCampUiColumns = (left, right, width) => {
710
+ if (width <= 0) return "";
711
+ const rightWidth = visibleWidth(right);
712
+ if (rightWidth >= width) return fitCampUiLine(right, width);
713
+ const leftWidth = Math.max(0, width - rightWidth - 2);
714
+ const fittedLeft = truncateToWidth(left, leftWidth);
715
+ const gap = " ".repeat(Math.max(1, width - visibleWidth(fittedLeft) - rightWidth));
716
+ return fitCampUiLine(`${fittedLeft}${gap}${right}`, width);
717
+ };
718
+ var jsonText = (value) => {
719
+ try {
720
+ return JSON.stringify(value, void 0, 2);
721
+ } catch {
722
+ return String(value);
723
+ }
724
+ };
725
+ var compactNumber = (value) => new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
726
+
727
+ // packages/host-ui/src/pi/shared/campUiVisual.ts
728
+ var CampUiTone = {
729
+ success: "success",
730
+ active: "active",
731
+ attention: "attention",
732
+ error: "error",
733
+ external: "external",
734
+ info: "info",
735
+ neutral: "neutral"
736
+ };
737
+ var CampUiGlyph = {
738
+ success: "\u2713",
739
+ active: "\u25B8",
740
+ attention: "\u25B8",
741
+ error: "\u2717",
742
+ external: "\u21C4",
743
+ neutral: "\xB7",
744
+ live: "\u25CF"
745
+ };
746
+ var ansiByTone = {
747
+ success: "\x1B[32m",
748
+ active: "\x1B[32m",
749
+ attention: "\x1B[33m",
750
+ error: "\x1B[31m",
751
+ external: "\x1B[34m",
752
+ info: "\x1B[34m",
753
+ neutral: "\x1B[90m"
754
+ };
755
+ var glyphByTone = {
756
+ success: CampUiGlyph.success,
757
+ active: CampUiGlyph.active,
758
+ attention: CampUiGlyph.attention,
759
+ error: CampUiGlyph.error,
760
+ external: CampUiGlyph.external,
761
+ info: CampUiGlyph.active,
762
+ neutral: CampUiGlyph.neutral
763
+ };
764
+ var statePresentation = /* @__PURE__ */ new Map([
765
+ ["pending", { tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }],
766
+ ["starting", { tone: CampUiTone.active, glyph: CampUiGlyph.active }],
767
+ ["running", { tone: CampUiTone.active, glyph: CampUiGlyph.active }],
768
+ ["recovering", { tone: CampUiTone.attention, glyph: CampUiGlyph.attention }],
769
+ ["completed", { tone: CampUiTone.success, glyph: CampUiGlyph.success }],
770
+ ["failed", { tone: CampUiTone.error, glyph: CampUiGlyph.error }],
771
+ ["closed", { tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }],
772
+ ["ready", { tone: CampUiTone.success, glyph: CampUiGlyph.live }],
773
+ ["degraded", { tone: CampUiTone.attention, glyph: CampUiGlyph.attention }],
774
+ ["streaming", { tone: CampUiTone.active, glyph: CampUiGlyph.active }],
775
+ ["idle", { tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }]
776
+ ]);
777
+ var styleCampUiTone = (tone, text) => `${ansiByTone[tone]}${text}\x1B[39m`;
778
+ var uniqueCampUiNames = (names) => [...new Set(names)].sort((left, right) => left.localeCompare(right));
779
+ var renderCampUiStatus = (label, tone, glyph = glyphByTone[tone]) => styleCampUiTone(tone, `${glyph} ${label}`);
780
+ var renderCampUiState = (state) => {
781
+ const presentation = statePresentation.get(state) ?? {
782
+ tone: CampUiTone.neutral,
783
+ glyph: CampUiGlyph.neutral
784
+ };
785
+ return renderCampUiStatus(state, presentation.tone, presentation.glyph);
786
+ };
787
+ var workerStatePresentation = /* @__PURE__ */ new Map([
788
+ ["queued", { label: "queued", tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }],
789
+ ["starting", { label: "starting", tone: CampUiTone.info, glyph: CampUiGlyph.active }],
790
+ ["running", { label: "working", tone: CampUiTone.active, glyph: CampUiGlyph.active }],
791
+ [
792
+ "waiting_for_orchestrator",
793
+ { label: "ask", tone: CampUiTone.attention, glyph: CampUiGlyph.attention }
794
+ ],
795
+ /* A bounded wait, not a failure: the session and the transcript are still there. */
796
+ ["retrying", { label: "retrying", tone: CampUiTone.attention, glyph: CampUiGlyph.attention }],
797
+ ["idle", { label: "idle", tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }],
798
+ ["interrupted", { label: "interrupted", tone: CampUiTone.error, glyph: CampUiGlyph.error }],
799
+ ["recovering", { label: "recovering", tone: CampUiTone.attention, glyph: CampUiGlyph.attention }],
800
+ ["completed", { label: "done", tone: CampUiTone.success, glyph: CampUiGlyph.success }],
801
+ ["failed", { label: "error", tone: CampUiTone.error, glyph: CampUiGlyph.error }],
802
+ ["closed", { label: "closed", tone: CampUiTone.neutral, glyph: CampUiGlyph.neutral }]
803
+ ]);
804
+ var fallbackWorkerPresentation = {
805
+ label: "unknown",
806
+ tone: CampUiTone.neutral,
807
+ glyph: CampUiGlyph.neutral
808
+ };
809
+ var renderCampUiWorkerState = (state, options) => {
810
+ const base = workerStatePresentation.get(state) ?? fallbackWorkerPresentation;
811
+ const terminal = state === "completed" || state === "closed";
812
+ const presentation = options.failed || state === "failed" || state === "interrupted" ? workerStatePresentation.get(state === "interrupted" ? "interrupted" : "failed") : !terminal && options.pendingAsks > 0 ? {
813
+ label: options.pendingAsks === 1 ? "ask" : `ask ${options.pendingAsks}`,
814
+ tone: CampUiTone.attention,
815
+ glyph: CampUiGlyph.attention
816
+ } : !terminal && options.streaming ? workerStatePresentation.get("running") : base;
817
+ const resolved = presentation ?? fallbackWorkerPresentation;
818
+ return renderCampUiStatus(resolved.label, resolved.tone, resolved.glyph);
819
+ };
820
+ var renderSelectedCampUiRow = (line, width) => {
821
+ const selected = getSelectListTheme().selectedText(fitCampUiLine(line, width));
822
+ return `\x1B[7m${selected}\x1B[27m`;
823
+ };
824
+
825
+ // packages/host-ui/src/pi/blocks/campUiDirectedBlock.ts
826
+ import { getMarkdownTheme, getSelectListTheme as getSelectListTheme2 } from "@earendil-works/pi-coding-agent";
827
+ import { Markdown, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
828
+ var DEFAULT_DIRECTED_BLOCK_BODY_LINES = 4;
829
+ var formatCampUiDuration = (durationMs) => {
830
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
831
+ const seconds = totalSeconds % 60;
832
+ const totalMinutes = Math.floor(totalSeconds / 60);
833
+ if (totalMinutes === 0) return `${seconds}s`;
834
+ const minutes = totalMinutes % 60;
835
+ const hours = Math.floor(totalMinutes / 60);
836
+ if (hours === 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
837
+ return `${hours}h ${String(minutes).padStart(2, "0")}m`;
838
+ };
839
+ var directedLine = (content, width, tone) => {
840
+ if (width <= 0) return "";
841
+ const border = styleCampUiTone(tone, "\u2502");
842
+ return width === 1 ? border : fitCampUiLine(`${border} ${content}`, width);
843
+ };
844
+ var collapseMarkdownLines = (lines, maximum, width) => {
845
+ if (lines.length <= maximum) return { lines, hidden: 0 };
846
+ if (maximum <= 0) return { lines: [], hidden: lines.length };
847
+ const visible = lines.slice(0, maximum);
848
+ const last = visible.at(-1);
849
+ if (last !== void 0) {
850
+ const previewWidth = Math.max(1, Math.floor(width * 0.7));
851
+ visible[visible.length - 1] = getSelectListTheme2().description(
852
+ `${truncateToWidth2(last, Math.max(1, previewWidth - 1))}\u2026`
853
+ );
854
+ }
855
+ return { lines: visible, hidden: lines.length - maximum };
856
+ };
857
+ var renderCampUiDirectedBlock = (block, width) => {
858
+ if (width <= 0) return [];
859
+ const contentWidth = Math.max(1, width - 2);
860
+ const markdownLines = new Markdown(block.markdown, 0, 0, getMarkdownTheme()).render(contentWidth);
861
+ const maximum = block.collapsedBodyLines ?? DEFAULT_DIRECTED_BLOCK_BODY_LINES;
862
+ const collapsed = collapseMarkdownLines(markdownLines, maximum, contentWidth);
863
+ const body = block.expanded ? markdownLines : collapsed.lines;
864
+ const expandable = markdownLines.length > maximum;
865
+ const hint = !expandable || block.showToggleHint === false ? [] : [
866
+ getSelectListTheme2().description(
867
+ block.expanded ? "enter collapse" : `enter expand \xB7 ${collapsed.hidden} lines hidden`
868
+ )
869
+ ];
870
+ return [
871
+ directedLine(renderCampUiStatus(block.label, block.tone), width, block.tone),
872
+ ...body.map((line) => directedLine(line, width, block.tone)),
873
+ directedLine(getSelectListTheme2().description(block.summary), width, block.tone),
874
+ ...hint.map((line) => directedLine(line, width, block.tone))
875
+ ];
876
+ };
877
+
878
+ // packages/host-ui/src/pi/blocks/campUiAskBlock.ts
879
+ var toneByState = {
880
+ [CampAskState.pending]: CampUiTone.attention,
881
+ [CampAskState.answered]: CampUiTone.success,
882
+ [CampAskState.expired]: CampUiTone.error,
883
+ [CampAskState.interrupted]: CampUiTone.error
884
+ };
885
+ var campUiPendingAskBlockRenderer = {
886
+ id: "pending-asks",
887
+ order: 100,
888
+ render: ({ model, worker, width }) => {
889
+ return worker.asks.map((ask) => {
890
+ const blockId = campUiAskBlockId(ask.askId);
891
+ const mode = ask.blocking ? "blocking" : "non-blocking";
892
+ const stateLabel = {
893
+ [CampAskState.pending]: `pending \xB7 ${formatCampUiDuration(
894
+ model.state.presentationNow - ask.openedAt
895
+ )}`,
896
+ [CampAskState.answered]: "answered",
897
+ [CampAskState.expired]: "expired",
898
+ [CampAskState.interrupted]: "interrupted"
899
+ }[ask.state];
900
+ const summary = {
901
+ [CampAskState.pending]: `Awaiting orchestrator reply \xB7 ${ask.askId}`,
902
+ [CampAskState.answered]: `${ask.answerSummary ?? "Answered"} \xB7 ${ask.askId}`,
903
+ [CampAskState.expired]: `Expired \xB7 ${ask.askId}`,
904
+ [CampAskState.interrupted]: `Interrupted by host restart \xB7 ${ask.askId}`
905
+ }[ask.state];
906
+ return {
907
+ id: blockId,
908
+ lines: renderCampUiDirectedBlock(
909
+ {
910
+ label: `Ask \xB7 to ${ask.controllerId} \xB7 ${mode} \xB7 ${stateLabel}`,
911
+ markdown: ask.question,
912
+ summary,
913
+ tone: toneByState[ask.state],
914
+ expanded: model.state.expandedDetailBlockIds.has(blockId),
915
+ ...ask.state === CampAskState.answered ? { collapsedBodyLines: 0, showToggleHint: false } : {}
916
+ },
917
+ width
918
+ ),
919
+ toggleable: ask.state !== CampAskState.answered
920
+ };
921
+ });
922
+ }
923
+ };
924
+
925
+ // packages/host-ui/src/pi/blocks/campUiAssistantMessage.ts
926
+ import { getMarkdownTheme as getMarkdownTheme2, getSelectListTheme as getSelectListTheme3 } from "@earendil-works/pi-coding-agent";
927
+ import { Container, Markdown as Markdown2, Spacer, Text } from "@earendil-works/pi-tui";
928
+ var CampUiAssistantMessage = class extends Container {
929
+ constructor(message) {
930
+ super();
931
+ const markdownTheme = getMarkdownTheme2();
932
+ const selectTheme = getSelectListTheme3();
933
+ const visible = message.content.filter(
934
+ (content) => content.kind !== "tool-call" && content.kind !== "image"
935
+ );
936
+ if (visible.length > 0) this.addChild(new Spacer(1));
937
+ for (const [index, content] of visible.entries()) {
938
+ if (content.kind === "text" && content.text.trim() !== "") {
939
+ this.addChild(new Markdown2(content.text.trim(), 1, 0, markdownTheme));
940
+ }
941
+ if (content.kind === "thinking" && content.text.trim() !== "") {
942
+ this.addChild(
943
+ new Markdown2(content.text.trim(), 1, 0, markdownTheme, {
944
+ color: selectTheme.description,
945
+ italic: true
946
+ })
947
+ );
948
+ }
949
+ if (content.kind === "unknown") {
950
+ this.addChild(
951
+ new Markdown2(
952
+ `Unknown content
953
+
954
+ \`\`\`json
955
+ ${jsonText(content.value)}
956
+ \`\`\``,
957
+ 1,
958
+ 0,
959
+ markdownTheme
960
+ )
961
+ );
962
+ }
963
+ if (index < visible.length - 1) this.addChild(new Spacer(1));
964
+ }
965
+ for (const content of message.content) {
966
+ if (content.kind === "image") {
967
+ this.addChild(new Text(selectTheme.description(`[Image: ${content.mimeType}]`), 1, 0));
968
+ }
969
+ }
970
+ if (message.errorMessage !== void 0) {
971
+ this.addChild(new Spacer(1));
972
+ this.addChild(new Text(selectTheme.noMatch(`Error: ${message.errorMessage}`), 1, 0));
973
+ }
974
+ }
975
+ };
976
+
977
+ // packages/host-ui/src/pi/blocks/campUiComposer.ts
978
+ import { getMarkdownTheme as getMarkdownTheme3, getSelectListTheme as getSelectListTheme4 } from "@earendil-works/pi-coding-agent";
979
+ import { Editor, Key, matchesKey } from "@earendil-works/pi-tui";
980
+ var makeCampUiComposerComponent = (context, tui) => {
981
+ const markdownTheme = getMarkdownTheme3();
982
+ const selectTheme = getSelectListTheme4();
983
+ const editors = /* @__PURE__ */ new Map();
984
+ const makeEditor = (workerId) => {
985
+ let handlingSubmit = false;
986
+ let syncing = false;
987
+ const editor = new Editor(
988
+ tui,
989
+ { borderColor: markdownTheme.codeBlockBorder, selectList: getSelectListTheme4() },
990
+ { paddingX: 1 }
991
+ );
992
+ editor.onChange = (draft) => {
993
+ if (!syncing && !(handlingSubmit && draft === "")) {
994
+ context.emit(CampUiIntents.SetDraft({ workerId, draft }));
995
+ }
996
+ };
997
+ editor.onSubmit = (text) => {
998
+ if (text.trim() === "") return;
999
+ editor.addToHistory(text);
1000
+ context.emit(CampUiIntents.Steer({ workerId, message: text }));
1001
+ };
1002
+ return {
1003
+ editor,
1004
+ handleInput: (input) => {
1005
+ handlingSubmit = matchesKey(input, Key.enter);
1006
+ editor.handleInput(input);
1007
+ handlingSubmit = false;
1008
+ },
1009
+ setDraft: (draft) => {
1010
+ if (draft === editor.getText()) return;
1011
+ syncing = true;
1012
+ editor.setText(draft);
1013
+ syncing = false;
1014
+ }
1015
+ };
1016
+ };
1017
+ const selectedEditor = () => {
1018
+ const workerId = context.readModel().state.selectedWorkerId;
1019
+ if (workerId === void 0) return void 0;
1020
+ const existing = editors.get(workerId);
1021
+ if (existing !== void 0) return existing;
1022
+ const created = makeEditor(workerId);
1023
+ editors.set(workerId, created);
1024
+ return created;
1025
+ };
1026
+ return {
1027
+ focusable: true,
1028
+ render: (viewport) => {
1029
+ const model = context.readModel();
1030
+ const workerId = model.state.selectedWorkerId;
1031
+ const entry = selectedEditor();
1032
+ if (workerId === void 0 || entry === void 0) {
1033
+ return [selectTheme.description("worker not selected")];
1034
+ }
1035
+ const { editor } = entry;
1036
+ editor.focused = model.state.focusedSlotId === CampUiSlot.composer;
1037
+ editor.borderColor = editor.focused ? selectTheme.selectedPrefix : markdownTheme.codeBlockBorder;
1038
+ entry.setDraft(model.state.drafts.get(workerId) ?? "");
1039
+ return editor.render(viewport.width).slice(0, viewport.height);
1040
+ },
1041
+ handleInput: (input) => {
1042
+ const entry = selectedEditor();
1043
+ if (entry === void 0) return false;
1044
+ const { editor } = entry;
1045
+ const autocompleteVisible = editor.isShowingAutocomplete();
1046
+ if (matchesKey(input, Key.escape) || matchesKey(input, Key.tab) || matchesKey(input, "shift+tab")) {
1047
+ if (!autocompleteVisible) return false;
1048
+ entry.handleInput(input);
1049
+ return true;
1050
+ }
1051
+ const arrow = matchesKey(input, Key.left) || matchesKey(input, Key.right) || matchesKey(input, Key.up) || matchesKey(input, Key.down);
1052
+ if (arrow) {
1053
+ const beforeCursor = editor.getCursor();
1054
+ const beforeText = editor.getText();
1055
+ entry.handleInput(input);
1056
+ const afterCursor = editor.getCursor();
1057
+ return autocompleteVisible || beforeText !== editor.getText() || beforeCursor.line !== afterCursor.line || beforeCursor.col !== afterCursor.col;
1058
+ }
1059
+ entry.handleInput(input);
1060
+ return true;
1061
+ },
1062
+ handlePointer: (pointer) => pointer.action === "press" && pointer.button === "left" ? [CampUiIntents.FocusSlot({ slotId: CampUiSlot.composer })] : [],
1063
+ invalidate: () => {
1064
+ for (const { editor } of editors.values()) editor.invalidate();
1065
+ }
1066
+ };
1067
+ };
1068
+
1069
+ // packages/host-ui/src/pi/blocks/campUiDetail.ts
1070
+ import { getSelectListTheme as getSelectListTheme7 } from "@earendil-works/pi-coding-agent";
1071
+ import { Key as Key2, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
1072
+
1073
+ // packages/host-ui/src/pi/blocks/campUiEquipmentBlock.ts
1074
+ import { getMarkdownTheme as getMarkdownTheme4, UserMessageComponent } from "@earendil-works/pi-coding-agent";
1075
+ var escapeMarkdown = (value) => value.replaceAll(/([\\`*_[\]{}()#+.!|>])/gu, "\\$1");
1076
+ var renderNames = (names) => names.length === 0 ? "none" : names.map(escapeMarkdown).join(", ");
1077
+ var renderMcpServers = (equipment) => renderNames(
1078
+ equipment.mcp.serverNames.map((serverName) => {
1079
+ const toolNames = equipment.mcp.toolNamesByServer?.[serverName];
1080
+ return toolNames === void 0 ? serverName : `${serverName} (${toolNames.join(", ")})`;
1081
+ })
1082
+ );
1083
+ var equipmentMarkdown = (equipment) => {
1084
+ const role = equipment.role ?? equipment.preset ?? "worker";
1085
+ const tools = [.../* @__PURE__ */ new Set([...equipment.builtinTools, ...equipment.customToolNames])].sort();
1086
+ const lines = [
1087
+ `**Equipment \xB7 ${escapeMarkdown(role)}**`,
1088
+ "",
1089
+ `- **Tools:** ${renderNames(tools)}`,
1090
+ `- **MCP:** ${renderMcpServers(equipment)}`,
1091
+ `- **Skills:** ${renderNames(equipment.skillNames)}`,
1092
+ `- **Access:** ${equipment.readOnly ? "read-only" : "read/write"}`
1093
+ ];
1094
+ return lines.join("\n");
1095
+ };
1096
+ var campUiEquipmentBlockRenderer = {
1097
+ id: "role-equipment",
1098
+ order: -100,
1099
+ render: ({ worker, width }) => [
1100
+ {
1101
+ id: "role-equipment",
1102
+ lines: new UserMessageComponent(
1103
+ equipmentMarkdown(worker.snapshot.worker.equipment),
1104
+ getMarkdownTheme4(),
1105
+ 0
1106
+ ).render(width)
1107
+ }
1108
+ ]
1109
+ };
1110
+
1111
+ // packages/host-ui/src/pi/blocks/campUiTranscriptBlocks.ts
1112
+ import { getMarkdownTheme as getMarkdownTheme7, UserMessageComponent as UserMessageComponent2 } from "@earendil-works/pi-coding-agent";
1113
+ import { Markdown as Markdown4 } from "@earendil-works/pi-tui";
1114
+
1115
+ // packages/host-ui/src/pi/shared/campUiPanel.ts
1116
+ import { getMarkdownTheme as getMarkdownTheme5, getSelectListTheme as getSelectListTheme5 } from "@earendil-works/pi-coding-agent";
1117
+ import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
1118
+ var rule = (start, label, width, borderColor, labelColor) => {
1119
+ const prefix = `${borderColor(`${start}\u2500 `)}${labelColor(label)}${borderColor(" ")}`;
1120
+ const truncated = truncateToWidth3(prefix, width);
1121
+ return `${truncated}${borderColor("\u2500".repeat(Math.max(0, width - visibleWidth2(truncated))))}`;
1122
+ };
1123
+ var renderCampUiPanel = (label, body, width) => {
1124
+ if (width <= 0) return [];
1125
+ const borderColor = getMarkdownTheme5().codeBlockBorder;
1126
+ const labelColor = getSelectListTheme5().selectedText;
1127
+ if (width === 1) {
1128
+ return [borderColor("\u2502"), ...body.map(() => borderColor("\u2502")), borderColor("\u2570")];
1129
+ }
1130
+ return [
1131
+ rule("\u256D", label, width, borderColor, labelColor),
1132
+ ...body.map((line) => fitCampUiLine(`${borderColor("\u2502")} ${line}`, width)),
1133
+ borderColor(`\u2570${"\u2500".repeat(width - 1)}`)
1134
+ ];
1135
+ };
1136
+
1137
+ // packages/host-ui/src/pi/blocks/campUiToolResult.ts
1138
+ import {
1139
+ getMarkdownTheme as getMarkdownTheme6,
1140
+ getSelectListTheme as getSelectListTheme6,
1141
+ truncateToVisualLines
1142
+ } from "@earendil-works/pi-coding-agent";
1143
+ import { Markdown as Markdown3 } from "@earendil-works/pi-tui";
1144
+ var CAMP_UI_DEFAULT_TOOL_PREVIEW_LINES = 12;
1145
+ var CAMP_UI_BASH_PREVIEW_LINES = 8;
1146
+ var previewLinesByTool = /* @__PURE__ */ new Map([["bash", CAMP_UI_BASH_PREVIEW_LINES]]);
1147
+ var toolResultContent = (result, markdown) => result.content.flatMap((content) => {
1148
+ if (content.kind === "text" || content.kind === "thinking") return [content.text];
1149
+ if (content.kind === "image") return [`[Image: ${content.mimeType}]`];
1150
+ if (content.kind === "tool-call") {
1151
+ return [
1152
+ markdown ? `**Tool: ${content.toolName}**
1153
+
1154
+ \`\`\`json
1155
+ ${jsonText(content.arguments)}
1156
+ \`\`\`` : jsonText(content.arguments)
1157
+ ];
1158
+ }
1159
+ return [
1160
+ markdown ? `**Unknown content**
1161
+
1162
+ \`\`\`json
1163
+ ${jsonText(content.value)}
1164
+ \`\`\`` : jsonText(content.value)
1165
+ ];
1166
+ }).join(markdown ? "\n\n" : "\n");
1167
+ var toolResultPlainText = (result) => [
1168
+ toolResultContent(result, false),
1169
+ ...result.errorMessage === void 0 ? [] : [`Error: ${result.errorMessage}`],
1170
+ ...result.toolResultDetails === void 0 ? [] : [`Details
1171
+ ${jsonText(result.toolResultDetails)}`]
1172
+ ].filter((part) => part !== "").join("\n");
1173
+ var renderCampUiToolResult = (result, width, expanded) => {
1174
+ const boundedWidth = Math.max(1, width);
1175
+ const plainText = toolResultPlainText(result);
1176
+ const preview = truncateToVisualLines(
1177
+ getSelectListTheme6().description(plainText),
1178
+ previewLinesByTool.get(result.toolName ?? "") ?? CAMP_UI_DEFAULT_TOOL_PREVIEW_LINES,
1179
+ boundedWidth
1180
+ );
1181
+ if (!expanded) {
1182
+ return [
1183
+ ...preview.visualLines,
1184
+ ...preview.skippedCount === 0 ? [] : [
1185
+ getSelectListTheme6().description(
1186
+ `... ${preview.skippedCount} more lines \xB7 enter expand`
1187
+ )
1188
+ ]
1189
+ ];
1190
+ }
1191
+ const content = toolResultContent(result, true);
1192
+ const lines = content === "" ? [] : new Markdown3(content, 0, 0, getMarkdownTheme6()).render(boundedWidth);
1193
+ return [
1194
+ ...lines,
1195
+ ...result.errorMessage === void 0 ? [] : [getSelectListTheme6().noMatch(`Error: ${result.errorMessage}`)],
1196
+ ...result.toolResultDetails === void 0 ? [] : [
1197
+ getSelectListTheme6().description("Details"),
1198
+ ...new Markdown3(jsonText(result.toolResultDetails), 0, 0, getMarkdownTheme6()).render(
1199
+ boundedWidth
1200
+ )
1201
+ ],
1202
+ ...preview.skippedCount === 0 ? [] : [getSelectListTheme6().description("enter collapse")]
1203
+ ];
1204
+ };
1205
+
1206
+ // packages/host-ui/src/pi/blocks/campUiTranscriptBlocks.ts
1207
+ var contentMarkdown = (message, includeToolCalls) => message.content.flatMap((content) => {
1208
+ if (content.kind === "text") return [content.text];
1209
+ if (content.kind === "thinking") {
1210
+ return [`> Thinking
1211
+ > ${content.text.replaceAll("\n", "\n> ")}`];
1212
+ }
1213
+ if (content.kind === "image") return [`[Image: ${content.mimeType}]`];
1214
+ if (content.kind === "tool-call") {
1215
+ return includeToolCalls ? [`**Tool: ${content.toolName}**
1216
+
1217
+ \`\`\`json
1218
+ ${jsonText(content.arguments)}
1219
+ \`\`\``] : [];
1220
+ }
1221
+ return [`**Unknown content**
1222
+
1223
+ \`\`\`json
1224
+ ${jsonText(content.value)}
1225
+ \`\`\``];
1226
+ }).join("\n\n");
1227
+ var renderMessage = (message, width) => {
1228
+ if (message.role === "user") {
1229
+ const markdown2 = contentMarkdown(message, false);
1230
+ if (markdown2 === "") return [];
1231
+ const attribution = message.source?.kind === "controller" ? [
1232
+ styleCampUiTone(
1233
+ CampUiTone.external,
1234
+ `controller ${message.source.controllerId}${message.source.actorId === void 0 ? "" : ` \xB7 ${message.source.actorId}`}`
1235
+ )
1236
+ ] : [];
1237
+ return [
1238
+ ...attribution,
1239
+ ...new UserMessageComponent2(markdown2, getMarkdownTheme7(), 0).render(width)
1240
+ ];
1241
+ }
1242
+ if (message.role === "assistant") {
1243
+ return new CampUiAssistantMessage(message).render(width);
1244
+ }
1245
+ const markdown = contentMarkdown(message, false);
1246
+ if (markdown === "") return [];
1247
+ const prefix = message.role === "assistant" ? "" : `${message.role}
1248
+
1249
+ `;
1250
+ const suffix = message.errorMessage === void 0 ? "" : `
1251
+
1252
+ Error: ${message.errorMessage}`;
1253
+ return new Markdown4(`${prefix}${markdown}${suffix}`, 0, 0, getMarkdownTheme7()).render(width);
1254
+ };
1255
+ var renderMarkdown = (markdown, width) => new Markdown4(markdown, 0, 0, getMarkdownTheme7()).render(Math.max(1, width));
1256
+ var firstStringArgument = (parameters, names) => {
1257
+ for (const name of names) {
1258
+ const value = parameters[name];
1259
+ if (typeof value === "string" && value.trim() !== "") return value;
1260
+ }
1261
+ return void 0;
1262
+ };
1263
+ var toolArgumentSummary = (toolName, parameters) => {
1264
+ const namesByTool = /* @__PURE__ */ new Map([
1265
+ ["bash", ["command"]],
1266
+ ["edit", ["path", "file_path"]],
1267
+ ["find", ["pattern", "path"]],
1268
+ ["grep", ["pattern", "path"]],
1269
+ ["ls", ["path"]],
1270
+ ["read", ["path", "file_path"]],
1271
+ ["write", ["path", "file_path"]]
1272
+ ]);
1273
+ return firstStringArgument(parameters, namesByTool.get(toolName) ?? []) ?? jsonText(parameters);
1274
+ };
1275
+ var renderToolCall = (toolCall, result, width, expanded) => {
1276
+ const innerWidth = Math.max(1, width - 2);
1277
+ const status = result === void 0 ? "running" : result.isError === true ? "failed" : "completed";
1278
+ const body = [
1279
+ ...renderMarkdown(toolArgumentSummary(toolCall.toolName, toolCall.arguments), innerWidth),
1280
+ ...result === void 0 ? [] : renderCampUiToolResult(result, innerWidth, expanded)
1281
+ ];
1282
+ return renderCampUiPanel(`${toolCall.toolName} \xB7 ${status}`, body, width);
1283
+ };
1284
+ var renderUnmatchedToolResult = (message, width, expanded) => {
1285
+ const status = message.isError === true ? "failed" : "completed";
1286
+ const name = message.toolName ?? "unknown";
1287
+ return renderCampUiPanel(
1288
+ `${name} \xB7 ${status}`,
1289
+ renderCampUiToolResult(message, Math.max(1, width - 2), expanded),
1290
+ width
1291
+ );
1292
+ };
1293
+ var renderTranscript = (transcript, width, expandedDetailBlockIds) => {
1294
+ const results = /* @__PURE__ */ new Map();
1295
+ const callIds = /* @__PURE__ */ new Set();
1296
+ for (const message of transcript) {
1297
+ if (message.role === "toolResult" && message.toolCallId !== void 0) {
1298
+ results.set(message.toolCallId, message);
1299
+ }
1300
+ for (const content of message.content) {
1301
+ if (content.kind === "tool-call") callIds.add(content.toolCallId);
1302
+ }
1303
+ }
1304
+ return transcript.flatMap((message, index) => {
1305
+ const messageId = message.id ?? String(index);
1306
+ if (message.role === "toolResult") {
1307
+ const blockId = `tool-result:${messageId}`;
1308
+ return message.toolCallId !== void 0 && callIds.has(message.toolCallId) ? [] : [
1309
+ {
1310
+ id: blockId,
1311
+ lines: renderUnmatchedToolResult(message, width, expandedDetailBlockIds.has(blockId)),
1312
+ toggleable: true
1313
+ }
1314
+ ];
1315
+ }
1316
+ const blocks = [];
1317
+ const messageLines = renderMessage(message, width);
1318
+ if (messageLines.length > 0) blocks.push({ id: `message:${messageId}`, lines: messageLines });
1319
+ for (const content of message.content) {
1320
+ if (content.kind !== "tool-call") continue;
1321
+ const blockId = `tool-call:${content.toolCallId}`;
1322
+ blocks.push({
1323
+ id: blockId,
1324
+ lines: renderToolCall(
1325
+ content,
1326
+ results.get(content.toolCallId),
1327
+ width,
1328
+ expandedDetailBlockIds.has(blockId)
1329
+ ),
1330
+ toggleable: results.has(content.toolCallId)
1331
+ });
1332
+ }
1333
+ return blocks;
1334
+ });
1335
+ };
1336
+ var campUiTranscriptBlockRenderer = {
1337
+ id: "transcript",
1338
+ order: 0,
1339
+ render: ({ model, worker, width }) => renderTranscript(
1340
+ worker.snapshot.session?.transcript ?? [],
1341
+ width,
1342
+ model.state.expandedDetailBlockIds
1343
+ )
1344
+ };
1345
+
1346
+ // packages/host-ui/src/pi/blocks/campUiDetail.ts
1347
+ var defaultCampUiDetailBlockRenderers = [
1348
+ campUiEquipmentBlockRenderer,
1349
+ campUiTranscriptBlockRenderer,
1350
+ campUiPendingAskBlockRenderer
1351
+ ];
1352
+ var sameCampUiDetailBlockSet = (left, right) => left.size === right.size && [...left].every((blockId) => right.has(blockId));
1353
+ var makeCampUiDetailComponent = (context, registry = defaultCampUiDetailBlockRenderers) => {
1354
+ const renderState = /* @__PURE__ */ new Map();
1355
+ const detailCache = /* @__PURE__ */ new Map();
1356
+ let renderedRanges = [];
1357
+ let visibleRanges = [];
1358
+ return {
1359
+ focusable: true,
1360
+ render: (viewport) => {
1361
+ renderedRanges = [];
1362
+ visibleRanges = [];
1363
+ const model = context.readModel();
1364
+ const selectTheme = getSelectListTheme7();
1365
+ const workerId = model.state.selectedWorkerId;
1366
+ if (workerId === void 0) return [selectTheme.description("No worker selected")];
1367
+ const worker = findCampUiWorker(model.snapshot, workerId);
1368
+ if (worker === void 0) return [selectTheme.noMatch("Selected worker is unavailable")];
1369
+ if (worker.error !== void 0) {
1370
+ return [selectTheme.noMatch(`Snapshot error: ${worker.error}`)];
1371
+ }
1372
+ const cached = detailCache.get(workerId);
1373
+ const detail = cached !== void 0 && cached.worker === worker && cached.width === viewport.width && cached.expandedDetailBlockIds === model.state.expandedDetailBlockIds && cached.presentationNow === model.state.presentationNow ? cached.detail : renderCampUiDetailBlockLayout(registry, {
1374
+ model,
1375
+ worker,
1376
+ width: viewport.width
1377
+ });
1378
+ if (detail !== cached?.detail) {
1379
+ detailCache.set(workerId, {
1380
+ worker,
1381
+ width: viewport.width,
1382
+ expandedDetailBlockIds: model.state.expandedDetailBlockIds,
1383
+ presentationNow: model.state.presentationNow,
1384
+ detail
1385
+ });
1386
+ }
1387
+ renderedRanges = detail.ranges.filter((range) => range.toggleable);
1388
+ const focusedRange = model.state.focusedSlotId === CampUiSlot.transcript ? renderedRanges.find((range) => range.id === model.state.focusedDetailBlockId) : void 0;
1389
+ const focusedLine = focusedRange === void 0 ? void 0 : Array.from(
1390
+ { length: focusedRange.end - focusedRange.start },
1391
+ (_, index) => focusedRange.start + index
1392
+ ).find((index) => hasCampUiTextContent(detail.lines[index] ?? "")) ?? focusedRange.start;
1393
+ const lines = focusedRange === void 0 ? detail.lines : detail.lines.map(
1394
+ (line, index) => index === focusedLine ? fitCampUiLine(
1395
+ `${styleCampUiTone(CampUiTone.active, `${CampUiGlyph.active} `)}${line}`,
1396
+ viewport.width
1397
+ ) : line
1398
+ );
1399
+ const previous = renderState.get(workerId);
1400
+ const offset = model.state.scrollOffsets.get(workerId) ?? 0;
1401
+ const growth = previous !== void 0 && previous.width === viewport.width && sameCampUiDetailBlockSet(
1402
+ previous.expandedDetailBlockIds,
1403
+ model.state.expandedDetailBlockIds
1404
+ ) ? Math.max(0, lines.length - previous.lineCount) : 0;
1405
+ const focusedOffset = focusedRange === void 0 ? void 0 : Math.max(
1406
+ 0,
1407
+ lines.length - Math.min(lines.length, focusedRange.start + viewport.height)
1408
+ );
1409
+ const maximumOffset = Math.max(0, lines.length - viewport.height);
1410
+ const desiredOffset = focusedOffset ?? (offset > 0 ? offset + growth : 0);
1411
+ const lockedOffset = Math.min(maximumOffset, desiredOffset);
1412
+ if (lockedOffset !== offset) {
1413
+ context.emit(CampUiIntents.SetScrollOffset({ workerId, offset: lockedOffset }));
1414
+ }
1415
+ renderState.set(workerId, {
1416
+ width: viewport.width,
1417
+ lineCount: lines.length,
1418
+ expandedDetailBlockIds: model.state.expandedDetailBlockIds
1419
+ });
1420
+ if (lines.length === 0) return [selectTheme.description("Waiting for worker output...")];
1421
+ const end = Math.max(0, lines.length - lockedOffset);
1422
+ const start = Math.max(0, end - viewport.height);
1423
+ visibleRanges = renderedRanges.filter((range) => range.end > start && range.start < end).map((range) => ({
1424
+ blockId: range.id,
1425
+ start: Math.max(0, range.start - start),
1426
+ end: Math.min(end - start, range.end - start)
1427
+ }));
1428
+ return lines.slice(start, end);
1429
+ },
1430
+ handleInput: (input) => {
1431
+ const delta = matchesKey2(input, Key2.up) ? -1 : matchesKey2(input, Key2.down) ? 1 : void 0;
1432
+ if (delta === void 0 || renderedRanges.length === 0) return false;
1433
+ const focusedBlockId = context.readModel().state.focusedDetailBlockId;
1434
+ const current = renderedRanges.findIndex((range) => range.id === focusedBlockId);
1435
+ const next = current < 0 ? delta < 0 ? renderedRanges.length - 1 : 0 : current + delta;
1436
+ const target = renderedRanges[next];
1437
+ if (target === void 0) return false;
1438
+ context.emit(CampUiIntents.FocusDetailBlock({ blockId: target.id }));
1439
+ return true;
1440
+ },
1441
+ handlePointer: (pointer) => {
1442
+ const workerId = context.readModel().state.selectedWorkerId;
1443
+ if (workerId === void 0) return [];
1444
+ if (pointer.action === "wheel") {
1445
+ return [
1446
+ CampUiIntents.FocusSlot({ slotId: CampUiSlot.transcript }),
1447
+ CampUiIntents.ScrollWorker({
1448
+ workerId,
1449
+ delta: pointer.direction === "up" ? 3 : -3
1450
+ })
1451
+ ];
1452
+ }
1453
+ if (pointer.action !== "press" || pointer.button !== "left") return [];
1454
+ const target = visibleRanges.find(
1455
+ (range) => pointer.y >= range.start && pointer.y < range.end
1456
+ );
1457
+ return target === void 0 ? [CampUiIntents.FocusSlot({ slotId: CampUiSlot.transcript })] : [
1458
+ CampUiIntents.FocusSlot({ slotId: CampUiSlot.transcript }),
1459
+ CampUiIntents.FocusDetailBlock({ blockId: target.blockId }),
1460
+ CampUiIntents.ToggleDetailBlock({ blockId: target.blockId })
1461
+ ];
1462
+ }
1463
+ };
1464
+ };
1465
+
1466
+ // packages/host-ui/src/pi/layout/campUiChrome.ts
1467
+ import { getSelectListTheme as getSelectListTheme8 } from "@earendil-works/pi-coding-agent";
1468
+ var makeCampUiCampHeader = (context) => ({
1469
+ render: (viewport) => {
1470
+ const snapshot = context.readModel().snapshot;
1471
+ const theme = getSelectListTheme8();
1472
+ const mode = snapshot.placement === "external" ? "agentless" : "foreman";
1473
+ return [
1474
+ fitCampUiLine(theme.description(snapshot.displayName), viewport.width),
1475
+ fitCampUiLine(
1476
+ `${theme.description(mode)} ${theme.description("\xB7")} ${renderCampUiState(snapshot.health.status)}`,
1477
+ viewport.width
1478
+ )
1479
+ ];
1480
+ }
1481
+ });
1482
+ var makeCampUiSeat = (context) => ({
1483
+ render: (viewport) => {
1484
+ const snapshot = context.readModel().snapshot;
1485
+ const theme = getSelectListTheme8();
1486
+ const external = snapshot.placement === "external";
1487
+ const controller = snapshot.controllerConnected ? renderCampUiStatus("live", CampUiTone.success, CampUiGlyph.live) : renderCampUiStatus("lost", CampUiTone.error);
1488
+ return [
1489
+ fitCampUiLine(
1490
+ external ? `${renderCampUiStatus("controller", CampUiTone.external)} ${theme.description("\xB7 external \xB7")} ${controller}` : `${renderCampUiStatus("foreman", CampUiTone.neutral)} ${theme.description("\xB7 session not reported \xB7")} ${controller}`,
1491
+ viewport.width
1492
+ )
1493
+ ];
1494
+ }
1495
+ });
1496
+ var makeCampUiRoster = (context) => ({
1497
+ focusable: true,
1498
+ render: (viewport) => {
1499
+ const model = context.readModel();
1500
+ const theme = getSelectListTheme8();
1501
+ const focused = model.state.focusedSlotId === CampUiSlot.roster;
1502
+ if (model.snapshot.workers.length === 0) {
1503
+ return [theme.description("No workers")];
1504
+ }
1505
+ const workers = model.snapshot.workers.map((entry) => {
1506
+ const worker = entry.snapshot.worker;
1507
+ const selected = worker.id === model.state.selectedWorkerId;
1508
+ const name = worker.alias ?? worker.id.slice(0, 8);
1509
+ const session = entry.snapshot.session;
1510
+ const active = uniqueCampUiNames([
1511
+ ...session?.activeToolNames ?? [],
1512
+ ...session?.pendingToolNames ?? []
1513
+ ]);
1514
+ const pendingAsks = entry.asks.filter((ask) => ask.state === "pending").length;
1515
+ const tool = active.length === 0 ? "" : styleCampUiTone(CampUiTone.active, ` ${active.slice(0, 2).join(", ")}`);
1516
+ const label = selected ? theme.selectedText(name) : name;
1517
+ const role = theme.description(`(${worker.equipment.role ?? "worker"})`);
1518
+ const status = renderCampUiWorkerState(worker.state, {
1519
+ failed: entry.error !== void 0,
1520
+ pendingAsks,
1521
+ streaming: session?.isStreaming === true
1522
+ });
1523
+ const line = `${status} ${label} ${role}${tool}`;
1524
+ return selected && focused ? renderSelectedCampUiRow(line, viewport.width) : fitCampUiLine(line, viewport.width);
1525
+ });
1526
+ return workers;
1527
+ },
1528
+ handlePointer: (pointer) => {
1529
+ if (pointer.action !== "press" || pointer.button !== "left") return [];
1530
+ const workerId = context.readModel().snapshot.workers[pointer.y]?.snapshot.worker.id;
1531
+ if (workerId === void 0) return [];
1532
+ return [
1533
+ CampUiIntents.FocusSlot({ slotId: CampUiSlot.roster }),
1534
+ pointer.mode === "narrow" ? CampUiIntents.OpenWorker({ workerId }) : CampUiIntents.SelectWorker({ workerId })
1535
+ ];
1536
+ }
1537
+ });
1538
+ var makeCampUiSessionHeader = (context) => ({
1539
+ render: (viewport) => {
1540
+ const model = context.readModel();
1541
+ const theme = getSelectListTheme8();
1542
+ const workerId = model.state.selectedWorkerId;
1543
+ if (workerId === void 0) {
1544
+ return [fitCampUiLine(theme.description("Select a worker"), viewport.width)];
1545
+ }
1546
+ const worker = findCampUiWorker(model.snapshot, workerId);
1547
+ if (worker === void 0) return [theme.noMatch("Worker unavailable")];
1548
+ const workerName = worker.snapshot.worker.alias ?? workerId.slice(0, 8);
1549
+ const role = worker.snapshot.worker.equipment.role ?? "worker";
1550
+ const line = `${theme.selectedText(`camp / ${workerName}`)} ${theme.description(`\xB7 ${role} \xB7 session ${worker.snapshot.worker.sessionState}`)}`;
1551
+ return [fitCampUiLine(line, viewport.width)];
1552
+ },
1553
+ handlePointer: (pointer) => pointer.action === "press" && pointer.button === "left" && pointer.y === 0 ? [
1554
+ CampUiIntents.ActivateScreen({ screen: CampUiScreen.camp }),
1555
+ CampUiIntents.FocusDetailBlock({}),
1556
+ CampUiIntents.FocusSlot({ slotId: CampUiSlot.roster })
1557
+ ] : []
1558
+ });
1559
+
1560
+ // packages/host-ui/src/pi/layout/campUiFooter.ts
1561
+ import { getMarkdownTheme as getMarkdownTheme8, getSelectListTheme as getSelectListTheme9 } from "@earendil-works/pi-coding-agent";
1562
+ var aggregateCampUsage = (workers) => {
1563
+ const usages = workers.flatMap(
1564
+ (worker) => worker.snapshot.session?.usage === void 0 ? [] : [worker.snapshot.session.usage]
1565
+ );
1566
+ return {
1567
+ cost: usages.reduce((total, usage) => total + usage.cost, 0),
1568
+ reportedWorkers: usages.length,
1569
+ tokens: usages.reduce((total, usage) => total + usage.tokens.total, 0),
1570
+ totalWorkers: workers.length
1571
+ };
1572
+ };
1573
+ var aggregateUsageText = (usage) => {
1574
+ if (usage.totalWorkers > 0 && usage.reportedWorkers === 0) {
1575
+ return "tokens unknown \xB7 cost unknown";
1576
+ }
1577
+ const partial = usage.reportedWorkers < usage.totalWorkers;
1578
+ const lowerBound = partial ? "\u2265" : "";
1579
+ const coverage = partial ? ` \xB7 ${usage.reportedWorkers}/${usage.totalWorkers} reported` : "";
1580
+ return `\u2193 ${lowerBound}${compactNumber(usage.tokens)} tokens \xB7 ${lowerBound}$${usage.cost.toFixed(3)}${coverage}`;
1581
+ };
1582
+ var footerDivider = (width) => fitCampUiLine(getMarkdownTheme8().codeBlockBorder("\u2500".repeat(width)), width);
1583
+ var retryText = (retryAt, now) => {
1584
+ if (retryAt === void 0) return "";
1585
+ return `retry in ${Math.max(0, Math.ceil((retryAt - now) / 1e3))}s`;
1586
+ };
1587
+ var diagnosticText = (value) => {
1588
+ const normalized = value?.replaceAll(/\s+/gu, " ").trim();
1589
+ return normalized === "" ? void 0 : normalized;
1590
+ };
1591
+ var makeCampUiCampFooter = (context) => ({
1592
+ render: (viewport) => {
1593
+ const model = context.readModel();
1594
+ const theme = getSelectListTheme9();
1595
+ const health = model.snapshot.health;
1596
+ const usage = aggregateUsageText(aggregateCampUsage(model.snapshot.workers));
1597
+ const actionError = diagnosticText(model.state.lastActionError);
1598
+ const healthSummary = diagnosticText(health.message);
1599
+ const healthMessage = health.code === void 0 ? healthSummary : healthSummary === void 0 ? health.code : `${health.code} \xB7 ${healthSummary}`;
1600
+ const healthTone = health.status === CampUiHealthStatus.failed ? CampUiTone.error : CampUiTone.attention;
1601
+ 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);
1602
+ const retry = retryText(health.retryAt, model.state.presentationNow);
1603
+ return [
1604
+ footerDivider(viewport.width),
1605
+ joinCampUiColumns(diagnostic, theme.description(usage), viewport.width),
1606
+ fitCampUiLine(retry === "" ? "" : theme.description(retry), viewport.width)
1607
+ ];
1608
+ }
1609
+ });
1610
+ var makeCampUiSessionFooter = (context) => ({
1611
+ render: (viewport) => {
1612
+ const model = context.readModel();
1613
+ const theme = getSelectListTheme9();
1614
+ const workerId = model.state.selectedWorkerId;
1615
+ const worker = workerId === void 0 ? void 0 : findCampUiWorker(model.snapshot, workerId);
1616
+ const usage = worker?.snapshot.session?.usage;
1617
+ const tokenText = usage === void 0 ? "tokens unknown" : `\u2193 ${compactNumber(usage.tokens.total)} tokens`;
1618
+ const costText = usage === void 0 ? "cost unknown" : `$${usage.cost.toFixed(3)}`;
1619
+ const cacheText = usage?.latestCacheHitRate === void 0 ? usage === void 0 || usage.tokens.total === 0 ? "cache n/a" : "cache not reported" : `cache ${usage.latestCacheHitRate.toFixed(0)}%`;
1620
+ const contextText = usage?.context?.percent === null || usage?.context?.percent === void 0 ? "context unknown" : `context ${usage.context.percent.toFixed(0)}%`;
1621
+ const modelText = usage?.model ?? worker?.snapshot.worker.equipment.model ?? "model unknown";
1622
+ const thinking = worker?.snapshot.session?.thinkingLevel ?? worker?.snapshot.worker.equipment.thinkingLevel ?? "unknown";
1623
+ 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}`)}`;
1624
+ const branch = worker === void 0 || model.snapshot.projectBranch === null ? "" : theme.description(`branch ${model.snapshot.projectBranch}`);
1625
+ return [
1626
+ footerDivider(viewport.width),
1627
+ branch === "" ? fitCampUiLine(identity, viewport.width) : joinCampUiColumns(identity, branch, viewport.width),
1628
+ joinCampUiColumns(
1629
+ theme.description(`${tokenText} \xB7 ${costText} \xB7 ${cacheText} \xB7 ${contextText}`),
1630
+ theme.description(`${modelText} \xB7 effort ${thinking}`),
1631
+ viewport.width
1632
+ )
1633
+ ];
1634
+ }
1635
+ });
1636
+
1637
+ // packages/host-ui/src/pi/layout/campUiResources.ts
1638
+ import { getSelectListTheme as getSelectListTheme10 } from "@earendil-works/pi-coding-agent";
1639
+ var renderResourceTree = (label, value, tone, width) => {
1640
+ const theme = getSelectListTheme10();
1641
+ const lines = [
1642
+ theme.description(label),
1643
+ `${theme.description("\u2514\u2500")} ${renderCampUiStatus(value, tone)}`
1644
+ ];
1645
+ return lines.map((line) => fitCampUiLine(line, width));
1646
+ };
1647
+ var makeCampUiResources = (context) => ({
1648
+ render: (viewport) => {
1649
+ const model = context.readModel();
1650
+ const snapshot = model.snapshot;
1651
+ const equipment = snapshot.workers.map((worker) => worker.snapshot.worker.equipment);
1652
+ const inventory = snapshot.resources;
1653
+ const resourcesReported = inventory.reportedWorkers > 0;
1654
+ const equipmentReported = equipment.length > 0;
1655
+ const partial = !inventory.complete;
1656
+ const resourceText = (items) => {
1657
+ if (!resourcesReported) return "not reported";
1658
+ const names = uniqueCampUiNames(items.map((item) => item.name));
1659
+ const value = names.length === 0 ? "none loaded" : names.join(", ");
1660
+ return partial ? `${value} \xB7 partial` : value;
1661
+ };
1662
+ const mcpNames = uniqueCampUiNames(equipment.flatMap((item) => item.mcp.serverNames));
1663
+ const mcpHealth = snapshot.workers.flatMap((worker) => worker.snapshot.mcp);
1664
+ const healthNames = new Set(mcpHealth.map((item) => item.serverName));
1665
+ const failedMcp = mcpHealth.some(
1666
+ (item) => item.status === "failed" || item.status === "closed"
1667
+ );
1668
+ const pendingMcp = mcpHealth.some(
1669
+ (item) => item.status === "connecting" || item.status === "retry-wait"
1670
+ );
1671
+ const partialMcp = mcpHealth.some((item) => !item.catalogComplete);
1672
+ const missingMcpHealth = mcpNames.some((name) => !healthNames.has(name));
1673
+ const mcpTone = failedMcp ? CampUiTone.error : pendingMcp || partialMcp || missingMcpHealth ? CampUiTone.attention : mcpNames.length > 0 ? CampUiTone.success : CampUiTone.neutral;
1674
+ const mcpNotes = [
1675
+ ...failedMcp ? ["down"] : [],
1676
+ ...pendingMcp ? ["connecting"] : [],
1677
+ ...partialMcp ? ["catalog partial"] : [],
1678
+ ...missingMcpHealth ? ["health not reported"] : []
1679
+ ];
1680
+ const mcpText = !equipmentReported ? "not reported" : mcpNames.length === 0 ? "none configured" : `${mcpNames.join(", ")}${mcpNotes.length === 0 ? "" : ` \xB7 ${mcpNotes.join(", ")}`}`;
1681
+ return [
1682
+ ...renderResourceTree(
1683
+ "Context",
1684
+ resourceText(inventory.contextFiles),
1685
+ inventory.contextFiles.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1686
+ viewport.width
1687
+ ),
1688
+ ...renderResourceTree(
1689
+ "Extensions",
1690
+ resourceText(inventory.extensions),
1691
+ inventory.extensions.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1692
+ viewport.width
1693
+ ),
1694
+ ...renderResourceTree(
1695
+ "Skills",
1696
+ resourceText(inventory.skills),
1697
+ inventory.skills.length > 0 ? CampUiTone.success : CampUiTone.neutral,
1698
+ viewport.width
1699
+ ),
1700
+ ...renderResourceTree("MCP", mcpText, mcpTone, viewport.width)
1701
+ ];
1702
+ }
1703
+ });
1704
+
1705
+ // packages/host-ui/src/pi/layout/campUiDefaultSlots.ts
1706
+ var makeDefaultCampUiContributions = (tui, detailBlocks) => [
1707
+ {
1708
+ id: "default-camp-header",
1709
+ slotId: CampUiSlot.campHeader,
1710
+ mount: (context) => Effect_exports.succeed(makeCampUiCampHeader(context))
1711
+ },
1712
+ {
1713
+ id: "default-seat",
1714
+ slotId: CampUiSlot.seat,
1715
+ mount: (context) => Effect_exports.succeed(makeCampUiSeat(context))
1716
+ },
1717
+ {
1718
+ id: "default-resources",
1719
+ slotId: CampUiSlot.resources,
1720
+ mount: (context) => Effect_exports.succeed(makeCampUiResources(context))
1721
+ },
1722
+ {
1723
+ id: "default-roster",
1724
+ slotId: CampUiSlot.roster,
1725
+ mount: (context) => Effect_exports.succeed(makeCampUiRoster(context))
1726
+ },
1727
+ {
1728
+ id: "default-session-header",
1729
+ slotId: CampUiSlot.sessionHeader,
1730
+ mount: (context) => Effect_exports.succeed(makeCampUiSessionHeader(context))
1731
+ },
1732
+ {
1733
+ id: "default-transcript",
1734
+ slotId: CampUiSlot.transcript,
1735
+ mount: (context) => Effect_exports.succeed(makeCampUiDetailComponent(context, detailBlocks))
1736
+ },
1737
+ {
1738
+ id: "default-composer",
1739
+ slotId: CampUiSlot.composer,
1740
+ mount: (context) => Effect_exports.succeed(makeCampUiComposerComponent(context, tui))
1741
+ },
1742
+ {
1743
+ id: "default-camp-footer",
1744
+ slotId: CampUiSlot.campFooter,
1745
+ mount: (context) => Effect_exports.succeed(makeCampUiCampFooter(context))
1746
+ },
1747
+ {
1748
+ id: "default-session-footer",
1749
+ slotId: CampUiSlot.sessionFooter,
1750
+ mount: (context) => Effect_exports.succeed(makeCampUiSessionFooter(context))
1751
+ }
1752
+ ];
1753
+
1754
+ // packages/host-ui/src/pi/layout/campUiSection.ts
1755
+ import { getMarkdownTheme as getMarkdownTheme9, getSelectListTheme as getSelectListTheme11 } from "@earendil-works/pi-coding-agent";
1756
+ import { visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
1757
+ var renderHeader = (header, width) => {
1758
+ const theme = getMarkdownTheme9();
1759
+ const selectTheme = getSelectListTheme11();
1760
+ const rule2 = header.focused === true ? selectTheme.selectedPrefix : theme.codeBlockBorder;
1761
+ const title = header.focused === true ? selectTheme.selectedText : theme.heading;
1762
+ const ruleCharacter = header.focused === true ? "\u2501" : "\u2500";
1763
+ const detail = header.detail === void 0 ? "" : ` ${selectTheme.description(header.detail)}`;
1764
+ const label = `${rule2(ruleCharacter.repeat(2))} ${title(header.title)}${detail} `;
1765
+ const ruleWidth = Math.max(0, width - visibleWidth3(label));
1766
+ return fitCampUiLine(`${label}${rule2(ruleCharacter.repeat(ruleWidth))}`, width);
1767
+ };
1768
+ var makeCampUiSection = (options) => {
1769
+ let childHeights = options.children.map(() => 0);
1770
+ const components = options.children.map((child) => child.component);
1771
+ const pointerComponents = components.some((component) => component.handlePointer !== void 0);
1772
+ const inputComponents = components.some((component) => component.handleInput !== void 0);
1773
+ const invalidatingComponents = components.some((component) => component.invalidate !== void 0);
1774
+ const disposableComponents = components.some((component) => component.dispose !== void 0);
1775
+ return {
1776
+ sectionSize: options.size,
1777
+ ...components.some((component) => component.focusable === true) ? { focusable: true } : {},
1778
+ render: (viewport) => {
1779
+ if (viewport.height <= 0) {
1780
+ childHeights = options.children.map(() => 0);
1781
+ return [];
1782
+ }
1783
+ childHeights = [
1784
+ ...allocateCampUiSectionRows(
1785
+ viewport.height - 1,
1786
+ options.children.map((child) => child.size)
1787
+ )
1788
+ ];
1789
+ return [
1790
+ renderHeader(options.header(), viewport.width),
1791
+ ...options.children.flatMap((child, index) => {
1792
+ const height = childHeights[index] ?? 0;
1793
+ const lines = child.component.render({ width: viewport.width, height }).slice(0, height);
1794
+ return [...lines, ...Array.from({ length: height - lines.length }, () => "")];
1795
+ })
1796
+ ];
1797
+ },
1798
+ ...inputComponents ? {
1799
+ handleInput: (input) => [...components].reverse().some((component) => component.handleInput?.(input) === true)
1800
+ } : {},
1801
+ ...pointerComponents ? {
1802
+ handlePointer: (pointer) => {
1803
+ const bodyY = pointer.y - 1;
1804
+ if (bodyY < 0) return [];
1805
+ let offset = 0;
1806
+ for (const [index, child] of options.children.entries()) {
1807
+ const height = childHeights[index] ?? 0;
1808
+ if (bodyY >= offset && bodyY < offset + height) {
1809
+ return child.component.handlePointer?.({ ...pointer, y: bodyY - offset }) ?? [];
1810
+ }
1811
+ offset += height;
1812
+ }
1813
+ return [];
1814
+ }
1815
+ } : {},
1816
+ ...invalidatingComponents ? {
1817
+ invalidate: () => {
1818
+ for (const component of components) component.invalidate?.();
1819
+ }
1820
+ } : {},
1821
+ ...disposableComponents ? {
1822
+ dispose: Effect_exports.forEach(
1823
+ [...components].reverse(),
1824
+ (component) => component.dispose ?? Effect_exports.void,
1825
+ { discard: true }
1826
+ )
1827
+ } : {}
1828
+ };
1829
+ };
1830
+ var decorateDefaultCampUiSections = (context, slots) => {
1831
+ const decorated = new Map(slots);
1832
+ const component = (slotId) => slots.get(slotId) ?? emptyCampUiComponent;
1833
+ decorated.set(
1834
+ CampUiSlot.campHeader,
1835
+ makeCampUiSection({
1836
+ header: () => ({ title: "Camp" }),
1837
+ size: defaultCampUiLeftSectionSizes.camp,
1838
+ children: [
1839
+ {
1840
+ component: component(CampUiSlot.campHeader),
1841
+ size: { kind: CampUiSectionSizeKind.fixed, rows: 2 }
1842
+ },
1843
+ {
1844
+ component: component(CampUiSlot.seat),
1845
+ size: { kind: CampUiSectionSizeKind.grow, weight: 1, minimumRows: 1 }
1846
+ }
1847
+ ]
1848
+ })
1849
+ );
1850
+ decorated.delete(CampUiSlot.seat);
1851
+ decorated.set(
1852
+ CampUiSlot.resources,
1853
+ makeCampUiSection({
1854
+ header: () => ({ title: "Resources" }),
1855
+ size: defaultCampUiLeftSectionSizes.resources,
1856
+ children: [
1857
+ {
1858
+ component: component(CampUiSlot.resources),
1859
+ size: { kind: CampUiSectionSizeKind.grow, weight: 1 }
1860
+ }
1861
+ ]
1862
+ })
1863
+ );
1864
+ decorated.set(
1865
+ CampUiSlot.roster,
1866
+ makeCampUiSection({
1867
+ header: () => ({
1868
+ title: "Workers",
1869
+ detail: String(context.readModel().snapshot.workers.length),
1870
+ focused: context.readModel().state.focusedSlotId === CampUiSlot.roster
1871
+ }),
1872
+ size: defaultCampUiLeftSectionSizes.workers,
1873
+ children: [
1874
+ {
1875
+ component: component(CampUiSlot.roster),
1876
+ size: { kind: CampUiSectionSizeKind.grow, weight: 1 }
1877
+ }
1878
+ ]
1879
+ })
1880
+ );
1881
+ return decorated;
1882
+ };
1883
+
1884
+ // packages/host-ui/src/pi/mountPiCampUi.ts
1885
+ import { initTheme } from "@earendil-works/pi-coding-agent";
1886
+ import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
1887
+
1888
+ // packages/host-ui/src/pi/piCampUiRoot.ts
1889
+ import { getMarkdownTheme as getMarkdownTheme10 } from "@earendil-works/pi-coding-agent";
1890
+ import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
1891
+
1892
+ // packages/host-ui/src/pi/runtime/campUiPointerInput.ts
1893
+ var CampUiPointerAction = {
1894
+ press: "press",
1895
+ release: "release",
1896
+ wheel: "wheel"
1897
+ };
1898
+ var CampUiPointerButton = {
1899
+ left: "left",
1900
+ middle: "middle",
1901
+ right: "right"
1902
+ };
1903
+ var CampUiPointerDirection = {
1904
+ up: "up",
1905
+ down: "down"
1906
+ };
1907
+ var sgrPointerPattern = /^<(\d+);(\d+);(\d+)([Mm])$/u;
1908
+ var parseCampUiPointerInput = (input) => {
1909
+ if (!input.startsWith("\x1B[")) return void 0;
1910
+ const match = sgrPointerPattern.exec(input.slice(2));
1911
+ if (match === null) return void 0;
1912
+ const code = Number(match[1]);
1913
+ const column = Number(match[2]);
1914
+ const row = Number(match[3]);
1915
+ if (!Number.isSafeInteger(code) || column < 1 || row < 1) return void 0;
1916
+ const base = {
1917
+ x: column - 1,
1918
+ y: row - 1,
1919
+ shift: (code & 4) !== 0,
1920
+ alt: (code & 8) !== 0,
1921
+ control: (code & 16) !== 0
1922
+ };
1923
+ if ((code & 64) !== 0) {
1924
+ return {
1925
+ ...base,
1926
+ action: CampUiPointerAction.wheel,
1927
+ direction: (code & 1) === 0 ? CampUiPointerDirection.up : CampUiPointerDirection.down
1928
+ };
1929
+ }
1930
+ const button = [CampUiPointerButton.left, CampUiPointerButton.middle, CampUiPointerButton.right][code & 3];
1931
+ if (button === void 0) return void 0;
1932
+ return {
1933
+ ...base,
1934
+ action: match[4] === "M" ? CampUiPointerAction.press : CampUiPointerAction.release,
1935
+ button
1936
+ };
1937
+ };
1938
+
1939
+ // packages/host-ui/src/pi/runtime/campUiPointerRouting.ts
1940
+ var routeCampUiPointer = (frame, pointer, slots) => {
1941
+ if (frame === void 0) return [];
1942
+ const hit = frame.regions.find(
1943
+ (region2) => pointer.x >= region2.x && pointer.x < region2.x + region2.width && pointer.y >= region2.y && pointer.y < region2.y + region2.height
1944
+ );
1945
+ if (hit === void 0) return [];
1946
+ return slots.get(hit.slotId)?.handlePointer?.({
1947
+ ...pointer,
1948
+ x: pointer.x - hit.x,
1949
+ y: pointer.y - hit.y,
1950
+ mode: frame.mode
1951
+ }) ?? [];
1952
+ };
1953
+
1954
+ // packages/host-ui/src/pi/piCampUiRoot.ts
1955
+ var focusDirectionFromInput = (input) => {
1956
+ if (matchesKey3(input, Key3.left)) return CampUiFocusDirection.left;
1957
+ if (matchesKey3(input, Key3.right)) return CampUiFocusDirection.right;
1958
+ if (matchesKey3(input, Key3.up)) return CampUiFocusDirection.up;
1959
+ return matchesKey3(input, Key3.down) ? CampUiFocusDirection.down : void 0;
1960
+ };
1961
+ var focusableSlotsInFrame = (frame, slots) => [
1962
+ ...new Set(
1963
+ (frame?.regions ?? []).filter((region2) => slots.get(region2.slotId)?.focusable === true).map((region2) => region2.slotId)
1964
+ )
1965
+ ];
1966
+ var PiCampUiRoot = class {
1967
+ mode = CampUiLayoutMode.narrow;
1968
+ frame;
1969
+ context;
1970
+ slots;
1971
+ layout;
1972
+ getHeight;
1973
+ wheelDirectionFromInput;
1974
+ constructor(context, slots, layout, getHeight, wheelDirectionFromInput) {
1975
+ this.context = context;
1976
+ this.slots = slots;
1977
+ this.layout = layout;
1978
+ this.getHeight = getHeight;
1979
+ this.wheelDirectionFromInput = wheelDirectionFromInput;
1980
+ }
1981
+ render(width) {
1982
+ const height = Math.max(0, this.getHeight());
1983
+ const viewport = { width: Math.max(0, width - 1), height };
1984
+ const frame = this.layout.resolve(viewport, this.context.readModel().state, this.slots);
1985
+ this.frame = frame;
1986
+ this.mode = frame.mode;
1987
+ const state = this.context.readModel().state;
1988
+ const focusOrder = focusableSlotsInFrame(frame, this.slots);
1989
+ if (!focusOrder.includes(state.focusedSlotId)) {
1990
+ const fallback = focusOrder[0];
1991
+ if (fallback !== void 0) this.context.emit(CampUiIntents.FocusSlot({ slotId: fallback }));
1992
+ }
1993
+ const detailVisible = frame.regions.some((region2) => region2.slotId === CampUiSlot.transcript);
1994
+ if (this.context.readModel().state.detailVisible !== detailVisible) {
1995
+ this.context.emit(CampUiIntents.SetTranscriptVisible({ visible: detailVisible }));
1996
+ }
1997
+ const rendered = new Map(
1998
+ frame.regions.map((region2) => [
1999
+ region2,
2000
+ this.slots.get(region2.slotId)?.render({ width: region2.width, height: region2.height }) ?? []
2001
+ ])
2002
+ );
2003
+ return Array.from({ length: height }, (_, row) => {
2004
+ const segments = [];
2005
+ for (const [region2, lines] of rendered) {
2006
+ if (row < region2.y || row >= region2.y + region2.height) continue;
2007
+ segments.push({
2008
+ x: region2.x,
2009
+ width: region2.width,
2010
+ text: lines[row - region2.y] ?? ""
2011
+ });
2012
+ }
2013
+ for (const divider of frame.dividers) {
2014
+ if (row >= divider.y && row < divider.y + divider.height) {
2015
+ segments.push({
2016
+ x: divider.x,
2017
+ width: 1,
2018
+ text: getMarkdownTheme10().codeBlockBorder("\u2502")
2019
+ });
2020
+ }
2021
+ }
2022
+ segments.sort((left, right) => left.x - right.x);
2023
+ let cursor = 0;
2024
+ let line = "";
2025
+ for (const segment of segments) {
2026
+ line += " ".repeat(Math.max(0, segment.x - cursor));
2027
+ line += fitCampUiLine(segment.text, segment.width);
2028
+ cursor = segment.x + segment.width;
2029
+ }
2030
+ return fitCampUiLine(
2031
+ `${line}${" ".repeat(Math.max(0, viewport.width - cursor))}`,
2032
+ viewport.width
2033
+ );
2034
+ });
2035
+ }
2036
+ handleInput(input) {
2037
+ const pointer = parseCampUiPointerInput(input);
2038
+ if (pointer !== void 0) {
2039
+ for (const intent of routeCampUiPointer(this.frame, pointer, this.slots)) {
2040
+ this.context.emit(intent);
2041
+ }
2042
+ return;
2043
+ }
2044
+ const wheelDirection = this.wheelDirectionFromInput?.(input);
2045
+ if (wheelDirection !== void 0) {
2046
+ const model2 = this.context.readModel();
2047
+ const workerId2 = model2.state.selectedWorkerId;
2048
+ const transcriptVisible = this.frame?.regions.some((region2) => region2.slotId === CampUiSlot.transcript) === true;
2049
+ if (workerId2 !== void 0 && transcriptVisible) {
2050
+ this.context.emit(
2051
+ CampUiIntents.ScrollWorker({
2052
+ workerId: workerId2,
2053
+ delta: wheelDirection === "up" ? 3 : -3
2054
+ })
2055
+ );
2056
+ }
2057
+ return;
2058
+ }
2059
+ const model = this.context.readModel();
2060
+ const focusOrder = focusableSlotsInFrame(this.frame, this.slots);
2061
+ const focusedSlotId = focusOrder.includes(model.state.focusedSlotId) ? model.state.focusedSlotId : focusOrder[0] ?? model.state.focusedSlotId;
2062
+ if (focusedSlotId !== model.state.focusedSlotId) {
2063
+ this.context.emit(CampUiIntents.FocusSlot({ slotId: focusedSlotId }));
2064
+ }
2065
+ if (this.slots.get(focusedSlotId)?.handleInput?.(input) === true) return;
2066
+ if (matchesKey3(input, Key3.escape)) {
2067
+ if (focusedSlotId === CampUiSlot.composer) {
2068
+ this.context.emit(CampUiIntents.FocusSlot({ slotId: CampUiSlot.transcript }));
2069
+ } else if (model.state.screen === CampUiScreen.worker) {
2070
+ this.context.emit(CampUiIntents.ActivateScreen({ screen: CampUiScreen.camp }));
2071
+ this.context.emit(CampUiIntents.FocusDetailBlock({}));
2072
+ this.context.emit(CampUiIntents.FocusSlot({ slotId: CampUiSlot.roster }));
2073
+ }
2074
+ return;
2075
+ }
2076
+ if (matchesKey3(input, Key3.tab) || matchesKey3(input, "shift+tab")) {
2077
+ if (focusOrder.length === 0) return;
2078
+ const current = focusOrder.indexOf(focusedSlotId);
2079
+ const delta = matchesKey3(input, "shift+tab") ? -1 : 1;
2080
+ const base = current < 0 ? delta > 0 ? -1 : 0 : current;
2081
+ const next = focusOrder[(base + delta + focusOrder.length) % focusOrder.length];
2082
+ if (next === void 0) return;
2083
+ this.context.emit(CampUiIntents.FocusSlot({ slotId: next }));
2084
+ return;
2085
+ }
2086
+ if (focusedSlotId !== CampUiSlot.composer && input === "n") {
2087
+ this.context.emit(CampUiIntents.NextPendingAsk());
2088
+ return;
2089
+ }
2090
+ if (focusedSlotId === CampUiSlot.transcript && model.state.focusedDetailBlockId !== void 0 && matchesKey3(input, Key3.enter)) {
2091
+ this.context.emit(
2092
+ CampUiIntents.ToggleDetailBlock({ blockId: model.state.focusedDetailBlockId })
2093
+ );
2094
+ return;
2095
+ }
2096
+ if (focusedSlotId === CampUiSlot.roster) {
2097
+ const delta = matchesKey3(input, Key3.up) ? -1 : matchesKey3(input, Key3.down) ? 1 : void 0;
2098
+ if (delta !== void 0) {
2099
+ const current = model.snapshot.workers.findIndex(
2100
+ (worker) => worker.snapshot.worker.id === model.state.selectedWorkerId
2101
+ );
2102
+ const next = current < 0 ? 0 : current + delta;
2103
+ if (next >= 0 && next < model.snapshot.workers.length && next !== current) {
2104
+ this.context.emit(CampUiIntents.SelectRelativeWorker({ delta }));
2105
+ return;
2106
+ }
2107
+ }
2108
+ if (matchesKey3(input, Key3.enter) && model.state.selectedWorkerId !== void 0) {
2109
+ this.context.emit(CampUiIntents.OpenWorker({ workerId: model.state.selectedWorkerId }));
2110
+ this.context.emit(
2111
+ CampUiIntents.FocusSlot({
2112
+ slotId: this.mode === CampUiLayoutMode.wide ? CampUiSlot.composer : CampUiSlot.transcript
2113
+ })
2114
+ );
2115
+ return;
2116
+ }
2117
+ }
2118
+ const workerId = model.state.selectedWorkerId;
2119
+ if (focusedSlotId === CampUiSlot.transcript && workerId !== void 0 && matchesKey3(input, Key3.end)) {
2120
+ this.context.emit(CampUiIntents.JumpToWorkerTail({ workerId }));
2121
+ return;
2122
+ }
2123
+ if (focusedSlotId === CampUiSlot.transcript && workerId !== void 0 && (matchesKey3(input, Key3.pageUp) || matchesKey3(input, Key3.pageDown))) {
2124
+ this.context.emit(
2125
+ CampUiIntents.ScrollWorker({
2126
+ workerId,
2127
+ delta: (matchesKey3(input, Key3.pageUp) ? 1 : -1) * Math.max(
2128
+ 1,
2129
+ (this.frame?.regions.find((region2) => region2.slotId === CampUiSlot.transcript)?.height ?? 10) - 2
2130
+ )
2131
+ })
2132
+ );
2133
+ return;
2134
+ }
2135
+ const direction = focusDirectionFromInput(input);
2136
+ if (direction === void 0) return;
2137
+ const target = findCampUiSpatialFocusTarget(
2138
+ this.frame,
2139
+ focusedSlotId,
2140
+ new Set(focusOrder),
2141
+ direction
2142
+ );
2143
+ if (target !== void 0) this.context.emit(CampUiIntents.FocusSlot({ slotId: target }));
2144
+ }
2145
+ invalidate() {
2146
+ for (const component of this.slots.values()) component.invalidate?.();
2147
+ }
2148
+ };
2149
+
2150
+ // packages/host-ui/src/pi/runtime/campUiPresentationClock.ts
2151
+ var hasPendingCampUiAsk = (model) => model.snapshot.workers.some(
2152
+ (worker) => worker.asks.some((ask) => ask.state === CampAskState.pending)
2153
+ );
2154
+ var runCampUiPresentationClock = (readModel, controller) => Effect_exports.forever(
2155
+ Effect_exports.sleep("1 second").pipe(
2156
+ Effect_exports.zipRight(
2157
+ Effect_exports.suspend(
2158
+ () => hasPendingCampUiAsk(readModel()) ? Clock_exports.currentTimeMillis.pipe(Effect_exports.flatMap(controller.setPresentationNow)) : Effect_exports.void
2159
+ )
2160
+ )
2161
+ )
2162
+ );
2163
+
2164
+ // packages/host-ui/src/pi/runtime/campUiSubscriptionRetry.ts
2165
+ var CAMP_UI_SUBSCRIPTION_MAX_RETRIES = 4;
2166
+ var CAMP_UI_SUBSCRIPTION_BASE_DELAY_MS = 250;
2167
+ var campUiSubscriptionRetryDelay = (attempt) => CAMP_UI_SUBSCRIPTION_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1);
2168
+ var sameCampUiRuntimeHealth = (left, right) => left.status === right.status && left.operation === right.operation && left.message === right.message && left.attempt === right.attempt && left.nextRetryAt === right.nextRetryAt;
2169
+
2170
+ // packages/host-ui/src/pi/runtime/campUiTerminalModes.ts
2171
+ var ENTER_ALTERNATE_SCREEN = "\x1B[?1049h";
2172
+ var LEAVE_ALTERNATE_SCREEN = "\x1B[?1049l";
2173
+ var ENABLE_ALTERNATE_SCROLL = "\x1B[?1007h";
2174
+ var DISABLE_ALTERNATE_SCROLL = "\x1B[?1007l";
2175
+ var CampUiWheelDirection = {
2176
+ down: "down",
2177
+ up: "up"
2178
+ };
2179
+ var makeCampUiTerminalModes = (terminal) => {
2180
+ let alternateScreenActive = false;
2181
+ let alternateScrollActive = false;
2182
+ return {
2183
+ enter: () => {
2184
+ if (alternateScreenActive) return;
2185
+ terminal.write(ENTER_ALTERNATE_SCREEN);
2186
+ alternateScreenActive = true;
2187
+ },
2188
+ exit: () => {
2189
+ if (alternateScrollActive) {
2190
+ terminal.write(DISABLE_ALTERNATE_SCROLL);
2191
+ alternateScrollActive = false;
2192
+ }
2193
+ if (alternateScreenActive) {
2194
+ terminal.write(LEAVE_ALTERNATE_SCREEN);
2195
+ alternateScreenActive = false;
2196
+ }
2197
+ },
2198
+ tryEnableAlternateScroll: () => {
2199
+ if (alternateScrollActive) return true;
2200
+ if (!alternateScreenActive || !terminal.kittyProtocolActive) return false;
2201
+ terminal.write(ENABLE_ALTERNATE_SCROLL);
2202
+ alternateScrollActive = true;
2203
+ return true;
2204
+ },
2205
+ wheelDirection: (input) => {
2206
+ if (!alternateScrollActive) return void 0;
2207
+ if (input === "\x1B[A") return CampUiWheelDirection.up;
2208
+ if (input === "\x1B[B") return CampUiWheelDirection.down;
2209
+ return void 0;
2210
+ }
2211
+ };
2212
+ };
2213
+
2214
+ // packages/host-ui/src/pi/mountPiCampUi.ts
2215
+ var mountPiCampUi = (options) => Effect_exports.gen(function* () {
2216
+ const startedAt = yield* Clock_exports.currentTimeMillis;
2217
+ const healthRef = yield* SubscriptionRef_exports.make({
2218
+ status: CampUiRuntimeStatus.starting,
2219
+ operation: "ui.mount",
2220
+ updatedAt: startedAt
2221
+ });
2222
+ const setHealth = (health) => SubscriptionRef_exports.get(healthRef).pipe(
2223
+ Effect_exports.flatMap(
2224
+ (current2) => sameCampUiRuntimeHealth(current2, { ...health, updatedAt: current2.updatedAt }) ? Effect_exports.void : Clock_exports.currentTimeMillis.pipe(
2225
+ Effect_exports.flatMap(
2226
+ (updatedAt) => SubscriptionRef_exports.set(healthRef, { ...health, updatedAt })
2227
+ )
2228
+ )
2229
+ )
2230
+ );
2231
+ yield* Effect_exports.sync(() => initTheme(void 0, false));
2232
+ const initial = yield* options.backend.snapshot;
2233
+ const store = yield* makeCampUiStore(initial, startedAt);
2234
+ let current = yield* store.current;
2235
+ const terminal = options.terminal ?? new ProcessTerminal();
2236
+ const terminalModes = makeCampUiTerminalModes(terminal);
2237
+ yield* Effect_exports.acquireRelease(
2238
+ Effect_exports.sync(() => terminalModes.enter()),
2239
+ () => Effect_exports.sync(() => terminalModes.exit())
2240
+ );
2241
+ const tui = new TUI(terminal, true);
2242
+ const runEffect = yield* FiberSet_exports.makeRuntime();
2243
+ const dispatch = (effect) => {
2244
+ runEffect(
2245
+ effect.pipe(
2246
+ Effect_exports.catchAllCause((cause) => store.controller.setActionError(Cause_exports.pretty(cause)))
2247
+ )
2248
+ );
2249
+ };
2250
+ const router = makeCampUiIntentRouter({
2251
+ backend: options.backend,
2252
+ controller: store.controller,
2253
+ readModel: () => current,
2254
+ dispatch
2255
+ });
2256
+ const context = {
2257
+ readModel: () => current,
2258
+ emit: router.emit
2259
+ };
2260
+ const detailBlocks = yield* composeCampUiDetailBlocks(
2261
+ defaultCampUiDetailBlockRenderers,
2262
+ options.detailBlocks ?? []
2263
+ );
2264
+ const composedSlots = yield* composeCampUiSlots(
2265
+ context,
2266
+ makeDefaultCampUiContributions(tui, detailBlocks),
2267
+ options.contributions ?? []
2268
+ );
2269
+ const slots = decorateDefaultCampUiSections(context, composedSlots);
2270
+ const root = new PiCampUiRoot(
2271
+ context,
2272
+ slots,
2273
+ options.layout ?? makeDefaultCampUiLayout(),
2274
+ () => terminal.rows,
2275
+ terminalModes.wheelDirection
2276
+ );
2277
+ tui.addChild(root);
2278
+ tui.setFocus(root);
2279
+ yield* Effect_exports.sync(() => {
2280
+ terminal.setTitle(initial.displayName);
2281
+ terminal.clearScreen();
2282
+ tui.start();
2283
+ });
2284
+ yield* Effect_exports.addFinalizer(
2285
+ () => Effect_exports.sync(() => {
2286
+ tui.stop();
2287
+ terminal.setProgress(false);
2288
+ }).pipe(
2289
+ Effect_exports.zipRight(setHealth({ status: CampUiRuntimeStatus.stopped, operation: "ui.stop" }))
2290
+ )
2291
+ );
2292
+ yield* Effect_exports.forkScoped(
2293
+ Effect_exports.gen(function* () {
2294
+ for (let attempt = 0; attempt < 25; attempt += 1) {
2295
+ if (yield* Effect_exports.sync(() => terminalModes.tryEnableAlternateScroll())) return;
2296
+ yield* Effect_exports.sleep("10 millis");
2297
+ }
2298
+ })
2299
+ );
2300
+ const fibers = yield* FiberSet_exports.make();
2301
+ yield* FiberSet_exports.run(
2302
+ fibers,
2303
+ store.changes.pipe(
2304
+ Stream_exports.runForEach(
2305
+ (model) => Effect_exports.sync(() => {
2306
+ current = model;
2307
+ tui.requestRender();
2308
+ })
2309
+ )
2310
+ )
2311
+ );
2312
+ yield* FiberSet_exports.run(
2313
+ fibers,
2314
+ runCampUiPresentationClock(() => current, store.controller)
2315
+ );
2316
+ yield* setHealth({ status: CampUiRuntimeStatus.ready, operation: "ui.mount" });
2317
+ const runSubscription = (failures) => options.backend.changes.pipe(
2318
+ Stream_exports.runForEach(
2319
+ (snapshot) => store.updateSnapshot(snapshot).pipe(
2320
+ Effect_exports.zipRight(
2321
+ setHealth({ status: CampUiRuntimeStatus.ready, operation: "ui.subscribe" })
2322
+ ),
2323
+ Effect_exports.zipRight(store.controller.setActionError(void 0))
2324
+ )
2325
+ ),
2326
+ Effect_exports.zipRight(
2327
+ Effect_exports.fail(
2328
+ new Error("Camp UI backend subscription ended before the UI scope was closed")
2329
+ )
2330
+ ),
2331
+ Effect_exports.catchAll((cause) => {
2332
+ const attempt = failures + 1;
2333
+ const message = cause instanceof Error ? cause.message : String(cause);
2334
+ if (attempt > CAMP_UI_SUBSCRIPTION_MAX_RETRIES) {
2335
+ return setHealth({
2336
+ status: CampUiRuntimeStatus.failed,
2337
+ operation: "ui.subscribe",
2338
+ message,
2339
+ attempt
2340
+ }).pipe(Effect_exports.zipRight(store.controller.setActionError(message)));
2341
+ }
2342
+ const delay = campUiSubscriptionRetryDelay(attempt);
2343
+ return Clock_exports.currentTimeMillis.pipe(
2344
+ Effect_exports.flatMap(
2345
+ (now) => setHealth({
2346
+ status: CampUiRuntimeStatus.retrying,
2347
+ operation: "ui.subscribe",
2348
+ message,
2349
+ attempt,
2350
+ nextRetryAt: now + delay
2351
+ })
2352
+ ),
2353
+ Effect_exports.zipRight(store.controller.setActionError(message)),
2354
+ Effect_exports.zipRight(Effect_exports.sleep(delay)),
2355
+ Effect_exports.zipRight(Effect_exports.suspend(() => runSubscription(attempt)))
2356
+ );
2357
+ })
2358
+ );
2359
+ yield* FiberSet_exports.run(fibers, runSubscription(0));
2360
+ tui.requestRender(true);
2361
+ return {
2362
+ controller: store.controller,
2363
+ tui,
2364
+ health: { current: SubscriptionRef_exports.get(healthRef), changes: healthRef.changes }
2365
+ };
2366
+ });
2367
+
2368
+ // packages/host-ui/src/run-selector/campRunSelector.ts
2369
+ import { getMarkdownTheme as getMarkdownTheme11, getSelectListTheme as getSelectListTheme12, initTheme as initTheme2 } from "@earendil-works/pi-coding-agent";
2370
+ import {
2371
+ Key as Key4,
2372
+ matchesKey as matchesKey4,
2373
+ ProcessTerminal as ProcessTerminal2,
2374
+ TUI as TUI2
2375
+ } from "@earendil-works/pi-tui";
2376
+ var PiCampRunSelectorRoot = class {
2377
+ selectedIndex = 0;
2378
+ items;
2379
+ getHeight;
2380
+ select;
2381
+ requestRender;
2382
+ constructor(items, getHeight, select, requestRender) {
2383
+ this.items = items;
2384
+ this.getHeight = getHeight;
2385
+ this.select = select;
2386
+ this.requestRender = requestRender;
2387
+ }
2388
+ render(width) {
2389
+ const height = Math.max(0, this.getHeight());
2390
+ const viewportWidth = Math.max(0, width - 1);
2391
+ const markdown = getMarkdownTheme11();
2392
+ const selection = getSelectListTheme12();
2393
+ const header = `${markdown.codeBlockBorder("\u2500\u2500")} ${markdown.heading("Resume camp")} ${markdown.codeBlockBorder("\u2500".repeat(Math.max(0, viewportWidth - 16)))}`;
2394
+ const available = Math.max(0, height - 2);
2395
+ const capacity = Math.max(1, Math.floor(available / 2));
2396
+ const start = Math.min(
2397
+ Math.max(0, this.selectedIndex - Math.floor(capacity / 2)),
2398
+ Math.max(0, this.items.length - capacity)
2399
+ );
2400
+ const visibleItems = this.items.slice(start, start + capacity);
2401
+ const rows2 = visibleItems.flatMap((item, index) => {
2402
+ const selected = start + index === this.selectedIndex;
2403
+ const prefix = selected ? selection.selectedPrefix("\u25CF ") : " ";
2404
+ const title = selected ? selection.selectedText(item.runId) : item.runId;
2405
+ const details = selection.description(
2406
+ `${item.lifecycle} \xB7 ${String(item.workerCount)} workers \xB7 ${item.updatedAt}`
2407
+ );
2408
+ return [
2409
+ fitCampUiLine(`${prefix}${title}`, viewportWidth),
2410
+ fitCampUiLine(` ${details}`, viewportWidth)
2411
+ ];
2412
+ });
2413
+ const content = [fitCampUiLine(header, viewportWidth), "", ...rows2].slice(0, height);
2414
+ return [...content, ...Array.from({ length: Math.max(0, height - content.length) }, () => "")];
2415
+ }
2416
+ handleInput(input) {
2417
+ if (matchesKey4(input, Key4.escape) || input === "q") {
2418
+ this.select(void 0);
2419
+ return;
2420
+ }
2421
+ if (matchesKey4(input, Key4.enter)) {
2422
+ this.select(this.items[this.selectedIndex]?.runId);
2423
+ return;
2424
+ }
2425
+ const delta = matchesKey4(input, Key4.up) ? -1 : matchesKey4(input, Key4.down) ? 1 : 0;
2426
+ if (delta === 0 || this.items.length === 0) return;
2427
+ this.selectedIndex = (this.selectedIndex + delta + this.items.length) % this.items.length;
2428
+ this.requestRender();
2429
+ }
2430
+ invalidate() {
2431
+ }
2432
+ };
2433
+ var selectCampRun = (options) => Effect_exports.scoped(
2434
+ Effect_exports.gen(function* () {
2435
+ yield* Effect_exports.sync(() => initTheme2(void 0, false));
2436
+ const selected = yield* Deferred_exports.make();
2437
+ const terminal = options.terminal ?? new ProcessTerminal2();
2438
+ const modes = makeCampUiTerminalModes(terminal);
2439
+ yield* Effect_exports.acquireRelease(
2440
+ Effect_exports.sync(() => modes.enter()),
2441
+ () => Effect_exports.sync(() => modes.exit())
2442
+ );
2443
+ const tui = new TUI2(terminal, true);
2444
+ const root = new PiCampRunSelectorRoot(
2445
+ options.items,
2446
+ () => terminal.rows,
2447
+ (runId) => {
2448
+ Effect_exports.runFork(Deferred_exports.succeed(selected, runId));
2449
+ },
2450
+ () => tui.requestRender()
2451
+ );
2452
+ tui.addChild(root);
2453
+ tui.setFocus(root);
2454
+ yield* Effect_exports.acquireRelease(
2455
+ Effect_exports.sync(() => {
2456
+ terminal.setTitle("Resume camp");
2457
+ terminal.clearScreen();
2458
+ tui.start();
2459
+ tui.requestRender(true);
2460
+ }),
2461
+ () => Effect_exports.sync(() => tui.stop())
2462
+ );
2463
+ return yield* Deferred_exports.await(selected);
2464
+ })
2465
+ );
2466
+
2467
+ // packages/host-ui/src/index.ts
2468
+ var PACKAGE = "@priiisk/host-ui";
2469
+
2470
+ export {
2471
+ CampUiBackendError,
2472
+ CampUiBackend,
2473
+ CampUiPlacement,
2474
+ CampUiHealthStatus,
2475
+ CampUiScreen,
2476
+ campUiAskBlockId,
2477
+ makeInitialCampUiState,
2478
+ makeCampUiModel,
2479
+ findCampUiWorker,
2480
+ CampUiControllerError,
2481
+ makeCampUiStore,
2482
+ makeCampUiComponentGroup,
2483
+ emptyCampUiComponent,
2484
+ CampUiSlot,
2485
+ CampUiContributionStrategy,
2486
+ CampUiCompositionError,
2487
+ composeCampUiSlots,
2488
+ composeCampUiDetailBlocks,
2489
+ renderCampUiDetailBlocks,
2490
+ renderCampUiDetailBlockLayout,
2491
+ campUiPendingAskTargets,
2492
+ nextCampUiPendingAskTarget,
2493
+ CampUiIntents,
2494
+ makeCampUiIntentHandler,
2495
+ makeCampUiIntentRouter,
2496
+ CampUiFocusDirection,
2497
+ findCampUiSpatialFocusTarget,
2498
+ defaultCampUiLayoutPolicy,
2499
+ CampUiSectionSizeKind,
2500
+ allocateCampUiSectionRows,
2501
+ defaultCampUiLeftSectionSizes,
2502
+ CampUiLayoutMode,
2503
+ makeDefaultCampUiLayout,
2504
+ CampUiRuntimeStatus,
2505
+ hasCampUiTextContent,
2506
+ fitCampUiLine,
2507
+ fitCampUiLines,
2508
+ joinCampUiColumns,
2509
+ jsonText,
2510
+ compactNumber,
2511
+ CampUiTone,
2512
+ CampUiGlyph,
2513
+ styleCampUiTone,
2514
+ uniqueCampUiNames,
2515
+ renderCampUiStatus,
2516
+ renderCampUiState,
2517
+ renderCampUiWorkerState,
2518
+ renderSelectedCampUiRow,
2519
+ DEFAULT_DIRECTED_BLOCK_BODY_LINES,
2520
+ formatCampUiDuration,
2521
+ renderCampUiDirectedBlock,
2522
+ campUiPendingAskBlockRenderer,
2523
+ CampUiAssistantMessage,
2524
+ makeCampUiComposerComponent,
2525
+ campUiEquipmentBlockRenderer,
2526
+ renderCampUiPanel,
2527
+ CAMP_UI_DEFAULT_TOOL_PREVIEW_LINES,
2528
+ CAMP_UI_BASH_PREVIEW_LINES,
2529
+ renderCampUiToolResult,
2530
+ campUiTranscriptBlockRenderer,
2531
+ defaultCampUiDetailBlockRenderers,
2532
+ makeCampUiDetailComponent,
2533
+ makeDefaultCampUiContributions,
2534
+ makeCampUiSection,
2535
+ decorateDefaultCampUiSections,
2536
+ PiCampUiRoot,
2537
+ hasPendingCampUiAsk,
2538
+ runCampUiPresentationClock,
2539
+ CAMP_UI_SUBSCRIPTION_MAX_RETRIES,
2540
+ CAMP_UI_SUBSCRIPTION_BASE_DELAY_MS,
2541
+ campUiSubscriptionRetryDelay,
2542
+ sameCampUiRuntimeHealth,
2543
+ mountPiCampUi,
2544
+ PiCampRunSelectorRoot,
2545
+ selectCampRun,
2546
+ PACKAGE
2547
+ };