@pi-unipi/compactor 0.1.1

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 (65) hide show
  1. package/README.md +86 -0
  2. package/package.json +54 -0
  3. package/skills/compactor/SKILL.md +74 -0
  4. package/skills/compactor-doctor/SKILL.md +74 -0
  5. package/skills/compactor-ops/SKILL.md +65 -0
  6. package/skills/compactor-stats/SKILL.md +49 -0
  7. package/skills/compactor-tools/SKILL.md +120 -0
  8. package/src/commands/index.ts +248 -0
  9. package/src/compaction/brief.ts +334 -0
  10. package/src/compaction/build-sections.ts +77 -0
  11. package/src/compaction/content.ts +47 -0
  12. package/src/compaction/cut.ts +80 -0
  13. package/src/compaction/extract/commits.ts +52 -0
  14. package/src/compaction/extract/files.ts +58 -0
  15. package/src/compaction/extract/goals.ts +36 -0
  16. package/src/compaction/extract/preferences.ts +40 -0
  17. package/src/compaction/filter-noise.ts +46 -0
  18. package/src/compaction/format.ts +48 -0
  19. package/src/compaction/hooks.ts +145 -0
  20. package/src/compaction/merge.ts +113 -0
  21. package/src/compaction/normalize.ts +68 -0
  22. package/src/compaction/recall-scope.ts +32 -0
  23. package/src/compaction/sanitize.ts +12 -0
  24. package/src/compaction/search-entries.ts +101 -0
  25. package/src/compaction/sections.ts +15 -0
  26. package/src/compaction/summarize.ts +29 -0
  27. package/src/config/manager.ts +89 -0
  28. package/src/config/presets.ts +83 -0
  29. package/src/config/schema.ts +55 -0
  30. package/src/display/bash-display.ts +28 -0
  31. package/src/display/diff-presentation.ts +20 -0
  32. package/src/display/diff-renderer.ts +255 -0
  33. package/src/display/line-width-safety.ts +16 -0
  34. package/src/display/pending-diff-preview.ts +51 -0
  35. package/src/display/render-utils.ts +52 -0
  36. package/src/display/thinking-label.ts +18 -0
  37. package/src/display/tool-overrides.ts +136 -0
  38. package/src/display/user-message-box.ts +16 -0
  39. package/src/executor/executor.ts +242 -0
  40. package/src/executor/runtime.ts +125 -0
  41. package/src/index.ts +211 -0
  42. package/src/info-screen.ts +60 -0
  43. package/src/security/evaluator.ts +142 -0
  44. package/src/security/policy.ts +74 -0
  45. package/src/security/scanner.ts +65 -0
  46. package/src/session/db.ts +237 -0
  47. package/src/session/extract.ts +107 -0
  48. package/src/session/resume-inject.ts +25 -0
  49. package/src/session/snapshot.ts +326 -0
  50. package/src/store/chunking.ts +126 -0
  51. package/src/store/db-base.ts +79 -0
  52. package/src/store/index.ts +364 -0
  53. package/src/tools/compact.ts +20 -0
  54. package/src/tools/ctx-batch-execute.ts +53 -0
  55. package/src/tools/ctx-doctor.ts +78 -0
  56. package/src/tools/ctx-execute-file.ts +26 -0
  57. package/src/tools/ctx-execute.ts +21 -0
  58. package/src/tools/ctx-fetch-and-index.ts +37 -0
  59. package/src/tools/ctx-index.ts +42 -0
  60. package/src/tools/ctx-search.ts +23 -0
  61. package/src/tools/ctx-stats.ts +37 -0
  62. package/src/tools/register.ts +360 -0
  63. package/src/tools/vcc-recall.ts +64 -0
  64. package/src/tui/settings-overlay.ts +290 -0
  65. package/src/types.ts +269 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * PolyglotExecutor — sandboxed code execution for 11 languages
