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