@docyrus/docyrus 0.0.34 → 0.0.35

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
@@ -14,15 +14,15 @@ import { complete, type Model, type Api, type UserMessage } from "@mariozechner/
14
14
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
15
15
  import { BorderedLoader } from "@mariozechner/pi-coding-agent";
16
16
  import {
17
- type Component,
18
- Editor,
19
- type EditorTheme,
20
- Key,
21
- matchesKey,
22
- truncateToWidth,
23
- type TUI,
24
- visibleWidth,
25
- wrapTextWithAnsi,
17
+ type Component,
18
+ Editor,
19
+ type EditorTheme,
20
+ Key,
21
+ matchesKey,
22
+ truncateToWidth,
23
+ type TUI,
24
+ visibleWidth,
25
+ wrapTextWithAnsi,
26
26
  } from "@mariozechner/pi-tui";
27
27
 
28
28
  // Structured output format for question extraction
@@ -74,459 +74,467 @@ const HAIKU_MODEL_ID = "claude-haiku-4-5";
74
74
  * Prefer Codex mini for extraction when available, otherwise fallback to haiku or the current model.
75
75
  */
76
76
  async function selectExtractionModel(
77
- currentModel: Model<Api>,
78
- modelRegistry: {
77
+ currentModel: Model<Api>,
78
+ modelRegistry: {
79
79
  find: (provider: string, modelId: string) => Model<Api> | undefined;
80
80
  getApiKey: (model: Model<Api>) => Promise<string | undefined>;
81
81
  },
82
82
  ): Promise<Model<Api>> {
83
- const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
84
- if (codexModel) {
85
- const apiKey = await modelRegistry.getApiKey(codexModel);
86
- if (apiKey) {
87
- return codexModel;
88
- }
89
- }
90
-
91
- const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
92
- if (!haikuModel) {
93
- return currentModel;
94
- }
95
-
96
- const apiKey = await modelRegistry.getApiKey(haikuModel);
97
- if (!apiKey) {
98
- return currentModel;
99
- }
100
-
101
- return haikuModel;
83
+ const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
84
+ if (codexModel) {
85
+ const apiKey = await modelRegistry.getApiKey(codexModel);
86
+ if (apiKey) {
87
+ return codexModel;
88
+ }
89
+ }
90
+
91
+ const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
92
+ if (!haikuModel) {
93
+ return currentModel;
94
+ }
95
+
96
+ const apiKey = await modelRegistry.getApiKey(haikuModel);
97
+ if (!apiKey) {
98
+ return currentModel;
99
+ }
100
+
101
+ return haikuModel;
102
102
  }
103
103
 
104
104
  /**
105
105
  * Parse the JSON response from the LLM
106
106
  */
107
107
  function parseExtractionResult(text: string): ExtractionResult | null {
108
- try {
108
+ try {
109
109
  // Try to find JSON in the response (it might be wrapped in markdown code blocks)
110
- let jsonStr = text;
110
+ let jsonStr = text;
111
111
 
112
112
  // Remove markdown code block if present
113
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
114
- if (jsonMatch) {
115
- jsonStr = jsonMatch[1].trim();
116
- }
117
-
118
- const parsed = JSON.parse(jsonStr);
119
- if (parsed && Array.isArray(parsed.questions)) {
120
- return parsed as ExtractionResult;
121
- }
122
- return null;
123
- } catch {
124
- return null;
125
- }
113
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
114
+ if (jsonMatch) {
115
+ jsonStr = jsonMatch[1].trim();
116
+ }
117
+
118
+ const parsed = JSON.parse(jsonStr);
119
+ if (parsed && Array.isArray(parsed.questions)) {
120
+ return parsed as ExtractionResult;
121
+ }
122
+ return null;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
126
127
  }
127
128
 
128
129
  /**
129
130
  * Interactive Q&A component for answering extracted questions
130
131
  */
131
132
  class QnAComponent implements Component {
132
- private questions: ExtractedQuestion[];
133
- private answers: string[];
134
- private currentIndex: number = 0;
135
- private editor: Editor;
136
- private tui: TUI;
137
- private onDone: (result: string | null) => void;
138
- private showingConfirmation: boolean = false;
133
+ private questions: ExtractedQuestion[];
134
+ private answers: string[];
135
+ private currentIndex = 0;
136
+ private editor: Editor;
137
+ private tui: TUI;
138
+ private onDone: (result: string | null) => void;
139
+ private showingConfirmation = false;
139
140
 
140
141
  // Cache
141
- private cachedWidth?: number;
142
- private cachedLines?: string[];
142
+ private cachedWidth?: number;
143
+ private cachedLines?: string[];
143
144
 
144
145
  // Colors - using proper reset sequences
145
- private dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
146
- private bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
147
- private cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
148
- private green = (s: string) => `\x1b[32m${s}\x1b[0m`;
149
- private yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
150
- private gray = (s: string) => `\x1b[90m${s}\x1b[0m`;
151
-
152
- constructor(
153
- questions: ExtractedQuestion[],
154
- tui: TUI,
155
- onDone: (result: string | null) => void,
156
- ) {
157
- this.questions = questions;
158
- this.answers = questions.map(() => "");
159
- this.tui = tui;
160
- this.onDone = onDone;
146
+ private dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
147
+ private bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
148
+ private cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
149
+ private green = (s: string) => `\x1b[32m${s}\x1b[0m`;
150
+ private yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
151
+ private gray = (s: string) => `\x1b[90m${s}\x1b[0m`;
152
+
153
+ constructor(
154
+ questions: ExtractedQuestion[],
155
+ tui: TUI,
156
+ onDone: (result: string | null) => void,
157
+ ) {
158
+ this.questions = questions;
159
+ this.answers = questions.map(() => "");
160
+ this.tui = tui;
161
+ this.onDone = onDone;
161
162
 
162
163
  // Create a minimal theme for the editor
163
- const editorTheme: EditorTheme = {
164
- borderColor: this.dim,
165
- selectList: {
166
- selectedBg: (s: string) => `\x1b[44m${s}\x1b[0m`,
167
- matchHighlight: this.cyan,
168
- itemSecondary: this.gray,
169
- },
170
- };
171
-
172
- this.editor = new Editor(tui, editorTheme);
164
+ const editorTheme: EditorTheme = {
165
+ borderColor: this.dim,
166
+ selectList: {
167
+ selectedBg: (s: string) => `\x1b[44m${s}\x1b[0m`,
168
+ matchHighlight: this.cyan,
169
+ itemSecondary: this.gray,
170
+ },
171
+ };
172
+
173
+ this.editor = new Editor(tui, editorTheme);
173
174
  // Disable the editor's built-in submit (which clears the editor)
174
175
  // We'll handle Enter ourselves to preserve the text
175
- this.editor.disableSubmit = true;
176
- this.editor.onChange = () => {
177
- this.invalidate();
178
- this.tui.requestRender();
179
- };
180
- }
181
-
182
- private allQuestionsAnswered(): boolean {
183
- this.saveCurrentAnswer();
184
- return this.answers.every((a) => (a?.trim() || "").length > 0);
185
- }
186
-
187
- private saveCurrentAnswer(): void {
188
- this.answers[this.currentIndex] = this.editor.getText();
189
- }
190
-
191
- private navigateTo(index: number): void {
192
- if (index < 0 || index >= this.questions.length) return;
193
- this.saveCurrentAnswer();
194
- this.currentIndex = index;
195
- this.editor.setText(this.answers[index] || "");
196
- this.invalidate();
197
- }
198
-
199
- private submit(): void {
200
- this.saveCurrentAnswer();
176
+ this.editor.disableSubmit = true;
177
+ this.editor.onChange = () => {
178
+ this.invalidate();
179
+ this.tui.requestRender();
180
+ };
181
+ }
182
+
183
+ private allQuestionsAnswered(): boolean {
184
+ this.saveCurrentAnswer();
185
+ return this.answers.every((a) => (a?.trim() || "").length > 0);
186
+ }
187
+
188
+ private saveCurrentAnswer(): void {
189
+ this.answers[this.currentIndex] = this.editor.getText();
190
+ }
191
+
192
+ private navigateTo(index: number): void {
193
+ if (index < 0 || index >= this.questions.length) {
194
+ return;
195
+ }
196
+ this.saveCurrentAnswer();
197
+ this.currentIndex = index;
198
+ this.editor.setText(this.answers[index] || "");
199
+ this.invalidate();
200
+ }
201
+
202
+ private submit(): void {
203
+ this.saveCurrentAnswer();
201
204
 
202
205
  // Build the response text
203
- const parts: string[] = [];
204
- for (let i = 0; i < this.questions.length; i++) {
205
- const q = this.questions[i];
206
- const a = this.answers[i]?.trim() || "(no answer)";
207
- parts.push(`Q: ${q.question}`);
208
- if (q.context) {
209
- parts.push(`> ${q.context}`);
210
- }
211
- parts.push(`A: ${a}`);
212
- parts.push("");
213
- }
214
-
215
- this.onDone(parts.join("\n").trim());
216
- }
217
-
218
- private cancel(): void {
219
- this.onDone(null);
220
- }
221
-
222
- invalidate(): void {
223
- this.cachedWidth = undefined;
224
- this.cachedLines = undefined;
225
- }
226
-
227
- handleInput(data: string): void {
206
+ const parts: string[] = [];
207
+ for (let i = 0; i < this.questions.length; i++) {
208
+ const q = this.questions[i];
209
+ const a = this.answers[i]?.trim() || "(no answer)";
210
+ parts.push(`Q: ${q.question}`);
211
+ if (q.context) {
212
+ parts.push(`> ${q.context}`);
213
+ }
214
+ parts.push(`A: ${a}`);
215
+ parts.push("");
216
+ }
217
+
218
+ this.onDone(parts.join("\n").trim());
219
+ }
220
+
221
+ private cancel(): void {
222
+ this.onDone(null);
223
+ }
224
+
225
+ invalidate(): void {
226
+ this.cachedWidth = undefined;
227
+ this.cachedLines = undefined;
228
+ }
229
+
230
+ handleInput(data: string): void {
228
231
  // Handle confirmation dialog
229
- if (this.showingConfirmation) {
230
- if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
231
- this.submit();
232
- return;
233
- }
234
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data.toLowerCase() === "n") {
235
- this.showingConfirmation = false;
236
- this.invalidate();
237
- this.tui.requestRender();
238
- return;
239
- }
240
- return;
241
- }
232
+ if (this.showingConfirmation) {
233
+ if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
234
+ this.submit();
235
+ return;
236
+ }
237
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data.toLowerCase() === "n") {
238
+ this.showingConfirmation = false;
239
+ this.invalidate();
240
+ this.tui.requestRender();
241
+ return;
242
+ }
243
+ return;
244
+ }
242
245
 
243
246
  // Global navigation and commands
244
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
245
- this.cancel();
246
- return;
247
- }
247
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
248
+ this.cancel();
249
+ return;
250
+ }
248
251
 
