@docyrus/docyrus 0.0.34 → 0.0.36

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 (66) hide show
  1. package/README.md +25 -0
  2. package/agent-loader.js +3 -2
  3. package/agent-loader.js.map +2 -2
  4. package/main.js +82162 -46093
  5. package/main.js.map +4 -4
  6. package/package.json +12 -3
  7. package/resources/chrome-tools/browser-content.js +46 -46
  8. package/resources/chrome-tools/browser-cookies.js +16 -16
  9. package/resources/chrome-tools/browser-eval.js +27 -27
  10. package/resources/chrome-tools/browser-hn-scraper.js +1 -1
  11. package/resources/chrome-tools/browser-nav.js +23 -23
  12. package/resources/chrome-tools/browser-pick.js +127 -127
  13. package/resources/chrome-tools/browser-screenshot.js +10 -10
  14. package/resources/chrome-tools/browser-start.js +38 -38
  15. package/resources/pi-agent/extensions/answer.ts +392 -384
  16. package/resources/pi-agent/extensions/context.ts +415 -415
  17. package/resources/pi-agent/extensions/control.ts +1287 -1287
  18. package/resources/pi-agent/extensions/diff.ts +171 -171
  19. package/resources/pi-agent/extensions/files.ts +155 -155
  20. package/resources/pi-agent/extensions/knowledge.ts +664 -0
  21. package/resources/pi-agent/extensions/loop.ts +375 -375
  22. package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
  23. package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
  24. package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
  25. package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
  26. package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
  27. package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
  28. package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
  29. package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
  30. package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
  54. package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
  55. package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
  56. package/resources/pi-agent/extensions/redraws.ts +14 -14
  57. package/resources/pi-agent/extensions/review.ts +1533 -1533
  58. package/resources/pi-agent/extensions/todos.ts +1735 -1735
  59. package/resources/pi-agent/extensions/tps.ts +40 -40
  60. package/resources/pi-agent/extensions/whimsical.ts +3 -3
  61. package/resources/pi-agent/prompts/agent-system.md +2 -0
  62. package/resources/pi-agent/prompts/coder-system.md +2 -0
  63. package/server-loader.js +82 -1
  64. package/server-loader.js.map +3 -3
  65. package/tui.mjs +2 -0
  66. package/tui.mjs.map +1 -1
@@ -33,133 +33,133 @@ const DEFAULT_MODE_ORDER = ["default"] as const;
33
33
  const CUSTOM_MODE_NAME = "custom" as const;
34
34
 
35
35
  function expandUserPath(p: string): string {
36
- if (p === "~") return os.homedir();
37
- if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
38
- return p;
36
+ if (p === "~") {return os.homedir();}
37
+ if (p.startsWith("~/")) {return path.join(os.homedir(), p.slice(2));}
38
+ return p;
39
39
  }
40
40
 
41
41
  function getGlobalAgentDir(): string {
42
42
  // Mirror pi-coding-agent's getAgentDir() behavior (best-effort).
43
43
  // For the canonical implementation see pi-mono/packages/coding-agent/src/config.ts
44
- const env = process.env.PI_CODING_AGENT_DIR;
45
- if (env) return expandUserPath(env);
46
- return path.join(os.homedir(), ".pi", "agent");
44
+ const env = process.env.PI_CODING_AGENT_DIR;
45
+ if (env) {return expandUserPath(env);}
46
+ return path.join(os.homedir(), ".pi", "agent");
47
47
  }
48
48
 
49
49
  function getGlobalModesPath(): string {
50
- return path.join(getGlobalAgentDir(), "modes.json");
50
+ return path.join(getGlobalAgentDir(), "modes.json");
51
51
  }
52
52
 
53
53
  function getProjectModesPath(cwd: string): string {
54
- return path.join(cwd, ".pi", "modes.json");
54
+ return path.join(cwd, ".pi", "modes.json");
55
55
  }
56
56
 
57
57
  async function fileExists(p: string): Promise<boolean> {
58
- try {
59
- await fs.stat(p);
60
- return true;
61
- } catch {
62
- return false;
63
- }
58
+ try {
59
+ await fs.stat(p);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
64
  }
65
65
 
66
66
  async function ensureDirForFile(filePath: string): Promise<void> {
67
- await fs.mkdir(path.dirname(filePath), { recursive: true });
67
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
68
68
  }
69
69
 
70
70
  async function getMtimeMs(p: string): Promise<number | null> {
71
- try {
72
- const st = await fs.stat(p);
73
- return st.mtimeMs;
74
- } catch {
75
- return null;
76
- }
71
+ try {
72
+ const st = await fs.stat(p);
73
+ return st.mtimeMs;
74
+ } catch {
75
+ return null;
76
+ }
77
77
  }
78
78
 
79
79
  function sleep(ms: number): Promise<void> {
80
- return new Promise((resolve) => setTimeout(resolve, ms));
80
+ return new Promise((resolve) => setTimeout(resolve, ms));
81
81
  }
82
82
 
83
83
  function getLockPathForFile(filePath: string): string {
84
84
  // Lock file next to the json so it works across processes.
85
- return `${filePath}.lock`;
85
+ return `${filePath}.lock`;
86
86
  }
87
87
 
88
88
  async function withFileLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
89
- const lockPath = getLockPathForFile(filePath);
90
- await ensureDirForFile(lockPath);
91
-
92
- const start = Date.now();
93
- while (true) {
94
- try {
95
- const handle = await fs.open(lockPath, "wx");
96
- try {
89
+ const lockPath = getLockPathForFile(filePath);
90
+ await ensureDirForFile(lockPath);
91
+
92
+ const start = Date.now();
93
+ while (true) {
94
+ try {
95
+ const handle = await fs.open(lockPath, "wx");
96
+ try {
97
97
  // Best-effort metadata for debugging stale locks.
98
- await handle.writeFile(
99
- JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }) + "\n",
100
- "utf8"
101
- );
102
- } catch {
98
+ await handle.writeFile(
99
+ JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }) + "\n",
100
+ "utf8"
101
+ );
102
+ } catch {
103
103
  // ignore
104
- }
104
+ }
105
105
 
106
- try {
107
- return await fn();
108
- } finally {
109
- await handle.close().catch(() => {});
110
- await fs.unlink(lockPath).catch(() => {});
111
- }
112
- } catch (err: any) {
113
- if (err?.code !== "EEXIST") throw err;
106
+ try {
107
+ return await fn();
108
+ } finally {
109
+ await handle.close().catch(() => {});
110
+ await fs.unlink(lockPath).catch(() => {});
111
+ }
112
+ } catch (err: any) {
113
+ if (err?.code !== "EEXIST") {throw err;}
114
114
 
115
115
  // If the lock looks stale (crash), break it.
116
- try {
117
- const st = await fs.stat(lockPath);
118
- if (Date.now() - st.mtimeMs > 30_000) {
119
- await fs.unlink(lockPath);
120
- continue;
121
- }
122
- } catch {
116
+ try {
117
+ const st = await fs.stat(lockPath);
118
+ if (Date.now() - st.mtimeMs > 30_000) {
119
+ await fs.unlink(lockPath);
120
+ continue;
121
+ }
122
+ } catch {
123
123
  // ignore
124
- }
124
+ }
125
125
 
126
- if (Date.now() - start > 5_000) {
126
+ if (Date.now() - start > 5_000) {
127
127
  // Don't hang the UI forever.
128
- throw new Error(`Timed out waiting for lock: ${lockPath}`);
129
- }
130
- await sleep(40 + Math.random() * 80);
131
- }
132
- }
128
+ throw new Error(`Timed out waiting for lock: ${lockPath}`);
129
+ }
130
+ await sleep(40 + Math.random() * 80);
131
+ }
132
+ }
133
133
  }
134
134
 
135
135
  async function atomicWriteUtf8(filePath: string, content: string): Promise<void> {
136
- await ensureDirForFile(filePath);
136
+ await ensureDirForFile(filePath);
137
137
 
138
- const dir = path.dirname(filePath);
139
- const base = path.basename(filePath);
140
- const tmpPath = path.join(dir, `.${base}.tmp.${process.pid}.${Math.random().toString(16).slice(2)}`);
138
+ const dir = path.dirname(filePath);
139
+ const base = path.basename(filePath);
140
+ const tmpPath = path.join(dir, `.${base}.tmp.${process.pid}.${Math.random().toString(16).slice(2)}`);
141
141
 
142
- await fs.writeFile(tmpPath, content, "utf8");
142
+ await fs.writeFile(tmpPath, content, "utf8");
143
143
 
144
- try {
144
+ try {
145
145
  // POSIX: atomic replace.
146
- await fs.rename(tmpPath, filePath);
147
- } catch (err: any) {
146
+ await fs.rename(tmpPath, filePath);
147
+ } catch (err: any) {
148
148
  // Windows: rename can't overwrite.
149
- if (err?.code === "EEXIST" || err?.code === "EPERM") {
150
- await fs.unlink(filePath).catch(() => {});
151
- await fs.rename(tmpPath, filePath);
152
- } else {
149
+ if (err?.code === "EEXIST" || err?.code === "EPERM") {
150
+ await fs.unlink(filePath).catch(() => {});
151
+ await fs.rename(tmpPath, filePath);
152
+ } else {
153
153
  // best-effort cleanup
154
- await fs.unlink(tmpPath).catch(() => {});
155
- throw err;
156
- }
157
- }
154
+ await fs.unlink(tmpPath).catch(() => {});
155
+ throw err;
156
+ }
157
+ }
158
158
  }
159
159
 
160
160
  function cloneModesFile(file: ModesFile): ModesFile {
161
161
  // JSON-based clone is fine here (small, plain data structure).
162
- return JSON.parse(JSON.stringify(file)) as ModesFile;
162
+ return JSON.parse(JSON.stringify(file)) as ModesFile;
163
163
  }
164
164
 
