@linxiraos/pi-tui 1.2.1 → 1.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/CHANGELOG.md +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
import { optionMarker } from "../tools/ask";
|
|
82
82
|
import { CountdownTimer } from "../chrome/countdown-timer";
|
|
83
83
|
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
84
|
-
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
84
|
+
import { formatKeyHint, formatKeyHints, formatTooltipKey } from "../app-keybindings";
|
|
85
85
|
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
86
86
|
import { handleTabSwitchKey } from "../chrome/selector-helpers";
|
|
87
87
|
import { col, node, span, text } from "../native/describe";
|
|
@@ -367,16 +367,49 @@ function renderAnswerSummary(question: ExtensionAskDialogQuestion, state: Questi
|
|
|
367
367
|
return selected[0] ?? theme.fg("warning", "unanswered");
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
/**
|
|
370
|
+
/** Inline markdown code (`` `x` ``) as mono `code` spans, the rest plain, as the ANSI rows render it. */
|
|
371
|
+
function inlineCodeSpans(value: string): TspSpan[] {
|
|
372
|
+
return value
|
|
373
|
+
.split(/(`[^`]+`)/)
|
|
374
|
+
.flatMap(piece =>
|
|
375
|
+
piece === ""
|
|
376
|
+
? []
|
|
377
|
+
: piece.length > 2 && piece.startsWith("`") && piece.endsWith("`")
|
|
378
|
+
? [span(piece.slice(1, -1), "code")]
|
|
379
|
+
: [span(piece)],
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** An option's native label: the `(Recommended)` suffix goes (the row's badge says it), inline code runs mono. */
|
|
384
|
+
function optionLabelSpans(question: ExtensionAskDialogQuestion, optionIndex: number, label: string): TspSpan[] {
|
|
385
|
+
const recommended = question.recommended === optionIndex && label.endsWith(RECOMMENDED_SUFFIX);
|
|
386
|
+
return inlineCodeSpans(recommended ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isAnswered(state: QuestionState): boolean {
|
|
390
|
+
return state.selectedOptions.size > 0 || state.customInput !== undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** The answer to one question for the Review tab: the chosen labels (code mono), the custom answer, the note. */
|
|
371
394
|
function describeAnswer(question: ExtensionAskDialogQuestion, state: QuestionState): readonly TspSpan[] {
|
|
372
|
-
if (state
|
|
373
|
-
const
|
|
395
|
+
if (!isAnswered(state)) return [span("Unanswered", "warning")];
|
|
396
|
+
const labels = displayOptionLabels(question);
|
|
397
|
+
const spans: TspSpan[] = [];
|
|
398
|
+
const push = (parts: readonly TspSpan[]): void => {
|
|
399
|
+
if (spans.length > 0) spans.push(span(", ", "dim"));
|
|
400
|
+
spans.push(...parts);
|
|
401
|
+
};
|
|
402
|
+
question.options.forEach((option, index) => {
|
|
403
|
+
if (state.selectedOptions.has(option.label))
|
|
404
|
+
push(optionLabelSpans(question, index, labels[index] ?? option.label));
|
|
405
|
+
});
|
|
406
|
+
if (state.customInput !== undefined) push([span(`“${normalizedInlineInput(state.customInput)}”`)]);
|
|
374
407
|
const note = noteForSubmittedAnswer(question, state)?.trim();
|
|
375
|
-
if (note) spans.push(span(
|
|
408
|
+
if (note) spans.push(span(" ✎ ", "dim"), span(normalizedInlineInput(note), "em"));
|
|
376
409
|
return spans;
|
|
377
410
|
}
|
|
378
411
|
|
|
379
|
-
/** Native `item.detail` of an option row: its description
|
|
412
|
+
/** Native `item.detail` of an option row: its description or the typed custom answer, then its note (`em`). */
|
|
380
413
|
function describeRowDetail(
|
|
381
414
|
rowItem: QuestionRow,
|
|
382
415
|
question: ExtensionAskDialogQuestion,
|
|
@@ -385,12 +418,12 @@ function describeRowDetail(
|
|
|
385
418
|
const spans: TspSpan[] = [];
|
|
386
419
|
if (rowItem.kind === "option") {
|
|
387
420
|
const description = question.options[rowItem.optionIndex ?? -1]?.description?.trim();
|
|
388
|
-
if (description) spans.push(
|
|
421
|
+
if (description) spans.push(...inlineCodeSpans(normalizedInlineInput(description)));
|
|
389
422
|
} else if (state.customInput !== undefined) {
|
|
390
|
-
spans.push(span(`“${normalizedInlineInput(state.customInput)}
|
|
423
|
+
spans.push(span(`“${normalizedInlineInput(state.customInput)}”`));
|
|
391
424
|
}
|
|
392
425
|
if (state.note && state.noteRowKey === rowItem.key) {
|
|
393
|
-
spans.push(span(`${spans.length > 0 ? "
|
|
426
|
+
spans.push(span(`${spans.length > 0 ? " " : ""}✎ `, "dim"), span(normalizedInlineInput(state.note), "em"));
|
|
394
427
|
}
|
|
395
428
|
return spans.length > 0 ? spans : undefined;
|
|
396
429
|
}
|
|
@@ -524,7 +557,6 @@ export function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]
|
|
|
524
557
|
}
|
|
525
558
|
|
|
526
559
|
export class AskDialogComponent implements Component {
|
|
527
|
-
readonly retireDisplacedTranscript = true;
|
|
528
560
|
#states: QuestionState[];
|
|
529
561
|
#activeTabIndex = 0;
|
|
530
562
|
#submitScrollOffset = 0;
|
|
@@ -543,6 +575,8 @@ export class AskDialogComponent implements Component {
|
|
|
543
575
|
#contentWidth = 76;
|
|
544
576
|
#headerExpandable = false;
|
|
545
577
|
#descExpandable = false;
|
|
578
|
+
/** Collapsed to one line (`app.ask.hide` / the Hide button) while the question stays pending. */
|
|
579
|
+
#hidden = false;
|
|
546
580
|
/** Memoized native description; dropped whenever dialog state may have changed. */
|
|
547
581
|
#native: NativeNode | undefined;
|
|
548
582
|
/** Input-guard blocked state {@link #native} was described with (the guard flips outside this dialog). */
|
|
@@ -632,7 +666,7 @@ export class AskDialogComponent implements Component {
|
|
|
632
666
|
}
|
|
633
667
|
|
|
634
668
|
#canToggleExpansion(): boolean {
|
|
635
|
-
if (this.#closed || this.#isSubmitTab()) return false;
|
|
669
|
+
if (this.#closed || this.#hidden || this.#isSubmitTab()) return false;
|
|
636
670
|
const question = this.#questions[this.#currentQuestionIndex()];
|
|
637
671
|
if (!question) return false;
|
|
638
672
|
if (this.#expanded) return true;
|
|
@@ -652,6 +686,10 @@ export class AskDialogComponent implements Component {
|
|
|
652
686
|
this.#finishCancel();
|
|
653
687
|
return;
|
|
654
688
|
}
|
|
689
|
+
if (getKeybindings().matches(keyData, "app.ask.hide")) {
|
|
690
|
+
this.#setHidden(!this.#hidden);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
655
693
|
// Expand before the draft-input guard so Ctrl+O can reveal a truncated
|
|
656
694
|
// question even while a pending prompt still owns typing.
|
|
657
695
|
if (matchesAppToolsExpand(keyData)) {
|
|
@@ -664,6 +702,12 @@ export class AskDialogComponent implements Component {
|
|
|
664
702
|
this.#requestRender();
|
|
665
703
|
return;
|
|
666
704
|
}
|
|
705
|
+
if (this.#hidden) {
|
|
706
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
707
|
+
this.#setHidden(false);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
667
711
|
if (this.#hasSubmitTab() && handleTabSwitchKey(keyData, direction => this.#switchTab(direction))) {
|
|
668
712
|
this.#requestRender();
|
|
669
713
|
return;
|
|
@@ -680,6 +724,7 @@ export class AskDialogComponent implements Component {
|
|
|
680
724
|
// renders as the next sibling in the same container, so this lands in the
|
|
681
725
|
// same frame).
|
|
682
726
|
this.options.inputGuard?.syncPresentation?.();
|
|
727
|
+
if (this.#hidden) return this.#renderHidden(width);
|
|
683
728
|
const innerWidth = Math.max(1, width - 4);
|
|
684
729
|
this.#contentWidth = innerWidth;
|
|
685
730
|
// Fixed panel height: measured from the tallest tab at spawn and
|
|
@@ -712,10 +757,15 @@ export class AskDialogComponent implements Component {
|
|
|
712
757
|
|
|
713
758
|
/**
|
|
714
759
|
* In the dock, in the composer's place and framed as the composer is: a
|
|
715
|
-
* `col` with
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
760
|
+
* `col` with an `zeta.editor.*` root role (`zeta.editor.ask`, collapsed
|
|
761
|
+
* `zeta.editor.ask.hidden`), so the terminal draws the composer's shell
|
|
762
|
+
* around it. Not a modal `overlay` sheet: one anchored at the bottom
|
|
763
|
+
* covered the transcript rows that explain the question and blocked
|
|
764
|
+
* scrolling until the question was answered.
|
|
765
|
+
*
|
|
766
|
+
* Anatomy: the question tabs (several questions), the question, its
|
|
767
|
+
* answers as radio/check rows, and the action bar as the footer (the
|
|
768
|
+
* countdown, Hide, then Note, Skip and the primary Submit/Next).
|
|
719
769
|
*/
|
|
720
770
|
describe(cx: DescribeContext): NativeNode {
|
|
721
771
|
const inputGuard = this.options.inputGuard;
|
|
@@ -725,61 +775,100 @@ export class AskDialogComponent implements Component {
|
|
|
725
775
|
const ring = this.#countdown !== undefined && cx.supports("meter");
|
|
726
776
|
if (ring) this.#startRingTick();
|
|
727
777
|
if (this.#native && this.#nativeBlocked === blocked) return this.#native;
|
|
778
|
+
if (this.#hidden) {
|
|
779
|
+
this.#native = this.#describeHidden(ring);
|
|
780
|
+
this.#nativeBlocked = blocked;
|
|
781
|
+
return this.#native;
|
|
782
|
+
}
|
|
728
783
|
const children: NativeChild[] = [];
|
|
729
|
-
const head = this.#describeHead(
|
|
784
|
+
const head = this.#describeHead();
|
|
730
785
|
if (head) children.push(head);
|
|
731
786
|
if (this.#isSubmitTab()) this.#describeSubmitBody(children);
|
|
732
787
|
else this.#describeQuestionBody(children);
|
|
733
|
-
children.push(this.#describeActions(blocked));
|
|
734
|
-
this.#native = col(children, { role: "zeta.editor", gap: "md" });
|
|
788
|
+
children.push(this.#describeActions(blocked, ring));
|
|
789
|
+
this.#native = col(children, { role: "zeta.editor.ask", gap: "md" });
|
|
735
790
|
this.#nativeBlocked = blocked;
|
|
736
791
|
return this.#native;
|
|
737
792
|
}
|
|
738
793
|
|
|
739
|
-
/**
|
|
740
|
-
#describeHead(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
794
|
+
/** The question tabs, an answered one checked, then Review; none for a lone single-choice question. */
|
|
795
|
+
#describeHead(): NativeNode | undefined {
|
|
796
|
+
if (!this.#hasSubmitTab()) return undefined;
|
|
797
|
+
const items = this.#questions.map((question, index) => {
|
|
798
|
+
const name = questionTabName(question, index);
|
|
799
|
+
const state = this.#states[index];
|
|
800
|
+
return {
|
|
744
801
|
id: String(index),
|
|
745
|
-
label:
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
802
|
+
label: state && isAnswered(state) ? [span("✓ ", "success"), span(name)] : [span(name)],
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
items.push({ id: "submit", label: [span(REVIEW_TAB)] });
|
|
806
|
+
const active = this.#isSubmitTab() ? "submit" : String(this.#activeTabIndex);
|
|
807
|
+
return node(
|
|
808
|
+
"row",
|
|
809
|
+
{ role: "zeta.ask.head", align: "center" },
|
|
810
|
+
[
|
|
750
811
|
node(
|
|
751
812
|
"tabs",
|
|
752
|
-
{
|
|
813
|
+
{
|
|
814
|
+
items,
|
|
815
|
+
active,
|
|
816
|
+
role: "zeta.ask.questions",
|
|
817
|
+
title: "Next question tab · Previous shift+tab",
|
|
818
|
+
actions: { click: "select" },
|
|
819
|
+
},
|
|
753
820
|
undefined,
|
|
754
821
|
"tabs",
|
|
755
822
|
),
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
823
|
+
],
|
|
824
|
+
"head",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** The countdown: a ring when the terminal draws meters, else a clocked `elapsed`. */
|
|
829
|
+
#describeTimer(countdown: CountdownTimer, ring: boolean): NativeNode {
|
|
830
|
+
if (!ring) return countdown.describe();
|
|
831
|
+
const total = Math.max(1, this.options.timeout ?? 1);
|
|
832
|
+
const left = Math.max(0, this.#countdownDeadline - Date.now());
|
|
833
|
+
return node(
|
|
834
|
+
"meter",
|
|
835
|
+
{
|
|
836
|
+
value: Math.round((left / total) * 1000) / 1000,
|
|
837
|
+
style: "ring",
|
|
838
|
+
size: "sm",
|
|
839
|
+
label: `${Math.ceil(left / 1000)}s`,
|
|
840
|
+
title: "Answers the recommended option when the time runs out",
|
|
841
|
+
},
|
|
842
|
+
undefined,
|
|
843
|
+
"timer",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Hidden: one row in the composer's place — the countdown, the question clipped to a line, Skip and Show. */
|
|
848
|
+
#describeHidden(ring: boolean): NativeNode {
|
|
849
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
850
|
+
const more = this.#questions.length > 1 ? ` +${this.#questions.length - 1} more` : "";
|
|
851
|
+
const summary = text(
|
|
852
|
+
[
|
|
853
|
+
span("Question ", "muted"),
|
|
854
|
+
span(normalizedInlineInput(sanitizeCarriageReturns(question?.question ?? ""))),
|
|
855
|
+
...(more ? [span(more, "muted")] : []),
|
|
856
|
+
],
|
|
857
|
+
{ truncate: "end", role: "zeta.ask.summary" },
|
|
858
|
+
);
|
|
859
|
+
const hideKey = getKeybindings().getKeys("app.ask.hide")[0];
|
|
860
|
+
const show = actionButton("Show", "show", {
|
|
861
|
+
tone: "accent",
|
|
862
|
+
...(hideKey ? { keys: hideKey } : {}),
|
|
863
|
+
title: `Show the question${hideKey ? ` ${formatTooltipKey(hideKey)}` : ""}`,
|
|
864
|
+
});
|
|
865
|
+
const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
866
|
+
const skip = actionButton("Skip", "cancel", cancelKey ? { keys: cancelKey } : {});
|
|
867
|
+
const items: (NativeNode | null)[] = [];
|
|
868
|
+
if (this.#countdown) items.push(this.#describeTimer(this.#countdown, ring));
|
|
869
|
+
// The summary grows (`zeta.ask.summary`), end-aligning Skip and Show.
|
|
870
|
+
items.push(summary, skip, show);
|
|
871
|
+
return col([actionBar(items)], { role: "zeta.editor.ask.hidden", gap: "md" });
|
|
783
872
|
}
|
|
784
873
|
|
|
785
874
|
/** Question tab: the question as markdown and its answers as radio/check rows (label, description, badge). */
|
|
@@ -797,7 +886,8 @@ export class AskDialogComponent implements Component {
|
|
|
797
886
|
"question",
|
|
798
887
|
),
|
|
799
888
|
);
|
|
800
|
-
|
|
889
|
+
// Radios (`zeta.ask.option`, `zeta.ask.other`) or checks (`zeta.ask.check`, `zeta.ask.other.check`); a chosen
|
|
890
|
+
// row is `accent`-toned (a check also carries the check glyph), apart from the highlighted row (`selected`).
|
|
801
891
|
const items = rows.map(rowItem => {
|
|
802
892
|
const option = rowItem.kind === "option" ? question.options[rowItem.optionIndex ?? -1] : undefined;
|
|
803
893
|
const checked =
|
|
@@ -805,33 +895,29 @@ export class AskDialogComponent implements Component {
|
|
|
805
895
|
? state.customInput !== undefined
|
|
806
896
|
: option !== undefined && state.selectedOptions.has(option.label);
|
|
807
897
|
const recommended = rowItem.kind === "option" && question.recommended === rowItem.optionIndex;
|
|
808
|
-
// The badge replaces the ANSI `(Recommended)` suffix.
|
|
809
|
-
const label =
|
|
810
|
-
rowItem.kind === "other"
|
|
811
|
-
? OTHER_LABEL
|
|
812
|
-
: recommended && rowItem.label.endsWith(RECOMMENDED_SUFFIX)
|
|
813
|
-
? rowItem.label.slice(0, -RECOMMENDED_SUFFIX.length)
|
|
814
|
-
: rowItem.label;
|
|
815
898
|
const detail =
|
|
816
899
|
describeRowDetail(rowItem, question, state) ??
|
|
817
|
-
(rowItem.kind === "other" ? [span("Type your own answer"
|
|
900
|
+
(rowItem.kind === "other" ? [span("Type your own answer")] : undefined);
|
|
901
|
+
const role =
|
|
902
|
+
rowItem.kind === "other"
|
|
903
|
+
? question.multi
|
|
904
|
+
? "zeta.ask.other.check"
|
|
905
|
+
: "zeta.ask.other"
|
|
906
|
+
: question.multi
|
|
907
|
+
? "zeta.ask.check"
|
|
908
|
+
: "zeta.ask.option";
|
|
818
909
|
return node(
|
|
819
910
|
"item",
|
|
820
911
|
{
|
|
821
|
-
//
|
|
822
|
-
label:
|
|
823
|
-
.
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
: piece.length > 2 && piece.startsWith("`") && piece.endsWith("`")
|
|
828
|
-
? [span(piece.slice(1, -1), "code")]
|
|
829
|
-
: [span(piece)],
|
|
830
|
-
),
|
|
831
|
-
role: rowItem.kind === "other" ? "zeta.ask.other" : optionRole,
|
|
912
|
+
// The badge replaces the ANSI `(Recommended)` suffix.
|
|
913
|
+
label:
|
|
914
|
+
rowItem.kind === "other"
|
|
915
|
+
? [span(OTHER_LABEL)]
|
|
916
|
+
: optionLabelSpans(question, rowItem.optionIndex ?? -1, rowItem.label),
|
|
917
|
+
role,
|
|
832
918
|
...(detail ? { detail } : {}),
|
|
833
|
-
...(recommended ? { value: [span("Recommended"
|
|
834
|
-
...(checked ? {
|
|
919
|
+
...(recommended ? { value: [span("Recommended")] } : {}),
|
|
920
|
+
...(checked ? { tone: "accent" as const, ...(question.multi ? { icon: "check" } : {}) } : {}),
|
|
835
921
|
},
|
|
836
922
|
undefined,
|
|
837
923
|
rowItem.key,
|
|
@@ -849,69 +935,87 @@ export class AskDialogComponent implements Component {
|
|
|
849
935
|
const highlighted = rows[state.cursorIndex];
|
|
850
936
|
const option = highlighted?.kind === "option" ? question.options[highlighted.optionIndex ?? -1] : undefined;
|
|
851
937
|
if (option?.preview?.trim()) {
|
|
852
|
-
children.push(
|
|
938
|
+
children.push(
|
|
939
|
+
node("md", { text: replaceTabs(option.preview), role: "zeta.ask.preview" }, undefined, "preview"),
|
|
940
|
+
);
|
|
853
941
|
}
|
|
854
942
|
}
|
|
855
943
|
|
|
856
|
-
/** Review tab: every answer as
|
|
944
|
+
/** Review tab: what is still unanswered, then every answer as question/answer pairs; Submit sends them. */
|
|
857
945
|
#describeSubmitBody(children: NativeChild[]): void {
|
|
858
946
|
const unanswered = this.#unansweredCount();
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
947
|
+
const notice =
|
|
948
|
+
unanswered > 0
|
|
949
|
+
? [
|
|
950
|
+
span(`${unanswered} unanswered`, "warning"),
|
|
951
|
+
span(` · Submit still sends ${unanswered === this.#questions.length ? "the questions" : "the rest"}`),
|
|
952
|
+
]
|
|
953
|
+
: [span("All answered · Submit sends them")];
|
|
954
|
+
children.push(node("text", { spans: notice, role: "zeta.ask.notice" }, undefined, "notice"));
|
|
865
955
|
const answers: { k: string; v: readonly TspSpan[] }[] = [];
|
|
866
956
|
for (let index = 0; index < this.#questions.length; index++) {
|
|
867
957
|
const question = this.#questions[index];
|
|
868
958
|
const state = this.#states[index];
|
|
869
959
|
if (!question || !state) continue;
|
|
870
|
-
answers.push({ k:
|
|
960
|
+
answers.push({ k: questionTabName(question, index), v: describeAnswer(question, state) });
|
|
871
961
|
}
|
|
872
|
-
children.push(node("kv", { items: answers }, undefined, "answers"));
|
|
962
|
+
children.push(node("kv", { items: answers, role: "zeta.ask.answers" }, undefined, "answers"));
|
|
873
963
|
}
|
|
874
964
|
|
|
875
965
|
/**
|
|
876
|
-
* The action bar
|
|
877
|
-
*
|
|
878
|
-
*
|
|
966
|
+
* The action bar, the sheet's footer: the countdown and Hide (apart from
|
|
967
|
+
* the answer actions), then Note, Skip and the primary Submit (Enter's
|
|
968
|
+
* action on this tab; `Next` while more questions follow). Buttons say
|
|
969
|
+
* what they do; their keys live in their tooltips. A draft that owns input
|
|
970
|
+
* shows its hint in place of the answer actions.
|
|
879
971
|
*/
|
|
880
|
-
#describeActions(blocked: boolean): NativeNode {
|
|
972
|
+
#describeActions(blocked: boolean, ring: boolean): NativeNode {
|
|
881
973
|
const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
882
|
-
const skip = actionButton("Skip", "cancel",
|
|
974
|
+
const skip = actionButton("Skip", "cancel", {
|
|
975
|
+
...(cancelKey ? { keys: cancelKey } : {}),
|
|
976
|
+
title: `Skip without answering${cancelKey ? ` ${formatTooltipKey(cancelKey)}` : ""}`,
|
|
977
|
+
});
|
|
978
|
+
const hideKey = getKeybindings().getKeys("app.ask.hide")[0];
|
|
979
|
+
const hide = actionButton("Hide", "hide", {
|
|
980
|
+
...(hideKey ? { keys: hideKey } : {}),
|
|
981
|
+
title: `Hide until you are ready to answer${hideKey ? ` ${formatTooltipKey(hideKey)}` : ""}`,
|
|
982
|
+
});
|
|
983
|
+
const lead: NativeNode[] = this.#countdown ? [this.#describeTimer(this.#countdown, ring), hide] : [hide];
|
|
883
984
|
const inputGuard = this.options.inputGuard;
|
|
884
985
|
if (blocked && inputGuard) {
|
|
885
|
-
return actionBar([
|
|
986
|
+
return actionBar([
|
|
987
|
+
...lead,
|
|
988
|
+
text([span(plainText(inputGuard.hint), "muted")], { truncate: "end", role: "zeta.ask.summary" }),
|
|
989
|
+
skip,
|
|
990
|
+
]);
|
|
886
991
|
}
|
|
887
|
-
const moves = ["up/down move"];
|
|
888
|
-
if (this.#hasSubmitTab()) moves.push("tab switches question");
|
|
889
992
|
if (this.#isSubmitTab()) {
|
|
890
993
|
const submit = actionButton("Submit", "submit", {
|
|
891
994
|
keys: "enter",
|
|
892
995
|
tone: "accent",
|
|
893
996
|
title: "Submit answers enter",
|
|
894
997
|
});
|
|
895
|
-
return actionBar([null, skip, submit]);
|
|
998
|
+
return actionBar([...lead, null, skip, submit]);
|
|
896
999
|
}
|
|
897
1000
|
const question = this.#questions[this.#currentQuestionIndex()];
|
|
898
1001
|
const last = this.#questions.length === 1;
|
|
899
1002
|
const enterLabel = last ? "Submit" : "Next";
|
|
900
|
-
|
|
1003
|
+
// A multi-select toggles on Space; Enter moves on.
|
|
901
1004
|
const submit = actionButton(enterLabel, "submit", {
|
|
902
1005
|
keys: "enter",
|
|
903
1006
|
tone: "accent",
|
|
904
|
-
title: `${enterLabel} enter ·
|
|
1007
|
+
title: question?.multi ? `${enterLabel} enter · Toggle space` : `${enterLabel} enter`,
|
|
905
1008
|
});
|
|
906
|
-
const note = actionButton("Note", "note", { keys: "n", title:
|
|
907
|
-
return actionBar([null, note, skip, submit]);
|
|
1009
|
+
const note = actionButton("Note", "note", { keys: "n", title: "Add a note to the highlighted answer n" });
|
|
1010
|
+
return actionBar([...lead, null, note, skip, submit]);
|
|
908
1011
|
}
|
|
909
1012
|
|
|
910
1013
|
/**
|
|
911
1014
|
* Pointer actions: a tab click switches to it; a click on an answer row is
|
|
912
1015
|
* Enter on it (Space for a multi-select option, so it toggles; its
|
|
913
1016
|
* double-click `activate` is Enter); the action bar's Submit, Note and Skip
|
|
914
|
-
* run exactly what Enter, `n` and Esc run on the highlighted row
|
|
1017
|
+
* run exactly what Enter, `n` and Esc run on the highlighted row; Hide/Show
|
|
1018
|
+
* collapse and restore the dialog.
|
|
915
1019
|
*/
|
|
916
1020
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
917
1021
|
if (this.#closed || this.#promptActive) return;
|
|
@@ -920,13 +1024,17 @@ export class AskDialogComponent implements Component {
|
|
|
920
1024
|
this.#finishCancel();
|
|
921
1025
|
return;
|
|
922
1026
|
}
|
|
923
|
-
if (
|
|
1027
|
+
if (event.act === "hide" || event.act === "show") {
|
|
1028
|
+
this.#setHidden(event.act === "hide");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (this.#hidden || this.options.inputGuard?.isBlocked()) return;
|
|
924
1032
|
if (event.act === "submit") this.#pressEnter();
|
|
925
1033
|
else if (event.act === "note") this.#pressNote();
|
|
926
1034
|
return;
|
|
927
1035
|
}
|
|
928
1036
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
929
|
-
if (this.options.inputGuard?.isBlocked()) return;
|
|
1037
|
+
if (this.#hidden || this.options.inputGuard?.isBlocked()) return;
|
|
930
1038
|
const key = leafKey(event.key);
|
|
931
1039
|
if (key === "tabs") {
|
|
932
1040
|
if (!this.#hasSubmitTab()) return;
|
|
@@ -1054,6 +1162,35 @@ export class AskDialogComponent implements Component {
|
|
|
1054
1162
|
return this.#remainingSeconds === undefined ? "Ask" : `Ask (${this.#remainingSeconds}s)`;
|
|
1055
1163
|
}
|
|
1056
1164
|
|
|
1165
|
+
#setHidden(hidden: boolean): void {
|
|
1166
|
+
if (this.#hidden === hidden) return;
|
|
1167
|
+
this.#hidden = hidden;
|
|
1168
|
+
this.#countdown?.reset();
|
|
1169
|
+
this.#requestRender();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Hidden: one line in the composer's place — title (with countdown), the question clipped to fit, and how to bring it back. */
|
|
1173
|
+
#renderHidden(width: number): readonly string[] {
|
|
1174
|
+
const hideKey = editorKey("app.ask.hide");
|
|
1175
|
+
const enter = formatKeyHint("enter");
|
|
1176
|
+
const inputGuard = this.options.inputGuard;
|
|
1177
|
+
const blocked = inputGuard?.isBlocked() ?? false;
|
|
1178
|
+
// While a draft owns input, Enter goes to the draft, so only the hide key restores.
|
|
1179
|
+
const show = blocked ? hideKey : hideKey ? `${hideKey}/${enter}` : enter;
|
|
1180
|
+
const hints: string[] = [];
|
|
1181
|
+
if (blocked && inputGuard) hints.push(inputGuard.hint);
|
|
1182
|
+
if (show) hints.push(`${show} show`);
|
|
1183
|
+
hints.push(`${editorKey("tui.select.cancel")} cancel`);
|
|
1184
|
+
const hint = theme.fg("dim", ` · ${hints.join(" · ")}`);
|
|
1185
|
+
const label = theme.fg("accent", `${this.#titleText()} hidden:`);
|
|
1186
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
1187
|
+
const more = this.#questions.length > 1 ? theme.fg("dim", ` (+${this.#questions.length - 1} more)`) : "";
|
|
1188
|
+
const summary = normalizedInlineInput(sanitizeCarriageReturns(question?.question ?? ""));
|
|
1189
|
+
const summaryWidth = Math.max(1, width - visibleWidth(label) - visibleWidth(more) - visibleWidth(hint) - 1);
|
|
1190
|
+
const clipped = truncateToWidth(summary, summaryWidth, Ellipsis.Unicode);
|
|
1191
|
+
return [truncateToWidth(`${label} ${theme.fg("muted", clipped)}${more}${hint}`, width, Ellipsis.Unicode)];
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1057
1194
|
#hasSubmitTab(): boolean {
|
|
1058
1195
|
// Multi questions confirm on the Submit tab (Enter toggles, never
|
|
1059
1196
|
// submits), so any multi question forces the tab even when there is
|
|
@@ -1119,7 +1256,9 @@ export class AskDialogComponent implements Component {
|
|
|
1119
1256
|
}
|
|
1120
1257
|
|
|
1121
1258
|
#footerHintText(indicator: string): string {
|
|
1122
|
-
|
|
1259
|
+
// Hide rides along with cancel: both leave the question view without answering.
|
|
1260
|
+
const hideKey = editorKey("app.ask.hide");
|
|
1261
|
+
const cancel = `${editorKey("tui.select.cancel")} cancel${hideKey ? ` · ${hideKey} hide` : ""}`;
|
|
1123
1262
|
const enter = formatKeyHint("enter");
|
|
1124
1263
|
const upDown = editorKeys("tui.select.up", "tui.select.down");
|
|
1125
1264
|
const inputGuard = this.options.inputGuard;
|