@pi-unipi/unipi 0.1.15 → 0.1.16

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 (80) hide show
  1. package/README.md +10 -0
  2. package/package.json +9 -4
  3. package/packages/autocomplete/src/constants.ts +20 -0
  4. package/packages/autocomplete/src/settings.ts +2 -2
  5. package/packages/compactor/src/commands/index.ts +2 -2
  6. package/packages/compactor/src/compaction/hooks.ts +3 -5
  7. package/packages/compactor/src/config/schema.ts +1 -1
  8. package/packages/compactor/src/display/diff-presentation.ts +6 -1
  9. package/packages/compactor/src/display/diff-renderer.ts +34 -8
  10. package/packages/compactor/src/display/diff-width-safety.ts +83 -0
  11. package/packages/compactor/src/display/line-width-safety.ts +14 -2
  12. package/packages/compactor/src/index.ts +131 -23
  13. package/packages/compactor/src/info-screen.ts +136 -51
  14. package/packages/compactor/src/session/analytics.ts +198 -0
  15. package/packages/compactor/src/session/db.ts +3 -0
  16. package/packages/compactor/src/tools/register.ts +2 -2
  17. package/packages/compactor/src/types.ts +5 -6
  18. package/packages/footer/src/commands.ts +3 -0
  19. package/packages/footer/src/config.ts +6 -6
  20. package/packages/footer/src/events.ts +34 -34
  21. package/packages/footer/src/index.ts +21 -9
  22. package/packages/footer/src/presets.ts +6 -6
  23. package/packages/footer/src/registry/index.ts +5 -7
  24. package/packages/footer/src/rendering/icons.ts +88 -88
  25. package/packages/footer/src/rendering/renderer.ts +4 -4
  26. package/packages/footer/src/segments/core.ts +14 -55
  27. package/packages/footer/src/segments/memory.ts +9 -7
  28. package/packages/footer/src/segments/ralph.ts +9 -10
  29. package/packages/footer/src/segments/status-ext.ts +17 -12
  30. package/packages/footer/src/segments/workflow.ts +5 -4
  31. package/packages/footer/src/tui/settings-tui.ts +216 -155
  32. package/packages/input-shortcuts/README.md +116 -0
  33. package/packages/input-shortcuts/index.ts +5 -0
  34. package/packages/input-shortcuts/src/chord-overlay.ts +235 -0
  35. package/packages/input-shortcuts/src/clipboard.ts +119 -0
  36. package/packages/input-shortcuts/src/index.ts +411 -0
  37. package/packages/input-shortcuts/src/registers.ts +92 -0
  38. package/packages/input-shortcuts/src/settings-overlay.ts +142 -0
  39. package/packages/input-shortcuts/src/status.ts +35 -0
  40. package/packages/input-shortcuts/src/types.ts +48 -0
  41. package/packages/input-shortcuts/src/undo-redo.ts +86 -0
  42. package/packages/mcp/src/index.ts +2 -5
  43. package/packages/notify/index.ts +2 -1
  44. package/packages/notify/skills/configure-notify/SKILL.md +43 -6
  45. package/packages/unipi/index.ts +4 -0
  46. package/packages/updater/README.md +71 -0
  47. package/packages/updater/index.ts +6 -0
  48. package/packages/updater/skills/configure-updater/SKILL.md +65 -0
  49. package/packages/updater/src/cache.ts +67 -0
  50. package/packages/updater/src/changelog.ts +141 -0
  51. package/packages/updater/src/checker.ts +84 -0
  52. package/packages/updater/src/commands.ts +83 -0
  53. package/packages/updater/src/index.ts +178 -0
  54. package/packages/updater/src/installer.ts +74 -0
  55. package/packages/updater/src/markdown.ts +173 -0
  56. package/packages/updater/src/readme.ts +139 -0
  57. package/packages/updater/src/settings.ts +98 -0
  58. package/packages/updater/src/tui/changelog-overlay.ts +256 -0
  59. package/packages/updater/src/tui/readme-overlay.ts +236 -0
  60. package/packages/updater/src/tui/settings-overlay.ts +191 -0
  61. package/packages/updater/src/tui/update-overlay.ts +261 -0
  62. package/packages/utility/src/diff/highlighter.ts +1 -2
  63. package/packages/utility/src/diff/renderer.ts +25 -52
  64. package/packages/utility/src/diff/wrapper.ts +60 -8
  65. package/packages/web-api/README.md +76 -15
  66. package/packages/web-api/skills/web/SKILL.md +54 -11
  67. package/packages/web-api/src/engine/constants.ts +36 -0
  68. package/packages/web-api/src/engine/dependencies.ts +145 -0
  69. package/packages/web-api/src/engine/dom.ts +266 -0
  70. package/packages/web-api/src/engine/extract.ts +642 -0
  71. package/packages/web-api/src/engine/format.ts +306 -0
  72. package/packages/web-api/src/engine/profiles.ts +102 -0
  73. package/packages/web-api/src/engine/types.ts +169 -0
  74. package/packages/web-api/src/index.ts +9 -2
  75. package/packages/web-api/src/providers/base.ts +9 -1
  76. package/packages/web-api/src/settings.ts +70 -4
  77. package/packages/web-api/src/tools.ts +281 -24
  78. package/packages/web-api/src/tui/progress.ts +168 -0
  79. package/packages/web-api/src/tui/result.ts +173 -0
  80. package/packages/web-api/src/tui/settings-dialog.ts +168 -0
