@badliveware/pi-footer-framework 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/index.ts +34 -50
  3. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Added cache read/write token and cost fields to the footer `stats` source so custom renderers can show cache activity alongside input, output, totals, and cost.
6
+ - Improved footer render performance by caching session stats and text-width metadata for repeated renders of unchanged content.
7
+ - Reduced startup and render work by deferring slower extension probes and rendering styled footer text in contiguous cell runs instead of per-cell wrappers.
8
+
9
+ ## 0.2.2
10
+
11
+ - Fixed custom footer rendering so layouts with all visible items on line 1 render as a single footer line instead of reserving an empty second line.
12
+
3
13
  ## 0.2.1
4
14
 
5
15
  - Fixed footer rendering with cell-buffer composition so ANSI styling, OSC8 hyperlinks, grapheme clusters, wide characters, and overlays preserve terminal cell alignment.
package/index.ts CHANGED
@@ -2,9 +2,12 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { Type } from "@mariozechner/pi-ai";
6
- import { defineTool, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
5
+ import { Type } from "@earendil-works/pi-ai";
6
+ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { renderFooterCellRuns } from "./src/cell-runs.ts";
9
+ import { createFooterStatsCache, type FooterStats } from "./src/stats-cache.ts";
10
+ import { createFooterTextMetricsCache } from "./src/text-metrics-cache.ts";
8
11
 
9
12
  type ChecksState = "pass" | "fail" | "running" | "unknown";
10
13
  export type FooterAnchorMode = "gap" | "left" | "center" | "right" | "spread";
@@ -58,6 +61,8 @@ export interface FooterCell {
58
61
  itemId?: string;
59
62
  continuation?: boolean;
60
63
  filler?: boolean;
64
+ prefix?: string;
65
+ suffix?: string;
61
66
  }
62
67
 
63
68
  export interface FooterColumnItem {
@@ -148,7 +153,7 @@ export function footerCellsFromText(text: string, itemId?: string): FooterCell[]
148
153
  pendingZeroWidthPlain += cluster;
149
154
  }
150
155
  } else {
151
- cells.push({ raw: `${pendingZeroWidthRaw}${activeCellPrefix(state)}${cluster}${activeCellSuffix(state)}`, plainText: `${pendingZeroWidthPlain}${cluster}`, itemId });
156
+ cells.push({ raw: `${pendingZeroWidthRaw}${cluster}`, plainText: `${pendingZeroWidthPlain}${cluster}`, itemId, prefix: activeCellPrefix(state), suffix: activeCellSuffix(state) });
152
157
  pendingZeroWidthRaw = "";
153
158
  pendingZeroWidthPlain = "";
154
159
  for (let i = 1; i < clusterWidth; i += 1) cells.push({ raw: "", plainText: "", itemId, continuation: true });
@@ -219,15 +224,7 @@ export function writeFooterText(cells: FooterCell[], start: number, text: string
219
224
  }
220
225
  }
221
226
 
222
- export function renderFooterCells(cells: FooterCell[]): string {
223
- let end = cells.length;
224
- while (end > 0) {
225
- const cell = cells[end - 1];
226
- if (cell.continuation || !cell.filler) break;
227
- end -= 1;
228
- }
229
- return cells.slice(0, end).map((cell) => (cell.continuation ? "" : cell.raw)).join("");
230
- }
227
+ export const renderFooterCells = renderFooterCellRuns;
231
228
 
