@herbertgao/pi-extensions 2026.8.3 → 2026.8.4

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 (122) hide show
  1. package/README.md +41 -0
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  4. package/node_modules/pi-footer/LICENSE +21 -0
  5. package/node_modules/pi-footer/README.md +354 -0
  6. package/node_modules/pi-footer/package.json +68 -0
  7. package/node_modules/pi-footer/src/cache.ts +106 -0
  8. package/node_modules/pi-footer/src/colors.ts +237 -0
  9. package/node_modules/pi-footer/src/config.ts +202 -0
  10. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  11. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  12. package/node_modules/pi-footer/src/git.ts +135 -0
  13. package/node_modules/pi-footer/src/index.ts +268 -0
  14. package/node_modules/pi-footer/src/metrics.ts +141 -0
  15. package/node_modules/pi-footer/src/presets.ts +383 -0
  16. package/node_modules/pi-footer/src/render.ts +114 -0
  17. package/node_modules/pi-footer/src/separators.ts +52 -0
  18. package/node_modules/pi-footer/src/types.ts +122 -0
  19. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  20. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  21. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  22. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  23. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  24. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  25. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  26. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  27. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  28. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  29. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  30. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  31. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  32. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  33. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  34. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  35. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  36. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  37. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  38. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  39. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  40. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  41. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  42. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  43. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  44. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  45. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  46. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  47. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  48. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  49. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  50. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  51. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  52. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  53. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  54. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  55. package/node_modules/pi-footer/src/ui.ts +66 -0
  56. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  57. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  58. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  59. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  60. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  61. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  62. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  63. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  64. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  65. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  66. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  67. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  68. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  69. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  70. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  71. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  72. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  73. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  74. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  75. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  76. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  77. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  78. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  79. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  80. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  81. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  82. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  83. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  84. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  85. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  86. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  87. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  88. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  89. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  90. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  91. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  92. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  93. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  94. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  95. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  96. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  97. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  99. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  100. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  102. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  103. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  104. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  105. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  106. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  107. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  108. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  109. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  110. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  111. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  112. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  113. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  114. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  115. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  116. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  117. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  118. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  119. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  120. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  121. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  122. package/package.json +7 -2
