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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SegmentKey,
|
|
3
|
+
FooterSettings,
|
|
4
|
+
FooterInput,
|
|
5
|
+
SegmentRenderer,
|
|
6
|
+
LayoutAssembler,
|
|
7
|
+
FooterEngineOptions,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
fmtDuration,
|
|
12
|
+
fmtTokens,
|
|
13
|
+
shortenPath,
|
|
14
|
+
thinkingColor,
|
|
15
|
+
contextUsageColor,
|
|
16
|
+
rainbowText,
|
|
17
|
+
} from "./format.js";
|
|
18
|
+
|
|
19
|
+
export { builtinRenderers } from "./segments.js";
|
|
20
|
+
export { defaultAssembler } from "./layout.js";
|
|
21
|
+
|
|
22
|
+
import { builtinRenderers } from "./segments.js";
|
|
23
|
+
import { defaultAssembler } from "./layout.js";
|
|
24
|
+
import type { FooterInput, FooterEngineOptions } from "./types.js";
|
|
25
|
+
|
|
26
|
+
export function renderFooter(input: FooterInput, width: number): string[] {
|
|
27
|
+
const segments: Record<string, string> = {};
|
|
28
|
+
for (const [key, renderer] of Object.entries(builtinRenderers)) {
|
|
29
|
+
if (input.settings.segments[key as keyof FooterInput["settings"]["segments"]]) {
|
|
30
|
+
segments[key] = renderer(input);
|
|
31
|
+
} else {
|
|
32
|
+
segments[key] = "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return defaultAssembler(segments, width, input.theme);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createFooterEngine(options: FooterEngineOptions) {
|
|
39
|
+
const segmentRenderers = { ...builtinRenderers, ...options.segments };
|
|
40
|
+
const assembler = options.layout ?? defaultAssembler;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
render(input: FooterInput, width: number): string[] {
|
|
44
|
+
const segments: Record<string, string> = {};
|
|
45
|
+
for (const [key, renderer] of Object.entries(segmentRenderers)) {
|
|
46
|
+
if (input.settings.segments[key as keyof FooterInput["settings"]["segments"]]) {
|
|
47
|
+
segments[key] = renderer(input);
|
|
48
|
+
} else {
|
|
49
|
+
segments[key] = "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return assembler(segments, width, input.theme);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { LayoutAssembler } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const defaultAssembler: LayoutAssembler = (segments, width, theme) => {
|
|
5
|
+
const sep = " " + theme.fg("dim", "▸") + " ";
|
|
6
|
+
|
|
7
|
+
const leftParts = [segments["modelThink"]].filter(Boolean);
|
|
8
|
+
const rightParts = [
|
|
9
|
+
segments["contextUsage"],
|
|
10
|
+
segments["tokens"],
|
|
11
|
+
segments["tps"],
|
|
12
|
+
segments["cost"],
|
|
13
|
+
].filter(Boolean);
|
|
14
|
+
const middleParts = [segments["runtime"], segments["pwd"], segments["git"]].filter(Boolean);
|
|
15
|
+
|
|
16
|
+
const leftStr = leftParts.join(sep);
|
|
17
|
+
const rightStr = rightParts.join(sep);
|
|
18
|
+
const middleStr = middleParts.join(sep);
|
|
19
|
+
|
|
20
|
+
const singleLine = middleStr
|
|
21
|
+
? leftStr + sep + middleStr + sep + rightStr
|
|
22
|
+
: leftStr + sep + rightStr;
|
|
23
|
+
|
|
24
|
+
if (visibleWidth(singleLine) <= width) {
|
|
25
|
+
const pad = width - visibleWidth(singleLine);
|
|
26
|
+
return [singleLine + " ".repeat(Math.max(0, pad))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback: two lines
|
|
30
|
+
function fitLine(parts: string[]): string {
|
|
31
|
+
const line = parts.filter(Boolean).join(sep);
|
|
32
|
+
const w = visibleWidth(line);
|
|
33
|
+
if (w < width) return line + " ".repeat(width - w);
|
|
34
|
+
if (w > width) return truncateToWidth(line, width);
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const line1 = fitLine([segments["modelThink"], segments["pwd"], segments["git"]]);
|
|
39
|
+
const line2 = fitLine([
|
|
40
|
+
segments["runtime"],
|
|
41
|
+
segments["contextUsage"],
|
|
42
|
+
segments["tokens"],
|
|
43
|
+
segments["tps"],
|
|
44
|
+
segments["cost"],
|
|
45
|
+
]);
|
|
46
|
+
return [line1, line2];
|
|
47
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { SegmentRenderer } from "./types.js";
|
|
3
|
+
import {
|
|
4
|
+
fmtDuration,
|
|
5
|
+
fmtTokens,
|
|
6
|
+
shortenPath,
|
|
7
|
+
thinkingColor,
|
|
8
|
+
contextUsageColor,
|
|
9
|
+
rainbowText,
|
|
10
|
+
} from "./format.js";
|
|
11
|
+
|
|
12
|
+
export const builtinRenderers: Record<string, SegmentRenderer> = {
|
|
13
|
+
modelThink(input) {
|
|
14
|
+
const { model, thinkingLevel, theme } = input;
|
|
15
|
+
const text = `${model}:${thinkingLevel}`;
|
|
16
|
+
if (thinkingLevel === "xhigh" || thinkingLevel === "max") {
|
|
17
|
+
return rainbowText(text);
|
|
18
|
+
}
|
|
19
|
+
return theme.fg(thinkingColor(thinkingLevel), text);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
runtime(input) {
|
|
23
|
+
return input.theme.fg("dim", `⏱ ${fmtDuration(input.runtimeMs)}`);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
pwd(input) {
|
|
27
|
+
const path = input.showFullPath ? shortenPath(input.cwd) : basename(input.cwd);
|
|
28
|
+
return input.theme.fg("dim", `📁 ${path}`);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
git(input) {
|
|
32
|
+
const { gitBranch, gitDiffAdded, gitDiffRemoved, theme } = input;
|
|
33
|
+
if (!gitBranch) return "";
|
|
34
|
+
let text = theme.fg("dim", ` ${gitBranch}`);
|
|
35
|
+
if (gitDiffAdded > 0 || gitDiffRemoved > 0) {
|
|
36
|
+
text += ` ${theme.fg("success", `+${gitDiffAdded}`)} ${theme.fg("error", `-${gitDiffRemoved}`)}`;
|
|
37
|
+
}
|
|
38
|
+
return text;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
contextUsage(input) {
|
|
42
|
+
const { contextUsage, theme, settings } = input;
|
|
43
|
+
if (!contextUsage || !contextUsage.contextWindow) return "";
|
|
44
|
+
|
|
45
|
+
const tokens = contextUsage.tokens || 0;
|
|
46
|
+
const max = contextUsage.contextWindow;
|
|
47
|
+
const pct = Math.min(100, Math.max(0, Math.round((tokens / max) * 100)));
|
|
48
|
+
|
|
49
|
+
let text = "ctx";
|
|
50
|
+
|
|
51
|
+
if (settings.segments.contextProgress) {
|
|
52
|
+
const barWidth = 10;
|
|
53
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
54
|
+
const empty = barWidth - filled;
|
|
55
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
56
|
+
text += ` [${bar}]`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (settings.segments.contextPercentage) {
|
|
60
|
+
text += ` ${pct}%`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (settings.segments.contextNumbers) {
|
|
64
|
+
text += ` ${fmtTokens(tokens)}/${fmtTokens(max)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return theme.fg(
|
|
68
|
+
contextUsageColor(pct, settings.contextZones.expert, settings.contextZones.warning),
|
|
69
|
+
text,
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
tokens(input) {
|
|
74
|
+
const { totalInputTokens, totalOutputTokens, theme } = input;
|
|
75
|
+
return theme.fg("dim", `↑${fmtTokens(totalInputTokens)} ↓${fmtTokens(totalOutputTokens)}`);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
tps(input) {
|
|
79
|
+
const { isStreaming, currentTurnStartTime, currentTurnUpdateCount, lastTurnTps, theme } = input;
|
|
80
|
+
if (isStreaming && currentTurnStartTime) {
|
|
81
|
+
const elapsed = (Date.now() - currentTurnStartTime) / 1000;
|
|
82
|
+
const liveTps = elapsed > 0 ? currentTurnUpdateCount / elapsed : 0;
|
|
83
|
+
return theme.fg("accent", `⚡${liveTps.toFixed(1)}`);
|
|
84
|
+
} else if (lastTurnTps > 0) {
|
|
85
|
+
return theme.fg("dim", `⚡${lastTurnTps.toFixed(1)}`);
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
cost(input) {
|
|
91
|
+
const { totalCost, theme } = input;
|
|
92
|
+
return theme.fg("dim", `$${totalCost.toFixed(4)}`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ContextUsage, Theme as PiTheme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type SegmentKey =
|
|
4
|
+
| "modelThink"
|
|
5
|
+
| "runtime"
|
|
6
|
+
| "pwd"
|
|
7
|
+
| "git"
|
|
8
|
+
| "contextUsage"
|
|
9
|
+
| "contextProgress"
|
|
10
|
+
| "contextPercentage"
|
|
11
|
+
| "contextNumbers"
|
|
12
|
+
| "tokens"
|
|
13
|
+
| "tps"
|
|
14
|
+
| "cost";
|
|
15
|
+
|
|
16
|
+
export interface FooterSettings {
|
|
17
|
+
segments: Record<SegmentKey, boolean>;
|
|
18
|
+
contextZones: { expert: number; warning: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FooterInput {
|
|
22
|
+
model: string;
|
|
23
|
+
thinkingLevel: string;
|
|
24
|
+
runtimeMs: number;
|
|
25
|
+
isStreaming: boolean;
|
|
26
|
+
currentTurnStartTime: number | null;
|
|
27
|
+
currentTurnUpdateCount: number;
|
|
28
|
+
lastTurnTps: number;
|
|
29
|
+
totalInputTokens: number;
|
|
30
|
+
totalOutputTokens: number;
|
|
31
|
+
totalCost: number;
|
|
32
|
+
contextUsage: ContextUsage | null;
|
|
33
|
+
cwd: string;
|
|
34
|
+
showFullPath: boolean;
|
|
35
|
+
gitBranch: string | null;
|
|
36
|
+
gitDiffAdded: number;
|
|
37
|
+
gitDiffRemoved: number;
|
|
38
|
+
settings: FooterSettings;
|
|
39
|
+
theme: PiTheme;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SegmentRenderer {
|
|
43
|
+
(input: FooterInput): string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LayoutAssembler {
|
|
47
|
+
(segments: Record<string, string>, width: number, theme: PiTheme): string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FooterEngineOptions {
|
|
51
|
+
segments?: Partial<Record<SegmentKey, SegmentRenderer>>;
|
|
52
|
+
layout?: LayoutAssembler;
|
|
53
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { DEFAULT_SETTINGS, PRESETS } from "./metadata.js";
|
|
2
|
+
import type { PresetName, SegmentKey, SettingsConfig, SettingsUpdateResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function createDefaultSettings(): SettingsConfig {
|
|
5
|
+
return structuredClone(DEFAULT_SETTINGS);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function applyPreset(config: SettingsConfig, preset: PresetName): SettingsConfig {
|
|
9
|
+
const next = structuredClone(config);
|
|
10
|
+
next.preset = preset;
|
|
11
|
+
const p = PRESETS[preset];
|
|
12
|
+
for (const [key, val] of Object.entries(p) as [SegmentKey, boolean][]) {
|
|
13
|
+
next.segments[key] = val;
|
|
14
|
+
}
|
|
15
|
+
return next;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toggleSegment(config: SettingsConfig, key: SegmentKey): SettingsConfig {
|
|
19
|
+
const next = structuredClone(config);
|
|
20
|
+
next.segments[key] = !next.segments[key];
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setSegment(
|
|
25
|
+
config: SettingsConfig,
|
|
26
|
+
key: SegmentKey,
|
|
27
|
+
value: boolean,
|
|
28
|
+
): SettingsConfig {
|
|
29
|
+
const next = structuredClone(config);
|
|
30
|
+
next.segments[key] = value;
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function setZone(
|
|
35
|
+
config: SettingsConfig,
|
|
36
|
+
key: "expert" | "warning",
|
|
37
|
+
value: number,
|
|
38
|
+
): SettingsConfig {
|
|
39
|
+
const next = structuredClone(config);
|
|
40
|
+
next.contextZones[key] = Math.max(0, Math.min(100, value));
|
|
41
|
+
// Ensure expert <= warning
|
|
42
|
+
if (next.contextZones.expert > next.contextZones.warning) {
|
|
43
|
+
if (key === "expert") {
|
|
44
|
+
next.contextZones.warning = next.contextZones.expert;
|
|
45
|
+
} else {
|
|
46
|
+
next.contextZones.expert = next.contextZones.warning;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateSettings(raw: unknown): SettingsConfig {
|
|
53
|
+
if (!raw || typeof raw !== "object") {
|
|
54
|
+
return createDefaultSettings();
|
|
55
|
+
}
|
|
56
|
+
const r = raw as Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
const preset = isPresetName(r.preset) ? r.preset : DEFAULT_SETTINGS.preset;
|
|
59
|
+
const segments = validateSegments(r.segments);
|
|
60
|
+
const contextZones = validateZones(r.contextZones);
|
|
61
|
+
|
|
62
|
+
return { version: 1, preset, segments, contextZones };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function migrateSettings(raw: unknown): SettingsConfig {
|
|
66
|
+
const validated = validateSettings(raw);
|
|
67
|
+
// If we ever need version migrations, add them here
|
|
68
|
+
return validated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function updateSetting(
|
|
72
|
+
config: SettingsConfig,
|
|
73
|
+
id: string,
|
|
74
|
+
value: string,
|
|
75
|
+
): SettingsUpdateResult {
|
|
76
|
+
let next = structuredClone(config);
|
|
77
|
+
const derivedUpdates: Array<{ id: string; value: string }> = [];
|
|
78
|
+
|
|
79
|
+
switch (id) {
|
|
80
|
+
case "preset": {
|
|
81
|
+
if (isPresetName(value)) {
|
|
82
|
+
next = applyPreset(next, value);
|
|
83
|
+
for (const key of Object.keys(next.segments) as SegmentKey[]) {
|
|
84
|
+
derivedUpdates.push({ id: key, value: next.segments[key] ? "true" : "false" });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "modelThink":
|
|
90
|
+
case "runtime":
|
|
91
|
+
case "pwd":
|
|
92
|
+
case "git":
|
|
93
|
+
case "contextUsage":
|
|
94
|
+
case "contextProgress":
|
|
95
|
+
case "contextPercentage":
|
|
96
|
+
case "contextNumbers":
|
|
97
|
+
case "tokens":
|
|
98
|
+
case "tps":
|
|
99
|
+
case "cost": {
|
|
100
|
+
next = setSegment(next, id, value === "true");
|
|
101
|
+
// Context sub-toggle dependency: if contextUsage is turned off, children are hidden
|
|
102
|
+
if (id === "contextUsage" && value === "false") {
|
|
103
|
+
for (const child of [
|
|
104
|
+
"contextProgress",
|
|
105
|
+
"contextPercentage",
|
|
106
|
+
"contextNumbers",
|
|
107
|
+
] as SegmentKey[]) {
|
|
108
|
+
derivedUpdates.push({ id: child, value: "false" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "expertZone": {
|
|
114
|
+
next = setZone(next, "expert", parseInt(value, 10));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "warningZone": {
|
|
118
|
+
next = setZone(next, "warning", parseInt(value, 10));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { config: next, derivedUpdates };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isPresetName(v: unknown): v is PresetName {
|
|
127
|
+
return v === "minimal" || v === "standard" || v === "verbose" || v === "performance";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateSegments(raw: unknown): Record<SegmentKey, boolean> {
|
|
131
|
+
const segments = { ...DEFAULT_SETTINGS.segments };
|
|
132
|
+
if (!raw || typeof raw !== "object") return segments;
|
|
133
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
134
|
+
if (key in segments && typeof val === "boolean") {
|
|
135
|
+
segments[key as SegmentKey] = val;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return segments;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateZones(raw: unknown): { expert: number; warning: number } {
|
|
142
|
+
const zones = { ...DEFAULT_SETTINGS.contextZones };
|
|
143
|
+
if (!raw || typeof raw !== "object") return zones;
|
|
144
|
+
const r = raw as Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
if (typeof r.expert === "number") zones.expert = clamp(0, r.expert, 100);
|
|
147
|
+
if (typeof r.warning === "number") zones.warning = clamp(0, r.warning, 100);
|
|
148
|
+
|
|
149
|
+
// Ensure expert <= warning
|
|
150
|
+
if (zones.expert > zones.warning) {
|
|
151
|
+
const avg = Math.round((zones.expert + zones.warning) / 2);
|
|
152
|
+
zones.expert = avg;
|
|
153
|
+
zones.warning = avg;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return zones;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function clamp(min: number, val: number, max: number): number {
|
|
160
|
+
return Math.max(min, Math.min(max, val));
|
|
161
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SegmentKey,
|
|
3
|
+
PresetName,
|
|
4
|
+
SettingsConfig,
|
|
5
|
+
SettingsListItem,
|
|
6
|
+
SettingsUpdateResult,
|
|
7
|
+
SegmentMetadata,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
export { DEFAULT_SETTINGS, PRESETS, SEGMENT_METADATA, ZONE_VALUE_OPTIONS } from "./metadata.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createDefaultSettings,
|
|
14
|
+
applyPreset,
|
|
15
|
+
toggleSegment,
|
|
16
|
+
setSegment,
|
|
17
|
+
setZone,
|
|
18
|
+
validateSettings,
|
|
19
|
+
migrateSettings,
|
|
20
|
+
updateSetting,
|
|
21
|
+
} from "./domain.js";
|
|
22
|
+
|
|
23
|
+
export { toSettingsListItems } from "./tui.js";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
createSettingsStorage,
|
|
27
|
+
createMemorySettingsStorage,
|
|
28
|
+
loadSettings,
|
|
29
|
+
saveSettings,
|
|
30
|
+
} from "./storage.js";
|
|
31
|
+
|
|
32
|
+
export { createSettingsManager, type SettingsManager } from "./manager.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { loadSettings, saveSettings } from "./storage.js";
|
|
2
|
+
import {
|
|
3
|
+
applyPreset,
|
|
4
|
+
toggleSegment,
|
|
5
|
+
setSegment,
|
|
6
|
+
setZone,
|
|
7
|
+
createDefaultSettings,
|
|
8
|
+
} from "./domain.js";
|
|
9
|
+
import type { PresetName, SegmentKey, SettingsConfig } from "./types.js";
|
|
10
|
+
import type { Storage } from "../storage/index.js";
|
|
11
|
+
|
|
12
|
+
export interface SettingsManager {
|
|
13
|
+
load(): Promise<void>;
|
|
14
|
+
save(): Promise<void>;
|
|
15
|
+
getConfig(): SettingsConfig;
|
|
16
|
+
applyPreset(preset: PresetName): SettingsConfig;
|
|
17
|
+
toggleSegment(key: SegmentKey): SettingsConfig;
|
|
18
|
+
setSegment(key: SegmentKey, value: boolean): SettingsConfig;
|
|
19
|
+
setZone(key: "expert" | "warning", value: number): SettingsConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createSettingsManager(storage: Storage): SettingsManager {
|
|
23
|
+
let config: SettingsConfig = createDefaultSettings();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
async load() {
|
|
27
|
+
config = await loadSettings(storage);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async save() {
|
|
31
|
+
await saveSettings(config, storage);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getConfig() {
|
|
35
|
+
return structuredClone(config);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
applyPreset(preset) {
|
|
39
|
+
config = applyPreset(config, preset);
|
|
40
|
+
return structuredClone(config);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
toggleSegment(key) {
|
|
44
|
+
config = toggleSegment(config, key);
|
|
45
|
+
return structuredClone(config);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
setSegment(key, value) {
|
|
49
|
+
config = setSegment(config, key, value);
|
|
50
|
+
return structuredClone(config);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
setZone(key, value) {
|
|
54
|
+
config = setZone(config, key, value);
|
|
55
|
+
return structuredClone(config);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PresetName, SegmentKey, SegmentMetadata, SettingsConfig } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SETTINGS: SettingsConfig = {
|
|
4
|
+
version: 1,
|
|
5
|
+
preset: "standard",
|
|
6
|
+
segments: {
|
|
7
|
+
modelThink: true,
|
|
8
|
+
runtime: true,
|
|
9
|
+
pwd: true,
|
|
10
|
+
git: true,
|
|
11
|
+
contextUsage: true,
|
|
12
|
+
contextProgress: true,
|
|
13
|
+
contextPercentage: true,
|
|
14
|
+
contextNumbers: true,
|
|
15
|
+
tokens: true,
|
|
16
|
+
tps: true,
|
|
17
|
+
cost: true,
|
|
18
|
+
},
|
|
19
|
+
contextZones: { expert: 70, warning: 85 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PRESETS: Record<PresetName, Partial<Record<SegmentKey, boolean>>> = {
|
|
23
|
+
minimal: {
|
|
24
|
+
modelThink: true,
|
|
25
|
+
contextUsage: true,
|
|
26
|
+
contextProgress: true,
|
|
27
|
+
contextPercentage: false,
|
|
28
|
+
contextNumbers: true,
|
|
29
|
+
runtime: false,
|
|
30
|
+
pwd: false,
|
|
31
|
+
git: false,
|
|
32
|
+
tokens: false,
|
|
33
|
+
tps: false,
|
|
34
|
+
cost: false,
|
|
35
|
+
},
|
|
36
|
+
standard: {
|
|
37
|
+
modelThink: true,
|
|
38
|
+
runtime: true,
|
|
39
|
+
pwd: true,
|
|
40
|
+
git: true,
|
|
41
|
+
contextUsage: true,
|
|
42
|
+
contextProgress: true,
|
|
43
|
+
contextPercentage: true,
|
|
44
|
+
contextNumbers: true,
|
|
45
|
+
tokens: true,
|
|
46
|
+
tps: false,
|
|
47
|
+
cost: true,
|
|
48
|
+
},
|
|
49
|
+
verbose: {
|
|
50
|
+
modelThink: true,
|
|
51
|
+
runtime: true,
|
|
52
|
+
pwd: true,
|
|
53
|
+
git: true,
|
|
54
|
+
contextUsage: true,
|
|
55
|
+
contextProgress: true,
|
|
56
|
+
contextPercentage: true,
|
|
57
|
+
contextNumbers: true,
|
|
58
|
+
tokens: true,
|
|
59
|
+
tps: true,
|
|
60
|
+
cost: true,
|
|
61
|
+
},
|
|
62
|
+
performance: {
|
|
63
|
+
modelThink: true,
|
|
64
|
+
runtime: false,
|
|
65
|
+
pwd: false,
|
|
66
|
+
git: false,
|
|
67
|
+
contextUsage: true,
|
|
68
|
+
contextProgress: false,
|
|
69
|
+
contextPercentage: true,
|
|
70
|
+
contextNumbers: true,
|
|
71
|
+
tokens: false,
|
|
72
|
+
tps: true,
|
|
73
|
+
cost: true,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const SEGMENT_METADATA: SegmentMetadata[] = [
|
|
78
|
+
{
|
|
79
|
+
id: "modelThink",
|
|
80
|
+
label: "Model & Thinking",
|
|
81
|
+
description: "Show current model and thinking level",
|
|
82
|
+
},
|
|
83
|
+
{ id: "runtime", label: "Runtime", description: "Show session runtime timer" },
|
|
84
|
+
{ id: "pwd", label: "Working Directory", description: "Show current working directory" },
|
|
85
|
+
{ id: "git", label: "Git Branch & Diff", description: "Show git branch and diff stats" },
|
|
86
|
+
{
|
|
87
|
+
id: "contextUsage",
|
|
88
|
+
label: "Context Usage",
|
|
89
|
+
description: "Master toggle for the context usage segment",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "contextProgress",
|
|
93
|
+
label: " └ Progress Bar",
|
|
94
|
+
description: "Show the progress bar in context usage",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "contextPercentage",
|
|
98
|
+
label: " └ Percentage",
|
|
99
|
+
description: "Show the percentage in context usage",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "contextNumbers",
|
|
103
|
+
label: " └ Used / Total",
|
|
104
|
+
description: "Show the token count in context usage",
|
|
105
|
+
},
|
|
106
|
+
{ id: "tokens", label: "Session Tokens", description: "Show total input/output token counts" },
|
|
107
|
+
{ id: "tps", label: "TPS (Tokens/Sec)", description: "Show live and last-turn TPS" },
|
|
108
|
+
{ id: "cost", label: "Cost", description: "Show estimated session cost" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
export const ZONE_VALUE_OPTIONS = {
|
|
112
|
+
expert: ["60", "65", "70", "75", "80"],
|
|
113
|
+
warning: ["75", "80", "85", "90", "95"],
|
|
114
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
createFileBackend,
|
|
5
|
+
createMemoryBackend,
|
|
6
|
+
createStorage,
|
|
7
|
+
type Storage,
|
|
8
|
+
} from "../storage/index.js";
|
|
9
|
+
import { migrateSettings } from "./domain.js";
|
|
10
|
+
import type { SettingsConfig } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_DIR = join(homedir(), ".pi", "agent", "observability");
|
|
13
|
+
|
|
14
|
+
export function createSettingsStorage(options?: { dir?: string }): Storage {
|
|
15
|
+
const dir = options?.dir ?? DEFAULT_DIR;
|
|
16
|
+
const backend = createFileBackend({ dir });
|
|
17
|
+
return createStorage(backend);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createMemorySettingsStorage(): Storage {
|
|
21
|
+
const backend = createMemoryBackend();
|
|
22
|
+
return createStorage(backend);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loadSettings(storage: Storage): Promise<SettingsConfig> {
|
|
26
|
+
const store = storage.json<SettingsConfig>("settings", { defaults: undefined });
|
|
27
|
+
try {
|
|
28
|
+
const raw = await store.load();
|
|
29
|
+
return migrateSettings(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return migrateSettings(undefined);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function saveSettings(config: SettingsConfig, storage: Storage): Promise<void> {
|
|
36
|
+
const store = storage.json<SettingsConfig>("settings");
|
|
37
|
+
await store.save(config);
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SEGMENT_METADATA, ZONE_VALUE_OPTIONS } from "./metadata.js";
|
|
2
|
+
import type { SettingsConfig, SettingsListItem } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function toSettingsListItems(config: SettingsConfig): SettingsListItem[] {
|
|
5
|
+
const items: SettingsListItem[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "preset",
|
|
8
|
+
label: "Layout Preset",
|
|
9
|
+
description:
|
|
10
|
+
"Quick layout presets. Individual segments can still be toggled after applying a preset.",
|
|
11
|
+
currentValue: config.preset,
|
|
12
|
+
values: ["minimal", "standard", "verbose", "performance"],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const meta of SEGMENT_METADATA) {
|
|
17
|
+
items.push({
|
|
18
|
+
id: meta.id,
|
|
19
|
+
label: meta.label,
|
|
20
|
+
description: meta.description,
|
|
21
|
+
currentValue: config.segments[meta.id] ? "true" : "false",
|
|
22
|
+
values: ["true", "false"],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
items.push(
|
|
27
|
+
{
|
|
28
|
+
id: "expertZone",
|
|
29
|
+
label: "Expert Zone Threshold",
|
|
30
|
+
description: "Context usage percentage where the bar turns green (0-100)",
|
|
31
|
+
currentValue: `${config.contextZones.expert}`,
|
|
32
|
+
values: ZONE_VALUE_OPTIONS.expert,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "warningZone",
|
|
36
|
+
label: "Warning Zone Threshold",
|
|
37
|
+
description: "Context usage percentage where the bar turns yellow (0-100)",
|
|
38
|
+
currentValue: `${config.contextZones.warning}`,
|
|
39
|
+
values: ZONE_VALUE_OPTIONS.warning,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return items;
|
|
44
|
+
}
|