tau-all-agent 0.2.0 → 0.3.0
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/extensions/answer.ts +112 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +67 -36
- package/extensions/insights.ts +67 -56
- package/extensions/loop.ts +14 -52
- package/extensions/memory.ts +65 -33
- package/extensions/notify.ts +5 -3
- package/extensions/review/fix.ts +10 -16
- package/extensions/review/git.ts +7 -1
- package/extensions/review/index.ts +2 -2
- package/extensions/review/models.ts +12 -2
- package/extensions/review/review.ts +3 -5
- package/extensions/review/runtime.ts +9 -8
- package/extensions/review/triage.ts +2 -4
- package/extensions/sandbox/README.md +5 -0
- package/extensions/sandbox/bash.ts +83 -85
- package/extensions/sandbox/index.ts +21 -11
- package/extensions/sandbox/permissions/network.ts +8 -3
- package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
- package/extensions/sandbox/runtime.ts +53 -19
- package/extensions/subagent/README.md +58 -1
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +54 -22
- package/extensions/telegram/README.md +2 -0
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +146 -194
- package/extensions/telegram/index.ts +11 -7
- package/extensions/tool-display-mode.ts +29 -50
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +139 -80
- package/extensions/websearch/README.md +8 -1
- package/extensions/websearch/config.ts +2 -1
- package/extensions/websearch/index.ts +18 -9
- package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
- package/extensions/websearch/providers/shared.ts +10 -3
- package/extensions/websearch/types.ts +2 -1
- package/extensions/worktree.ts +33 -89
- package/package.json +1 -1
- package/skills/oracle/SKILL.md +14 -8
- package/skills/oracle/scripts/oracle +43 -19
- package/skills/update-changelog/SKILL.md +8 -4
package/extensions/answer.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Model, Api, UserMessage } from "@earendil-works/pi-ai";
|
|
|
14
14
|
import {
|
|
15
15
|
BorderedLoader,
|
|
16
16
|
type ExtensionAPI,
|
|
17
|
+
type KeybindingsManager,
|
|
17
18
|
type ModelRegistry,
|
|
18
19
|
type Theme,
|
|
19
20
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
type Focusable,
|
|
25
26
|
Key,
|
|
26
27
|
matchesKey,
|
|
27
|
-
truncateToWidth,
|
|
28
28
|
type TUI,
|
|
29
29
|
visibleWidth,
|
|
30
30
|
wrapTextWithAnsi,
|
|
@@ -84,8 +84,8 @@ Example output:
|
|
|
84
84
|
"questions": []
|
|
85
85
|
}`;
|
|
86
86
|
|
|
87
|
-
const OPENAI_FAST_MODEL_ID = "gpt-
|
|
88
|
-
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-
|
|
87
|
+
const OPENAI_FAST_MODEL_ID = "gpt-6-luna";
|
|
88
|
+
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-5-5";
|
|
89
89
|
|
|
90
90
|
// Structured output format for question extraction
|
|
91
91
|
interface ExtractedQuestion {
|
|
@@ -204,10 +204,15 @@ class QnAComponent implements Component, Focusable {
|
|
|
204
204
|
private theme: Theme;
|
|
205
205
|
private onDone: (result: string | null) => void;
|
|
206
206
|
private showingConfirmation: boolean = false;
|
|
207
|
+
private questionOffset = 0;
|
|
208
|
+
private readingHeight = 0;
|
|
209
|
+
private maxQuestionOffset = 0;
|
|
210
|
+
private usable = false;
|
|
207
211
|
|
|
208
212
|
// Cache
|
|
209
213
|
private renderWidth?: number;
|
|
210
214
|
private cachedWidth?: number;
|
|
215
|
+
private cachedHeight?: number;
|
|
211
216
|
private cachedLines?: string[];
|
|
212
217
|
|
|
213
218
|
private _focused = false;
|
|
@@ -219,12 +224,14 @@ class QnAComponent implements Component, Focusable {
|
|
|
219
224
|
set focused(value: boolean) {
|
|
220
225
|
this._focused = value;
|
|
221
226
|
this.editor.focused = value;
|
|
227
|
+
this.invalidate();
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
constructor(
|
|
225
231
|
questions: ExtractedQuestion[],
|
|
226
232
|
tui: TUI,
|
|
227
233
|
theme: Theme,
|
|
234
|
+
private keybindings: KeybindingsManager,
|
|
228
235
|
onDone: (result: string | null) => void,
|
|
229
236
|
) {
|
|
230
237
|
this.questions = questions;
|
|
@@ -263,6 +270,7 @@ class QnAComponent implements Component, Focusable {
|
|
|
263
270
|
if (index < 0 || index >= this.questions.length) return;
|
|
264
271
|
this.saveCurrentAnswer();
|
|
265
272
|
this.currentIndex = index;
|
|
273
|
+
this.questionOffset = 0;
|
|
266
274
|
this.editor.setText(this.answers[index] || "");
|
|
267
275
|
this.invalidate();
|
|
268
276
|
}
|
|
@@ -295,26 +303,57 @@ class QnAComponent implements Component, Focusable {
|
|
|
295
303
|
|
|
296
304
|
invalidate(): void {
|
|
297
305
|
this.cachedWidth = undefined;
|
|
306
|
+
this.cachedHeight = undefined;
|
|
298
307
|
this.cachedLines = undefined;
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
handleInput(data: string): void {
|
|
302
|
-
|
|
311
|
+
// Recheck current geometry before accepting input, including resize before repaint.
|
|
312
|
+
// Both dimensions reserve the one-cell margins from overlayOptions below.
|
|
313
|
+
this.render(
|
|
314
|
+
Math.min(this.renderWidth ?? this.tui.terminal.columns - 2, this.tui.terminal.columns - 2),
|
|
315
|
+
);
|
|
316
|
+
if (!this.usable) {
|
|
303
317
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) this.cancel();
|
|
304
318
|
return;
|
|
305
319
|
}
|
|
306
320
|
|
|
321
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
322
|
+
if (this.showingConfirmation) {
|
|
323
|
+
this.showingConfirmation = false;
|
|
324
|
+
this.invalidate();
|
|
325
|
+
this.tui.requestRender();
|
|
326
|
+
} else {
|
|
327
|
+
this.cancel();
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Viewport navigation reads the question. Unmatched keys retain native editing behavior.
|
|
333
|
+
const scrollActions = [
|
|
334
|
+
["tui.altScreen.pageUp", -this.readingHeight],
|
|
335
|
+
["tui.altScreen.pageDown", this.readingHeight],
|
|
336
|
+
["tui.altScreen.top", -Infinity],
|
|
337
|
+
["tui.altScreen.bottom", Infinity],
|
|
338
|
+
] as const;
|
|
339
|
+
for (const [action, delta] of scrollActions) {
|
|
340
|
+
if (!this.keybindings.matches(data, action)) continue;
|
|
341
|
+
this.questionOffset = Math.max(
|
|
342
|
+
0,
|
|
343
|
+
Math.min(this.maxQuestionOffset, this.questionOffset + delta),
|
|
344
|
+
);
|
|
345
|
+
this.invalidate();
|
|
346
|
+
this.tui.requestRender();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
307
350
|
// Handle confirmation dialog
|
|
308
351
|
if (this.showingConfirmation) {
|
|
309
352
|
if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
|
|
310
353
|
this.submit();
|
|
311
354
|
return;
|
|
312
355
|
}
|
|
313
|
-
if (
|
|
314
|
-
matchesKey(data, Key.escape) ||
|
|
315
|
-
matchesKey(data, Key.ctrl("c")) ||
|
|
316
|
-
data.toLowerCase() === "n"
|
|
317
|
-
) {
|
|
356
|
+
if (data.toLowerCase() === "n") {
|
|
318
357
|
this.showingConfirmation = false;
|
|
319
358
|
this.invalidate();
|
|
320
359
|
this.tui.requestRender();
|
|
@@ -323,12 +362,6 @@ class QnAComponent implements Component, Focusable {
|
|
|
323
362
|
return;
|
|
324
363
|
}
|
|
325
364
|
|
|
326
|
-
// Global navigation and commands
|
|
327
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
328
|
-
this.cancel();
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
365
|
// Tab / Shift+Tab for navigation
|
|
333
366
|
if (matchesKey(data, Key.tab)) {
|
|
334
367
|
if (this.currentIndex < this.questions.length - 1) {
|
|
@@ -386,26 +419,47 @@ class QnAComponent implements Component, Focusable {
|
|
|
386
419
|
|
|
387
420
|
render(width: number): string[] {
|
|
388
421
|
this.renderWidth = width;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.theme.fg(
|
|
393
|
-
"muted",
|
|
394
|
-
`Resize to ${MIN_QUESTIONNAIRE_WIDTH}+ columns to answer. Esc cancels.`,
|
|
395
|
-
),
|
|
396
|
-
width,
|
|
397
|
-
),
|
|
398
|
-
];
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (this.cachedLines && this.cachedWidth === width) {
|
|
422
|
+
const height = Math.max(1, this.tui.terminal.rows - 2);
|
|
423
|
+
if (this.cachedLines && this.cachedWidth === width && this.cachedHeight === height) {
|
|
424
|
+
this.usable = true;
|
|
402
425
|
return this.cachedLines;
|
|
403
426
|
}
|
|
404
427
|
|
|
405
428
|
const theme = this.theme;
|
|
406
429
|
const lines: string[] = [];
|
|
407
|
-
const boxWidth = Math.min(width - 4, 120);
|
|
408
|
-
const contentWidth = boxWidth - 4;
|
|
430
|
+
const boxWidth = Math.min(Math.max(MIN_QUESTIONNAIRE_WIDTH, width) - 4, 120);
|
|
431
|
+
const contentWidth = boxWidth - 4;
|
|
432
|
+
const editorLines = this.editor.render(contentWidth - 7);
|
|
433
|
+
const readingKeys = [
|
|
434
|
+
...this.keybindings.getKeys("tui.altScreen.pageUp"),
|
|
435
|
+
...this.keybindings.getKeys("tui.altScreen.pageDown"),
|
|
436
|
+
...this.keybindings.getKeys("tui.altScreen.top"),
|
|
437
|
+
...this.keybindings.getKeys("tui.altScreen.bottom"),
|
|
438
|
+
].join("/");
|
|
439
|
+
const controls = this.showingConfirmation
|
|
440
|
+
? `${theme.fg("warning", "Submit all answers?")} Enter/y confirm · Esc/n back`
|
|
441
|
+
: "Tab/Enter next · Shift+Tab prev · Shift+Enter newline · Esc cancel";
|
|
442
|
+
const footer = [
|
|
443
|
+
...wrapTextWithAnsi(
|
|
444
|
+
theme.fg("dim", `Read question: ${readingKeys || "navigation keys unbound"}`),
|
|
445
|
+
contentWidth,
|
|
446
|
+
),
|
|
447
|
+
...wrapTextWithAnsi(controls, contentWidth),
|
|
448
|
+
];
|
|
449
|
+
// Six frame rows (top/title/divider, editor spacer, footer divider, bottom),
|
|
450
|
+
// the full native editor and wrapped footer, plus at least one reading row.
|
|
451
|
+
const minimumHeight = 6 + editorLines.length + footer.length + 1;
|
|
452
|
+
this.usable = width >= MIN_QUESTIONNAIRE_WIDTH && height >= minimumHeight;
|
|
453
|
+
if (!this.usable) {
|
|
454
|
+
this.invalidate();
|
|
455
|
+
return wrapTextWithAnsi(
|
|
456
|
+
theme.fg(
|
|
457
|
+
"muted",
|
|
458
|
+
`Resize to ${MIN_QUESTIONNAIRE_WIDTH + 2}+ columns and more rows. Esc cancels.`,
|
|
459
|
+
),
|
|
460
|
+
Math.max(1, width),
|
|
461
|
+
).slice(0, height);
|
|
462
|
+
}
|
|
409
463
|
|
|
410
464
|
// Helper to create horizontal lines (dim the whole thing at once)
|
|
411
465
|
const horizontalLine = (count: number) => "─".repeat(count);
|
|
@@ -418,10 +472,6 @@ class QnAComponent implements Component, Focusable {
|
|
|
418
472
|
return theme.fg("dim", "│") + paddedContent + " ".repeat(rightPad) + theme.fg("dim", "│");
|
|
419
473
|
};
|
|
420
474
|
|
|
421
|
-
const emptyBoxLine = (): string => {
|
|
422
|
-
return theme.fg("dim", "│") + " ".repeat(boxWidth - 2) + theme.fg("dim", "│");
|
|
423
|
-
};
|
|
424
|
-
|
|
425
475
|
const padToWidth = (line: string): string => {
|
|
426
476
|
const len = visibleWidth(line);
|
|
427
477
|
return line + " ".repeat(Math.max(0, width - len));
|
|
@@ -433,7 +483,14 @@ class QnAComponent implements Component, Focusable {
|
|
|
433
483
|
lines.push(padToWidth(boxLine(title)));
|
|
434
484
|
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
435
485
|
|
|
436
|
-
//
|
|
486
|
+
// Start with the question even when only one reading row fits.
|
|
487
|
+
const q = this.questions[this.currentIndex];
|
|
488
|
+
const reading = wrapTextWithAnsi(`> ${q.question}`, contentWidth);
|
|
489
|
+
if (q.context) {
|
|
490
|
+
reading.push("", ...wrapTextWithAnsi(theme.fg("muted", q.context), contentWidth - 2));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Progress belongs to the reading region so every indicator remains reachable.
|
|
437
494
|
const progressParts: string[] = [];
|
|
438
495
|
for (let i = 0; i < this.questions.length; i++) {
|
|
439
496
|
const answered = (this.answers[i]?.trim() || "").length > 0;
|
|
@@ -446,61 +503,30 @@ class QnAComponent implements Component, Focusable {
|
|
|
446
503
|
progressParts.push(theme.fg("dim", "○"));
|
|
447
504
|
}
|
|
448
505
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
for (const line of wrappedQuestion) {
|
|
506
|
+
reading.push("", ...wrapTextWithAnsi(progressParts.join(" "), contentWidth));
|
|
507
|
+
|
|
508
|
+
this.readingHeight = height - minimumHeight + 1;
|
|
509
|
+
this.maxQuestionOffset = Math.max(0, reading.length - this.readingHeight);
|
|
510
|
+
this.questionOffset = Math.min(this.questionOffset, this.maxQuestionOffset);
|
|
511
|
+
for (const line of reading.slice(
|
|
512
|
+
this.questionOffset,
|
|
513
|
+
this.questionOffset + this.readingHeight,
|
|
514
|
+
)) {
|
|
459
515
|
lines.push(padToWidth(boxLine(line)));
|
|
460
516
|
}
|
|
517
|
+
lines.push(padToWidth(boxLine("")));
|
|
461
518
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
lines.push(padToWidth(
|
|
465
|
-
const contextText = theme.fg("muted", `${q.context}`);
|
|
466
|
-
const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
|
|
467
|
-
for (const line of wrappedContext) {
|
|
468
|
-
lines.push(padToWidth(boxLine(line)));
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
lines.push(padToWidth(emptyBoxLine()));
|
|
473
|
-
|
|
474
|
-
// Render the editor component (multi-line input) with padding
|
|
475
|
-
// Skip the first and last lines (editor's own border lines)
|
|
476
|
-
const answerPrefix = theme.bold("A: ");
|
|
477
|
-
const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
|
|
478
|
-
const editorLines = this.editor.render(editorWidth);
|
|
479
|
-
for (let i = 1; i < editorLines.length - 1; i++) {
|
|
480
|
-
if (i === 1) {
|
|
481
|
-
// First content line gets the "A: " prefix
|
|
482
|
-
lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
|
|
483
|
-
} else {
|
|
484
|
-
// Subsequent lines get padding to align with the first line
|
|
485
|
-
lines.push(padToWidth(boxLine(" " + editorLines[i])));
|
|
486
|
-
}
|
|
519
|
+
// Keep the entire native editor, including its cursor and scrolling indicators.
|
|
520
|
+
for (let i = 0; i < editorLines.length; i++) {
|
|
521
|
+
lines.push(padToWidth(boxLine((i === 1 ? theme.bold("A: ") : " ") + editorLines[i])));
|
|
487
522
|
}
|
|
488
523
|
|
|
489
|
-
lines.push(padToWidth(
|
|
490
|
-
|
|
491
|
-
// Confirmation dialog or footer with controls
|
|
492
|
-
if (this.showingConfirmation) {
|
|
493
|
-
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
494
|
-
const confirmMsg = `${theme.fg("warning", "Submit all answers?")} ${theme.fg("dim", "(Enter/y to confirm, Esc/n to cancel)")}`;
|
|
495
|
-
lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
|
|
496
|
-
} else {
|
|
497
|
-
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
498
|
-
const controls = `${theme.fg("dim", "Tab/Enter")} next · ${theme.fg("dim", "Shift+Tab")} prev · ${theme.fg("dim", "Shift+Enter")} newline · ${theme.fg("dim", "Esc")} cancel`;
|
|
499
|
-
lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
|
|
500
|
-
}
|
|
524
|
+
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
525
|
+
for (const line of footer) lines.push(padToWidth(boxLine(line)));
|
|
501
526
|
lines.push(padToWidth(theme.fg("dim", "╰" + horizontalLine(boxWidth - 2) + "╯")));
|
|
502
527
|
|
|
503
528
|
this.cachedWidth = width;
|
|
529
|
+
this.cachedHeight = height;
|
|
504
530
|
this.cachedLines = lines;
|
|
505
531
|
return lines;
|
|
506
532
|
}
|
|
@@ -633,9 +659,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
659
|
}
|
|
634
660
|
|
|
635
661
|
// Show the Q&A component
|
|
636
|
-
const answersResult = await ctx.ui.custom<string | null>(
|
|
637
|
-
|
|
638
|
-
|
|
662
|
+
const answersResult = await ctx.ui.custom<string | null>(
|
|
663
|
+
(tui, theme, kb, done) => new QnAComponent(questions, tui, theme, kb, done),
|
|
664
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
|
|
665
|
+
);
|
|
639
666
|
|
|
640
667
|
if (answersResult === null) {
|
|
641
668
|
ctx.ui.notify("Cancelled", "info");
|
|
@@ -157,7 +157,7 @@ function showCommandMessage(
|
|
|
157
157
|
command: string,
|
|
158
158
|
): void {
|
|
159
159
|
const copiedToClipboard = copyToClipboard(command);
|
|
160
|
-
if (
|
|
160
|
+
if (ctx.mode === "print") {
|
|
161
161
|
console.log(formatCommandMessageIntro(intro, copiedToClipboard));
|
|
162
162
|
console.log(command);
|
|
163
163
|
return;
|
|
@@ -269,9 +269,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
const leafId = ctx.sessionManager.getLeafId();
|
|
272
|
-
const
|
|
272
|
+
const hasPersistedMessage = ctx.sessionManager
|
|
273
273
|
.getEntries()
|
|
274
|
-
.some(
|
|
274
|
+
.some(
|
|
275
|
+
(entry) =>
|
|
276
|
+
entry.type === "message" &&
|
|
277
|
+
(entry.message.role === "user" || entry.message.role === "assistant"),
|
|
278
|
+
);
|
|
275
279
|
|
|
276
280
|
let forkFile: string;
|
|
277
281
|
if (leafId && hasValidSessionFile(sessionFile)) {
|
|
@@ -285,7 +289,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
285
289
|
}
|
|
286
290
|
forkFile = persistFork(forkManager);
|
|
287
291
|
} else {
|
|
288
|
-
if (leafId &&
|
|
292
|
+
if (leafId && hasPersistedMessage) {
|
|
289
293
|
throw new Error(`Current session file is missing or invalid: ${sessionFile}`);
|
|
290
294
|
}
|
|
291
295
|
|
|
@@ -293,7 +297,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
297
|
? "Current session has no persisted history yet. Opening a fresh session."
|
|
294
298
|
: "Selected conversation is empty. Opening a fresh session.";
|
|
295
299
|
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
296
|
-
else console.
|
|
300
|
+
else console.error(message);
|
|
297
301
|
|
|
298
302
|
forkFile = persistFork(
|
|
299
303
|
SessionManager.create(ctx.cwd, ctx.sessionManager.getSessionDir(), {
|
package/extensions/btw.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
matchesKey,
|
|
25
25
|
truncateToWidth,
|
|
26
26
|
type Component,
|
|
27
|
+
type KeybindingsManager,
|
|
27
28
|
type TUI,
|
|
28
29
|
visibleWidth,
|
|
29
30
|
wrapTextWithAnsi,
|
|
@@ -32,6 +33,8 @@ import {
|
|
|
32
33
|
const STATUS_KEY = "0-btw";
|
|
33
34
|
const RESULT_MARKDOWN_THEME = getMarkdownTheme();
|
|
34
35
|
const MIN_RESULT_WIDTH = 50;
|
|
36
|
+
const RESULT_CHROME_ROWS = 6;
|
|
37
|
+
const MIN_RESULT_ROWS = RESULT_CHROME_ROWS + 3; // Two overlay margins and one reading row.
|
|
35
38
|
const STATUS_SPINNER_INTERVAL_MS = 80;
|
|
36
39
|
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
37
40
|
|
|
@@ -350,9 +353,11 @@ function seedSessionManager(sessionManager: SessionManager, messages: AgentMessa
|
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
async function showResultDialog(ctx: ExtensionContext, result: BtwResult): Promise<void> {
|
|
353
|
-
await ctx.ui.custom<void>(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
await ctx.ui.custom<void>(
|
|
357
|
+
(tui, theme, keybindings, done) =>
|
|
358
|
+
new BtwResultComponent(result, tui, theme, keybindings, done),
|
|
359
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
|
|
360
|
+
);
|
|
356
361
|
}
|
|
357
362
|
|
|
358
363
|
function getLastAssistantMessage(messages: AgentMessage[]): AssistantMessage | null {
|
|
@@ -389,6 +394,7 @@ class BtwResultComponent implements Component {
|
|
|
389
394
|
private readonly result: BtwResult,
|
|
390
395
|
private readonly tui: TUI,
|
|
391
396
|
private readonly theme: Theme,
|
|
397
|
+
private readonly keybindings: KeybindingsManager,
|
|
392
398
|
private readonly onDone: () => void,
|
|
393
399
|
) {
|
|
394
400
|
this.markdown = new Markdown(result.answer, 0, 0, RESULT_MARKDOWN_THEME);
|
|
@@ -412,49 +418,61 @@ class BtwResultComponent implements Component {
|
|
|
412
418
|
}
|
|
413
419
|
|
|
414
420
|
const width = this.renderWidth ?? this.tui.terminal.columns;
|
|
415
|
-
if (width < MIN_RESULT_WIDTH) return;
|
|
421
|
+
if (width < MIN_RESULT_WIDTH || this.tui.terminal.rows < MIN_RESULT_ROWS) return;
|
|
416
422
|
|
|
417
423
|
const bodyHeight = this.getBodyHeight();
|
|
418
|
-
const boxWidth =
|
|
424
|
+
const boxWidth = width;
|
|
419
425
|
const bodyLines = this.getBodyLines(this.getContentWidth(boxWidth));
|
|
420
426
|
const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
|
|
421
427
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
this.scrollOffset
|
|
426
|
-
} else if (
|
|
427
|
-
this.scrollOffset
|
|
428
|
-
} else if (
|
|
429
|
-
this.scrollOffset
|
|
430
|
-
} else if (
|
|
428
|
+
this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
|
|
429
|
+
const kb = this.keybindings;
|
|
430
|
+
if (kb.matches(data, "tui.altScreen.pageUp")) {
|
|
431
|
+
this.scrollOffset -= bodyHeight;
|
|
432
|
+
} else if (kb.matches(data, "tui.altScreen.pageDown")) {
|
|
433
|
+
this.scrollOffset += bodyHeight;
|
|
434
|
+
} else if (kb.matches(data, "tui.altScreen.halfPageUp")) {
|
|
435
|
+
this.scrollOffset -= Math.max(1, Math.floor(bodyHeight / 2));
|
|
436
|
+
} else if (kb.matches(data, "tui.altScreen.halfPageDown")) {
|
|
437
|
+
this.scrollOffset += Math.max(1, Math.floor(bodyHeight / 2));
|
|
438
|
+
} else if (kb.matches(data, "tui.altScreen.top")) {
|
|
431
439
|
this.scrollOffset = 0;
|
|
432
|
-
} else if (
|
|
440
|
+
} else if (kb.matches(data, "tui.altScreen.bottom")) {
|
|
433
441
|
this.scrollOffset = maxScroll;
|
|
442
|
+
} else if (kb.matches(data, "tui.altScreen.lineUp")) {
|
|
443
|
+
this.scrollOffset -= 1;
|
|
444
|
+
} else if (kb.matches(data, "tui.altScreen.lineDown")) {
|
|
445
|
+
this.scrollOffset += 1;
|
|
446
|
+
} else if (matchesKey(data, Key.up) || data.toLowerCase() === "k") {
|
|
447
|
+
this.scrollOffset -= 1;
|
|
448
|
+
} else if (matchesKey(data, Key.down) || data.toLowerCase() === "j") {
|
|
449
|
+
this.scrollOffset += 1;
|
|
434
450
|
} else {
|
|
435
451
|
return;
|
|
436
452
|
}
|
|
437
453
|
|
|
454
|
+
this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
|
|
438
455
|
this.tui.requestRender();
|
|
439
456
|
}
|
|
440
457
|
|
|
441
458
|
render(width: number): string[] {
|
|
442
459
|
this.renderWidth = width;
|
|
443
|
-
if (width <
|
|
460
|
+
if (width < 1 || this.tui.terminal.rows <= 2) return [];
|
|
461
|
+
if (width < MIN_RESULT_WIDTH || this.tui.terminal.rows < MIN_RESULT_ROWS) {
|
|
444
462
|
return [
|
|
445
463
|
truncateToWidth(
|
|
446
|
-
this.theme.fg(
|
|
464
|
+
this.theme.fg(
|
|
465
|
+
"muted",
|
|
466
|
+
`Esc close · Resize to ${MIN_RESULT_WIDTH + 2}+ columns, ${MIN_RESULT_ROWS}+ rows.`,
|
|
467
|
+
),
|
|
447
468
|
width,
|
|
469
|
+
"",
|
|
448
470
|
),
|
|
449
471
|
];
|
|
450
472
|
}
|
|
451
473
|
|
|
452
|
-
const boxWidth =
|
|
474
|
+
const boxWidth = width;
|
|
453
475
|
const contentWidth = this.getContentWidth(boxWidth);
|
|
454
|
-
const questionLines = wrapTextWithAnsi(
|
|
455
|
-
this.theme.fg("muted", this.result.question),
|
|
456
|
-
contentWidth,
|
|
457
|
-
);
|
|
458
476
|
const bodyLines = this.getBodyLines(contentWidth);
|
|
459
477
|
const bodyHeight = this.getBodyHeight();
|
|
460
478
|
const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
|
|
@@ -462,11 +480,7 @@ class BtwResultComponent implements Component {
|
|
|
462
480
|
|
|
463
481
|
const lines: string[] = [];
|
|
464
482
|
lines.push(this.borderLine("╭", "╮", boxWidth));
|
|
465
|
-
lines.push(this.boxLine(this.theme.bold("
|
|
466
|
-
for (const line of questionLines) {
|
|
467
|
-
lines.push(this.boxLine(line, boxWidth));
|
|
468
|
-
}
|
|
469
|
-
lines.push(this.separatorLine(boxWidth));
|
|
483
|
+
lines.push(this.boxLine(this.theme.bold("BTW"), boxWidth));
|
|
470
484
|
|
|
471
485
|
const visibleBody = bodyLines.slice(this.scrollOffset, this.scrollOffset + bodyHeight);
|
|
472
486
|
for (const line of visibleBody) {
|
|
@@ -478,15 +492,30 @@ class BtwResultComponent implements Component {
|
|
|
478
492
|
|
|
479
493
|
lines.push(this.separatorLine(boxWidth));
|
|
480
494
|
const scrollText = `${Math.min(bodyLines.length, this.scrollOffset + 1)}-${Math.min(bodyLines.length, this.scrollOffset + visibleBody.length)}/${bodyLines.length}`;
|
|
481
|
-
const
|
|
482
|
-
|
|
495
|
+
const navigation = [
|
|
496
|
+
["pageUp", "pageDown", "page"],
|
|
497
|
+
["top", "bottom", "ends"],
|
|
498
|
+
] as const;
|
|
499
|
+
const hints = [
|
|
500
|
+
"↑↓/j/k scroll",
|
|
501
|
+
...navigation.flatMap(([up, down, label]) => {
|
|
502
|
+
const keys = [up, down].map((action) =>
|
|
503
|
+
this.keybindings
|
|
504
|
+
.getKeys(`tui.altScreen.${action}`)
|
|
505
|
+
.find((key) => !["enter", "escape", "ctrl+c", "q"].includes(key)),
|
|
506
|
+
);
|
|
507
|
+
return keys.some(Boolean) ? [`${keys.map((key) => key ?? "—").join("/")} ${label}`] : [];
|
|
508
|
+
}),
|
|
509
|
+
];
|
|
510
|
+
lines.push(this.boxLine(this.theme.fg("dim", hints.join(" · ")), boxWidth));
|
|
511
|
+
lines.push(this.boxLine(this.theme.fg("dim", `Enter/Esc close · ${scrollText}`), boxWidth));
|
|
483
512
|
lines.push(this.borderLine("╰", "╯", boxWidth));
|
|
484
513
|
|
|
485
514
|
return lines;
|
|
486
515
|
}
|
|
487
516
|
|
|
488
517
|
private getBodyHeight(): number {
|
|
489
|
-
return
|
|
518
|
+
return this.tui.terminal.rows - 2 - RESULT_CHROME_ROWS;
|
|
490
519
|
}
|
|
491
520
|
|
|
492
521
|
private getBodyLines(contentWidth: number): string[] {
|
|
@@ -494,18 +523,20 @@ class BtwResultComponent implements Component {
|
|
|
494
523
|
return this.cachedBodyLines;
|
|
495
524
|
}
|
|
496
525
|
|
|
497
|
-
const lines =
|
|
526
|
+
const lines = [
|
|
527
|
+
this.theme.bold("Request"),
|
|
528
|
+
...wrapTextWithAnsi(this.theme.fg("muted", this.result.question), contentWidth),
|
|
529
|
+
"",
|
|
530
|
+
this.theme.bold("Answer"),
|
|
531
|
+
...this.markdown.render(contentWidth),
|
|
532
|
+
];
|
|
498
533
|
this.cachedBodyWidth = contentWidth;
|
|
499
534
|
this.cachedBodyLines = lines;
|
|
500
535
|
return lines;
|
|
501
536
|
}
|
|
502
537
|
|
|
503
|
-
private getBoxWidth(width: number): number {
|
|
504
|
-
return Math.max(MIN_RESULT_WIDTH, Math.min(width - 2, 140));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
538
|
private getContentWidth(boxWidth: number): number {
|
|
508
|
-
return
|
|
539
|
+
return boxWidth - 4;
|
|
509
540
|
}
|
|
510
541
|
|
|
511
542
|
private borderLine(left: string, right: string, width: number): string {
|