249
252
  // Tab / Shift+Tab for navigation
250
- if (matchesKey(data, Key.tab)) {
251
- if (this.currentIndex < this.questions.length - 1) {
252
- this.navigateTo(this.currentIndex + 1);
253
- this.tui.requestRender();
254
- }
255
- return;
256
- }
257
- if (matchesKey(data, Key.shift("tab"))) {
258
- if (this.currentIndex > 0) {
259
- this.navigateTo(this.currentIndex - 1);
260
- this.tui.requestRender();
261
- }
262
- return;
263
- }
253
+ if (matchesKey(data, Key.tab)) {
254
+ if (this.currentIndex < this.questions.length - 1) {
255
+ this.navigateTo(this.currentIndex + 1);
256
+ this.tui.requestRender();
257
+ }
258
+ return;
259
+ }
260
+ if (matchesKey(data, Key.shift("tab"))) {
261
+ if (this.currentIndex > 0) {
262
+ this.navigateTo(this.currentIndex - 1);
263
+ this.tui.requestRender();
264
+ }
265
+ return;
266
+ }
264
267
 
265
268
  // Arrow up/down for question navigation when editor is empty
266
269
  // (Editor handles its own cursor navigation when there's content)
267
- if (matchesKey(data, Key.up) && this.editor.getText() === "") {
268
- if (this.currentIndex > 0) {
269
- this.navigateTo(this.currentIndex - 1);
270
- this.tui.requestRender();
271
- return;
272
- }
273
- }
274
- if (matchesKey(data, Key.down) && this.editor.getText() === "") {
275
- if (this.currentIndex < this.questions.length - 1) {
276
- this.navigateTo(this.currentIndex + 1);
277
- this.tui.requestRender();
278
- return;
279
- }
280
- }
270
+ if (matchesKey(data, Key.up) && this.editor.getText() === "") {
271
+ if (this.currentIndex > 0) {
272
+ this.navigateTo(this.currentIndex - 1);
273
+ this.tui.requestRender();
274
+ return;
275
+ }
276
+ }
277
+ if (matchesKey(data, Key.down) && this.editor.getText() === "") {
278
+ if (this.currentIndex < this.questions.length - 1) {
279
+ this.navigateTo(this.currentIndex + 1);
280
+ this.tui.requestRender();
281
+ return;
282
+ }
283
+ }
281
284
 