165
165
  type ModeSpecPatch = {
@@ -175,243 +175,243 @@ type ModesPatch = {
175
175
  };
176
176
 
177
177
  function computeModesPatch(base: ModesFile, next: ModesFile, includeCurrentMode: boolean): ModesPatch | null {
178
- const patch: ModesPatch = {};
179
-
180
- if (includeCurrentMode && base.currentMode !== next.currentMode) {
181
- patch.currentMode = next.currentMode;
182
- }
183
-
184
- const keys = new Set([...Object.keys(base.modes), ...Object.keys(next.modes)]);
185
- const modesPatch: Record<ModeName, ModeSpecPatch | null> = {};
186
-
187
- for (const k of keys) {
188
- const a = base.modes[k];
189
- const b = next.modes[k];
190
-
191
- if (!b) {
192
- if (a) modesPatch[k] = null;
193
- continue;
194
- }
195
- if (!a) {
196
- modesPatch[k] = { ...b };
197
- continue;
198
- }
199
-
200
- const diff: ModeSpecPatch = {};
201
- const fields: (keyof ModeSpec)[] = ["provider", "modelId", "thinkingLevel", "color"];
202
- for (const f of fields) {
203
- const av = a[f];
204
- const bv = b[f];
205
- if (av !== bv) {
206
- (diff as any)[f] = bv === undefined ? null : bv;
207
- }
208
- }
209
- if (Object.keys(diff).length > 0) {
210
- modesPatch[k] = diff;
211
- }
212
- }
213
-
214
- if (Object.keys(modesPatch).length > 0) {
215
- patch.modes = modesPatch;
216
- }
217
-
218
- if (!patch.modes && patch.currentMode === undefined) return null;
219
- return patch;
178
+ const patch: ModesPatch = {};
179
+
180
+ if (includeCurrentMode && base.currentMode !== next.currentMode) {
181
+ patch.currentMode = next.currentMode;
182
+ }
183
+
184
+ const keys = new Set([...Object.keys(base.modes), ...Object.keys(next.modes)]);
185
+ const modesPatch: Record<ModeName, ModeSpecPatch | null> = {};
186
+
187
+ for (const k of keys) {
188
+ const a = base.modes[k];
189
+ const b = next.modes[k];
190
+
191
+ if (!b) {
192
+ if (a) {modesPatch[k] = null;}
193
+ continue;
194
+ }
195
+ if (!a) {
196
+ modesPatch[k] = { ...b };
197
+ continue;
198
+ }
199
+
200
+ const diff: ModeSpecPatch = {};
201
+ const fields: (keyof ModeSpec)[] = ["provider", "modelId", "thinkingLevel", "color"];
202
+ for (const f of fields) {
203
+ const av = a[f];
204
+ const bv = b[f];
205
+ if (av !== bv) {
206
+ (diff as any)[f] = bv === undefined ? null : bv;
207
+ }
208
+ }
209
+ if (Object.keys(diff).length > 0) {
210
+ modesPatch[k] = diff;
211
+ }
212
+ }
213
+
214
+ if (Object.keys(modesPatch).length > 0) {
215
+ patch.modes = modesPatch;
216
+ }
217
+
218
+ if (!patch.modes && patch.currentMode === undefined) {return null;}
219
+ return patch;
220
220
  }
221
221
 
222
222
  function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
223
- if (patch.currentMode !== undefined) {
224
- target.currentMode = patch.currentMode;
225
- }
226
-
227
- if (!patch.modes) return;
228
- for (const [mode, specPatch] of Object.entries(patch.modes)) {
229
- if (specPatch === null) {
230
- delete target.modes[mode];
231
- continue;
232
- }
233
-
234
- const targetSpec: Record<string, unknown> = ((target.modes[mode] ??= {}) as any) ?? {};
235
- for (const [k, v] of Object.entries(specPatch)) {
236
- if (v === null || v === undefined) {
237
- delete targetSpec[k];
238
- } else {
239
- targetSpec[k] = v;
240
- }
241
- }
242
- }
223
+ if (patch.currentMode !== undefined) {
224
+ target.currentMode = patch.currentMode;
225
+ }
226
+
227
+ if (!patch.modes) {return;}
228
+ for (const [mode, specPatch] of Object.entries(patch.modes)) {
229
+ if (specPatch === null) {
230
+ delete target.modes[mode];
231
+ continue;
232
+ }
233
+
234
+ const targetSpec: Record<string, unknown> = ((target.modes[mode] ??= {}) as any) ?? {};
235
+ for (const [k, v] of Object.entries(specPatch)) {
236
+ if (v === null || v === undefined) {
237
+ delete targetSpec[k];
238
+ } else {
239
+ targetSpec[k] = v;
240
+ }
241
+ }
242
+ }
243
243
  }
244
244
 
245
245
  function normalizeThinkingLevel(level: unknown): ThinkingLevel | undefined {
246
- if (typeof level !== "string") return undefined;
247
- const v = level as ThinkingLevel;
246
+ if (typeof level !== "string") {return undefined;}
247
+ const v = level as ThinkingLevel;
248
248
  // Keep the list local to avoid importing internal enums.
249
- const allowed: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
250
- return allowed.includes(v) ? v : undefined;
249
+ const allowed: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
250
+ return allowed.includes(v) ? v : undefined;
251
251
  }
252
252
 
253
253
  function sanitizeModeSpec(spec: unknown): ModeSpec {
254
- const obj = (spec && typeof spec === "object" ? spec : {}) as Record<string, unknown>;
255
- return {
256
- provider: typeof obj.provider === "string" ? obj.provider : undefined,
257
- modelId: typeof obj.modelId === "string" ? obj.modelId : undefined,
258
- thinkingLevel: normalizeThinkingLevel(obj.thinkingLevel),
259
- color: typeof obj.color === "string" ? obj.color : undefined,
260
- };
254
+ const obj = (spec && typeof spec === "object" ? spec : {}) as Record<string, unknown>;
255
+ return {
256
+ provider: typeof obj.provider === "string" ? obj.provider : undefined,
257
+ modelId: typeof obj.modelId === "string" ? obj.modelId : undefined,
258
+ thinkingLevel: normalizeThinkingLevel(obj.thinkingLevel),
259
+ color: typeof obj.color === "string" ? obj.color : undefined,
260
+ };
261
261
  }
262
262
 
263
263
  function createDefaultModes(ctx: ExtensionContext, pi: ExtensionAPI): ModesFile {
264
- const currentModel = ctx.model;
265
- const currentThinking = pi.getThinkingLevel();
266
-
267
- const base: ModeSpec = {
268
- provider: currentModel?.provider,
269
- modelId: currentModel?.id,
270
- thinkingLevel: currentThinking,
271
- };
272
-
273
- return {
274
- version: 1,
275
- currentMode: "default",
276
- modes: {
264
+ const currentModel = ctx.model;
265
+ const currentThinking = pi.getThinkingLevel();
266
+
267
+ const base: ModeSpec = {
268
+ provider: currentModel?.provider,
269
+ modelId: currentModel?.id,
270
+ thinkingLevel: currentThinking,
271
+ };
272
+
273
+ return {
274
+ version: 1,
275
+ currentMode: "default",
276
+ modes: {
277
277
  // Forced default mode
278
- default: { ...base },
278
+ default: { ...base },
279
279
  // Convenience mode (user can delete/rename)
280
- fast: { ...base, thinkingLevel: "off" },
281
- },
282
- };
280
+ fast: { ...base, thinkingLevel: "off" },
281
+ },
282
+ };
283
283
  }
284
284
 
285
285
  function ensureDefaultModeEntries(file: ModesFile, ctx: ExtensionContext, pi: ExtensionAPI): void {
286
- for (const name of DEFAULT_MODE_ORDER) {
287
- if (!file.modes[name]) {
288
- const defaults = createDefaultModes(ctx, pi);
289
- file.modes[name] = defaults.modes[name];
290
- }
291
- }
286
+ for (const name of DEFAULT_MODE_ORDER) {
287
+ if (!file.modes[name]) {
288
+ const defaults = createDefaultModes(ctx, pi);
289
+ file.modes[name] = defaults.modes[name];
290
+ }
291
+ }
292
292
 
293
293
  // "custom" is an overlay mode; never treat it as a valid persisted current mode.
294
- if (file.currentMode === CUSTOM_MODE_NAME) {
295
- file.currentMode = "" as any;
296
- }
297
-
298
- if (!file.currentMode || !(file.currentMode in file.modes) || file.currentMode === CUSTOM_MODE_NAME) {
299
- const first = Object.keys(file.modes).find((k) => k !== CUSTOM_MODE_NAME);
300
- file.currentMode = file.modes.default ? "default" : first || "default";
301
- }
294
+ if (file.currentMode === CUSTOM_MODE_NAME) {
295
+ file.currentMode = "" as any;
296
+ }
297
+
298
+ if (!file.currentMode || !(file.currentMode in file.modes) || file.currentMode === CUSTOM_MODE_NAME) {
299
+ const first = Object.keys(file.modes).find((k) => k !== CUSTOM_MODE_NAME);
300
+ file.currentMode = file.modes.default ? "default" : first || "default";
301
+ }
302
302
  }
303
303
 
304
304
  async function loadModesFile(filePath: string, ctx: ExtensionContext, pi: ExtensionAPI): Promise<ModesFile> {
305
- try {
306
- const raw = await fs.readFile(filePath, "utf8");
307
- const parsed = JSON.parse(raw) as Record<string, unknown>;
308
- const currentMode = typeof parsed.currentMode === "string" ? parsed.currentMode : "default";
309
- const modesRaw = parsed.modes && typeof parsed.modes === "object" ? (parsed.modes as Record<string, unknown>) : {};
310
- const modes: Record<string, ModeSpec> = {};
311
- for (const [k, v] of Object.entries(modesRaw)) {
312
- modes[k] = sanitizeModeSpec(v);
313
- }
314
- const file: ModesFile = {
315
- version: 1,
316
- currentMode,
317
- modes,
318
- };
319
- ensureDefaultModeEntries(file, ctx, pi);
320
- return file;
321
- } catch {
322
- return createDefaultModes(ctx, pi);
323
- }
305
+ try {
306
+ const raw = await fs.readFile(filePath, "utf8");
307
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
308
+ const currentMode = typeof parsed.currentMode === "string" ? parsed.currentMode : "default";
309
+ const modesRaw = parsed.modes && typeof parsed.modes === "object" ? (parsed.modes as Record<string, unknown>) : {};
310
+ const modes: Record<string, ModeSpec> = {};
311
+ for (const [k, v] of Object.entries(modesRaw)) {
312
+ modes[k] = sanitizeModeSpec(v);
313
+ }
314
+ const file: ModesFile = {
315
+ version: 1,
316
+ currentMode,
317
+ modes,
318
+ };
319
+ ensureDefaultModeEntries(file, ctx, pi);
320
+ return file;
321
+ } catch {
322
+ return createDefaultModes(ctx, pi);
323
+ }
324
324
  }
325
325
 
326
326
  async function saveModesFile(filePath: string, data: ModesFile): Promise<void> {
327
- await atomicWriteUtf8(filePath, JSON.stringify(data, null, 2) + "\n");
327
+ await atomicWriteUtf8(filePath, JSON.stringify(data, null, 2) + "\n");
328
328
  }
329
329
 
330
330
  function orderedModeNames(modes: Record<string, ModeSpec>): string[] {
331
331
  // Preserve insertion order from the JSON file.
332
332
  // Object key iteration order is stable in modern JS runtimes.
333
333
  // NOTE: "custom" is an overlay mode and must not be selectable/persisted.
334
- return Object.keys(modes).filter((name) => name !== CUSTOM_MODE_NAME);
334
+ return Object.keys(modes).filter((name) => name !== CUSTOM_MODE_NAME);
335
335
  }
336
336
 
337
337
  function getModeBorderColor(ctx: ExtensionContext, pi: ExtensionAPI, mode: string): (text: string) => string {
338
- const theme = ctx.ui.theme;
339
- const spec = runtime.data.modes[mode];
338
+ const theme = ctx.ui.theme;
339
+ const spec = runtime.data.modes[mode];
340
340
 
341
341
  // Explicit color override in JSON.
342
- if (spec?.color) {
343
- try {
342
+ if (spec?.color) {
343
+ try {
344
344
  // Validate early so we don't crash during render.
345
- theme.getFgAnsi(spec.color as any);
346
- return (text: string) => theme.fg(spec.color as any, text);
347
- } catch {
345
+ theme.getFgAnsi(spec.color as any);
346
+ return (text: string) => theme.fg(spec.color as any, text);
347
+ } catch {
348
348
  // fall through to thinking-based colors
349
- }
350
- }
349
+ }
350
+ }
351
351
 
352
352
  // Default: derive from the current thinking level.
353
- return theme.getThinkingBorderColor(pi.getThinkingLevel());
353
+ return theme.getThinkingBorderColor(pi.getThinkingLevel());
354
354
  }
355
355
 
356
356
  function formatModeLabel(mode: string): string {
357
- return mode;
357
+ return mode;
358
358
  }
359
359
 
360
360
  async function resolveModesPath(cwd: string): Promise<string> {
361
- const projectPath = getProjectModesPath(cwd);
362
- if (await fileExists(projectPath)) return projectPath;
363
- return getGlobalModesPath();
361
+ const projectPath = getProjectModesPath(cwd);
362
+ if (await fileExists(projectPath)) {return projectPath;}
363
+ return getGlobalModesPath();
364
364
  }
365
365
 
