pi-jev-lens 0.1.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.
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/STATUS.md +391 -0
- package/index.ts +591 -0
- package/package.json +67 -0
- package/src/classifier.ts +151 -0
- package/src/config.ts +188 -0
- package/src/ledger.ts +25 -0
- package/src/memory-file.ts +51 -0
- package/src/pi-types.ts +6 -0
- package/src/policy.ts +145 -0
- package/src/presend.ts +217 -0
- package/src/shell-display.ts +90 -0
- package/src/text.ts +68 -0
- package/src/treesitter.ts +220 -0
- package/src/types.ts +45 -0
- package/src/ui.ts +141 -0
- package/src/views.ts +611 -0
package/src/ui.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI integration: what the model got versus what the tool really returned.
|
|
3
|
+
*
|
|
4
|
+
* - Built-in tools (read, bash, grep, find, ls) are re-registered with renderers that show, for
|
|
5
|
+
* a compressed result, a one-line savings header and (expanded) the exact text the model saw.
|
|
6
|
+
* - `/jev-lens diff [n]` opens an overlay with the original output, omitted lines marked, and
|
|
7
|
+
* `t` toggles to the sent view.
|
|
8
|
+
*/
|
|
9
|
+
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
/** Everything needed to show one compressed result. Stored per session and in the tool result details. */
|
|
12
|
+
export interface CompressedRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
args: unknown;
|
|
16
|
+
kind: string;
|
|
17
|
+
view: string;
|
|
18
|
+
tokensBefore: number;
|
|
19
|
+
tokensAfter: number;
|
|
20
|
+
full: string;
|
|
21
|
+
sent: string;
|
|
22
|
+
/** 1-based original line numbers present in the view. */
|
|
23
|
+
included: number[];
|
|
24
|
+
needsFull?: number;
|
|
25
|
+
pFull?: number;
|
|
26
|
+
recalls: number;
|
|
27
|
+
at: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Minimal theme surface used here, so the module is testable without pi's Theme class. */
|
|
31
|
+
export interface ThemeLike {
|
|
32
|
+
fg(color: string, text: string): string;
|
|
33
|
+
bold(text: string): string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function savingsLine(r: CompressedRecord, theme: ThemeLike): string {
|
|
37
|
+
const pct = r.tokensBefore ? Math.round((100 * (r.tokensBefore - r.tokensAfter)) / r.tokensBefore) : 0;
|
|
38
|
+
return (
|
|
39
|
+
theme.fg("accent", "⌁ jev-lens ") +
|
|
40
|
+
theme.fg("toolTitle", theme.bold(r.view)) +
|
|
41
|
+
theme.fg("muted", ` · ${r.tokensAfter} of ${r.tokensBefore} tokens (−${pct} %)`) +
|
|
42
|
+
(r.recalls ? theme.fg("warning", ` · recalled ${r.recalls}×`) : "") +
|
|
43
|
+
theme.fg("dim", " · /jev-lens diff")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function describeArgs(toolName: string, args: unknown): string {
|
|
48
|
+
const a = (args ?? {}) as Record<string, unknown>;
|
|
49
|
+
if (typeof a.path === "string") return a.path;
|
|
50
|
+
if (typeof a.command === "string") return a.command.replace(/\s+/g, " ").slice(0, 80);
|
|
51
|
+
if (typeof a.pattern === "string") return a.pattern;
|
|
52
|
+
return JSON.stringify(a).slice(0, 80);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Overlay showing the original output with omitted lines marked (mode "annotated"), or exactly
|
|
57
|
+
* what was sent (mode "sent"). Keys: ↑/↓ scroll, PgUp/PgDn, Home/End, t toggle, Esc/q close.
|
|
58
|
+
*/
|
|
59
|
+
export class DiffOverlay {
|
|
60
|
+
private offset = 0;
|
|
61
|
+
private mode: "annotated" | "sent" = "annotated";
|
|
62
|
+
private cache?: { width: number; lines: string[] };
|
|
63
|
+
constructor(
|
|
64
|
+
private record: CompressedRecord,
|
|
65
|
+
private theme: ThemeLike,
|
|
66
|
+
private height: number,
|
|
67
|
+
private onClose: () => void,
|
|
68
|
+
private onChange?: () => void,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
private bodyLines(width: number): string[] {
|
|
72
|
+
const r = this.record;
|
|
73
|
+
const out: string[] = [];
|
|
74
|
+
if (this.mode === "sent") {
|
|
75
|
+
for (const l of r.sent.split("\n")) out.push(truncateToWidth(l, width));
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
const inc = new Set(r.included);
|
|
79
|
+
const lines = r.full.split("\n");
|
|
80
|
+
const w = String(lines.length).length;
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const n = String(i + 1).padStart(w);
|
|
83
|
+
if (inc.has(i + 1)) out.push(truncateToWidth(this.theme.fg("dim", `${n} `) + this.theme.fg("text", "│ ") + lines[i], width));
|
|
84
|
+
else out.push(truncateToWidth(this.theme.fg("toolDiffRemoved", `${n} − ${lines[i]}`), width));
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
header(width: number): string[] {
|
|
90
|
+
const r = this.record;
|
|
91
|
+
const pct = r.tokensBefore ? Math.round((100 * (r.tokensBefore - r.tokensAfter)) / r.tokensBefore) : 0;
|
|
92
|
+
const omitted = r.full.split("\n").length - r.included.length;
|
|
93
|
+
return [
|
|
94
|
+
truncateToWidth(this.theme.fg("accent", this.theme.bold(`jev-lens · ${r.toolName} ${describeArgs(r.toolName, r.args)}`)), width),
|
|
95
|
+
truncateToWidth(this.theme.fg("muted", `${r.kind} → ${r.view} · ${r.tokensAfter} of ${r.tokensBefore} tokens (−${pct} %) · ${omitted} of ${r.full.split("\n").length} lines omitted` + (r.needsFull !== undefined ? ` · P(needs full)=${r.needsFull.toFixed(2)} P(full)=${(r.pFull ?? 0).toFixed(2)}` : "") + (r.recalls ? ` · recalled ${r.recalls}×` : "")), width),
|
|
96
|
+
truncateToWidth(this.theme.fg("dim", this.mode === "annotated" ? "original output; − marks lines the model did not get · t: show what was sent · ↑↓ PgUp PgDn · Esc" : "exactly what the model got · t: show original with omissions · ↑↓ PgUp PgDn · Esc"), width),
|
|
97
|
+
"",
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render(width: number): string[] {
|
|
102
|
+
if (this.cache && this.cache.width === width) return this.cache.lines;
|
|
103
|
+
const head = this.header(width);
|
|
104
|
+
const body = this.bodyLines(width);
|
|
105
|
+
const room = Math.max(3, this.height - head.length - 1);
|
|
106
|
+
const maxOffset = Math.max(0, body.length - room);
|
|
107
|
+
if (this.offset > maxOffset) this.offset = maxOffset;
|
|
108
|
+
const slice = body.slice(this.offset, this.offset + room);
|
|
109
|
+
const footer = truncateToWidth(this.theme.fg("dim", `lines ${body.length ? this.offset + 1 : 0}-${Math.min(body.length, this.offset + room)} of ${body.length}`), width);
|
|
110
|
+
this.cache = { width, lines: [...head, ...slice, footer] };
|
|
111
|
+
return this.cache.lines;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
handleInput(data: string): void {
|
|
115
|
+
const page = Math.max(1, this.height - 6);
|
|
116
|
+
if (matchesKey(data, "escape") || data === "q") { this.onClose(); return; }
|
|
117
|
+
else if (matchesKey(data, "up")) this.offset = Math.max(0, this.offset - 1);
|
|
118
|
+
else if (matchesKey(data, "down")) this.offset += 1;
|
|
119
|
+
else if (matchesKey(data, "pageUp")) this.offset = Math.max(0, this.offset - page);
|
|
120
|
+
else if (matchesKey(data, "pageDown")) this.offset += page;
|
|
121
|
+
else if (matchesKey(data, "home")) this.offset = 0;
|
|
122
|
+
else if (matchesKey(data, "end")) this.offset = Number.MAX_SAFE_INTEGER;
|
|
123
|
+
else if (data === "t") { this.mode = this.mode === "annotated" ? "sent" : "annotated"; this.offset = 0; }
|
|
124
|
+
else return;
|
|
125
|
+
this.invalidate();
|
|
126
|
+
this.onChange?.();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
invalidate(): void {
|
|
130
|
+
this.cache = undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** One row per compressed result, newest last. */
|
|
135
|
+
export function listLines(records: CompressedRecord[], theme: ThemeLike): string[] {
|
|
136
|
+
if (records.length === 0) return [theme.fg("muted", "no compressed tool results in this session yet")];
|
|
137
|
+
return records.map((r, i) => {
|
|
138
|
+
const pct = r.tokensBefore ? Math.round((100 * (r.tokensBefore - r.tokensAfter)) / r.tokensBefore) : 0;
|
|
139
|
+
return `${String(records.length - i).padStart(3)} ${r.view.padEnd(9)} ${String(r.tokensBefore).padStart(6)}→${String(r.tokensAfter).padEnd(6)} −${String(pct).padStart(3)} % ${r.recalls ? `recalled ${r.recalls}× ` : ""}${r.toolName} ${describeArgs(r.toolName, r.args)}`;
|
|
140
|
+
});
|
|
141
|
+
}
|