pi-observability 1.0.1 → 1.3.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.
- package/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +15 -0
- package/.zed/settings.json +222 -0
- package/README.md +98 -31
- package/demo-preview.gif +0 -0
- package/extensions/lib/footer-engine/format.ts +67 -0
- package/extensions/lib/footer-engine/index.ts +55 -0
- package/extensions/lib/footer-engine/layout.ts +47 -0
- package/extensions/lib/footer-engine/segments.ts +95 -0
- package/extensions/lib/footer-engine/types.ts +56 -0
- package/extensions/lib/settings/domain.ts +161 -0
- package/extensions/lib/settings/index.ts +32 -0
- package/extensions/lib/settings/manager.ts +58 -0
- package/extensions/lib/settings/metadata.ts +114 -0
- package/extensions/lib/settings/storage.ts +38 -0
- package/extensions/lib/settings/tui.ts +44 -0
- package/extensions/lib/settings/types.ts +40 -0
- package/extensions/lib/storage/file-backend.ts +62 -0
- package/extensions/lib/storage/index.ts +33 -0
- package/extensions/lib/storage/json-store.ts +32 -0
- package/extensions/lib/storage/jsonl-store.ts +29 -0
- package/extensions/lib/storage/memory-backend.ts +37 -0
- package/extensions/lib/storage/types.ts +23 -0
- package/extensions/observability.ts +653 -558
- package/package.json +55 -48
|
@@ -5,615 +5,710 @@
|
|
|
5
5
|
* - Session input/output tokens & cost
|
|
6
6
|
* - Live TPS during streaming (chunk-based estimate)
|
|
7
7
|
* - Session runtime
|
|
8
|
-
* - Current model & git branch
|
|
8
|
+
* - Current model, thinking level, fast mode & git branch
|
|
9
9
|
* - Git diff stats (added/removed lines)
|
|
10
10
|
* - Context usage (current/max)
|
|
11
11
|
*
|
|
12
|
+
* It also prints the legacy TPS summary notification at the end of each
|
|
13
|
+
* agent run, so the standalone TPS extension is no longer needed.
|
|
14
|
+
*
|
|
12
15
|
* Commands:
|
|
13
16
|
* /obs - Print full observability dashboard + last 10 sessions
|
|
14
17
|
* /obs-toggle - Toggle the observability footer on/off
|
|
18
|
+
* /obs-settings - Open status bar settings (presets, segments, zones)
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
21
|
import { homedir } from "node:os";
|
|
19
22
|
import { join } from "node:path";
|
|
20
|
-
import type { AssistantMessage } from "@
|
|
23
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
21
24
|
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
ExtensionAPI,
|
|
26
|
+
ExtensionContext,
|
|
27
|
+
Theme as PiTheme,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import {
|
|
30
|
+
Key,
|
|
31
|
+
matchesKey,
|
|
32
|
+
SettingsList,
|
|
33
|
+
truncateToWidth,
|
|
34
|
+
visibleWidth,
|
|
35
|
+
} from "@earendil-works/pi-tui";
|
|
36
|
+
|
|
25
37
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
loadSettings,
|
|
39
|
+
saveSettings,
|
|
40
|
+
updateSetting,
|
|
41
|
+
toSettingsListItems,
|
|
42
|
+
type SettingsConfig,
|
|
43
|
+
} from "./lib/settings/index.js";
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
renderFooter,
|
|
47
|
+
fmtDuration,
|
|
48
|
+
fmtTokens,
|
|
49
|
+
shortenPath,
|
|
50
|
+
type FooterInput,
|
|
51
|
+
} from "./lib/footer-engine/index.js";
|
|
52
|
+
|
|
53
|
+
import { createFileStorage, type Storage } from "./lib/storage/index.js";
|
|
31
54
|
|
|
32
55
|
/* ───── Types ───── */
|
|
33
56
|
|
|
34
57
|
interface TurnRecord {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
turnIndex: number;
|
|
59
|
+
inputTokens: number;
|
|
60
|
+
outputTokens: number;
|
|
61
|
+
cost: number;
|
|
62
|
+
durationMs: number;
|
|
63
|
+
tps: number;
|
|
64
|
+
model: string;
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
interface PersistedTurn {
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
customType: "obs-turn";
|
|
69
|
+
data: TurnRecord;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
interface SessionState {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
startTime: number;
|
|
74
|
+
turns: TurnRecord[];
|
|
75
|
+
currentTurnStartTime: number | null;
|
|
76
|
+
currentTurnUpdateCount: number;
|
|
77
|
+
agentStartTime: number | null;
|
|
78
|
+
isStreaming: boolean;
|
|
79
|
+
footerEnabled: boolean;
|
|
80
|
+
fastModeSupported: boolean;
|
|
81
|
+
fastModeEnabled: boolean;
|
|
82
|
+
serviceTier: string | null;
|
|
83
|
+
showFullPath: boolean;
|
|
84
|
+
settings: SettingsConfig;
|
|
56
85
|
}
|
|
57
86
|
|
|
58
87
|
interface SessionSummary {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
endedAt: number;
|
|
89
|
+
runtimeMs: number;
|
|
90
|
+
turns: number;
|
|
91
|
+
inputTokens: number;
|
|
92
|
+
outputTokens: number;
|
|
93
|
+
cost: number;
|
|
94
|
+
model: string;
|
|
95
|
+
cwd: string;
|
|
96
|
+
branch: string | null;
|
|
68
97
|
}
|
|
69
98
|
|
|
70
99
|
/* ───── Helpers ───── */
|
|
71
100
|
|
|
72
|
-
function fmtDuration(ms: number): string {
|
|
73
|
-
if (!Number.isFinite(ms) || ms < 0) ms = 0;
|
|
74
|
-
const s = Math.floor(ms / 1000);
|
|
75
|
-
const h = Math.floor(s / 3600);
|
|
76
|
-
const m = Math.floor((s % 3600) / 60);
|
|
77
|
-
const sec = s % 60;
|
|
78
|
-
if (h > 0)
|
|
79
|
-
return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
|
|
80
|
-
return `${m}:${sec.toString().padStart(2, "0")}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function fmtTokens(n: number): string {
|
|
84
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
85
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
86
|
-
return `${n}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function shortenPath(p: string): string {
|
|
90
|
-
const home = homedir();
|
|
91
|
-
if (home && p.startsWith(home)) return p.replace(home, "~");
|
|
92
|
-
return p;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
101
|
function scanHistoricalTurns(ctx: ExtensionContext): TurnRecord[] {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const turns: TurnRecord[] = [];
|
|
103
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
104
|
+
if (entry.type === "custom" && entry.customType === "obs-turn") {
|
|
105
|
+
turns.push((entry as unknown as PersistedTurn).data);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return turns;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
function getSessionStartTime(ctx: ExtensionContext): number {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
const entries = ctx.sessionManager.getBranch();
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
if (typeof e.timestamp === "number" && Number.isFinite(e.timestamp)) {
|
|
115
|
+
return e.timestamp;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Date.now();
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
function alignCell(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
):
|
|
120
|
-
const vis = visibleWidth(str);
|
|
121
|
-
if (vis > width) return truncateToWidth(str, width);
|
|
122
|
-
const pad = width - vis;
|
|
123
|
-
return align === "right" ? " ".repeat(pad) + str : str + " ".repeat(pad);
|
|
121
|
+
function alignCell(str: string, width: number, align: "left" | "right" = "left"): string {
|
|
122
|
+
const vis = visibleWidth(str);
|
|
123
|
+
if (vis > width) return truncateToWidth(str, width);
|
|
124
|
+
const pad = width - vis;
|
|
125
|
+
return align === "right" ? " ".repeat(pad) + str : str + " ".repeat(pad);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
function getStringProp(value: unknown, key: string): string | undefined {
|
|
129
|
+
if (!value || typeof value !== "object") return undefined;
|
|
130
|
+
const prop = (value as Record<string, unknown>)[key];
|
|
131
|
+
return typeof prop === "string" ? prop : undefined;
|
|
132
|
+
}
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
const
|
|
134
|
+
function getServiceTierFromPayload(payload: unknown): string | null {
|
|
135
|
+
const tier = getStringProp(payload, "service_tier") ?? getStringProp(payload, "serviceTier");
|
|
136
|
+
return tier?.trim().toLowerCase() || null;
|
|
137
|
+
}
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return lines.map((l) => JSON.parse(l));
|
|
136
|
-
} catch {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
+
function isFastServiceTier(serviceTier: string | null): boolean {
|
|
140
|
+
// OpenAI's actual fast/priority tier is `priority`. Older/local shims may
|
|
141
|
+
// still emit `fast`, so keep accepting it for backwards-compatible display.
|
|
142
|
+
return serviceTier === "priority" || serviceTier === "fast";
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
function supportsFastMode(ctx: ExtensionContext): boolean {
|
|
146
|
+
const model = ctx.model;
|
|
147
|
+
if (!model) return false;
|
|
148
|
+
if (model.provider !== "openai" && model.provider !== "openai-codex") return false;
|
|
149
|
+
return model.api === "openai-responses" || model.api === "openai-codex-responses";
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
/* ───── Dashboard formatting ───── */
|
|
148
153
|
|
|
149
|
-
type
|
|
150
|
-
fg: (color: string, text: string) => string;
|
|
151
|
-
bold: (text: string) => string;
|
|
152
|
-
};
|
|
154
|
+
type DashboardTheme = Pick<PiTheme, "fg" | "bold">;
|
|
153
155
|
|
|
154
156
|
function buildDashboard(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
state: SessionState,
|
|
158
|
+
ctx: ExtensionContext,
|
|
159
|
+
branch: string | null,
|
|
160
|
+
history: SessionSummary[],
|
|
161
|
+
termWidth: number,
|
|
162
|
+
theme: DashboardTheme,
|
|
161
163
|
): string[] {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (tableW > termWidth && colW[colW.length - 1]! > 10) {
|
|
281
|
-
colW[colW.length - 1] = Math.max(
|
|
282
|
-
10,
|
|
283
|
-
colW[colW.length - 1]! - (tableW - termWidth),
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const pad = " ";
|
|
288
|
-
const hdr = ` ${headers.map((h, i) => alignCell(h, colW[i]!)).join(pad)}`;
|
|
289
|
-
lines.push(theme.fg("dim", hdr));
|
|
290
|
-
lines.push(B(` ${"─".repeat(visibleWidth(hdr) - 2)}`));
|
|
291
|
-
for (const row of rows) {
|
|
292
|
-
const cells = row.map((c, i) =>
|
|
293
|
-
alignCell(c, colW[i]!, i === 0 || i >= 2 ? "left" : "right"),
|
|
294
|
-
);
|
|
295
|
-
lines.push(` ${cells.join(pad)}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return lines;
|
|
164
|
+
const runtime = Date.now() - state.startTime;
|
|
165
|
+
const totalIn = state.turns.reduce((s, t) => s + t.inputTokens, 0);
|
|
166
|
+
const totalOut = state.turns.reduce((s, t) => s + t.outputTokens, 0);
|
|
167
|
+
const totalCost = state.turns.reduce((s, t) => s + t.cost, 0);
|
|
168
|
+
|
|
169
|
+
const B = (s: string) => theme.fg("border", s);
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
|
|
172
|
+
// ── Summary Card ──
|
|
173
|
+
const summaryLines = [
|
|
174
|
+
theme.bold("Agent Observability Dashboard"),
|
|
175
|
+
`Runtime: ${fmtDuration(runtime)} Dir: ${shortenPath(ctx.cwd)}`,
|
|
176
|
+
branch
|
|
177
|
+
? `Branch: ${branch} Model: ${ctx.model?.id ?? "none"}`
|
|
178
|
+
: `Model: ${ctx.model?.id ?? "none"}`,
|
|
179
|
+
state.serviceTier
|
|
180
|
+
? `Service tier: ${state.serviceTier}${state.fastModeEnabled ? " (fast)" : ""}`
|
|
181
|
+
: `Fast mode: ${state.fastModeSupported ? "available" : "not available"}`,
|
|
182
|
+
`Tokens: ↑${fmtTokens(totalIn)} ↓${fmtTokens(totalOut)}`,
|
|
183
|
+
`Cost: $${totalCost.toFixed(6)}`,
|
|
184
|
+
];
|
|
185
|
+
const summaryW = Math.min(Math.max(...summaryLines.map((c) => visibleWidth(c))) + 4, termWidth);
|
|
186
|
+
const inner = summaryW - 4;
|
|
187
|
+
const padSummary = (text: string) => {
|
|
188
|
+
const safe = truncateToWidth(text, inner);
|
|
189
|
+
const vis = visibleWidth(safe);
|
|
190
|
+
const pad = Math.max(0, inner - vis);
|
|
191
|
+
return B("│ ") + safe + B(`${" ".repeat(pad)} │`);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
lines.push(B(`┌${"─".repeat(summaryW - 2)}┐`));
|
|
195
|
+
lines.push(padSummary(summaryLines[0]));
|
|
196
|
+
lines.push(B(`├${"─".repeat(summaryW - 2)}┤`));
|
|
197
|
+
for (let i = 1; i < summaryLines.length; i++) {
|
|
198
|
+
lines.push(padSummary(summaryLines[i]));
|
|
199
|
+
}
|
|
200
|
+
lines.push(B(`└${"─".repeat(summaryW - 2)}┘`));
|
|
201
|
+
|
|
202
|
+
// ── Turns Table ──
|
|
203
|
+
if (state.turns.length > 0) {
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(` ${theme.bold(theme.fg("accent", `TURNS (${state.turns.length})`))}`);
|
|
206
|
+
|
|
207
|
+
const headers = ["#", "Input", "Output", "Time", "TPS", "Cost", "Model"];
|
|
208
|
+
const rows = state.turns.map((t, i) => [
|
|
209
|
+
`${i + 1}`,
|
|
210
|
+
`↑${fmtTokens(t.inputTokens)}`,
|
|
211
|
+
`↓${fmtTokens(t.outputTokens)}`,
|
|
212
|
+
fmtDuration(t.durationMs),
|
|
213
|
+
`${t.tps.toFixed(1)}`,
|
|
214
|
+
`$${t.cost.toFixed(2)}`,
|
|
215
|
+
t.model,
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const colW = headers.map((h, i) =>
|
|
219
|
+
Math.max(visibleWidth(h), ...rows.map((r) => visibleWidth(r[i]))),
|
|
220
|
+
);
|
|
221
|
+
const tableW = colW.reduce((a, b) => a + b, 0) + 2 * (colW.length - 1) + 2;
|
|
222
|
+
if (tableW > termWidth && colW[colW.length - 1]! > 10) {
|
|
223
|
+
colW[colW.length - 1] = Math.max(10, colW[colW.length - 1]! - (tableW - termWidth));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const pad = " ";
|
|
227
|
+
const hdr = ` ${headers.map((h, i) => alignCell(h, colW[i]!)).join(pad)}`;
|
|
228
|
+
lines.push(theme.fg("dim", hdr));
|
|
229
|
+
lines.push(B(` ${"─".repeat(visibleWidth(hdr) - 2)}`));
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
const cells = row.map((c, i) => alignCell(c, colW[i]!, i === 0 || i >= 3 ? "left" : "right"));
|
|
232
|
+
lines.push(` ${cells.join(pad)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── History Table ──
|
|
237
|
+
if (history.length > 0) {
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push(` ${theme.bold(theme.fg("accent", "LAST 10 SESSIONS"))}`);
|
|
240
|
+
|
|
241
|
+
const headers = ["When", "Duration", "Turns", "Input", "Output", "Cost", "Model"];
|
|
242
|
+
const rows = history
|
|
243
|
+
.slice()
|
|
244
|
+
.reverse()
|
|
245
|
+
.map((h) => {
|
|
246
|
+
const date = new Date(h.endedAt).toLocaleDateString("en-US", {
|
|
247
|
+
month: "short",
|
|
248
|
+
day: "numeric",
|
|
249
|
+
hour: "2-digit",
|
|
250
|
+
minute: "2-digit",
|
|
251
|
+
});
|
|
252
|
+
return [
|
|
253
|
+
date,
|
|
254
|
+
fmtDuration(h.runtimeMs),
|
|
255
|
+
`${h.turns}`,
|
|
256
|
+
`↑${fmtTokens(h.inputTokens)}`,
|
|
257
|
+
`↓${fmtTokens(h.outputTokens)}`,
|
|
258
|
+
`$${h.cost.toFixed(2)}`,
|
|
259
|
+
h.model,
|
|
260
|
+
];
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const colW = headers.map((h, i) =>
|
|
264
|
+
Math.max(visibleWidth(h), ...rows.map((r) => visibleWidth(r[i]))),
|
|
265
|
+
);
|
|
266
|
+
const tableW = colW.reduce((a, b) => a + b, 0) + 2 * (colW.length - 1) + 2;
|
|
267
|
+
if (tableW > termWidth && colW[colW.length - 1]! > 10) {
|
|
268
|
+
colW[colW.length - 1] = Math.max(10, colW[colW.length - 1]! - (tableW - termWidth));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const pad = " ";
|
|
272
|
+
const hdr = ` ${headers.map((h, i) => alignCell(h, colW[i]!)).join(pad)}`;
|
|
273
|
+
lines.push(theme.fg("dim", hdr));
|
|
274
|
+
lines.push(B(` ${"─".repeat(visibleWidth(hdr) - 2)}`));
|
|
275
|
+
for (const row of rows) {
|
|
276
|
+
const cells = row.map((c, i) => alignCell(c, colW[i]!, i === 0 || i >= 2 ? "left" : "right"));
|
|
277
|
+
lines.push(` ${cells.join(pad)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return lines;
|
|
300
282
|
}
|
|
301
283
|
|
|
302
284
|
/* ───── Extension ───── */
|
|
303
285
|
|
|
304
286
|
export default function (pi: ExtensionAPI) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
287
|
+
const storage: Storage = createFileStorage({
|
|
288
|
+
dir: join(homedir(), ".pi", "agent", "observability"),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const state: SessionState = {
|
|
292
|
+
startTime: Date.now(),
|
|
293
|
+
turns: [],
|
|
294
|
+
currentTurnStartTime: null,
|
|
295
|
+
currentTurnUpdateCount: 0,
|
|
296
|
+
agentStartTime: null,
|
|
297
|
+
isStreaming: false,
|
|
298
|
+
footerEnabled: true,
|
|
299
|
+
fastModeSupported: false,
|
|
300
|
+
fastModeEnabled: false,
|
|
301
|
+
serviceTier: null,
|
|
302
|
+
showFullPath: false,
|
|
303
|
+
settings: {
|
|
304
|
+
version: 1,
|
|
305
|
+
preset: "standard",
|
|
306
|
+
segments: {
|
|
307
|
+
modelThink: true,
|
|
308
|
+
runtime: true,
|
|
309
|
+
pwd: true,
|
|
310
|
+
git: true,
|
|
311
|
+
contextUsage: true,
|
|
312
|
+
contextProgress: true,
|
|
313
|
+
contextPercentage: true,
|
|
314
|
+
contextNumbers: true,
|
|
315
|
+
tokens: true,
|
|
316
|
+
tps: true,
|
|
317
|
+
cost: true,
|
|
318
|
+
},
|
|
319
|
+
contextZones: { expert: 70, warning: 85 },
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/* ─── Lifecycle ─── */
|
|
324
|
+
|
|
325
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
326
|
+
state.startTime = getSessionStartTime(ctx);
|
|
327
|
+
state.turns = scanHistoricalTurns(ctx);
|
|
328
|
+
state.currentTurnStartTime = null;
|
|
329
|
+
state.currentTurnUpdateCount = 0;
|
|
330
|
+
state.agentStartTime = null;
|
|
331
|
+
state.isStreaming = false;
|
|
332
|
+
state.fastModeSupported = supportsFastMode(ctx);
|
|
333
|
+
state.fastModeEnabled = false;
|
|
334
|
+
state.serviceTier = null;
|
|
335
|
+
state.settings = await loadSettings(storage);
|
|
336
|
+
|
|
337
|
+
if (state.footerEnabled && ctx.hasUI) {
|
|
338
|
+
setupFooter(ctx);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
pi.on("agent_start", async () => {
|
|
343
|
+
state.agentStartTime = Date.now();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
pi.on("turn_start", async (_event, _ctx) => {
|
|
347
|
+
state.currentTurnStartTime = Date.now();
|
|
348
|
+
state.currentTurnUpdateCount = 0;
|
|
349
|
+
state.isStreaming = true;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
353
|
+
state.fastModeSupported = supportsFastMode(ctx);
|
|
354
|
+
state.fastModeEnabled = false;
|
|
355
|
+
state.serviceTier = null;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
359
|
+
state.serviceTier = getServiceTierFromPayload(event.payload);
|
|
360
|
+
state.fastModeEnabled = isFastServiceTier(state.serviceTier);
|
|
361
|
+
state.fastModeSupported = supportsFastMode(ctx) || state.fastModeEnabled;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
pi.on("message_update", async (_event, _ctx) => {
|
|
365
|
+
state.currentTurnUpdateCount++;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
369
|
+
const duration = state.currentTurnStartTime ? Date.now() - state.currentTurnStartTime : 0;
|
|
370
|
+
|
|
371
|
+
let inputTokens = 0;
|
|
372
|
+
let outputTokens = 0;
|
|
373
|
+
let cost = 0;
|
|
374
|
+
|
|
375
|
+
const branch = ctx.sessionManager.getBranch();
|
|
376
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
377
|
+
const entry = branch[i];
|
|
378
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
379
|
+
const m = entry.message as AssistantMessage;
|
|
380
|
+
inputTokens = m.usage?.input ?? 0;
|
|
381
|
+
outputTokens = m.usage?.output ?? 0;
|
|
382
|
+
cost = m.usage?.cost?.total ?? 0;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const tps = duration > 0 && outputTokens >= 0 ? outputTokens / (duration / 1000) : 0;
|
|
388
|
+
|
|
389
|
+
const record: TurnRecord = {
|
|
390
|
+
turnIndex: event.turnIndex,
|
|
391
|
+
inputTokens,
|
|
392
|
+
outputTokens,
|
|
393
|
+
cost,
|
|
394
|
+
durationMs: duration,
|
|
395
|
+
tps,
|
|
396
|
+
model: ctx.model?.id ?? "unknown",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
state.turns.push(record);
|
|
400
|
+
state.isStreaming = false;
|
|
401
|
+
state.currentTurnStartTime = null;
|
|
402
|
+
state.currentTurnUpdateCount = 0;
|
|
403
|
+
|
|
404
|
+
pi.appendEntry("obs-turn", record);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
408
|
+
state.isStreaming = false;
|
|
409
|
+
|
|
410
|
+
if (!ctx.hasUI || state.agentStartTime === null) {
|
|
411
|
+
state.agentStartTime = null;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const elapsedMs = Date.now() - state.agentStartTime;
|
|
416
|
+
state.agentStartTime = null;
|
|
417
|
+
if (elapsedMs <= 0) return;
|
|
418
|
+
|
|
419
|
+
let input = 0;
|
|
420
|
+
let output = 0;
|
|
421
|
+
let cacheRead = 0;
|
|
422
|
+
let cacheWrite = 0;
|
|
423
|
+
let totalTokens = 0;
|
|
424
|
+
|
|
425
|
+
for (const message of event.messages) {
|
|
426
|
+
if (message.role !== "assistant") continue;
|
|
427
|
+
input += message.usage?.input ?? 0;
|
|
428
|
+
output += message.usage?.output ?? 0;
|
|
429
|
+
cacheRead += message.usage?.cacheRead ?? 0;
|
|
430
|
+
cacheWrite += message.usage?.cacheWrite ?? 0;
|
|
431
|
+
totalTokens += message.usage?.totalTokens ?? 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (output <= 0) return;
|
|
435
|
+
|
|
436
|
+
const elapsedSeconds = elapsedMs / 1000;
|
|
437
|
+
const tokensPerSecond = output / elapsedSeconds;
|
|
438
|
+
ctx.ui.notify(
|
|
439
|
+
`TPS ${tokensPerSecond.toFixed(1)} tok/s. out ${output.toLocaleString()}, in ${input.toLocaleString()}, cache r/w ${cacheRead.toLocaleString()}/${cacheWrite.toLocaleString()}, total ${totalTokens.toLocaleString()}, ${elapsedSeconds.toFixed(1)}s`,
|
|
440
|
+
"info",
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
445
|
+
const totalIn = state.turns.reduce((s, t) => s + t.inputTokens, 0);
|
|
446
|
+
const totalOut = state.turns.reduce((s, t) => s + t.outputTokens, 0);
|
|
447
|
+
const totalCost = state.turns.reduce((s, t) => s + t.cost, 0);
|
|
448
|
+
const runtime = Date.now() - state.startTime;
|
|
449
|
+
|
|
450
|
+
let branch: string | null = null;
|
|
451
|
+
try {
|
|
452
|
+
const result = await pi.exec("git", ["branch", "--show-current"], {
|
|
453
|
+
cwd: ctx.cwd,
|
|
454
|
+
});
|
|
455
|
+
branch = result.stdout?.trim() || null;
|
|
456
|
+
} catch {
|
|
457
|
+
branch = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const summary: SessionSummary = {
|
|
461
|
+
endedAt: Date.now(),
|
|
462
|
+
runtimeMs: runtime,
|
|
463
|
+
turns: state.turns.length,
|
|
464
|
+
inputTokens: totalIn,
|
|
465
|
+
outputTokens: totalOut,
|
|
466
|
+
cost: totalCost,
|
|
467
|
+
model: ctx.model?.id ?? "unknown",
|
|
468
|
+
cwd: ctx.cwd,
|
|
469
|
+
branch,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const historyStore = storage.jsonl<SessionSummary>("history");
|
|
473
|
+
await historyStore.append(summary);
|
|
474
|
+
await historyStore.trim({ keepLast: 10 });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
/* ─── Footer ─── */
|
|
478
|
+
|
|
479
|
+
function setupFooter(ctx: ExtensionContext) {
|
|
480
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
481
|
+
let diffAdded = 0;
|
|
482
|
+
let diffRemoved = 0;
|
|
483
|
+
|
|
484
|
+
async function refreshDiff() {
|
|
485
|
+
try {
|
|
486
|
+
const result = await pi.exec("git", ["diff", "--numstat"], {
|
|
487
|
+
cwd: ctx.cwd,
|
|
488
|
+
});
|
|
489
|
+
if (result.code === 0 && result.stdout) {
|
|
490
|
+
let added = 0;
|
|
491
|
+
let removed = 0;
|
|
492
|
+
for (const line of result.stdout.split("\n")) {
|
|
493
|
+
const parts = line.trim().split(/\s+/);
|
|
494
|
+
if (parts.length >= 2) {
|
|
495
|
+
const a = parseInt(parts[0], 10);
|
|
496
|
+
const b = parseInt(parts[1], 10);
|
|
497
|
+
if (!Number.isNaN(a)) added += a;
|
|
498
|
+
if (!Number.isNaN(b)) removed += b;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
diffAdded = added;
|
|
502
|
+
diffRemoved = removed;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
/* ignore */
|
|
507
|
+
}
|
|
508
|
+
diffAdded = 0;
|
|
509
|
+
diffRemoved = 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
refreshDiff();
|
|
513
|
+
|
|
514
|
+
const unsubBranch = footerData.onBranchChange(() => {
|
|
515
|
+
refreshDiff();
|
|
516
|
+
tui.requestRender();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const timer = setInterval(() => {
|
|
520
|
+
refreshDiff();
|
|
521
|
+
tui.requestRender();
|
|
522
|
+
}, 1000);
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
dispose() {
|
|
526
|
+
unsubBranch();
|
|
527
|
+
clearInterval(timer);
|
|
528
|
+
},
|
|
529
|
+
invalidate() {},
|
|
530
|
+
render(width: number): string[] {
|
|
531
|
+
let totalIn = 0;
|
|
532
|
+
let totalOut = 0;
|
|
533
|
+
let totalCost = 0;
|
|
534
|
+
for (const t of state.turns) {
|
|
535
|
+
totalIn += t.inputTokens;
|
|
536
|
+
totalOut += t.outputTokens;
|
|
537
|
+
totalCost += t.cost;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const lastTurnTps = state.turns.length > 0 ? state.turns[state.turns.length - 1]!.tps : 0;
|
|
541
|
+
|
|
542
|
+
const input: FooterInput = {
|
|
543
|
+
model: ctx.model?.id ?? "no-model",
|
|
544
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
545
|
+
runtimeMs: Date.now() - state.startTime,
|
|
546
|
+
isStreaming: state.isStreaming,
|
|
547
|
+
currentTurnStartTime: state.currentTurnStartTime,
|
|
548
|
+
currentTurnUpdateCount: state.currentTurnUpdateCount,
|
|
549
|
+
lastTurnTps,
|
|
550
|
+
totalInputTokens: totalIn,
|
|
551
|
+
totalOutputTokens: totalOut,
|
|
552
|
+
totalCost,
|
|
553
|
+
fastModeSupported: state.fastModeSupported,
|
|
554
|
+
fastModeEnabled: state.fastModeEnabled,
|
|
555
|
+
serviceTier: state.serviceTier,
|
|
556
|
+
contextUsage: ctx.getContextUsage() ?? null,
|
|
557
|
+
cwd: ctx.cwd,
|
|
558
|
+
showFullPath: state.showFullPath,
|
|
559
|
+
gitBranch: footerData.getGitBranch(),
|
|
560
|
+
gitDiffAdded: diffAdded,
|
|
561
|
+
gitDiffRemoved: diffRemoved,
|
|
562
|
+
settings: state.settings,
|
|
563
|
+
theme,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return renderFooter(input, width);
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function teardownFooter(ctx: ExtensionContext) {
|
|
573
|
+
ctx.ui.setFooter(undefined);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* ─── Commands ─── */
|
|
577
|
+
|
|
578
|
+
pi.registerCommand("obs", {
|
|
579
|
+
description: "Show observability dashboard (tokens, cost, TPS, runtime, history)",
|
|
580
|
+
handler: async (_args, ctx) => {
|
|
581
|
+
const branchResult = await pi.exec("git", ["branch", "--show-current"], {
|
|
582
|
+
cwd: ctx.cwd,
|
|
583
|
+
});
|
|
584
|
+
const branch = branchResult.stdout?.trim() || null;
|
|
585
|
+
const history = await storage.jsonl<SessionSummary>("history").read();
|
|
586
|
+
|
|
587
|
+
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
|
588
|
+
let cachedWidth = 0;
|
|
589
|
+
let cachedLines: string[] = [];
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
invalidate() {
|
|
593
|
+
cachedWidth = 0;
|
|
594
|
+
cachedLines = [];
|
|
595
|
+
},
|
|
596
|
+
handleInput(data: string) {
|
|
597
|
+
if (
|
|
598
|
+
matchesKey(data, Key.escape) ||
|
|
599
|
+
matchesKey(data, Key.enter) ||
|
|
600
|
+
matchesKey(data, Key.space)
|
|
601
|
+
) {
|
|
602
|
+
done();
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
render(width: number): string[] {
|
|
606
|
+
if (cachedWidth === width && cachedLines.length > 0) {
|
|
607
|
+
return cachedLines;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
cachedLines = buildDashboard(state, ctx, branch, history, width, theme);
|
|
611
|
+
|
|
612
|
+
// Add hint at bottom
|
|
613
|
+
const hint = theme.fg("dim", "Press ESC or Enter to close");
|
|
614
|
+
const hintVisible = visibleWidth(hint);
|
|
615
|
+
const pad = Math.max(0, width - hintVisible);
|
|
616
|
+
cachedLines.push("");
|
|
617
|
+
cachedLines.push(hint + " ".repeat(pad));
|
|
618
|
+
|
|
619
|
+
cachedWidth = width;
|
|
620
|
+
return cachedLines;
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
pi.registerCommand("obs-toggle", {
|
|
628
|
+
description: "Toggle the observability footer on/off",
|
|
629
|
+
handler: async (_args, ctx) => {
|
|
630
|
+
state.footerEnabled = !state.footerEnabled;
|
|
631
|
+
if (state.footerEnabled) {
|
|
632
|
+
setupFooter(ctx);
|
|
633
|
+
ctx.ui.notify("Observability footer enabled", "info");
|
|
634
|
+
} else {
|
|
635
|
+
teardownFooter(ctx);
|
|
636
|
+
ctx.ui.notify("Observability footer disabled", "info");
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
pi.registerCommand("obs-toggle-path", {
|
|
642
|
+
description: "Toggle between folder name and full path in footer",
|
|
643
|
+
handler: async (_args, ctx) => {
|
|
644
|
+
state.showFullPath = !state.showFullPath;
|
|
645
|
+
const mode = state.showFullPath ? "full path" : "folder name";
|
|
646
|
+
ctx.ui.notify(`Footer path: ${mode}`, "info");
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
pi.registerCommand("obs-settings", {
|
|
651
|
+
description: "Open status bar settings (layout presets, segment toggles, context zones)",
|
|
652
|
+
handler: async (_args, ctx) => {
|
|
653
|
+
if (!ctx.hasUI) {
|
|
654
|
+
ctx.ui.notify("Settings UI requires interactive mode", "error");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
659
|
+
let config = state.settings;
|
|
660
|
+
|
|
661
|
+
const settingsListTheme = {
|
|
662
|
+
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
663
|
+
value: (text: string, selected: boolean) =>
|
|
664
|
+
selected ? theme.fg("accent", text) : theme.fg("muted", text),
|
|
665
|
+
description: (text: string) => theme.fg("dim", text),
|
|
666
|
+
cursor: theme.fg("accent", "→ "),
|
|
667
|
+
hint: (text: string) => theme.fg("dim", text),
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
let settingsList: InstanceType<typeof SettingsList> | null = null;
|
|
671
|
+
|
|
672
|
+
function rebuildSettingsList() {
|
|
673
|
+
settingsList = new SettingsList(
|
|
674
|
+
toSettingsListItems(config),
|
|
675
|
+
10,
|
|
676
|
+
settingsListTheme,
|
|
677
|
+
async (id, newValue) => {
|
|
678
|
+
const result = updateSetting(config, id, newValue);
|
|
679
|
+
config = result.config;
|
|
680
|
+
state.settings = config;
|
|
681
|
+
|
|
682
|
+
for (const u of result.derivedUpdates) {
|
|
683
|
+
settingsList?.updateValue(u.id, u.value);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
await saveSettings(config, storage);
|
|
687
|
+
tui.requestRender();
|
|
688
|
+
},
|
|
689
|
+
done,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
rebuildSettingsList();
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
invalidate() {
|
|
697
|
+
settingsList?.invalidate();
|
|
698
|
+
},
|
|
699
|
+
handleInput(data: string) {
|
|
700
|
+
if (matchesKey(data, Key.escape)) {
|
|
701
|
+
done();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
settingsList?.handleInput(data);
|
|
705
|
+
},
|
|
706
|
+
render(width: number): string[] {
|
|
707
|
+
if (!settingsList) return [];
|
|
708
|
+
return settingsList.render(width);
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
},
|
|
713
|
+
});
|
|
619
714
|
}
|