@pi-unipi/footer 3.0.0-alpha.30 → 3.0.0-alpha.31

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/index.ts CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "3.0.0-alpha.30",
3
+ "version": "3.0.0-alpha.31",
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": "3.0.0-alpha.30",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.30"
35
+ "@pi-unipi/core": "3.0.0-alpha.31",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.31"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
package/src/index.ts CHANGED
@@ -12,25 +12,21 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
12
12
  import type { TUI } from "@earendil-works/pi-tui";
13
13
  import {
14
14
  UNIPI_EVENTS,
15
- emitEvent,
15
+ STICKY_EVENTS,
16
+ bus,
16
17
  UNIPI_PREFIX,
17
18
  FOOTER_COMMANDS,
18
- getSharedFusionStatus,
19
- getSharedKanboardStatus,
20
- getSharedLongHorizonMode,
21
- getSharedPlanPermissionStatus,
22
19
  getPackageVersion,
23
20
  findPackageRoot,
24
21
  } from "@pi-unipi/core";
25
22
  import { getFooterRegistry, type FooterRegistry } from "./registry/index.js";
26
- import { subscribeToEvents } from "./events.js";
27
23
  import { loadFooterSettings, saveFooterSettings } from "./config.js";
28
24
  import { registerCommands } from "./commands.js";
29
25
  import { GlanceEditor } from "./glance-editor.js";
30
26
  import type { GlanceStatus } from "./glance-editor.js";
31
27
  import { tpsTracker } from "./tps-tracker.js";
32
28
  import { renderProcessLine, countBgProcesses } from "./process-line.js";
33
- import { MODE_LABELS } from "./segments/long-horizon.js";
29
+ import { lhModeLabel } from "./segments/long-horizon.js";
34
30
  import { SessionScanner } from "./session-scan.js";
35
31
  import { renderSessionStrip, stripVisibleAtRows } from "./strip.js";
36
32
  import { setIconStyle } from "./rendering/icons.js";
