@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.
- package/README.md +10 -0
- package/package.json +9 -4
- package/packages/autocomplete/src/constants.ts +20 -0
- package/packages/autocomplete/src/settings.ts +2 -2
- package/packages/compactor/src/commands/index.ts +2 -2
- package/packages/compactor/src/compaction/hooks.ts +3 -5
- package/packages/compactor/src/config/schema.ts +1 -1
- package/packages/compactor/src/display/diff-presentation.ts +6 -1
- package/packages/compactor/src/display/diff-renderer.ts +34 -8
- package/packages/compactor/src/display/diff-width-safety.ts +83 -0
- package/packages/compactor/src/display/line-width-safety.ts +14 -2
- package/packages/compactor/src/index.ts +131 -23
- package/packages/compactor/src/info-screen.ts +136 -51
- package/packages/compactor/src/session/analytics.ts +198 -0
- package/packages/compactor/src/session/db.ts +3 -0
- package/packages/compactor/src/tools/register.ts +2 -2
- package/packages/compactor/src/types.ts +5 -6
- package/packages/footer/src/commands.ts +3 -0
- package/packages/footer/src/config.ts +6 -6
- package/packages/footer/src/events.ts +34 -34
- package/packages/footer/src/index.ts +21 -9
- package/packages/footer/src/presets.ts +6 -6
- package/packages/footer/src/registry/index.ts +5 -7
- package/packages/footer/src/rendering/icons.ts +88 -88
- package/packages/footer/src/rendering/renderer.ts +4 -4
- package/packages/footer/src/segments/core.ts +14 -55
- package/packages/footer/src/segments/memory.ts +9 -7
- package/packages/footer/src/segments/ralph.ts +9 -10
- package/packages/footer/src/segments/status-ext.ts +17 -12
- package/packages/footer/src/segments/workflow.ts +5 -4
- package/packages/footer/src/tui/settings-tui.ts +216 -155
- package/packages/input-shortcuts/README.md +116 -0
- package/packages/input-shortcuts/index.ts +5 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +235 -0
- package/packages/input-shortcuts/src/clipboard.ts +119 -0
- package/packages/input-shortcuts/src/index.ts +411 -0
- package/packages/input-shortcuts/src/registers.ts +92 -0
- package/packages/input-shortcuts/src/settings-overlay.ts +142 -0
- package/packages/input-shortcuts/src/status.ts +35 -0
- package/packages/input-shortcuts/src/types.ts +48 -0
- package/packages/input-shortcuts/src/undo-redo.ts +86 -0
- package/packages/mcp/src/index.ts +2 -5
- package/packages/notify/index.ts +2 -1
- package/packages/notify/skills/configure-notify/SKILL.md +43 -6
- package/packages/unipi/index.ts +4 -0
- package/packages/updater/README.md +71 -0
- package/packages/updater/index.ts +6 -0
- package/packages/updater/skills/configure-updater/SKILL.md +65 -0
- package/packages/updater/src/cache.ts +67 -0
- package/packages/updater/src/changelog.ts +141 -0
- package/packages/updater/src/checker.ts +84 -0
- package/packages/updater/src/commands.ts +83 -0
- package/packages/updater/src/index.ts +178 -0
- package/packages/updater/src/installer.ts +74 -0
- package/packages/updater/src/markdown.ts +173 -0
- package/packages/updater/src/readme.ts +139 -0
- package/packages/updater/src/settings.ts +98 -0
- package/packages/updater/src/tui/changelog-overlay.ts +256 -0
- package/packages/updater/src/tui/readme-overlay.ts +236 -0
- package/packages/updater/src/tui/settings-overlay.ts +191 -0
- package/packages/updater/src/tui/update-overlay.ts +261 -0
- package/packages/utility/src/diff/highlighter.ts +1 -2
- package/packages/utility/src/diff/renderer.ts +25 -52
- package/packages/utility/src/diff/wrapper.ts +60 -8
- package/packages/web-api/README.md +76 -15
- package/packages/web-api/skills/web/SKILL.md +54 -11
- package/packages/web-api/src/engine/constants.ts +36 -0
- package/packages/web-api/src/engine/dependencies.ts +145 -0
- package/packages/web-api/src/engine/dom.ts +266 -0
- package/packages/web-api/src/engine/extract.ts +642 -0
- package/packages/web-api/src/engine/format.ts +306 -0
- package/packages/web-api/src/engine/profiles.ts +102 -0
- package/packages/web-api/src/engine/types.ts +169 -0
- package/packages/web-api/src/index.ts +9 -2
- package/packages/web-api/src/providers/base.ts +9 -1
- package/packages/web-api/src/settings.ts +70 -4
- package/packages/web-api/src/tools.ts +281 -24
- package/packages/web-api/src/tui/progress.ts +168 -0
- package/packages/web-api/src/tui/result.ts +173 -0
- 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.
|
|
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
|
|
56
|
-
|
|
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(
|
|
38
|
-
|
|
37
|
+
function deprecationLog(_oldName: string, _newName: string): void {
|
|
38
|
+
// Deprecation logging disabled — was 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 = (
|
|
25
|
-
|
|
26
|
-
|
|
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 => {
|
|
@@ -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) =>
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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 (
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
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: "
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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) => {
|