232
229
  export function resolveFooterColumn(column: FooterColumn | undefined, width: number, itemWidth: number): number | undefined {
233
230
  if (column === undefined) return undefined;
@@ -1823,11 +1820,11 @@ export default function footerFramework(pi: ExtensionAPI): void {
1823
1820
  return sorted.sort((a, b) => a.placement.order - b.placement.order || a.id.localeCompare(b.id));
1824
1821
  }
1825
1822
 
1826
- function renderVisibilityDiagnostics(items: FooterItem[], lines: Array<{ line: FooterLine; plainText: string }>): FooterRenderDiagnostic[] {
1823
+ function renderVisibilityDiagnostics(items: FooterItem[], lines: Array<{ line: FooterLine; plainText: string }>, plainTextFor: (text: string) => string = plainFooterText): FooterRenderDiagnostic[] {
1827
1824
  const lineTextByNumber = new Map(lines.map((line) => [line.line, line.plainText]));
1828
1825
  const diagnostics: FooterRenderDiagnostic[] = [];
1829
1826
  for (const item of items) {
1830
- const itemPlainText = plainFooterText(item.text).trim();
1827
+ const itemPlainText = plainTextFor(item.text).trim();
1831
1828
  if (!itemPlainText) continue;
1832
1829
  const linePlainText = lineTextByNumber.get(item.placement.line);
1833
1830
  if (linePlainText === undefined) {
@@ -1898,7 +1895,7 @@ export default function footerFramework(pi: ExtensionAPI): void {
1898
1895
 
1899
1896
  function buildPiSources(
1900
1897
  footerData: { getGitBranch(): string | null; getExtensionStatuses(): ReadonlyMap<string, string> },
1901
- stats: { input: number; output: number; cost: number; inputText: string; outputText: string; costText: string; value: string },
1898
+ stats: FooterStats,
1902
1899
  ): Record<string, FooterAdapterSourceValue> {
1903
1900
  const sources: Record<string, FooterAdapterSourceValue> = {
1904
1901
  cwd: { label: "cwd", value: currentCtx?.cwd ?? "", tone: "muted", data: { path: currentCtx?.cwd ?? "" } },
@@ -2004,7 +2001,7 @@ export default function footerFramework(pi: ExtensionAPI): void {
2004
2001
  function collectItems(
2005
2002
  theme: ExtensionContext["ui"]["theme"],
2006
2003
  footerData: { getGitBranch(): string | null; getExtensionStatuses(): ReadonlyMap<string, string> },
2007
- stats: { input: number; output: number; cost: number; inputText: string; outputText: string; costText: string; value: string },
2004
+ stats: FooterStats,
2008
2005
  diagnostics: FooterTemplateDiagnostic[],
2009
2006
  ): FooterItem[] {
2010
2007
  const items: FooterItem[] = [];
@@ -2141,6 +2138,8 @@ export default function footerFramework(pi: ExtensionAPI): void {
2141
2138
  ctx.ui.setFooter((tui, theme, footerData) => {
2142
2139
  requestRender = () => tui.requestRender();
2143
2140
  const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
2141
+ const footerStats = createFooterStatsCache(formatTokens);
2142
+ const textMetrics = createFooterTextMetricsCache(plainFooterText, visibleWidth);
2144
2143
 
2145
2144
  return {
2146
2145
  dispose() {
@@ -2149,47 +2148,32 @@ export default function footerFramework(pi: ExtensionAPI): void {
2149
2148
  },
2150
2149
  invalidate() {},
2151
2150
  render(width: number): string[] {
2152
- let input = 0;
2153
- let output = 0;
2154
- let cost = 0;
2155
- for (const entry of ctx.sessionManager.getEntries()) {
2156
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
2157
- input += entry.message.usage.input;
2158
- output += entry.message.usage.output;
2159
- cost += entry.message.usage.cost.total;
2160
- }
2161
-
2162
- const stats = {
2163
- input,
2164
- output,
2165
- cost,
2166
- inputText: formatTokens(input),
2167
- outputText: formatTokens(output),
2168
- costText: cost.toFixed(3),
2169
- value: `↑${formatTokens(input)} ↓${formatTokens(output)} $${cost.toFixed(3)}`,
2170
- };
2151
+ const stats = footerStats(ctx.sessionManager.getEntries());
2171
2152
  const diagnostics: FooterTemplateDiagnostic[] = [];
2172
2153
  const items = collectItems(theme, footerData, stats, diagnostics);
2173
2154
  lastTemplateDiagnostics = diagnostics;
2174
- const maxLine = Math.max(2, ...items.map((item) => item.placement.line));
2155
+ const maxLine = Math.max(1, ...items.map((item) => item.placement.line));
2175
2156
  const lineResults = Array.from({ length: maxLine }, (_, index) => {
2176
2157
  const line = index + 1;
2177
2158
  const result = renderFooterLine(theme, width, items, line, getLineAnchor(settings, line));
2178
- return { line, text: result.line, plainText: plainFooterText(result.line), layout: result.layout };
2159
+ return { line, text: result.line, plainText: textMetrics(result.line).plainText, layout: result.layout };
2179
2160
  });
2180
2161
  const line1Result = lineResults[0];
2181
2162
  const line2Result = lineResults[1];
2182
- const renderedItems = items.map((item) => ({
2183
- id: item.id,
2184
- line: item.placement.line,
2185
- zone: item.placement.zone,
2186
- order: item.placement.order,
2187
- column: item.placement.column,
2188
- width: visibleWidth(item.text),
2189
- plainText: plainFooterText(item.text),
2190
- renderSource: item.renderSource,
2191
- tokens: item.tokens,
2192
- }));
2163
+ const renderedItems = items.map((item) => {
2164
+ const metrics = textMetrics(item.text);
2165
+ return {
2166
+ id: item.id,
2167
+ line: item.placement.line,
2168
+ zone: item.placement.zone,
2169
+ order: item.placement.order,
2170
+ column: item.placement.column,
2171
+ width: metrics.width,
2172
+ plainText: metrics.plainText,
2173
+ renderSource: item.renderSource,
2174
+ tokens: item.tokens,
2175
+ };
2176
+ });
2193
2177
 
2194
2178
  lastFooterSnapshot = {
2195
2179
  width,
@@ -2202,7 +2186,7 @@ export default function footerFramework(pi: ExtensionAPI): void {
2202
2186
  line2Layout: line2Result?.layout ?? renderFooterLine(theme, width, [], 2, getLineAnchor(settings, 2)).layout,
2203
2187
  gitBranch: footerData.getGitBranch(),
2204
2188
  renderedItems,
2205
- renderDiagnostics: renderVisibilityDiagnostics(items, lineResults),
2189
+ renderDiagnostics: renderVisibilityDiagnostics(items, lineResults, (text) => textMetrics(text).plainText),
2206
2190
  extensionStatuses: Array.from(footerData.getExtensionStatuses().entries()).map(([key, value]) => ({ key, value })),
2207
2191
  model: ctx.model?.id ?? "no-model",
2208
2192
  contextUsage: ctx.getContextUsage() ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@badliveware/pi-footer-framework",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Configurable footer framework extension for Pi.",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -43,9 +43,9 @@
43
43
  ]
44
44
  },
45
45
  "peerDependencies": {
46
- "@mariozechner/pi-ai": "*",
47
- "@mariozechner/pi-coding-agent": "*",
48
- "@mariozechner/pi-tui": "*"
46
+ "@earendil-works/pi-ai": "*",
47
+ "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-tui": "*"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=20"