@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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, data: Record<string, unknown>) => {
23
+ const dbg = (debug: boolean, event: string, data?: Record<string, unknown>) => {
25
24
  if (!debug) return;
26
- try { writeFileSync("/tmp/compactor-debug.json", JSON.stringify(data, null, 2)); } catch {}
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) return;
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
- // Register commands with deps (they need sessionDB/contentStore)
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
- if (snapshot) {
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
- if ((event as any).message?.thinking) {
196
- // Handled by display engine
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
- // Thinking label persistence
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 ctx = (event as any).context;
207
- if (typeof ctx === "string") {
208
- (event as any).context = sanitizeThinkingArtifacts(ctx);
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
- try {
52
- sqliteLib = await import("node:sqlite" as any);
53
- return sqliteLib;
54
- } catch {
55
- sqliteLib = await import("better-sqlite3");
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
- const Database = sqlite.Database ?? sqlite.default?.Database ?? sqlite;
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" | "node" | "better-sqlite3" | null = null;
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("node:sqlite" as any);
29
- sqliteFlavor = "node";
31
+ sqliteLib = await import("better-sqlite3");
32
+ sqliteFlavor = "better-sqlite3";
30
33
  return { lib: sqliteLib, flavor: sqliteFlavor };
31
34
  } catch {
32
- try {
33
- sqliteLib = await import("better-sqlite3");
34
- sqliteFlavor = "better-sqlite3";
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
- const Database = lib.Database ?? lib.default?.Database ?? lib;
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 + STRATEGIES.length) % STRATEGIES.length;
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) % STRATEGIES.length;
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 = STRATEGIES[this.selectedIndex];
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 = STRATEGIES[this.selectedIndex];
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 = STRATEGIES[this.selectedIndex];
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 < STRATEGIES.length; i++) {
253
- const item = STRATEGIES[i];
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 asks agent to OUTPUT the title directly (not call a tool)", () => {
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
- ? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nReply with ONLY the title. No quotes, no explanation, no punctuation.`
358
- : `Generate a concise session title (MAX 5 WORDS) for the current session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
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, "explore", prompt, {
388
+ manager.spawn(pi, sessionCtx, "name-gen", prompt, {
386
389
  description: "Generate session name",
387
390
  model: resolvedModel,
388
391
  isBackground: true,
389
- maxTurns: 3,
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