366
366
  function inferModeFromSelection(ctx: ExtensionContext, pi: ExtensionAPI, data: ModesFile): string | null {
367
- const provider = ctx.model?.provider;
368
- const modelId = ctx.model?.id;
369
- const thinkingLevel = pi.getThinkingLevel();
370
- if (!provider || !modelId) return null;
367
+ const provider = ctx.model?.provider;
368
+ const modelId = ctx.model?.id;
369
+ const thinkingLevel = pi.getThinkingLevel();
370
+ if (!provider || !modelId) {return null;}
371
371
 
372
372
  // Only consider persisted/real modes (exclude the overlay "custom").
373
- const names = orderedModeNames(data.modes);
373
+ const names = orderedModeNames(data.modes);
374
374
 
375
- const supportsThinking = Boolean(ctx.model?.reasoning);
375
+ const supportsThinking = Boolean(ctx.model?.reasoning);
376
376
 
377
377
  // 1) If thinking is supported, require an exact match so modes can differ by thinking level.
378
- if (supportsThinking) {
379
- for (const name of names) {
380
- const spec = data.modes[name];
381
- if (!spec) continue;
382
- if (spec.provider !== provider || spec.modelId !== modelId) continue;
383
- if ((spec.thinkingLevel ?? undefined) !== thinkingLevel) continue;
384
- return name;
385
- }
386
- return null;
387
- }
378
+ if (supportsThinking) {
379
+ for (const name of names) {
380
+ const spec = data.modes[name];
381
+ if (!spec) {continue;}
382
+ if (spec.provider !== provider || spec.modelId !== modelId) {continue;}
383
+ if ((spec.thinkingLevel ?? undefined) !== thinkingLevel) {continue;}
384
+ return name;
385
+ }
386
+ return null;
387
+ }
388
388
 
389
389
  // 2) If thinking is NOT supported by the model, the effective level will always be "off".
390
390
  // In that case, treat thinkingLevel differences in modes.json as non-distinguishing.
391
- const candidates: string[] = [];
392
- for (const name of names) {
393
- const spec = data.modes[name];
394
- if (!spec) continue;
395
- if (spec.provider !== provider || spec.modelId !== modelId) continue;
396
- candidates.push(name);
397
- }
398
- if (candidates.length === 0) return null;
391
+ const candidates: string[] = [];
392
+ for (const name of names) {
393
+ const spec = data.modes[name];
394
+ if (!spec) {continue;}
395
+ if (spec.provider !== provider || spec.modelId !== modelId) {continue;}
396
+ candidates.push(name);
397
+ }
398
+ if (candidates.length === 0) {return null;}
399
399
 
400
400
  // Prefer a candidate that explicitly matches the effective thinking level.
401
- for (const name of candidates) {
402
- const spec = data.modes[name];
403
- if (!spec) continue;
404
- if ((spec.thinkingLevel ?? "off") === thinkingLevel) return name;
405
- }
401
+ for (const name of candidates) {
402
+ const spec = data.modes[name];
403
+ if (!spec) {continue;}
404
+ if ((spec.thinkingLevel ?? "off") === thinkingLevel) {return name;}
405
+ }
406
406
 
407
407
  // Next prefer a candidate with no thinkingLevel configured.
408
- for (const name of candidates) {
409
- const spec = data.modes[name];
410
- if (!spec) continue;
411
- if (!spec.thinkingLevel) return name;
412
- }
408
+ for (const name of candidates) {
409
+ const spec = data.modes[name];
410
+ if (!spec) {continue;}
411
+ if (!spec.thinkingLevel) {return name;}
412
+ }
413
413
 
414
- return candidates[0] ?? null;
414
+ return candidates[0] ?? null;
415
415
  }
416
416
 
417
417
  type ModeRuntime = {
@@ -439,74 +439,74 @@ type ModeRuntime = {
439
439
  };
440
440
 
441
441
  const runtime: ModeRuntime = {
442
- filePath: "",
443
- fileMtimeMs: null,
444
- baseline: null,
445
- data: { version: 1, currentMode: "default", modes: {} },
446
- lastRealMode: "default",
447
- currentMode: "default",
448
- applying: false,
442
+ filePath: "",
443
+ fileMtimeMs: null,
444
+ baseline: null,
445
+ data: { version: 1, currentMode: "default", modes: {} },
446
+ lastRealMode: "default",
447
+ currentMode: "default",
448
+ applying: false,
449
449
  };
450
450
 
451
451
  // Updated by setEditor() when the custom editor is instantiated.
452
452
  let requestEditorRender: (() => void) | undefined;
453
453
 
454
454
  async function ensureRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
455
- const filePath = await resolveModesPath(ctx.cwd);
455
+ const filePath = await resolveModesPath(ctx.cwd);
456
456
 
457
- const mtimeMs = await getMtimeMs(filePath);
458
- const filePathChanged = runtime.filePath !== filePath;
459
- const fileChanged = filePathChanged || runtime.fileMtimeMs !== mtimeMs;
457
+ const mtimeMs = await getMtimeMs(filePath);
458
+ const filePathChanged = runtime.filePath !== filePath;
459
+ const fileChanged = filePathChanged || runtime.fileMtimeMs !== mtimeMs;
460
460
 
461
- if (fileChanged) {
462
- runtime.filePath = filePath;
463
- runtime.fileMtimeMs = mtimeMs;
461
+ if (fileChanged) {
462
+ runtime.filePath = filePath;
463
+ runtime.fileMtimeMs = mtimeMs;
464
464
 
465
- const loaded = await loadModesFile(filePath, ctx, pi);
465
+ const loaded = await loadModesFile(filePath, ctx, pi);
466
466
  // Normalize/ensure defaults *before* we snapshot baseline so later persistence
467
467
  // only reflects explicit user actions ("store").
468
- ensureDefaultModeEntries(loaded, ctx, pi);
469
- runtime.data = loaded;
470
- runtime.baseline = cloneModesFile(runtime.data);
468
+ ensureDefaultModeEntries(loaded, ctx, pi);
469
+ runtime.data = loaded;
470
+ runtime.baseline = cloneModesFile(runtime.data);
471
471
 
472
472
  // Reset overlay when switching projects.
473
- if (filePathChanged && runtime.currentMode !== CUSTOM_MODE_NAME) {
474
- runtime.currentMode = runtime.data.currentMode;
475
- runtime.lastRealMode = runtime.currentMode;
476
- }
477
- }
473
+ if (filePathChanged && runtime.currentMode !== CUSTOM_MODE_NAME) {
474
+ runtime.currentMode = runtime.data.currentMode;
475
+ runtime.lastRealMode = runtime.currentMode;
476
+ }
477
+ }
478
478
 
479
479
  // If we're not in the overlay "custom" mode, ensure currentMode is valid.
480
- if (runtime.currentMode !== CUSTOM_MODE_NAME) {
481
- if (!runtime.currentMode || !(runtime.currentMode in runtime.data.modes)) {
482
- runtime.currentMode = runtime.data.currentMode;
483
- }
484
- if (!runtime.lastRealMode || !(runtime.lastRealMode in runtime.data.modes)) {
485
- runtime.lastRealMode = runtime.currentMode;
486
- }
487
- }
480
+ if (runtime.currentMode !== CUSTOM_MODE_NAME) {
481
+ if (!runtime.currentMode || !(runtime.currentMode in runtime.data.modes)) {
482
+ runtime.currentMode = runtime.data.currentMode;
483
+ }
484
+ if (!runtime.lastRealMode || !(runtime.lastRealMode in runtime.data.modes)) {
485
+ runtime.lastRealMode = runtime.currentMode;
486
+ }
487
+ }
488
488
  }
489
489
 
490
490
  async function persistRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
491
- if (!runtime.filePath) return;
491
+ if (!runtime.filePath) {return;}
492
492
 
493
493
  // Do not persist currentMode; multiple running pi sessions would fight over it.
494
494
  // Instead we infer the mode on startup from the active model + thinking level.
495
- runtime.baseline ??= cloneModesFile(runtime.data);
496
- const patch = computeModesPatch(runtime.baseline, runtime.data, false);
497
- if (!patch) return;
495
+ runtime.baseline ??= cloneModesFile(runtime.data);
496
+ const patch = computeModesPatch(runtime.baseline, runtime.data, false);
497
+ if (!patch) {return;}
498
498
 
499
- await withFileLock(runtime.filePath, async () => {
499
+ await withFileLock(runtime.filePath, async() => {
500
500
  // Merge our local patch into the latest on disk to avoid clobbering other agents.
501
- const latest = await loadModesFile(runtime.filePath, ctx, pi);
502
- applyModesPatch(latest, patch);
503
- ensureDefaultModeEntries(latest, ctx, pi);
504
- await saveModesFile(runtime.filePath, latest);
505
-
506
- runtime.data = latest;
507
- runtime.baseline = cloneModesFile(latest);
508
- runtime.fileMtimeMs = await getMtimeMs(runtime.filePath);
509
- });
501
+ const latest = await loadModesFile(runtime.filePath, ctx, pi);
502
+ applyModesPatch(latest, patch);
503
+ ensureDefaultModeEntries(latest, ctx, pi);
504
+ await saveModesFile(runtime.filePath, latest);
505
+
506
+ runtime.data = latest;
507
+ runtime.baseline = cloneModesFile(latest);
508
+ runtime.fileMtimeMs = await getMtimeMs(runtime.filePath);
509
+ });
510
510
  }
511
511
 
512
512
  // We cannot reliably read the *current* model immediately after pi.setModel() in the same tick,
@@ -515,95 +515,95 @@ async function persistRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<
515
515
  let lastObservedModel: { provider?: string; modelId?: string } = {};
516
516
 
517
517
  function getCurrentSelectionSpec(pi: ExtensionAPI, _ctx: ExtensionContext): ModeSpec {
518
- return {
519
- provider: lastObservedModel.provider,
520
- modelId: lastObservedModel.modelId,
521
- thinkingLevel: pi.getThinkingLevel(),
522
- };
518
+ return {
519
+ provider: lastObservedModel.provider,
520
+ modelId: lastObservedModel.modelId,
521
+ thinkingLevel: pi.getThinkingLevel(),
522
+ };
523
523
  }
524
524
 
525
525
  async function storeSelectionIntoMode(pi: ExtensionAPI, ctx: ExtensionContext, mode: string, selection: ModeSpec): Promise<void> {
526
526
  // "custom" is an overlay; it is not persisted.
527
- if (mode === CUSTOM_MODE_NAME) return;
527
+ if (mode === CUSTOM_MODE_NAME) {return;}
528
528
 
529
- await ensureRuntime(pi, ctx);
529
+ await ensureRuntime(pi, ctx);
530
530
 
531
- const existingTarget = runtime.data.modes[mode] ?? {};
532
- const next: ModeSpec = { ...existingTarget };
531
+ const existingTarget = runtime.data.modes[mode] ?? {};
532
+ const next: ModeSpec = { ...existingTarget };
533
533
 
534
534
  // Only overwrite fields that we can actually observe.
535
- if (selection.provider && selection.modelId) {
536
- next.provider = selection.provider;
537
- next.modelId = selection.modelId;
538
- }
539
- if (selection.thinkingLevel) next.thinkingLevel = selection.thinkingLevel;
540
-
541
- runtime.data.modes[mode] = next;
542
- await persistRuntime(pi, ctx);
535
+ if (selection.provider && selection.modelId) {
536
+ next.provider = selection.provider;
537
+ next.modelId = selection.modelId;
538
+ }
539
+ if (selection.thinkingLevel) {next.thinkingLevel = selection.thinkingLevel;}
540
+
541
+ runtime.data.modes[mode] = next;
542
+ await persistRuntime(pi, ctx);
543
543
  }
544
544
 
