@underactive/pi-topping-moa-fusion 0.4.4 → 0.5.0

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.
@@ -31,13 +31,19 @@
31
31
  import type { Message } from "@earendil-works/pi-ai";
32
32
  import type { ExtensionContext, ThemeColor } from "@earendil-works/pi-coding-agent";
33
33
  import { visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
34
- import { ACTIVITY_METER_WIDTH, ActivityMeter, rateToLevel, TokRateTracker } from "../activityMeter.ts";
34
+ import { ACTIVITY_METER_WIDTH, ActivityMeter, rateToLevel, TokRateTracker } from "@underactive/pi-topping-primitives/activity";
35
+ import {
36
+ type AnsiTheme,
37
+ highlightActivity,
38
+ renderIdleMeter,
39
+ renderMeter,
40
+ shimmerString,
41
+ thinkingLevelThemeColor,
42
+ } from "@underactive/pi-topping-primitives/activity-tui";
35
43
  import { modelRefLabel, shortModelName, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
36
44
  import { contextPercent, CTX_COL_WIDTH, formatCost, formatElapsed, formatTokens } from "./agentStatus.ts";
37
45
  import { formatAgentPreview } from "./agentTranscript.ts";
38
46
  import { QUADRANT_SPINNER_FRAMES, UI_TICK_MS, fitVisible, ratioViewport } from "./chrome.ts";
39
- import { shimmerString, type ShimmerTheme } from "./shimmer.ts";
40
- import { highlightActivity } from "./toolActivity.ts";
41
47
 
42
48
  const WIDGET_KEY = "mf-plan-moa-status";
43
49
 
@@ -140,28 +146,6 @@ const PREVIEW_GUTTER = "│ ";
140
146
  const PREVIEW_MIN_WIDTH = 16;
141
147
  const TRANSCRIPT_MESSAGE_LIMIT = 40;
142
148
 
143
- /**
144
- * MONITOR meter hue per selected thinking level: each row's activity meter is
145
- * tinted with its level's native theme color (`thinkingOff` … `thinkingMax`),
146
- * so the table reads the thinking effort at a glance. Rows whose level is
147
- * unknown fall back to `accent` (see `meterColorFor`). Exhaustive over
148
- * `ThinkingLevel` so a new level cannot silently render as accent.
149
- */
150
- const THINKING_METER_COLORS: Record<ThinkingLevel, ThemeColor> = {
151
- off: "thinkingOff",
152
- minimal: "thinkingMinimal",
153
- low: "thinkingLow",
154
- medium: "thinkingMedium",
155
- high: "thinkingHigh",
156
- xhigh: "thinkingXhigh",
157
- max: "thinkingMax",
158
- };
159
-
160
- /** Resolve a row's meter hue, defaulting an unknown level to the neutral accent. */
161
- function meterColorFor(thinking: ThinkingLevel | undefined): ThemeColor {
162
- return thinking ? THINKING_METER_COLORS[thinking] : "accent";
163
- }
164
-
165
149
  /** Push an activity onto a bounded history buffer (most-recent-last). */
166
150
  function pushActivity(history: string[], activity: string): void {
167
151
  history.push(activity);
@@ -747,7 +731,7 @@ export class MoaProgressTableComponent implements Component {
747
731
  const row = rows[i];
748
732
  let entry = this.meters[i];
749
733
  if (!entry) {
750
- entry = { meter: new ActivityMeter("rtl"), tracker: new TokRateTracker(), revision: row.outputRevision };
734
+ entry = { meter: new ActivityMeter({ direction: "rtl" }), tracker: new TokRateTracker(), revision: row.outputRevision };
751
735
  this.meters[i] = entry;
752
736
  }
753
737
  if (entry.revision !== row.outputRevision) {
@@ -762,14 +746,11 @@ export class MoaProgressTableComponent implements Component {
762
746
 
763
747
  private renderMeter(index: number, active: boolean, thinking: ThinkingLevel | undefined): string {
764
748
  const meter = this.meters[index]?.meter;
765
- if (!meter) return this.theme.fg("dim", "⢀".repeat(ACTIVITY_METER_WIDTH));
749
+ if (!meter) return renderIdleMeter(this.theme);
766
750
  // Each row's meter carries its selected thinking level's native theme hue;
767
751
  // an unknown level falls back to accent. IDLE cells and settled traces keep
768
- // their existing dimming inside colorizeCell regardless of the hue.
769
- const color = meterColorFor(thinking);
770
- return meter.render((level, char) =>
771
- ActivityMeter.colorizeCell(level, char, this.theme, color, !active),
772
- );
752
+ // their existing dimming inside renderMeter regardless of the hue.
753
+ return renderMeter(meter, this.theme, thinkingLevelThemeColor(thinking), !active);
773
754
  }
774
755
 
775
756
  /** How many lines the table may use, so it never crowds out the editor below it. */
@@ -798,7 +779,7 @@ export class MoaProgressTableComponent implements Component {
798
779
  const th = this.theme;
799
780
  if (!lit) return th.fg("dim", text);
800
781
  return th.getFgAnsi
801
- ? shimmerString(text, now - this.animationStartedAt, th as ShimmerTheme, "ltr", "normal", true)
782
+ ? shimmerString(text, now - this.animationStartedAt, th as AnsiTheme, "ltr", "normal", true)
802
783
  : th.fg("text", text);
803
784
  }
804
785
 
@@ -21,7 +21,8 @@ import {
21
21
  MAX_VERIFICATION_REPAIRS_LIMIT,
22
22
  MIN_CONCURRENT_AGENTS,
23
23
  MIN_VERIFICATION_REPAIRS,
24
- saveMoaConfig,
24
+ moaSaveFailureMessage,
25
+ updateMoaConfig,
25
26
  type MoaConfig,
26
27
  } from "../config/settings.ts";
27
28
  import type { PlanRoster } from "../config/rosters.ts";
@@ -29,8 +30,8 @@ import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/mode
29
30
  import { CONFIGURABLE_AGENTS, readAgentDefault, writeAgentDefault } from "../agents/defaults.ts";
30
31
  import { getAvailableModelRefs } from "./moaModelPicker.ts";
31
32
  import { showRosterManager } from "./rosterEditor.ts";
32
- import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
33
- import { getModelCatalogue } from "../config/modelCatalogue.ts";
33
+ import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
34
+ import { getModelCatalogue } from "@underactive/pi-topping-primitives/models";
34
35
  import {
35
36
  MenuComponent,
36
37
  PLAN_OVERLAY_OPTIONS,
@@ -108,7 +109,7 @@ class MoaSetupComponent implements Component {
108
109
  title: string,
109
110
  private readonly done: (result: SetupResult) => void,
110
111
  ) {
111
- this.twoPane = new TwoPaneModelThinking(tui, theme, availableRefs, config, currentThinking, ctx);
112
+ this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
112
113
  this.autoResolveConflicts = draft.autoResolveConflicts;
113
114
  this.useSummaryName = draft.useSummaryName;
114
115
  this.maxConcurrentAgents = draft.maxConcurrentAgents;
@@ -377,8 +378,8 @@ export async function showMoaSetup(
377
378
  draft.rosters = await showRosterManager(ctx, draft.rosters, {
378
379
  currentThinking,
379
380
  onChange: (rosters) => {
380
- const current = loadMoaConfig();
381
- saveMoaConfig({ ...current, rosters });
381
+ const persisted = updateMoaConfig((current) => ({ ...current, rosters }));
382
+ if (!persisted.ok) ctx.ui.notify(moaSaveFailureMessage(persisted), "warning");
382
383
  },
383
384
  });
384
385
  continue;
@@ -408,8 +409,7 @@ export async function showMoaSetup(
408
409
  // intentionally left untouched: they remain load-bearing for resume,
409
410
  // verifier fallback, and picker seeding, and are only written by a run's
410
411
  // own confirmations.
411
- const current = loadMoaConfig();
412
- saveMoaConfig({
412
+ const persisted = updateMoaConfig((current) => ({
413
413
  ...current,
414
414
  mode: "moa",
415
415
  agentDefaultsConfigured: true,
@@ -421,6 +421,10 @@ export async function showMoaSetup(
421
421
  cheap: finalDraft.selections[AGENT_SLOT_COUNT],
422
422
  rosters: finalDraft.rosters,
423
423
  thinkingOverrides: { ...current.thinkingOverrides, ...finalDraft.thinkingOverrides },
424
- });
424
+ }));
425
+ if (!persisted.ok) {
426
+ ctx.ui.notify(moaSaveFailureMessage(persisted), "warning");
427
+ return false;
428
+ }
425
429
  return true;
426
430
  }
@@ -10,8 +10,8 @@ import {
10
10
  ratioViewport,
11
11
  } from "./chrome.ts";
12
12
  import { getAvailableModelRefs } from "./moaModelPicker.ts";
13
- import { smartTruncateModelLabel } from "./modelLabel.ts";
14
- import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
13
+ import { smartTruncateModelLabel } from "@underactive/pi-topping-primitives/models";
14
+ import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
15
15
 
16
16
  const HINT_BASE = "type filters models • ↑↓ navigate pane • tab panes/buttons • ←→ switch/select • enter select";
17
17
 
@@ -54,7 +54,7 @@ export class OpinionModelPickerComponent implements Component {
54
54
  this.theme = theme;
55
55
  this.defaults = defaults;
56
56
  this.done = done;
57
- this.twoPane = new TwoPaneModelThinking(tui, theme, availableRefs, config, currentThinking, ctx);
57
+ this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
58
58
  }
59
59
 
60
60
  private assignedCount(): number {
@@ -1,272 +1,35 @@
1
1
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
3
- import { fuzzyFilter, Key, matchesKey, SelectList, truncateToWidth, visibleWidth, type SelectItem, type TUI } from "@earendil-works/pi-tui";
2
+ import type { TUI } from "@earendil-works/pi-tui";
3
+ import { registryLevelOptions, TwoPaneModelThinking } from "@underactive/pi-topping-primitives/models";
4
+ import { defaultThinkingForModel, type MoaConfig } from "../config/settings.ts";
5
+ import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
4
6
  import { SELECTOR_POINTER } from "./chrome.ts";
5
- import { renderMenuFooterContents } from "./menu.ts";
6
- import { MODEL_LIST_LAYOUT } from "./modelLabel.ts";
7
- import { getModelCatalogue } from "../config/modelCatalogue.ts";
8
- import {
9
- defaultThinkingForModel,
10
- thinkingOptionsForModel,
11
- type MoaConfig,
12
- } from "../config/settings.ts";
13
- import {
14
- isThinkingLevel,
15
- modelRefLabel,
16
- parseRef,
17
- THINKING_LEVELS,
18
- type ModelRef,
19
- type ThinkingLevel,
20
- } from "../shared/modelRefs.ts";
21
7
 
22
- const MAX_VISIBLE_ROWS = 10;
23
-
24
- /** SelectList hardcodes "→ "; the extension's pointer is a plain "> " everywhere. */
25
- const withPointer = (line: string): string => line.replace("→ ", SELECTOR_POINTER);
26
-
27
- function modelThinkingPaneWidths(bodyWidth: number): { left: number; right: number } {
28
- const divider = 1;
29
- const longestLevel = THINKING_LEVELS.reduce((longest, level) => level.length > longest.length ? level : longest);
30
- // One composed inset plus SelectList's two safety columns after its selector.
31
- const rightMin = Math.max(visibleWidth(" Thinking"), visibleWidth(`→ ${longestLevel}`) + 3);
32
- const right = Math.max(1, Math.min(rightMin, Math.max(1, bodyWidth - divider - 1)));
33
- return { left: Math.max(1, bodyWidth - divider - right), right };
34
- }
35
-
36
- function toModelItems(refs: ModelRef[]): SelectItem[] {
37
- return refs.map((ref) => ({ value: modelRefLabel(ref), label: modelRefLabel(ref) }));
38
- }
39
-
40
- /**
41
- * Reusable two-pane selector: models on the left and model-specific thinking
42
- * levels on the right. The model filter always owns text input; Tab or the
43
- * left/right arrows move the selection focus between panes.
44
- */
45
- export class TwoPaneModelThinking {
46
- private modelList: SelectList;
47
- private levelList: SelectList;
48
- private filter = "";
49
- private activePane: "model" | "level" | "buttons" = "model";
50
- private activeButton = 0;
51
- private maxVisibleRows = MAX_VISIBLE_ROWS;
52
- private readonly modelItems: SelectItem[];
53
-
54
- constructor(
55
- private readonly tui: TUI,
56
- private readonly theme: Theme,
57
- availableRefs: ModelRef[],
58
- private readonly config: MoaConfig,
59
- private readonly currentThinking: ThinkingLevel,
60
- private readonly ctx: ExtensionContext,
61
- ) {
62
- this.modelItems = toModelItems(availableRefs);
63
- this.modelList = new SelectList([], 1, getSelectListTheme());
64
- this.levelList = new SelectList([], 1, getSelectListTheme());
65
- this.reset();
66
- }
67
-
68
- isModelPane(): boolean {
69
- return this.activePane === "model";
70
- }
71
-
72
- /** Limit the complete two-pane body, including filter/header/scroll rows. */
73
- setMaxVisibleRows(limit: number): void {
74
- const next = Math.max(1, Math.floor(limit));
75
- if (next === this.maxVisibleRows) return;
76
- this.maxVisibleRows = next;
77
- this.rebuildPreservingSelection();
78
- }
79
-
80
- reset(defaultRef?: ModelRef, defaultThinking?: ThinkingLevel): void {
81
- this.filter = "";
82
- this.activePane = "model";
83
- this.activeButton = 0;
84
- this.rebuildModelList();
85
- if (defaultRef) {
86
- const defaultIndex = this.modelItems.findIndex((item) => item.value === modelRefLabel(defaultRef));
87
- if (defaultIndex >= 0) this.modelList.setSelectedIndex(defaultIndex);
88
- }
89
- this.rebuildThinkingList();
90
- if (defaultThinking) this.selectThinking(defaultThinking);
91
- }
92
-
93
- private selectThinking(thinking: ThinkingLevel): void {
94
- const key = this.modelList.getSelectedItem()?.value;
95
- if (!key) return;
96
- const levels = thinkingOptionsForModel(getModelCatalogue(this.ctx.modelRegistry).thinkingLevelsFor(parseRef(key)));
97
- const index = levels.indexOf(thinking);
98
- if (index >= 0) this.levelList.setSelectedIndex(index);
99
- }
100
-
101
- private listItemRows(itemCount: number): number {
102
- const chromeRows = 1 + (this.filter ? 1 : 0);
103
- const available = Math.max(1, this.maxVisibleRows - chromeRows);
104
- return itemCount > available ? Math.max(1, available - 1) : available;
105
- }
106
-
107
- private filteredModelItems(): SelectItem[] {
108
- return this.filter.trim()
109
- ? fuzzyFilter(this.modelItems, this.filter, (item) => item.label)
110
- : this.modelItems;
111
- }
112
-
113
- private rebuildModelList(): SelectItem[] {
114
- const items = this.filteredModelItems();
115
- this.modelList = new SelectList(
116
- items,
117
- Math.min(Math.max(items.length, 1), this.listItemRows(items.length)),
118
- getSelectListTheme(),
119
- MODEL_LIST_LAYOUT,
120
- );
121
- this.modelList.onSelectionChange = () => {
122
- this.rebuildThinkingList();
123
- this.tui.requestRender();
124
- };
125
- return items;
126
- }
127
-
128
- private rebuildPreservingSelection(): void {
129
- const selectedModel = this.modelList.getSelectedItem()?.value;
130
- const selectedLevel = this.levelList.getSelectedItem()?.value;
131
- const items = this.rebuildModelList();
132
- if (selectedModel) {
133
- const index = items.findIndex((item) => item.value === selectedModel);
134
- if (index >= 0) this.modelList.setSelectedIndex(index);
135
- }
136
- this.rebuildThinkingList();
137
- if (selectedLevel && isThinkingLevel(selectedLevel)) this.selectThinking(selectedLevel);
138
- }
139
-
140
- private rebuildThinkingList(): void {
141
- const key = this.modelList.getSelectedItem()?.value;
142
- const registryLevels = key
143
- ? getModelCatalogue(this.ctx.modelRegistry).thinkingLevelsFor(parseRef(key))
144
- : [];
145
- const levels = thinkingOptionsForModel(registryLevels);
146
- const items = (levels.length > 0 ? levels : ["off"]).map((level) => ({ value: level, label: level }));
147
- this.levelList = new SelectList(items, Math.min(Math.max(items.length, 1), this.listItemRows(items.length)), getSelectListTheme());
148
- if (!key) return;
149
- const preferred = defaultThinkingForModel(key, this.config, this.currentThinking, registryLevels);
150
- const preferredIndex = levels.indexOf(preferred);
151
- if (preferredIndex >= 0) this.levelList.setSelectedIndex(preferredIndex);
152
- }
153
-
154
- handleInput(data: string): "confirm" | "back" | undefined {
155
- if (matchesKey(data, Key.escape)) return "back";
156
- if (matchesKey(data, Key.tab)) {
157
- // Tab cycles Models → Thinking → action buttons → Models;
158
- // left/right continue to switch between Models and Thinking (or
159
- // between buttons, once the action bar is focused).
160
- this.activePane = this.activePane === "model" ? "level" : this.activePane === "level" ? "buttons" : "model";
161
- this.tui.requestRender();
162
- return undefined;
163
- }
164
- if (this.activePane === "buttons") {
165
- if (matchesKey(data, Key.left) || matchesKey(data, Key.right)) {
166
- this.activeButton = this.activeButton === 0 ? 1 : 0;
167
- this.tui.requestRender();
168
- return undefined;
169
- }
170
- if (matchesKey(data, Key.enter)) {
171
- return this.activeButton === 0 && this.modelList.getSelectedItem() && this.levelList.getSelectedItem()
172
- ? "confirm"
173
- : "back";
174
- }
175
- return undefined;
176
- }
177
- if (matchesKey(data, Key.enter)) {
178
- return this.modelList.getSelectedItem() && this.levelList.getSelectedItem() ? "confirm" : undefined;
179
- }
180
- if (matchesKey(data, Key.left) || matchesKey(data, Key.right)) {
181
- this.activePane = this.activePane === "model" ? "level" : "model";
182
- this.tui.requestRender();
183
- return undefined;
184
- }
185
- if (data === "\x7f" || data === "\b") {
186
- this.activePane = "model";
187
- if (this.filter.length > 0) {
188
- this.filter = this.filter.slice(0, -1);
189
- this.rebuildPreservingSelection();
190
- this.tui.requestRender();
191
- }
192
- return undefined;
193
- }
194
- if (data.length === 1 && data >= " " && data !== "\x7f") {
195
- this.activePane = "model";
196
- this.filter += data;
197
- this.rebuildPreservingSelection();
198
- this.tui.requestRender();
199
- return undefined;
200
- }
201
- (this.activePane === "model" ? this.modelList : this.levelList).handleInput(data);
202
- this.tui.requestRender();
203
- return undefined;
204
- }
205
-
206
- getSelected(): { ref: ModelRef; thinking: ThinkingLevel } {
207
- const model = this.modelList.getSelectedItem();
208
- const level = this.levelList.getSelectedItem();
209
- if (!model || !level || !isThinkingLevel(level.value)) {
210
- throw new Error("Cannot confirm an empty model or thinking-level selection");
211
- }
212
- return { ref: parseRef(model.value), thinking: level.value };
213
- }
214
-
215
- render(bodyWidth: number): string[] {
216
- const { left: leftWidth, right: rightWidth } = modelThinkingPaneWidths(bodyWidth);
217
- const column = (text: string, width: number) => truncateToWidth(text, width, "", true);
218
- const paneDivider = this.theme.fg("border", "│");
219
- const headers = column(
220
- this.activePane === "model" ? this.theme.bold(this.theme.fg("accent", " Models")) : this.theme.bold(" Models"),
221
- leftWidth,
222
- ) + paneDivider + column(
223
- ` ${this.activePane === "level" ? this.theme.bold(this.theme.fg("accent", "Thinking")) : this.theme.bold("Thinking")}`,
224
- rightWidth,
225
- );
226
- const modelLines = this.modelList.render(Math.max(1, leftWidth - 1));
227
- const levelLines = this.levelList.render(Math.max(1, rightWidth - 1));
228
- const rows = Math.max(modelLines.length, levelLines.length);
229
- const lines = [
230
- ...(this.filter ? [column(this.theme.fg("muted", ` filter: ${this.filter}`), bodyWidth)] : []),
231
- headers,
232
- ];
233
- for (let index = 0; index < rows; index++) {
234
- const modelLine = modelLines[index] ?? "";
235
- const levelLine = levelLines[index] ?? "";
236
- let leftCell = column(` ${withPointer(modelLine)}`, leftWidth);
237
- let rightCell = column(` ${withPointer(levelLine)}`, rightWidth);
238
- // SelectList renders "→ " only on the selected row; other rows use " ".
239
- if (this.activePane === "model" && modelLine.includes("→")) {
240
- leftCell = this.theme.bg("selectedBg", leftCell);
241
- }
242
- if (this.activePane === "level" && levelLine.includes("→")) {
243
- rightCell = this.theme.bg("selectedBg", rightCell);
244
- }
245
- lines.push(leftCell + paneDivider + rightCell);
246
- }
247
- return lines.slice(0, this.maxVisibleRows);
248
- }
249
-
250
- renderFooter(bodyWidth: number, hints: string): { actionRow: string; hintRows: string[] } {
251
- const buttonText = (label: string, index: number) => {
252
- const isActive = this.activeButton === index;
253
- const text = isActive ? `[ ${label} ]` : `‹ ${label} ›`;
254
- return isActive && this.activePane === "buttons"
255
- ? this.theme.bold(this.theme.fg("accent", text))
256
- : this.theme.fg("muted", text);
257
- };
258
- const rows = renderMenuFooterContents(
259
- this.theme,
260
- bodyWidth,
261
- `${buttonText("Select", 0)} ${buttonText("Cancel", 1)}`,
262
- hints,
263
- { wrapHints: true },
264
- );
265
- return { actionRow: rows[1] ?? "", hintRows: rows.slice(3) };
266
- }
267
-
268
- invalidate(): void {
269
- this.modelList.invalidate();
270
- this.levelList.invalidate();
271
- }
8
+ export type { TwoPaneModelThinking } from "@underactive/pi-topping-primitives/models";
9
+
10
+ /** Keep MoA's registry fallback and saved/current thinking policy in the host. */
11
+ export function createMoaModelPicker(
12
+ tui: TUI,
13
+ theme: Theme,
14
+ refs: ModelRef[],
15
+ config: MoaConfig,
16
+ currentThinking: ThinkingLevel,
17
+ ctx: ExtensionContext,
18
+ ): TwoPaneModelThinking {
19
+ // Filter edits rebuild the level list for the same model: keep the user's
20
+ // level then, and apply the saved/current default only when the model changes.
21
+ let lastKey: string | undefined;
22
+ return new TwoPaneModelThinking({
23
+ theme,
24
+ refs,
25
+ levelOptions: registryLevelOptions(ctx.modelRegistry, { fallback: ["off"] }),
26
+ preferredLevel: (ref, options, previous) => {
27
+ const key = modelRefLabel(ref);
28
+ const keep = key === lastKey && previous !== undefined && options.includes(previous);
29
+ lastKey = key;
30
+ return keep ? previous : defaultThinkingForModel(key, config, currentThinking, options);
31
+ },
32
+ requestRender: () => tui.requestRender(),
33
+ marker: SELECTOR_POINTER,
34
+ });
272
35
  }
@@ -11,9 +11,9 @@
11
11
  import type { ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
12
12
  import { isKeyRelease, Key, matchesKey, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
13
13
  import { setTimeout as sleep } from "node:timers/promises";
14
+ import { shimmerString, type AnsiTheme } from "@underactive/pi-topping-primitives/activity-tui";
14
15
  import { formatElapsed } from "./agentStatus.ts";
15
16
  import { BRAILLE_SPINNER_FRAMES, ROUNDED_SINGLE_BOX, UI_TICK_MS, createFrame } from "./chrome.ts";
16
- import { shimmerString, type ShimmerTheme } from "./shimmer.ts";
17
17
 
18
18
  const OVERLAY_MARGIN = 1;
19
19
  /** Grace before mounting: work that settles inside this never flashes a card. */
@@ -246,7 +246,7 @@ class WorkingOverlayComponent implements Component {
246
246
 
247
247
  const spin = th.fg("accent", BRAILLE_SPINNER_FRAMES[this.spinFrame % BRAILLE_SPINNER_FRAMES.length]!);
248
248
  const detailText = th.getFgAnsi
249
- ? shimmerString(this.options.detail, Date.now() - this.startedAt, th as ShimmerTheme, "ltr", "normal")
249
+ ? shimmerString(this.options.detail, Date.now() - this.startedAt, th as AnsiTheme, "ltr", "normal")
250
250
  : th.fg("dim", this.options.detail);
251
251
  const detailRow = frame.row(`${spin} ${detailText}`);
252
252