@neilurk12/pi-clean-footer 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # pi-clean-footer
2
2
 
3
- Clean adaptive footer extension for [pi](https://pi.dev).
3
+ Clean, minimal, and lightweight powerline-style footer extension for [pi](https://pi.dev).
4
4
 
5
5
  Shows a compact split footer:
6
6
 
7
- ![pi-clean-footer example](./example.png)
7
+ ![pi-clean-footer example](https://raw.githubusercontent.com/Neil-urk12/pi-dots/main/footer/example.png)
8
8
 
9
9
  ## Features
10
10
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@neilurk12/pi-clean-footer",
3
- "version": "0.1.1",
4
- "description": "Clean adaptive footer extension for pi coding agent.",
3
+ "version": "0.2.1",
4
+ "description": "Clean, minimal, and lightweight powerline-style footer extension for pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",
@@ -9,6 +9,10 @@
9
9
  "footer",
10
10
  "terminal"
11
11
  ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/Neil-urk12/pi-dots.git"
15
+ },
12
16
  "license": "MIT",
13
17
  "files": [
14
18
  "src",
@@ -24,5 +28,8 @@
24
28
  "@earendil-works/pi-ai": "*",
25
29
  "@earendil-works/pi-coding-agent": "*",
26
30
  "@earendil-works/pi-tui": "*"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^6.0.3"
27
34
  }
28
35
  }
package/src/config.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ export const DEFAULT_GIT_REFRESH_DEBOUNCE_MS = 500;
4
+
5
+ export type CleanFooterConfig = {
6
+ enabled?: boolean;
7
+ showGit?: boolean;
8
+ showTokens?: boolean;
9
+ showCache?: boolean;
10
+ showContext?: boolean;
11
+ showDirectory?: boolean;
12
+ showEffort?: boolean;
13
+ gitRefreshDebounceMs?: number;
14
+ contextWarningPercent?: number;
15
+ contextDangerPercent?: number;
16
+ modelAliases?: Record<string, string>;
17
+ colors?: Partial<ColorConfig>;
18
+ };
19
+
20
+ export type ResolvedConfig = Required<
21
+ Omit<CleanFooterConfig, "modelAliases" | "colors">
22
+ > & {
23
+ modelAliases: Record<string, string>;
24
+ colors: ColorConfig;
25
+ };
26
+
27
+ export type ColorConfig = {
28
+ model: string;
29
+ directory: string;
30
+ git: string;
31
+ gitDirty: string;
32
+ contextNormal: string;
33
+ contextWarning: string;
34
+ contextDanger: string;
35
+ tokens: string;
36
+ separator: string;
37
+ };
38
+
39
+ export type ConfigLoadResult = {
40
+ config: ResolvedConfig;
41
+ loadedPaths: string[];
42
+ error?: string;
43
+ };
44
+
45
+ export const defaultConfig: ResolvedConfig = {
46
+ enabled: true,
47
+ showGit: true,
48
+ showTokens: true,
49
+ showCache: true,
50
+ showContext: true,
51
+ showDirectory: true,
52
+ showEffort: true,
53
+ gitRefreshDebounceMs: DEFAULT_GIT_REFRESH_DEBOUNCE_MS,
54
+ contextWarningPercent: 70,
55
+ contextDangerPercent: 85,
56
+ modelAliases: {},
57
+ colors: {
58
+ model: "accent",
59
+ directory: "dim",
60
+ git: "success",
61
+ gitDirty: "warning",
62
+ contextNormal: "success",
63
+ contextWarning: "warning",
64
+ contextDanger: "error",
65
+ tokens: "muted",
66
+ separator: "dim",
67
+ },
68
+ };
69
+
70
+ export function loadConfig(paths: string[]): ConfigLoadResult {
71
+ const loaded: string[] = [];
72
+ let merged: CleanFooterConfig = {};
73
+ let error: string | undefined;
74
+
75
+ for (const configPath of paths) {
76
+ if (!existsSync(configPath)) continue;
77
+ try {
78
+ const parsed = JSON.parse(
79
+ readFileSync(configPath, "utf8"),
80
+ ) as CleanFooterConfig;
81
+ merged = mergeConfig(merged, parsed);
82
+ loaded.push(configPath);
83
+ } catch (err) {
84
+ error = `${configPath}: ${err instanceof Error ? err.message : String(err)}`;
85
+ }
86
+ }
87
+
88
+ return {
89
+ config: resolveConfig(merged),
90
+ loadedPaths: loaded,
91
+ error,
92
+ };
93
+ }
94
+
95
+ function mergeConfig(
96
+ base: CleanFooterConfig,
97
+ override: CleanFooterConfig,
98
+ ): CleanFooterConfig {
99
+ return {
100
+ ...base,
101
+ ...override,
102
+ modelAliases: {
103
+ ...(base.modelAliases ?? {}),
104
+ ...(override.modelAliases ?? {}),
105
+ },
106
+ colors: {
107
+ ...(base.colors ?? {}),
108
+ ...(override.colors ?? {}),
109
+ },
110
+ };
111
+ }
112
+
113
+ function resolveConfig(config: CleanFooterConfig): ResolvedConfig {
114
+ return {
115
+ ...defaultConfig,
116
+ ...config,
117
+ gitRefreshDebounceMs: positiveNumber(
118
+ config.gitRefreshDebounceMs,
119
+ defaultConfig.gitRefreshDebounceMs,
120
+ ),
121
+ contextWarningPercent: percentNumber(
122
+ config.contextWarningPercent,
123
+ defaultConfig.contextWarningPercent,
124
+ ),
125
+ contextDangerPercent: percentNumber(
126
+ config.contextDangerPercent,
127
+ defaultConfig.contextDangerPercent,
128
+ ),
129
+ modelAliases: {
130
+ ...defaultConfig.modelAliases,
131
+ ...(config.modelAliases ?? {}),
132
+ },
133
+ colors: { ...defaultConfig.colors, ...(config.colors ?? {}) },
134
+ };
135
+ }
136
+
137
+ function positiveNumber(value: unknown, fallback: number): number {
138
+ return typeof value === "number" && Number.isFinite(value) && value > 0
139
+ ? value
140
+ : fallback;
141
+ }
142
+
143
+ function percentNumber(value: unknown, fallback: number): number {
144
+ return typeof value === "number" &&
145
+ Number.isFinite(value) &&
146
+ value >= 0 &&
147
+ value <= 100
148
+ ? value
149
+ : fallback;
150
+ }
package/src/index.ts CHANGED
@@ -5,49 +5,19 @@ import type {
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
7
  import { execFile } from "node:child_process";
8
- import { existsSync, readFileSync } from "node:fs";
9
8
  import os from "node:os";
10
9
  import path from "node:path";
11
10
  import { promisify } from "node:util";
12
11
 
13
12
  const execFileAsync = promisify(execFile);
14
- const DEFAULT_GIT_REFRESH_DEBOUNCE_MS = 500;
15
13
 
16
14
  type Theme = ExtensionContext["ui"]["theme"];
17
15
 
18
- type CleanFooterConfig = {
19
- enabled?: boolean;
20
- showGit?: boolean;
21
- showTokens?: boolean;
22
- showCache?: boolean;
23
- showContext?: boolean;
24
- showDirectory?: boolean;
25
- showEffort?: boolean;
26
- gitRefreshDebounceMs?: number;
27
- contextWarningPercent?: number;
28
- contextDangerPercent?: number;
29
- modelAliases?: Record<string, string>;
30
- colors?: Partial<ColorConfig>;
31
- };
32
-
33
- type ResolvedConfig = Required<
34
- Omit<CleanFooterConfig, "modelAliases" | "colors">
35
- > & {
36
- modelAliases: Record<string, string>;
37
- colors: ColorConfig;
38
- };
39
-
40
- type ColorConfig = {
41
- model: string;
42
- directory: string;
43
- git: string;
44
- gitDirty: string;
45
- contextNormal: string;
46
- contextWarning: string;
47
- contextDanger: string;
48
- tokens: string;
49
- separator: string;
50
- };
16
+ import {
17
+ defaultConfig,
18
+ loadConfig,
19
+ type ResolvedConfig,
20
+ } from "./config.js";
51
21
 
52
22
  type GitState = {
53
23
  inRepo: boolean;
@@ -74,31 +44,6 @@ type FooterRuntime = {
74
44
  configError?: string;
75
45
  };
76
46
 
77
- const defaultConfig: ResolvedConfig = {
78
- enabled: true,
79
- showGit: true,
80
- showTokens: true,
81
- showCache: true,
82
- showContext: true,
83
- showDirectory: true,
84
- showEffort: true,
85
- gitRefreshDebounceMs: DEFAULT_GIT_REFRESH_DEBOUNCE_MS,
86
- contextWarningPercent: 70,
87
- contextDangerPercent: 85,
88
- modelAliases: {},
89
- colors: {
90
- model: "accent",
91
- directory: "dim",
92
- git: "success",
93
- gitDirty: "warning",
94
- contextNormal: "success",
95
- contextWarning: "warning",
96
- contextDanger: "error",
97
- tokens: "muted",
98
- separator: "dim",
99
- },
100
- };
101
-
102
47
  const runtime: FooterRuntime = {
103
48
  enabled: true,
104
49
  git: { inRepo: false, dirtyCount: 0 },
@@ -124,7 +69,7 @@ export default function (pi: ExtensionAPI) {
124
69
  }
125
70
 
126
71
  if (command === "reload") {
127
- loadConfig(ctx.cwd);
72
+ loadRuntimeConfig(ctx.cwd);
128
73
  runtime.enabled = runtime.config.enabled;
129
74
  if (ctx.hasUI && runtime.enabled) installFooter(ctx);
130
75
  if (ctx.hasUI && !runtime.enabled) ctx.ui.setFooter(undefined);
@@ -155,7 +100,7 @@ export default function (pi: ExtensionAPI) {
155
100
  });
156
101
 
157
102
  pi.on("session_start", async (_event, ctx) => {
158
- loadConfig(ctx.cwd);
103
+ loadRuntimeConfig(ctx.cwd);
159
104
  runtime.enabled = runtime.config.enabled;
160
105
  runtime.thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel?.());
161
106
  if (!ctx.hasUI || !runtime.enabled) return;
@@ -270,90 +215,13 @@ function installFooter(ctx: ExtensionContext) {
270
215
  });
271
216
  }
272
217
 
273
- function loadConfig(cwd: string) {
274
- const configPaths = {
275
- global: path.join(os.homedir(), ".pi", "agent", "clean-footer.json"),
276
- project: path.join(cwd, ".pi", "clean-footer.json"),
277
- };
278
-
279
- const loaded: string[] = [];
280
- let merged: CleanFooterConfig = {};
281
- let error: string | undefined;
282
-
283
- for (const configPath of [configPaths.global, configPaths.project]) {
284
- if (!existsSync(configPath)) continue;
285
- try {
286
- const parsed = JSON.parse(
287
- readFileSync(configPath, "utf8"),
288
- ) as CleanFooterConfig;
289
- merged = mergeConfig(merged, parsed);
290
- loaded.push(configPath);
291
- } catch (err) {
292
- error = `${configPath}: ${err instanceof Error ? err.message : String(err)}`;
293
- }
294
- }
295
-
296
- runtime.configPaths = configPaths;
297
- runtime.loadedConfigPaths = loaded;
298
- runtime.configError = error;
299
- runtime.config = resolveConfig(merged);
300
- }
301
-
302
- function mergeConfig(
303
- base: CleanFooterConfig,
304
- override: CleanFooterConfig,
305
- ): CleanFooterConfig {
306
- return {
307
- ...base,
308
- ...override,
309
- modelAliases: {
310
- ...(base.modelAliases ?? {}),
311
- ...(override.modelAliases ?? {}),
312
- },
313
- colors: {
314
- ...(base.colors ?? {}),
315
- ...(override.colors ?? {}),
316
- },
317
- };
318
- }
319
-
320
- function resolveConfig(config: CleanFooterConfig): ResolvedConfig {
321
- return {
322
- ...defaultConfig,
323
- ...config,
324
- gitRefreshDebounceMs: positiveNumber(
325
- config.gitRefreshDebounceMs,
326
- defaultConfig.gitRefreshDebounceMs,
327
- ),
328
- contextWarningPercent: percentNumber(
329
- config.contextWarningPercent,
330
- defaultConfig.contextWarningPercent,
331
- ),
332
- contextDangerPercent: percentNumber(
333
- config.contextDangerPercent,
334
- defaultConfig.contextDangerPercent,
335
- ),
336
- modelAliases: {
337
- ...defaultConfig.modelAliases,
338
- ...(config.modelAliases ?? {}),
339
- },
340
- colors: { ...defaultConfig.colors, ...(config.colors ?? {}) },
341
- };
342
- }
343
-
344
- function positiveNumber(value: unknown, fallback: number): number {
345
- return typeof value === "number" && Number.isFinite(value) && value > 0
346
- ? value
347
- : fallback;
348
- }
349
-
350
- function percentNumber(value: unknown, fallback: number): number {
351
- return typeof value === "number" &&
352
- Number.isFinite(value) &&
353
- value >= 0 &&
354
- value <= 100
355
- ? value
356
- : fallback;
218
+ function loadRuntimeConfig(cwd: string) {
219
+ const projectPath = path.join(cwd, ".pi", "clean-footer.json");
220
+ const result = loadConfig([runtime.configPaths.global, projectPath]);
221
+ runtime.configPaths = { global: runtime.configPaths.global, project: projectPath };
222
+ runtime.loadedConfigPaths = result.loadedPaths;
223
+ runtime.configError = result.error;
224
+ runtime.config = result.config;
357
225
  }
358
226
 
359
227
  function notifyConfigStatus(ctx: ExtensionContext) {