545
545
  async function applyMode(pi: ExtensionAPI, ctx: ExtensionContext, mode: string): Promise<void> {
546
- await ensureRuntime(pi, ctx);
546
+ await ensureRuntime(pi, ctx);
547
547
 
548
548
  // "custom" is a runtime-only overlay mode.
549
- if (mode === CUSTOM_MODE_NAME) {
550
- runtime.currentMode = CUSTOM_MODE_NAME;
551
- customOverlay = getCurrentSelectionSpec(pi, ctx);
552
- if (ctx.hasUI) requestEditorRender?.();
553
- return;
554
- }
555
-
556
- const spec = runtime.data.modes[mode];
557
- if (!spec) {
558
- if (ctx.hasUI) {
559
- ctx.ui.notify(`Unknown mode: ${mode}`, "warning");
560
- }
561
- return;
562
- }
563
-
564
- runtime.currentMode = mode;
565
- runtime.lastRealMode = mode;
566
- customOverlay = null;
567
-
568
- runtime.applying = true;
569
- let modelAppliedOk = true;
570
- try {
549
+ if (mode === CUSTOM_MODE_NAME) {
550
+ runtime.currentMode = CUSTOM_MODE_NAME;
551
+ customOverlay = getCurrentSelectionSpec(pi, ctx);
552
+ if (ctx.hasUI) {requestEditorRender?.();}
553
+ return;
554
+ }
555
+
556
+ const spec = runtime.data.modes[mode];
557
+ if (!spec) {
558
+ if (ctx.hasUI) {
559
+ ctx.ui.notify(`Unknown mode: ${mode}`, "warning");
560
+ }
561
+ return;
562
+ }
563
+
564
+ runtime.currentMode = mode;
565
+ runtime.lastRealMode = mode;
566
+ customOverlay = null;
567
+
568
+ runtime.applying = true;
569
+ let modelAppliedOk = true;
570
+ try {
571
571
  // Apply model
572
- if (spec.provider && spec.modelId) {
573
- const m = ctx.modelRegistry.find(spec.provider, spec.modelId);
574
- if (m) {
575
- const ok = await pi.setModel(m);
576
- modelAppliedOk = ok;
577
- if (!ok && ctx.hasUI) {
578
- ctx.ui.notify(`No API key available for ${spec.provider}/${spec.modelId}`, "warning");
579
- }
580
- } else {
581
- modelAppliedOk = false;
582
- if (ctx.hasUI) {
583
- ctx.ui.notify(`Mode "${mode}" references unknown model ${spec.provider}/${spec.modelId}`, "warning");
584
- }
585
- }
586
- }
572
+ if (spec.provider && spec.modelId) {
573
+ const m = ctx.modelRegistry.find(spec.provider, spec.modelId);
574
+ if (m) {
575
+ const ok = await pi.setModel(m);
576
+ modelAppliedOk = ok;
577
+ if (!ok && ctx.hasUI) {
578
+ ctx.ui.notify(`No API key available for ${spec.provider}/${spec.modelId}`, "warning");
579
+ }
580
+ } else {
581
+ modelAppliedOk = false;
582
+ if (ctx.hasUI) {
583
+ ctx.ui.notify(`Mode "${mode}" references unknown model ${spec.provider}/${spec.modelId}`, "warning");
584
+ }
585
+ }
586
+ }
587
587
 
588
588
  // Apply thinking level
589
- if (spec.thinkingLevel) {
590
- pi.setThinkingLevel(spec.thinkingLevel);
591
- }
592
- } finally {
593
- runtime.applying = false;
594
- }
589
+ if (spec.thinkingLevel) {
590
+ pi.setThinkingLevel(spec.thinkingLevel);
591
+ }
592
+ } finally {
593
+ runtime.applying = false;
594
+ }
595
595
 
596
596
  // If we couldn't apply the requested model (e.g. missing API key), switch to overlay.
597
597
  // We do *not* treat thinking-level clamping as a failure: clamping is expected when
598
598
  // switching between models with different thinking capabilities.
599
- if (!modelAppliedOk) {
600
- runtime.currentMode = CUSTOM_MODE_NAME;
601
- customOverlay = getCurrentSelectionSpec(pi, ctx);
602
- }
603
-
604
- if (ctx.hasUI) {
605
- requestEditorRender?.();
606
- }
599
+ if (!modelAppliedOk) {
600
+ runtime.currentMode = CUSTOM_MODE_NAME;
601
+ customOverlay = getCurrentSelectionSpec(pi, ctx);
602
+ }
603
+
604
+ if (ctx.hasUI) {
605
+ requestEditorRender?.();
606
+ }
607
607
  }
608
608
 
609
609
  const MODE_UI_CONFIGURE = "Configure modes…";
@@ -614,304 +614,304 @@ const ALL_THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium",
614
614
  const THINKING_UNSET_LABEL = "(don't change)";
615
615
 
616
616
  function isDefaultModeName(name: string): boolean {
617
- return (DEFAULT_MODE_ORDER as readonly string[]).includes(name);
617
+ return (DEFAULT_MODE_ORDER as readonly string[]).includes(name);
618
618
  }
619
619
 
620
620
  function isReservedModeName(name: string): boolean {
621
- return name === CUSTOM_MODE_NAME || name === MODE_UI_CONFIGURE || name === MODE_UI_ADD || name === MODE_UI_BACK;
621
+ return name === CUSTOM_MODE_NAME || name === MODE_UI_CONFIGURE || name === MODE_UI_ADD || name === MODE_UI_BACK;
622
622
  }
623
623
 
624
624
  function normalizeModeNameInput(name: string | undefined): string {
625
- return (name ?? "").trim();
625
+ return (name ?? "").trim();
626
626
  }
627
627
 
628
628
  function validateModeNameOrError(
629
- name: string,
630
- existing: Record<string, ModeSpec>,
631
- opts?: { allowExisting?: boolean },
629
+ name: string,
630
+ existing: Record<string, ModeSpec>,
631
+ opts?: { allowExisting?: boolean },
632
632
  ): string | null {
633
- if (!name) return "Mode name cannot be empty";
634
- if (/\s/.test(name)) return "Mode name cannot contain whitespace";
635
- if (isReservedModeName(name)) return `Mode name \"${name}\" is reserved`;
636
- if (!opts?.allowExisting && existing[name]) return `Mode \"${name}\" already exists`;
637
- return null;
633
+ if (!name) {return "Mode name cannot be empty";}
634
+ if (/\s/.test(name)) {return "Mode name cannot contain whitespace";}
635
+ if (isReservedModeName(name)) {return `Mode name \"${name}\" is reserved`;}
636
+ if (!opts?.allowExisting && existing[name]) {return `Mode \"${name}\" already exists`;}
637
+ return null;
638
638
  }
639
639
 
640
640
  async function handleModeChoiceUI(pi: ExtensionAPI, ctx: ExtensionContext, choice: string): Promise<void> {
641
641
  // Special behavior: when we're in "custom" and select another mode,
642
642
  // offer to either *use* it (switch) or *store* the current custom selection into it.
643
- if (runtime.currentMode === CUSTOM_MODE_NAME && choice !== CUSTOM_MODE_NAME) {
644
- const action = await ctx.ui.select(`Mode \"${choice}\"`, ["use", "store"]);
645
- if (!action) return;
643
+ if (runtime.currentMode === CUSTOM_MODE_NAME && choice !== CUSTOM_MODE_NAME) {
644
+ const action = await ctx.ui.select(`Mode \"${choice}\"`, ["use", "store"]);
645
+ if (!action) {return;}
646
646
 
647
- if (action === "use") {
648
- await applyMode(pi, ctx, choice);
649
- return;
650
- }
647
+ if (action === "use") {
648
+ await applyMode(pi, ctx, choice);
649
+ return;
650
+ }
651
651
 
652
652
  // "store": overwrite target mode with the current overlay selection (keep target color if set)
653
- await ensureRuntime(pi, ctx);
654
- const overlay = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
655
- await storeSelectionIntoMode(pi, ctx, choice, overlay);
656
- await applyMode(pi, ctx, choice);
657
- ctx.ui.notify(`Stored ${CUSTOM_MODE_NAME} into \"${choice}\"`, "info");
658
- return;
659
- }
660
-
661
- await applyMode(pi, ctx, choice);
653
+ await ensureRuntime(pi, ctx);
654
+ const overlay = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
655
+ await storeSelectionIntoMode(pi, ctx, choice, overlay);
656
+ await applyMode(pi, ctx, choice);
657
+ ctx.ui.notify(`Stored ${CUSTOM_MODE_NAME} into \"${choice}\"`, "info");
658
+ return;
659
+ }
660
+
661
+ await applyMode(pi, ctx, choice);
662
662
  }
663
663
 
664
664
  async function selectModeUI(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
665
- if (!ctx.hasUI) return;
666
-
667
- while (true) {
668
- await ensureRuntime(pi, ctx);
669
- const names = orderedModeNames(runtime.data.modes);
670
- const choice = await ctx.ui.select(`Mode (current: ${runtime.currentMode})`, [...names, MODE_UI_CONFIGURE]);
671
- if (!choice) return;
672
-
673
- if (choice === MODE_UI_CONFIGURE) {
674
- await configureModesUI(pi, ctx);
675
- continue;
676
- }
677
-
678
- await handleModeChoiceUI(pi, ctx, choice);
679
- return;
680
- }
665
+ if (!ctx.hasUI) {return;}
666
+
667
+ while (true) {
668
+ await ensureRuntime(pi, ctx);
669
+ const names = orderedModeNames(runtime.data.modes);
670
+ const choice = await ctx.ui.select(`Mode (current: ${runtime.currentMode})`, [...names, MODE_UI_CONFIGURE]);
671
+ if (!choice) {return;}
672
+
673
+ if (choice === MODE_UI_CONFIGURE) {
674
+ await configureModesUI(pi, ctx);
675
+ continue;
676
+ }
677
+
678
+ await handleModeChoiceUI(pi, ctx, choice);
679
+ return;
680
+ }
681
681
  }
682
682
 
683
683
  async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
684
- if (!ctx.hasUI) return;
685
-
686
- while (true) {
687
- await ensureRuntime(pi, ctx);
688
- const names = orderedModeNames(runtime.data.modes);
689
- const choice = await ctx.ui.select("Configure modes", [...names, MODE_UI_ADD, MODE_UI_BACK]);
690
- if (!choice || choice === MODE_UI_BACK) return;
691
-
692
- if (choice === MODE_UI_ADD) {
693
- const created = await addModeUI(pi, ctx);
694
- if (created) {
695
- await editModeUI(pi, ctx, created);
696
- }
697
- continue;
698
- }
699
-
700
- await editModeUI(pi, ctx, choice);
701
- }
684
+ if (!ctx.hasUI) {return;}
685
+
686
+ while (true) {
687
+ await ensureRuntime(pi, ctx);
688
+ const names = orderedModeNames(runtime.data.modes);
689
+ const choice = await ctx.ui.select("Configure modes", [...names, MODE_UI_ADD, MODE_UI_BACK]);
690
+ if (!choice || choice === MODE_UI_BACK) {return;}
691
+
692
+ if (choice === MODE_UI_ADD) {
693
+ const created = await addModeUI(pi, ctx);
694
+ if (created) {
695
+ await editModeUI(pi, ctx, created);
696
+ }
697
+ continue;
698
+ }
699
+
700
+ await editModeUI(pi, ctx, choice);
701
+ }
702
702
  }
703
703
 
