@pi-unipi/footer 2.16.1 → 2.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.16.1",
3
+ "version": "2.17.2",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.16.1",
36
- "@pi-unipi/background-tasks": "2.16.1"
35
+ "@pi-unipi/core": "2.17.0",
36
+ "@pi-unipi/background-tasks": "2.17.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -35,6 +35,12 @@ export interface GlanceStatus {
35
35
  modelName: string;
36
36
  /** Current thinking level (already "off"-filtered by the provider). */
37
37
  thinkingLevel: string | null;
38
+ /**
39
+ * Active Fusion pair, when selected. When set, the model slot renders
40
+ * `Fusion · <lead> <effort> ◆ <sidekick>` — lead lit, sidekick muted —
41
+ * and the plain thinking slot is suppressed (efforts are inline).
42
+ */
43
+ fusion: { leadName: string; leadEffort: string; sidekickName: string; sidekickEffort: string; savedUsd?: number } | null;
38
44
  }
39
45
 
40
46
  const BORDER = {
@@ -47,6 +53,9 @@ const BORDER = {
47
53
  } as const;
48
54
 
49
55
  const SEP = " \u2502 "; // │
56
+ const RESET = "\x1b[0m";
57
+ const LEAD_FG = "\x1b[1m\x1b[38;2;181;189;104m"; // bold accent (matches success tint)
58
+ const DIM_FG = "\x1b[38;2;120;124;134m"; // muted grey
50
59
 
51
60
  /**
52
61
  * Every invisible sequence pi-tui embeds in editor lines: CSI SGR, OSC 133
@@ -216,9 +225,19 @@ export class GlanceEditor extends CustomEditor {
216
225
  const pctLabel = ctxPct !== null ? `${Math.round(ctxPct)}%` : "?%";
217
226
  const winLabel = st.contextWindow > 0 ? `/${fmtTokens(st.contextWindow)}` : "";
218
227
  rightParts.push(`${pctLabel}${winLabel}`);
219
- if (st.modelName) rightParts.push(st.modelName);
220
- if (st.thinkingLevel && st.thinkingLevel !== "off") {
221
- rightParts.push(`thinking:${st.thinkingLevel}`);
228
+ if (st.fusion) {
229
+ // The working lead is lit; the sidekick stays muted. Efforts are
230
+ // inline so the separate thinking slot is dropped.
231
+ const lead = `${st.fusion.leadName}${st.fusion.leadEffort ? ` ${st.fusion.leadEffort}` : ""}`;
232
+ const side = `${st.fusion.sidekickName}${st.fusion.sidekickEffort ? ` ${st.fusion.sidekickEffort}` : ""}`;
233
+ rightParts.push(
234
+ `${LEAD_FG}Fusion · ${lead}${RESET} ${DIM_FG}◆ ${side}${st.fusion.savedUsd !== undefined && st.fusion.savedUsd > 0.005 ? ` · saved $${st.fusion.savedUsd.toFixed(2)}` : ""}${RESET}`,
235
+ );
236
+ } else {
237
+ if (st.modelName) rightParts.push(st.modelName);
238
+ if (st.thinkingLevel && st.thinkingLevel !== "off") {
239
+ rightParts.push(`thinking:${st.thinkingLevel}`);
240
+ }
222
241
  }
223
242
  // Workspace label per icon style (glyph prefix, or workspace: in text mode).
224
243
  let cluster = rightParts.join(SEP);
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ExtensionAPI, Theme, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
- import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
10
+ import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS, getSharedFusionStatus } from "@pi-unipi/core";
11
11
  import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
12
12
  import { FooterRenderer } from "./rendering/renderer.js";
13
13
  import { subscribeToEvents } from "./events.js";
@@ -153,13 +153,10 @@ export default function footerExtension(pi: ExtensionAPI): void {
153
153
  // Glance-style input surface (pi-glance-inspired). Preserves all default
154
154
  // editor behavior via CustomEditor subclassing; only paint differs.
155
155
  //
156
- // FOCUS-SAFETY DEFERRAL: setEditorComponent() internally calls
157
- // ui.setFocus(newEditor). info-screen (loaded before us) opens its boot
158
- // dashboard during ITS session_start handler, so our session_start runs
159
- // while that overlay owns keyboard focus. Swapping now would steal focus
160
- // and strand the dashboard unclosable (q/Esc would type into the editor).
161
- // The boot overlay auto-closes after ~2s; we install after a grace period
162
- // longer than any sane bootTimeoutMs.
156
+ // FOCUS-SAFETY DEFERRAL: the timer remains a grace period for the boot
157
+ // dashboard, but installGlanceEditor also restores any overlay focus after
158
+ // setEditorComponent() calls ui.setFocus(newEditor). This covers the
159
+ // updater prompt too, so q/Esc cannot be stranded in the editor.
163
160
  state.glanceInstallTimer = setTimeout(() => installGlanceEditor(state, ctx), 3500);
164
161
 
165
162
  // Sync TPS cursor with persisted assistant messages so streaming-hook
@@ -420,6 +417,15 @@ function installGlanceEditor(
420
417
  ): void {
421
418
  if (st.glanceInstalled || !st.piContext || !st.glanceMode) return;
422
419
  try {
420
+ const tui = st.tuiRef as (import("@earendil-works/pi-tui").TUI & {
421
+ isOverlayFocused?: () => boolean;
422
+ getFocusedComponent?: () => import("@earendil-works/pi-tui").Component | null;
423
+ }) | null | undefined;
424
+ const overlayFocused = tui !== undefined && tui !== null
425
+ && (typeof tui.isOverlayFocused === "function" ? tui.isOverlayFocused() : tui.hasOverlay());
426
+ const overlayOwner = overlayFocused && typeof tui?.getFocusedComponent === "function"
427
+ ? tui.getFocusedComponent() ?? null
428
+ : null;
423
429
  const piCtx = st.piContext as Record<string, unknown> | undefined;
424
430
  const cwd = (piCtx?.sessionManager as any)?.getCwd?.() ?? (piCtx as any)?.cwd ?? process.cwd();
425
431
  const workspace = String(cwd).split("/").filter(Boolean).pop() ?? "~";
@@ -433,6 +439,7 @@ function installGlanceEditor(
433
439
  let modelName = (model?.name || model?.id || "") as string;
434
440
  if (modelName.startsWith("Claude ")) modelName = modelName.slice(7);
435
441
  const branch = (st.footerData as any)?.getGitBranch?.() ?? null;
442
+ const fusion = getSharedFusionStatus() ?? null;
436
443
  return {
437
444
  workspace,
438
445
  branch: typeof branch === "string" ? branch : null,
@@ -440,9 +447,11 @@ function installGlanceEditor(
440
447
  contextWindow: typeof usage?.contextWindow === "number" ? usage.contextWindow : 0,
441
448
  modelName,
442
449
  thinkingLevel: typeof p?.thinkingLevel === "string" ? p.thinkingLevel : null,
450
+ fusion,
443
451
  };
444
452
  }),
445
453
  );
454
+ if (overlayOwner && tui) tui.setFocus(overlayOwner);
446
455
  st.glanceInstalled = true;
447
456
  } catch {
448
457
  st.glanceInstalled = false;