@linxiraos/pi-tui 1.2.1 → 1.3.1
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 +245 -224
- 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/status-notice.d.ts +2 -0
- 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 +23 -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/web-search-types.d.ts +1 -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/charts/table-data.ts +17 -6
- 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/status-notice.ts +5 -0
- 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 +482 -12
- 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 +4 -1
- package/src/prompt/composer.ts +200 -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 +79 -11
- 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 +300 -62
- 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 +190 -63
- 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-types.d.ts +1 -0
- package/src/tools/web-search-types.ts +1 -0
- 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 +160 -43
- 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
|
@@ -37,7 +37,7 @@ import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList
|
|
|
37
37
|
import type { TspScrollBy, TspSpan } from "@linxiraos/pi-wire";
|
|
38
38
|
import type { NativeChild, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
39
39
|
import { col, md, node, span, text } from "../native/describe";
|
|
40
|
-
import { actionBar, actionButton
|
|
40
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
41
41
|
|
|
42
42
|
interface BtwHistoryPanelOptions {
|
|
43
43
|
records: readonly BtwHistoryRecord[];
|
|
@@ -97,6 +97,25 @@ const STATUS: Record<BtwHistoryRecord["status"], { label: string; color: ThemeCo
|
|
|
97
97
|
interrupted: { label: "Interrupted", color: "warning" },
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
const SHEET = { role: "zeta.overlay.btwHistory", anchor: "center", size: "lg", head: "BTW history" } as const;
|
|
101
|
+
const EMPTY_SHEET = { role: "zeta.overlay.btwHistory", anchor: "center", size: "md", head: "BTW history" } as const;
|
|
102
|
+
|
|
103
|
+
/** A status worth naming next to an answer natively; an answered question needs none. */
|
|
104
|
+
function describeStatus(status: BtwHistoryTurn["status"]): TspSpan[] | undefined {
|
|
105
|
+
switch (status) {
|
|
106
|
+
case "running":
|
|
107
|
+
return [span("●", "accent", { fx: "pulse" }), span(" Answering", "accent")];
|
|
108
|
+
case "cancelled":
|
|
109
|
+
return [span("Cancelled", "warning")];
|
|
110
|
+
case "error":
|
|
111
|
+
return [span("Failed", "error")];
|
|
112
|
+
case "interrupted":
|
|
113
|
+
return [span("Interrupted", "warning")];
|
|
114
|
+
case "complete":
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
/** Session-local side questions. Selecting or copying never promotes them into chat. */
|
|
101
120
|
export class BtwHistoryPanel implements Component, Focusable {
|
|
102
121
|
readonly #options: BtwHistoryPanelOptions;
|
|
@@ -283,6 +302,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
283
302
|
#openComposer(record: BtwHistoryRecord): void {
|
|
284
303
|
const input = new Input();
|
|
285
304
|
input.prompt = theme.fg("accent", "Follow up: ");
|
|
305
|
+
input.placeholder = "Ask about this answer…";
|
|
286
306
|
if (this.#options.spaceHoldKeys !== undefined) input.spaceHold.keys = this.#options.spaceHoldKeys;
|
|
287
307
|
input.spaceHold.handler = this.#options.spaceHold?.(input);
|
|
288
308
|
const composer: FollowUpComposer = { recordId: record.id, input, abortController: new AbortController() };
|
|
@@ -349,8 +369,9 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
349
369
|
}
|
|
350
370
|
handleInput(data: string): void {
|
|
351
371
|
if (this.#composer) {
|
|
352
|
-
// The input owns every key, including panel shortcuts and
|
|
353
|
-
|
|
372
|
+
// The input owns every key it handles, including panel shortcuts and
|
|
373
|
+
// pasted text; the line keys it ignores still scroll the answer above.
|
|
374
|
+
if (!this.#composer.input.handleInput(data)) this.#scrollDetail(data);
|
|
354
375
|
this.#options.requestRender();
|
|
355
376
|
return;
|
|
356
377
|
}
|
|
@@ -397,33 +418,41 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
397
418
|
else if (matchesKey(data, "home")) this.#select(0);
|
|
398
419
|
else if (matchesKey(data, "end")) this.#select(this.#records.length - 1);
|
|
399
420
|
else return;
|
|
400
|
-
} else {
|
|
401
|
-
|
|
402
|
-
? "line-up"
|
|
403
|
-
: matchesSelectDown(data)
|
|
404
|
-
? "line-down"
|
|
405
|
-
: matchesSelectPageUp(data)
|
|
406
|
-
? "page-up"
|
|
407
|
-
: matchesSelectPageDown(data)
|
|
408
|
-
? "page-down"
|
|
409
|
-
: matchesKey(data, "home")
|
|
410
|
-
? "start"
|
|
411
|
-
: matchesKey(data, "end")
|
|
412
|
-
? "end"
|
|
413
|
-
: undefined;
|
|
414
|
-
if (by === "line-up") this.#detail.scroll(-1);
|
|
415
|
-
else if (by === "line-down") this.#detail.scroll(1);
|
|
416
|
-
else if (by === "page-up") this.#detail.page(-1);
|
|
417
|
-
else if (by === "page-down") this.#detail.page(1);
|
|
418
|
-
else if (!this.#detail.handleScrollKey(data)) return;
|
|
419
|
-
if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
420
|
-
this.#followLatest = matchesKey(data, "end");
|
|
421
|
+
} else if (!this.#scrollDetail(data)) {
|
|
422
|
+
return;
|
|
421
423
|
}
|
|
422
424
|
this.#options.requestRender();
|
|
423
425
|
}
|
|
424
426
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
+
/** Apply an answer-pane scroll key; a scroll stops following the newest row unless it is End. */
|
|
428
|
+
#scrollDetail(data: string): boolean {
|
|
429
|
+
const by: TspScrollBy | undefined = matchesSelectUp(data)
|
|
430
|
+
? "line-up"
|
|
431
|
+
: matchesSelectDown(data)
|
|
432
|
+
? "line-down"
|
|
433
|
+
: matchesSelectPageUp(data)
|
|
434
|
+
? "page-up"
|
|
435
|
+
: matchesSelectPageDown(data)
|
|
436
|
+
? "page-down"
|
|
437
|
+
: matchesKey(data, "home")
|
|
438
|
+
? "start"
|
|
439
|
+
: matchesKey(data, "end")
|
|
440
|
+
? "end"
|
|
441
|
+
: undefined;
|
|
442
|
+
if (by === "line-up") this.#detail.scroll(-1);
|
|
443
|
+
else if (by === "line-down") this.#detail.scroll(1);
|
|
444
|
+
else if (by === "page-up") this.#detail.page(-1);
|
|
445
|
+
else if (by === "page-down") this.#detail.page(1);
|
|
446
|
+
else if (!this.#detail.handleScrollKey(data)) return false;
|
|
447
|
+
if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
448
|
+
this.#followLatest = matchesKey(data, "end");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** A large glass sheet (the list beside the reader), palette-sized while there is nothing to list. */
|
|
453
|
+
get nativeOverlay(): NonNullable<Component["nativeOverlay"]> {
|
|
454
|
+
return this.#records.length > 0 ? SHEET : EMPTY_SHEET;
|
|
455
|
+
}
|
|
427
456
|
|
|
428
457
|
describe(): NativeNode {
|
|
429
458
|
const record = this.#selected();
|
|
@@ -445,55 +474,30 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
445
474
|
) {
|
|
446
475
|
return memo.node;
|
|
447
476
|
}
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
{ gap: "sm", shrink: 0, basis: 0.3, max: { w: "40ch" } },
|
|
469
|
-
[text(this.#describeFocusLabel("list"), { truncate: "end", lines: 1 }), this.#describeList()],
|
|
470
|
-
"history",
|
|
471
|
-
),
|
|
472
|
-
answerPane,
|
|
473
|
-
]
|
|
474
|
-
: [answerPane];
|
|
475
|
-
const children: NativeChild[] = [node("row", { gap: "lg", align: "start" }, panes, "body")];
|
|
476
|
-
if (composer) {
|
|
477
|
-
composer.input.focused = this.#focused;
|
|
478
|
-
const lines: NativeChild[] = [
|
|
479
|
-
text([span(`Topic: ${sanitizeDisplayLine(record?.question ?? "")}`, "dim")], { truncate: "end" }),
|
|
480
|
-
];
|
|
481
|
-
if (composer.notice) {
|
|
482
|
-
lines.push(text([span(composer.notice, this.#followUpPending ? "dim" : "warning")], { wrap: "word" }));
|
|
483
|
-
}
|
|
484
|
-
lines.push(composer.input);
|
|
485
|
-
children.push(node("col", undefined, lines, "composer"));
|
|
477
|
+
// The body row fills the sheet: the list and the reader scroll on their own,
|
|
478
|
+
// so the list stays put while a long answer scrolls. Plain columns, not
|
|
479
|
+
// `section`s (Tern draws those as foldable `##` headings). With one side
|
|
480
|
+
// question the list is noise: the reader alone.
|
|
481
|
+
const children: NativeChild[] = [];
|
|
482
|
+
if (record) {
|
|
483
|
+
const reader: NativeNode = {
|
|
484
|
+
...node(
|
|
485
|
+
"col",
|
|
486
|
+
{ role: "zeta.btw.reader", grow: 1, basis: 0, min: { w: 0 } },
|
|
487
|
+
[this.#describeThread(record)],
|
|
488
|
+
"details",
|
|
489
|
+
),
|
|
490
|
+
// Arrow/page keys on the answer pane move its scroller.
|
|
491
|
+
scroll: this.#scroll,
|
|
492
|
+
};
|
|
493
|
+
const panes = this.#records.length > 1 ? [this.#describeList(), reader] : [reader];
|
|
494
|
+
children.push(node("row", { role: "zeta.btw.body" }, panes, "body"));
|
|
495
|
+
} else {
|
|
496
|
+
children.push(this.#describeEmpty());
|
|
486
497
|
}
|
|
487
|
-
children.push(
|
|
488
|
-
//
|
|
489
|
-
const
|
|
490
|
-
const escapeCloses = !composer && !(running && !this.#options.escapeHides);
|
|
491
|
-
children.push({
|
|
492
|
-
...actionBar([null, actionButton("Close", "close", escapeCloses ? { keys: "escape" } : undefined)]),
|
|
493
|
-
key: "actions",
|
|
494
|
-
});
|
|
495
|
-
// The sheet is the frame (`nativeOverlay`): a borderless column.
|
|
496
|
-
const described = col(children, { gap: "md" });
|
|
498
|
+
children.push(this.#describeActions(record, canFollowUp, copied));
|
|
499
|
+
// The sheet is the frame (`nativeOverlay`); its last row is the pinned footer.
|
|
500
|
+
const described = col(children, { gap: "none" });
|
|
497
501
|
this.#native = {
|
|
498
502
|
records: this.#records,
|
|
499
503
|
selectedId: this.#selectedId,
|
|
@@ -511,8 +515,31 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
511
515
|
}
|
|
512
516
|
|
|
513
517
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
514
|
-
if (event.type === "action"
|
|
515
|
-
this.#
|
|
518
|
+
if (event.type === "action") {
|
|
519
|
+
const record = this.#selected();
|
|
520
|
+
switch (event.act) {
|
|
521
|
+
case "close":
|
|
522
|
+
this.#options.onClose();
|
|
523
|
+
return;
|
|
524
|
+
case "copy-answer":
|
|
525
|
+
if (record && getBtwCopyText(record) !== undefined) this.#options.onCopy(record);
|
|
526
|
+
break;
|
|
527
|
+
case "follow-up":
|
|
528
|
+
if (record) this.openFollowUp(record.id);
|
|
529
|
+
break;
|
|
530
|
+
case "stop":
|
|
531
|
+
if (record && getBtwLatestTurn(record).status === "running") this.#options.onCancel(record);
|
|
532
|
+
break;
|
|
533
|
+
case "send":
|
|
534
|
+
this.#composer?.input.submit();
|
|
535
|
+
break;
|
|
536
|
+
case "discard":
|
|
537
|
+
this.#composer?.input.onEscape?.();
|
|
538
|
+
break;
|
|
539
|
+
default:
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.#options.requestRender();
|
|
516
543
|
return;
|
|
517
544
|
}
|
|
518
545
|
if ((event.type !== "select" && event.type !== "activate") || this.#composer) return;
|
|
@@ -525,96 +552,130 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
525
552
|
this.#options.requestRender();
|
|
526
553
|
}
|
|
527
554
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
555
|
+
/**
|
|
556
|
+
* The questions, newest first, as two-line palette rows: the question, then
|
|
557
|
+
* its time, follow-up count and any status other than answered. While the
|
|
558
|
+
* reader has the keys the selection rests (`zeta.btw.list.idle`).
|
|
559
|
+
*/
|
|
533
560
|
#describeList(): NativeNode {
|
|
534
561
|
const items = this.#records.map(record => {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
record.id,
|
|
545
|
-
);
|
|
562
|
+
const followUps = record.followUps?.length ?? 0;
|
|
563
|
+
const detail: TspSpan[] = [span(this.#timeFormat.format(record.createdAt), "mono")];
|
|
564
|
+
if (followUps > 0) detail.push(span(` · ${followUps} follow-up${followUps === 1 ? "" : "s"}`));
|
|
565
|
+
const status = describeStatus(getBtwLatestTurn(record).status);
|
|
566
|
+
// The question is markdown: its `code` reads as mono, without the backticks.
|
|
567
|
+
const label = sanitizeDisplayLine(record.question)
|
|
568
|
+
.split(/`([^`]+)`/)
|
|
569
|
+
.flatMap((part, index) => (part ? [span(part, index % 2 === 1 ? "mono" : undefined)] : []));
|
|
570
|
+
return node("item", { label, detail, ...(status ? { value: status } : {}) }, undefined, record.id);
|
|
546
571
|
});
|
|
572
|
+
const role = this.#focus === "list" ? "zeta.btw.list" : "zeta.btw.list.idle";
|
|
547
573
|
return node(
|
|
548
|
-
"
|
|
549
|
-
{
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
},
|
|
553
|
-
items,
|
|
554
|
-
"records",
|
|
574
|
+
"col",
|
|
575
|
+
{ role: "zeta.btw.history", shrink: 0 },
|
|
576
|
+
[node("list", { role, selected: this.#selectedId ?? null }, items, "records")],
|
|
577
|
+
"history",
|
|
555
578
|
);
|
|
556
579
|
}
|
|
557
580
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return text([span("No side questions yet. Use /btw QUESTION to start one.", "muted")], { wrap: "word" });
|
|
561
|
-
const children: NativeChild[] = [];
|
|
562
|
-
if (copied)
|
|
563
|
-
children.push(node("text", { spans: [span("✓ Copied to clipboard", "success")] }, undefined, "copied"));
|
|
581
|
+
/** The selected side question as a short document: the asked question titles each answer. */
|
|
582
|
+
#describeThread(record: BtwHistoryRecord): NativeNode {
|
|
564
583
|
const turns = getBtwTurns(record);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
return col(children, { gap: "md" });
|
|
584
|
+
const children: NativeChild[] = turns.map((turn, index) => this.#describeTurn(turn, index, `turn-${index}`));
|
|
585
|
+
if (this.#composer) children.push(this.#describeComposer(this.#composer));
|
|
586
|
+
return node("col", { role: "zeta.btw.thread" }, children, "thread");
|
|
570
587
|
}
|
|
571
588
|
|
|
572
|
-
#describeTurn(turn: BtwHistoryTurn, key: string): NativeNode {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
589
|
+
#describeTurn(turn: BtwHistoryTurn, index: number, key: string): NativeNode {
|
|
590
|
+
const meta: TspSpan[] =
|
|
591
|
+
index === 0
|
|
592
|
+
? [span(this.#dateFormat.format(turn.createdAt))]
|
|
593
|
+
: [span("Follow-up · "), span(this.#timeFormat.format(turn.createdAt))];
|
|
594
|
+
const status = describeStatus(turn.status);
|
|
595
|
+
if (status) meta.push(span(" · "), ...status);
|
|
596
|
+
// The question stays markdown so its inline code and links render.
|
|
579
597
|
const children: NativeChild[] = [
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
span("Question", "accent strong"),
|
|
583
|
-
span(" · ", "dim"),
|
|
584
|
-
span(status.label, status.color),
|
|
585
|
-
span(` · ${this.#dateFormat.format(turn.createdAt)}`, "dim"),
|
|
586
|
-
],
|
|
587
|
-
{ wrap: "word" },
|
|
588
|
-
),
|
|
589
|
-
md(sanitizeDisplayText(turn.question)),
|
|
590
|
-
text([span("Answer", "accent strong")]),
|
|
591
|
-
answer,
|
|
598
|
+
md(sanitizeDisplayText(turn.question), { role: index === 0 ? "zeta.btw.question" : "zeta.btw.followup" }),
|
|
599
|
+
text(meta, { role: "zeta.btw.meta", truncate: "end", lines: 1 }),
|
|
592
600
|
];
|
|
593
|
-
|
|
594
|
-
if (turn.
|
|
595
|
-
|
|
601
|
+
// A failed turn with no text says only why it failed.
|
|
602
|
+
if (turn.answer.trim() || !turn.error) {
|
|
603
|
+
const answer = turn.answer.trim()
|
|
604
|
+
? md(sanitizeDisplayText(turn.answer), { stream: turn.status === "running" })
|
|
605
|
+
: text([span(turn.status === "running" ? "Thinking…" : "No answer text.", "dim")], {
|
|
606
|
+
role: "zeta.btw.pending",
|
|
607
|
+
});
|
|
608
|
+
children.push(node("col", { role: "zeta.btw.answer" }, [answer], "answer"));
|
|
609
|
+
}
|
|
610
|
+
if (turn.error) {
|
|
611
|
+
children.push(text([span(sanitizeErrorLine(turn.error), "error")], { role: "zeta.btw.error", wrap: "word" }));
|
|
612
|
+
}
|
|
613
|
+
if (turn.status === "interrupted") {
|
|
614
|
+
children.push(text([span("Not resumed in this view.", "muted")], { role: "zeta.btw.note" }));
|
|
615
|
+
}
|
|
616
|
+
return node("col", { role: "zeta.btw.turn" }, children, key);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** The follow-up field under the newest answer, where the conversation continues. */
|
|
620
|
+
#describeComposer(composer: FollowUpComposer): NativeNode {
|
|
621
|
+
composer.input.focused = this.#focused;
|
|
622
|
+
const children: NativeChild[] = [composer.input];
|
|
623
|
+
if (composer.notice) {
|
|
624
|
+
children.push(
|
|
625
|
+
text([span(composer.notice, this.#followUpPending ? "dim" : "warning")], {
|
|
626
|
+
role: "zeta.btw.notice",
|
|
627
|
+
wrap: "word",
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return node("col", { role: "zeta.btw.composer", gap: "sm" }, children, "composer");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#describeEmpty(): NativeNode {
|
|
635
|
+
return node(
|
|
636
|
+
"col",
|
|
637
|
+
{ role: "zeta.btw.empty", align: "center", gap: "sm" },
|
|
638
|
+
[
|
|
639
|
+
text("No side questions yet", { role: "zeta.btw.empty.title" }),
|
|
640
|
+
text(
|
|
641
|
+
[
|
|
642
|
+
span("Ask one with "),
|
|
643
|
+
span("/btw <question>", "code"),
|
|
644
|
+
span(". The answer stays out of the main conversation."),
|
|
645
|
+
],
|
|
646
|
+
{ role: "zeta.btw.empty.hint", wrap: "word" },
|
|
647
|
+
),
|
|
648
|
+
],
|
|
649
|
+
"empty",
|
|
650
|
+
);
|
|
596
651
|
}
|
|
597
652
|
|
|
598
|
-
|
|
653
|
+
/**
|
|
654
|
+
* The footer: what the selected answer allows on the left, Close and the one
|
|
655
|
+
* primary action (Follow up, or Send while composing) in the corner. Keys
|
|
656
|
+
* live in the buttons' tooltips.
|
|
657
|
+
*/
|
|
658
|
+
#describeActions(record: BtwHistoryRecord | undefined, canFollowUp: boolean, copied: boolean): NativeNode {
|
|
599
659
|
if (this.#composer) {
|
|
600
|
-
return
|
|
601
|
-
|
|
602
|
-
|
|
660
|
+
return actionBar([
|
|
661
|
+
null,
|
|
662
|
+
actionButton("Cancel", "discard", { keys: "escape" }),
|
|
663
|
+
actionButton(this.#followUpPending ? "Sending…" : "Send", "send", { keys: "enter", tone: "accent" }),
|
|
603
664
|
]);
|
|
604
665
|
}
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
running && this.#options.escapeHides ? { keys: ["x"], label: "cancel" } : undefined,
|
|
610
|
-
this.#records.length > 1 ? { keys: ["tab", "ctrl+/"], label: "switch pane" } : undefined,
|
|
611
|
-
actionHint(["tui.select.up", "tui.select.down"], this.#focus === "list" ? "select" : "scroll"),
|
|
612
|
-
];
|
|
613
|
-
if (canFollowUp) hints.push({ keys: ["f", "enter"], label: "follow up" });
|
|
666
|
+
const running = record !== undefined && getBtwLatestTurn(record).status === "running";
|
|
667
|
+
const hides = this.#options.escapeHides === true;
|
|
668
|
+
const buttons: (NativeNode | null)[] = [];
|
|
669
|
+
if (running) buttons.push(actionButton("Stop answer", "stop", { keys: hides ? "x" : "escape" }));
|
|
614
670
|
if (record && getBtwCopyText(record) !== undefined) {
|
|
615
|
-
|
|
671
|
+
buttons.push(actionButton(copied ? "Copy again" : "Copy answer", "copy-answer", { keys: "c" }));
|
|
672
|
+
if (copied) buttons.push(text([span("✓ Copied", "success")], { role: "zeta.btw.copied" }));
|
|
616
673
|
}
|
|
617
|
-
|
|
674
|
+
buttons.push(null);
|
|
675
|
+
// Clicking Close always dismisses the sheet; its tooltip names Esc only where Esc does too.
|
|
676
|
+
buttons.push(actionButton("Close", "close", running && !hides ? undefined : { keys: "escape" }));
|
|
677
|
+
if (canFollowUp) buttons.push(actionButton("Follow up", "follow-up", { keys: "f", tone: "accent" }));
|
|
678
|
+
return actionBar(buttons);
|
|
618
679
|
}
|
|
619
680
|
|
|
620
681
|
#renderList(width: number, height: number): readonly string[] {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { TspPickerGroup, TspPickerItem, TspProps, TspSpan, TspTone } from "@linxiraos/pi-wire";
|
|
9
9
|
import type { Component } from "../../tui";
|
|
10
|
-
import { formatKeyHint } from "../../app-keybindings";
|
|
11
10
|
import { node, span, stableKey } from "../../native/describe";
|
|
12
11
|
import type { NativeNode } from "../../native/node";
|
|
13
12
|
import { pickerFuzzyHits, pickerQuery } from "../../native/picker";
|
|
@@ -500,11 +499,10 @@ export class ExtensionList implements Component {
|
|
|
500
499
|
]
|
|
501
500
|
: undefined,
|
|
502
501
|
facts: { kind: this.#getKindLabel(ext.kind) },
|
|
503
|
-
disabled
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
: undefined,
|
|
502
|
+
// Only a disabled provider makes the row inert. An extension switched
|
|
503
|
+
// off by its own toggle must stay activatable, or the click/dblclick
|
|
504
|
+
// that turned it off could never turn it back on.
|
|
505
|
+
disabled: masterDisabled ? "Provider disabled" : undefined,
|
|
508
506
|
};
|
|
509
507
|
}
|
|
510
508
|
|
|
@@ -30,12 +30,21 @@ import { OverlayPanel } from "../chrome/overlay-box";
|
|
|
30
30
|
import { FormField } from "../components/form";
|
|
31
31
|
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
32
32
|
import { boundKeys, editorKey, interruptKey } from "../chrome/keybinding-hints";
|
|
33
|
-
import {
|
|
34
|
-
import
|
|
35
|
-
import {
|
|
33
|
+
import { formatTooltipKey, type KeyName } from "../key-hint-format";
|
|
34
|
+
import { col, node, span } from "../native/describe";
|
|
35
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
36
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
36
37
|
import { plainText } from "../native/spans";
|
|
37
38
|
import { sanitizeCarriageReturns } from "../render/render-utils";
|
|
38
39
|
|
|
40
|
+
/** Root role of the native sheet. */
|
|
41
|
+
const ROLE = "zeta.overlay.hook-editor";
|
|
42
|
+
/**
|
|
43
|
+
* Role prefix of its parts (`.head`, `.title`, `.detail`, `.eyebrow`, `.field`/`.line`); outside
|
|
44
|
+
* `zeta.overlay.*`, which Tern draws as a sheet of its own.
|
|
45
|
+
*/
|
|
46
|
+
const PART = "zeta.hook-editor";
|
|
47
|
+
|
|
39
48
|
/** Most title rows the terminal prompt shows, so a long question cannot push the input row off-screen. */
|
|
40
49
|
const MAX_PROMPT_TITLE_ROWS = 3;
|
|
41
50
|
/** Border (2) + padX (2) columns consumed by the panel chrome. */
|
|
@@ -109,7 +118,7 @@ export class HookEditorComponent extends OverlayPanel implements Focusable {
|
|
|
109
118
|
#pendingPastes: { settled: boolean; text: string | undefined }[] = [];
|
|
110
119
|
#submitQueued = false;
|
|
111
120
|
#disposed = false;
|
|
112
|
-
/**
|
|
121
|
+
/** Head, field and action bar; every part but the editor (which describes itself) is fixed, so it is built once. */
|
|
113
122
|
#nativeRoot: NativeNode;
|
|
114
123
|
/** Focus state mirrored to the nested editor during rendering. */
|
|
115
124
|
focused = false;
|
|
@@ -128,7 +137,7 @@ export class HookEditorComponent extends OverlayPanel implements Focusable {
|
|
|
128
137
|
const question = options?.question;
|
|
129
138
|
const terminalTitle = question === undefined ? title : boundPromptTitle(`${title}: `, question);
|
|
130
139
|
const [titleLine = "", ...detailLines] = terminalTitle.split("\n");
|
|
131
|
-
super(titleLine,
|
|
140
|
+
super(titleLine, ROLE);
|
|
132
141
|
|
|
133
142
|
this.#tui = tui;
|
|
134
143
|
this.#onSubmitCallback = onSubmit;
|
|
@@ -175,24 +184,24 @@ export class HookEditorComponent extends OverlayPanel implements Focusable {
|
|
|
175
184
|
this.addChild(this.#field);
|
|
176
185
|
this.addChild(new Spacer(1));
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{ keys: ["escape"], label: "cancel" },
|
|
186
|
-
externalHint,
|
|
187
|
-
]
|
|
188
|
-
: [
|
|
189
|
-
{ keys: followUpKeys, label: "submit" },
|
|
190
|
-
{ keys: boundKeys("app.interrupt", ["escape"]).slice(0, 1), label: "cancel" },
|
|
191
|
-
externalHint,
|
|
192
|
-
];
|
|
193
|
-
const nativeChildren: NativeChild[] = [];
|
|
187
|
+
// Native: the field drops its terminal frame for the sheet's, and says what to type while empty.
|
|
188
|
+
const fieldRole = this.#promptStyle ? `${PART}.line` : `${PART}.field`;
|
|
189
|
+
this.#editor.describeLayout = input => ({ role: fieldRole, children: [input], caret: "input" });
|
|
190
|
+
const placeholder = [span(this.#promptStyle ? "Type your answer" : "Type here")];
|
|
191
|
+
this.#editor.describePlaceholder = () => placeholder;
|
|
192
|
+
|
|
193
|
+
const head: NativeChild[] = [];
|
|
194
194
|
if (question !== undefined) {
|
|
195
|
-
|
|
195
|
+
// The question is the heading, as on the ask sheet; the prompt's own title names the answer above it.
|
|
196
|
+
head.push(
|
|
197
|
+
node(
|
|
198
|
+
"text",
|
|
199
|
+
{ spans: [span(plainText(title))], role: `${PART}.eyebrow`, wrap: "none", truncate: "end" },
|
|
200
|
+
undefined,
|
|
201
|
+
"eyebrow",
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
head.push(
|
|
196
205
|
node(
|
|
197
206
|
"md",
|
|
198
207
|
{ text: replaceTabs(sanitizeCarriageReturns(question)), role: "zeta.ask.question" },
|
|
@@ -200,30 +209,79 @@ export class HookEditorComponent extends OverlayPanel implements Focusable {
|
|
|
200
209
|
"question",
|
|
201
210
|
),
|
|
202
211
|
);
|
|
203
|
-
} else
|
|
204
|
-
|
|
212
|
+
} else {
|
|
213
|
+
head.push(
|
|
205
214
|
node(
|
|
206
215
|
"text",
|
|
207
|
-
{ spans: [span(plainText(
|
|
216
|
+
{ spans: [span(plainText(titleLine))], role: `${PART}.title`, wrap: "word" },
|
|
208
217
|
undefined,
|
|
209
|
-
"
|
|
218
|
+
"title",
|
|
210
219
|
),
|
|
211
220
|
);
|
|
221
|
+
if (detailLines.length > 0) {
|
|
222
|
+
head.push(
|
|
223
|
+
node(
|
|
224
|
+
"text",
|
|
225
|
+
{ spans: [span(plainText(detailLines.join("\n")))], wrap: "word", role: `${PART}.detail` },
|
|
226
|
+
undefined,
|
|
227
|
+
"detail",
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
212
231
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
|
|
233
|
+
// Buttons say what they do; their keys live in the tooltips.
|
|
234
|
+
const tip = (clauses: readonly (readonly [string, KeyName | undefined])[]): string =>
|
|
235
|
+
clauses.map(([label, key]) => (key ? `${label} ${formatTooltipKey(key)}` : label)).join(" · ");
|
|
236
|
+
const submitKey: KeyName = this.#promptStyle ? "enter" : primaryFollowUpKey;
|
|
237
|
+
const newlineKey: KeyName = this.#promptStyle ? "shift+enter" : "enter";
|
|
238
|
+
const cancelKey = this.#promptStyle ? "escape" : (boundKeys("app.interrupt", ["escape"])[0] ?? "escape");
|
|
239
|
+
const buttons: (NativeNode | null)[] = [];
|
|
240
|
+
if (this.#externalEditor) {
|
|
241
|
+
const [externalKey] = boundKeys("app.editor.external", ["ctrl+g"]);
|
|
242
|
+
buttons.push(
|
|
243
|
+
actionButton("Open in editor", "external", {
|
|
244
|
+
...(externalKey ? { keys: externalKey } : {}),
|
|
245
|
+
title: tip([["Edit in your external editor", externalKey]]),
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
buttons.push(
|
|
250
|
+
null,
|
|
251
|
+
actionButton("Cancel", "cancel", { keys: cancelKey, title: tip([["Cancel", cancelKey]]) }),
|
|
252
|
+
actionButton("Submit", "submit", {
|
|
253
|
+
keys: submitKey,
|
|
254
|
+
tone: "accent",
|
|
255
|
+
title: tip([
|
|
256
|
+
["Submit", submitKey],
|
|
257
|
+
["New line", newlineKey],
|
|
258
|
+
]),
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.#nativeRoot = col(
|
|
263
|
+
[node("col", { role: `${PART}.head`, gap: "xs" }, head, "head"), this.#editor, actionBar(buttons)],
|
|
264
|
+
{ role: ROLE },
|
|
218
265
|
);
|
|
219
266
|
}
|
|
220
267
|
|
|
221
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* A sheet in the composer's place: the title (or, answering a question, the question under the
|
|
270
|
+
* prompt's name) over a hairline, the editor, and Open in editor · Cancel · Submit.
|
|
271
|
+
*/
|
|
222
272
|
override describe(_cx: DescribeContext): NativeNode {
|
|
223
273
|
this.#field.focused = this.focused;
|
|
224
274
|
return this.#nativeRoot;
|
|
225
275
|
}
|
|
226
276
|
|
|
277
|
+
/** The action bar's buttons run what their keys run. */
|
|
278
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
279
|
+
if (this.#disposed || event.type !== "action") return;
|
|
280
|
+
if (event.act === "submit") this.#submitCurrentText();
|
|
281
|
+
else if (event.act === "cancel") this.#cancel();
|
|
282
|
+
else if (event.act === "external") void this.#openExternalEditor();
|
|
283
|
+
}
|
|
284
|
+
|
|
227
285
|
/** Keep the nested editor's software/hardware cursor mode aligned with the dialog focus target. */
|
|
228
286
|
setUseTerminalCursor(useTerminalCursor: boolean): void {
|
|
229
287
|
this.#field.setUseTerminalCursor(useTerminalCursor);
|