704
704
  async function addModeUI(pi: ExtensionAPI, ctx: ExtensionContext): Promise<string | undefined> {
705
- if (!ctx.hasUI) return undefined;
706
- await ensureRuntime(pi, ctx);
705
+ if (!ctx.hasUI) {return undefined;}
706
+ await ensureRuntime(pi, ctx);
707
707
 
708
- while (true) {
709
- const raw = await ctx.ui.input("New mode name", "e.g. docs, review, planning");
710
- if (raw === undefined) return undefined;
708
+ while (true) {
709
+ const raw = await ctx.ui.input("New mode name", "e.g. docs, review, planning");
710
+ if (raw === undefined) {return undefined;}
711
711
 
712
- const name = normalizeModeNameInput(raw);
713
- const err = validateModeNameOrError(name, runtime.data.modes);
714
- if (err) {
715
- ctx.ui.notify(err, "warning");
716
- continue;
717
- }
712
+ const name = normalizeModeNameInput(raw);
713
+ const err = validateModeNameOrError(name, runtime.data.modes);
714
+ if (err) {
715
+ ctx.ui.notify(err, "warning");
716
+ continue;
717
+ }
718
718
 
719
719
  // Default new modes to the current selection so they behave as expected immediately.
720
- const selection = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
721
- runtime.data.modes[name] = {
722
- provider: selection.provider,
723
- modelId: selection.modelId,
724
- thinkingLevel: selection.thinkingLevel,
725
- };
726
- await persistRuntime(pi, ctx);
727
- ctx.ui.notify(`Added mode \"${name}\"`, "info");
728
- return name;
729
- }
720
+ const selection = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
721
+ runtime.data.modes[name] = {
722
+ provider: selection.provider,
723
+ modelId: selection.modelId,
724
+ thinkingLevel: selection.thinkingLevel,
725
+ };
726
+ await persistRuntime(pi, ctx);
727
+ ctx.ui.notify(`Added mode \"${name}\"`, "info");
728
+ return name;
729
+ }
730
730
  }
731
731
 
732
732
  async function editModeUI(pi: ExtensionAPI, ctx: ExtensionContext, mode: string): Promise<void> {
733
- if (!ctx.hasUI) return;
734
-
735
- let modeName = mode;
736
-
737
- while (true) {
738
- await ensureRuntime(pi, ctx);
739
- const spec = runtime.data.modes[modeName];
740
- if (!spec) return;
741
-
742
- const modelLabel = spec.provider && spec.modelId ? `${spec.provider}/${spec.modelId}` : "(no model)";
743
- const thinkingLabel = spec.thinkingLevel ?? THINKING_UNSET_LABEL;
744
-
745
- const actions = ["Change name", "Change model", "Change thinking level"];
746
- if (!isDefaultModeName(modeName)) actions.push("Delete mode");
747
- actions.push(MODE_UI_BACK);
748
-
749
- const action = await ctx.ui.select(
750
- `Edit mode \"${modeName}\" model: ${modelLabel} thinking: ${thinkingLabel}`,
751
- actions,
752
- );
753
- if (!action || action === MODE_UI_BACK) return;
754
-
755
- if (action === "Change name") {
756
- const renamed = await renameModeUI(pi, ctx, modeName);
757
- if (renamed) modeName = renamed;
758
- continue;
759
- }
760
-
761
- if (action === "Change model") {
762
- const selected = await pickModelForModeUI(ctx, spec);
763
- if (!selected) continue;
764
- spec.provider = selected.provider;
765
- spec.modelId = selected.modelId;
766
- runtime.data.modes[modeName] = spec;
767
- await persistRuntime(pi, ctx);
768
- ctx.ui.notify(`Updated model for \"${modeName}\"`, "info");
769
-
770
- if (runtime.currentMode === modeName) {
771
- await applyMode(pi, ctx, modeName);
772
- }
773
- continue;
774
- }
775
-
776
- if (action === "Change thinking level") {
777
- const level = await pickThinkingLevelForModeUI(ctx, spec.thinkingLevel);
778
- if (level === undefined) continue;
779
-
780
- if (level === null) {
781
- delete spec.thinkingLevel;
782
- } else {
783
- spec.thinkingLevel = level;
784
- }
785
-
786
- runtime.data.modes[modeName] = spec;
787
- await persistRuntime(pi, ctx);
788
- ctx.ui.notify(`Updated thinking level for \"${modeName}\"`, "info");
789
-
790
- if (runtime.currentMode === modeName) {
791
- await applyMode(pi, ctx, modeName);
792
- }
793
- continue;
794
- }
795
-
796
- if (action === "Delete mode") {
797
- const ok = await ctx.ui.confirm("Delete mode", `Delete mode \"${modeName}\"?`);
798
- if (!ok) continue;
799
-
800
- delete runtime.data.modes[modeName];
801
- await persistRuntime(pi, ctx);
802
-
803
- if (runtime.currentMode === modeName) {
804
- runtime.currentMode = CUSTOM_MODE_NAME;
805
- customOverlay = getCurrentSelectionSpec(pi, ctx);
806
- }
807
- if (runtime.lastRealMode === modeName) {
808
- runtime.lastRealMode = "default";
809
- }
810
- requestEditorRender?.();
811
- ctx.ui.notify(`Deleted mode \"${modeName}\"`, "info");
812
- return;
813
- }
814
- }
733
+ if (!ctx.hasUI) {return;}
734
+
735
+ let modeName = mode;
736
+
737
+ while (true) {
738
+ await ensureRuntime(pi, ctx);
739
+ const spec = runtime.data.modes[modeName];
740
+ if (!spec) {return;}
741
+
742
+ const modelLabel = spec.provider && spec.modelId ? `${spec.provider}/${spec.modelId}` : "(no model)";
743
+ const thinkingLabel = spec.thinkingLevel ?? THINKING_UNSET_LABEL;
744
+
745
+ const actions = ["Change name", "Change model", "Change thinking level"];
746
+ if (!isDefaultModeName(modeName)) {actions.push("Delete mode");}
747
+ actions.push(MODE_UI_BACK);
748
+
749
+ const action = await ctx.ui.select(
750
+ `Edit mode \"${modeName}\" model: ${modelLabel} thinking: ${thinkingLabel}`,
751
+ actions,
752
+ );
753
+ if (!action || action === MODE_UI_BACK) {return;}
754
+
755
+ if (action === "Change name") {
756
+ const renamed = await renameModeUI(pi, ctx, modeName);
757
+ if (renamed) {modeName = renamed;}
758
+ continue;
759
+ }
760
+
761
+ if (action === "Change model") {
762
+ const selected = await pickModelForModeUI(ctx, spec);
763
+ if (!selected) {continue;}
764
+ spec.provider = selected.provider;
765
+ spec.modelId = selected.modelId;
766
+ runtime.data.modes[modeName] = spec;
767
+ await persistRuntime(pi, ctx);
768
+ ctx.ui.notify(`Updated model for \"${modeName}\"`, "info");
769
+
770
+ if (runtime.currentMode === modeName) {
771
+ await applyMode(pi, ctx, modeName);
772
+ }
773
+ continue;
774
+ }
775
+
776
+ if (action === "Change thinking level") {
777
+ const level = await pickThinkingLevelForModeUI(ctx, spec.thinkingLevel);
778
+ if (level === undefined) {continue;}
779
+
780
+ if (level === null) {
781
+ delete spec.thinkingLevel;
782
+ } else {
783
+ spec.thinkingLevel = level;
784
+ }
785
+
786
+ runtime.data.modes[modeName] = spec;
787
+ await persistRuntime(pi, ctx);
788
+ ctx.ui.notify(`Updated thinking level for \"${modeName}\"`, "info");
789
+
790
+ if (runtime.currentMode === modeName) {
791
+ await applyMode(pi, ctx, modeName);
792
+ }
793
+ continue;
794
+ }
795
+
796
+ if (action === "Delete mode") {
797
+ const ok = await ctx.ui.confirm("Delete mode", `Delete mode \"${modeName}\"?`);
798
+ if (!ok) {continue;}
799
+
800
+ delete runtime.data.modes[modeName];
801
+ await persistRuntime(pi, ctx);
802
+
803
+ if (runtime.currentMode === modeName) {
804
+ runtime.currentMode = CUSTOM_MODE_NAME;
805
+ customOverlay = getCurrentSelectionSpec(pi, ctx);
806
+ }
807
+ if (runtime.lastRealMode === modeName) {
808
+ runtime.lastRealMode = "default";
809
+ }
810
+ requestEditorRender?.();
811
+ ctx.ui.notify(`Deleted mode \"${modeName}\"`, "info");
812
+ return;
813
+ }
814
+ }
815
815
  }
816
816
 
817
817
  function renameModesRecord(modes: Record<string, ModeSpec>, oldName: string, newName: string): Record<string, ModeSpec> {
818
- const out: Record<string, ModeSpec> = {};
819
- for (const [k, v] of Object.entries(modes)) {
820
- if (k === oldName) out[newName] = v;
821
- else out[k] = v;
822
- }
823
- return out;
818
+ const out: Record<string, ModeSpec> = {};
819
+ for (const [k, v] of Object.entries(modes)) {
820
+ if (k === oldName) {out[newName] = v;}
821
+ else {out[k] = v;}
822
+ }
823
+ return out;
824
824
  }
825
825
 
826
826
  async function renameModeUI(pi: ExtensionAPI, ctx: ExtensionContext, oldName: string): Promise<string | undefined> {
827
- if (!ctx.hasUI) return undefined;
827
+ if (!ctx.hasUI) {return undefined;}
828
828
 
829
- if (isDefaultModeName(oldName)) {
830
- ctx.ui.notify(`Cannot rename default mode \"${oldName}\"`, "warning");
831
- return oldName;
832
- }
829
+ if (isDefaultModeName(oldName)) {
830
+ ctx.ui.notify(`Cannot rename default mode \"${oldName}\"`, "warning");
831
+ return oldName;
832
+ }
833
833
 
834
- await ensureRuntime(pi, ctx);
834
+ await ensureRuntime(pi, ctx);
835
835
 
836
- while (true) {
837
- const raw = await ctx.ui.input(`Rename mode \"${oldName}\"`, oldName);
838
- if (raw === undefined) return undefined;
836
+ while (true) {
837
+ const raw = await ctx.ui.input(`Rename mode \"${oldName}\"`, oldName);
838
+ if (raw === undefined) {return undefined;}
839
839
 
840
- const newName = normalizeModeNameInput(raw);
841
- if (!newName || newName === oldName) return oldName;
840
+ const newName = normalizeModeNameInput(raw);
841
+ if (!newName || newName === oldName) {return oldName;}
842
842
 
843
- const err = validateModeNameOrError(newName, runtime.data.modes);
844
- if (err) {
845
- ctx.ui.notify(err, "warning");
846
- continue;
847
- }
843
+ const err = validateModeNameOrError(newName, runtime.data.modes);
844
+ if (err) {
845
+ ctx.ui.notify(err, "warning");
846
+ continue;
847
+ }
848
848
 
849
- runtime.data.modes = renameModesRecord(runtime.data.modes, oldName, newName);
850
- await persistRuntime(pi, ctx);
849
+ runtime.data.modes = renameModesRecord(runtime.data.modes, oldName, newName);
850
+ await persistRuntime(pi, ctx);
851
851
 
852
- if (runtime.currentMode === oldName) runtime.currentMode = newName;
853
- if (runtime.lastRealMode === oldName) runtime.lastRealMode = newName;
854
- requestEditorRender?.();
852
+ if (runtime.currentMode === oldName) {runtime.currentMode = newName;}
853
+ if (runtime.lastRealMode === oldName) {runtime.lastRealMode = newName;}
854
+ requestEditorRender?.();
855
855
 
856
- ctx.ui.notify(`Renamed \"${oldName}\" → \"${newName}\"`, "info");
857
- return newName;
858
- }
856
+ ctx.ui.notify(`Renamed \"${oldName}\" → \"${newName}\"`, "info");
857
+ return newName;
858
+ }
859
859
  }
860
860
 
