@pi-unipi/footer 3.0.0-alpha.30 → 3.0.0-alpha.32
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 +0 -0
- package/package.json +3 -3
- package/src/index.ts +24 -36
- package/src/registry/index.ts +2 -3
- package/src/segments/long-horizon.ts +13 -2
- package/src/events.ts +0 -83
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.
|
|
3
|
+
"version": "3.0.0-alpha.32",
|
|
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.
|
|
36
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
35
|
+
"@pi-unipi/core": "3.0.0-alpha.32",
|
|
36
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.32"
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
142
|
-
|
|
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.
|
|
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 =
|
|
385
|
-
|
|
386
|
-
//
|
|
387
|
-
const
|
|
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:
|
|
393
|
+
lhMode: lhModeLabel(bus.get(UNIPI_EVENTS.LH_STATE)),
|
|
394
|
+
planMode: wf?.planMode === true,
|
|
403
395
|
permissionMode:
|
|
404
|
-
typeof
|
|
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,
|
package/src/registry/index.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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
|
|
5
|
-
* "Goal Mode" · "Ralph Mode" · "Swarm 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
|
-
}
|