282
285
  // Handle Enter ourselves (editor's submit is disabled)
283
286
  // Plain Enter moves to next question or shows confirmation on last question
284
287
  // Shift+Enter adds a newline (handled by editor)
285
- if (matchesKey(data, Key.enter) && !matchesKey(data, Key.shift("enter"))) {
286
- this.saveCurrentAnswer();
287
- if (this.currentIndex < this.questions.length - 1) {
288
- this.navigateTo(this.currentIndex + 1);
289
- } else {
288
+ if (matchesKey(data, Key.enter) && !matchesKey(data, Key.shift("enter"))) {
289
+ this.saveCurrentAnswer();
290
+ if (this.currentIndex < this.questions.length - 1) {
291
+ this.navigateTo(this.currentIndex + 1);
292
+ }
293
+ else {
290
294
  // On last question - show confirmation
291
- this.showingConfirmation = true;
292
- }
293
- this.invalidate();
294
- this.tui.requestRender();
295
- return;
296
- }
295
+ this.showingConfirmation = true;
296
+ }
297
+ this.invalidate();
298
+ this.tui.requestRender();
299
+ return;
300
+ }
297
301
 
298
302
  // Pass to editor
299
- this.editor.handleInput(data);
300
- this.invalidate();
301
- this.tui.requestRender();
302
- }
303
-
304
- render(width: number): string[] {
305
- if (this.cachedLines && this.cachedWidth === width) {
306
- return this.cachedLines;
307
- }
303
+ this.editor.handleInput(data);
304
+ this.invalidate();
305
+ this.tui.requestRender();
306
+ }
307
+
308
+ render(width: number): string[] {
309
+ if (this.cachedLines && this.cachedWidth === width) {
310
+ return this.cachedLines;
311
+ }
308
312
 
309
- const lines: string[] = [];
310
- const boxWidth = Math.min(width - 4, 120); // Allow wider box
311
- const contentWidth = boxWidth - 4; // 2 chars padding on each side
313
+ const lines: string[] = [];
314
+ const boxWidth = Math.min(width - 4, 120); // Allow wider box
315
+ const contentWidth = boxWidth - 4; // 2 chars padding on each side
312
316
 
313
317
  // Helper to create horizontal lines (dim the whole thing at once)
314
- const horizontalLine = (count: number) => "─".repeat(count);
318
+ const horizontalLine = (count: number) => "─".repeat(count);
315
319
 
316
320
  // Helper to create a box line
317
- const boxLine = (content: string, leftPad: number = 2): string => {
318
- const paddedContent = " ".repeat(leftPad) + content;
319
- const contentLen = visibleWidth(paddedContent);
320
- const rightPad = Math.max(0, boxWidth - contentLen - 2);
321
- return this.dim("│") + paddedContent + " ".repeat(rightPad) + this.dim("│");
322
- };
323
-
324
- const emptyBoxLine = (): string => {
325
- return this.dim("│") + " ".repeat(boxWidth - 2) + this.dim("│");
326
- };
327
-
328
- const padToWidth = (line: string): string => {
329
- const len = visibleWidth(line);
330
- return line + " ".repeat(Math.max(0, width - len));
331
- };
321
+ const boxLine = (content: string, leftPad = 2): string => {
322
+ const paddedContent = " ".repeat(leftPad) + content;
323
+ const contentLen = visibleWidth(paddedContent);
324
+ const rightPad = Math.max(0, boxWidth - contentLen - 2);
325
+ return this.dim("│") + paddedContent + " ".repeat(rightPad) + this.dim("│");
326
+ };
327
+
328
+ const emptyBoxLine = (): string => {
329
+ return this.dim("│") + " ".repeat(boxWidth - 2) + this.dim("│");
330
+ };
331
+
332
+ const padToWidth = (line: string): string => {
333
+ const len = visibleWidth(line);
334
+ return line + " ".repeat(Math.max(0, width - len));
335
+ };
332
336
 
333
337
  // Title
334
- lines.push(padToWidth(this.dim("╭" + horizontalLine(boxWidth - 2) + "╮")));
335
- const title = `${this.bold(this.cyan("Questions"))} ${this.dim(`(${this.currentIndex + 1}/${this.questions.length})`)}`;
336
- lines.push(padToWidth(boxLine(title)));
337
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
338
+ lines.push(padToWidth(this.dim("╭" + horizontalLine(boxWidth - 2) + "╮")));
339
+ const title = `${this.bold(this.cyan("Questions"))} ${this.dim(`(${this.currentIndex + 1}/${this.questions.length})`)}`;
340
+ lines.push(padToWidth(boxLine(title)));
341
+ lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
338
342
 
339
343
  // Progress indicator
340
- const progressParts: string[] = [];
341
- for (let i = 0; i < this.questions.length; i++) {
342
- const answered = (this.answers[i]?.trim() || "").length > 0;
343
- const current = i === this.currentIndex;
344
- if (current) {
345
- progressParts.push(this.cyan("●"));
346
- } else if (answered) {
347
- progressParts.push(this.green("●"));
348
- } else {
349
- progressParts.push(this.dim("○"));
350
- }
351
- }
352
- lines.push(padToWidth(boxLine(progressParts.join(" "))));
353
- lines.push(padToWidth(emptyBoxLine()));
344
+ const progressParts: string[] = [];
345
+ for (let i = 0; i < this.questions.length; i++) {
346
+ const answered = (this.answers[i]?.trim() || "").length > 0;
347
+ const current = i === this.currentIndex;
348
+ if (current) {
349
+ progressParts.push(this.cyan("●"));
350
+ }
351
+ else if (answered) {
352
+ progressParts.push(this.green("●"));
353
+ }
354
+ else {
355
+ progressParts.push(this.dim("○"));
356
+ }
357
+ }
358
+ lines.push(padToWidth(boxLine(progressParts.join(" "))));
359
+ lines.push(padToWidth(emptyBoxLine()));
354
360
 
355
361
  // Current question
356
- const q = this.questions[this.currentIndex];
357
- const questionText = `${this.bold("Q:")} ${q.question}`;
358
- const wrappedQuestion = wrapTextWithAnsi(questionText, contentWidth);
359
- for (const line of wrappedQuestion) {
360
- lines.push(padToWidth(boxLine(line)));
361
- }
362
+ const q = this.questions[this.currentIndex];
363
+ const questionText = `${this.bold("Q:")} ${q.question}`;
364
+ const wrappedQuestion = wrapTextWithAnsi(questionText, contentWidth);
365
+ for (const line of wrappedQuestion) {
366
+ lines.push(padToWidth(boxLine(line)));
367
+ }
362
368
 
363
369
  // Context if present
364
- if (q.context) {
365
- lines.push(padToWidth(emptyBoxLine()));
366
- const contextText = this.gray(`> ${q.context}`);
367
- const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
368
- for (const line of wrappedContext) {
369
- lines.push(padToWidth(boxLine(line)));
370
- }
371
- }
370
+ if (q.context) {
371
+ lines.push(padToWidth(emptyBoxLine()));
372
+ const contextText = this.gray(`> ${q.context}`);
373
+ const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
374
+ for (const line of wrappedContext) {
375
+ lines.push(padToWidth(boxLine(line)));
376
+ }
377
+ }
372
378
 
373
- lines.push(padToWidth(emptyBoxLine()));
379
+ lines.push(padToWidth(emptyBoxLine()));
374
380
 
375
381
  // Render the editor component (multi-line input) with padding
376
382
  // Skip the first and last lines (editor's own border lines)
377
- const answerPrefix = this.bold("A: ");
378
- const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
379
- const editorLines = this.editor.render(editorWidth);
380
- for (let i = 1; i < editorLines.length - 1; i++) {
381
- if (i === 1) {
383
+ const answerPrefix = this.bold("A: ");
384
+ const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
385
+ const editorLines = this.editor.render(editorWidth);
386
+ for (let i = 1; i < editorLines.length - 1; i++) {
387
+ if (i === 1) {
382
388
  // First content line gets the "A: " prefix
383
- lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
384
- } else {
389
+ lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
390
+ }
391
+ else {
385
392
  // Subsequent lines get padding to align with the first line
386
- lines.push(padToWidth(boxLine(" " + editorLines[i])));
387
- }
388
- }
393
+ lines.push(padToWidth(boxLine(" " + editorLines[i])));
394
+ }
395
+ }
389
396
 
390
- lines.push(padToWidth(emptyBoxLine()));
397
+ lines.push(padToWidth(emptyBoxLine()));
391
398
 
392
399
  // Confirmation dialog or footer with controls
393
- if (this.showingConfirmation) {
394
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
395
- const confirmMsg = `${this.yellow("Submit all answers?")} ${this.dim("(Enter/y to confirm, Esc/n to cancel)")}`;
396
- lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
397
- } else {
398
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
399
- const controls = `${this.dim("Tab/Enter")} next · ${this.dim("Shift+Tab")} prev · ${this.dim("Shift+Enter")} newline · ${this.dim("Esc")} cancel`;
400
- lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
401
- }
402
- lines.push(padToWidth(this.dim("╰" + horizontalLine(boxWidth - 2) + "╯")));
403
-
404
- this.cachedWidth = width;
405
- this.cachedLines = lines;
406
- return lines;
407
- }
400
+ if (this.showingConfirmation) {
401
+ lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
402
+ const confirmMsg = `${this.yellow("Submit all answers?")} ${this.dim("(Enter/y to confirm, Esc/n to cancel)")}`;
403
+ lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
404
+ }
405
+ else {
406
+ lines.push(padToWidth(this.dim("" + horizontalLine(boxWidth - 2) + "")));
407
+ const controls = `${this.dim("Tab/Enter")} next · ${this.dim("Shift+Tab")} prev · ${this.dim("Shift+Enter")} newline · ${this.dim("Esc")} cancel`;
408
+ lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
409
+ }
410
+ lines.push(padToWidth(this.dim("╰" + horizontalLine(boxWidth - 2) + "╯")));
411
+
412
+ this.cachedWidth = width;
413
+ this.cachedLines = lines;
414
+ return lines;
415
+ }
408
416
  }
