@quandev104/pi-style 0.1.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { boundedDiagnostics, type ConfigDiagnostic } from "./config-diagnostics.js";
|
|
2
|
+
import { presetConfig } from "./config-presets.js";
|
|
3
|
+
import {
|
|
4
|
+
type ConfigSources,
|
|
5
|
+
type NormalizedPiStyleConfig,
|
|
6
|
+
PI_STYLE_SCHEMA_VERSION,
|
|
7
|
+
type PiStyleConfig,
|
|
8
|
+
} from "./config-types.js";
|
|
9
|
+
import { normalizeStatusLayout } from "./status-presets.js";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CONFIG: NormalizedPiStyleConfig = Object.freeze({
|
|
12
|
+
schemaVersion: PI_STYLE_SCHEMA_VERSION,
|
|
13
|
+
enabled: true,
|
|
14
|
+
preset: "default",
|
|
15
|
+
placement: "below",
|
|
16
|
+
startup: Object.freeze({ mode: "compact", showResources: false, alwaysExpanded: false }),
|
|
17
|
+
statusLine: Object.freeze({
|
|
18
|
+
enabled: true,
|
|
19
|
+
separator: "powerline-thin",
|
|
20
|
+
layout: Object.freeze({
|
|
21
|
+
left: ["path", "git", "context_bar", "cost"],
|
|
22
|
+
right: ["model_effort"],
|
|
23
|
+
secondary: [],
|
|
24
|
+
}),
|
|
25
|
+
disabledSegments: [],
|
|
26
|
+
customItems: [],
|
|
27
|
+
bottomMargin: 1,
|
|
28
|
+
contextBarWidth: 10,
|
|
29
|
+
}),
|
|
30
|
+
editor: Object.freeze({ enabled: true, style: "compact", frame: "auto", showMetadata: false }),
|
|
31
|
+
messages: Object.freeze({ enabled: true, userPrefix: true, assistantPrefix: true, specialBlocks: true }),
|
|
32
|
+
tools: Object.freeze({
|
|
33
|
+
enabled: true,
|
|
34
|
+
style: "compact-box",
|
|
35
|
+
maxCollapsedLines: 10,
|
|
36
|
+
maxExpandedLines: 50,
|
|
37
|
+
dimOutput: false,
|
|
38
|
+
showElapsed: true,
|
|
39
|
+
}),
|
|
40
|
+
theme: Object.freeze({ nerdFonts: "auto", terminalBackgroundSync: "auto", colors: {}, glyphs: {} }),
|
|
41
|
+
compatibility: Object.freeze({
|
|
42
|
+
allowSafePatches: true,
|
|
43
|
+
allowCorePatches: false,
|
|
44
|
+
preferExistingEditor: true,
|
|
45
|
+
preferExistingFooter: true,
|
|
46
|
+
}),
|
|
47
|
+
debug: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function merge(base: Record<string, unknown>, source: unknown): Record<string, unknown> {
|
|
54
|
+
if (!isRecord(source)) return base;
|
|
55
|
+
const result: Record<string, unknown> = { ...base };
|
|
56
|
+
for (const [key, value] of Object.entries(source))
|
|
57
|
+
result[key] =
|
|
58
|
+
isRecord(value) && isRecord(result[key]) ? merge(result[key] as Record<string, unknown>, value) : value;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function bool(value: unknown, fallback: boolean): boolean {
|
|
62
|
+
return typeof value === "boolean" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
function boundedInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
66
|
+
? Math.min(max, Math.max(min, Math.floor(value)))
|
|
67
|
+
: fallback;
|
|
68
|
+
}
|
|
69
|
+
function stringEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
|
|
70
|
+
return typeof value === "string" && (allowed as readonly string[]).includes(value) ? (value as T) : fallback;
|
|
71
|
+
}
|
|
72
|
+
function strings(value: unknown, fallback: readonly string[]): readonly string[] {
|
|
73
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? [...value] : fallback;
|
|
74
|
+
}
|
|
75
|
+
function stringMap(value: unknown): Readonly<Record<string, string>> {
|
|
76
|
+
return isRecord(value)
|
|
77
|
+
? Object.fromEntries(
|
|
78
|
+
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
79
|
+
)
|
|
80
|
+
: {};
|
|
81
|
+
}
|
|
82
|
+
function customItems(value: unknown): readonly import("./config-types.js").StatusCustomItemConfig[] {
|
|
83
|
+
if (!Array.isArray(value)) return [];
|
|
84
|
+
return value.filter((item): item is import("./config-types.js").StatusCustomItemConfig => {
|
|
85
|
+
if (!isRecord(item) || typeof item.id !== "string" || typeof item.statusKey !== "string") return false;
|
|
86
|
+
return item.placement === undefined || ["left", "right", "secondary"].includes(item.placement as string);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeConfig(
|
|
91
|
+
input: unknown,
|
|
92
|
+
defaults: NormalizedPiStyleConfig = DEFAULT_CONFIG,
|
|
93
|
+
): NormalizedPiStyleConfig {
|
|
94
|
+
const value = merge(defaults as unknown as Record<string, unknown>, acceptedInput(input));
|
|
95
|
+
const inputRecord = isRecord(input) ? input : {};
|
|
96
|
+
const inputStatus = isRecord(inputRecord.statusLine) ? inputRecord.statusLine : {};
|
|
97
|
+
const inputLayout = isRecord(inputStatus.layout) ? inputStatus.layout : undefined;
|
|
98
|
+
const startup = isRecord(value.startup) ? value.startup : {};
|
|
99
|
+
const status = isRecord(value.statusLine) ? value.statusLine : {};
|
|
100
|
+
const editor = isRecord(value.editor) ? value.editor : {};
|
|
101
|
+
const messages = isRecord(value.messages) ? value.messages : {};
|
|
102
|
+
const tools = isRecord(value.tools) ? value.tools : {};
|
|
103
|
+
const theme = isRecord(value.theme) ? value.theme : {};
|
|
104
|
+
const compatibility = isRecord(value.compatibility) ? value.compatibility : {};
|
|
105
|
+
const max =
|
|
106
|
+
typeof tools.maxCollapsedLines === "number" &&
|
|
107
|
+
Number.isFinite(tools.maxCollapsedLines) &&
|
|
108
|
+
tools.maxCollapsedLines >= 0
|
|
109
|
+
? Math.floor(tools.maxCollapsedLines)
|
|
110
|
+
: defaults.tools.maxCollapsedLines;
|
|
111
|
+
const maxExpanded =
|
|
112
|
+
typeof tools.maxExpandedLines === "number" && Number.isFinite(tools.maxExpandedLines) && tools.maxExpandedLines >= 0
|
|
113
|
+
? Math.min(Math.floor(tools.maxExpandedLines), 1000)
|
|
114
|
+
: defaults.tools.maxExpandedLines;
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
schemaVersion: PI_STYLE_SCHEMA_VERSION,
|
|
117
|
+
enabled: bool(value.enabled, defaults.enabled),
|
|
118
|
+
preset: stringEnum(value.preset, ["default", "minimal", "compact", "full", "ascii", "native"], defaults.preset),
|
|
119
|
+
placement: stringEnum(value.placement, ["above", "below"], defaults.placement),
|
|
120
|
+
startup: Object.freeze({
|
|
121
|
+
mode: stringEnum(startup.mode, ["off", "compact", "overlay"], defaults.startup.mode),
|
|
122
|
+
showResources: bool(startup.showResources, defaults.startup.showResources),
|
|
123
|
+
alwaysExpanded: bool(startup.alwaysExpanded, defaults.startup.alwaysExpanded),
|
|
124
|
+
}),
|
|
125
|
+
statusLine: Object.freeze({
|
|
126
|
+
enabled: bool(status.enabled, defaults.statusLine.enabled),
|
|
127
|
+
separator: typeof status.separator === "string" ? status.separator : defaults.statusLine.separator,
|
|
128
|
+
layout: normalizeStatusLayout(
|
|
129
|
+
stringEnum(value.preset, ["default", "minimal", "compact", "full", "ascii", "native"], defaults.preset),
|
|
130
|
+
inputLayout
|
|
131
|
+
? {
|
|
132
|
+
left:
|
|
133
|
+
inputLayout.left === undefined ? undefined : strings(inputLayout.left, defaults.statusLine.layout.left),
|
|
134
|
+
right:
|
|
135
|
+
inputLayout.right === undefined
|
|
136
|
+
? undefined
|
|
137
|
+
: strings(inputLayout.right, defaults.statusLine.layout.right),
|
|
138
|
+
secondary:
|
|
139
|
+
inputLayout.secondary === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: strings(inputLayout.secondary, defaults.statusLine.layout.secondary),
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
),
|
|
145
|
+
disabledSegments: strings(status.disabledSegments, defaults.statusLine.disabledSegments),
|
|
146
|
+
customItems: customItems(status.customItems),
|
|
147
|
+
bottomMargin: boundedInt(status.bottomMargin, defaults.statusLine.bottomMargin, 0, 4),
|
|
148
|
+
contextBarWidth: boundedInt(status.contextBarWidth, defaults.statusLine.contextBarWidth, 4, 40),
|
|
149
|
+
}),
|
|
150
|
+
editor: Object.freeze({
|
|
151
|
+
enabled: bool(editor.enabled, defaults.editor.enabled),
|
|
152
|
+
style: stringEnum(editor.style, ["compact", "boxed", "dock", "native"], defaults.editor.style),
|
|
153
|
+
frame: stringEnum(
|
|
154
|
+
editor.frame,
|
|
155
|
+
["auto", "halfblock", "line", "solid", "outline", "native"],
|
|
156
|
+
defaults.editor.frame,
|
|
157
|
+
),
|
|
158
|
+
showMetadata: bool(editor.showMetadata, defaults.editor.showMetadata),
|
|
159
|
+
}),
|
|
160
|
+
messages: Object.freeze({
|
|
161
|
+
enabled: bool(messages.enabled, defaults.messages.enabled),
|
|
162
|
+
userPrefix: bool(messages.userPrefix, defaults.messages.userPrefix),
|
|
163
|
+
assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
|
|
164
|
+
specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
|
|
165
|
+
}),
|
|
166
|
+
tools: Object.freeze({
|
|
167
|
+
enabled: bool(tools.enabled, defaults.tools.enabled),
|
|
168
|
+
style: typeof tools.style === "string" ? tools.style : defaults.tools.style,
|
|
169
|
+
maxCollapsedLines: max,
|
|
170
|
+
maxExpandedLines: maxExpanded,
|
|
171
|
+
dimOutput: bool(tools.dimOutput, defaults.tools.dimOutput),
|
|
172
|
+
showElapsed: bool(tools.showElapsed, defaults.tools.showElapsed),
|
|
173
|
+
}),
|
|
174
|
+
theme: Object.freeze({
|
|
175
|
+
nerdFonts: stringEnum(theme.nerdFonts, ["auto", "on", "off"], defaults.theme.nerdFonts),
|
|
176
|
+
terminalBackgroundSync: stringEnum(
|
|
177
|
+
theme.terminalBackgroundSync,
|
|
178
|
+
["auto", "on", "off"],
|
|
179
|
+
defaults.theme.terminalBackgroundSync,
|
|
180
|
+
),
|
|
181
|
+
colors: stringMap(theme.colors),
|
|
182
|
+
glyphs: stringMap(theme.glyphs),
|
|
183
|
+
}),
|
|
184
|
+
compatibility: Object.freeze({
|
|
185
|
+
allowSafePatches: bool(compatibility.allowSafePatches, defaults.compatibility.allowSafePatches),
|
|
186
|
+
allowCorePatches: bool(compatibility.allowCorePatches, defaults.compatibility.allowCorePatches),
|
|
187
|
+
preferExistingEditor: bool(compatibility.preferExistingEditor, defaults.compatibility.preferExistingEditor),
|
|
188
|
+
preferExistingFooter: bool(compatibility.preferExistingFooter, defaults.compatibility.preferExistingFooter),
|
|
189
|
+
}),
|
|
190
|
+
debug: bool(value.debug, defaults.debug),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resolveConfig(sources: ConfigSources): NormalizedPiStyleConfig {
|
|
195
|
+
return resolveConfigDetailed(sources).config;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const PRESET_NAMES = ["default", "minimal", "compact", "full", "ascii", "native"] as const;
|
|
199
|
+
const ENUMS: Readonly<Record<string, readonly string[]>> = {
|
|
200
|
+
preset: PRESET_NAMES,
|
|
201
|
+
placement: ["above", "below"],
|
|
202
|
+
"startup.mode": ["off", "compact", "overlay"],
|
|
203
|
+
"editor.style": ["compact", "boxed", "dock", "native"],
|
|
204
|
+
"editor.frame": ["auto", "halfblock", "line", "solid", "outline", "native"],
|
|
205
|
+
"theme.nerdFonts": ["auto", "on", "off"],
|
|
206
|
+
"theme.terminalBackgroundSync": ["auto", "on", "off"],
|
|
207
|
+
};
|
|
208
|
+
const BOOL_PATHS = new Set([
|
|
209
|
+
"enabled",
|
|
210
|
+
"startup.showResources",
|
|
211
|
+
"startup.alwaysExpanded",
|
|
212
|
+
"statusLine.enabled",
|
|
213
|
+
"editor.enabled",
|
|
214
|
+
"editor.showMetadata",
|
|
215
|
+
"messages.enabled",
|
|
216
|
+
"messages.userPrefix",
|
|
217
|
+
"messages.assistantPrefix",
|
|
218
|
+
"messages.specialBlocks",
|
|
219
|
+
"tools.enabled",
|
|
220
|
+
"tools.showElapsed",
|
|
221
|
+
"tools.dimOutput",
|
|
222
|
+
"compatibility.allowSafePatches",
|
|
223
|
+
"compatibility.allowCorePatches",
|
|
224
|
+
"compatibility.preferExistingEditor",
|
|
225
|
+
"compatibility.preferExistingFooter",
|
|
226
|
+
"debug",
|
|
227
|
+
]);
|
|
228
|
+
const STRING_ARRAY_PATHS = new Set([
|
|
229
|
+
"statusLine.layout.left",
|
|
230
|
+
"statusLine.layout.right",
|
|
231
|
+
"statusLine.layout.secondary",
|
|
232
|
+
"statusLine.disabledSegments",
|
|
233
|
+
]);
|
|
234
|
+
const MAP_PATHS = new Set(["theme.colors", "theme.glyphs"]);
|
|
235
|
+
const CONTAINER_PATHS = new Set([
|
|
236
|
+
"startup",
|
|
237
|
+
"statusLine",
|
|
238
|
+
"statusLine.layout",
|
|
239
|
+
"editor",
|
|
240
|
+
"messages",
|
|
241
|
+
"tools",
|
|
242
|
+
"theme",
|
|
243
|
+
"compatibility",
|
|
244
|
+
]);
|
|
245
|
+
function validCustomItem(item: unknown): boolean {
|
|
246
|
+
if (!isRecord(item) || typeof item.id !== "string" || typeof item.statusKey !== "string") return false;
|
|
247
|
+
if (Object.keys(item).some((key) => !["id", "statusKey", "label", "priority", "placement"].includes(key)))
|
|
248
|
+
return false;
|
|
249
|
+
if (item.label !== undefined && typeof item.label !== "string") return false;
|
|
250
|
+
if (item.priority !== undefined && (typeof item.priority !== "number" || !Number.isFinite(item.priority)))
|
|
251
|
+
return false;
|
|
252
|
+
return (
|
|
253
|
+
item.placement === undefined ||
|
|
254
|
+
item.placement === "left" ||
|
|
255
|
+
item.placement === "right" ||
|
|
256
|
+
item.placement === "secondary"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function validLeaf(path: string, value: unknown): boolean {
|
|
260
|
+
if (BOOL_PATHS.has(path)) return typeof value === "boolean";
|
|
261
|
+
if (ENUMS[path]) return typeof value === "string" && ENUMS[path].includes(value);
|
|
262
|
+
if (path === "statusLine.separator" || path === "tools.style") return typeof value === "string";
|
|
263
|
+
if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
|
|
264
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
265
|
+
if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
|
|
266
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
267
|
+
if (STRING_ARRAY_PATHS.has(path)) return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
268
|
+
if (MAP_PATHS.has(path)) return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
|
269
|
+
if (path === "statusLine.customItems") return Array.isArray(value) && value.every(validCustomItem);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
export interface ConfigLayerResult {
|
|
273
|
+
readonly accepted: unknown;
|
|
274
|
+
readonly diagnostics: readonly ConfigDiagnostic[];
|
|
275
|
+
readonly paths: ReadonlySet<string>;
|
|
276
|
+
}
|
|
277
|
+
function cloneLeaf(value: unknown): unknown {
|
|
278
|
+
return Array.isArray(value)
|
|
279
|
+
? value.map((item) => (isRecord(item) ? { ...item } : item))
|
|
280
|
+
: isRecord(value)
|
|
281
|
+
? { ...value }
|
|
282
|
+
: value;
|
|
283
|
+
}
|
|
284
|
+
export function validateConfigLayer(input: unknown): ConfigLayerResult {
|
|
285
|
+
const diagnostics: ConfigDiagnostic[] = [];
|
|
286
|
+
const paths = new Set<string>();
|
|
287
|
+
const walk = (value: unknown, prefix: string): unknown => {
|
|
288
|
+
if (!isRecord(value)) return value;
|
|
289
|
+
const result: Record<string, unknown> = {};
|
|
290
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
291
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
292
|
+
if (path === "schemaVersion") {
|
|
293
|
+
if (nestedValue === undefined || nestedValue === PI_STYLE_SCHEMA_VERSION) result[key] = nestedValue;
|
|
294
|
+
else
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
code: "CFG-SCHEMA",
|
|
297
|
+
level: "warning",
|
|
298
|
+
path,
|
|
299
|
+
message: "unsupported schema version ignored",
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (path === "statusLine.customItems" && Array.isArray(nestedValue)) {
|
|
304
|
+
const acceptedItems: Record<string, unknown>[] = [];
|
|
305
|
+
for (const [index, item] of nestedValue.entries()) {
|
|
306
|
+
if (!isRecord(item)) {
|
|
307
|
+
diagnostics.push({
|
|
308
|
+
code: "CFG-VALUE",
|
|
309
|
+
level: "warning",
|
|
310
|
+
path: `${path}[${index}]`,
|
|
311
|
+
message: "custom item must be an object",
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const allowed = ["id", "statusKey", "label", "priority", "placement"];
|
|
316
|
+
let valid = typeof item.id === "string" && typeof item.statusKey === "string";
|
|
317
|
+
if (typeof item.id !== "string")
|
|
318
|
+
diagnostics.push({
|
|
319
|
+
code: "CFG-VALUE",
|
|
320
|
+
level: "warning",
|
|
321
|
+
path: `${path}[${index}].id`,
|
|
322
|
+
message: "required custom item field is invalid or missing",
|
|
323
|
+
});
|
|
324
|
+
if (typeof item.statusKey !== "string")
|
|
325
|
+
diagnostics.push({
|
|
326
|
+
code: "CFG-VALUE",
|
|
327
|
+
level: "warning",
|
|
328
|
+
path: `${path}[${index}].statusKey`,
|
|
329
|
+
message: "required custom item field is invalid or missing",
|
|
330
|
+
});
|
|
331
|
+
for (const field of Object.keys(item)) {
|
|
332
|
+
if (!allowed.includes(field)) {
|
|
333
|
+
diagnostics.push({
|
|
334
|
+
code: "CFG-VALUE",
|
|
335
|
+
level: "warning",
|
|
336
|
+
path: `${path}[${index}].${field}`,
|
|
337
|
+
message: "unknown custom item field ignored",
|
|
338
|
+
});
|
|
339
|
+
valid = false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (item.label !== undefined && typeof item.label !== "string") {
|
|
343
|
+
diagnostics.push({
|
|
344
|
+
code: "CFG-VALUE",
|
|
345
|
+
level: "warning",
|
|
346
|
+
path: `${path}[${index}].label`,
|
|
347
|
+
message: "invalid custom item field ignored",
|
|
348
|
+
});
|
|
349
|
+
valid = false;
|
|
350
|
+
}
|
|
351
|
+
if (item.priority !== undefined && (typeof item.priority !== "number" || !Number.isFinite(item.priority))) {
|
|
352
|
+
diagnostics.push({
|
|
353
|
+
code: "CFG-VALUE",
|
|
354
|
+
level: "warning",
|
|
355
|
+
path: `${path}[${index}].priority`,
|
|
356
|
+
message: "invalid custom item field ignored",
|
|
357
|
+
});
|
|
358
|
+
valid = false;
|
|
359
|
+
}
|
|
360
|
+
if (item.placement !== undefined && !["left", "right", "secondary"].includes(item.placement as string)) {
|
|
361
|
+
diagnostics.push({
|
|
362
|
+
code: "CFG-VALUE",
|
|
363
|
+
level: "warning",
|
|
364
|
+
path: `${path}[${index}].placement`,
|
|
365
|
+
message: "invalid custom item field ignored",
|
|
366
|
+
});
|
|
367
|
+
valid = false;
|
|
368
|
+
}
|
|
369
|
+
if (valid) {
|
|
370
|
+
acceptedItems.push({ ...item });
|
|
371
|
+
paths.add(`${path}[${index}].id`);
|
|
372
|
+
paths.add(`${path}[${index}].statusKey`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
result[key] = acceptedItems;
|
|
376
|
+
paths.add(path);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (validLeaf(path, nestedValue)) {
|
|
380
|
+
result[key] = cloneLeaf(nestedValue);
|
|
381
|
+
paths.add(path);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (
|
|
385
|
+
isRecord(nestedValue) &&
|
|
386
|
+
CONTAINER_PATHS.has(path) &&
|
|
387
|
+
!MAP_PATHS.has(path) &&
|
|
388
|
+
path !== "statusLine.customItems"
|
|
389
|
+
) {
|
|
390
|
+
const child = walk(nestedValue, path);
|
|
391
|
+
if (isRecord(child) && Object.keys(child).length > 0) result[key] = child;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (path !== "statusLine.customItems" || !Array.isArray(nestedValue))
|
|
395
|
+
diagnostics.push({ code: "CFG-VALUE", level: "warning", path, message: "invalid or unknown field ignored" });
|
|
396
|
+
if (path === "statusLine.customItems" && Array.isArray(nestedValue))
|
|
397
|
+
for (const [index, item] of nestedValue.entries()) {
|
|
398
|
+
if (!isRecord(item)) continue;
|
|
399
|
+
for (const field of ["id", "statusKey", "label", "priority", "placement"]) {
|
|
400
|
+
if (Object.hasOwn(item, field)) {
|
|
401
|
+
const fieldValue = item[field];
|
|
402
|
+
const valid =
|
|
403
|
+
field === "id" || field === "statusKey" || field === "label"
|
|
404
|
+
? typeof fieldValue === "string"
|
|
405
|
+
: field === "priority"
|
|
406
|
+
? typeof fieldValue === "number" && Number.isFinite(fieldValue)
|
|
407
|
+
: fieldValue === "left" || fieldValue === "right" || fieldValue === "secondary";
|
|
408
|
+
if (!valid)
|
|
409
|
+
diagnostics.push({
|
|
410
|
+
code: "CFG-VALUE",
|
|
411
|
+
level: "warning",
|
|
412
|
+
path: `${path}[${index}].${field}`,
|
|
413
|
+
message: "invalid custom item field ignored",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const field of Object.keys(item))
|
|
418
|
+
if (!["id", "statusKey", "label", "priority", "placement"].includes(field))
|
|
419
|
+
diagnostics.push({
|
|
420
|
+
code: "CFG-VALUE",
|
|
421
|
+
level: "warning",
|
|
422
|
+
path: `${path}[${index}].${field}`,
|
|
423
|
+
message: "unknown custom item field ignored",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
};
|
|
429
|
+
return { accepted: walk(input, ""), diagnostics: boundedDiagnostics(diagnostics), paths };
|
|
430
|
+
}
|
|
431
|
+
function acceptedInput(input: unknown): unknown {
|
|
432
|
+
return validateConfigLayer(input).accepted;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function resolveConfigDetailed(sources: ConfigSources): {
|
|
436
|
+
readonly config: NormalizedPiStyleConfig;
|
|
437
|
+
readonly diagnostics: readonly ConfigDiagnostic[];
|
|
438
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
439
|
+
} {
|
|
440
|
+
const diagnostics: ConfigDiagnostic[] = [];
|
|
441
|
+
const layerResults = new Map<string, ConfigLayerResult>();
|
|
442
|
+
const layers: readonly [string, unknown][] = [
|
|
443
|
+
["global", sources.global],
|
|
444
|
+
["project", sources.projectTrusted === false ? undefined : sources.project],
|
|
445
|
+
["session", sources.session],
|
|
446
|
+
];
|
|
447
|
+
for (const [name, value] of layers) {
|
|
448
|
+
const result = validateConfigLayer(value);
|
|
449
|
+
layerResults.set(name, result);
|
|
450
|
+
diagnostics.push(...result.diagnostics);
|
|
451
|
+
}
|
|
452
|
+
const defaults = sources.defaults ?? DEFAULT_CONFIG;
|
|
453
|
+
let merged: unknown = defaults;
|
|
454
|
+
const presetCandidates = [
|
|
455
|
+
["session", sources.session],
|
|
456
|
+
["project", sources.projectTrusted === false ? undefined : sources.project],
|
|
457
|
+
["global", sources.global],
|
|
458
|
+
["default", defaults],
|
|
459
|
+
] as const;
|
|
460
|
+
let selectedPreset: unknown;
|
|
461
|
+
for (const [, source] of presetCandidates) {
|
|
462
|
+
const candidate = isRecord(source) ? source.preset : undefined;
|
|
463
|
+
if (candidate === undefined) continue;
|
|
464
|
+
if (typeof candidate === "string" && (PRESET_NAMES as readonly string[]).includes(candidate)) {
|
|
465
|
+
selectedPreset = candidate;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
diagnostics.push({
|
|
469
|
+
code: "CFG-ENUM",
|
|
470
|
+
level: "warning",
|
|
471
|
+
path: "preset",
|
|
472
|
+
message: "unsupported value; lower-precedence preset used",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
merged = merge(isRecord(merged) ? merged : {}, presetConfig(selectedPreset));
|
|
476
|
+
for (const name of ["global", "project"] as const)
|
|
477
|
+
merged = merge(isRecord(merged) ? merged : {}, layerResults.get(name)?.accepted);
|
|
478
|
+
const env = sources.environment ?? {};
|
|
479
|
+
const envPatch: PiStyleConfig = {};
|
|
480
|
+
if (env.PI_STYLE_DISABLED === "1") envPatch.enabled = false;
|
|
481
|
+
if (env.PI_STYLE_NERD_FONTS === "1" || env.PI_STYLE_NERD_FONTS === "0")
|
|
482
|
+
envPatch.theme = { nerdFonts: env.PI_STYLE_NERD_FONTS === "1" ? "on" : "off" };
|
|
483
|
+
if (env.PI_STYLE_EDITOR && ["native", "compact", "boxed", "dock"].includes(env.PI_STYLE_EDITOR))
|
|
484
|
+
envPatch.editor = { style: env.PI_STYLE_EDITOR };
|
|
485
|
+
if (env.PI_STYLE_OSC11 === "1" || env.PI_STYLE_OSC11 === "0")
|
|
486
|
+
envPatch.theme = { ...(envPatch.theme ?? {}), terminalBackgroundSync: env.PI_STYLE_OSC11 === "1" ? "on" : "off" };
|
|
487
|
+
if (env.PI_STYLE_DEBUG === "1") envPatch.debug = true;
|
|
488
|
+
if (env.PI_STYLE_STATUS === "above" || env.PI_STYLE_STATUS === "below") envPatch.placement = env.PI_STYLE_STATUS;
|
|
489
|
+
if (env.PI_STYLE_STATUS === "off") envPatch.statusLine = { enabled: false };
|
|
490
|
+
if (env.PI_STYLE_DISABLED !== undefined && env.PI_STYLE_DISABLED !== "1")
|
|
491
|
+
diagnostics.push({
|
|
492
|
+
code: "CFG-ENV",
|
|
493
|
+
level: "warning",
|
|
494
|
+
path: "PI_STYLE_DISABLED",
|
|
495
|
+
message: "expected 1; override ignored",
|
|
496
|
+
});
|
|
497
|
+
if (env.PI_STYLE_NERD_FONTS !== undefined && !["0", "1"].includes(env.PI_STYLE_NERD_FONTS))
|
|
498
|
+
diagnostics.push({
|
|
499
|
+
code: "CFG-ENV",
|
|
500
|
+
level: "warning",
|
|
501
|
+
path: "PI_STYLE_NERD_FONTS",
|
|
502
|
+
message: "expected 0 or 1; override ignored",
|
|
503
|
+
});
|
|
504
|
+
if (env.PI_STYLE_STATUS !== undefined && !["above", "below", "off"].includes(env.PI_STYLE_STATUS))
|
|
505
|
+
diagnostics.push({
|
|
506
|
+
code: "CFG-ENV",
|
|
507
|
+
level: "warning",
|
|
508
|
+
path: "PI_STYLE_STATUS",
|
|
509
|
+
message: "expected above, below, or off; override ignored",
|
|
510
|
+
});
|
|
511
|
+
if (env.PI_STYLE_EDITOR !== undefined && !["native", "compact", "boxed", "dock"].includes(env.PI_STYLE_EDITOR))
|
|
512
|
+
diagnostics.push({
|
|
513
|
+
code: "CFG-ENV",
|
|
514
|
+
level: "warning",
|
|
515
|
+
path: "PI_STYLE_EDITOR",
|
|
516
|
+
message: "unknown editor style; override ignored",
|
|
517
|
+
});
|
|
518
|
+
for (const [key, value] of Object.entries(env))
|
|
519
|
+
if (
|
|
520
|
+
value !== undefined &&
|
|
521
|
+
key.startsWith("PI_STYLE_") &&
|
|
522
|
+
![
|
|
523
|
+
"PI_STYLE_DISABLED",
|
|
524
|
+
"PI_STYLE_NERD_FONTS",
|
|
525
|
+
"PI_STYLE_EDITOR",
|
|
526
|
+
"PI_STYLE_OSC11",
|
|
527
|
+
"PI_STYLE_DEBUG",
|
|
528
|
+
"PI_STYLE_STATUS",
|
|
529
|
+
].includes(key)
|
|
530
|
+
)
|
|
531
|
+
diagnostics.push({
|
|
532
|
+
code: "CFG-ENV",
|
|
533
|
+
level: "warning",
|
|
534
|
+
path: key,
|
|
535
|
+
message: "unsupported environment override ignored",
|
|
536
|
+
});
|
|
537
|
+
merged = merge(isRecord(merged) ? merged : {}, envPatch);
|
|
538
|
+
merged = merge(isRecord(merged) ? merged : {}, layerResults.get("session")?.accepted);
|
|
539
|
+
const sourceMap: Record<string, string> = {};
|
|
540
|
+
const sourcePath = (value: unknown, prefix: string, sourceName: string) => {
|
|
541
|
+
if (!isRecord(value)) return;
|
|
542
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
543
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
544
|
+
if (validLeaf(path, nested)) {
|
|
545
|
+
sourceMap[path] = sourceName;
|
|
546
|
+
if (path === "statusLine.customItems" && Array.isArray(nested))
|
|
547
|
+
for (const [index, item] of nested.entries())
|
|
548
|
+
if (isRecord(item))
|
|
549
|
+
for (const field of ["id", "statusKey", "label", "priority", "placement"])
|
|
550
|
+
if (Object.hasOwn(item, field)) sourceMap[`${path}[${index}].${field}`] = sourceName;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (isRecord(nested)) sourcePath(nested, path, sourceName);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
sourcePath(defaults, "", "default");
|
|
557
|
+
sourcePath(
|
|
558
|
+
presetConfig(selectedPreset),
|
|
559
|
+
"",
|
|
560
|
+
`preset:${typeof selectedPreset === "string" ? selectedPreset : "default"}`,
|
|
561
|
+
);
|
|
562
|
+
sourcePath(layerResults.get("global")?.accepted, "", "global");
|
|
563
|
+
if (sources.projectTrusted !== false) sourcePath(layerResults.get("project")?.accepted, "", "project");
|
|
564
|
+
sourcePath(envPatch, "", "environment");
|
|
565
|
+
sourcePath(layerResults.get("session")?.accepted, "", "session");
|
|
566
|
+
return { config: normalizeConfig(merged), diagnostics: boundedDiagnostics(diagnostics), sources: sourceMap };
|
|
567
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PiStyleConfig, PresetName } from "./config-types.js";
|
|
2
|
+
|
|
3
|
+
/** Code-defined coordinated defaults. Explicit fields are merged after these values. */
|
|
4
|
+
export const CONFIG_PRESETS: Readonly<Record<PresetName, Readonly<PiStyleConfig>>> = Object.freeze({
|
|
5
|
+
default: Object.freeze({
|
|
6
|
+
startup: { mode: "compact" },
|
|
7
|
+
}),
|
|
8
|
+
minimal: Object.freeze({
|
|
9
|
+
statusLine: { layout: { left: ["path", "git"], right: ["context_pct"], secondary: [] } },
|
|
10
|
+
editor: { style: "native", frame: "native", showMetadata: false },
|
|
11
|
+
startup: { mode: "off" },
|
|
12
|
+
messages: { enabled: false },
|
|
13
|
+
tools: { enabled: false },
|
|
14
|
+
theme: { terminalBackgroundSync: "off" },
|
|
15
|
+
}),
|
|
16
|
+
compact: Object.freeze({
|
|
17
|
+
statusLine: {
|
|
18
|
+
layout: { left: ["model", "thinking", "git"], right: ["context_pct"], secondary: ["extension_statuses"] },
|
|
19
|
+
},
|
|
20
|
+
editor: { style: "compact", frame: "auto", showMetadata: false },
|
|
21
|
+
startup: { mode: "compact" },
|
|
22
|
+
}),
|
|
23
|
+
full: Object.freeze({
|
|
24
|
+
statusLine: {
|
|
25
|
+
layout: {
|
|
26
|
+
left: ["hostname", "model", "thinking", "path", "git", "session"],
|
|
27
|
+
right: ["token_in", "token_out", "cache_read", "cost", "context_pct", "time_spent", "time"],
|
|
28
|
+
secondary: ["extension_statuses"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
editor: { style: "boxed", frame: "outline", showMetadata: true },
|
|
32
|
+
startup: { mode: "overlay", showResources: true },
|
|
33
|
+
}),
|
|
34
|
+
ascii: Object.freeze({
|
|
35
|
+
editor: { style: "compact", frame: "auto" },
|
|
36
|
+
startup: { mode: "compact" },
|
|
37
|
+
theme: { nerdFonts: "off", terminalBackgroundSync: "off" },
|
|
38
|
+
}),
|
|
39
|
+
native: Object.freeze({
|
|
40
|
+
statusLine: { layout: { left: ["model", "path"], right: ["context_pct"], secondary: [] } },
|
|
41
|
+
editor: { style: "native", frame: "native", showMetadata: false },
|
|
42
|
+
startup: { mode: "off" },
|
|
43
|
+
messages: { enabled: false },
|
|
44
|
+
tools: { enabled: false },
|
|
45
|
+
theme: { terminalBackgroundSync: "off" },
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function presetConfig(name: unknown): Readonly<PiStyleConfig> {
|
|
50
|
+
return CONFIG_PRESETS[(typeof name === "string" && name in CONFIG_PRESETS ? name : "default") as PresetName];
|
|
51
|
+
}
|