@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.
- package/README.md +25 -0
- package/agent-loader.js +3 -2
- package/agent-loader.js.map +2 -2
- package/main.js +82162 -46093
- package/main.js.map +4 -4
- package/package.json +12 -3
- package/resources/chrome-tools/browser-content.js +46 -46
- package/resources/chrome-tools/browser-cookies.js +16 -16
- package/resources/chrome-tools/browser-eval.js +27 -27
- package/resources/chrome-tools/browser-hn-scraper.js +1 -1
- package/resources/chrome-tools/browser-nav.js +23 -23
- package/resources/chrome-tools/browser-pick.js +127 -127
- package/resources/chrome-tools/browser-screenshot.js +10 -10
- package/resources/chrome-tools/browser-start.js +38 -38
- package/resources/pi-agent/extensions/answer.ts +392 -384
- package/resources/pi-agent/extensions/context.ts +415 -415
- package/resources/pi-agent/extensions/control.ts +1287 -1287
- package/resources/pi-agent/extensions/diff.ts +171 -171
- package/resources/pi-agent/extensions/files.ts +155 -155
- package/resources/pi-agent/extensions/knowledge.ts +664 -0
- package/resources/pi-agent/extensions/loop.ts +375 -375
- package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
- package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
- package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
- package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
- package/resources/pi-agent/extensions/redraws.ts +14 -14
- package/resources/pi-agent/extensions/review.ts +1533 -1533
- package/resources/pi-agent/extensions/todos.ts +1735 -1735
- package/resources/pi-agent/extensions/tps.ts +40 -40
- package/resources/pi-agent/extensions/whimsical.ts +3 -3
- package/resources/pi-agent/prompts/agent-system.md +2 -0
- package/resources/pi-agent/prompts/coder-system.md +2 -0
- package/server-loader.js +82 -1
- package/server-loader.js.map +3 -3
- package/tui.mjs +2 -0
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
+
try {
|
|
109
109
|
// Try to find JSON in the response (it might be wrapped in markdown code blocks)
|
|
110
|
-
|
|
110
|
+
let jsonStr = text;
|
|
111
111
|
|
|
112
112
|
// Remove markdown code block if present
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
142
|
+
private cachedWidth?: number;
|
|
143
|
+
private cachedLines?: string[];
|
|
143
144
|
|
|
144
145
|
// Colors - using proper reset sequences
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
this.showingConfirmation = true;
|
|
296
|
+
}
|
|
297
|
+
this.invalidate();
|
|
298
|
+
this.tui.requestRender();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
297
301
|
|
|
298
302
|
// Pass to editor
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
318
|
+
const horizontalLine = (count: number) => "─".repeat(count);
|
|
315
319
|
|
|
316
320
|
// Helper to create a box line
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
389
|
+
lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
385
392
|
// Subsequent lines get padding to align with the first line
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
lines.push(padToWidth(boxLine(" " + editorLines[i])));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
389
396
|
|
|
390
|
-
|
|
397
|
+
lines.push(padToWidth(emptyBoxLine()));
|
|
391
398
|
|
|
392
399
|
// Confirmation dialog or footer with controls
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
460
|
+
const extractionModel = await selectExtractionModel(ctx.model, ctx.modelRegistry);
|
|
453
461
|
|
|
454
462
|
// Run extraction with loader UI
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
}
|