@@ -44,7 +40,6 @@ const VERSION = getPackageVersion(
44
40
  export interface FooterState {
45
41
  enabled: boolean;
46
42
  registry: FooterRegistry;
47
- unsubscribeEvents: (() => void) | null;
48
43
  piContext: unknown;
49
44
  footerData: unknown;
50
45
  tuiRef: TUI | null | undefined;
@@ -53,6 +48,8 @@ export interface FooterState {
53
48
  glanceInstalled: boolean;
54
49
  /** Deferred install timer (focus-safety deferral past the boot overlay) */
55
50
  glanceInstallTimer: ReturnType<typeof setTimeout> | null;
51
+ /** Bus sticky-state subscriptions (requestRender on change); re-created per session. */
52
+ busUnsubs: Array<() => void>;
56
53
  /** Incremental branch scanner (tracker feeding + strip snapshot). */
57
54
  scanner: SessionScanner;
58
55
  /** Cheap key of everything the tick renders — requestRender only on change. */
@@ -68,13 +65,13 @@ export default function footerExtension(pi: ExtensionAPI): void {
68
65
  const state: FooterState = {
69
66
  enabled: true,
70
67
  registry: getFooterRegistry(),
71
- unsubscribeEvents: null,
72
68
  piContext: null,
73
69
  footerData: null,
74
70
  tuiRef: null,
75
71
  refreshTimer: null,
76
72
  glanceInstalled: false,
77
73
  glanceInstallTimer: null,
74
+ busUnsubs: [],
78
75
  scanner: new SessionScanner(),
79
76
  lastRenderKey: "",
80
77
  activeToolCalls: 0,
@@ -129,7 +126,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
129
126
  setIconStyle(settings.iconStyle);
130
127
 
131
128
  // Announce the module regardless of UI availability.
132
- emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
129
+ bus.emit(UNIPI_EVENTS.MODULE_READY, {
133
130
  name: "@pi-unipi/footer",
134
131
  version: VERSION,
135
132
  commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`],
@@ -138,8 +135,15 @@ export default function footerExtension(pi: ExtensionAPI): void {
138
135
 
139
136
  if (!settings.enabled || !ctx.hasUI) return;
140
137
 
141
- // Subscribe to events
142
- state.unsubscribeEvents = subscribeToEvents(pi, state.registry);
138
+ // Glance frame data now rides the bus's sticky state; request a render
139
+ // whenever a publisher updates any of the four sticky keys. One set of
140
+ // subscriptions per session: drop the previous set before re-subscribing
141
+ // (session_start can fire again on the same pi; the bus also auto-removes
142
+ // these on session_shutdown).
143
+ for (const unsub of state.busUnsubs.splice(0)) unsub();
144
+ state.busUnsubs = [...STICKY_EVENTS].map((key) =>
145
+ bus.on(pi, key, () => (state.tuiRef as { requestRender?: () => void } | null | undefined)?.requestRender?.()),
146
+ );
143
147
 
144
148
  // Glance-style input surface (pi-glance-inspired). Preserves all default
145
149
  // editor behavior via CustomEditor subclassing; only paint differs.
@@ -168,8 +172,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
168
172
  clearTimeout(state.glanceInstallTimer);
169
173
  state.glanceInstallTimer = null;
170
174
  }
171
- state.unsubscribeEvents?.();
172
- state.unsubscribeEvents = null;
175
+ for (const unsub of state.busUnsubs.splice(0)) unsub();
173
176
  state.piContext = null;
174
177
  state.footerData = null;
175
178
  if (state.refreshTimer) {
@@ -381,31 +384,16 @@ export function installGlanceEditor(
381
384
  let modelName = (model?.name || model?.id || "") as string;
382
385
  if (modelName.startsWith("Claude ")) modelName = modelName.slice(7);
383
386
  const branch = (st.footerData as any)?.getGitBranch?.() ?? null;
384
- const fusion = getSharedFusionStatus() ?? null;
385
- // Pull the mode from the shared holder (set on both resume and live
386
- // turns); fall back to the registry data the event listener writes.
387
- const lhModeRaw = getSharedLongHorizonMode()
388
- ?? (st.registry.getGroupData("core") as { lhMode?: string } | undefined)?.lhMode;
389
- const lhMode = typeof lhModeRaw === "string" && lhModeRaw.length > 0
390
- ? MODE_LABELS[lhModeRaw] ?? lhModeRaw
391
- : null;
392
- const kanboard = getSharedKanboardStatus() ?? null;
393
- // Plan/permission state rides the same CORE registry data, written by
394
- // the workflow module's events (also re-emitted on session_start).
395
- const coreData = st.registry.getGroupData("core") as
396
- | { planMode?: boolean; permissionMode?: string }
397
- | undefined;
398
- const sharedBadges = getSharedPlanPermissionStatus();
387
+ const fusion = bus.get(UNIPI_EVENTS.FUSION_STATUS) ?? null;
388
+ const kanboard = bus.get(UNIPI_EVENTS.KANBOARD_STATUS) ?? null;
389
+ // Plan/permission mode is one sticky snapshot published by workflow.
390
+ const wf = bus.get(UNIPI_EVENTS.WORKFLOW_STATUS);
399
391
  return {
400
392
  workspace,
401
- lhMode,
402
- planMode: sharedBadges.planMode || coreData?.planMode === true,
393
+ lhMode: lhModeLabel(bus.get(UNIPI_EVENTS.LH_STATE)),
394
+ planMode: wf?.planMode === true,
403
395
  permissionMode:
404
- typeof sharedBadges.permissionMode === "string" && sharedBadges.permissionMode.length > 0
405
- ? sharedBadges.permissionMode
406
- : typeof coreData?.permissionMode === "string" && coreData.permissionMode.length > 0
407
- ? coreData.permissionMode
408
- : null,
396
+ typeof wf?.permissionMode === "string" && wf.permissionMode.length > 0 ? wf.permissionMode : null,
409
397
  branch: typeof branch === "string" ? branch : null,
410
398
  contextPct: typeof usage?.percent === "number" ? usage.percent : null,
411
399
  contextWindow: typeof usage?.contextWindow === "number" ? usage.contextWindow : 0,
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * @pi-unipi/footer — Shared status data store
3
3
  *
4
- * A tiny cache keyed by group id ("core" today). Event handlers write mode /
5
- * permission fallbacks here; the glance frame reads them when the shared
6
- * status holders (core's getShared* helpers) have no value yet.
4
+ * A tiny cache keyed by group id. Glance widgets cache per-group render data
5
+ * here; the bus (bus.ts) is the cross-module state channel.
7
6
  */
8
7
 
9
8
  /** Type for the reactive update callback */
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * @pi-unipi/footer — Long-horizon mode labels
3
3
  *
4
- * Maps the mode ids carried by LONG_HORIZON_MODE_RESOLVED to display labels:
5
- * "Goal Mode" · "Ralph Mode" · "Swarm Mode" · "Graph Mode" · "Regular Mode".
4
+ * Maps the mode ids carried by LONG_HORIZON_MODE_RESOLVED / the bus's sticky
5
+ * LH_STATE to display labels: "Goal Mode" · "Ralph Mode" · "Swarm Mode" ·
6
+ * "Graph Mode" · "Regular Mode".
6
7
  */
7
8
 
9
+ import type { LhStateEvent } from "@pi-unipi/core";
10
+
8
11
  export const MODE_LABELS: Record<string, string> = {
9
12
  goal: "Goal Mode",
10
13
  ralph: "Ralph Mode",
@@ -12,3 +15,11 @@ export const MODE_LABELS: Record<string, string> = {
12
15
  graph: "Graph Mode",
13
16
  none: "Regular Mode",
14
17
  };
18
+
19
+ /** Glance title for the bus's sticky LH_STATE payload. A parked owner while
20
+ * the session is regular shows "<Mode> · paused". Null without state. */
21
+ export function lhModeLabel(state?: LhStateEvent): string | null {
22
+ if (!state) return null;
23
+ if (state.paused && state.mode === "none") return `${MODE_LABELS[state.paused] ?? state.paused} · paused`;
24
+ return MODE_LABELS[state.mode] ?? state.mode;
25
+ }
package/src/events.ts DELETED
@@ -1,83 +0,0 @@
1
- /**
2
- * @pi-unipi/footer — Event subscription wiring
3
- *
4
- * The glance frame reads mode/permission state from core's shared holders
5
- * (getShared* helpers). These handlers feed the registry's "core" data as the
6
- * FALLBACK for when a shared holder has no value yet (e.g. before the
7
- * workflow module re-emits on resume).
8
- *
9
- * Note: pi.events.on() returns an unsubscribe function directly.
10
- */
11
-
12
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
- import { UNIPI_EVENTS } from "@pi-unipi/core";
14
- import type { FooterRegistry } from "./registry/index.js";
15
-
16
- /** Cleanup function returned by subscribeToEvents */
17
- type UnsubscribeFn = () => void;
18
-
19
- function updateCoreData(registry: FooterRegistry, patch: Record<string, unknown>): void {
20
- const existing = registry.getGroupData("core") as Record<string, unknown> | undefined;
21
- registry.updateData("core", { ...existing, ...patch });
22
- }
23
-
24
- /**
25
- * Subscribe to the UNIPI_EVENTS the glance frame still consumes.
26
- * Returns an unsubscribe function for cleanup on session shutdown.
27
- */
28
- export function subscribeToEvents(
29
- pi: ExtensionAPI,
30
- registry: FooterRegistry,
31
- ): UnsubscribeFn {
32
- const unsubscribers: UnsubscribeFn[] = [];
33
-
34
- const on = (event: string, handler: (data: unknown) => void) => {
35
- unsubscribers.push(pi.events.on(event, handler));
36
- };
37
-
38
- // ─── Plan mode / permission mode (workflow) ─────────────────────────────
39
-
40
- on(UNIPI_EVENTS.PLAN_MODE_CHANGED, (event: unknown) => {
41
- try {
42
- const active = (event as { active?: boolean })?.active === true;
43
- updateCoreData(registry, { planMode: active });
44
- } catch {
45
- // Silently ignore — event handler errors are non-blocking.
46
- }
47
- });
48
-
49
- on(UNIPI_EVENTS.PERMISSION_MODE_CHANGED, (event: unknown) => {
50
- try {
51
- const mode = (event as { mode?: string })?.mode;
52
- if (typeof mode === "string" && mode.length > 0) {
53
- updateCoreData(registry, { permissionMode: mode });
54
- }
55
- } catch {
56
- // Silently ignore — event handler errors are non-blocking.
57
- }
58
- });
59
-
60
- // ─── Long-horizon mode ──────────────────────────────────────────────────
61
-
62
- on(UNIPI_EVENTS.LONG_HORIZON_MODE_RESOLVED, (event: unknown) => {
63
- try {
64
- const mode = (event as { mode?: string })?.mode;
65
- if (typeof mode === "string" && mode.length > 0) {
66
- updateCoreData(registry, { lhMode: mode });
67
- }
68
- } catch {
69
- // Silently ignore — event handler errors are non-blocking.
70
- }
71
- });
72
-
73
- // Return composite unsubscribe function
74
- return () => {
75
- for (const unsub of unsubscribers) {
76
- try {
77
- unsub();
78
- } catch {
79
- // Ignore cleanup errors
80
- }
81
- }
82
- };
83
- }