@pi-unipi/unipi 0.1.12 → 0.1.13
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/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- package/packages/compactor/src/commands/index.ts +9 -9
- package/packages/compactor/src/compaction/hooks.ts +14 -5
- package/packages/compactor/src/index.ts +39 -11
- package/packages/compactor/src/session/db.ts +10 -9
- package/packages/compactor/src/store/db-base.ts +9 -12
- package/packages/compactor/src/store/index.ts +4 -1
- package/packages/compactor/src/tui/settings-overlay.ts +23 -8
- package/packages/subagents/src/__tests__/badge-generation.test.ts +56 -1
- package/packages/subagents/src/index.ts +18 -14
- package/packages/subagents/src/types.ts +11 -0
- package/packages/workflow/skills/brainstorm/SKILL.md +3 -3
package/package.json
CHANGED
|
@@ -104,6 +104,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
104
104
|
"unipi:badge-gen": "utility",
|
|
105
105
|
"unipi:badge-toggle": "utility",
|
|
106
106
|
"unipi:badge-settings": "utility",
|
|
107
|
+
"unipi:util-settings": "utility",
|
|
107
108
|
|
|
108
109
|
// ask-user (1 command)
|
|
109
110
|
"unipi:ask-user-settings": "ask-user",
|
|
@@ -193,6 +194,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
193
194
|
"unipi:badge-gen": "Generate session name via background agent",
|
|
194
195
|
"unipi:badge-toggle": "Configure badge settings (autoGen, badgeEnabled, agentTool)",
|
|
195
196
|
"unipi:badge-settings": "Configure badge settings via TUI overlay",
|
|
197
|
+
"unipi:util-settings": "Unified settings — badge + diff rendering config",
|
|
196
198
|
"unipi:kanboard": "Start the kanboard visualization server",
|
|
197
199
|
"unipi:kanboard-doctor": "Diagnose and fix kanboard parser issues",
|
|
198
200
|
|
|
@@ -27,7 +27,7 @@ export interface CommandDeps {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
30
|
-
pi.registerCommand("compact", {
|
|
30
|
+
pi.registerCommand("unipi:compact", {
|
|
31
31
|
description: "Trigger manual compaction with stats",
|
|
32
32
|
handler: async (_args: string, ctx: any) => {
|
|
33
33
|
const result = compactTool();
|
|
@@ -39,7 +39,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
pi.registerCommand("compact-recall", {
|
|
42
|
+
pi.registerCommand("unipi:compact-recall", {
|
|
43
43
|
description: "Search session history (BM25 or regex)",
|
|
44
44
|
handler: async (args: string, ctx: any) => {
|
|
45
45
|
const query = args.trim();
|
|
@@ -64,7 +64,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
pi.registerCommand("compact-stats", {
|
|
67
|
+
pi.registerCommand("unipi:compact-stats", {
|
|
68
68
|
description: "Show context savings dashboard",
|
|
69
69
|
handler: async (_args: string, ctx: any) => {
|
|
70
70
|
if (!deps?.sessionDB || !deps?.contentStore) {
|
|
@@ -89,7 +89,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
pi.registerCommand("compact-doctor", {
|
|
92
|
+
pi.registerCommand("unipi:compact-doctor", {
|
|
93
93
|
description: "Run diagnostics checklist",
|
|
94
94
|
handler: async (_args: string, ctx: any) => {
|
|
95
95
|
if (!deps?.sessionDB || !deps?.contentStore) {
|
|
@@ -111,7 +111,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
pi.registerCommand("compact-settings", {
|
|
114
|
+
pi.registerCommand("unipi:compact-settings", {
|
|
115
115
|
description: "Open TUI settings overlay",
|
|
116
116
|
handler: async (_args: string, ctx: any) => {
|
|
117
117
|
try {
|
|
@@ -128,7 +128,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
128
128
|
},
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
pi.registerCommand("compact-preset", {
|
|
131
|
+
pi.registerCommand("unipi:compact-preset", {
|
|
132
132
|
description: "Apply quick preset (opencode/balanced/verbose/minimal)",
|
|
133
133
|
handler: async (args: string, ctx: any) => {
|
|
134
134
|
const presetName = parsePreset(args.trim());
|
|
@@ -146,7 +146,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
pi.registerCommand("compact-index", {
|
|
149
|
+
pi.registerCommand("unipi:compact-index", {
|
|
150
150
|
description: "Index current project files into FTS5",
|
|
151
151
|
handler: async (_args: string, ctx: any) => {
|
|
152
152
|
if (!deps?.contentStore) {
|
|
@@ -202,7 +202,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
202
202
|
},
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
pi.registerCommand("compact-search", {
|
|
205
|
+
pi.registerCommand("unipi:compact-search", {
|
|
206
206
|
description: "Search indexed content",
|
|
207
207
|
handler: async (args: string, ctx: any) => {
|
|
208
208
|
const query = args.trim();
|
|
@@ -230,7 +230,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
230
230
|
},
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
-
pi.registerCommand("compact-purge", {
|
|
233
|
+
pi.registerCommand("unipi:compact-purge", {
|
|
234
234
|
description: "Wipe all indexed content from FTS5",
|
|
235
235
|
handler: async (_args: string, ctx: any) => {
|
|
236
236
|
if (!deps?.contentStore) {
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { convertToLlm } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
import { writeFileSync } from "node:fs";
|
|
8
7
|
import { compile } from "./summarize.js";
|
|
9
8
|
import { loadConfig } from "../config/manager.js";
|
|
10
9
|
import { buildOwnCut } from "./cut.js";
|
|
@@ -21,9 +20,11 @@ const formatTokens = (n: number): string => {
|
|
|
21
20
|
return String(n);
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
const dbg = (debug: boolean,
|
|
23
|
+
const dbg = (debug: boolean, event: string, data?: Record<string, unknown>) => {
|
|
25
24
|
if (!debug) return;
|
|
26
|
-
|
|
25
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
26
|
+
const details = data ? " " + JSON.stringify(data) : "";
|
|
27
|
+
console.error(`[compactor:${ts}] ${event}${details}`);
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const previewContent = (content: unknown): string => {
|
|
@@ -53,11 +54,16 @@ export function registerCompactionHooks(pi: ExtensionAPI): void {
|
|
|
53
54
|
pi.on("session_before_compact", (event, ctx) => {
|
|
54
55
|
const { preparation, branchEntries, customInstructions } = event;
|
|
55
56
|
const config = loadConfig();
|
|
57
|
+
dbg(config.debug, "session_before_compact:enter", { entryCount: (branchEntries as any[])?.length, hasPrevSummary: !!preparation?.previousSummary, isCompactor: customInstructions === COMPACTOR_INSTRUCTION });
|
|
56
58
|
|
|
57
59
|
const isCompactor = customInstructions === COMPACTOR_INSTRUCTION;
|
|
58
|
-
if (!isCompactor && !config.overrideDefaultCompaction)
|
|
60
|
+
if (!isCompactor && !config.overrideDefaultCompaction) {
|
|
61
|
+
dbg(config.debug, "session_before_compact:skip", { reason: "not_compactor_and_no_override" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
59
64
|
|
|
60
65
|
const ownCut = buildOwnCut(branchEntries as any[]);
|
|
66
|
+
dbg(config.debug, "buildOwnCut", { ok: ownCut.ok, reason: !ownCut.ok ? (ownCut as any).reason : undefined });
|
|
61
67
|
if (!ownCut.ok) {
|
|
62
68
|
try {
|
|
63
69
|
ctx?.ui?.notify?.(REASON_MESSAGES[ownCut.reason], "warning");
|
|
@@ -90,6 +96,7 @@ export function registerCompactionHooks(pi: ExtensionAPI): void {
|
|
|
90
96
|
keptTokensEst: Math.round(keptChars / 4),
|
|
91
97
|
};
|
|
92
98
|
|
|
99
|
+
dbg(config.debug, "compile", { messageCount: messages.length, hasPrevSummary: !!preparation.previousSummary });
|
|
93
100
|
const summary = compile({
|
|
94
101
|
messages,
|
|
95
102
|
previousSummary: preparation.previousSummary,
|
|
@@ -99,7 +106,7 @@ export function registerCompactionHooks(pi: ExtensionAPI): void {
|
|
|
99
106
|
},
|
|
100
107
|
});
|
|
101
108
|
|
|
102
|
-
dbg(config.debug, {
|
|
109
|
+
dbg(config.debug, "compaction_pipeline", {
|
|
103
110
|
usedOwnCut: true,
|
|
104
111
|
messagesToSummarize: agentMessages.length,
|
|
105
112
|
firstKeptEntryId,
|
|
@@ -129,6 +136,8 @@ export function registerCompactionHooks(pi: ExtensionAPI): void {
|
|
|
129
136
|
});
|
|
130
137
|
|
|
131
138
|
pi.on("session_compact", (event, ctx) => {
|
|
139
|
+
const config = loadConfig();
|
|
140
|
+
dbg(config.debug, "session_compact", { fromExtension: event.fromExtension, lastCompactWasCompactor });
|
|
132
141
|
if (!event.fromExtension) return;
|
|
133
142
|
if (lastCompactWasCompactor) return;
|
|
134
143
|
const stats = lastStats;
|
|
@@ -17,6 +17,16 @@ import { normalizeMessages } from "./compaction/normalize.js";
|
|
|
17
17
|
import { filterNoise } from "./compaction/filter-noise.js";
|
|
18
18
|
import type { NormalizedBlock } from "./types.js";
|
|
19
19
|
|
|
20
|
+
/** Debug logger — only logs when config.debug === true */
|
|
21
|
+
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}`);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export default function compactorExtension(pi: ExtensionAPI): void {
|
|
21
31
|
let sessionDB: SessionDB | null = null;
|
|
22
32
|
let contentStore: ContentStore | null = null;
|
|
@@ -25,6 +35,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
25
35
|
let cachedBlocks: NormalizedBlock[] = [];
|
|
26
36
|
let currentSessionId = "default";
|
|
27
37
|
|
|
38
|
+
const debug = createDebugLogger(() => config);
|
|
39
|
+
|
|
28
40
|
const init = async () => {
|
|
29
41
|
scaffoldConfig();
|
|
30
42
|
config = loadConfig();
|
|
@@ -42,14 +54,13 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
42
54
|
|
|
43
55
|
registerCompactionHooks(pi);
|
|
44
56
|
|
|
45
|
-
//
|
|
57
|
+
// Commands will be registered inside session_start when deps are ready
|
|
46
58
|
const getCommandDeps = () => ({
|
|
47
59
|
sessionDB,
|
|
48
60
|
contentStore,
|
|
49
61
|
getSessionId: () => currentSessionId,
|
|
50
62
|
getBlocks: () => cachedBlocks,
|
|
51
63
|
});
|
|
52
|
-
registerCommands(pi, getCommandDeps());
|
|
53
64
|
|
|
54
65
|
pi.on("session_start", async (_event, ctx) => {
|
|
55
66
|
await init();
|
|
@@ -60,6 +71,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
60
71
|
const fullSessionId = `${sessionId}${suffix}`;
|
|
61
72
|
currentSessionId = fullSessionId;
|
|
62
73
|
|
|
74
|
+
debug("session_start", { sessionId: fullSessionId, projectDir });
|
|
75
|
+
|
|
63
76
|
sessionDB?.ensureSession(fullSessionId, projectDir);
|
|
64
77
|
|
|
65
78
|
// Register all compactor tools with Pi
|
|
@@ -82,6 +95,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
82
95
|
tools: Object.values(COMPACTOR_TOOLS),
|
|
83
96
|
});
|
|
84
97
|
|
|
98
|
+
debug("MODULE_READY", { commands: Object.values(COMPACTOR_COMMANDS), tools: Object.values(COMPACTOR_TOOLS) });
|
|
99
|
+
|
|
85
100
|
if (config.fts5Index.mode === "auto" && contentStore) {
|
|
86
101
|
// TODO: index project files
|
|
87
102
|
}
|
|
@@ -92,6 +107,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
92
107
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
93
108
|
config = loadConfig();
|
|
94
109
|
currentSessionId = `${(ctx as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
110
|
+
debug("before_agent_start", { sessionId: currentSessionId, configDebug: config.debug });
|
|
95
111
|
|
|
96
112
|
// Re-cache normalized blocks for vcc_recall
|
|
97
113
|
try {
|
|
@@ -106,9 +122,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
106
122
|
|
|
107
123
|
if (sessionDB) {
|
|
108
124
|
const snapshot = await injectResumeSnapshot(sessionDB, currentSessionId);
|
|
109
|
-
|
|
110
|
-
// Snapshot injected as context
|
|
111
|
-
}
|
|
125
|
+
debug("resume_snapshot", { injected: !!snapshot });
|
|
112
126
|
}
|
|
113
127
|
});
|
|
114
128
|
|
|
@@ -117,6 +131,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
117
131
|
const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
118
132
|
const events = sessionDB.getEvents(sessionId, { limit: 1000 });
|
|
119
133
|
const stats = sessionDB.getSessionStats(sessionId);
|
|
134
|
+
debug("session_before_compact", { sessionId, eventCount: events.length, compactCount: stats?.compact_count ?? 0 });
|
|
120
135
|
const { buildResumeSnapshot } = await import("./session/snapshot.js");
|
|
121
136
|
const snapshot = buildResumeSnapshot(events, {
|
|
122
137
|
compactCount: stats?.compact_count ?? 1,
|
|
@@ -129,10 +144,12 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
129
144
|
if (sessionDB) {
|
|
130
145
|
const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
131
146
|
sessionDB.incrementCompactCount(sessionId);
|
|
147
|
+
debug("session_compact", { sessionId });
|
|
132
148
|
}
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
152
|
+
debug("session_shutdown");
|
|
136
153
|
if (sessionDB) {
|
|
137
154
|
sessionDB.cleanupOldSessions(7);
|
|
138
155
|
}
|
|
@@ -144,6 +161,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
144
161
|
pi.on("input", async (event, _ctx) => {
|
|
145
162
|
const toolName = (event as any).toolName ?? "";
|
|
146
163
|
const args = (event as any).args ?? {};
|
|
164
|
+
debug("input", { toolName, args: JSON.stringify(args).slice(0, 200) });
|
|
147
165
|
if (toolName === "bash" || toolName === "Bash") {
|
|
148
166
|
const cmd = String(args.command ?? "");
|
|
149
167
|
if (/\b(curl|wget|nc|netcat)\b/.test(cmd)) {
|
|
@@ -156,6 +174,10 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
156
174
|
pi.on("tool_result", async (event, _ctx) => {
|
|
157
175
|
if (!sessionDB) return;
|
|
158
176
|
const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
177
|
+
const toolNameRaw = (event as any).toolName ?? "";
|
|
178
|
+
const isError = (event as any).isError ?? false;
|
|
179
|
+
|
|
180
|
+
debug("tool_result", { toolName: toolNameRaw, isError, sessionId });
|
|
159
181
|
|
|
160
182
|
// Extract and store session events
|
|
161
183
|
const toolEvents = extractEventsFromToolResult({
|
|
@@ -167,6 +189,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
167
189
|
|
|
168
190
|
for (const ev of toolEvents) {
|
|
169
191
|
sessionDB.insertEvent(sessionId, ev, "PostToolUse");
|
|
192
|
+
debug("event_stored", { category: ev.category, type: ev.type });
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
// Apply display overrides for built-in tools
|
|
@@ -192,20 +215,25 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
192
215
|
});
|
|
193
216
|
|
|
194
217
|
pi.on("message_update", async (event, _ctx) => {
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
const msg = (event as any).message;
|
|
219
|
+
if (msg?.thinking) {
|
|
220
|
+
debug("message_update", { thinking: true, length: String(msg.thinking).length });
|
|
197
221
|
}
|
|
198
222
|
});
|
|
199
223
|
|
|
200
224
|
pi.on("message_end", async (_event, _ctx) => {
|
|
201
|
-
|
|
225
|
+
debug("message_end");
|
|
202
226
|
});
|
|
203
227
|
|
|
204
228
|
pi.on("context", async (event, _ctx) => {
|
|
205
229
|
const { sanitizeThinkingArtifacts } = await import("./display/thinking-label.js");
|
|
206
|
-
const
|
|
207
|
-
if (typeof
|
|
208
|
-
|
|
230
|
+
const ctxStr = (event as any).context;
|
|
231
|
+
if (typeof ctxStr === "string") {
|
|
232
|
+
const sanitized = sanitizeThinkingArtifacts(ctxStr);
|
|
233
|
+
if (sanitized !== ctxStr) {
|
|
234
|
+
debug("context", { sanitized: true, beforeLen: ctxStr.length, afterLen: sanitized.length });
|
|
235
|
+
}
|
|
236
|
+
(event as any).context = sanitized;
|
|
209
237
|
}
|
|
210
238
|
});
|
|
211
239
|
}
|
|
@@ -44,17 +44,16 @@ let sqliteLib: any = null;
|
|
|
44
44
|
|
|
45
45
|
async function getSQLite() {
|
|
46
46
|
if (sqliteLib) return sqliteLib;
|
|
47
|
+
// Try bun:sqlite first (Bun runtime)
|
|
47
48
|
try {
|
|
48
49
|
sqliteLib = await import("bun:sqlite" as any);
|
|
49
50
|
return sqliteLib;
|
|
50
51
|
} catch {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return sqliteLib;
|
|
57
|
-
}
|
|
52
|
+
// Skip node:sqlite — its API (DatabaseSync) is incompatible with
|
|
53
|
+
// better-sqlite3's constructor pattern used by SessionDB.
|
|
54
|
+
// Go straight to better-sqlite3 which has the expected shape.
|
|
55
|
+
sqliteLib = await import("better-sqlite3");
|
|
56
|
+
return sqliteLib;
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -78,8 +77,10 @@ export class SessionDB {
|
|
|
78
77
|
|
|
79
78
|
async init(): Promise<void> {
|
|
80
79
|
const sqlite: any = await getSQLite();
|
|
81
|
-
// Handle different SQLite API shapes
|
|
82
|
-
|
|
80
|
+
// Handle different SQLite API shapes:
|
|
81
|
+
// - bun:sqlite exports Database as a named export
|
|
82
|
+
// - better-sqlite3 (CJS) exports the constructor as default when imported via ESM
|
|
83
|
+
const Database = sqlite.Database ?? sqlite.default?.Database ?? sqlite.default ?? sqlite;
|
|
83
84
|
this.db = new Database(this.dbPath);
|
|
84
85
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
85
86
|
this.initSchema();
|
|
@@ -14,30 +14,27 @@ export function defaultDBPath(name: string): string {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
let sqliteLib: any = null;
|
|
17
|
-
let sqliteFlavor: "bun" | "
|
|
17
|
+
let sqliteFlavor: "bun" | "better-sqlite3" | null = null;
|
|
18
18
|
|
|
19
19
|
export async function loadSQLite() {
|
|
20
20
|
if (sqliteLib) return { lib: sqliteLib, flavor: sqliteFlavor! };
|
|
21
21
|
|
|
22
|
+
// Try bun:sqlite first (Bun runtime)
|
|
22
23
|
try {
|
|
23
24
|
sqliteLib = await import("bun:sqlite" as any);
|
|
24
25
|
sqliteFlavor = "bun";
|
|
25
26
|
return { lib: sqliteLib, flavor: sqliteFlavor };
|
|
26
27
|
} catch {
|
|
28
|
+
// Skip node:sqlite — its API (DatabaseSync) is incompatible with
|
|
29
|
+
// better-sqlite3's constructor pattern (Database class).
|
|
27
30
|
try {
|
|
28
|
-
sqliteLib = await import("
|
|
29
|
-
sqliteFlavor = "
|
|
31
|
+
sqliteLib = await import("better-sqlite3");
|
|
32
|
+
sqliteFlavor = "better-sqlite3";
|
|
30
33
|
return { lib: sqliteLib, flavor: sqliteFlavor };
|
|
31
34
|
} catch {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { lib: sqliteLib, flavor: sqliteFlavor };
|
|
36
|
-
} catch {
|
|
37
|
-
sqliteLib = {};
|
|
38
|
-
sqliteFlavor = "better-sqlite3";
|
|
39
|
-
return { lib: sqliteLib, flavor: sqliteFlavor };
|
|
40
|
-
}
|
|
35
|
+
sqliteLib = {};
|
|
36
|
+
sqliteFlavor = "better-sqlite3";
|
|
37
|
+
return { lib: sqliteLib, flavor: sqliteFlavor };
|
|
41
38
|
}
|
|
42
39
|
}
|
|
43
40
|
}
|
|
@@ -183,7 +183,10 @@ export class ContentStore {
|
|
|
183
183
|
|
|
184
184
|
async init(): Promise<void> {
|
|
185
185
|
const { lib } = await loadSQLite();
|
|
186
|
-
|
|
186
|
+
// Handle different SQLite API shapes:
|
|
187
|
+
// - bun:sqlite exports Database as a named export
|
|
188
|
+
// - better-sqlite3 (CJS) exports the constructor as default when imported via ESM
|
|
189
|
+
const Database = lib.Database ?? lib.default?.Database ?? lib.default ?? lib;
|
|
187
190
|
this.db = new Database(this.dbPath);
|
|
188
191
|
applyWALPragmas(this.db);
|
|
189
192
|
this.initSchema();
|
|
@@ -40,6 +40,18 @@ interface StrategyItem {
|
|
|
40
40
|
setMode: (c: CompactorConfig, v: string) => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Top-level debug toggle that mirrors config.debug */
|
|
44
|
+
const GLOBAL_DEBUG: StrategyItem = {
|
|
45
|
+
key: "debug",
|
|
46
|
+
label: "Verbose Debug",
|
|
47
|
+
description: "Log ALL compaction events to console",
|
|
48
|
+
modes: ["on", "off"],
|
|
49
|
+
getEnabled: (c) => c.debug,
|
|
50
|
+
setEnabled: (c, v) => (c.debug = v),
|
|
51
|
+
getMode: (c) => (c.debug ? "on" : "off"),
|
|
52
|
+
setMode: (c, v) => (c.debug = v === "on"),
|
|
53
|
+
};
|
|
54
|
+
|
|
43
55
|
/** All configurable strategies */
|
|
44
56
|
const STRATEGIES: StrategyItem[] = [
|
|
45
57
|
{
|
|
@@ -144,6 +156,9 @@ const STRATEGIES: StrategyItem[] = [
|
|
|
144
156
|
},
|
|
145
157
|
];
|
|
146
158
|
|
|
159
|
+
/** All navigable items: debug toggle first, then strategies */
|
|
160
|
+
const ALL_ITEMS: StrategyItem[] = [GLOBAL_DEBUG, ...STRATEGIES];
|
|
161
|
+
|
|
147
162
|
const PRESETS: CompactorPreset[] = ["opencode", "balanced", "verbose", "minimal"];
|
|
148
163
|
|
|
149
164
|
/**
|
|
@@ -170,7 +185,7 @@ export class CompactorSettingsOverlay implements Component {
|
|
|
170
185
|
case "\x1b[A": // Up
|
|
171
186
|
case "k":
|
|
172
187
|
if (this.mode === "strategy") {
|
|
173
|
-
this.selectedIndex = (this.selectedIndex - 1 +
|
|
188
|
+
this.selectedIndex = (this.selectedIndex - 1 + ALL_ITEMS.length) % ALL_ITEMS.length;
|
|
174
189
|
} else {
|
|
175
190
|
this.presetIndex = (this.presetIndex - 1 + PRESETS.length) % PRESETS.length;
|
|
176
191
|
}
|
|
@@ -178,21 +193,21 @@ export class CompactorSettingsOverlay implements Component {
|
|
|
178
193
|
case "\x1b[B": // Down
|
|
179
194
|
case "j":
|
|
180
195
|
if (this.mode === "strategy") {
|
|
181
|
-
this.selectedIndex = (this.selectedIndex + 1) %
|
|
196
|
+
this.selectedIndex = (this.selectedIndex + 1) % ALL_ITEMS.length;
|
|
182
197
|
} else {
|
|
183
198
|
this.presetIndex = (this.presetIndex + 1) % PRESETS.length;
|
|
184
199
|
}
|
|
185
200
|
break;
|
|
186
201
|
case " ": // Space - toggle enabled
|
|
187
202
|
if (this.mode === "strategy") {
|
|
188
|
-
const item =
|
|
203
|
+
const item = ALL_ITEMS[this.selectedIndex];
|
|
189
204
|
item.setEnabled(this.config, !item.getEnabled(this.config));
|
|
190
205
|
}
|
|
191
206
|
break;
|
|
192
207
|
case "\x1b[C": // Right - cycle mode forward
|
|
193
208
|
case "\r": // Enter
|
|
194
209
|
if (this.mode === "strategy") {
|
|
195
|
-
const strat =
|
|
210
|
+
const strat = ALL_ITEMS[this.selectedIndex];
|
|
196
211
|
const modes = strat.modes;
|
|
197
212
|
const currentIdx = modes.indexOf(strat.getMode(this.config));
|
|
198
213
|
const nextIdx = (currentIdx + 1) % modes.length;
|
|
@@ -205,7 +220,7 @@ export class CompactorSettingsOverlay implements Component {
|
|
|
205
220
|
break;
|
|
206
221
|
case "\x1b[D": // Left - cycle mode backward
|
|
207
222
|
if (this.mode === "strategy") {
|
|
208
|
-
const strat2 =
|
|
223
|
+
const strat2 = ALL_ITEMS[this.selectedIndex];
|
|
209
224
|
const modes2 = strat2.modes;
|
|
210
225
|
const curIdx = modes2.indexOf(strat2.getMode(this.config));
|
|
211
226
|
const prevIdx = (curIdx - 1 + modes2.length) % modes2.length;
|
|
@@ -248,9 +263,9 @@ export class CompactorSettingsOverlay implements Component {
|
|
|
248
263
|
add("");
|
|
249
264
|
add(`${ansi.dim}↑↓ navigate • Enter apply • p back to strategies • s save • Esc cancel${ansi.reset}`);
|
|
250
265
|
} else {
|
|
251
|
-
// Strategy list
|
|
252
|
-
for (let i = 0; i <
|
|
253
|
-
const item =
|
|
266
|
+
// Strategy list (GLOBAL_DEBUG at top, then all strategies)
|
|
267
|
+
for (let i = 0; i < ALL_ITEMS.length; i++) {
|
|
268
|
+
const item = ALL_ITEMS[i];
|
|
254
269
|
const isSelected = i === this.selectedIndex;
|
|
255
270
|
const enabled = item.getEnabled(this.config);
|
|
256
271
|
const mode = item.getMode(this.config);
|
|
@@ -60,9 +60,14 @@ describe("Badge generation — tool availability", () => {
|
|
|
60
60
|
// ─── Test: Prompt no longer references non-existent tool ───────────
|
|
61
61
|
|
|
62
62
|
describe("Badge generation — prompt fix", () => {
|
|
63
|
-
it("prompt
|
|
63
|
+
it("prompt includes conversation context inline", () => {
|
|
64
64
|
const src = readSource("packages/subagents/src/index.ts");
|
|
65
65
|
|
|
66
|
+
assert.ok(
|
|
67
|
+
src.includes("Conversation:"),
|
|
68
|
+
"Prompt should include conversation context inline",
|
|
69
|
+
);
|
|
70
|
+
|
|
66
71
|
assert.ok(
|
|
67
72
|
src.includes("Reply with ONLY the title"),
|
|
68
73
|
"Prompt should ask agent to reply with only the title",
|
|
@@ -93,6 +98,56 @@ describe("Badge generation — onComplete callback", () => {
|
|
|
93
98
|
});
|
|
94
99
|
});
|
|
95
100
|
|
|
101
|
+
// ─── Test: Agent configuration ────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("Badge generation — agent configuration", () => {
|
|
104
|
+
it("badge generation uses 'name-gen' agent type (not 'explore')", () => {
|
|
105
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
106
|
+
|
|
107
|
+
assert.ok(
|
|
108
|
+
src.includes('manager.spawn(pi, sessionCtx, "name-gen", prompt'),
|
|
109
|
+
"Badge generation should spawn a 'name-gen' agent",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
assert.ok(
|
|
113
|
+
!src.includes('manager.spawn(pi, sessionCtx, "explore"'),
|
|
114
|
+
"Should NOT use 'explore' — that type can be overridden by user custom agents",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("'name-gen' type is defined in BUILTIN_CONFIGS with empty tools", () => {
|
|
119
|
+
const src = readSource("packages/subagents/src/types.ts");
|
|
120
|
+
|
|
121
|
+
assert.ok(
|
|
122
|
+
src.includes('"name-gen"'),
|
|
123
|
+
"types.ts should define name-gen agent config",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
assert.ok(
|
|
127
|
+
src.includes('builtinToolNames: []'),
|
|
128
|
+
"name-gen should have empty tool list",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("background agent is isolated (no extensions, no skills, minimal system prompt)", () => {
|
|
133
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
134
|
+
|
|
135
|
+
assert.ok(
|
|
136
|
+
src.includes("isolated: true"),
|
|
137
|
+
"Should spawn with isolated: true to avoid loading extensions/skills",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("background agent maxTurns is 1 (single response)", () => {
|
|
142
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
143
|
+
|
|
144
|
+
assert.ok(
|
|
145
|
+
src.includes("maxTurns: 1"),
|
|
146
|
+
"Badge generation agent should have maxTurns: 1",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
96
151
|
// ─── Test: Cross-module event bus — the critical fix ───────────────
|
|
97
152
|
|
|
98
153
|
describe("Badge generation — event bus (CRITICAL FIX)", () => {
|
|
@@ -148,6 +148,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
148
148
|
// Build notification details
|
|
149
149
|
const details = buildNotificationDetails(record, agentActivity.get(record.id));
|
|
150
150
|
|
|
151
|
+
// Badge generation: extract name from agent result and set directly.
|
|
152
|
+
// Mark resultConsumed BEFORE the notification check so the main agent
|
|
153
|
+
// never sees this subagent.
|
|
154
|
+
if (record.description === "Generate session name" && record.result && record.status === "completed") {
|
|
155
|
+
const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
|
|
156
|
+
if (name && !name.startsWith("Error") && !name.includes("error")) {
|
|
157
|
+
try {
|
|
158
|
+
pi.setSessionName(name);
|
|
159
|
+
} catch { /* best effort */ }
|
|
160
|
+
}
|
|
161
|
+
record.resultConsumed = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
151
164
|
// Send styled notification via message renderer
|
|
152
165
|
const status = getStatusLabel(record.status, record.error);
|
|
153
166
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
@@ -179,16 +192,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
192
|
);
|
|
180
193
|
}
|
|
181
194
|
|
|
182
|
-
// Badge generation: extract name from agent result and set directly
|
|
183
|
-
if (record.description === "Generate session name" && record.result && record.status === "completed") {
|
|
184
|
-
const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
|
|
185
|
-
if (name && !name.startsWith("Error") && !name.includes("error")) {
|
|
186
|
-
try {
|
|
187
|
-
pi.setSessionName(name);
|
|
188
|
-
} catch { /* best effort */ }
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
195
|
pi.events.emit("subagents:completed", {
|
|
193
196
|
id: record.id,
|
|
194
197
|
type: record.type,
|
|
@@ -354,8 +357,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
354
357
|
|
|
355
358
|
const summary = event?.conversationSummary ?? "";
|
|
356
359
|
const prompt = summary
|
|
357
|
-
? `
|
|
358
|
-
: `Generate a concise session title (MAX 5 WORDS) for
|
|
360
|
+
? `Based on this conversation, generate a concise session title (MAX 5 WORDS). Reply with ONLY the title. No quotes, no explanation, no punctuation.\n\nConversation:\n${summary}`
|
|
361
|
+
: `Generate a concise session title (MAX 5 WORDS) for this session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
|
|
359
362
|
|
|
360
363
|
// Try with configured model, fallback to inherit
|
|
361
364
|
let modelInput: string | undefined = undefined;
|
|
@@ -382,11 +385,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
382
385
|
// If result is a string (error), resolvedModel stays undefined → inherit parent
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
manager.spawn(pi, sessionCtx, "
|
|
388
|
+
manager.spawn(pi, sessionCtx, "name-gen", prompt, {
|
|
386
389
|
description: "Generate session name",
|
|
387
390
|
model: resolvedModel,
|
|
388
391
|
isBackground: true,
|
|
389
|
-
|
|
392
|
+
isolated: true,
|
|
393
|
+
maxTurns: 1,
|
|
390
394
|
});
|
|
391
395
|
});
|
|
392
396
|
|
|
@@ -44,6 +44,17 @@ export const BUILTIN_CONFIGS: Record<string, AgentConfig> = {
|
|
|
44
44
|
promptMode: "append",
|
|
45
45
|
source: "builtin",
|
|
46
46
|
},
|
|
47
|
+
"name-gen": {
|
|
48
|
+
name: "name-gen",
|
|
49
|
+
displayName: "Name Generator",
|
|
50
|
+
description: "Minimal agent for generating session names from conversation context.",
|
|
51
|
+
builtinToolNames: [],
|
|
52
|
+
extensions: false,
|
|
53
|
+
skills: false,
|
|
54
|
+
systemPrompt: "You are a session name generator. Generate concise titles from conversation context. Reply with ONLY the title.",
|
|
55
|
+
promptMode: "replace",
|
|
56
|
+
source: "builtin",
|
|
57
|
+
},
|
|
47
58
|
} as const;
|
|
48
59
|
|
|
49
60
|
/** Memory scope for persistent agent memory. */
|
|
@@ -61,7 +61,7 @@ Start with:
|
|
|
61
61
|
2. "Who has this problem and when?" — context changes solutions
|
|
62
62
|
3. "What does success look like?" — outcomes, not features
|
|
63
63
|
|
|
64
|
-
Prefer multiple choice when natural options exist. Validate assumptions explicitly.
|
|
64
|
+
Use `ask_user` for structured decisions (approach selection, scope tradeoffs) when the tool is available. If `ask_user` is not available, use conversational text with clearly numbered options (e.g., "1. Option A — ... 2. Option B — ... Please pick one."). Use conversational text for open-ended exploration. Prefer multiple choice when natural options exist. Validate assumptions explicitly.
|
|
65
65
|
|
|
66
66
|
**Exit:** Problem statement clear and reframed. Both agree on what solving.
|
|
67
67
|
|
|
@@ -76,7 +76,7 @@ Propose 2-3 different approaches with trade-offs:
|
|
|
76
76
|
|
|
77
77
|
Present conversationally with recommendation and reasoning.
|
|
78
78
|
|
|
79
|
-
**If open questions emerge:** MUST ask user about each one. Don't assume.
|
|
79
|
+
**If open questions emerge:** MUST ask user about each one. Don't assume. Use `ask_user` with clearly labeled options and descriptions for each approach when available; otherwise present numbered options conversationally.
|
|
80
80
|
|
|
81
81
|
**Exit:** Approach chosen. User signals decision.
|
|
82
82
|
|
|
@@ -86,7 +86,7 @@ Present conversationally with recommendation and reasoning.
|
|
|
86
86
|
|
|
87
87
|
Once approach chosen, present design in sections:
|
|
88
88
|
- Scale each section to complexity (few sentences if straightforward, 200-300 words if nuanced)
|
|
89
|
-
- Ask after each section whether it looks right
|
|
89
|
+
- Ask after each section whether it looks right (use `ask_user` for "approve / needs changes / go back" checkpoints if available; otherwise ask conversationally and wait for response)
|
|
90
90
|
- Cover: architecture, components, data flow, error handling, testing
|
|
91
91
|
- Be ready to go back and clarify
|
|
92
92
|
|