@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -0
  4. package/dist/extensions/pi-style.js +7897 -0
  5. package/dist/extensions/pi-style.js.map +1 -0
  6. package/extension-src/pi-style/app/command-service.ts +244 -0
  7. package/extension-src/pi-style/app/commands.ts +12 -0
  8. package/extension-src/pi-style/app/config-storage.ts +98 -0
  9. package/extension-src/pi-style/app/doctor.ts +53 -0
  10. package/extension-src/pi-style/app/index.ts +322 -0
  11. package/extension-src/pi-style/app/providers.ts +268 -0
  12. package/extension-src/pi-style/app/render-scheduler.ts +48 -0
  13. package/extension-src/pi-style/app/runtime.ts +404 -0
  14. package/extension-src/pi-style/app/snapshot.ts +15 -0
  15. package/extension-src/pi-style/domain/capabilities.ts +34 -0
  16. package/extension-src/pi-style/domain/config-authorization.ts +20 -0
  17. package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
  18. package/extension-src/pi-style/domain/config-diff.ts +30 -0
  19. package/extension-src/pi-style/domain/config-migrations.ts +52 -0
  20. package/extension-src/pi-style/domain/config-normalization.ts +567 -0
  21. package/extension-src/pi-style/domain/config-presets.ts +51 -0
  22. package/extension-src/pi-style/domain/config-types.ts +115 -0
  23. package/extension-src/pi-style/domain/providers.ts +37 -0
  24. package/extension-src/pi-style/domain/status-presets.ts +60 -0
  25. package/extension-src/pi-style/domain/status-renderer.ts +142 -0
  26. package/extension-src/pi-style/domain/status.ts +430 -0
  27. package/extension-src/pi-style/domain/theme.ts +232 -0
  28. package/extension-src/pi-style/features/.gitkeep +0 -0
  29. package/extension-src/pi-style/features/editor/index.ts +304 -0
  30. package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
  31. package/extension-src/pi-style/features/messages/index.ts +145 -0
  32. package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
  33. package/extension-src/pi-style/features/startup/index.ts +564 -0
  34. package/extension-src/pi-style/features/startup/logo.ts +151 -0
  35. package/extension-src/pi-style/features/status-line/index.ts +319 -0
  36. package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
  37. package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
  38. package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
  39. package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
  40. package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
  41. package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
  42. package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
  43. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
  44. package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
  45. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
  46. package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
  47. package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
  48. package/extension-src/pi-style/features/tools/index.ts +480 -0
  49. package/extension-src/pi-style/pi/commands.ts +17 -0
  50. package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
  51. package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
  52. package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
  53. package/extension-src/pi-style/pi/config-host.ts +52 -0
  54. package/extension-src/pi-style/pi/config-session.ts +105 -0
  55. package/extension-src/pi-style/pi/index.ts +77 -0
  56. package/extension-src/pi-style/pi/operational-state.ts +29 -0
  57. package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
  58. package/extension-src/pi-style/pi/session-usage.ts +62 -0
  59. package/extension-src/pi-style/pi/startup-resources.ts +70 -0
  60. package/extension-src/pi-style/shared/.gitkeep +0 -0
  61. package/extension-src/pi-style/shared/ansi.ts +386 -0
  62. package/extension-src/pi-style/shared/box.ts +805 -0
  63. package/extension-src/pi-style/shared/disposable-store.ts +28 -0
  64. package/extension-src/pi-style/shared/elapsed.ts +88 -0
  65. package/extension-src/pi-style/shared/render-budget.ts +367 -0
  66. package/extension-src/pi-style/shared/split-diff.ts +692 -0
  67. package/extension-src/pi-style/shared/theme-extras.ts +292 -0
  68. package/package.json +68 -0
  69. package/themes/.gitkeep +0 -0