3
+ */
4
+
5
+ import { spawn, execSync, execFileSync } from "node:child_process";
6
+ import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { detectRuntimes, buildCommand, type RuntimeMap, type Language } from "./runtime.js";
10
+ import type { ExecResult } from "../types.js";
11
+
12
+ const isWin = process.platform === "win32";
13
+
14
+ const OS_TMPDIR = (() => {
15
+ if (isWin) return process.env.TEMP ?? process.env.TMP ?? tmpdir();
16
+ try {
17
+ const result = execFileSync(
18
+ process.platform === "darwin" ? "getconf" : "mktemp",
19
+ process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"],
20
+ { env: { ...process.env, TMPDIR: undefined as unknown as string }, encoding: "utf-8" },
21
+ ).trim();
22
+ const dir = process.platform === "darwin" ? result : resolve(result, "..");
23
+ if (dir && dir !== process.cwd()) return dir;
24
+ } catch { /* fall through */ }
25
+ return "/tmp";
26
+ })();
27
+
28
+ function killTree(proc: ReturnType<typeof spawn>): void {
29
+ if (isWin && proc.pid) {
30
+ try {
31
+ execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: "pipe" });
32
+ } catch { /* already dead */ }
33
+ } else if (proc.pid) {
34
+ try {
35
+ process.kill(-proc.pid, "SIGKILL");
36
+ } catch { /* already dead */ }
37
+ }
38
+ }
39
+
40
+ const DANGEROUS_ENV_VARS = [
41
+ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
42
+ "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID",
43
+ "GCP_SERVICE_ACCOUNT_KEY", "GOOGLE_APPLICATION_CREDENTIALS",
44
+ "GITHUB_TOKEN", "GH_TOKEN", "GITLAB_TOKEN",
45
+ "DOCKER_AUTH_CONFIG", "KUBECONFIG",
46
+ "NPM_TOKEN", "NODE_AUTH_TOKEN", "YARN_AUTH_TOKEN",
47
+ "PYPI_TOKEN", "POETRY_PYPI_TOKEN_PYPI",
48
+ "SSH_PRIVATE_KEY", "SSH_AUTH_SOCK",
49
+ "DATABASE_URL", "REDIS_URL", "MONGO_URL",
50
+ "SECRET_KEY", "JWT_SECRET", "API_KEY", "API_SECRET",
51
+ "PASSWORD", "PASSPHRASE", "TOKEN", "CREDENTIALS",
52
+ ];
53
+
54
+ function sanitizeEnv(): NodeJS.ProcessEnv {
55
+ const env = { ...process.env };
56
+ for (const key of DANGEROUS_ENV_VARS) {
57
+ delete env[key];
58
+ }
59
+ // Also strip anything matching *SECRET*, *PASSWORD*, *TOKEN*, *KEY*
60
+ for (const key of Object.keys(env)) {
61
+ const upper = key.toUpperCase();
62
+ if (upper.includes("SECRET") || upper.includes("PASSWORD") || upper.includes("TOKEN") || upper.includes("PRIVATE_KEY")) {
63
+ delete env[key];
64
+ }
65
+ }
66
+ return env;
67
+ }
68
+
69
+ interface ExecuteOptions {
70
+ language: Language;
71
+ code: string;
72
+ timeout?: number;
73
+ background?: boolean;
74
+ }
75
+
76
+ interface ExecuteFileOptions extends ExecuteOptions {
77
+ path: string;
78
+ }
79
+
80
+ export class PolyglotExecutor {
81
+ #hardCapBytes: number;
82
+ #projectRoot: string;
83
+ #runtimes: RuntimeMap;
84
+ #backgroundedPids = new Set<number>();
85
+
86
+ constructor(opts?: { hardCapBytes?: number; projectRoot?: string; runtimes?: RuntimeMap }) {
87
+ this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024;
88
+ this.#projectRoot = opts?.projectRoot ?? process.cwd();
89
+ this.#runtimes = opts?.runtimes ?? detectRuntimes();
90
+ }
91
+
92
+ get runtimes(): RuntimeMap {
93
+ return { ...this.#runtimes };
94
+ }
95
+
96
+ cleanupBackgrounded(): void {
97
+ for (const pid of this.#backgroundedPids) {
98
+ try {
99
+ process.kill(isWin ? pid : -pid, "SIGTERM");
100
+ } catch { /* already dead */ }
101
+ }
102
+ this.#backgroundedPids.clear();
103
+ }
104
+
105
+ async execute(opts: ExecuteOptions): Promise<ExecResult> {
106
+ const { language, code, timeout = 30_000, background = false } = opts;
107
+ const tmpDir = mkdtempSync(join(OS_TMPDIR, ".compactor-"));
108
+
109
+ try {
110
+ const filePath = this.#writeScript(tmpDir, code, language);
111
+ const cmd = buildCommand(this.#runtimes, language, filePath);
112
+
113
+ if (cmd[0] === "__rust_compile_run__") {
114
+ return await this.#compileAndRun(filePath, tmpDir, timeout);
115
+ }
116
+
117
+ const cwd = language === "shell" ? this.#projectRoot : tmpDir;
118
+ const result = await this.#spawn(cmd, cwd, tmpDir, timeout, background);
119
+
120
+ if (!result.backgrounded) {
121
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
122
+ }
123
+
124
+ return result;
125
+ } catch (err: any) {
126
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
127
+ return {
128
+ stdout: "",
129
+ stderr: err?.message ?? String(err),
130
+ exitCode: 1,
131
+ timedOut: false,
132
+ };
133
+ }
134
+ }
135
+
136
+ async executeFile(opts: ExecuteFileOptions): Promise<ExecResult> {
137
+ const { language, path, timeout = 30_000, background = false } = opts;
138
+ const cmd = buildCommand(this.#runtimes, language, path);
139
+ const cwd = language === "shell" ? this.#projectRoot : resolve(path, "..");
140
+ return this.#spawn(cmd, cwd, "", timeout, background);
141
+ }
142
+
143
+ #writeScript(tmpDir: string, code: string, language: Language): string {
144
+ const extensions: Record<Language, string> = {
145
+ javascript: "js", typescript: "ts", python: "py", shell: "sh",
146
+ ruby: "rb", go: "go", rust: "rs", php: "php", perl: "pl", r: "r", elixir: "ex",
147
+ };
148
+ const ext = extensions[language];
149
+ const filePath = join(tmpDir, `script.${ext}`);
150
+ writeFileSync(filePath, code, "utf-8");
151
+ return filePath;
152
+ }
153
+
154
+ async #spawn(
155
+ cmd: string[],
156
+ cwd: string,
157
+ tmpDir: string,
158
+ timeout: number,
159
+ background: boolean,
160
+ ): Promise<ExecResult> {
161
+ return new Promise((resolve) => {
162
+ const proc = spawn(cmd[0], cmd.slice(1), {
163
+ cwd,
164
+ env: sanitizeEnv(),
165
+ stdio: ["ignore", "pipe", "pipe"],
166
+ detached: !isWin,
167
+ });
168
+
169
+ let stdout = "";
170
+ let stderr = "";
171
+ let killed = false;
172
+ let stdoutLen = 0;
173
+ let stderrLen = 0;
174
+
175
+ const killIfOversized = () => {
176
+ if (stdoutLen + stderrLen > this.#hardCapBytes && !killed) {
177
+ killed = true;
178
+ killTree(proc);
179
+ }
180
+ };
181
+
182
+ proc.stdout?.on("data", (chunk: Buffer) => {
183
+ stdout += chunk.toString("utf-8");
184
+ stdoutLen += chunk.length;
185
+ killIfOversized();
186
+ });
187
+
188
+ proc.stderr?.on("data", (chunk: Buffer) => {
189
+ stderr += chunk.toString("utf-8");
190
+ stderrLen += chunk.length;
191
+ killIfOversized();
192
+ });
193
+
194
+ const timer = background ? null : setTimeout(() => {
195
+ if (!killed) {
196
+ killed = true;
197
+ killTree(proc);
198
+ }
199
+ }, timeout);
200
+
201
+ proc.on("error", (err) => {
202
+ if (timer) clearTimeout(timer);
203
+ resolve({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
204
+ });
205
+
206
+ proc.on("close", (code) => {
207
+ if (timer) clearTimeout(timer);
208
+ if (background && proc.pid) {
209
+ this.#backgroundedPids.add(proc.pid);
210
+ }
211
+ resolve({
212
+ stdout: stdout.slice(0, this.#hardCapBytes),
213
+ stderr: stderr.slice(0, this.#hardCapBytes),
214
+ exitCode: code ?? 0,
215
+ timedOut: killed && !background,
216
+ backgrounded: background && !!proc.pid,
217
+ });
218
+ });
219
+ });
220
+ }
221
+
222
+ async #compileAndRun(filePath: string, tmpDir: string, timeout: number): Promise<ExecResult> {
223
+ const binPath = join(tmpDir, "script");
224
+ try {
225
+ execSync(`rustc "${filePath}" -o "${binPath}"`, {
226
+ cwd: tmpDir,
227
+ env: sanitizeEnv(),
228
+ timeout: timeout / 2,
229
+ stdio: ["ignore", "pipe", "pipe"],
230
+ });
231
+ } catch (err: any) {
232
+ return {
233
+ stdout: "",
234
+ stderr: err?.stderr?.toString?.() ?? err?.message ?? "Rust compilation failed",
235
+ exitCode: 1,
236
+ timedOut: false,
237
+ };
238
+ }
239
+
240
+ return this.#spawn([binPath], tmpDir, tmpDir, timeout / 2, false);
241
+ }
242
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Runtime detection for sandbox executor
3
+ */
4
+
5
+ export type Language =
6
+ | "javascript"
7
+ | "typescript"
8
+ | "python"
9
+ | "shell"
10
+ | "ruby"
11
+ | "go"
12
+ | "rust"
13
+ | "php"
14
+ | "perl"
15
+ | "r"
16
+ | "elixir";
17
+
18
+ export interface RuntimeMap {
19
+ javascript: string;
20
+ typescript: string;
21
+ python: string;
22
+ shell: string;
23
+ ruby?: string;
24
+ go?: string;
25
+ rust?: string;
26
+ php?: string;
27
+ perl?: string;
28
+ r?: string;
29
+ elixir?: string;
30
+ }
31
+
32
+ export function detectRuntimes(): RuntimeMap {
33
+ const runtimes: Partial<RuntimeMap> = {};
34
+
35
+ // JavaScript / TypeScript — prefer Bun, fall back to Node
36
+ try {
37
+ const { execSync } = require("node:child_process");
38
+ try {
39
+ execSync("command -v bun", { stdio: "ignore" });
40
+ runtimes.javascript = "bun";
41
+ runtimes.typescript = "bun";
42
+ } catch {
43
+ runtimes.javascript = "node";
44
+ runtimes.typescript = "npx tsx";
45
+ }
46
+ } catch {
47
+ runtimes.javascript = "node";
48
+ runtimes.typescript = "npx tsx";
49
+ }
50
+
51
+ // Shell
52
+ runtimes.shell = "bash";
53
+
54
+ // Python
55
+ try {
56
+ const { execSync } = require("node:child_process");
57
+ try {
58
+ execSync("command -v python3", { stdio: "ignore" });
59
+ runtimes.python = "python3";
60
+ } catch {
61
+ runtimes.python = "python";
62
+ }
63
+ } catch {
64
+ runtimes.python = "python3";
65
+ }
66
+
67
+ // Optional runtimes
68
+ const optional = [
69
+ ["ruby", "ruby"],
70
+ ["go", "go"],
71
+ ["rustc", "rust"],
72
+ ["php", "php"],
73
+ ["perl", "perl"],
74
+ ["Rscript", "r"],
75
+ ["elixir", "elixir"],
76
+ ] as const;
77
+
78
+ for (const [cmd, lang] of optional) {
79
+ try {
80
+ const { execSync } = require("node:child_process");
81
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
82
+ (runtimes as any)[lang] = cmd;
83
+ } catch {
84
+ // not available
85
+ }
86
+ }
87
+
88
+ return runtimes as RuntimeMap;
89
+ }
90
+
91
+ export function buildCommand(
92
+ runtimes: RuntimeMap,
93
+ language: Language,
94
+ filePath: string,
95
+ ): string[] {
96
+ const runtime = runtimes[language];
97
+ if (!runtime) throw new Error(`No runtime available for ${language}`);
98
+
99
+ switch (language) {
100
+ case "javascript":
101
+ return runtime === "bun" ? ["bun", "run", filePath] : ["node", filePath];
102
+ case "typescript":
103
+ return runtime === "bun" ? ["bun", "run", filePath] : ["npx", "tsx", filePath];
104
+ case "python":
105
+ return [runtime, filePath];
106
+ case "shell":
107
+ return ["bash", filePath];
108
+ case "ruby":
109
+ return [runtime, filePath];
110
+ case "go":
111
+ return ["go", "run", filePath];
112
+ case "rust":
113
+ return ["__rust_compile_run__", filePath];
114
+ case "php":
115
+ return [runtime, filePath];
116
+ case "perl":
117
+ return [runtime, filePath];
118
+ case "r":
119
+ return [runtime, filePath];
120
+ case "elixir":
121
+ return [runtime, filePath];
122
+ default:
123
+ throw new Error(`Unsupported language: ${language}`);
124
+ }
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @pi-unipi/compactor — Extension entry point
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { MODULES, UNIPI_EVENTS, COMPACTOR_COMMANDS, COMPACTOR_TOOLS, emitEvent } from "@pi-unipi/core";
7
+ import { scaffoldConfig, loadConfig } from "./config/manager.js";
8
+ import { registerCompactionHooks } from "./compaction/hooks.js";
9
+ import { SessionDB, getWorktreeSuffix } from "./session/db.js";
10
+ import { extractEventsFromToolResult } from "./session/extract.js";
11
+ import { injectResumeSnapshot } from "./session/resume-inject.js";
12
+ import { ContentStore } from "./store/index.js";
13
+ import { PolyglotExecutor } from "./executor/executor.js";
14
+ import { registerCommands } from "./commands/index.js";
15
+ import { registerCompactorTools } from "./tools/register.js";
16
+ import { normalizeMessages } from "./compaction/normalize.js";
17
+ import { filterNoise } from "./compaction/filter-noise.js";
18
+ import type { NormalizedBlock } from "./types.js";
19
+
20
+ export default function compactorExtension(pi: ExtensionAPI): void {
21
+ let sessionDB: SessionDB | null = null;
22
+ let contentStore: ContentStore | null = null;
23
+ let executor: PolyglotExecutor | null = null;
24
+ let config = loadConfig();
25
+ let cachedBlocks: NormalizedBlock[] = [];
26
+ let currentSessionId = "default";
27
+
28
+ const init = async () => {
29
+ scaffoldConfig();
30
+ config = loadConfig();
31
+
32
+ sessionDB = new SessionDB();
33
+ await sessionDB.init();
34
+
35
+ if (config.fts5Index.enabled) {
36
+ contentStore = new ContentStore();
37
+ await contentStore.init();
38
+ }
39
+
40
+ executor = new PolyglotExecutor();
41
+ };
42
+
43
+ registerCompactionHooks(pi);
44
+
45
+ // Register commands with deps (they need sessionDB/contentStore)
46
+ const getCommandDeps = () => ({
47
+ sessionDB,
48
+ contentStore,
49
+ getSessionId: () => currentSessionId,
50
+ getBlocks: () => cachedBlocks,
51
+ });
52
+ registerCommands(pi, getCommandDeps());
53
+
54
+ pi.on("session_start", async (_event, ctx) => {
55
+ await init();
56
+
57
+ const sessionId = (ctx as any).sessionId ?? "default";
58
+ const projectDir = (ctx as any).cwd ?? process.cwd();
59
+ const suffix = getWorktreeSuffix();
60
+ const fullSessionId = `${sessionId}${suffix}`;
61
+ currentSessionId = fullSessionId;
62
+
63
+ sessionDB?.ensureSession(fullSessionId, projectDir);
64
+
65
+ // Register all compactor tools with Pi
66
+ if (sessionDB) {
67
+ registerCompactorTools(pi, {
68
+ sessionDB,
69
+ contentStore,
70
+ getSessionId: () => currentSessionId,
71
+ getBlocks: () => cachedBlocks,
72
+ });
73
+ }
74
+
75
+ // Re-register commands with fresh deps now that sessionDB is ready
76
+ registerCommands(pi, getCommandDeps());
77
+
78
+ emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
79
+ name: MODULES.COMPACTOR,
80
+ version: "0.1.0",
81
+ commands: Object.values(COMPACTOR_COMMANDS),
82
+ tools: Object.values(COMPACTOR_TOOLS),
83
+ });
84
+
85
+ if (config.fts5Index.mode === "auto" && contentStore) {
86
+ // TODO: index project files
87
+ }
88
+
89
+ ctx.ui.notify("🗜️ Compactor ready", "info");
90
+ });
91
+
92
+ pi.on("before_agent_start", async (_event, ctx) => {
93
+ config = loadConfig();
94
+ currentSessionId = `${(ctx as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
95
+
96
+ // Re-cache normalized blocks for vcc_recall
97
+ try {
98
+ const messages = (ctx as any).messages ?? [];
99
+ if (messages.length > 0) {
100
+ const normalized = normalizeMessages(messages);
101
+ cachedBlocks = filterNoise(normalized);
102
+ }
103
+ } catch {
104
+ // Non-fatal: recall will work on empty blocks
105
+ }
106
+
107
+ if (sessionDB) {
108
+ const snapshot = await injectResumeSnapshot(sessionDB, currentSessionId);
109
+ if (snapshot) {
110
+ // Snapshot injected as context
111
+ }
112
+ }
113
+ });
114
+
115
+ pi.on("session_before_compact", async (event, _ctx) => {
116
+ if (sessionDB) {
117
+ const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
118
+ const events = sessionDB.getEvents(sessionId, { limit: 1000 });
119
+ const stats = sessionDB.getSessionStats(sessionId);
120
+ const { buildResumeSnapshot } = await import("./session/snapshot.js");
121
+ const snapshot = buildResumeSnapshot(events, {
122
+ compactCount: stats?.compact_count ?? 1,
123
+ });
124
+ sessionDB.upsertResume(sessionId, snapshot, events.length);
125
+ }
126
+ });
127
+
128
+ pi.on("session_compact", async (event, _ctx) => {
129
+ if (sessionDB) {
130
+ const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
131
+ sessionDB.incrementCompactCount(sessionId);
132
+ }
133
+ });
134
+
135
+ pi.on("session_shutdown", async (_event, _ctx) => {
136
+ if (sessionDB) {
137
+ sessionDB.cleanupOldSessions(7);
138
+ }
139
+ executor?.cleanupBackgrounded();
140
+ contentStore?.close();
141
+ sessionDB?.close();
142
+ });
143
+
144
+ pi.on("input", async (event, _ctx) => {
145
+ const toolName = (event as any).toolName ?? "";
146
+ const args = (event as any).args ?? {};
147
+ if (toolName === "bash" || toolName === "Bash") {
148
+ const cmd = String(args.command ?? "");
149
+ if (/\b(curl|wget|nc|netcat)\b/.test(cmd)) {
150
+ return { cancel: true } as any;
151
+ }
152
+ }
153
+ return undefined;
154
+ });
155
+
156
+ pi.on("tool_result", async (event, _ctx) => {
157
+ if (!sessionDB) return;
158
+ const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
159
+
160
+ // Extract and store session events
161
+ const toolEvents = extractEventsFromToolResult({
162
+ toolName: (event as any).toolName ?? "",
163
+ toolInput: (event as any).input ?? {},
164
+ toolResponse: (event as any).content ? JSON.stringify((event as any).content).slice(0, 1000) : undefined,
165
+ isError: (event as any).isError ?? false,
166
+ });
167
+
168
+ for (const ev of toolEvents) {
169
+ sessionDB.insertEvent(sessionId, ev, "PostToolUse");
170
+ }
171
+
172
+ // Apply display overrides for built-in tools
173
+ const toolName = (event as any).toolName ?? "";
174
+ const td = config.toolDisplay;
175
+ const toolConfig = {
176
+ readOutputMode: td?.mode as any,
177
+ searchOutputMode: td?.mode as any,
178
+ bashOutputMode: td?.mode as any,
179
+ previewLines: 20,
180
+ bashCollapsedLines: 5,
181
+ showTruncationHints: true,
182
+ };
183
+ try {
184
+ const { applyToolDisplayOverride } = await import("./display/tool-overrides.js");
185
+ const override = applyToolDisplayOverride(toolName, event as any, toolConfig);
186
+ if (override !== undefined) {
187
+ return override as any;
188
+ }
189
+ } catch {
190
+ // Non-fatal: display override failed
191
+ }
192
+ });
193
+
194
+ pi.on("message_update", async (event, _ctx) => {
195
+ if ((event as any).message?.thinking) {
196
+ // Handled by display engine
197
+ }
198
+ });
199
+
200
+ pi.on("message_end", async (_event, _ctx) => {
201
+ // Thinking label persistence
202
+ });
203
+
204
+ pi.on("context", async (event, _ctx) => {
205
+ const { sanitizeThinkingArtifacts } = await import("./display/thinking-label.js");
206
+ const ctx = (event as any).context;
207
+ if (typeof ctx === "string") {
208
+ (event as any).context = sanitizeThinkingArtifacts(ctx);
209
+ }
210
+ });
211
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Info-screen integration for @pi-unipi/compactor
3
+ */
4
+
5
+ import type { SessionDB } from "./session/db.js";
6
+ import type { ContentStore } from "./store/index.js";
7
+ import { getLastCompactionStats } from "./compaction/hooks.js";
8
+
9
+ export interface InfoScreenData {
10
+ sessionEvents: { value: string; detail: string };
11
+ compactions: { value: string; detail: string };
12
+ tokensSaved: { value: string; detail: string };
13
+ compressionRatio: { value: string; detail: string };
14
+ indexedDocs: { value: string; detail: string };
15
+ sandboxExecutions: { value: string; detail: string };
16
+ searchQueries: { value: string; detail: string };
17
+ }
18
+
19
+ export async function getInfoScreenData(
20
+ sessionDB: SessionDB,
21
+ contentStore: ContentStore,
22
+ sessionId: string,
23
+ ): Promise<InfoScreenData> {
24
+ const stats = sessionDB.getSessionStats(sessionId);
25
+ const compactStats = getLastCompactionStats();
26
+ const storeStats = await contentStore.getStats();
27
+
28
+ return {
29
+ sessionEvents: {
30
+ value: String(stats?.event_count ?? 0),
31
+ detail: "Session events tracked",
32
+ },
33
+ compactions: {
34
+ value: String(stats?.compact_count ?? 0),
35
+ detail: compactStats ? `Last: ${compactStats.summarized} msgs` : "No compactions yet",
36
+ },
37
+ tokensSaved: {
38
+ value: compactStats ? `~${compactStats.keptTokensEst}` : "0",
39
+ detail: "Estimated tokens kept",
40
+ },
41
+ compressionRatio: {
42
+ value: compactStats && compactStats.summarized > 0
43
+ ? `${Math.round(compactStats.summarized / Math.max(compactStats.kept, 1))}:1`
44
+ : "N/A",
45
+ detail: "Compression ratio",
46
+ },
47
+ indexedDocs: {
48
+ value: String(storeStats.sources),
49
+ detail: `${storeStats.chunks} chunks indexed`,
50
+ },
51
+ sandboxExecutions: {
52
+ value: "0",
53
+ detail: "Sandbox runs this session",
54
+ },
55
+ searchQueries: {
56
+ value: "0",
57
+ detail: "Search queries this session",
58
+ },
59
+ };
60
+ }