package/README.md CHANGED
@@ -27,6 +27,7 @@ pi install npm:@pi-unipi/ask-user
27
27
  pi install npm:@pi-unipi/milestone
28
28
  pi install npm:@pi-unipi/kanboard
29
29
  pi install npm:@pi-unipi/footer
30
+ pi install npm:@pi-unipi/updater
30
31
  ```
31
32
 
32
33
  ## Packages
@@ -49,6 +50,7 @@ pi install npm:@pi-unipi/footer
49
50
  | `@pi-unipi/milestone` | Milestone tracking and project progress management |
50
51
  | `@pi-unipi/kanboard` | Kanboard visualization server with TUI overlay |
51
52
  | `@pi-unipi/footer` | Persistent status bar with live stats from all packages |
53
+ | `@pi-unipi/updater` | Auto-updater, changelog browser, and readme browser |
52
54
 
53
55
  ## Commands
54
56
 
@@ -227,6 +229,14 @@ pi install npm:@pi-unipi/footer
227
229
  | `/unipi:footer` | Toggle footer or switch preset |
228
230
  | `/unipi:footer-settings` | Open footer settings — toggle groups and segments |
229
231
 
232
+ ### Updater (`/unipi:updater*`)
233
+
234
+ | Command | Description |
235
+ |---------|-------------|
236
+ | `/unipi:readme [package]` | Browse package README files in TUI overlay |
237
+ | `/unipi:changelog` | Browse CHANGELOG.md with version list and detail view |
238
+ | `/unipi:updater-settings` | Configure check interval and auto-update mode |
239
+
230
240
  ### Name Badge
231
241
 
232
242
  ## How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,7 +51,9 @@
51
51
  "node_modules/@pi-unipi/milestone/index.ts",
52
52
  "node_modules/@pi-unipi/kanboard/index.ts",
53
53
  "node_modules/@pi-unipi/command-enchantment/src/index.ts",
54
- "node_modules/@pi-unipi/footer/index.ts"
54
+ "node_modules/@pi-unipi/footer/src/index.ts",
55
+ "node_modules/@pi-unipi/updater/src/index.ts",
56
+ "node_modules/@pi-unipi/input-shortcuts/src/index.ts"
55
57
  ],
56
58
  "skills": [
57
59
  "node_modules/@pi-unipi/workflow/skills",
@@ -64,7 +66,8 @@
64
66
  "node_modules/@pi-unipi/compactor/skills",
65
67
  "node_modules/@pi-unipi/notify/skills",
66
68
  "node_modules/@pi-unipi/milestone/skills",
67
- "node_modules/@pi-unipi/kanboard/skills"
69
+ "node_modules/@pi-unipi/kanboard/skills",
70
+ "node_modules/@pi-unipi/updater/skills"
68
71
  ]
69
72
  },
70
73
  "peerDependencies": {
@@ -90,7 +93,9 @@
90
93
  "@pi-unipi/kanboard": "*",
91
94
  "@pi-unipi/web-api": "*",
92
95
  "@pi-unipi/workflow": "*",
93
- "@pi-unipi/footer": "*"
96
+ "@pi-unipi/footer": "*",
97
+ "@pi-unipi/updater": "*",
98
+ "@pi-unipi/input-shortcuts": "*"
94
99
  },
95
100
  "devDependencies": {
96
101
  "@types/node": "^25.6.0",
@@ -30,6 +30,8 @@ export const PACKAGE_ORDER: string[] = [
30
30
  "notify",
31
31
  "kanboard",
32
32
  "footer",
33
+ "updater",
34
+ "input-shortcuts",
33
35
  ];
34
36
 
35
37
  // ─── Package Colors ──────────────────────────────────────────────────
@@ -48,6 +50,8 @@ export const PACKAGE_COLORS: Record<string, string> = {
48
50
  notify: `${ESC}[96m`, // Bright Cyan
49
51
  kanboard: `${ESC}[92m`, // Bright Green
50
52
  footer: `${ESC}[34m`, // Blue
53
+ updater: `${ESC}[93m`, // Bright Yellow
54
+ "input-shortcuts": `${ESC}[95m`, // Bright Magenta
51
55
  };
52
56
 
53
57
  // ─── Command Registry ────────────────────────────────────────────────
@@ -149,6 +153,14 @@ export const COMMAND_REGISTRY: Record<string, string> = {
149
153
  // footer (2 commands)
150
154
  "unipi:footer": "footer",
151
155
  "unipi:footer-settings": "footer",
156
+
157
+ // updater (3 commands)
158
+ "unipi:readme": "updater",
159
+ "unipi:changelog": "updater",
160
+ "unipi:updater-settings": "updater",
161
+
162
+ // input-shortcuts (1 command)
163
+ "unipi:stash-settings": "input-shortcuts",
152
164
  };
153
165
 
154
166
  // ─── Description Map ─────────────────────────────────────────────────
@@ -236,6 +248,12 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
236
248
 
237
249
  "unipi:footer": "Toggle footer or switch preset",
238
250
  "unipi:footer-settings": "Open footer settings — toggle groups and segments",
251
+
252
+ "unipi:readme": "Browse package README files",
253
+ "unipi:changelog": "Browse changelog (Keep a Changelog format)",
254
+ "unipi:updater-settings": "Configure updater — check interval and auto-update",
255
+
256
+ "unipi:stash-settings": "Open input shortcuts settings — customize keybindings",
239
257
  };
240
258
 
241
259
  // ─── Package Display Names ───────────────────────────────────────────
@@ -254,4 +272,6 @@ export const PACKAGE_LABELS: Record<string, string> = {
254
272
  notify: "notify",
255
273
  kanboard: "kanboard",
256
274
  footer: "footer",
275
+ updater: "updater",
276
+ "input-shortcuts": "input-shortcuts",
257
277
  };
@@ -52,8 +52,8 @@ export function loadConfig(): CommandEnchantmentConfig {
52
52
  ...config,
53
53
  };
54
54
  }
55
- } catch (error) {
56
- console.error("[command-enchantment] Failed to load config:", error);
55
+ } catch {
56
+ // Silently ignore config load failure falls back to defaults.
57
57
  }
58
58
  return DEFAULT_CONFIG;
59
59
  }
@@ -34,8 +34,8 @@ export interface CommandDeps {
34
34
  getCounters?: () => RuntimeCounters;
35
35
  }
36
36
 
37
- function deprecationLog(oldName: string, newName: string): void {
38
- console.error(`[compactor] DEPRECATED: Command "${oldName}" used use "${newName}" instead.`);
37
+ function deprecationLog(_oldName: string, _newName: string): void {
38
+ // Deprecation logging disabledwas writing to stdout causing TUI rendering issues.
39
39
  }
40
40
 
41
41
  export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
@@ -21,11 +21,9 @@ const formatTokens = (n: number): string => {
21
21
  return String(n);
22
22
  };
23
23
 
24
- const dbg = (debug: boolean, event: string, data?: Record<string, unknown>) => {
25
- if (!debug) return;
26
- const ts = new Date().toISOString().slice(11, 23);
27
- const details = data ? " " + JSON.stringify(data) : "";
28
- console.error(`[compactor:${ts}] ${event}${details}`);
24
+ const dbg = (_debug: boolean, _event: string, _data?: Record<string, unknown>) => {
25
+ // Debug logging disabled — was writing to stdout causing TUI rendering issues.
26
+ return;
29
27
  };
30
28
 
31
29
  const previewContent = (content: unknown): string => {
@@ -58,7 +58,7 @@ export const DEFAULT_COMPACTOR_CONFIG: CompactorConfig = {
58
58
  mmapPragma: false,
59
59
  customNoisePatterns: [],
60
60
  },
61
- overrideDefaultCompaction: false,
61
+ overrideDefaultCompaction: true,
62
62
  debug: false,
63
63
  showTruncationHints: true,
64
64
  };
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { DiffLayout } from "../types.js";
6
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
6
7
 
7
8
  export function selectDiffLayout(
8
9
  terminalWidth: number,
@@ -12,9 +13,13 @@ export function selectDiffLayout(
12
13
  return terminalWidth >= 100 ? "split" : "unified";
13
14
  }
14
15
 
16
+ /** Clamp text to maxWidth visible columns, ANSI-aware */
15
17
  export function clampWidth(text: string, maxWidth: number): string {
16
18
  return text
17
19
  .split("\n")
18
- .map((line) => (line.length > maxWidth ? line.slice(0, maxWidth - 3) + "..." : line))
20
+ .map((line) => {
21
+ if (visibleWidth(line) <= maxWidth) return line;
22
+ return truncateToWidth(line, maxWidth, "…");
23
+ })
19
24
  .join("\n");
20
25
  }
@@ -3,6 +3,8 @@
3
3
  * syntax highlighting, and Nerd Font detection
4
4
  */
5
5
 
6
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
7
+
6
8
  export type DiffLayout = "auto" | "split" | "unified";
7
9
  export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
8
10
 
@@ -150,16 +152,28 @@ function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
150
152
  return type === "add" ? "+ " : type === "remove" ? "- " : " ";
151
153
  }
152
154
 
153
- function renderUnified(diff: DiffLine[], indicator: DiffIndicator): string {
155
+ function renderUnified(
156
+ diff: DiffLine[],
157
+ indicator: DiffIndicator,
158
+ maxWidth?: number,
159
+ ): string {
154
160
  return diff.map((line) => {
155
161
  const prefix = indicator === "bars"
156
162
  ? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
157
163
  : indicatorChar(line.type, indicator);
158
- return prefix + line.text;
164
+ const rendered = prefix + line.text;
165
+ if (maxWidth && visibleWidth(rendered) > maxWidth) {
166
+ return truncateToWidth(rendered, maxWidth, "…");
167
+ }
168
+ return rendered;
159
169
  }).join("\n");
160
170
  }
161
171
 
162
- function renderSplit(diff: DiffLine[], indicator: DiffIndicator): string {
172
+ function renderSplit(
173
+ diff: DiffLine[],
174
+ indicator: DiffIndicator,
175
+ maxW?: number,
176
+ ): string {
163
177
  const left: string[] = [];
164
178
  const right: string[] = [];
165
179
 
@@ -176,12 +190,24 @@ function renderSplit(diff: DiffLine[], indicator: DiffIndicator): string {
176
190
  }
177
191
  }
178
192
 
179
- const maxWidth = Math.max(...left.map((l) => l.length), 40);
193
+ const halfW = maxW ? Math.floor(maxW / 2) - 2 : 40;
194
+ const colW = Math.max(
195
+ ...left.map((l) => visibleWidth(l)),
196
+ ...right.map((l) => visibleWidth(l)),
197
+ Math.min(halfW, 40),
198
+ );
180
199
  const result: string[] = [];
181
200
  for (let i = 0; i < left.length; i++) {
182
- const l = left[i].padEnd(maxWidth);
201
+ const lTrunc = visibleWidth(left[i]) > colW
202
+ ? truncateToWidth(left[i], colW, "…")
203
+ : left[i].padEnd(colW);
183
204
  const sep = left[i] && right[i] ? " │ " : " ";
184
- result.push(l + sep + right[i]);
205
+ let rLine = right[i];
206
+ if (maxW && visibleWidth(lTrunc + sep + rLine) > maxW) {
207
+ const rBudget = maxW - visibleWidth(lTrunc + sep);
208
+ rLine = truncateToWidth(rLine, Math.max(1, rBudget), "…");
209
+ }
210
+ result.push(lTrunc + sep + rLine);
185
211
  }
186
212
 
187
213
  return result.join("\n");
@@ -229,10 +255,10 @@ export function renderDiff(
229
255
  }
230
256
 
231
257
  if (effectiveLayout === "split") {
232
- return renderSplit(highlightedDiff, effectiveIndicator);
258
+ return renderSplit(highlightedDiff, effectiveIndicator, maxWidth);
233
259
  }
234
260
 
235
- return renderUnified(highlightedDiff, effectiveIndicator);
261
+ return renderUnified(highlightedDiff, effectiveIndicator, maxWidth);
236
262
  }
237
263
 
238
264
  export function renderEditDiffResult(
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Diff width safety — truncate diff lines to terminal width
3
+ *
4
+ * Pi's renderDiff() in diff.js produces lines without width
5
+ * truncation. When a diff line's visible content exceeds the
6
+ * terminal width, the TUI crashes with:
7
+ * "Rendered line N exceeds terminal width (X > Y)"
8
+ *
9
+ * This module provides clampDiffToWidth() which truncates
10
+ * each diff line to a safe width, preventing TUI crashes.
11
+ *
12
+ * The diff format from pi's generateDiffString() is:
13
+ * [+/-/ ]LINE_NUM CONTENT
14
+ * e.g.: "+ 38 │ The root cause is a **compound failure**..."
15
+ *
16
+ * We detect the terminal width from process.stdout and clamp
17
+ * each line, accounting for the rendering overhead of the
18
+ * edit tool's Box nesting (approx 4-6 chars of padding).
19
+ */
20
+
21
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
22
+
23
+ /** Rendering overhead from Box nesting in edit tool components */
24
+ const RENDER_OVERHEAD = 6;
25
+
26
+ /** Minimum useful line width (don't clamp below this) */
27
+ const MIN_LINE_WIDTH = 20;
28
+
29
+ /**
30
+ * Get the terminal width with fallback.
31
+ * Uses process.stdout.columns (updated on resize).
32
+ */
33
+ function getTerminalWidth(): number {
34
+ return process.stdout?.columns ?? 80;
35
+ }
36
+
37
+ /**
38
+ * Clamp a diff string so every line fits within terminal width.
39
+ * Preserves the diff prefix (+/-/ ) and line number while
40
+ * truncating the content portion.
41
+ *
42
+ * @param diff - The diff string from generateDiffString()
43
+ * @param maxWidth - Override for terminal width (for testing)
44
+ * @returns The clamped diff string (may be same reference if no clamping needed)
45
+ */
46
+ export function clampDiffToWidth(
47
+ diff: string,
48
+ maxWidth?: number,
49
+ ): string {
50
+ const termW = maxWidth ?? getTerminalWidth();
51
+ const safeW = Math.max(MIN_LINE_WIDTH, termW - RENDER_OVERHEAD);
52
+
53
+ const lines = diff.split("\n");
54
+ let changed = false;
55
+
56
+ const result = lines.map((line) => {
57
+ const vw = visibleWidth(line);
58
+ if (vw <= safeW) return line;
59
+
60
+ changed = true;
61
+
62
+ // Try to preserve the diff prefix (+/-/ ) and line number
63
+ // Format: [+/-/ ]LINE_NUM CONTENT
64
+ // The prefix and line number are critical for readability
65
+ const prefixMatch = line.match(/^([+\- ])\s*(\d*)\s/);
66
+ if (prefixMatch) {
67
+ const prefixLen = prefixMatch[0].length;
68
+ // Calculate how much content we can keep
69
+ const contentBudget = safeW - prefixLen;
70
+ if (contentBudget >= MIN_LINE_WIDTH) {
71
+ const prefix = line.slice(0, prefixLen);
72
+ const content = line.slice(prefixLen);
73
+ const truncated = truncateToWidth(content, contentBudget, "…");
74
+ return prefix + truncated;
75
+ }
76
+ }
77
+
78
+ // Fallback: truncate the entire line
79
+ return truncateToWidth(line, safeW, "…");
80
+ });
81
+
82
+ return changed ? result.join("\n") : diff;
83
+ }
@@ -1,11 +1,23 @@
1
1
  /**
2
2
  * Line width safety — width clamping with collapsed hints
3
+ *
4
+ * Uses ANSI-aware visibleWidth measurement from pi-tui to properly
5
+ * handle lines containing escape codes. Falls back to raw-length
6
+ * measurement when pi-tui is unavailable.
3
7
  */
4
8
 
9
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
10
+
11
+ /**
12
+ * Clamp each line to maxWidth visible columns.
13
+ * Uses pi-tui's visibleWidth() for ANSI-aware measurement and
14
+ * truncateToWidth() for ANSI-safe truncation.
15
+ */
5
16
  export function clampLineWidth(lines: string[], maxWidth: number): string[] {
6
17
  return lines.map((line) => {
7
- if (line.length <= maxWidth) return line;
8
- return line.slice(0, maxWidth - 3) + "...";
18
+ const vw = visibleWidth(line);
19
+ if (vw <= maxWidth) return line;
20
+ return truncateToWidth(line, maxWidth, "…");
9
21
  });
10
22
  }
11
23
 
@@ -16,17 +16,45 @@ import { registerCompactorTools } from "./tools/register.js";
16
16
  import { normalizeMessages } from "./compaction/normalize.js";
17
17
  import { filterNoise } from "./compaction/filter-noise.js";
18
18
  import type { NormalizedBlock, CompactorStrategyConfig, RuntimeCounters } from "./types.js";
19
+ import type { RuntimeStats } from "./session/analytics.js";
19
20
 
20
21
  /** Debug logger — only logs when config.debug === true */
21
22
  function createDebugLogger(getConfig: () => { debug: boolean }) {
22
- return (event: string, data?: Record<string, unknown>) => {
23
- if (!getConfig().debug) return;
24
- const ts = new Date().toISOString().slice(11, 23);
25
- const details = data ? " " + JSON.stringify(data) : "";
26
- console.error(`[compactor:${ts}] ${event}${details}`);
23
+ return (_event: string, _data?: Record<string, unknown>) => {
24
+ // Debug logging disabled — was writing to stdout causing TUI rendering issues.
25
+ return;
27
26
  };
28
27
  }
29
28
 
29
+ /** Measure byte size of a tool_result event's response content. */
30
+ function measureResponseBytes(event: any): number {
31
+ try {
32
+ const content = event.content;
33
+ if (typeof content === "string") return Buffer.byteLength(content, "utf-8");
34
+ if (Array.isArray(content)) {
35
+ return content.reduce((sum: number, block: any) => {
36
+ if (typeof block?.text === "string") return sum + Buffer.byteLength(block.text, "utf-8");
37
+ if (typeof block === "string") return sum + Buffer.byteLength(block, "utf-8");
38
+ return sum;
39
+ }, 0);
40
+ }
41
+ if (event.output && typeof event.output === "string") return Buffer.byteLength(event.output, "utf-8");
42
+ } catch {
43
+ // Non-blocking: byte measurement errors silently skipped
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ /** Check if a tool is a sandbox tool (output stays in sandbox, not context). */
49
+ function isSandboxTool(name: string): boolean {
50
+ return name === "bash" || name === "Bash";
51
+ }
52
+
53
+ /** Check if a tool is an index tool (content goes to FTS5, not context). Future-proofing. */
54
+ function isIndexTool(_name: string): boolean {
55
+ return false;
56
+ }
57
+
30
58
  export default function compactorExtension(pi: ExtensionAPI): void {
31
59
  let sessionDB: SessionDB | null = null;
32
60
  let contentStore: ContentStore | null = null;
@@ -43,6 +71,16 @@ export default function compactorExtension(pi: ExtensionAPI): void {
43
71
  };
44
72
  const getCounters = () => counters;
45
73
 
74
+ const runtimeStats: RuntimeStats = {
75
+ bytesReturned: {},
76
+ bytesIndexed: 0,
77
+ bytesSandboxed: 0,
78
+ calls: {},
79
+ sessionStart: Date.now(),
80
+ cacheHits: 0,
81
+ cacheBytesSaved: 0,
82
+ };
83
+
46
84
  const debug = createDebugLogger(() => config);
47
85
 
48
86
  const init = async () => {
@@ -58,8 +96,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
58
96
  const db = new SessionDB();
59
97
  await db.init();
60
98
  sessionDB = db;
61
- } catch (err) {
62
- console.error(`[compactor] SessionDB init failed: ${String(err)}`);
99
+ } catch {
100
+ // Silently ignore — SessionDB init failure is handled gracefully.
63
101
  sessionDB = null;
64
102
  }
65
103
 
@@ -70,8 +108,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
70
108
  const cs = new ContentStore();
71
109
  await cs.init();
72
110
  contentStore = cs;
73
- } catch (err) {
74
- console.error(`[compactor] ContentStore init failed: ${String(err)}`);
111
+ } catch {
112
+ // Silently ignore — ContentStore init failure is handled gracefully.
75
113
  contentStore = null;
76
114
  }
77
115
  }
@@ -101,6 +139,15 @@ export default function compactorExtension(pi: ExtensionAPI): void {
101
139
 
102
140
  debug("session_start", { sessionId: fullSessionId, projectDir });
103
141
 
142
+ // Reset runtime stats for new session
143
+ runtimeStats.bytesReturned = {};
144
+ runtimeStats.bytesIndexed = 0;
145
+ runtimeStats.bytesSandboxed = 0;
146
+ runtimeStats.calls = {};
147
+ runtimeStats.sessionStart = Date.now();
148
+ runtimeStats.cacheHits = 0;
149
+ runtimeStats.cacheBytesSaved = 0;
150
+
104
151
  sessionDB?.ensureSession(fullSessionId, projectDir);
105
152
 
106
153
  // Register all compactor tools with Pi (deps now have live sessionDB)
@@ -119,9 +166,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
119
166
 
120
167
  // Register info-screen group
121
168
  const infoRegistry = (globalThis as any).__unipi_info_registry;
122
- if (infoRegistry && sessionDB && contentStore) {
169
+ if (infoRegistry && sessionDB) {
123
170
  const sdb = sessionDB;
124
- const cs = contentStore;
125
171
  const sid = () => currentSessionId;
126
172
  infoRegistry.registerGroup({
127
173
  id: "compactor",
@@ -131,23 +177,25 @@ export default function compactorExtension(pi: ExtensionAPI): void {
131
177
  config: {
132
178
  showByDefault: true,
133
179
  stats: [
134
- { id: "sessionEvents", label: "Session events", show: true },
180
+ { id: "tokensSaved", label: "Tokens saved", show: true },
181
+ { id: "costSaved", label: "Cost saved", show: true },
182
+ { id: "pctReduction", label: "% Reduction", show: true },
183
+ { id: "topTools", label: "Top tools", show: true },
135
184
  { id: "compactions", label: "Compactions", show: true },
136
- { id: "tokensSaved", label: "Tokens compacted", show: true },
137
- { id: "compressionRatio", label: "Compression ratio", show: true },
138
- { id: "indexedDocs", label: "Indexed docs", show: true },
185
+ { id: "toolCalls", label: "Tool calls", show: true },
139
186
  ],
140
187
  },
141
188
  dataProvider: async () => {
142
189
  try {
143
190
  const { getInfoScreenData } = await import("./info-screen.js");
144
- const data = await getInfoScreenData(sdb, cs, sid(), counters);
191
+ const data = await getInfoScreenData(sdb, sid(), runtimeStats);
145
192
  return {
146
- sessionEvents: { value: data.sessionEvents.value, detail: data.sessionEvents.detail },
147
- compactions: { value: data.compactions.value, detail: data.compactions.detail },
148
193
  tokensSaved: { value: data.tokensSaved.value, detail: data.tokensSaved.detail },
149
- compressionRatio: { value: data.compressionRatio.value, detail: data.compressionRatio.detail },
150
- indexedDocs: { value: data.indexedDocs.value, detail: data.indexedDocs.detail },
194
+ costSaved: { value: data.costSaved.value, detail: data.costSaved.detail },
195
+ pctReduction: { value: data.pctReduction.value, detail: data.pctReduction.detail },
196
+ topTools: { value: data.topTools.value, detail: data.topTools.detail },
197
+ compactions: { value: data.compactions.value, detail: data.compactions.detail },
198
+ toolCalls: { value: data.toolCalls.value, detail: data.toolCalls.detail },
151
199
  };
152
200
  } catch {
153
201
  return {};
@@ -252,11 +300,29 @@ export default function compactorExtension(pi: ExtensionAPI): void {
252
300
  const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
253
301
  sessionDB.incrementCompactCount(sessionId);
254
302
  counters.compactions++;
303
+
304
+ // Use actual runtimeStats for byte measurement instead of heuristic
305
+ const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce((s, b) => s + b, 0);
306
+ const totalBytesProcessed = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed + totalBytesReturned;
307
+ // charsBefore = total bytes processed by all tools (proxy for context window usage)
308
+ // charsKept = bytes that stayed in context (bytesReturned, minus what compaction removed)
255
309
  const tokensBefore = (event as any).tokensBefore ?? 0;
256
- if (tokensBefore > 0) {
257
- counters.totalTokensCompacted += Math.round(tokensBefore * 0.85); // rough estimate
310
+ if (totalBytesProcessed > 0 && tokensBefore > 0) {
311
+ // Use actual token count from Pi, estimate chars from it
312
+ const charsBefore = tokensBefore * 4;
313
+ // Estimate kept chars: proportional to what remains after compaction
314
+ const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
315
+ const charsKept = tokensAfter * 4;
316
+ const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
317
+ counters.totalTokensCompacted += tokensBefore - tokensAfter;
318
+ sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
319
+ } else if (tokensBefore > 0) {
320
+ // Fallback: only tokensBefore available, use conservative estimate
321
+ const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
322
+ counters.totalTokensCompacted += tokensBefore - tokensAfter;
323
+ sessionDB.addCompactionStats(sessionId, tokensBefore * 4, tokensAfter * 4, 1);
258
324
  }
259
- debug("session_compact", { sessionId });
325
+ debug("session_compact", { sessionId, tokensBefore, totalBytesProcessed });
260
326
  }
261
327
  });
262
328
 
@@ -365,6 +431,24 @@ export default function compactorExtension(pi: ExtensionAPI): void {
365
431
  debug("event_stored", { category: ev.category, type: ev.type });
366
432
  }
367
433
 
434
+ // Track byte consumption per tool for analytics
435
+ try {
436
+ const responseBytes = measureResponseBytes(event);
437
+ if (responseBytes > 0) {
438
+ const tName = (event as any).toolName ?? "unknown";
439
+ runtimeStats.calls[tName] = (runtimeStats.calls[tName] || 0) + 1;
440
+ runtimeStats.bytesReturned[tName] = (runtimeStats.bytesReturned[tName] || 0) + responseBytes;
441
+ if (isSandboxTool(tName)) {
442
+ runtimeStats.bytesSandboxed += responseBytes;
443
+ }
444
+ if (isIndexTool(tName)) {
445
+ runtimeStats.bytesIndexed += responseBytes;
446
+ }
447
+ }
448
+ } catch {
449
+ // Non-blocking: byte tracking errors silently skipped
450
+ }
451
+
368
452
  // Apply display overrides for built-in tools
369
453
  const toolName = (event as any).toolName ?? "";
370
454
  const td = config.toolDisplay;
@@ -385,6 +469,30 @@ export default function compactorExtension(pi: ExtensionAPI): void {
385
469
  } catch {
386
470
  // Non-fatal: display override failed
387
471
  }
472
+
473
+ // Width-safe diff truncation for edit/write tool results.
474
+ // Pi's renderDiff() does not truncate lines to terminal width,
475
+ // causing TUI crashes on narrow terminals. We truncate the
476
+ // diff string in details.diff before it reaches the TUI.
477
+ const diffToolNames = ["edit", "Edit", "write", "Write"];
478
+ if (diffToolNames.includes(toolName)) {
479
+ try {
480
+ const details = (event as any).details as
481
+ { diff?: string } | undefined;
482
+ if (details?.diff) {
483
+ const { clampDiffToWidth } = await import(
484
+ "./display/diff-width-safety.js"
485
+ );
486
+ const clamped = clampDiffToWidth(details.diff);
487
+ if (clamped !== details.diff) {
488
+ debug("diff_width_clamped", { toolName });
489
+ return { details: { ...details, diff: clamped } } as any;
490
+ }
491
+ }
492
+ } catch (err) {
493
+ debug("diff_width_clamp_error", { error: String(err) });
494
+ }
495
+ }
388
496
  });
389
497
 
390
498
  pi.on("message_update", async (event, _ctx) => {