@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
|
@@ -40,17 +40,10 @@ import {
|
|
|
40
40
|
} from "../chrome/overlay-box";
|
|
41
41
|
import { appExternalEditorKey, matchesAppExternalEditor } from "../keybinding-matchers";
|
|
42
42
|
import type { KeyName } from "../key-hint-format";
|
|
43
|
-
import {
|
|
43
|
+
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
44
|
+
import { col, item, keyed, node, row as rowNode, span, text } from "../native/describe";
|
|
44
45
|
import { leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
45
|
-
import {
|
|
46
|
-
escCloseButton,
|
|
47
|
-
hintsRow,
|
|
48
|
-
itemIndex,
|
|
49
|
-
type NativeHint,
|
|
50
|
-
noteSpans,
|
|
51
|
-
overlayCard,
|
|
52
|
-
selectList,
|
|
53
|
-
} from "../native/overlay";
|
|
46
|
+
import { actionButton, hintsRow, itemIndex, type NativeHint, selectList } from "../native/overlay";
|
|
54
47
|
|
|
55
48
|
function fit(text: string, width: number): string {
|
|
56
49
|
if (width <= 0) return "";
|
|
@@ -158,6 +151,65 @@ function isTextSource(value: readonly ReviewDiffFile[] | TextReviewSource): valu
|
|
|
158
151
|
return !Array.isArray(value);
|
|
159
152
|
}
|
|
160
153
|
|
|
154
|
+
/** The native sheet: full-pane glass, titled by the review kind with what is under review after it. */
|
|
155
|
+
interface NativeSheet {
|
|
156
|
+
readonly role: "zeta.overlay.codeReview" | "zeta.overlay.annotateText";
|
|
157
|
+
readonly size: "full";
|
|
158
|
+
readonly head: TspSpan[];
|
|
159
|
+
}
|
|
160
|
+
const DIFF_SHEET_TITLE = "Code review";
|
|
161
|
+
const TEXT_SHEET_TITLE = "Annotate text";
|
|
162
|
+
|
|
163
|
+
const NOTE_PLACEHOLDER = "Write a note…";
|
|
164
|
+
|
|
165
|
+
type FileStatus = "added" | "deleted" | "renamed" | "modified";
|
|
166
|
+
const FILE_STATUS_ICON: Record<FileStatus, string> = {
|
|
167
|
+
added: "file-plus",
|
|
168
|
+
deleted: "trash",
|
|
169
|
+
renamed: "forward",
|
|
170
|
+
modified: "file",
|
|
171
|
+
};
|
|
172
|
+
const FILE_STATUS_TONE: Record<FileStatus, "success" | "error" | "info" | undefined> = {
|
|
173
|
+
added: "success",
|
|
174
|
+
deleted: "error",
|
|
175
|
+
renamed: "info",
|
|
176
|
+
modified: undefined,
|
|
177
|
+
};
|
|
178
|
+
const kFileStatus = Symbol("annotation.fileStatus");
|
|
179
|
+
|
|
180
|
+
/** How a file changed, read from its diff header (memoized on the frozen file). */
|
|
181
|
+
function fileStatus(file: ReviewDiffFile & { [kFileStatus]?: FileStatus }): FileStatus {
|
|
182
|
+
let status = file[kFileStatus];
|
|
183
|
+
if (status) return status;
|
|
184
|
+
const hunk = file.rawDiff.indexOf("\n@@");
|
|
185
|
+
const header = file.rawDiff.slice(0, hunk < 0 ? 4096 : hunk);
|
|
186
|
+
status = /^new file mode/m.test(header)
|
|
187
|
+
? "added"
|
|
188
|
+
: /^deleted file mode/m.test(header)
|
|
189
|
+
? "deleted"
|
|
190
|
+
: file.oldPath !== undefined && file.newPath !== undefined && file.oldPath !== file.newPath
|
|
191
|
+
? "renamed"
|
|
192
|
+
: "modified";
|
|
193
|
+
file[kFileStatus] = status;
|
|
194
|
+
return status;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `src/ui/app.ts` → `{ dir: "src/ui", leaf: "app.ts" }`. */
|
|
198
|
+
function splitPath(path: string): { dir: string; leaf: string } {
|
|
199
|
+
const slash = path.lastIndexOf("/");
|
|
200
|
+
return slash < 0 ? { dir: "", leaf: path } : { dir: path.slice(0, slash), leaf: path.slice(slash + 1) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `3 files`, `1 note`. */
|
|
204
|
+
function plural(count: number, noun: string): string {
|
|
205
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A review mode line, muted, with its `backticked` refs as code chips. */
|
|
209
|
+
function modeSpans(mode: string): TspSpan[] {
|
|
210
|
+
return mode.split("`").flatMap((part, index) => (part ? [span(part, index % 2 === 1 ? "code" : "muted")] : []));
|
|
211
|
+
}
|
|
212
|
+
|
|
161
213
|
/** A fullscreen, annotated diff picker that only relies on public OMP APIs. */
|
|
162
214
|
export class AnnotationOverlay implements Focusable {
|
|
163
215
|
focused = false;
|
|
@@ -196,6 +248,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
196
248
|
#nativeRoot: NativeNode | undefined;
|
|
197
249
|
/** Body the memoized root was built from. */
|
|
198
250
|
#nativeRootBody: NativeBody | undefined;
|
|
251
|
+
#nativeSheet: NativeSheet | undefined;
|
|
199
252
|
|
|
200
253
|
readonly #tui: TUI;
|
|
201
254
|
readonly #theme: Theme;
|
|
@@ -285,6 +338,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
285
338
|
});
|
|
286
339
|
this.#editor.setBorderVisible(false);
|
|
287
340
|
this.#editor.setPromptGutter("> ");
|
|
341
|
+
this.#editor.describePlaceholder = () => NOTE_PLACEHOLDER;
|
|
288
342
|
this.#editor.setScrollbarVisible(true);
|
|
289
343
|
this.#editor.onSubmit = value => this.#commitAnnotation(value);
|
|
290
344
|
this.#resetSourceCursor();
|
|
@@ -1331,6 +1385,30 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1331
1385
|
}
|
|
1332
1386
|
}
|
|
1333
1387
|
|
|
1388
|
+
/**
|
|
1389
|
+
* The terminal draws the frame: a full-pane glass sheet titled `Code review`
|
|
1390
|
+
* (or `Annotate text`), then what is under review as a quieter subtitle.
|
|
1391
|
+
*/
|
|
1392
|
+
get nativeOverlay(): NativeSheet {
|
|
1393
|
+
if (!this.#nativeSheet) {
|
|
1394
|
+
const subtitle = this.#textSource
|
|
1395
|
+
? [span(sanitizeStatusText(this.#textSource.label), "muted")]
|
|
1396
|
+
: modeSpans(sanitizeStatusText(this.#mode));
|
|
1397
|
+
this.#nativeSheet = {
|
|
1398
|
+
role: this.#textSource ? "zeta.overlay.annotateText" : "zeta.overlay.codeReview",
|
|
1399
|
+
size: "full",
|
|
1400
|
+
head: [span(this.#textSource ? TEXT_SHEET_TITLE : DIFF_SHEET_TITLE), ...subtitle],
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
return this.#nativeSheet;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* The sheet's body: the file rail (totals over the changed files) beside
|
|
1408
|
+
* the current file's diff (text: its numbered lines), the note composer
|
|
1409
|
+
* docked under the lines while a note is open, and a footer of key hints
|
|
1410
|
+
* and the actions.
|
|
1411
|
+
*/
|
|
1334
1412
|
describe(): NativeNode {
|
|
1335
1413
|
// The terminal lays out the file sidebar; diffs always have one, text never does.
|
|
1336
1414
|
this.#sidebarShown = !this.#textSource;
|
|
@@ -1338,6 +1416,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1338
1416
|
// Page size for PgUp/PgDn, matching the rendered body height.
|
|
1339
1417
|
const terminalHeight = this.#tui.terminal.rows;
|
|
1340
1418
|
this.#bodyHeight = Math.max(MIN_BODY_ROWS, terminalHeight - (this.#actions.length + 8));
|
|
1419
|
+
this.#editor.setMaxHeight(Math.max(1, Math.min(MAX_ANNOTATION_EDITOR_ROWS, terminalHeight - 12)));
|
|
1341
1420
|
this.#editor.focused = this.focused && this.#annotating;
|
|
1342
1421
|
const body = this.#describeBodyItems();
|
|
1343
1422
|
const chooser = this.#annotationChooser;
|
|
@@ -1346,73 +1425,55 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1346
1425
|
this.#fileIndex,
|
|
1347
1426
|
this.#sourceIndex,
|
|
1348
1427
|
this.#actionIndex,
|
|
1428
|
+
this.#bodyHeight,
|
|
1349
1429
|
this.#annotating ? `annotating:${this.#annotationScope}:${this.#editingAnnotationIndex}` : "",
|
|
1350
1430
|
chooser ? `chooser:${chooser.selected}:${chooser.entries.join()}` : "",
|
|
1351
1431
|
].join("|");
|
|
1352
1432
|
if (this.#nativeRoot && sig === this.#nativeSig && body === this.#nativeRootBody) return this.#nativeRoot;
|
|
1353
1433
|
|
|
1354
|
-
const
|
|
1434
|
+
const editing = this.#annotating || chooser !== undefined;
|
|
1435
|
+
const rowCount = this.#textSource ? this.#textLines.length : this.#currentSourceRows().length;
|
|
1355
1436
|
const lines = selectList("lines", body.items, {
|
|
1356
|
-
selected:
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1437
|
+
selected: rowCount > 0 ? `l${this.#sourceIndex}` : null,
|
|
1438
|
+
// Past 200 rows the terminal virtualizes the list itself (fixed one-line rows).
|
|
1439
|
+
max: { lines: this.#bodyHeight },
|
|
1440
|
+
empty: [span(this.#textSource ? "This text is empty" : "Nothing to review", "muted")],
|
|
1441
|
+
role: this.#textSource ? "zeta.review.text" : "zeta.review.diff",
|
|
1442
|
+
tone: this.#focus === "diff" && !editing ? "accent" : undefined,
|
|
1361
1443
|
});
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1444
|
+
const pane: NativeChild[] = [this.#describePaneHead()];
|
|
1445
|
+
pane.push(lines);
|
|
1446
|
+
const composer = this.#describeComposer();
|
|
1447
|
+
if (composer) pane.push(composer);
|
|
1448
|
+
const main = node("col", { role: "zeta.review.pane", grow: 1, min: { w: 0 } }, pane, "pane");
|
|
1449
|
+
const children: NativeChild[] = [];
|
|
1366
1450
|
if (this.#sidebarShown) {
|
|
1367
|
-
|
|
1368
|
-
"files",
|
|
1369
|
-
this.#files.map((file, index) => {
|
|
1370
|
-
const count = this.#annotationCount(index);
|
|
1371
|
-
const value = [
|
|
1372
|
-
span(`+${file.linesAdded}`, "ins"),
|
|
1373
|
-
span("/", "dim"),
|
|
1374
|
-
span(`-${file.linesRemoved}`, "del"),
|
|
1375
|
-
];
|
|
1376
|
-
if (count) value.push(span(` ✎${count}`, "warning"));
|
|
1377
|
-
return item(`f${index}`, { label: [span(displayFileLabel(file), "path")], value });
|
|
1378
|
-
}),
|
|
1379
|
-
{
|
|
1380
|
-
selected: this.#files.length > 0 ? `f${this.#fileIndex}` : null,
|
|
1381
|
-
role: "zeta.overlay.codeReview.files",
|
|
1382
|
-
tone: this.#focus === "files" ? "accent" : undefined,
|
|
1383
|
-
},
|
|
1384
|
-
);
|
|
1385
|
-
const sidebar = node("col", { max: { w: "34ch" }, shrink: 0 }, [files], "sidebar");
|
|
1386
|
-
children.push(keyed(rowNode([sidebar, main], { gap: "md", align: "start" }), "split"));
|
|
1451
|
+
children.push(keyed(rowNode([this.#describeRail(editing), main], { role: "zeta.review.split" }), "split"));
|
|
1387
1452
|
} else {
|
|
1388
1453
|
children.push(main);
|
|
1389
1454
|
}
|
|
1390
|
-
children.push(
|
|
1391
|
-
children.push(
|
|
1392
|
-
selectList(
|
|
1393
|
-
"actions",
|
|
1394
|
-
this.#actions.map((label, index) =>
|
|
1395
|
-
item(`a${index}`, { label, disabled: this.#actionDisabled(index) || undefined }),
|
|
1396
|
-
),
|
|
1397
|
-
{ selected: `a${this.#actionIndex}`, tone: this.#focus === "actions" ? "accent" : undefined },
|
|
1398
|
-
),
|
|
1399
|
-
);
|
|
1400
|
-
children.push(...this.#describeFooter());
|
|
1455
|
+
children.push(this.#describeFooter(editing));
|
|
1401
1456
|
|
|
1402
1457
|
this.#nativeSig = sig;
|
|
1403
1458
|
this.#nativeRootBody = body;
|
|
1404
|
-
this.#nativeRoot =
|
|
1405
|
-
this.#textSource ? "zeta.overlay.annotateText" : "zeta.overlay.codeReview",
|
|
1406
|
-
this.#textSource ? TEXT_OVERLAY_TITLE : OVERLAY_TITLE,
|
|
1407
|
-
children,
|
|
1408
|
-
);
|
|
1459
|
+
this.#nativeRoot = col(children);
|
|
1409
1460
|
return this.#nativeRoot;
|
|
1410
1461
|
}
|
|
1411
1462
|
|
|
1412
1463
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1413
1464
|
if (this.#finished || this.#externalOperation) return;
|
|
1414
|
-
if (event.type === "action"
|
|
1415
|
-
|
|
1465
|
+
if (event.type === "action") {
|
|
1466
|
+
if (event.act === "close") {
|
|
1467
|
+
this.#escape();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
// A draft or the note chooser is open: finish or leave it first.
|
|
1471
|
+
if (this.#annotating || this.#annotationChooser) return;
|
|
1472
|
+
const index =
|
|
1473
|
+
event.act === "review" && !this.#textSource ? 0 : event.act === "paste" ? (this.#textSource ? 0 : 1) : -1;
|
|
1474
|
+
if (index < 0 || this.#actionDisabled(index)) return;
|
|
1475
|
+
this.#actionIndex = index;
|
|
1476
|
+
this.#confirmAction();
|
|
1416
1477
|
return;
|
|
1417
1478
|
}
|
|
1418
1479
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
@@ -1445,99 +1506,124 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1445
1506
|
this.#focus = "diff";
|
|
1446
1507
|
this.#sourceIndex = index;
|
|
1447
1508
|
this.#viewportDriven = false;
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1451
|
-
case "actions": {
|
|
1452
|
-
const index = itemIndex(event.item, "a");
|
|
1453
|
-
if (index < 0 || index >= this.#actions.length || this.#actionDisabled(index)) return;
|
|
1454
|
-
this.#focus = "actions";
|
|
1455
|
-
this.#actionIndex = index;
|
|
1456
|
-
if (activate) this.#confirmAction();
|
|
1509
|
+
// Double-clicking a line opens its note, like `a`.
|
|
1510
|
+
if (activate) this.#startAnnotation("line");
|
|
1457
1511
|
return;
|
|
1458
1512
|
}
|
|
1459
1513
|
}
|
|
1460
1514
|
}
|
|
1461
1515
|
|
|
1462
|
-
/**
|
|
1516
|
+
/**
|
|
1517
|
+
* One item per diff/text row; notes are disabled `warning` items under
|
|
1518
|
+
* the row they belong to (file and whole-text notes lead). Diff rows carry
|
|
1519
|
+
* their kind as the tone (`success` added, `error` removed, `info` hunk).
|
|
1520
|
+
*/
|
|
1463
1521
|
#describeBodyItems(): NativeBody {
|
|
1464
1522
|
const cached = this.#nativeBody;
|
|
1465
1523
|
if (cached && cached.fileIndex === this.#fileIndex && cached.rev === this.#annotationRev) return cached;
|
|
1466
1524
|
const items: NativeNode[] = [];
|
|
1467
|
-
const note = (key: string,
|
|
1525
|
+
const note = (key: string, body: string, scope?: string) =>
|
|
1468
1526
|
items.push(
|
|
1469
1527
|
item(key, {
|
|
1470
|
-
label:
|
|
1528
|
+
label: [span(sanitizeStatusText(body))],
|
|
1529
|
+
value: scope ? [span(scope, "muted")] : undefined,
|
|
1530
|
+
icon: "pen",
|
|
1471
1531
|
disabled: true,
|
|
1472
1532
|
tone: "warning",
|
|
1473
|
-
role: "zeta.
|
|
1533
|
+
role: "zeta.review.note",
|
|
1474
1534
|
}),
|
|
1475
1535
|
);
|
|
1536
|
+
const notice = (key: string, label: string, icon: string) =>
|
|
1537
|
+
items.push(item(key, { label: [span(label, "muted")], icon, disabled: true, tone: "muted" }));
|
|
1476
1538
|
if (this.#textSource) {
|
|
1477
1539
|
for (const [index, entry] of this.#textAnnotations.entries()) {
|
|
1478
|
-
if (entry.annotation.scope === "text") note(`n${index}`,
|
|
1540
|
+
if (entry.annotation.scope === "text") note(`n${index}`, entry.annotation.note, "Whole text");
|
|
1479
1541
|
}
|
|
1542
|
+
const mono = this.#textSource.kind !== "message" && this.#textSource.kind !== "prompt";
|
|
1543
|
+
const width = String(this.#textLines.length).length;
|
|
1480
1544
|
for (const [sourceIndex, sourceLine] of this.#textLines.entries()) {
|
|
1545
|
+
items.push(
|
|
1546
|
+
item(`l${sourceIndex}`, {
|
|
1547
|
+
label: [span(replaceTabs(sanitizeText(sourceLine)), mono ? "mono" : undefined)],
|
|
1548
|
+
value: [span(String(sourceIndex + 1).padStart(width), "mono")],
|
|
1549
|
+
}),
|
|
1550
|
+
);
|
|
1481
1551
|
for (const [index, entry] of this.#textAnnotations.entries()) {
|
|
1482
1552
|
if (entry.annotation.scope === "line" && entry.sourceIndex === sourceIndex) {
|
|
1483
|
-
note(`n${index}`,
|
|
1553
|
+
note(`n${index}`, entry.annotation.note);
|
|
1484
1554
|
}
|
|
1485
1555
|
}
|
|
1486
|
-
items.push(item(`l${sourceIndex}`, { label: sanitizeText(sourceLine) }));
|
|
1487
1556
|
}
|
|
1488
1557
|
} else {
|
|
1489
1558
|
const file = this.#currentFile();
|
|
1490
1559
|
if (file) {
|
|
1491
1560
|
for (const [index, entry] of this.#annotations.entries()) {
|
|
1492
1561
|
if (entry.fileIndex === this.#fileIndex && entry.annotation.scope === "file") {
|
|
1493
|
-
note(`n${index}`,
|
|
1562
|
+
note(`n${index}`, entry.annotation.note, "File note");
|
|
1494
1563
|
}
|
|
1495
1564
|
}
|
|
1496
1565
|
if (file.isBinary) {
|
|
1497
|
-
|
|
1498
|
-
item("binary", { label: [span("Binary diff; no annotatable source rows", "dim")], disabled: true }),
|
|
1499
|
-
);
|
|
1566
|
+
notice("binary", "Binary file: no lines to annotate, but a file note works", "binary");
|
|
1500
1567
|
} else if (file.rows.length === 0) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1568
|
+
notice("empty", "No hunks: this looks like a rename-only change", "forward");
|
|
1569
|
+
}
|
|
1570
|
+
let lineWidth = 1;
|
|
1571
|
+
for (const diffRow of file.isBinary ? [] : file.rows) {
|
|
1572
|
+
if (!isSourceRow(diffRow)) continue;
|
|
1573
|
+
lineWidth = Math.max(
|
|
1574
|
+
lineWidth,
|
|
1575
|
+
String(diffRow.oldLine ?? "").length,
|
|
1576
|
+
String(diffRow.newLine ?? "").length,
|
|
1506
1577
|
);
|
|
1507
1578
|
}
|
|
1579
|
+
// The old/new line gutter is the item's value (drawn first); rows without numbers keep its width.
|
|
1580
|
+
const blankGutter = [span(" ".repeat(lineWidth * 2 + 1), "mono")];
|
|
1508
1581
|
let sourceIndex = 0;
|
|
1509
1582
|
for (const [rowIndex, diffRow] of file.isBinary ? [] : file.rows.entries()) {
|
|
1510
1583
|
if (!isSourceRow(diffRow)) {
|
|
1511
|
-
|
|
1512
|
-
|
|
1584
|
+
if (diffRow.kind === "hunk") {
|
|
1585
|
+
const match = /^(@@[^@]*@@)\s?(.*)$/.exec(sanitizeText(diffRow.raw));
|
|
1586
|
+
const label = match
|
|
1587
|
+
? [span(match[1]!, "mono"), span(match[2] ? ` ${replaceTabs(match[2])}` : "", "muted mono")]
|
|
1588
|
+
: [span(sanitizeText(diffRow.raw), "mono")];
|
|
1589
|
+
items.push(item(`r${rowIndex}`, { label, value: blankGutter, disabled: true, tone: "info" }));
|
|
1590
|
+
} else {
|
|
1591
|
+
items.push(
|
|
1592
|
+
item(`r${rowIndex}`, {
|
|
1593
|
+
label: [span("No newline at end of file", "muted")],
|
|
1594
|
+
value: blankGutter,
|
|
1595
|
+
disabled: true,
|
|
1596
|
+
tone: "muted",
|
|
1597
|
+
}),
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1513
1600
|
continue;
|
|
1514
1601
|
}
|
|
1602
|
+
const added = diffRow.kind === "added";
|
|
1603
|
+
const removed = diffRow.kind === "removed";
|
|
1604
|
+
const oldCell = String((added ? undefined : diffRow.oldLine) ?? "").padStart(lineWidth);
|
|
1605
|
+
const newCell = String((removed ? undefined : diffRow.newLine) ?? "").padStart(lineWidth);
|
|
1606
|
+
const color: DiffColor = added ? "toolDiffAdded" : removed ? "toolDiffRemoved" : "toolDiffContext";
|
|
1607
|
+
items.push(
|
|
1608
|
+
item(`l${sourceIndex}`, {
|
|
1609
|
+
label: [
|
|
1610
|
+
span(added ? "+ " : removed ? "- " : " ", `${color} mono`),
|
|
1611
|
+
span(replaceTabs(sanitizeText(diffRow.content)), "mono"),
|
|
1612
|
+
],
|
|
1613
|
+
value: [span(`${oldCell} ${newCell}`, "mono")],
|
|
1614
|
+
tone: added ? "success" : removed ? "error" : undefined,
|
|
1615
|
+
}),
|
|
1616
|
+
);
|
|
1617
|
+
// A line's notes sit under it, like review comments.
|
|
1515
1618
|
for (const [index, entry] of this.#annotations.entries()) {
|
|
1516
1619
|
if (
|
|
1517
1620
|
entry.fileIndex === this.#fileIndex &&
|
|
1518
1621
|
entry.sourceIndex === sourceIndex &&
|
|
1519
1622
|
entry.annotation.scope === "line"
|
|
1520
1623
|
) {
|
|
1521
|
-
note(`n${index}`,
|
|
1624
|
+
note(`n${index}`, entry.annotation.note);
|
|
1522
1625
|
}
|
|
1523
1626
|
}
|
|
1524
|
-
const marker = diffRow.kind === "added" ? "+" : diffRow.kind === "removed" ? "-" : " ";
|
|
1525
|
-
const lineNumber = diffRow.kind === "removed" ? diffRow.oldLine : (diffRow.newLine ?? diffRow.oldLine);
|
|
1526
|
-
const color: DiffColor =
|
|
1527
|
-
diffRow.kind === "added"
|
|
1528
|
-
? "toolDiffAdded"
|
|
1529
|
-
: diffRow.kind === "removed"
|
|
1530
|
-
? "toolDiffRemoved"
|
|
1531
|
-
: "toolDiffContext";
|
|
1532
|
-
items.push(
|
|
1533
|
-
item(`l${sourceIndex}`, {
|
|
1534
|
-
label: [
|
|
1535
|
-
span(marker, `${color} mono`),
|
|
1536
|
-
span(`${lineNumber ?? ""} `, "num dim mono"),
|
|
1537
|
-
span(sanitizeText(diffRow.content), `${color} mono`),
|
|
1538
|
-
],
|
|
1539
|
-
}),
|
|
1540
|
-
);
|
|
1541
1627
|
sourceIndex++;
|
|
1542
1628
|
}
|
|
1543
1629
|
}
|
|
@@ -1546,93 +1632,261 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1546
1632
|
return this.#nativeBody;
|
|
1547
1633
|
}
|
|
1548
1634
|
|
|
1549
|
-
|
|
1635
|
+
/** The file rail: the totals (file count, notes, `+a −r`) over the changed files. */
|
|
1636
|
+
#describeRail(editing: boolean): NativeNode {
|
|
1637
|
+
let added = 0;
|
|
1638
|
+
let removed = 0;
|
|
1639
|
+
for (const file of this.#files) {
|
|
1640
|
+
added += file.linesAdded;
|
|
1641
|
+
removed += file.linesRemoved;
|
|
1642
|
+
}
|
|
1643
|
+
const total: NativeChild[] = [text([span(plural(this.#files.length, "file"))], { grow: 1, wrap: "none" })];
|
|
1644
|
+
if (this.#annotations.length > 0) {
|
|
1645
|
+
total.push(
|
|
1646
|
+
text([span(plural(this.#annotations.length, "note"))], { role: "zeta.review.count", wrap: "none" }),
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
total.push(
|
|
1650
|
+
text([span(`+${added}`, "toolDiffAdded mono"), span(" "), span(`−${removed}`, "toolDiffRemoved mono")], {
|
|
1651
|
+
wrap: "none",
|
|
1652
|
+
}),
|
|
1653
|
+
);
|
|
1654
|
+
const files = selectList(
|
|
1655
|
+
"files",
|
|
1656
|
+
this.#files.map((file, index) => {
|
|
1657
|
+
const { dir, leaf } = splitPath(displayFileLabel(file));
|
|
1658
|
+
const status = fileStatus(file);
|
|
1659
|
+
const count = this.#annotationCount(index);
|
|
1660
|
+
const value: TspSpan[] = [];
|
|
1661
|
+
if (count) value.push(span(`✎ ${count}`, "warning"), span(" "));
|
|
1662
|
+
if (file.isBinary) value.push(span("bin", "muted"));
|
|
1663
|
+
else
|
|
1664
|
+
value.push(
|
|
1665
|
+
span(`+${file.linesAdded}`, "toolDiffAdded mono"),
|
|
1666
|
+
span(" "),
|
|
1667
|
+
span(`−${file.linesRemoved}`, "toolDiffRemoved mono"),
|
|
1668
|
+
);
|
|
1669
|
+
return item(`f${index}`, {
|
|
1670
|
+
label: [span(sanitizeStatusText(leaf))],
|
|
1671
|
+
detail: dir ? [span(sanitizeStatusText(dir))] : undefined,
|
|
1672
|
+
value,
|
|
1673
|
+
icon: file.isBinary ? "binary" : FILE_STATUS_ICON[status],
|
|
1674
|
+
tone: FILE_STATUS_TONE[status],
|
|
1675
|
+
});
|
|
1676
|
+
}),
|
|
1677
|
+
{
|
|
1678
|
+
selected: this.#files.length > 0 ? `f${this.#fileIndex}` : null,
|
|
1679
|
+
max: { lines: this.#bodyHeight },
|
|
1680
|
+
empty: [span("No files", "muted")],
|
|
1681
|
+
role: "zeta.review.files",
|
|
1682
|
+
tone: this.#focus === "files" && !editing ? "accent" : undefined,
|
|
1683
|
+
},
|
|
1684
|
+
);
|
|
1685
|
+
return node(
|
|
1686
|
+
"col",
|
|
1687
|
+
{ role: "zeta.review.rail" },
|
|
1688
|
+
[node("row", { role: "zeta.review.total", gap: "sm", align: "center" }, total, "total"), files],
|
|
1689
|
+
"rail",
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Over the lines: the current file's full path, how it changed, its note
|
|
1695
|
+
* count and `+a −r` (text: its line and note counts).
|
|
1696
|
+
*/
|
|
1697
|
+
#describePaneHead(): NativeNode {
|
|
1698
|
+
const children: NativeChild[] = [];
|
|
1699
|
+
const props = { role: "zeta.review.head", gap: "sm", align: "center" } as const;
|
|
1550
1700
|
if (this.#textSource) {
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
if (
|
|
1554
|
-
return
|
|
1701
|
+
children.push(text([span(plural(this.#textLines.length, "line"))], { grow: 1, wrap: "none" }));
|
|
1702
|
+
const notes = this.#textAnnotations.length;
|
|
1703
|
+
if (notes) children.push(text([span(plural(notes, "note"))], { role: "zeta.review.count", wrap: "none" }));
|
|
1704
|
+
return node("row", props, children, "head");
|
|
1555
1705
|
}
|
|
1556
1706
|
const file = this.#currentFile();
|
|
1557
|
-
if (!file) return
|
|
1707
|
+
if (!file) return node("row", props, [text([span("No reviewable files", "muted")])], "head");
|
|
1708
|
+
const status = fileStatus(file);
|
|
1709
|
+
children.push(
|
|
1710
|
+
text([span(sanitizeStatusText(displayFileLabel(file)), "path")], { truncate: "middle", min: { w: 0 } }),
|
|
1711
|
+
);
|
|
1712
|
+
const what =
|
|
1713
|
+
status === "renamed" && file.oldPath
|
|
1714
|
+
? `Renamed from ${sanitizeStatusText(file.oldPath)}`
|
|
1715
|
+
: status === "added"
|
|
1716
|
+
? "New file"
|
|
1717
|
+
: status === "deleted"
|
|
1718
|
+
? "Deleted"
|
|
1719
|
+
: file.isBinary
|
|
1720
|
+
? "Binary"
|
|
1721
|
+
: undefined;
|
|
1722
|
+
if (what) {
|
|
1723
|
+
children.push(
|
|
1724
|
+
text([span(what)], {
|
|
1725
|
+
role: "zeta.review.status",
|
|
1726
|
+
tone: FILE_STATUS_TONE[status],
|
|
1727
|
+
truncate: "end",
|
|
1728
|
+
min: { w: 0 },
|
|
1729
|
+
}),
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
children.push(node("spacer", { grow: 1 }));
|
|
1558
1733
|
const count = this.#annotationCount(this.#fileIndex);
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1734
|
+
if (count) children.push(text([span(plural(count, "note"))], { role: "zeta.review.count", wrap: "none" }));
|
|
1735
|
+
if (!file.isBinary) {
|
|
1736
|
+
children.push(
|
|
1737
|
+
text(
|
|
1738
|
+
[
|
|
1739
|
+
span(`+${file.linesAdded}`, "toolDiffAdded mono"),
|
|
1740
|
+
span(" "),
|
|
1741
|
+
span(`−${file.linesRemoved}`, "toolDiffRemoved mono"),
|
|
1742
|
+
],
|
|
1743
|
+
{ wrap: "none" },
|
|
1744
|
+
),
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
return node("row", props, children, "head");
|
|
1567
1748
|
}
|
|
1568
1749
|
|
|
1569
|
-
|
|
1750
|
+
/** Docked under the lines: the note being written (what it is on, then the field) or the note chooser. */
|
|
1751
|
+
#describeComposer(): NativeNode | undefined {
|
|
1570
1752
|
const chooser = this.#annotationChooser;
|
|
1571
1753
|
if (chooser) {
|
|
1572
1754
|
const entries = chooser.entries.map((index, i) => {
|
|
1573
1755
|
const entry = chooser.kind === "text" ? this.#textAnnotations[index] : this.#annotations[index];
|
|
1574
|
-
|
|
1756
|
+
// Every candidate is on the current file (or text), so the place is just the line.
|
|
1757
|
+
const annotation = entry?.annotation;
|
|
1758
|
+
const location = !annotation
|
|
1575
1759
|
? ""
|
|
1576
|
-
: "
|
|
1577
|
-
?
|
|
1578
|
-
:
|
|
1760
|
+
: annotation.scope === "file"
|
|
1761
|
+
? "File note"
|
|
1762
|
+
: annotation.scope === "text"
|
|
1763
|
+
? "Whole text"
|
|
1764
|
+
: "line" in annotation
|
|
1765
|
+
? `Line ${annotation.line}`
|
|
1766
|
+
: annotation.newLine !== undefined
|
|
1767
|
+
? `Line ${annotation.newLine}`
|
|
1768
|
+
: `Removed line ${annotation.oldLine ?? ""}`.trim();
|
|
1579
1769
|
return item(`c${i}`, {
|
|
1580
|
-
label: sanitizeStatusText(
|
|
1581
|
-
|
|
1770
|
+
label: sanitizeStatusText(annotation?.note.split(/\r?\n/, 1)[0] ?? ""),
|
|
1771
|
+
value: location,
|
|
1772
|
+
icon: "pen",
|
|
1582
1773
|
});
|
|
1583
1774
|
});
|
|
1584
|
-
return
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
[
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
"chooserHints",
|
|
1594
|
-
),
|
|
1595
|
-
];
|
|
1775
|
+
return node(
|
|
1776
|
+
"col",
|
|
1777
|
+
{ role: "zeta.review.composer" },
|
|
1778
|
+
[
|
|
1779
|
+
keyed(text([span("Edit which note?", "strong")]), "caption"),
|
|
1780
|
+
selectList("chooser", entries, { selected: `c${chooser.selected}`, tone: "accent" }),
|
|
1781
|
+
],
|
|
1782
|
+
"chooser",
|
|
1783
|
+
);
|
|
1596
1784
|
}
|
|
1597
|
-
if (this.#annotating)
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1785
|
+
if (!this.#annotating) return undefined;
|
|
1786
|
+
return node(
|
|
1787
|
+
"col",
|
|
1788
|
+
{ role: "zeta.review.composer" },
|
|
1789
|
+
[keyed(text(this.#noteCaption(), { truncate: "end" }), "caption"), this.#editor],
|
|
1790
|
+
"composer",
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/** Where the open note lands: `Note on line 42 in src/app.ts`, `Edit note on src/app.ts`, `Note on the whole text`. */
|
|
1795
|
+
#noteCaption(): TspSpan[] {
|
|
1796
|
+
const verb = span(this.#editingAnnotationIndex === undefined ? "Note on " : "Edit note on ", "muted");
|
|
1797
|
+
if (this.#textSource) {
|
|
1798
|
+
return this.#annotationScope === "text"
|
|
1799
|
+
? [verb, span("the whole text", "strong")]
|
|
1800
|
+
: [verb, span(`line ${this.#sourceIndex + 1}`, "strong")];
|
|
1801
|
+
}
|
|
1802
|
+
const file = this.#currentFile();
|
|
1803
|
+
const path = file ? sanitizeStatusText(displayFileLabel(file)) : "file";
|
|
1804
|
+
if (this.#annotationScope === "file") return [verb, span(path, "path")];
|
|
1805
|
+
const source = this.#currentSourceRows()[this.#sourceIndex];
|
|
1806
|
+
const where =
|
|
1807
|
+
source?.kind === "removed"
|
|
1808
|
+
? `removed line ${source.oldLine ?? ""}`
|
|
1809
|
+
: `line ${source?.newLine ?? source?.oldLine ?? ""}`;
|
|
1810
|
+
return [verb, span(where.trim(), "strong"), span(" in ", "muted"), span(path, "path")];
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* The footer: key hints for what has focus, then Cancel and the actions
|
|
1815
|
+
* (the primary last). Each button's key lives in its tooltip; the action
|
|
1816
|
+
* picked from the keyboard is marked `pick`, an unavailable one `drop`.
|
|
1817
|
+
*/
|
|
1818
|
+
#describeFooter(editing: boolean): NativeNode {
|
|
1819
|
+
const pick = (index: number): "pick" | "drop" | undefined =>
|
|
1820
|
+
editing || this.#actionDisabled(index)
|
|
1821
|
+
? "drop"
|
|
1822
|
+
: this.#focus === "actions" && this.#actionIndex === index
|
|
1823
|
+
? "pick"
|
|
1824
|
+
: undefined;
|
|
1825
|
+
const marked = (button: NativeNode, mark: "pick" | "drop" | undefined): NativeNode =>
|
|
1826
|
+
mark ? ({ ...button, p: { ...button.p, mark } } as NativeNode) : button;
|
|
1827
|
+
const hasNotes = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
|
|
1828
|
+
const pasteTitle = hasNotes ? PASTE_CODE_REVIEW_ACTION : "Add a note to paste it into the prompt";
|
|
1829
|
+
const buttons: NativeNode[] = [actionButton("Cancel", "close", { keys: "escape" })];
|
|
1830
|
+
if (this.#textSource) {
|
|
1831
|
+
buttons.push(
|
|
1832
|
+
marked(actionButton("Paste into prompt", "paste", { tone: "accent", title: pasteTitle }), pick(0)),
|
|
1833
|
+
);
|
|
1834
|
+
} else {
|
|
1835
|
+
buttons.push(
|
|
1836
|
+
marked(actionButton("Paste into prompt", "paste", { title: pasteTitle }), pick(1)),
|
|
1837
|
+
marked(
|
|
1838
|
+
actionButton("Review with LLM", "review", { tone: "accent", title: CONTINUE_CODE_REVIEW_ACTION }),
|
|
1839
|
+
pick(0),
|
|
1610
1840
|
),
|
|
1611
|
-
|
|
1841
|
+
);
|
|
1612
1842
|
}
|
|
1843
|
+
return node(
|
|
1844
|
+
"row",
|
|
1845
|
+
{ role: "zeta.actions", gap: "sm", align: "center" },
|
|
1846
|
+
[hintsRow(this.#footerHints()), ...buttons],
|
|
1847
|
+
"actions",
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/** At most a handful of hints, for the region (or draft) that has the keys. */
|
|
1852
|
+
#footerHints(): (NativeHint | undefined)[] {
|
|
1613
1853
|
const key = (keys: KeyName | readonly KeyName[], label: string): NativeHint => ({
|
|
1614
1854
|
keys: typeof keys === "string" ? [keys] : keys,
|
|
1615
1855
|
label,
|
|
1616
1856
|
});
|
|
1617
1857
|
const upDown = (label: string) => this.#hint(["tui.select.up", "tui.select.down"], label);
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1858
|
+
if (this.#annotationChooser) {
|
|
1859
|
+
return [upDown("choose"), this.#hint("tui.select.confirm", "edit"), this.#hint("tui.select.cancel", "back")];
|
|
1860
|
+
}
|
|
1861
|
+
if (this.#annotating) {
|
|
1862
|
+
return [
|
|
1863
|
+
this.#hint("tui.input.submit", "save"),
|
|
1864
|
+
this.#hint("tui.input.newLine", "new line"),
|
|
1865
|
+
this.#hint("tui.select.cancel", "discard"),
|
|
1866
|
+
this.#callbacks.onAnnotationExternalEditor ? key(appExternalEditorKey(), "editor") : undefined,
|
|
1867
|
+
];
|
|
1868
|
+
}
|
|
1869
|
+
const undo = this.#undoStack.length > 0 ? key("u", "undo") : undefined;
|
|
1870
|
+
if (this.#focus === "files") {
|
|
1871
|
+
return [
|
|
1872
|
+
upDown("file"),
|
|
1873
|
+
this.#hint("tui.select.confirm", "open"),
|
|
1874
|
+
key("a", "file note"),
|
|
1875
|
+
undo,
|
|
1876
|
+
key("tab", "pane"),
|
|
1877
|
+
];
|
|
1878
|
+
}
|
|
1879
|
+
if (this.#focus === "diff") {
|
|
1880
|
+
return [
|
|
1881
|
+
upDown("line"),
|
|
1882
|
+
key("a", "note"),
|
|
1883
|
+
key("shift+a", this.#textSource ? "note on all" : "file note"),
|
|
1884
|
+
this.#annotationCandidates().length > 0 ? key("e", "edit note") : undefined,
|
|
1885
|
+
undo,
|
|
1886
|
+
key("tab", this.#textSource ? "actions" : "pane"),
|
|
1887
|
+
];
|
|
1888
|
+
}
|
|
1889
|
+
return [upDown("choose"), this.#hint("tui.select.confirm", "run"), key("tab", "pane")];
|
|
1636
1890
|
}
|
|
1637
1891
|
|
|
1638
1892
|
/** Keycap hint for the primary keys this overlay's keybindings bind to `actions`. */
|