@@ -0,0 +1,292 @@
1
+ // Theme extras reader.
2
+ //
3
+ // Reads extension-specific "extras" plus raw theme vars/export values from the
4
+ // active theme's JSON file on disk. Pi does not expose all of these raw values
5
+ // through the theme object, so we locate and parse the file directly.
6
+ //
7
+ // pi-style constraint: no filesystem I/O inside render. The cache is populated
8
+ // by `syncThemeExtras(theme, force)` at session start and on theme change; the
9
+ // render-facing `getThemeExtra`/`getThemePageBackground`/`getThemeVarBackground`
10
+ // read cached values only and return "" (graceful fallback) when unavailable.
11
+
12
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { isHexColor } from "../shared/ansi.js";
17
+
18
+ const extensionDir = dirname(fileURLToPath(import.meta.url));
19
+
20
+ export const THEME_EXTRA_DEFAULTS: Readonly<Record<string, string>> = Object.freeze({
21
+ assistantPrefix: "•",
22
+ assistantPrefixColor: "",
23
+ userPrefix: "❯",
24
+ userPrefixColor: "accent",
25
+ dividerChar: "─",
26
+ dividerColor: "",
27
+ showDivider: "true",
28
+ quoteStyle: "false",
29
+ quoteChar: "┆",
30
+ quoteColor: "",
31
+ inputBorderColor: "",
32
+ bashPromptColor: "",
33
+ tagBgColor: "",
34
+ parensTextColor: "",
35
+ parensBracketColor: "",
36
+ slashSelectedColor: "",
37
+ slashCommandColor: "",
38
+ slashDescriptionColor: "",
39
+ slashHintColor: "",
40
+ userBoxBorderColor: "",
41
+ gitInsertionColor: "#2ea043",
42
+ gitDeletionColor: "#f85149",
43
+ });
44
+
45
+ let cachedExtras: Record<string, string | boolean> | null = null;
46
+ let cachedVars: Record<string, string> | null = null;
47
+ let cachedColors: Record<string, string> | null = null;
48
+ let cachedThemeExport: Record<string, string> | null = null;
49
+ let cachedThemeName: string | null = null;
50
+
51
+ type ThemeDiscovery = {
52
+ extras: Record<string, string | boolean> | null;
53
+ vars: Record<string, string> | null;
54
+ colors: Record<string, string> | null;
55
+ themeExport: Record<string, string> | null;
56
+ };
57
+
58
+ function themeDiscoveryFromContent(content: unknown): ThemeDiscovery | null {
59
+ if (!content || typeof content !== "object") return null;
60
+ const record = content as Record<string, unknown>;
61
+ const extras =
62
+ record.extras && typeof record.extras === "object" ? (record.extras as Record<string, string | boolean>) : null;
63
+ const vars = record.vars && typeof record.vars === "object" ? (record.vars as Record<string, string>) : null;
64
+ const colors = record.colors && typeof record.colors === "object" ? (record.colors as Record<string, string>) : null;
65
+ const themeExport =
66
+ record.export && typeof record.export === "object" ? (record.export as Record<string, string>) : null;
67
+ return extras || vars || colors || themeExport ? { extras, vars, colors, themeExport } : null;
68
+ }
69
+
70
+ function readThemeDiscoveryFromPath(filePath: string): ThemeDiscovery | null {
71
+ try {
72
+ if (!filePath || !existsSync(filePath)) return null;
73
+ const content = JSON.parse(readFileSync(filePath, "utf-8")) as unknown;
74
+ return themeDiscoveryFromContent(content);
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function resolveThemeSourcePath(theme: unknown): string {
81
+ if (!theme || typeof theme !== "object") return "";
82
+ const record = theme as Record<string, unknown>;
83
+ if (typeof record.sourcePath === "string") return record.sourcePath;
84
+ const definition =
85
+ record.definition && typeof record.definition === "object"
86
+ ? (record.definition as Record<string, unknown>)
87
+ : undefined;
88
+ return typeof definition?.sourcePath === "string" ? definition.sourcePath : "";
89
+ }
90
+
91
+ function addThemeDir(searchDirs: Set<string>, dir: string): void {
92
+ if (existsSync(dir)) searchDirs.add(dir);
93
+ }
94
+
95
+ function addBundledThemeDirs(searchDirs: Set<string>): void {
96
+ for (const root of [extensionDir, process.cwd()]) {
97
+ for (const scope of ["@earendil-works", "@mariozechner"]) {
98
+ addThemeDir(
99
+ searchDirs,
100
+ resolve(root, "node_modules", scope, "pi-coding-agent", "dist", "modes", "interactive", "theme"),
101
+ );
102
+ addThemeDir(searchDirs, resolve(root, "node_modules", scope, "pi-coding-agent", "dist", "theme"));
103
+ }
104
+ }
105
+ }
106
+
107
+ function collectThemeDirs(root: string, searchDirs: Set<string>, maxDepth = 4): void {
108
+ if (maxDepth < 0 || !existsSync(root)) return;
109
+ try {
110
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
111
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
112
+ const dir = join(root, entry.name);
113
+ if (entry.name === "themes") {
114
+ searchDirs.add(dir);
115
+ continue;
116
+ }
117
+ collectThemeDirs(dir, searchDirs, maxDepth - 1);
118
+ }
119
+ } catch {
120
+ // best effort discovery only
121
+ }
122
+ }
123
+
124
+ function readSettingsPackagePaths(settingsPath: string): string[] {
125
+ if (!existsSync(settingsPath)) return [];
126
+ try {
127
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
128
+ const entries = [
129
+ ...(Array.isArray(settings.packages) ? settings.packages : []),
130
+ ...(Array.isArray(settings.extensions) ? settings.extensions : []),
131
+ ];
132
+ return entries
133
+ .map((entry) =>
134
+ typeof entry === "string"
135
+ ? entry
136
+ : entry && typeof entry === "object"
137
+ ? String((entry as Record<string, unknown>).source ?? "")
138
+ : "",
139
+ )
140
+ .filter((entry) => entry && !entry.startsWith("npm:") && !entry.startsWith("git:"));
141
+ } catch {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ /** Scan known theme directories for a JSON file whose "name" matches themeName. */
147
+ function discoverThemeExtras(themeName: string): ThemeDiscovery | null {
148
+ const searchDirs = new Set<string>();
149
+
150
+ addThemeDir(searchDirs, join(homedir(), ".pi", "agent", "themes"));
151
+ addThemeDir(searchDirs, resolve(process.cwd(), ".pi", "themes"));
152
+ addBundledThemeDirs(searchDirs);
153
+ collectThemeDirs(join(homedir(), ".pi", "agent", "git"), searchDirs);
154
+ collectThemeDirs(resolve(process.cwd(), ".pi", "git"), searchDirs);
155
+
156
+ const localPackagePaths = [
157
+ ...readSettingsPackagePaths(join(homedir(), ".pi", "agent", "settings.json")),
158
+ ...readSettingsPackagePaths(resolve(process.cwd(), ".pi", "settings.json")),
159
+ ];
160
+ for (const packagePath of localPackagePaths) {
161
+ addThemeDir(searchDirs, resolve(process.cwd(), packagePath, "themes"));
162
+ }
163
+
164
+ for (const dir of searchDirs) {
165
+ try {
166
+ for (const file of readdirSync(dir)) {
167
+ if (!file.endsWith(".json")) continue;
168
+ const filePath = join(dir, file);
169
+ try {
170
+ const content = JSON.parse(readFileSync(filePath, "utf-8")) as Record<string, unknown>;
171
+ if (content?.name === themeName) {
172
+ const result = themeDiscoveryFromContent(content);
173
+ if (result) return result;
174
+ }
175
+ } catch {
176
+ // skip unparsable theme files
177
+ }
178
+ }
179
+ } catch {
180
+ // skip unreadable directories
181
+ }
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ function resolveThemeName(theme: unknown): string | null {
188
+ if (!theme || typeof theme !== "object") return null;
189
+ const record = theme as Record<string, unknown>;
190
+ const definition =
191
+ record.definition && typeof record.definition === "object"
192
+ ? (record.definition as Record<string, unknown>)
193
+ : undefined;
194
+ if (typeof definition?.name === "string") return definition.name;
195
+ if (typeof record.name === "string") return record.name;
196
+ try {
197
+ const settingsPath = join(homedir(), ".pi", "agent", "settings.json");
198
+ if (existsSync(settingsPath)) {
199
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
200
+ if (typeof settings.theme === "string") return settings.theme;
201
+ }
202
+ } catch {
203
+ // fall through
204
+ }
205
+ return null;
206
+ }
207
+
208
+ function readThemeToken(name: string): string {
209
+ const varValue = cachedVars && typeof cachedVars[name] === "string" ? cachedVars[name] : "";
210
+ if (varValue) return varValue;
211
+ return cachedColors && typeof cachedColors[name] === "string" ? cachedColors[name] : "";
212
+ }
213
+
214
+ function resolveThemeColorToken(value: string): string {
215
+ let resolved = value;
216
+ const seen = new Set<string>();
217
+ for (let depth = 0; depth < 8; depth++) {
218
+ if (!resolved) return "";
219
+ if (isHexColor(resolved)) return resolved;
220
+ if (seen.has(resolved)) return "";
221
+ seen.add(resolved);
222
+
223
+ const next = readThemeToken(resolved);
224
+ if (!next) return "";
225
+ resolved = next;
226
+ }
227
+ return "";
228
+ }
229
+
230
+ function resolveThemeExtraValue(key: string, value: string): string {
231
+ if (!key.endsWith("Color")) return value;
232
+ return resolveThemeColorToken(value) || value;
233
+ }
234
+
235
+ function resolveThemeExportColor(key: string): string {
236
+ if (!cachedThemeExport) return "";
237
+ const value = cachedThemeExport[key];
238
+ if (typeof value !== "string" || !value) return "";
239
+ const resolved = cachedVars && typeof cachedVars[value] === "string" ? cachedVars[value] : value;
240
+ return isHexColor(resolved) ? resolved : "";
241
+ }
242
+
243
+ /** Populate the extras cache from a theme (session start / theme change only). */
244
+ export function setFullTheme(theme: unknown, force = false): void {
245
+ const themeName = resolveThemeName(theme);
246
+ const sourcePath = resolveThemeSourcePath(theme);
247
+ if (!themeName && !sourcePath) return;
248
+
249
+ const cacheKey = sourcePath || themeName;
250
+ if (
251
+ !force &&
252
+ cacheKey === cachedThemeName &&
253
+ (cachedExtras !== null || cachedVars !== null || cachedColors !== null || cachedThemeExport !== null)
254
+ )
255
+ return;
256
+
257
+ cachedThemeName = cacheKey;
258
+ const result = readThemeDiscoveryFromPath(sourcePath) ?? (themeName ? discoverThemeExtras(themeName) : null);
259
+ cachedExtras = result?.extras ?? null;
260
+ cachedVars = result?.vars ?? null;
261
+ cachedColors = result?.colors ?? null;
262
+ cachedThemeExport = result?.themeExport ?? null;
263
+ }
264
+
265
+ export function resetThemeExtrasCache(): void {
266
+ cachedExtras = null;
267
+ cachedVars = null;
268
+ cachedColors = null;
269
+ cachedThemeExport = null;
270
+ cachedThemeName = null;
271
+ }
272
+
273
+ /** Resolve a theme-extra key from the cache; "" when unavailable (graceful fallback). */
274
+ export function getThemeExtra(_theme: unknown, key: string): string {
275
+ const extraValue = cachedExtras?.[key];
276
+ if (typeof extraValue === "string" || typeof extraValue === "boolean") {
277
+ return resolveThemeExtraValue(key, String(extraValue));
278
+ }
279
+ return resolveThemeExtraValue(key, THEME_EXTRA_DEFAULTS[key] ?? "");
280
+ }
281
+
282
+ export function getThemePageBackground(_theme: unknown): string {
283
+ const directBg = cachedVars && typeof cachedVars.bg === "string" ? cachedVars.bg : "";
284
+ if (isHexColor(directBg)) return directBg;
285
+ return resolveThemeExportColor("pageBg");
286
+ }
287
+
288
+ export function getThemeVarBackground(_theme: unknown, varName: string): string {
289
+ const value = cachedVars && typeof cachedVars[varName] === "string" ? cachedVars[varName] : "";
290
+ const resolved = cachedVars && value && typeof cachedVars[value] === "string" ? cachedVars[value] : value;
291
+ return isHexColor(resolved) ? resolved : "";
292
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@quandev104/pi-style",
3
+ "version": "0.1.1",
4
+ "description": "A native-layout, cohesive visual style package for Pi.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "pi-coding-agent",
11
+ "extension",
12
+ "tui",
13
+ "theme"
14
+ ],
15
+ "pi": {
16
+ "extensions": [
17
+ "./extension-src/pi-style/pi/index.ts"
18
+ ],
19
+ "themes": [
20
+ "./themes"
21
+ ]
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "themes",
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE",
29
+ "extension-src"
30
+ ],
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit",
33
+ "lint": "biome check .",
34
+ "lint:fix": "biome check --write .",
35
+ "depcruise": "depcruise extension-src/pi-style --config dependency-cruiser.config.cjs",
36
+ "test": "vitest run",
37
+ "test:rendering-and-runtime-contracts": "vitest run test/render/*.test.ts test/performance/*.test.ts test/e2e/*.test.ts",
38
+ "test:watch": "vitest",
39
+ "build": "tsup",
40
+ "package:smoke": "node --input-type=module -e \"const manifest = await import('./package.json', { with: { type: 'json' } }); const entry = manifest.default.pi.extensions[0]; if (entry !== './extension-src/pi-style/pi/index.ts') throw new Error('invalid Pi extension manifest path: ' + entry); const { readFileSync } = await import('node:fs'); const source = readFileSync('./extension-src/pi-style/pi/index.ts', 'utf8'); if (!/export\\s+default\\s+function/.test(source)) throw new Error('extension TS entry has no default factory'); if (!source.includes('createPiStyleSessionCoordinator')) throw new Error('extension TS entry missing coordinator wiring'); console.log('package smoke: extension TS entry valid (jiti loads with Pi runtime aliases)');\"\"\"",
41
+ "dev": "tsup --watch",
42
+ "check": "npm run typecheck && npm run lint && npm run depcruise && npm run test && npm run build && npm run package:smoke"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-agent-core": ">=0.83.0 <0.84.0",
46
+ "@earendil-works/pi-ai": ">=0.83.0 <0.84.0",
47
+ "@earendil-works/pi-coding-agent": ">=0.83.0 <0.84.0",
48
+ "@earendil-works/pi-tui": ">=0.83.0 <0.84.0",
49
+ "typebox": ">=1.3.9 <2.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "^2.5.0",
53
+ "@earendil-works/pi-agent-core": "^0.83.0",
54
+ "@earendil-works/pi-ai": "^0.83.0",
55
+ "@earendil-works/pi-coding-agent": "^0.83.0",
56
+ "@earendil-works/pi-tui": "^0.83.0",
57
+ "@types/node": "^24.0.0",
58
+ "dependency-cruiser": "^17.4.3",
59
+ "git-cliff": "^2.13.1",
60
+ "tsup": "^8.5.0",
61
+ "typebox": "^1.3.9",
62
+ "typescript": "^5.8.3",
63
+ "vitest": "^4.1.8"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.0.0"
67
+ }
68
+ }
File without changes