@astralyn/pi 0.87.4 → 0.87.6
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/CHANGELOG.md +33 -0
- package/dist/bundle/chunks/{chunk-CUDKRZ2V.js → chunk-4W4Q2H6W.js} +58 -57
- package/dist/bundle/chunks/{chunk-ANT4TSUE.js → chunk-IORR2FW4.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-Y2YZRMU3.js → virtual-modules-KUEIEAZN.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session.d.ts +8 -24
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +54 -109
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/background/session.d.ts +11 -7
- package/dist/core/background/session.d.ts.map +1 -1
- package/dist/core/background/session.js +42 -16
- package/dist/core/background/session.js.map +1 -1
- package/dist/core/context-snapshot.d.ts +17 -4
- package/dist/core/context-snapshot.d.ts.map +1 -1
- package/dist/core/context-snapshot.js +48 -50
- package/dist/core/context-snapshot.js.map +1 -1
- package/dist/core/extensions/distribution-api.d.ts +0 -7
- package/dist/core/extensions/distribution-api.d.ts.map +1 -1
- package/dist/core/extensions/distribution-api.js +6 -6
- package/dist/core/extensions/distribution-api.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -5
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +3 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +59 -7
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -3
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +0 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +0 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +0 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +0 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +0 -7
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +0 -11
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/renderers/bash.d.ts.map +1 -1
- package/dist/core/tools/renderers/bash.js +59 -5
- package/dist/core/tools/renderers/bash.js.map +1 -1
- package/dist/core/tools/renderers/edit.d.ts.map +1 -1
- package/dist/core/tools/renderers/edit.js +4 -22
- package/dist/core/tools/renderers/edit.js.map +1 -1
- package/dist/core/tools/renderers/find.d.ts.map +1 -1
- package/dist/core/tools/renderers/find.js +7 -6
- package/dist/core/tools/renderers/find.js.map +1 -1
- package/dist/core/tools/renderers/grep.d.ts.map +1 -1
- package/dist/core/tools/renderers/grep.js +8 -7
- package/dist/core/tools/renderers/grep.js.map +1 -1
- package/dist/core/tools/renderers/ls.d.ts.map +1 -1
- package/dist/core/tools/renderers/ls.js +3 -2
- package/dist/core/tools/renderers/ls.js.map +1 -1
- package/dist/core/tools/renderers/read.d.ts.map +1 -1
- package/dist/core/tools/renderers/read.js +7 -12
- package/dist/core/tools/renderers/read.js.map +1 -1
- package/dist/core/tools/renderers/write.d.ts.map +1 -1
- package/dist/core/tools/renderers/write.js +3 -2
- package/dist/core/tools/renderers/write.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts +2 -6
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +5 -4
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/extensions/background/completion-render.d.ts.map +1 -1
- package/dist/extensions/background/completion-render.js +3 -4
- package/dist/extensions/background/completion-render.js.map +1 -1
- package/dist/extensions/background/constants.d.ts +2 -0
- package/dist/extensions/background/constants.d.ts.map +1 -1
- package/dist/extensions/background/constants.js +2 -0
- package/dist/extensions/background/constants.js.map +1 -1
- package/dist/extensions/background/index.d.ts.map +1 -1
- package/dist/extensions/background/index.js +31 -5
- package/dist/extensions/background/index.js.map +1 -1
- package/dist/extensions/question/detail-pane.d.ts +12 -0
- package/dist/extensions/question/detail-pane.d.ts.map +1 -0
- package/dist/extensions/question/detail-pane.js +42 -0
- package/dist/extensions/question/detail-pane.js.map +1 -0
- package/dist/extensions/question/dialog.d.ts.map +1 -1
- package/dist/extensions/question/dialog.js +175 -105
- package/dist/extensions/question/dialog.js.map +1 -1
- package/dist/extensions/question/option-window.d.ts +26 -0
- package/dist/extensions/question/option-window.d.ts.map +1 -0
- package/dist/extensions/question/option-window.js +53 -0
- package/dist/extensions/question/option-window.js.map +1 -0
- package/dist/extensions/question/render-cache.d.ts +4 -4
- package/dist/extensions/question/render-cache.d.ts.map +1 -1
- package/dist/extensions/question/render-cache.js +10 -11
- package/dist/extensions/question/render-cache.js.map +1 -1
- package/dist/extensions/todo/constants.d.ts +1 -1
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +2 -2
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +12 -8
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +1 -0
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +1 -1
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +15 -5
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +4 -4
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +11 -30
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/status-marker.d.ts +2 -2
- package/dist/modes/interactive/components/status-marker.js +2 -2
- package/dist/modes/interactive/components/status-marker.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +2 -3
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/tool-view/chat.d.ts +20 -0
- package/dist/modes/interactive/tool-view/chat.d.ts.map +1 -0
- package/dist/modes/interactive/tool-view/chat.js +46 -0
- package/dist/modes/interactive/tool-view/chat.js.map +1 -0
- package/dist/modes/interactive/tool-view/fallback.d.ts +14 -0
- package/dist/modes/interactive/tool-view/fallback.d.ts.map +1 -0
- package/dist/modes/interactive/tool-view/fallback.js +52 -0
- package/dist/modes/interactive/tool-view/fallback.js.map +1 -0
- package/dist/modes/interactive/tool-view/style.d.ts +63 -0
- package/dist/modes/interactive/tool-view/style.d.ts.map +1 -0
- package/dist/modes/interactive/tool-view/style.js +83 -0
- package/dist/modes/interactive/tool-view/style.js.map +1 -0
- package/dist/modes/interactive/{components → tool-view}/tool-execution.d.ts +17 -24
- package/dist/modes/interactive/tool-view/tool-execution.d.ts.map +1 -0
- package/dist/modes/interactive/{components → tool-view}/tool-execution.js +42 -116
- package/dist/modes/interactive/tool-view/tool-execution.js.map +1 -0
- package/docs/bundled/README.md +1 -1
- package/docs/bundled/extensions/background.md +1 -1
- package/docs/bundled/extensions/question.md +6 -5
- package/docs/bundled/extensions/todo.md +5 -5
- package/docs/bundled/tool-presentation.md +41 -31
- package/docs/extensions.md +1 -4
- package/docs/keybindings.md +11 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/core/model-stream-options.d.ts +0 -6
- package/dist/core/model-stream-options.d.ts.map +0 -1
- package/dist/core/model-stream-options.js +0 -20
- package/dist/core/model-stream-options.js.map +0 -1
- package/dist/modes/interactive/components/tool-chrome.d.ts +0 -28
- package/dist/modes/interactive/components/tool-chrome.d.ts.map +0 -1
- package/dist/modes/interactive/components/tool-chrome.js +0 -84
- package/dist/modes/interactive/components/tool-chrome.js.map +0 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +0 -1
- package/dist/modes/interactive/components/tool-execution.js.map +0 -1
- package/dist/modes/interactive/components/tool-group.d.ts +0 -17
- package/dist/modes/interactive/components/tool-group.d.ts.map +0 -1
- package/dist/modes/interactive/components/tool-group.js +0 -78
- package/dist/modes/interactive/components/tool-group.js.map +0 -1
- package/dist/modes/interactive/tool-chat.d.ts +0 -21
- package/dist/modes/interactive/tool-chat.d.ts.map +0 -1
- package/dist/modes/interactive/tool-chat.js +0 -58
- package/dist/modes/interactive/tool-chat.js.map +0 -1
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import "./keybindings.js";
|
|
2
|
-
import { Editor, Markdown, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { CURSOR_MARKER, Editor, Markdown, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
3
3
|
import { keyLabel as configuredKeyLabel } from "../../modes/interactive/components/keybinding-hints.js";
|
|
4
4
|
import { getMarkdownTheme } from "../../modes/interactive/theme/theme.js";
|
|
5
|
+
import { DetailPane } from "./detail-pane.js";
|
|
5
6
|
import { ruleBorder, wrapWithPrefix } from "./dialog-primitives.js";
|
|
6
7
|
import { QUESTION_LIMITS } from "./limits.js";
|
|
8
|
+
import { moreOptionsHint, windowItems } from "./option-window.js";
|
|
7
9
|
import { PreviewLinesCache, WidthCachedRender } from "./render-cache.js";
|
|
8
10
|
import { displayOptions, firstUnanswered, hasAnswer, hasMultiAnswer, newQuestionState, orderedAnswers, } from "./state.js";
|
|
9
|
-
const PREVIEW_MAX_LINES = 16;
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Reserved so the preview clamps short of the terminal height.
|
|
12
|
+
* The dialog replaces the editor in the bottom dock, and the dock grows with its
|
|
13
|
+
* content. Capping the dialog at half the terminal keeps the transcript (often the
|
|
14
|
+
* assistant reply the question refers to) readable while the dialog is open.
|
|
15
15
|
*/
|
|
16
|
-
const
|
|
16
|
+
const DIALOG_MAX_FRACTION = 0.5;
|
|
17
|
+
const DIALOG_MIN_ROWS = 16;
|
|
17
18
|
const VIEWPORT_SCROLL_STEP = 4;
|
|
18
|
-
function
|
|
19
|
-
const
|
|
19
|
+
function dialogRowBudget(terminalRows) {
|
|
20
|
+
const rows = Math.max(1, Math.floor(terminalRows));
|
|
21
|
+
return Math.min(rows, Math.max(DIALOG_MIN_ROWS, Math.floor(rows * DIALOG_MAX_FRACTION)));
|
|
22
|
+
}
|
|
23
|
+
/** Clamp `lines` to `maxRows` between the first and last line, scrolling around the anchor. */
|
|
24
|
+
function applyVerticalViewport(lines, width, maxRows, theme, options = {}) {
|
|
20
25
|
if (lines.length <= maxRows)
|
|
21
26
|
return { lines, start: 0, maxStart: 0 };
|
|
22
27
|
if (maxRows < 5) {
|
|
@@ -60,6 +65,7 @@ export function createQuestionDialog(questions, signal) {
|
|
|
60
65
|
let finished = false;
|
|
61
66
|
const cache = new WidthCachedRender();
|
|
62
67
|
const previewCache = new PreviewLinesCache();
|
|
68
|
+
const details = new DetailPane();
|
|
63
69
|
const states = questions.map(() => newQuestionState());
|
|
64
70
|
const optionsByQuestion = questions.map((question) => displayOptions(question));
|
|
65
71
|
const editorTheme = {
|
|
@@ -111,6 +117,7 @@ export function createQuestionDialog(questions, signal) {
|
|
|
111
117
|
editor.focused = dialogFocused && inputMode !== undefined;
|
|
112
118
|
}
|
|
113
119
|
function setCurrentIdx(index) {
|
|
120
|
+
details.reset();
|
|
114
121
|
currentIdx = Math.max(0, Math.min(questions.length - 1, index));
|
|
115
122
|
view = "question";
|
|
116
123
|
inputMode = undefined;
|
|
@@ -160,6 +167,7 @@ export function createQuestionDialog(questions, signal) {
|
|
|
160
167
|
setCurrentIdx(missing);
|
|
161
168
|
}
|
|
162
169
|
function beginCustomInput() {
|
|
170
|
+
details.reset();
|
|
163
171
|
const state = currentState();
|
|
164
172
|
inputMode = "custom";
|
|
165
173
|
noteTarget = undefined;
|
|
@@ -182,6 +190,7 @@ export function createQuestionDialog(questions, signal) {
|
|
|
182
190
|
state.warning = undefined;
|
|
183
191
|
}
|
|
184
192
|
function beginNotesInput() {
|
|
193
|
+
details.reset();
|
|
185
194
|
const question = currentQuestion();
|
|
186
195
|
const state = currentState();
|
|
187
196
|
const option = focusedOption();
|
|
@@ -357,6 +366,13 @@ export function createQuestionDialog(questions, signal) {
|
|
|
357
366
|
setCurrentIdx(currentIdx - 1);
|
|
358
367
|
return;
|
|
359
368
|
}
|
|
369
|
+
if (keyMatches(data, "app.question.pageUp") || keyMatches(data, "app.question.pageDown")) {
|
|
370
|
+
// Resolve the current pane's height even if another key arrived before a redraw.
|
|
371
|
+
render(tui.terminal.columns);
|
|
372
|
+
details.page(keyMatches(data, "app.question.pageUp") ? -1 : 1);
|
|
373
|
+
refresh();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
360
376
|
if (keyMatches(data, "tui.editor.cursorRight")) {
|
|
361
377
|
if (currentIdx < questions.length - 1)
|
|
362
378
|
setCurrentIdx(currentIdx + 1);
|
|
@@ -473,20 +489,9 @@ export function createQuestionDialog(questions, signal) {
|
|
|
473
489
|
: theme.fg(firstUnanswered(states) === undefined ? "success" : "dim", " ✓ Submit "),
|
|
474
490
|
].join(" ");
|
|
475
491
|
wrapWithPrefix(" ", tabs, renderWidth, lines);
|
|
476
|
-
lines.push("");
|
|
477
492
|
}
|
|
478
|
-
function previewLines(previewText, width
|
|
479
|
-
|
|
480
|
-
return previewCache.get(previewText, width, maxLines, () => {
|
|
481
|
-
const markdown = new Markdown(previewText, 1, 0, getMarkdownTheme());
|
|
482
|
-
const rendered = markdown.render(Math.max(1, width));
|
|
483
|
-
if (rendered.length <= maxLines)
|
|
484
|
-
return rendered;
|
|
485
|
-
return [
|
|
486
|
-
...rendered.slice(0, maxLines),
|
|
487
|
-
theme.fg("dim", truncateToWidth(`… ${rendered.length - maxLines} preview lines hidden`, Math.max(1, width))),
|
|
488
|
-
];
|
|
489
|
-
});
|
|
493
|
+
function previewLines(previewText, width) {
|
|
494
|
+
return previewCache.get(previewText, width, () => new Markdown(previewText, 1, 0, getMarkdownTheme()).render(Math.max(1, width)));
|
|
490
495
|
}
|
|
491
496
|
/** Multi-select checkbox marker; the other row shows no box until a custom answer exists. */
|
|
492
497
|
function multiMarker(option, checked, hasCustomAnswer) {
|
|
@@ -510,15 +515,90 @@ export function createQuestionDialog(questions, signal) {
|
|
|
510
515
|
}
|
|
511
516
|
wrapWithPrefix(" ", theme.fg("dim", joinHints(keyAction("tui.select.confirm", "submit"), keyAction("tui.select.cancel", "edit last question"), keyGroupAction(["tui.select.up", "tui.select.down"], "scroll"))), renderWidth, lines);
|
|
512
517
|
}
|
|
518
|
+
/** Chat row, warning, and key hints: always visible below the scrolling option area. */
|
|
519
|
+
function renderQuestionFooter(renderWidth, digitRange) {
|
|
520
|
+
const state = currentState();
|
|
521
|
+
const isMulti = currentQuestion().multiSelect === true;
|
|
522
|
+
const lines = [];
|
|
523
|
+
const chatPrefix = footerFocused ? theme.fg("accent", "→") : " ";
|
|
524
|
+
wrapWithPrefix(`${chatPrefix} `, theme.fg(footerFocused ? "accent" : "text", "Chat about this"), renderWidth, lines);
|
|
525
|
+
if (state.warning) {
|
|
526
|
+
lines.push("");
|
|
527
|
+
wrapWithPrefix(" ", theme.fg("warning", state.warning), renderWidth, lines);
|
|
528
|
+
}
|
|
529
|
+
let hints;
|
|
530
|
+
if (inputMode === "notes") {
|
|
531
|
+
hints = joinHints(editorSubmitAction("save notes"), keyAction("tui.select.cancel", "back"));
|
|
532
|
+
}
|
|
533
|
+
else if (inputMode === "custom") {
|
|
534
|
+
hints = joinHints(editorSubmitAction(isMulti ? "save custom answer" : "continue"), keyAction("tui.select.cancel", "back"));
|
|
535
|
+
}
|
|
536
|
+
else if (footerFocused) {
|
|
537
|
+
hints = joinHints(keyAction("tui.select.confirm", "discuss"), keyAction("tui.select.up", "return to options"), keyAction("tui.select.cancel", "cancel"));
|
|
538
|
+
}
|
|
539
|
+
else if (isMulti) {
|
|
540
|
+
hints = joinHints(keyGroupAction(["tui.select.up", "tui.select.down"], "navigate"), `${digitRange} toggle`, keyAction("app.list.toggle", "toggle focused"), keyAction("tui.input.tab", "notes/custom"), keyAction("tui.select.confirm", "continue"), questions.length > 1
|
|
541
|
+
? keyGroupAction(["tui.editor.cursorLeft", "tui.editor.cursorRight"], "questions")
|
|
542
|
+
: "", keyAction("tui.select.cancel", "cancel"));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
hints = joinHints(keyGroupAction(["tui.select.up", "tui.select.down"], "navigate"), `${digitRange} select`, keyAction("tui.input.tab", "notes/custom"), keyAction("tui.select.confirm", "select"), questions.length > 1
|
|
546
|
+
? keyGroupAction(["tui.editor.cursorLeft", "tui.editor.cursorRight"], "questions")
|
|
547
|
+
: "", keyAction("tui.select.cancel", "cancel"));
|
|
548
|
+
}
|
|
549
|
+
// Wrap between actions, so a key stays beside the action it performs.
|
|
550
|
+
let hintLine = "";
|
|
551
|
+
for (const hint of hints.split(" • ")) {
|
|
552
|
+
const joined = hintLine ? `${hintLine} • ${hint}` : hint;
|
|
553
|
+
if (hintLine && visibleWidth(joined) > renderWidth - 1) {
|
|
554
|
+
wrapWithPrefix(" ", theme.fg("dim", hintLine), renderWidth, lines);
|
|
555
|
+
hintLine = hint;
|
|
556
|
+
}
|
|
557
|
+
else
|
|
558
|
+
hintLine = joined;
|
|
559
|
+
}
|
|
560
|
+
if (hintLine)
|
|
561
|
+
wrapWithPrefix(" ", theme.fg("dim", hintLine), renderWidth, lines);
|
|
562
|
+
lines.push(ruleBorder(theme, renderWidth));
|
|
563
|
+
return lines;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Show whole items from `optionLines` in `rows` rows around `focus`, with
|
|
567
|
+
* "more options" hints on the sides that have hidden items. `focusRow` is the
|
|
568
|
+
* row of the focused item's first line.
|
|
569
|
+
*/
|
|
570
|
+
function windowOptionLines(optionLines, items, focus, rows, width) {
|
|
571
|
+
const window = windowItems(items, focus, rows);
|
|
572
|
+
const rowsBefore = (count) => items.slice(0, count).reduce((sum, item) => sum + item.height, 0);
|
|
573
|
+
const from = rowsBefore(window.first);
|
|
574
|
+
const block = optionLines.slice(from, rowsBefore(window.last + 1));
|
|
575
|
+
const visible = applyVerticalViewport(block, width, window.contentRows, theme, {
|
|
576
|
+
anchorLine: Math.max(0, block.findIndex((line) => line.includes(CURSOR_MARKER))),
|
|
577
|
+
}).lines;
|
|
578
|
+
if (!window.showAbove && !window.showBelow)
|
|
579
|
+
return { rows: visible, focusRow: rowsBefore(focus) - from };
|
|
580
|
+
const hint = (direction, count) => theme.fg("dim", ` ${truncateToWidth(moreOptionsHint(direction, count), Math.max(1, width - 1))}`);
|
|
581
|
+
const spare = Array.from({ length: Math.max(0, window.contentRows - visible.length) }, () => "");
|
|
582
|
+
return {
|
|
583
|
+
rows: [
|
|
584
|
+
...(window.showAbove ? [hint("up", window.hiddenAbove)] : []),
|
|
585
|
+
...visible,
|
|
586
|
+
...(window.showBelow ? [hint("down", window.hiddenBelow)] : []),
|
|
587
|
+
...spare,
|
|
588
|
+
],
|
|
589
|
+
focusRow: (window.showAbove ? 1 : 0) + rowsBefore(focus) - from,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
513
592
|
function compute(width, terminalRows) {
|
|
514
593
|
const lines = [];
|
|
515
594
|
const renderWidth = Math.max(1, width);
|
|
595
|
+
const maxRows = dialogRowBudget(terminalRows);
|
|
516
596
|
lines.push(ruleBorder(theme, renderWidth));
|
|
517
597
|
renderTabs(renderWidth, lines);
|
|
518
598
|
if (view === "review") {
|
|
519
599
|
renderReview(renderWidth, lines);
|
|
520
600
|
lines.push(ruleBorder(theme, renderWidth));
|
|
521
|
-
const viewport = applyVerticalViewport(lines, renderWidth,
|
|
601
|
+
const viewport = applyVerticalViewport(lines, renderWidth, maxRows, theme, {
|
|
522
602
|
preferredStart: reviewScrollOffset,
|
|
523
603
|
});
|
|
524
604
|
reviewMaxScrollOffset = viewport.maxStart;
|
|
@@ -529,16 +609,20 @@ export function createQuestionDialog(questions, signal) {
|
|
|
529
609
|
const state = currentState();
|
|
530
610
|
const options = currentOptions();
|
|
531
611
|
const isMulti = question.multiSelect === true;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
612
|
+
wrapWithPrefix(" ", theme.fg("text", question.question), renderWidth, lines);
|
|
613
|
+
// The pinned head and footer leave one bounded area for choices and reading.
|
|
614
|
+
const footerLines = renderQuestionFooter(renderWidth, `1-${Math.min(9, options.length)}`);
|
|
615
|
+
const areaRows = Math.max(1, maxRows - lines.length - footerLines.length);
|
|
616
|
+
const sideBySide = !inputMode && renderWidth >= 72;
|
|
617
|
+
// Measure every authored label, reserving the six-column prefix and a checkmark.
|
|
618
|
+
// Saved answers and focus changes must not move the divider or reflow the preview.
|
|
619
|
+
const listWidth = sideBySide
|
|
620
|
+
? Math.min(Math.floor((renderWidth - 3) / 2), Math.max(24, ...options.map((option, index) => 6 + visibleWidth(`${index + 1}. ${option.label.replace(/\s+/gu, " ")}`) + 2)))
|
|
621
|
+
: renderWidth;
|
|
622
|
+
const detailWidth = sideBySide ? renderWidth - listWidth - 3 : renderWidth;
|
|
535
623
|
const optionLines = [];
|
|
536
|
-
|
|
537
|
-
let
|
|
538
|
-
let inputAnchorOffset;
|
|
539
|
-
const hasPreview = inputMode === undefined && !isMulti && options.some((option) => option.kind === "option" && option.preview);
|
|
540
|
-
const showPreviewSideBySide = hasPreview && renderWidth >= 60;
|
|
541
|
-
const listWidth = showPreviewSideBySide ? Math.max(20, Math.floor(renderWidth * 0.4)) : renderWidth;
|
|
624
|
+
const items = [];
|
|
625
|
+
let focusItem = footerFocused ? options.length - 1 : state.optionIndex;
|
|
542
626
|
for (let index = 0; index < options.length; index++) {
|
|
543
627
|
const option = options[index];
|
|
544
628
|
const focused = !footerFocused && index === state.optionIndex;
|
|
@@ -556,91 +640,77 @@ export function createQuestionDialog(questions, signal) {
|
|
|
556
640
|
? (state.customAnswer?.text ??
|
|
557
641
|
(state.singleAnswer?.kind === "custom" ? state.singleAnswer.answer : undefined))
|
|
558
642
|
: undefined;
|
|
559
|
-
const labelText = customText ? `${option.label} ✎ ${customText}` : option.label;
|
|
643
|
+
const labelText = (customText ? `${option.label} ✎ ${customText}` : option.label).replace(/\s+/gu, " ");
|
|
560
644
|
const label = `${index + 1}. ${labelText}${selectedSingle ? " ✓" : ""}${note}`;
|
|
561
645
|
const color = focused ? "accent" : selectedSingle || checked ? "success" : "text";
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (option.kind === "option" && option.description) {
|
|
566
|
-
wrapWithPrefix(" ", theme.fg("muted", option.description), listWidth, optionLines);
|
|
567
|
-
}
|
|
568
|
-
if (focused)
|
|
569
|
-
focusedOptionEnd = optionLines.length;
|
|
646
|
+
const row = truncateToWidth(`${focusArrow} ${marker} ${theme.fg(color, label)}`, listWidth);
|
|
647
|
+
optionLines.push(focused ? theme.bg("selectedBg", row + " ".repeat(Math.max(0, listWidth - visibleWidth(row)))) : row);
|
|
648
|
+
items.push({ height: 1, countsAsOption: true });
|
|
570
649
|
}
|
|
571
650
|
if (inputMode) {
|
|
572
|
-
optionLines.
|
|
651
|
+
const start = optionLines.length;
|
|
573
652
|
const label = inputMode === "notes" ? `Notes for ${noteTarget ?? "option"}:` : "Your answer:";
|
|
574
|
-
|
|
653
|
+
optionLines.push(theme.fg("muted", truncateToWidth(` ${label}`, listWidth)));
|
|
575
654
|
for (const line of editor.render(Math.max(1, listWidth - 2)))
|
|
576
655
|
optionLines.push(` ${line}`);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
? previewLines(previewText, rightWidth, terminalRows)
|
|
587
|
-
: [theme.fg("dim", "(no preview)")];
|
|
588
|
-
const rightLines = [...Array.from({ length: focusedOptionStart }, () => ""), ...preview];
|
|
589
|
-
const maxRows = Math.max(optionLines.length, rightLines.length);
|
|
590
|
-
const pad = " ".repeat(gap);
|
|
591
|
-
for (let row = 0; row < maxRows; row++) {
|
|
592
|
-
const left = row < optionLines.length ? optionLines[row] : "";
|
|
593
|
-
const leftPadded = left + " ".repeat(Math.max(0, listWidth - visibleWidth(left)));
|
|
594
|
-
const right = row < rightLines.length ? rightLines[row] : "";
|
|
595
|
-
lines.push(`${leftPadded}${pad}${right}`);
|
|
596
|
-
}
|
|
656
|
+
items.push({ height: optionLines.length - start, countsAsOption: false });
|
|
657
|
+
focusItem = items.length - 1;
|
|
658
|
+
}
|
|
659
|
+
const regionStart = lines.length;
|
|
660
|
+
let focusRow;
|
|
661
|
+
if (inputMode) {
|
|
662
|
+
const windowed = windowOptionLines(optionLines, items, focusItem, areaRows, renderWidth);
|
|
663
|
+
focusRow = windowed.focusRow;
|
|
664
|
+
lines.push(...windowed.rows);
|
|
597
665
|
}
|
|
598
666
|
else {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
667
|
+
const option = options[state.optionIndex];
|
|
668
|
+
const content = [];
|
|
669
|
+
const hasPreview = !footerFocused && !isMulti && option.kind === "option" && Boolean(option.preview);
|
|
670
|
+
const title = footerFocused ? "Chat about this" : `${hasPreview ? "Preview" : "Details"} · ${option.label}`;
|
|
671
|
+
// The compact list may elide a long label; its full text stays readable here.
|
|
672
|
+
if (!footerFocused &&
|
|
673
|
+
visibleWidth(`${state.optionIndex + 1}. ${option.label.replace(/\s+/gu, " ")}`) > listWidth - 6) {
|
|
674
|
+
wrapWithPrefix(" ", theme.fg("text", option.label), detailWidth, content);
|
|
675
|
+
}
|
|
676
|
+
if (footerFocused) {
|
|
677
|
+
wrapWithPrefix(" ", theme.fg("muted", "Discuss this question before choosing an answer."), detailWidth, content);
|
|
678
|
+
}
|
|
679
|
+
else if (option.kind === "option") {
|
|
680
|
+
wrapWithPrefix(" ", theme.fg("muted", option.description), detailWidth, content);
|
|
681
|
+
const note = state.notesByOption.get(option.label);
|
|
682
|
+
if (note)
|
|
683
|
+
wrapWithPrefix(" ", theme.fg("success", `Note: ${note}`), detailWidth, content);
|
|
684
|
+
if (hasPreview && option.preview)
|
|
685
|
+
content.push("", ...previewLines(option.preview, detailWidth));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const custom = state.customAnswer?.text ??
|
|
689
|
+
(state.singleAnswer?.kind === "custom" ? state.singleAnswer.answer : undefined);
|
|
690
|
+
wrapWithPrefix(" ", theme.fg("muted", custom || "Write your own answer instead of choosing an option."), detailWidth, content);
|
|
691
|
+
}
|
|
692
|
+
// On narrow screens reserve reading space first, then show as many compact
|
|
693
|
+
// choices as fit. The focused choice always remains in the list window.
|
|
694
|
+
const optionRows = sideBySide ? areaRows : Math.max(1, Math.min(options.length, areaRows - 4));
|
|
695
|
+
const detailRows = sideBySide ? areaRows : Math.max(1, areaRows - optionRows);
|
|
696
|
+
const left = windowOptionLines(optionLines, items, focusItem, optionRows, listWidth);
|
|
697
|
+
const right = details.render(`${currentIdx}:${state.optionIndex}:${footerFocused}`, title, content, detailWidth, detailRows, keyGroupAction(["app.question.pageUp", "app.question.pageDown"], "scroll"), theme);
|
|
698
|
+
focusRow = left.focusRow;
|
|
699
|
+
if (sideBySide) {
|
|
700
|
+
for (let row = 0; row < Math.max(left.rows.length, right.length); row++) {
|
|
701
|
+
const leftText = left.rows[row] ?? "";
|
|
702
|
+
const leftPadded = leftText + " ".repeat(Math.max(0, listWidth - visibleWidth(leftText)));
|
|
703
|
+
lines.push(`${leftPadded}${theme.fg("dim", " │ ")}${right[row] ?? ""}`);
|
|
604
704
|
}
|
|
605
705
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const chatLine = lines.length;
|
|
610
|
-
const chatPrefix = footerFocused ? theme.fg("accent", "→") : " ";
|
|
611
|
-
wrapWithPrefix(`${chatPrefix} `, theme.fg(footerFocused ? "accent" : "text", "Chat about this"), renderWidth, lines);
|
|
612
|
-
let warningLine;
|
|
613
|
-
if (state.warning) {
|
|
614
|
-
lines.push("");
|
|
615
|
-
warningLine = lines.length;
|
|
616
|
-
wrapWithPrefix(" ", theme.fg("warning", state.warning), renderWidth, lines);
|
|
617
|
-
}
|
|
618
|
-
lines.push("");
|
|
619
|
-
if (inputMode === "notes") {
|
|
620
|
-
wrapWithPrefix(" ", theme.fg("dim", joinHints(editorSubmitAction("save notes"), keyAction("tui.select.cancel", "back"))), renderWidth, lines);
|
|
621
|
-
}
|
|
622
|
-
else if (inputMode === "custom") {
|
|
623
|
-
const hint = isMulti ? "save custom answer" : "continue";
|
|
624
|
-
wrapWithPrefix(" ", theme.fg("dim", joinHints(editorSubmitAction(hint), keyAction("tui.select.cancel", "back"))), renderWidth, lines);
|
|
625
|
-
}
|
|
626
|
-
else if (footerFocused) {
|
|
627
|
-
wrapWithPrefix(" ", theme.fg("dim", joinHints(keyAction("tui.select.confirm", "discuss"), keyAction("tui.select.up", "return to options"), keyAction("tui.select.cancel", "cancel"))), renderWidth, lines);
|
|
628
|
-
}
|
|
629
|
-
else if (isMulti) {
|
|
630
|
-
wrapWithPrefix(" ", theme.fg("dim", joinHints(`${digitRange} toggle`, keyAction("app.list.toggle", "toggle focused"), keyAction("tui.input.tab", "notes/custom"), keyAction("tui.select.confirm", "continue"), keyGroupAction(["tui.editor.cursorLeft", "tui.editor.cursorRight"], "questions"), keyAction("tui.select.cancel", "cancel"))), renderWidth, lines);
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
wrapWithPrefix(" ", theme.fg("dim", joinHints(`${digitRange} select`, keyAction("tui.input.tab", "notes/custom"), keyAction("tui.select.confirm", "select"), keyGroupAction(["tui.editor.cursorLeft", "tui.editor.cursorRight"], "questions"), keyAction("tui.select.cancel", "cancel"))), renderWidth, lines);
|
|
706
|
+
else {
|
|
707
|
+
lines.push(...left.rows, ...right);
|
|
708
|
+
}
|
|
634
709
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
else if (warningLine !== undefined)
|
|
640
|
-
anchorLine = warningLine;
|
|
641
|
-
else if (footerFocused)
|
|
642
|
-
anchorLine = chatLine;
|
|
643
|
-
return applyVerticalViewport(lines, renderWidth, terminalRows, theme, { anchorLine }).lines;
|
|
710
|
+
const anchorLine = regionStart + focusRow;
|
|
711
|
+
lines.push(...footerLines);
|
|
712
|
+
// Only a terminal too short for the pinned rows has lines left to drop here.
|
|
713
|
+
return applyVerticalViewport(lines, renderWidth, maxRows, theme, { anchorLine }).lines;
|
|
644
714
|
}
|
|
645
715
|
return {
|
|
646
716
|
get focused() {
|