861
861
  async function pickModelForModeUI(
862
- ctx: ExtensionContext,
863
- spec: ModeSpec,
862
+ ctx: ExtensionContext,
863
+ spec: ModeSpec,
864
864
  ): Promise<{ provider: string; modelId: string } | undefined> {
865
- if (!ctx.hasUI) return undefined;
865
+ if (!ctx.hasUI) {return undefined;}
866
866
 
867
- const settingsManager = SettingsManager.inMemory();
868
- const currentModel = spec.provider && spec.modelId ? ctx.modelRegistry.find(spec.provider, spec.modelId) : ctx.model;
867
+ const settingsManager = SettingsManager.inMemory();
868
+ const currentModel = spec.provider && spec.modelId ? ctx.modelRegistry.find(spec.provider, spec.modelId) : ctx.model;
869
869
 
870
- const scopedModels: Array<{ model: any; thinkingLevel: string }> = [];
870
+ const scopedModels: Array<{ model: any; thinkingLevel: string }> = [];
871
871
 
872
- return ctx.ui.custom<{ provider: string; modelId: string } | undefined>((tui, _theme, _keybindings, done) => {
873
- const selector = new ModelSelectorComponent(
874
- tui,
875
- currentModel,
876
- settingsManager,
872
+ return ctx.ui.custom<{ provider: string; modelId: string } | undefined>((tui, _theme, _keybindings, done) => {
873
+ const selector = new ModelSelectorComponent(
874
+ tui,
875
+ currentModel,
876
+ settingsManager,
877
877
  ctx.modelRegistry as any,
878
878
  scopedModels as any,
879
879
  (model) => done({ provider: model.provider, modelId: model.id }),
880
880
  () => done(undefined),
881
- );
882
- return selector;
883
- });
881
+ );
882
+ return selector;
883
+ });
884
884
  }
885
885
 
886
886
  async function pickThinkingLevelForModeUI(
887
- ctx: ExtensionContext,
888
- current: ThinkingLevel | undefined,
887
+ ctx: ExtensionContext,
888
+ current: ThinkingLevel | undefined,
889
889
  ): Promise<ThinkingLevel | null | undefined> {
890
- if (!ctx.hasUI) return undefined;
890
+ if (!ctx.hasUI) {return undefined;}
891
891
 
892
- const defaultValue = current ?? "off";
893
- const options = [...ALL_THINKING_LEVELS, THINKING_UNSET_LABEL];
892
+ const defaultValue = current ?? "off";
893
+ const options = [...ALL_THINKING_LEVELS, THINKING_UNSET_LABEL];
894
894
  // Prefer the current selection by ordering it first.
895
- const ordered = [defaultValue, ...options.filter((x) => x !== defaultValue)];
895
+ const ordered = [defaultValue, ...options.filter((x) => x !== defaultValue)];
896
896
 
897
- const choice = await ctx.ui.select("Thinking level", ordered);
898
- if (!choice) return undefined;
899
- if (choice === THINKING_UNSET_LABEL) return null;
900
- if (ALL_THINKING_LEVELS.includes(choice as ThinkingLevel)) return choice as ThinkingLevel;
901
- return undefined;
897
+ const choice = await ctx.ui.select("Thinking level", ordered);
898
+ if (!choice) {return undefined;}
899
+ if (choice === THINKING_UNSET_LABEL) {return null;}
900
+ if (ALL_THINKING_LEVELS.includes(choice as ThinkingLevel)) {return choice as ThinkingLevel;}
901
+ return undefined;
902
902
  }
903
903
 
904
904
  async function cycleMode(pi: ExtensionAPI, ctx: ExtensionContext, direction: 1 | -1 = 1): Promise<void> {
905
- if (!ctx.hasUI) return;
906
- await ensureRuntime(pi, ctx);
907
- const names = orderedModeNames(runtime.data.modes);
908
- if (names.length === 0) return;
905
+ if (!ctx.hasUI) {return;}
906
+ await ensureRuntime(pi, ctx);
907
+ const names = orderedModeNames(runtime.data.modes);
908
+ if (names.length === 0) {return;}
909
909
 
910
910
  // If we're currently in the overlay mode, cycle relative to the last real mode.
911
- const baseMode = runtime.currentMode === CUSTOM_MODE_NAME ? runtime.lastRealMode : runtime.currentMode;
912
- const idx = Math.max(0, names.indexOf(baseMode));
913
- const next = names[(idx + direction + names.length) % names.length] ?? names[0]!;
914
- await applyMode(pi, ctx, next);
911
+ const baseMode = runtime.currentMode === CUSTOM_MODE_NAME ? runtime.lastRealMode : runtime.currentMode;
912
+ const idx = Math.max(0, names.indexOf(baseMode));
913
+ const next = names[(idx + direction + names.length) % names.length] ?? names[0]!;
914
+ await applyMode(pi, ctx, next);
915
915
  }
916
916
 
917
917
  // =============================================================================
@@ -927,206 +927,206 @@ interface PromptEntry {
927
927
  }
928
928
 
929
929
  class PromptEditor extends CustomEditor {
930
- public modeLabelProvider?: () => string;
930
+ public modeLabelProvider?: () => string;
931
931
  /**
932
932
  * Color function for the mode label. If unset, the label inherits the border color.
933
933
  * We use this to keep the label consistent (e.g. same as the footer/status bar).
934
934
  */
935
- public modeLabelColor?: (text: string) => string;
936
- private lockedBorder = false;
937
- private _borderColor?: (text: string) => string;
938
-
939
- constructor(
940
- tui: ConstructorParameters<typeof CustomEditor>[0],
941
- theme: ConstructorParameters<typeof CustomEditor>[1],
942
- keybindings: ConstructorParameters<typeof CustomEditor>[2],
943
- ) {
944
- super(tui, theme, keybindings);
945
- delete (this as { borderColor?: (text: string) => string }).borderColor;
946
- Object.defineProperty(this, "borderColor", {
947
- get: () => this._borderColor ?? ((text: string) => text),
948
- set: (value: (text: string) => string) => {
949
- if (this.lockedBorder) return;
950
- this._borderColor = value;
951
- },
952
- configurable: true,
953
- enumerable: true,
954
- });
955
- }
956
-
957
- lockBorderColor() {
958
- this.lockedBorder = true;
959
- }
960
-
961
- render(width: number): string[] {
962
- const lines = super.render(width);
963
- const mode = this.modeLabelProvider?.();
964
- if (!mode) return lines;
965
-
966
- const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
967
- const topPlain = stripAnsi(lines[0] ?? "");
935
+ public modeLabelColor?: (text: string) => string;
936
+ private lockedBorder = false;
937
+ private _borderColor?: (text: string) => string;
938
+
939
+ constructor(
940
+ tui: ConstructorParameters<typeof CustomEditor>[0],
941
+ theme: ConstructorParameters<typeof CustomEditor>[1],
942
+ keybindings: ConstructorParameters<typeof CustomEditor>[2],
943
+ ) {
944
+ super(tui, theme, keybindings);
945
+ delete (this as { borderColor?: (text: string) => string }).borderColor;
946
+ Object.defineProperty(this, "borderColor", {
947
+ get: () => this._borderColor ?? ((text: string) => text),
948
+ set: (value: (text: string) => string) => {
949
+ if (this.lockedBorder) {return;}
950
+ this._borderColor = value;
951
+ },
952
+ configurable: true,
953
+ enumerable: true,
954
+ });
955
+ }
956
+
957
+ lockBorderColor() {
958
+ this.lockedBorder = true;
959
+ }
960
+
961
+ render(width: number): string[] {
962
+ const lines = super.render(width);
963
+ const mode = this.modeLabelProvider?.();
964
+ if (!mode) {return lines;}
965
+
966
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
967
+ const topPlain = stripAnsi(lines[0] ?? "");
968
968
 
969
969
  // If the editor is scrolled, the built-in editor renders a scroll indicator on the top border.
970
970
  // Preserve it, but still show the mode label.
971
- const scrollPrefixMatch = topPlain.match(/^(─── ↑ \d+ more )/);
972
- const prefix = scrollPrefixMatch?.[1] ?? "──";
971
+ const scrollPrefixMatch = topPlain.match(/^(─── ↑ \d+ more )/);
972
+ const prefix = scrollPrefixMatch?.[1] ?? "──";
973
973
 
974
- let label = formatModeLabel(mode);
974
+ let label = formatModeLabel(mode);
975
975
 
976
976
  // Compute how much room we have for the label core (without truncating the prefix).
977
- const labelLeftSpace = prefix.endsWith(" ") ? "" : " ";
978
- const labelRightSpace = " ";
979
- const minRightBorder = 1; // keep at least one border cell on the right
980
- const maxLabelLen = Math.max(0, width - prefix.length - labelLeftSpace.length - labelRightSpace.length - minRightBorder);
981
- if (maxLabelLen <= 0) return lines;
982
- if (label.length > maxLabelLen) label = label.slice(0, maxLabelLen);
977
+ const labelLeftSpace = prefix.endsWith(" ") ? "" : " ";
978
+ const labelRightSpace = " ";
979
+ const minRightBorder = 1; // keep at least one border cell on the right
980
+ const maxLabelLen = Math.max(0, width - prefix.length - labelLeftSpace.length - labelRightSpace.length - minRightBorder);
981
+ if (maxLabelLen <= 0) {return lines;}
982
+ if (label.length > maxLabelLen) {label = label.slice(0, maxLabelLen);}
983
983
 
984
- const labelChunk = `${labelLeftSpace}${label}${labelRightSpace}`;
984
+ const labelChunk = `${labelLeftSpace}${label}${labelRightSpace}`;
985
985
 
986
- const remaining = width - prefix.length - labelChunk.length;
987
- if (remaining < 0) return lines;
986
+ const remaining = width - prefix.length - labelChunk.length;
987
+ if (remaining < 0) {return lines;}
988
988
 
989
- const right = "─".repeat(Math.max(0, remaining));
989
+ const right = "─".repeat(Math.max(0, remaining));
990
990
 
991
- const labelColor = this.modeLabelColor ?? ((text: string) => this.borderColor(text));
992
- lines[0] = this.borderColor(prefix) + labelColor(labelChunk) + this.borderColor(right);
993
- return lines;
994
- }
991
+ const labelColor = this.modeLabelColor ?? ((text: string) => this.borderColor(text));
992
+ lines[0] = this.borderColor(prefix) + labelColor(labelChunk) + this.borderColor(right);
993
+ return lines;
994
+ }
995
995
 
996
- public requestRenderNow(): void {
997
- this.tui.requestRender();
998
- }
996
+ public requestRenderNow(): void {
997
+ this.tui.requestRender();
998
+ }
999
999
  }
1000
1000
 
1001
1001
  function extractText(content: Array<{ type: string; text?: string }>): string {
1002
- return content
1003
- .filter((item) => item.type === "text" && typeof item.text === "string")
1004
- .map((item) => item.text ?? "")
1005
- .join("")
1006
- .trim();
1002
+ return content
1003
+ .filter((item) => item.type === "text" && typeof item.text === "string")
1004
+ .map((item) => item.text ?? "")
1005
+ .join("")
1006
+ .trim();
1007
1007
  }
1008
1008
 
1009
1009
  function collectUserPromptsFromEntries(entries: Array<any>): PromptEntry[] {
1010
- const prompts: PromptEntry[] = [];
1011
-
1012
- for (const entry of entries) {
1013
- if (entry?.type !== "message") continue;
1014
- const message = entry?.message;
1015
- if (!message || message.role !== "user" || !Array.isArray(message.content)) continue;
1016
- const text = extractText(message.content);
1017
- if (!text) continue;
1018
- const timestamp = Number(message.timestamp ?? entry.timestamp ?? Date.now());
1019
- prompts.push({ text, timestamp });
1020
- }
1021
-
1022
- return prompts;
1010
+ const prompts: PromptEntry[] = [];
1011
+
1012
+ for (const entry of entries) {
1013
+ if (entry?.type !== "message") {continue;}
1014
+ const message = entry?.message;
1015
+ if (!message || message.role !== "user" || !Array.isArray(message.content)) {continue;}
1016
+ const text = extractText(message.content);
1017
+ if (!text) {continue;}
1018
+ const timestamp = Number(message.timestamp ?? entry.timestamp ?? Date.now());
1019
+ prompts.push({ text, timestamp });
1020
+ }
1021
+
1022
+ return prompts;
1023
1023
  }