409
417
 
410
- export default function (pi: ExtensionAPI) {
411
- const answerHandler = async (ctx: ExtensionContext) => {
412
- if (!ctx.hasUI) {
413
- ctx.ui.notify("answer requires interactive mode", "error");
414
- return;
415
- }
418
+ export default function(pi: ExtensionAPI) {
419
+ const answerHandler = async(ctx: ExtensionContext) => {
420
+ if (!ctx.hasUI) {
421
+ ctx.ui.notify("answer requires interactive mode", "error");
422
+ return;
423
+ }
416
424
 
417
- if (!ctx.model) {
418
- ctx.ui.notify("No model selected", "error");
419
- return;
420
- }
425
+ if (!ctx.model) {
426
+ ctx.ui.notify("No model selected", "error");
427
+ return;
428
+ }
421
429
 
422
430
  // Find the last assistant message on the current branch
423
- const branch = ctx.sessionManager.getBranch();
424
- let lastAssistantText: string | undefined;
425
-
426
- for (let i = branch.length - 1; i >= 0; i--) {
427
- const entry = branch[i];
428
- if (entry.type === "message") {
429
- const msg = entry.message;
430
- if ("role" in msg && msg.role === "assistant") {
431
- if (msg.stopReason !== "stop") {
432
- ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
433
- return;
434
- }
435
- const textParts = msg.content
436
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
437
- .map((c) => c.text);
438
- if (textParts.length > 0) {
439
- lastAssistantText = textParts.join("\n");
440
- break;
441
- }
442
- }
443
- }
444
- }
445
-
446
- if (!lastAssistantText) {
447
- ctx.ui.notify("No assistant messages found", "error");
448
- return;
449
- }
431
+ const branch = ctx.sessionManager.getBranch();
432
+ let lastAssistantText: string | undefined;
433
+
434
+ for (let i = branch.length - 1; i >= 0; i--) {
435
+ const entry = branch[i];
436
+ if (entry.type === "message") {
437
+ const msg = entry.message;
438
+ if ("role" in msg && msg.role === "assistant") {
439
+ if (msg.stopReason !== "stop") {
440
+ ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
441
+ return;
442
+ }
443
+ const textParts = msg.content
444
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
445
+ .map((c) => c.text);
446
+ if (textParts.length > 0) {
447
+ lastAssistantText = textParts.join("\n");
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ if (!lastAssistantText) {
455
+ ctx.ui.notify("No assistant messages found", "error");
456
+ return;
457
+ }
450
458
 
451
459
  // Select the best model for extraction (prefer Codex mini, then haiku)
452
- const extractionModel = await selectExtractionModel(ctx.model, ctx.modelRegistry);
460
+ const extractionModel = await selectExtractionModel(ctx.model, ctx.modelRegistry);
453
461
 
454
462
  // Run extraction with loader UI
455
- const extractionResult = await ctx.ui.custom<ExtractionResult | null>((tui, theme, _kb, done) => {
456
- const loader = new BorderedLoader(tui, theme, `Extracting questions using ${extractionModel.id}...`);
457
- loader.onAbort = () => done(null);
458
-
459
- const doExtract = async () => {
460
- const apiKey = await ctx.modelRegistry.getApiKey(extractionModel);
461
- const userMessage: UserMessage = {
462
- role: "user",
463
- content: [{ type: "text", text: lastAssistantText! }],
464
- timestamp: Date.now(),
465
- };
466
-
467
- const response = await complete(
468
- extractionModel,
469
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
470
- { apiKey, signal: loader.signal },
471
- );
472
-
473
- if (response.stopReason === "aborted") {
474
- return null;
475
- }
476
-
477
- const responseText = response.content
478
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
479
- .map((c) => c.text)
480
- .join("\n");
481
-
482
- return parseExtractionResult(responseText);
483
- };
484
-
485
- doExtract()
486
- .then(done)
487
- .catch(() => done(null));
488
-
489
- return loader;
490
- });
491
-
492
- if (extractionResult === null) {
493
- ctx.ui.notify("Cancelled", "info");
494
- return;
495
- }
496
-
497
- if (extractionResult.questions.length === 0) {
498
- ctx.ui.notify("No questions found in the last message", "info");
499
- return;
500
- }
463
+ const extractionResult = await ctx.ui.custom<ExtractionResult | null>((tui, theme, _kb, done) => {
464
+ const loader = new BorderedLoader(tui, theme, `Extracting questions using ${extractionModel.id}...`);
465
+ loader.onAbort = () => done(null);
466
+
467
+ const doExtract = async() => {
468
+ const apiKey = await ctx.modelRegistry.getApiKey(extractionModel);
469
+ const userMessage: UserMessage = {
470
+ role: "user",
471
+ content: [{ type: "text", text: lastAssistantText! }],
472
+ timestamp: Date.now(),
473
+ };
474
+
475
+ const response = await complete(
476
+ extractionModel,
477
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
478
+ { apiKey, signal: loader.signal },
479
+ );
480
+
481
+ if (response.stopReason === "aborted") {
482
+ return null;
483
+ }
484
+
485
+ const responseText = response.content
486
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
487
+ .map((c) => c.text)
488
+ .join("\n");
489
+
490
+ return parseExtractionResult(responseText);
491
+ };
492
+
493
+ doExtract()
494
+ .then(done)
495
+ .catch(() => done(null));
496
+
497
+ return loader;
498
+ });
499
+
500
+ if (extractionResult === null) {
501
+ ctx.ui.notify("Cancelled", "info");
502
+ return;
503
+ }
504
+
505
+ if (extractionResult.questions.length === 0) {
506
+ ctx.ui.notify("No questions found in the last message", "info");
507
+ return;
508
+ }
501
509
 
502
510
  // Show the Q&A component
503
- const answersResult = await ctx.ui.custom<string | null>((tui, _theme, _kb, done) => {
504
- return new QnAComponent(extractionResult.questions, tui, done);
505
- });
511
+ const answersResult = await ctx.ui.custom<string | null>((tui, _theme, _kb, done) => {
512
+ return new QnAComponent(extractionResult.questions, tui, done);
513
+ });
506
514
 
507
- if (answersResult === null) {
508
- ctx.ui.notify("Cancelled", "info");
509
- return;
510
- }
515
+ if (answersResult === null) {
516
+ ctx.ui.notify("Cancelled", "info");
517
+ return;
518
+ }
511
519
 
512
520
  // Send the answers directly as a message and trigger a turn
513
- pi.sendMessage(
514
- {
515
- customType: "answers",
516
- content: "I answered your questions in the following way:\n\n" + answersResult,
517
- display: true,
518
- },
519
- { triggerTurn: true },
520
- );
521
- };
522
-
523
- pi.registerCommand("answer", {
524
- description: "Extract questions from last assistant message into interactive Q&A",
525
- handler: (_args, ctx) => answerHandler(ctx),
526
- });
527
-
528
- pi.registerShortcut("ctrl+.", {
529
- description: "Extract and answer questions",
530
- handler: answerHandler,
531
- });
521
+ pi.sendMessage(
522
+ {
523
+ customType: "answers",
524
+ content: "I answered your questions in the following way:\n\n" + answersResult,
525
+ display: true,
526
+ },
527
+ { triggerTurn: true },
528
+ );
529
+ };
530
+
531
+ pi.registerCommand("answer", {
532
+ description: "Extract questions from last assistant message into interactive Q&A",
533
+ handler: (_args, ctx) => answerHandler(ctx),
534
+ });
535
+
536
+ pi.registerShortcut("ctrl+.", {
537
+ description: "Extract and answer questions",
538
+ handler: answerHandler,
539
+ });
532
540
  }