@@ -0,0 +1,18 @@
1
+ import { defineWidget } from "../types.js";
2
+ import { formatTokenCount, tokenFormatStyleProperty } from "../utils/token-format.js";
3
+
4
+ export const OutputTokensWidget = defineWidget({
5
+ type: "output-tokens",
6
+ label: "Output Tokens",
7
+ category: "Tokens",
8
+ description: "Output token total",
9
+ dependencies: ["metrics"],
10
+ baseOptions: ["raw", "hideWhenZero", "icon"],
11
+ baseOptionDefaults: {},
12
+ properties: [tokenFormatStyleProperty()],
13
+ icons: { emoji: "⬇️", nerd: "󰧚", text: "out" },
14
+ defaultStyle: { fg: "white", bg: "default", bold: false },
15
+ render({ ctx, options, renderWidget }) {
16
+ return renderWidget(formatTokenCount(ctx.metrics.outputTokens, options.tokenFormatStyle));
17
+ },
18
+ });
@@ -0,0 +1,20 @@
1
+ import { defineWidget } from "../types.js";
2
+ import { formatTokenCount, tokenFormatStyleProperty } from "../utils/token-format.js";
3
+
4
+ export const TokensWidget = defineWidget({
5
+ type: "tokens",
6
+ label: "Input/Output Tokens",
7
+ category: "Tokens",
8
+ description: "Input and output token totals",
9
+ dependencies: ["metrics"],
10
+ baseOptions: ["raw", "icon"],
11
+ baseOptionDefaults: {},
12
+ properties: [tokenFormatStyleProperty()],
13
+ icons: { emoji: "🔢", nerd: "󰓹", text: "tok" },
14
+ defaultStyle: { fg: "cyan", bg: "default", bold: false },
15
+ render({ ctx, options, renderWidget }) {
16
+ return renderWidget(
17
+ `↑${formatTokenCount(ctx.metrics.inputTokens, options.tokenFormatStyle)} ↓${formatTokenCount(ctx.metrics.outputTokens, options.tokenFormatStyle)}`,
18
+ );
19
+ },
20
+ });
@@ -0,0 +1,25 @@
1
+ import { defineWidget } from "../types.js";
2
+ import { formatTokenSpeed, tokenFormatStyleProperty } from "../utils/token-format.js";
3
+
4
+ export const TotalSpeedWidget = defineWidget({
5
+ type: "total-speed",
6
+ label: "Total Speed",
7
+ category: "Tokens",
8
+ description: "Average total tokens per minute",
9
+ dependencies: ["metrics"],
10
+ baseOptions: ["raw", "hideWhenZero", "icon"],
11
+ baseOptionDefaults: {},
12
+ properties: [tokenFormatStyleProperty()],
13
+ icons: { emoji: "⚡", nerd: "↕", text: "tok/min" },
14
+ defaultStyle: { fg: "brightGreen", bg: "default", bold: false },
15
+ render({ ctx, options, renderWidget }) {
16
+ return renderWidget(
17
+ formatTokenSpeed(
18
+ ctx.metrics.totalTokens,
19
+ ctx.metrics.firstTimestampMs,
20
+ ctx.metrics.lastTimestampMs,
21
+ options.tokenFormatStyle,
22
+ ),
23
+ );
24
+ },
25
+ });
@@ -0,0 +1,18 @@
1
+ import { defineWidget } from "../types.js";
2
+ import { formatTokenCount, tokenFormatStyleProperty } from "../utils/token-format.js";
3
+
4
+ export const TotalTokensWidget = defineWidget({
5
+ type: "total-tokens",
6
+ label: "Total Tokens",
7
+ category: "Tokens",
8
+ description: "Total token count",
9
+ dependencies: ["metrics"],
10
+ baseOptions: ["raw", "hideWhenZero", "icon"],
11
+ baseOptionDefaults: {},
12
+ properties: [tokenFormatStyleProperty()],
13
+ icons: { emoji: "🔢", nerd: "󰓹", text: "tok" },
14
+ defaultStyle: { fg: "cyan", bg: "default", bold: false },
15
+ render({ ctx, options, renderWidget }) {
16
+ return renderWidget(formatTokenCount(ctx.metrics.totalTokens, options.tokenFormatStyle));
17
+ },
18
+ });
@@ -0,0 +1,181 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { ColorLevel } from "../colors.js";
4
+ import type { GetExtensionStatuses } from "../extension-statuses.js";
5
+ import type {
6
+ IconMode,
7
+ StatuslineData,
8
+ WidgetEntry,
9
+ WidgetOptions,
10
+ WidgetStyle,
11
+ } from "../types.js";
12
+
13
+ export interface WidgetIconSet {
14
+ emoji: string;
15
+ nerd: string;
16
+ text: string;
17
+ }
18
+
19
+ export type PropertyKind = "boolean" | "number" | "choice" | "text";
20
+
21
+ export interface ChoiceOption<TId extends string = string> {
22
+ id: TId;
23
+ label: string;
24
+ list: string;
25
+ }
26
+
27
+ export type WidgetPropertyDefault = string | number | boolean;
28
+
29
+ // Edit-action discriminant for properties that open a dedicated editor flow. Extend as needed.
30
+ export type WidgetEditAction = "external-status-key";
31
+
32
+ export interface WidgetProperty {
33
+ id: string;
34
+ label: string;
35
+ kind: PropertyKind;
36
+ description: string;
37
+ default: WidgetPropertyDefault;
38
+ showWhen?: {
39
+ property: string;
40
+ equals: string | number | boolean;
41
+ };
42
+ options?: {
43
+ min?: number;
44
+ max?: number;
45
+ choices?: readonly ChoiceOption[];
46
+ label?: string;
47
+ showInFields?: boolean;
48
+ showInWidgets?: boolean;
49
+ showInColors?: boolean;
50
+ listProperty?: string;
51
+ quoteValue?: boolean;
52
+ editAction?: WidgetEditAction;
53
+ };
54
+ }
55
+
56
+ export type WidgetBaseOption = "raw" | "hideWhenEmpty" | "hideWhenZero" | "text" | "icon";
57
+
58
+ export interface WidgetRenderOptions extends WidgetStyle {
59
+ icons?: WidgetIconSet;
60
+ preservedTrimStyles?: string;
61
+ stripIncomingStyles?: boolean;
62
+ }
63
+
64
+ // The widget dependency set is the materialized snapshot (StatuslineData) plus the one live
65
+ // accessor that must never enter the snapshot — see F1 / pi live-accessor rule.
66
+ export type WidgetDependencyValues = StatuslineData & {
67
+ getExtensionStatuses: GetExtensionStatuses | undefined;
68
+ };
69
+
70
+ export type WidgetDependency = keyof WidgetDependencyValues;
71
+
72
+ export interface BaseWidgetContext {
73
+ iconMode: IconMode;
74
+ minimalist: boolean;
75
+ colorLevel: ColorLevel;
76
+ theme?: Theme;
77
+ requestRender?: () => void;
78
+ }
79
+
80
+ export type WidgetContext<TDeps extends readonly WidgetDependency[] = readonly []> =
81
+ BaseWidgetContext & Pick<WidgetDependencyValues, TDeps[number]>;
82
+
83
+ export type WidgetInstanceOptions<TOptions extends object> = WidgetOptions & TOptions;
84
+
85
+ export interface Widget<TOptions extends object = {}> {
86
+ readonly entry: WidgetEntry;
87
+ readonly id: string;
88
+ readonly type: WidgetEntry["type"];
89
+ enabled: boolean;
90
+ options: WidgetInstanceOptions<TOptions>;
91
+
92
+ render(ctx: WidgetContext): string | undefined;
93
+ toggle(enabled?: boolean): void;
94
+ update(options: Partial<WidgetInstanceOptions<TOptions>>): void;
95
+ toEntry(): WidgetEntry;
96
+ }
97
+
98
+ type BaseOptionsToObject<TBaseOptions extends readonly WidgetBaseOption[]> = Required<
99
+ Pick<WidgetOptions, TBaseOptions[number]>
100
+ >;
101
+
102
+ type WidgetChoiceValue<TProperty> = TProperty extends {
103
+ readonly kind: "choice";
104
+ readonly options: { readonly choices: readonly (infer TChoice)[] };
105
+ }
106
+ ? TChoice extends { readonly id: infer TId extends string }
107
+ ? TId
108
+ : string
109
+ : string;
110
+
111
+ type WidgetPropertyValue<TProperty extends Pick<WidgetProperty, "kind">> =
112
+ TProperty["kind"] extends "boolean"
113
+ ? boolean
114
+ : TProperty["kind"] extends "number"
115
+ ? number
116
+ : TProperty["kind"] extends "choice"
117
+ ? WidgetChoiceValue<TProperty>
118
+ : string;
119
+
120
+ type PropertiesToObject<TProperties extends readonly WidgetProperty[]> = {
121
+ [TProperty in TProperties[number] as TProperty["id"]]: WidgetPropertyValue<TProperty>;
122
+ };
123
+
124
+ export type OptionsFor<TSpec extends Pick<WidgetSpec, "baseOptions" | "properties">> =
125
+ WidgetOptions &
126
+ BaseOptionsToObject<TSpec["baseOptions"]> &
127
+ PropertiesToObject<TSpec["properties"]> &
128
+ WidgetStyle;
129
+
130
+ export type ContextFor<TSpec extends Pick<WidgetSpec, "dependencies">> = BaseWidgetContext &
131
+ Pick<WidgetDependencyValues, TSpec["dependencies"][number]>;
132
+
133
+ export interface TypedWidgetRenderArgs<
134
+ TSpec extends Pick<WidgetSpec, "dependencies" | "baseOptions" | "properties">,
135
+ > {
136
+ readonly ctx: ContextFor<TSpec>;
137
+ readonly options: OptionsFor<TSpec>;
138
+ readonly renderWidget: (
139
+ value: string | undefined,
140
+ renderOptions?: WidgetRenderOptions,
141
+ ) => string | undefined;
142
+ }
143
+
144
+ export interface WidgetSpec<
145
+ TType extends string = string,
146
+ TCategory extends string = string,
147
+ TDependencies extends readonly WidgetDependency[] = readonly WidgetDependency[],
148
+ TBaseOptions extends readonly WidgetBaseOption[] = readonly WidgetBaseOption[],
149
+ TProperties extends readonly WidgetProperty[] = readonly WidgetProperty[],
150
+ > {
151
+ readonly type: TType;
152
+ readonly label: string;
153
+ readonly category: TCategory;
154
+ readonly description: string;
155
+ readonly dependencies: TDependencies;
156
+ readonly baseOptions: TBaseOptions;
157
+ readonly baseOptionDefaults?: Partial<WidgetOptions>;
158
+ readonly properties: TProperties;
159
+ readonly icons: WidgetIconSet;
160
+ readonly defaultStyle: WidgetStyle;
161
+ readonly createOptionDefaults?: () => Partial<WidgetOptions>;
162
+ render(
163
+ args: TypedWidgetRenderArgs<{
164
+ readonly dependencies: TDependencies;
165
+ readonly baseOptions: TBaseOptions;
166
+ readonly properties: TProperties;
167
+ }>,
168
+ ): string | undefined;
169
+ }
170
+
171
+ export function defineWidget<
172
+ const TType extends string,
173
+ const TCategory extends string,
174
+ const TDependencies extends readonly WidgetDependency[],
175
+ const TBaseOptions extends readonly WidgetBaseOption[],
176
+ const TProperties extends readonly WidgetProperty[],
177
+ >(
178
+ spec: WidgetSpec<TType, TCategory, TDependencies, TBaseOptions, TProperties>,
179
+ ): WidgetSpec<TType, TCategory, TDependencies, TBaseOptions, TProperties> {
180
+ return spec;
181
+ }
@@ -0,0 +1,11 @@
1
+ import type { ColorName } from "../../colors.js";
2
+
3
+ export function colorPair(
4
+ fg: ColorName | undefined,
5
+ bg: ColorName | undefined,
6
+ ): { fg?: ColorName; bg?: ColorName } {
7
+ return {
8
+ ...(fg === undefined ? {} : { fg }),
9
+ ...(bg === undefined ? {} : { bg }),
10
+ };
11
+ }
@@ -0,0 +1,112 @@
1
+ import { normalizeColor, type ConditionalColorFields } from "../../colors.js";
2
+ import type { WidgetOptions } from "../../types.js";
3
+ import { colorPair } from "./colors.js";
4
+
5
+ export function contextPercent(tokens: number | undefined, maxTokens: number | undefined) {
6
+ if (tokens === undefined || maxTokens === undefined || maxTokens <= 0) return undefined;
7
+ return Math.min(100, Math.max(0, (tokens / maxTokens) * 100));
8
+ }
9
+
10
+ interface ContextColorOptions extends WidgetOptions, ConditionalColorFields {
11
+ contextConditionalColors: boolean;
12
+ contextWarningPercent: number;
13
+ contextDangerPercent: number;
14
+ }
15
+
16
+ export function contextColors(
17
+ options: ContextColorOptions,
18
+ contextTokens: number | undefined,
19
+ contextMaxTokens: number | undefined,
20
+ ) {
21
+ if (!options.contextConditionalColors) return colorPair(options.fg, options.bg);
22
+ const percent = contextPercent(contextTokens, contextMaxTokens);
23
+ if (percent === undefined) return colorPair(options.fg, options.bg);
24
+
25
+ if (percent >= options.contextDangerPercent) {
26
+ return colorPair(
27
+ normalizeColor(options.dangerFg) ?? options.fg,
28
+ normalizeColor(options.dangerBg) ?? options.bg,
29
+ );
30
+ }
31
+
32
+ if (percent >= options.contextWarningPercent) {
33
+ return colorPair(
34
+ normalizeColor(options.warningFg) ?? options.fg,
35
+ normalizeColor(options.warningBg) ?? options.bg,
36
+ );
37
+ }
38
+
39
+ return colorPair(options.fg, options.bg);
40
+ }
41
+
42
+ export function contextColorProperties() {
43
+ return [
44
+ {
45
+ id: "contextConditionalColors",
46
+ label: "Conditional colors",
47
+ kind: "boolean",
48
+ description: "Use warning/danger colors based on context usage",
49
+ default: false,
50
+ options: {
51
+ label: "with-colors",
52
+ showInWidgets: true,
53
+ showInColors: true,
54
+ listProperty: "",
55
+ },
56
+ },
57
+ {
58
+ id: "contextWarningPercent",
59
+ label: "Warning zone threshold",
60
+ kind: "number",
61
+ description: "Context usage percentage for warning colors",
62
+ default: 70,
63
+ showWhen: { property: "contextConditionalColors", equals: true },
64
+ options: { min: 0, max: 100, showInWidgets: false, showInColors: false },
65
+ },
66
+ {
67
+ id: "contextDangerPercent",
68
+ label: "Danger zone threshold",
69
+ kind: "number",
70
+ description: "Context usage percentage for danger colors",
71
+ default: 90,
72
+ showWhen: { property: "contextConditionalColors", equals: true },
73
+ options: { min: 0, max: 100, showInWidgets: false, showInColors: false },
74
+ },
75
+ {
76
+ id: "warningFg",
77
+ label: "Warning foreground",
78
+ kind: "text",
79
+ description: "Warning foreground",
80
+ default: "yellow",
81
+ showWhen: { property: "contextConditionalColors", equals: true },
82
+ options: { showInFields: false, showInWidgets: false, showInColors: false },
83
+ },
84
+ {
85
+ id: "warningBg",
86
+ label: "Warning background",
87
+ kind: "text",
88
+ description: "Warning background",
89
+ default: "default",
90
+ showWhen: { property: "contextConditionalColors", equals: true },
91
+ options: { showInFields: false, showInWidgets: false, showInColors: false },
92
+ },
93
+ {
94
+ id: "dangerFg",
95
+ label: "Danger foreground",
96
+ kind: "text",
97
+ description: "Danger foreground",
98
+ default: "red",
99
+ showWhen: { property: "contextConditionalColors", equals: true },
100
+ options: { showInFields: false, showInWidgets: false, showInColors: false },
101
+ },
102
+ {
103
+ id: "dangerBg",
104
+ label: "Danger background",
105
+ kind: "text",
106
+ description: "Danger background",
107
+ default: "default",
108
+ showWhen: { property: "contextConditionalColors", equals: true },
109
+ options: { showInFields: false, showInWidgets: false, showInColors: false },
110
+ },
111
+ ] as const;
112
+ }
@@ -0,0 +1,19 @@
1
+ export function formatTime(timestamp: number | undefined): string {
2
+ if (timestamp === undefined) return "";
3
+ return new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
4
+ }
5
+
6
+ function formatDuration(ms: number): string {
7
+ if (!Number.isFinite(ms) || ms <= 0) return "0m";
8
+ const totalMinutes = Math.max(1, Math.floor(ms / 60_000));
9
+ const hours = Math.floor(totalMinutes / 60);
10
+ const minutes = totalMinutes % 60;
11
+ if (hours === 0) return `${minutes}m`;
12
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
13
+ }
14
+
15
+ export function formatElapsed(first: number | undefined, last: number | undefined): string {
16
+ if (first === undefined) return "0m";
17
+ const end = last === undefined || last < first ? Date.now() : last;
18
+ return formatDuration(end - first);
19
+ }
@@ -0,0 +1,78 @@
1
+ export function formatCount(value: number) {
2
+ if (value < 1000) return `${value}`;
3
+ if (value < 1000000) return `${trimFixed(value / 1000, 1)}k`;
4
+ return `${trimFixed(value / 1000000, 1)}m`;
5
+ }
6
+
7
+ export function formatPiTokenCount(value: number) {
8
+ if (value < 1000) return `${value}`;
9
+ if (value < 10000) return `${(value / 1000).toFixed(1)}k`;
10
+ if (value < 1000000) return `${Math.round(value / 1000)}k`;
11
+ if (value < 10000000) return `${(value / 1000000).toFixed(1)}M`;
12
+ return `${Math.round(value / 1000000)}M`;
13
+ }
14
+
15
+ function trimFixed(value: number, digits: number) {
16
+ return value.toFixed(digits).replace(/\.0$/, "");
17
+ }
18
+ const TOKEN_FORMAT_STYLES = {
19
+ default: {
20
+ label: "Default",
21
+ list: "Default",
22
+ format: formatCount,
23
+ },
24
+ compact: {
25
+ label: "Compact",
26
+ list: "Compact",
27
+ format: formatPiTokenCount,
28
+ },
29
+ } as const;
30
+
31
+ export type TokenFormatStyle = keyof typeof TOKEN_FORMAT_STYLES;
32
+
33
+ const TOKEN_FORMAT_STYLE_VALUES = Object.keys(TOKEN_FORMAT_STYLES) as TokenFormatStyle[];
34
+
35
+ const TOKEN_FORMAT_CHOICES = TOKEN_FORMAT_STYLE_VALUES.map((style) => ({
36
+ id: style,
37
+ label: TOKEN_FORMAT_STYLES[style].label,
38
+ list: TOKEN_FORMAT_STYLES[style].list,
39
+ }));
40
+
41
+ export function formatTokenCount(value: number, style: TokenFormatStyle) {
42
+ return TOKEN_FORMAT_STYLES[style].format(value);
43
+ }
44
+
45
+ export function formatTokenSpeed(
46
+ tokens: number,
47
+ first: number | undefined,
48
+ last: number | undefined,
49
+ tokenFormatStyle: TokenFormatStyle,
50
+ ) {
51
+ if (
52
+ first === undefined ||
53
+ last === undefined ||
54
+ !Number.isFinite(first) ||
55
+ !Number.isFinite(last) ||
56
+ last <= first
57
+ ) {
58
+ return "0/min";
59
+ }
60
+ const minutes = Math.max((last - first) / 60000, 1 / 60);
61
+ return `${formatTokenCount(Math.round(tokens / minutes), tokenFormatStyle)}/min`;
62
+ }
63
+
64
+ export function tokenFormatStyleProperty() {
65
+ return {
66
+ id: "tokenFormatStyle",
67
+ label: "Token format",
68
+ kind: "choice",
69
+ description: "Token display format",
70
+ default: "default",
71
+ options: {
72
+ choices: TOKEN_FORMAT_CHOICES,
73
+ showInWidgets: true,
74
+ showInColors: false,
75
+ listProperty: "format",
76
+ },
77
+ } as const;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-extensions",
3
- "version": "2026.8.3",
3
+ "version": "2026.8.4",
4
4
  "description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
5
5
  "keywords": [
6
6
  "extensions",
@@ -20,6 +20,7 @@
20
20
  "directory": "packages/pi-extensions"
21
21
  },
22
22
  "files": [
23
+ "examples/pi-footer.json",
23
24
  "README.md",
24
25
  "LICENSE"
25
26
  ],
@@ -68,6 +69,7 @@
68
69
  "@sinclair/typebox": "^0.34.49",
69
70
  "ajv": "^8.17.1",
70
71
  "ajv-formats": "^3.0.1",
72
+ "chalk": "^6.0.0",
71
73
  "croner": "^10.0.1",
72
74
  "cross-spawn": "^7.0.6",
73
75
  "effect": "4.0.0-beta.103",
@@ -79,6 +81,7 @@
79
81
  "nanoid": "^5.0.0",
80
82
  "open": "^10.2.0",
81
83
  "p-limit": "^6.1.0",
84
+ "pi-footer": "0.5.1",
82
85
  "pi-lens": "3.8.74",
83
86
  "pi-mcp-adapter": "2.23.0",
84
87
  "pi-web-access": "0.22.0",
@@ -117,6 +120,7 @@
117
120
  "@juicesharp/rpiv-ask-user-question",
118
121
  "@narumitw/pi-btw",
119
122
  "@pi-plugins/fast-mode",
123
+ "pi-footer",
120
124
  "pi-lens",
121
125
  "pi-mcp-adapter",
122
126
  "pi-web-access",
@@ -147,7 +151,8 @@
147
151
  "./node_modules/pi-lens/dist/index.js",
148
152
  "./node_modules/pi-web-access/index.ts",
149
153
  "./node_modules/remote-pi/dist/index.js",
150
- "./node_modules/@czottmann/pi-automode/extensions/auto-mode.ts"
154
+ "./node_modules/@czottmann/pi-automode/extensions/auto-mode.ts",
155
+ "./node_modules/pi-footer/src/index.ts"
151
156
  ],
152
157
  "themes": [
153
158
  "./node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json",