1024
1024
 
1025
1025
  function getSessionDirForCwd(cwd: string): string {
1026
- const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1027
- return path.join(getGlobalAgentDir(), "sessions", safePath);
1026
+ const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1027
+ return path.join(getGlobalAgentDir(), "sessions", safePath);
1028
1028
  }
1029
1029
 
1030
1030
  async function readTail(filePath: string, maxBytes = 256 * 1024): Promise<string> {
1031
- let fileHandle: fs.FileHandle | undefined;
1032
- try {
1033
- const stats = await fs.stat(filePath);
1034
- const size = stats.size;
1035
- const start = Math.max(0, size - maxBytes);
1036
- const length = size - start;
1037
- if (length <= 0) return "";
1038
-
1039
- const buffer = Buffer.alloc(length);
1040
- fileHandle = await fs.open(filePath, "r");
1041
- const { bytesRead } = await fileHandle.read(buffer, 0, length, start);
1042
- if (bytesRead === 0) return "";
1043
- let chunk = buffer.subarray(0, bytesRead).toString("utf8");
1044
- if (start > 0) {
1045
- const firstNewline = chunk.indexOf("\n");
1046
- if (firstNewline !== -1) {
1047
- chunk = chunk.slice(firstNewline + 1);
1048
- }
1049
- }
1050
- return chunk;
1051
- } catch {
1052
- return "";
1053
- } finally {
1054
- await fileHandle?.close();
1055
- }
1031
+ let fileHandle: fs.FileHandle | undefined;
1032
+ try {
1033
+ const stats = await fs.stat(filePath);
1034
+ const size = stats.size;
1035
+ const start = Math.max(0, size - maxBytes);
1036
+ const length = size - start;
1037
+ if (length <= 0) {return "";}
1038
+
1039
+ const buffer = Buffer.alloc(length);
1040
+ fileHandle = await fs.open(filePath, "r");
1041
+ const { bytesRead } = await fileHandle.read(buffer, 0, length, start);
1042
+ if (bytesRead === 0) {return "";}
1043
+ let chunk = buffer.subarray(0, bytesRead).toString("utf8");
1044
+ if (start > 0) {
1045
+ const firstNewline = chunk.indexOf("\n");
1046
+ if (firstNewline !== -1) {
1047
+ chunk = chunk.slice(firstNewline + 1);
1048
+ }
1049
+ }
1050
+ return chunk;
1051
+ } catch {
1052
+ return "";
1053
+ } finally {
1054
+ await fileHandle?.close();
1055
+ }
1056
1056
  }
1057
1057
 
1058
1058
  async function loadPromptHistoryForCwd(cwd: string, excludeSessionFile?: string): Promise<PromptEntry[]> {
1059
- const sessionDir = getSessionDirForCwd(path.resolve(cwd));
1060
- const resolvedExclude = excludeSessionFile ? path.resolve(excludeSessionFile) : undefined;
1061
- const prompts: PromptEntry[] = [];
1062
-
1063
- let entries: Dirent[] = [];
1064
- try {
1065
- entries = await fs.readdir(sessionDir, { withFileTypes: true });
1066
- } catch {
1067
- return prompts;
1068
- }
1069
-
1070
- const files = await Promise.all(
1071
- entries
1072
- .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
1073
- .map(async (entry) => {
1074
- const filePath = path.join(sessionDir, entry.name);
1075
- try {
1076
- const stats = await fs.stat(filePath);
1077
- return { filePath, mtimeMs: stats.mtimeMs };
1078
- } catch {
1079
- return undefined;
1080
- }
1081
- }),
1082
- );
1083
-
1084
- const sortedFiles = files
1085
- .filter((file): file is { filePath: string; mtimeMs: number } => Boolean(file))
1086
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
1087
-
1088
- for (const file of sortedFiles) {
1089
- if (resolvedExclude && path.resolve(file.filePath) === resolvedExclude) continue;
1090
-
1091
- const tail = await readTail(file.filePath);
1092
- if (!tail) continue;
1093
- const lines = tail.split("\n").filter(Boolean);
1094
- for (const line of lines) {
1095
- let entry: any;
1096
- try {
1097
- entry = JSON.parse(line);
1098
- } catch {
1099
- continue;
1100
- }
1101
- if (entry?.type !== "message") continue;
1102
- const message = entry?.message;
1103
- if (!message || message.role !== "user" || !Array.isArray(message.content)) continue;
1104
- const text = extractText(message.content);
1105
- if (!text) continue;
1106
- const timestamp = Number(message.timestamp ?? entry.timestamp ?? Date.now());
1107
- prompts.push({ text, timestamp });
1108
- if (prompts.length >= MAX_RECENT_PROMPTS) break;
1109
- }
1110
- if (prompts.length >= MAX_RECENT_PROMPTS) break;
1111
- }
1112
-
1113
- return prompts;
1059
+ const sessionDir = getSessionDirForCwd(path.resolve(cwd));
1060
+ const resolvedExclude = excludeSessionFile ? path.resolve(excludeSessionFile) : undefined;
1061
+ const prompts: PromptEntry[] = [];
1062
+
1063
+ let entries: Dirent[] = [];
1064
+ try {
1065
+ entries = await fs.readdir(sessionDir, { withFileTypes: true });
1066
+ } catch {
1067
+ return prompts;
1068
+ }
1069
+
1070
+ const files = await Promise.all(
1071
+ entries
1072
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
1073
+ .map(async(entry) => {
1074
+ const filePath = path.join(sessionDir, entry.name);
1075
+ try {
1076
+ const stats = await fs.stat(filePath);
1077
+ return { filePath, mtimeMs: stats.mtimeMs };
1078
+ } catch {
1079
+ return undefined;
1080
+ }
1081
+ }),
1082
+ );
1083
+
1084
+ const sortedFiles = files
1085
+ .filter((file): file is { filePath: string; mtimeMs: number } => Boolean(file))
1086
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
1087
+
1088
+ for (const file of sortedFiles) {
1089
+ if (resolvedExclude && path.resolve(file.filePath) === resolvedExclude) {continue;}
1090
+
1091
+ const tail = await readTail(file.filePath);
1092
+ if (!tail) {continue;}
1093
+ const lines = tail.split("\n").filter(Boolean);
1094
+ for (const line of lines) {
1095
+ let entry: any;
1096
+ try {
1097
+ entry = JSON.parse(line);
1098
+ } catch {
1099
+ continue;
1100
+ }
1101
+ if (entry?.type !== "message") {continue;}
1102
+ const message = entry?.message;
1103
+ if (!message || message.role !== "user" || !Array.isArray(message.content)) {continue;}
1104
+ const text = extractText(message.content);
1105
+ if (!text) {continue;}
1106
+ const timestamp = Number(message.timestamp ?? entry.timestamp ?? Date.now());
1107
+ prompts.push({ text, timestamp });
1108
+ if (prompts.length >= MAX_RECENT_PROMPTS) {break;}
1109
+ }
1110
+ if (prompts.length >= MAX_RECENT_PROMPTS) {break;}
1111
+ }
1112
+
1113
+ return prompts;
1114
1114
  }
1115
1115
 
1116
1116
  function buildHistoryList(currentSession: PromptEntry[], previousSessions: PromptEntry[]): PromptEntry[] {
1117
- const all = [...currentSession, ...previousSessions];
1118
- all.sort((a, b) => a.timestamp - b.timestamp);
1119
-
1120
- const seen = new Set<string>();
1121
- const deduped: PromptEntry[] = [];
1122
- for (const prompt of all) {
1123
- const key = `${prompt.timestamp}:${prompt.text}`;
1124
- if (seen.has(key)) continue;
1125
- seen.add(key);
1126
- deduped.push(prompt);
1127
- }
1128
-
1129
- return deduped.slice(-MAX_HISTORY_ENTRIES);
1117
+ const all = [...currentSession, ...previousSessions];
1118
+ all.sort((a, b) => a.timestamp - b.timestamp);
1119
+
1120
+ const seen = new Set<string>();
1121
+ const deduped: PromptEntry[] = [];
1122
+ for (const prompt of all) {
1123
+ const key = `${prompt.timestamp}:${prompt.text}`;
1124
+ if (seen.has(key)) {continue;}
1125
+ seen.add(key);
1126
+ deduped.push(prompt);
1127
+ }
1128
+
1129
+ return deduped.slice(-MAX_HISTORY_ENTRIES);
1130
1130
  }
1131
1131
 
1132
1132
  // Overlay mode state ("custom"). Not selectable, not cycled into.
@@ -1135,181 +1135,181 @@ let customOverlay: ModeSpec | null = null;
1135
1135
  let loadCounter = 0;
1136
1136
 
1137
1137
  function historiesMatch(a: PromptEntry[], b: PromptEntry[]): boolean {
1138
- if (a.length !== b.length) return false;
1139
- for (let i = 0; i < a.length; i += 1) {
1140
- if (a[i]?.text !== b[i]?.text || a[i]?.timestamp !== b[i]?.timestamp) return false;
1141
- }
1142
- return true;
1138
+ if (a.length !== b.length) {return false;}
1139
+ for (let i = 0; i < a.length; i += 1) {
1140
+ if (a[i]?.text !== b[i]?.text || a[i]?.timestamp !== b[i]?.timestamp) {return false;}
1141
+ }
1142
+ return true;
1143
1143
  }
1144
1144
 
1145
1145
  function setEditor(pi: ExtensionAPI, ctx: ExtensionContext, history: PromptEntry[]) {
1146
- ctx.ui.setEditorComponent((tui, theme, keybindings) => {
1147
- const editor = new PromptEditor(tui, theme, keybindings);
1148
- requestEditorRender = () => editor.requestRenderNow();
1149
- editor.modeLabelProvider = () => runtime.currentMode;
1146
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => {
1147
+ const editor = new PromptEditor(tui, theme, keybindings);
1148
+ requestEditorRender = () => editor.requestRenderNow();
1149
+ editor.modeLabelProvider = () => runtime.currentMode;
1150
1150
  // Keep the mode label color stable (match footer/status bar).
1151
- editor.modeLabelColor = (text: string) => ctx.ui.theme.fg("dim", text);
1152
- const borderColor = (text: string) => {
1153
- const isBashMode = editor.getText().trimStart().startsWith("!");
1154
- if (isBashMode) {
1155
- return ctx.ui.theme.getBashModeBorderColor()(text);
1156
- }
1157
- return getModeBorderColor(ctx, pi, runtime.currentMode)(text);
1158
- };
1159
-
1160
- editor.borderColor = borderColor;
1161
- editor.lockBorderColor();
1162
- for (const prompt of history) {
1163
- editor.addToHistory?.(prompt.text);
1164
- }
1165
- return editor;
1166
- });
1151
+ editor.modeLabelColor = (text: string) => ctx.ui.theme.fg("dim", text);
1152
+ const borderColor = (text: string) => {
1153
+ const isBashMode = editor.getText().trimStart().startsWith("!");
1154
+ if (isBashMode) {
1155
+ return ctx.ui.theme.getBashModeBorderColor()(text);
1156
+ }
1157
+ return getModeBorderColor(ctx, pi, runtime.currentMode)(text);
1158
+ };
1159
+
1160
+ editor.borderColor = borderColor;
1161
+ editor.lockBorderColor();
1162
+ for (const prompt of history) {
1163
+ editor.addToHistory?.(prompt.text);
1164
+ }
1165
+ return editor;
1166
+ });
1167
1167
  }
1168
1168
 
1169
1169
  function applyEditor(pi: ExtensionAPI, ctx: ExtensionContext) {
1170
- if (!ctx.hasUI) return;
1171
-
1172
- const sessionFile = ctx.sessionManager.getSessionFile();
1173
- const currentEntries = ctx.sessionManager.getBranch();
1174
- const currentPrompts = collectUserPromptsFromEntries(currentEntries);
1175
- const immediateHistory = buildHistoryList(currentPrompts, []);
1176
-
1177
- const currentLoad = ++loadCounter;
1178
- const initialText = ctx.ui.getEditorText();
1179
- setEditor(pi, ctx, immediateHistory);
1180
-
1181
- void (async () => {
1182
- const previousPrompts = await loadPromptHistoryForCwd(ctx.cwd, sessionFile ?? undefined);
1183
- if (currentLoad !== loadCounter) return;
1184
- if (ctx.ui.getEditorText() !== initialText) return;
1185
- const history = buildHistoryList(currentPrompts, previousPrompts);
1186
- if (historiesMatch(history, immediateHistory)) return;
1187
- setEditor(pi, ctx, history);
1188
- })();
1170
+ if (!ctx.hasUI) {return;}
1171
+
1172
+ const sessionFile = ctx.sessionManager.getSessionFile();
1173
+ const currentEntries = ctx.sessionManager.getBranch();
1174
+ const currentPrompts = collectUserPromptsFromEntries(currentEntries);
1175
+ const immediateHistory = buildHistoryList(currentPrompts, []);
1176
+
1177
+ const currentLoad = ++loadCounter;
1178
+ const initialText = ctx.ui.getEditorText();
1179
+ setEditor(pi, ctx, immediateHistory);
1180
+
1181
+ void (async() => {
1182
+ const previousPrompts = await loadPromptHistoryForCwd(ctx.cwd, sessionFile ?? undefined);
1183
+ if (currentLoad !== loadCounter) {return;}
1184
+ if (ctx.ui.getEditorText() !== initialText) {return;}
1185
+ const history = buildHistoryList(currentPrompts, previousPrompts);
1186
+ if (historiesMatch(history, immediateHistory)) {return;}
1187
+ setEditor(pi, ctx, history);
1188
+ })();
1189
1189
  }
1190
1190
 
1191
1191
  // =============================================================================
1192
1192
  // Extension Export
1193
1193
  // =============================================================================
1194
1194
 
1195
- export default function (pi: ExtensionAPI) {
1196
- pi.registerCommand("mode", {
1197
- description: "Select prompt mode",
1198
- handler: async (args, ctx) => {
1199
- const tokens = args
1200
- .split(/\s+/)
1201
- .map((x) => x.trim())
1202
- .filter(Boolean);
1195
+ export default function(pi: ExtensionAPI) {
1196
+ pi.registerCommand("mode", {
1197
+ description: "Select prompt mode",
1198
+ handler: async(args, ctx) => {
1199
+ const tokens = args
1200
+ .split(/\s+/)
1201
+ .map((x) => x.trim())
1202
+ .filter(Boolean);
1203
1203
 
1204
1204
  // /mode
1205
- if (tokens.length === 0) {
1206
- await selectModeUI(pi, ctx);
1207
- return;
1208
- }
1205
+ if (tokens.length === 0) {
1206
+ await selectModeUI(pi, ctx);
1207
+ return;
1208
+ }
1209
1209
 
1210
1210
  // /mode store [name]
1211
- if (tokens[0] === "store") {
1212
- await ensureRuntime(pi, ctx);
1213
-
1214
- let target = tokens[1];
1215
- if (!target) {
1216
- if (!ctx.hasUI) return;
1217
- const names = orderedModeNames(runtime.data.modes);
1218
- target = await ctx.ui.select("Store current selection into mode", names);
1219
- if (!target) return;
1220
- }
1221
-
1222
- if (target === CUSTOM_MODE_NAME) {
1223
- if (ctx.hasUI) ctx.ui.notify(`Cannot store into "${CUSTOM_MODE_NAME}"`, "warning");
1224
- return;
1225
- }
1226
-
1227
- const selection = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
1228
- await storeSelectionIntoMode(pi, ctx, target, selection);
1229
- if (ctx.hasUI) ctx.ui.notify(`Stored current selection into "${target}"`, "info");
1230
- return;
1231
- }
1211
+ if (tokens[0] === "store") {
1212
+ await ensureRuntime(pi, ctx);
1213
+
1214
+ let target = tokens[1];
1215
+ if (!target) {
1216
+ if (!ctx.hasUI) {return;}
1217
+ const names = orderedModeNames(runtime.data.modes);
1218
+ target = await ctx.ui.select("Store current selection into mode", names);
1219
+ if (!target) {return;}
1220
+ }
1221
+
1222
+ if (target === CUSTOM_MODE_NAME) {
1223
+ if (ctx.hasUI) {ctx.ui.notify(`Cannot store into "${CUSTOM_MODE_NAME}"`, "warning");}
1224
+ return;
1225
+ }
1226
+
1227
+ const selection = customOverlay ?? getCurrentSelectionSpec(pi, ctx);
1228
+ await storeSelectionIntoMode(pi, ctx, target, selection);
1229
+ if (ctx.hasUI) {ctx.ui.notify(`Stored current selection into "${target}"`, "info");}
1230
+ return;
1231
+ }
1232
1232
 
1233
1233
  // /mode <name>
1234
- await applyMode(pi, ctx, tokens[0]!);
1235
- },
1236
- });
1237
-
1238
- pi.registerShortcut("ctrl+shift+m", {
1239
- description: "Select prompt mode",
1240
- handler: async (ctx) => {
1241
- await selectModeUI(pi, ctx);
1242
- },
1243
- });
1244
-
1245
- pi.registerShortcut("ctrl+space", {
1246
- description: "Cycle prompt mode",
1247
- handler: async (ctx) => {
1248
- await cycleMode(pi, ctx, 1);
1249
- },
1250
- });
1251
-
1252
- pi.on("session_start", async (_event, ctx) => {
1253
- lastObservedModel = { provider: ctx.model?.provider, modelId: ctx.model?.id };
1254
- await ensureRuntime(pi, ctx);
1255
- customOverlay = null;
1256
-
1257
- const inferred = inferModeFromSelection(ctx, pi, runtime.data);
1258
- if (inferred) {
1259
- runtime.currentMode = inferred;
1260
- runtime.lastRealMode = inferred;
1261
- } else {
1234
+ await applyMode(pi, ctx, tokens[0]!);
1235
+ },
1236
+ });
1237
+
1238
+ pi.registerShortcut("ctrl+shift+m", {
1239
+ description: "Select prompt mode",
1240
+ handler: async(ctx) => {
1241
+ await selectModeUI(pi, ctx);
1242
+ },
1243
+ });
1244
+
1245
+ pi.registerShortcut("ctrl+space", {
1246
+ description: "Cycle prompt mode",
1247
+ handler: async(ctx) => {
1248
+ await cycleMode(pi, ctx, 1);
1249
+ },
1250
+ });
1251
+
1252
+ pi.on("session_start", async(_event, ctx) => {
1253
+ lastObservedModel = { provider: ctx.model?.provider, modelId: ctx.model?.id };
1254
+ await ensureRuntime(pi, ctx);
1255
+ customOverlay = null;
1256
+
1257
+ const inferred = inferModeFromSelection(ctx, pi, runtime.data);
1258
+ if (inferred) {
1259
+ runtime.currentMode = inferred;
1260
+ runtime.lastRealMode = inferred;
1261
+ } else {
1262
1262
  // No exact match → treat as overlay.
1263
- runtime.currentMode = CUSTOM_MODE_NAME;
1264
- customOverlay = getCurrentSelectionSpec(pi, ctx);
1265
- }
1263
+ runtime.currentMode = CUSTOM_MODE_NAME;
1264
+ customOverlay = getCurrentSelectionSpec(pi, ctx);
1265
+ }
1266
1266
 
1267
- applyEditor(pi, ctx);
1268
- });
1267
+ applyEditor(pi, ctx);
1268
+ });
1269
1269
 
1270
- pi.on("session_switch", async (_event, ctx) => {
1271
- lastObservedModel = { provider: ctx.model?.provider, modelId: ctx.model?.id };
1272
- await ensureRuntime(pi, ctx);
1273
- customOverlay = null;
1270
+ pi.on("session_switch", async(_event, ctx) => {
1271
+ lastObservedModel = { provider: ctx.model?.provider, modelId: ctx.model?.id };
1272
+ await ensureRuntime(pi, ctx);
1273
+ customOverlay = null;
1274
1274
 
1275
- const inferred = inferModeFromSelection(ctx, pi, runtime.data);
1276
- if (inferred) {
1277
- runtime.currentMode = inferred;
1278
- runtime.lastRealMode = inferred;
1279
- } else {
1280
- runtime.currentMode = CUSTOM_MODE_NAME;
1281
- customOverlay = getCurrentSelectionSpec(pi, ctx);
1282
- }
1275
+ const inferred = inferModeFromSelection(ctx, pi, runtime.data);
1276
+ if (inferred) {
1277
+ runtime.currentMode = inferred;
1278
+ runtime.lastRealMode = inferred;
1279
+ } else {
1280
+ runtime.currentMode = CUSTOM_MODE_NAME;
1281
+ customOverlay = getCurrentSelectionSpec(pi, ctx);
1282
+ }
1283
1283
 
1284
- applyEditor(pi, ctx);
1285
- });
1284
+ applyEditor(pi, ctx);
1285
+ });
1286
1286
 
1287
1287
 
1288
- pi.on("model_select", async (event: ModelSelectEvent, ctx) => {
1288
+ pi.on("model_select", async(event: ModelSelectEvent, ctx) => {
1289
1289
  // Always track the last observed model for overlay/store correctness.
1290
- lastObservedModel = { provider: event.model.provider, modelId: event.model.id };
1290
+ lastObservedModel = { provider: event.model.provider, modelId: event.model.id };
1291
1291
 
1292
1292
  // Skip mode switching triggered by applyMode() itself, otherwise we'd jump to "custom"
1293
1293
  // while we are in the middle of applying a mode.
1294
- if (runtime.applying) return;
1294
+ if (runtime.applying) {return;}
1295
1295
 
1296
1296
  // Manual model changes always go into the overlay "custom" mode.
1297
- await ensureRuntime(pi, ctx);
1298
- if (runtime.currentMode !== CUSTOM_MODE_NAME) {
1299
- runtime.lastRealMode = runtime.currentMode;
1300
- }
1301
- runtime.currentMode = CUSTOM_MODE_NAME;
1302
-
1303
- customOverlay = {
1304
- provider: event.model.provider,
1305
- modelId: event.model.id,
1306
- thinkingLevel: pi.getThinkingLevel(),
1307
- };
1297
+ await ensureRuntime(pi, ctx);
1298
+ if (runtime.currentMode !== CUSTOM_MODE_NAME) {
1299
+ runtime.lastRealMode = runtime.currentMode;
1300
+ }
1301
+ runtime.currentMode = CUSTOM_MODE_NAME;
1302
+
1303
+ customOverlay = {
1304
+ provider: event.model.provider,
1305
+ modelId: event.model.id,
1306
+ thinkingLevel: pi.getThinkingLevel(),
1307
+ };
1308
1308
 
1309
1309
  // Do not persist/select custom.
1310
- if (ctx.hasUI) {
1311
- requestEditorRender?.();
1312
- }
1313
- });
1310
+ if (ctx.hasUI) {
1311
+ requestEditorRender?.();
1312
+ }
1313
+ });
1314
1314
 
1315
1315
  }