@fresh-editor/fresh-editor 0.2.25 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,161 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.3.0
4
+
5
+ This version brings major features and many quality-of-life improvements and bug fixes:
6
+
7
+ - A cool dashboard plugin
8
+ - Devcontainer support
9
+ - init.ts
10
+
11
+ And more (see below). A large version is more likely to contain regression bugs, so please bear with me if you encounter problems, and open github issues without hesitation.
12
+
13
+ ### Features
14
+
15
+ * **Dashboard plugin**: Built-in TUI dashboard that replaces the usual "[No Name]" with useful at-a-glance info.
16
+ - Default widgets: git status + repo URL, a "vs master" row (commits ahead/behind), and disk usage for common mounts.
17
+ - Opt-in widgets: weather, and open GitHub PRs for the current repo.
18
+ - Auto-open (on startup / last-buffer-close) is configurable — e.g. `editor.getPluginApi("dashboard")?.setAutoOpen(false)` in `init.ts`. When off, use the "Show Dashboard" command in the palette.
19
+ - Third-party plugins and `init.ts` can contribute their own rows via the `registerSection()` API. The `init.ts` starter template includes ready-to-paste snippets for enabling the opt-in widgets, toggling auto-open, and registering custom sections (see below).
20
+
21
+ * **Devcontainer support** (thanks @masak1yu!): Fresh integrates with the [devcontainer CLI](https://github.com/devcontainers/cli) (install it yourself).
22
+ - Detects `.devcontainer/devcontainer.json` and offers Attach / Rebuild / Detach.
23
+ - Embedded terminal, filesystem, and LSP servers all run inside the devcontainer.
24
+ - `Dev Container: Create Config` scaffolds a config for projects that don't have one.
25
+ - `Dev Container: Show Ports` merges configured `forwardPorts` with live `docker port` output.
26
+ - `Dev Container: Show Logs` captures the container's recent stdout/stderr.
27
+ - Build log streams into a workspace split; failed attaches offer Retry / Show Logs / Detach via a recovery popup.
28
+ - `initializeCommand` runs on attach.
29
+
30
+ * **`init.ts`**: Fresh now auto-loads `~/.config/fresh/init.ts`! Allows you to run plugin code on startup, which complements the purely declarative config system with imperative, environment-aware logic. Use command palette `init: Edit` to generate a template with some examples. Use `init: Reload` to run it after editing. Use `--no-init` / `--safe` to skip loading.
31
+ - Tip: *Enable LSP* when editing `init.ts` to get help and completions.
32
+ - Example (for the Dashboard plugin):
33
+ ```typescript
34
+ // in your init.ts file:
35
+ const dash = editor.getPluginApi("dashboard");
36
+ if (dash) {
37
+ dash.registerSection("env", async (ctx) => {
38
+ ctx.kv("USER", editor.getEnv("USER") || "?");
39
+ });
40
+ }
41
+ ```
42
+ Will add a line like this to your dashboard:
43
+ ```
44
+ │ ▎ ENV │
45
+ │ USER someone │
46
+ ```
47
+
48
+ * **`{remote}` status-bar indicator**: Clickable status-bar element that lights up when you're attached to an SSH remote or devcontainer, with a context-aware menu (detach, show logs, retry attach, …). Surfaces `Connecting` / `Connected` / `FailedAttach` states. Fresh's config v1→v2 migration injects `{remote}` into customized `status_bar.left`.
49
+
50
+ * **Hot-exit restore split from session restore**: `editor.restore_previous_session` config and the `--no-restore` / `--restore` CLI flags now control workspace/tab restoration separately from hot-exit content — unsaved scratch buffers come back even when you opt out of full session restore (#1404).
51
+
52
+ * **File explorer — cut/copy/paste + multi-selection + right-click context menu** (thanks @theogravity!):
53
+ - `Ctrl+C` / `Ctrl+X` / `Ctrl+V` with same-dir auto-rename and per-file conflict prompt on cross-dir paste.
54
+ - `Shift+Up/Down` for multi-select.
55
+ - Right-click context menu (#1684) with the usual file operations, honoring the active multi-selection.
56
+ - Cut-pending items are dimmed until pasted; cancel a pending cut with Escape or by pasting back into the same directory.
57
+ - Renaming a file or directory relocates any open buffers inside it; deleting a file closes its buffer.
58
+
59
+ * **File explorer — keyboard preview**: Moving the cursor with Up/Down in the explorer previews the highlighted file in a preview tab (#1570), so you can scan files without leaving the keyboard.
60
+
61
+ * **Quick Open / Go-to Line live preview**: Typing `:<N>` in the file finder (or in the standalone `:` mode) scrolls the cursor to the target line live as you type; Enter commits, Escape reverts, mouse movement or clicks also commit.
62
+
63
+ * **Terminal shell override (#1637)**: New `terminal.shell` config option lets you pick a different shell for the integrated terminal without reassigning `$SHELL` (which affects `format_on_save` and other features).
64
+
65
+ * **Suspend process (Unix)**: New `Suspend Process` action sends Fresh to the background like Ctrl+Z in a shell. Routed through the client in session mode so the server stays up.
66
+
67
+ * **Current-column highlight**: New `highlight_current_column` / `Toggle Current Column Highlight` — highlights the cursor's column for alignment work.
68
+
69
+ * **Post-EOF shading** (#779): Rows past end-of-file render with a distinct background so the boundary is obvious; works alongside `show_tilde`.
70
+
71
+ * **Regex replacement escapes**: `\n`, `\t`, `\r`, and `\\` in the replacement string are now interpreted when regex mode is on.
72
+
73
+ ### Improvements
74
+
75
+ * **SSH URLs on the CLI**: `fresh ssh://user@host:port/path` launches a session whose filesystem and process authority point at the remote host.
76
+
77
+ * **Redraw Screen command** (#1070): Added a `redraw_screen` action and palette entry that clears the terminal and fully repaints the UI, useful when an external program scribbles over the TUI.
78
+
79
+ * **Terminal window title** (#1618): Fresh sets the terminal window title from the active buffer's filename, matching other editors.
80
+
81
+ * **LSP status popup upgrades**: LSP popup now shows better options for enabling/disabling the nudge.
82
+
83
+ * **Find Next centers vertically** (#1251): When the next match is off-screen, scroll it to roughly the middle of the viewport so you keep context above and below it. Matches that are already visible are not re-scrolled.
84
+
85
+ * **Adaptive line-number gutter** (#1204): The gutter now grows with the buffer's line count rather than reserving 4 digits by default — a small file reclaims 2–3 columns of editor width.
86
+
87
+ * **File explorer width in percent or columns** (#1118, #1212, #1213): `file_explorer.width` now accepts `"30%"` (percent of terminal width) or `"24"` (absolute columns). Dragging the divider preserves whichever form you configured. Legacy integer/fraction values keep working.
88
+
89
+ * **Relative paths to theme files** (#1621): User themes in `config.json` can be spelled out as relative to your themes directory:
90
+ - "dark" or "builtin://dark" — any built-in by name
91
+ - "my-theme.json" or "subdir/dark.json" — nested relative path in your user themes dir - useful for sharing Fresh config.json in a dotfiles repo
92
+ - "file://${HOME}/themes/x.json" — absolute path; ${HOME}, ${XDG_CONFIG_HOME} are expanded
93
+ - "https://github.com/foo/themes#dark" — URL-packaged theme
94
+
95
+ * **Plugin API additions**:
96
+ - `editor.overrideThemeColors(...)` for in-memory theme mutation.
97
+ - `editor.parseJsonc(...)` for host-side JSONC parsing.
98
+ - Plugin-created terminals now have an ephemeral lifetime — they close cleanly when the action that spawned them finishes.
99
+ - Plugin authors can augment `FreshPluginRegistry` to make `editor.getPluginApi("name")` return a typed interface (no `as`-cast needed). Augmentations are emitted to `~/.config/fresh/types/plugins.d.ts` at load time.
100
+ - `spawnHostProcess` now returns a handle with `kill()` (and a matching `KillHostProcess` command).
101
+ - `BufferInfo.splits` surfaces which splits display a buffer, for "focus-if-visible" dedupe.
102
+ - `editor.setRemoteIndicatorState(...)` / `clearRemoteIndicatorState()` let remote plugins drive the status-bar `{remote}` element.
103
+ - Dashboard gains `dash.registerSection()` (with a returned remover) and `dash.clearAllSections()` for plugin extension.
104
+
105
+ * **JSONC language**: `.jsonc` files and well-known JSONC-with-`.json`-suffix files (`devcontainer.json`, `tsconfig.json`, `.eslintrc.json`, `.babelrc`, VS Code settings files) now get a dedicated `jsonc` language with comment-tolerant highlighting and LSP routing through `vscode-json-language-server` with the correct `languageId`.
106
+
107
+ * **macOS Alt+Right / Option+Right stops at word end** (#1288): Selection no longer extends past trailing whitespace, matching TextEdit / VS Code on Mac.
108
+
109
+ ### Bug Fixes
110
+
111
+ * **File Explorer `.gitignore` improvements** (#1388): Files are now visible only if they aren't hidden by ANY of the filters (hidden files, `.gitignore` files). Also, File Explorer will do a better job of auto-reloading when `.gitignore` changes.
112
+
113
+ * **Scrollbar theme colours** (#1554): The scrollbar now honours `theme.scrollbar_track_fg` / `scrollbar_thumb_fg`. A few themes were updated to define this missing value.
114
+
115
+ * **Fixed panic when clicking split + terminal** (#1620).
116
+
117
+ * **Fixed LSP server crash loop** (#1612): When LSP fails on startup, restart bypassed the normal restart count limiter, now fixed.
118
+
119
+ * **Fixed Markdown preview/compose wrapping when File Explorer is open**: When compose width was set (e.g. 80), opening the File Explorer sidebar pushed tables off the right edge. Separator rows no longer overflow when table cells are truncated.
120
+
121
+ * **More settings propagate live**: File-explorer width and flag changes made in the Settings UI apply immediately on save, without a restart.
122
+
123
+ * **Devcontainer: no re-prompt after restart**: Fresh no longer shows the "Attach?" prompt again after the post-attach self-restart.
124
+
125
+ * **Dashboard polish**: Doesn't steal focus from a CLI-supplied file, underline only on clickable spans (not trailing padding), clicks dispatch only from underlined column ranges, immediate repaint on split resize.
126
+
127
+ * **Quieter LSP**: Suppress `MethodNotFound` errors from LSP servers (#1649) — servers that don't implement an optional method no longer spam the log.
128
+
129
+ * **Plugin action popups survive buffer switches**: Popups stay visible when the active buffer changes, and concurrent popups queue LIFO so the newest shows first.
130
+
131
+ * **Encoding detection on CJK files** (#1635): Files whose only non-ASCII bytes sat past the 8 KB sample window were mis-detected; the sample boundary is now treated as truncation so the full file is considered before the encoding is guessed.
132
+
133
+ * **Review diff — no fold jitter**: Toggling a fold no longer re-centers the viewport.
134
+
135
+ * **LSP — cleaner disables**: No spurious warning when opening a file for a language whose LSP is explicitly disabled in config. The indicator shows buffer-skip state (e.g. file too large) instead of a generic warning.
136
+
137
+ * **Windows — preserve UNC paths**: `pathJoin` plugin API now preserves `\\?\` UNC prefixes on Windows.
138
+
139
+ * **Hardware cursor no longer bleeds through popups**: The terminal hardware cursor is hidden when an overlay popup covers it.
140
+
141
+ * **Focus — tab clicks reset explorer context** (#1540): Clicking a tab or buffer no longer leaves the FileExplorer key context active.
142
+
143
+ * **File explorer poll fixes**: Background refresh no longer collapses folders you've expanded, and resets the cursor to the root only when the selected path is genuinely gone.
144
+
145
+ * **Review PR Branch — default-branch detection**: The prompt now pre-fills the repo's actual default branch (via `origin/HEAD`) instead of hard-coding `main`.
146
+
147
+ * **Review: PageUp/PageDown**: Paging in review-branch mode now scrolls the commit list instead of moving the cursor by one row.
148
+
149
+ ### Under the Hood
150
+
151
+ * **Authority abstraction**: Filesystem, process-spawning, and LSP routing are now consolidated behind a single `Authority` slot, with plugin ops (`editor.setAuthority` / `clearAuthority` / `spawnHostProcess`) for plugins that want to target the host even while attached elsewhere. This is what makes the devcontainer and `ssh://` flows work uniformly.
152
+
3
153
  ## 0.2.25
4
154
 
5
155
  ### Improvements
6
156
 
157
+ * **Redraw Screen command** (#1070): Added a "Redraw Screen" entry to the command palette (action `redraw_screen`) that clears the terminal and fully repaints the UI. Useful when an external program (e.g. a macOS pasteboard diagnostic leaked by the host terminal on Ctrl+C) scribbles over the TUI and leaves ghost text behind.
158
+
7
159
  * **PageUp/PageDown in wrapped buffers**: Page motion is now view-row-aware, so paging through heavily wrapped text no longer stalls mid-buffer and the cursor stays visible after every press. Each page also keeps 3 rows of overlap with the previous page (matching vim / less) so you don't lose context across the jump.
8
160
 
9
161
  * **Smarter char-wrapping of long tokens**: When a token has to be split mid-word because it doesn't fit on a fresh line, the break now prefers a UAX #29 word boundary within a lookback window instead of an arbitrary grapheme position — e.g. `dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener` now wraps after `BUTTON_NEUTRAL` rather than mid-identifier.
package/README.md CHANGED
@@ -187,6 +187,12 @@ See [flatpak/README.md](flatpak/README.md) for building from source.
187
187
 
188
188
  Download the latest release for your platform from the [releases page](https://github.com/sinelaw/fresh/releases).
189
189
 
190
+ ### Using mise
191
+
192
+ ```bash
193
+ mise use github:sinelaw/fresh
194
+ ```
195
+
190
196
  ### npm
191
197
 
192
198
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.2.25",
3
+ "version": "0.3.0",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@
14
14
  "cmd.refresh_review_branch_desc": "Re-fetch the commit list for the current base ref",
15
15
  "cmd.review_range": "Review Range (Commit or Branch)",
16
16
  "cmd.review_range_desc": "Review a commit or branch as a flattened diff (e.g. main..HEAD, HEAD~3..HEAD, or a single commit SHA)",
17
- "prompt.branch_base": "Base ref to compare against (default: main):",
17
+ "prompt.branch_base": "Base ref to compare against (default: %{default}):",
18
18
  "prompt.review_range": "Review (range A..B or commit SHA): ",
19
19
  "status.review_range_empty": "No changes in %{range}",
20
20
  "status.review_branch_ready": "Reviewing %{count} commits in %{base}..HEAD",
@@ -92,7 +92,7 @@
92
92
  "cmd.review_range_desc": "Zrevidovat commit nebo větev jako sloučený rozdíl (např. main..HEAD, HEAD~3..HEAD nebo hash jednoho commitu)",
93
93
  "prompt.review_range": "Revize (rozsah A..B nebo SHA commitu): ",
94
94
  "status.review_range_empty": "Žádné změny v %{range}",
95
- "prompt.branch_base": "Základní ref pro porovnání (výchozí: main):",
95
+ "prompt.branch_base": "Základní ref pro porovnání (výchozí: %{default}):",
96
96
  "status.review_branch_ready": "Kontroluje se %{count} commitů v %{base}..HEAD",
97
97
  "status.review_branch_empty": "Žádné commity v %{base}..HEAD — není co revidovat.",
98
98
  "panel.review_branch_header": "Commity (%{base}..HEAD)",
@@ -168,7 +168,7 @@
168
168
  "cmd.review_range_desc": "Einen Commit oder Branch als zusammengeführten Diff prüfen (z. B. main..HEAD, HEAD~3..HEAD oder ein einzelner Commit-SHA)",
169
169
  "prompt.review_range": "Prüfen (Bereich A..B oder Commit-SHA): ",
170
170
  "status.review_range_empty": "Keine Änderungen in %{range}",
171
- "prompt.branch_base": "Basis-Ref zum Vergleichen (Standard: main):",
171
+ "prompt.branch_base": "Basis-Ref zum Vergleichen (Standard: %{default}):",
172
172
  "status.review_branch_ready": "%{count} Commits in %{base}..HEAD werden überprüft",
173
173
  "status.review_branch_empty": "Keine Commits in %{base}..HEAD — nichts zu überprüfen.",
174
174
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
@@ -244,7 +244,7 @@
244
244
  "cmd.review_range_desc": "Revisar un commit o una rama como un diff aplanado (p. ej. main..HEAD, HEAD~3..HEAD o un SHA de commit)",
245
245
  "prompt.review_range": "Revisar (rango A..B o SHA de commit): ",
246
246
  "status.review_range_empty": "Sin cambios en %{range}",
247
- "prompt.branch_base": "Ref base para comparar (por defecto: main):",
247
+ "prompt.branch_base": "Ref base para comparar (por defecto: %{default}):",
248
248
  "status.review_branch_ready": "Revisando %{count} commits en %{base}..HEAD",
249
249
  "status.review_branch_empty": "No hay commits en %{base}..HEAD — nada que revisar.",
250
250
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
@@ -320,7 +320,7 @@
320
320
  "cmd.review_range_desc": "Revoir un commit ou une branche comme un diff aplati (par ex. main..HEAD, HEAD~3..HEAD ou un SHA de commit)",
321
321
  "prompt.review_range": "Revoir (plage A..B ou SHA de commit) : ",
322
322
  "status.review_range_empty": "Aucun changement dans %{range}",
323
- "prompt.branch_base": "Ref de base pour la comparaison (par défaut : main) :",
323
+ "prompt.branch_base": "Ref de base pour la comparaison (par défaut : %{default}) :",
324
324
  "status.review_branch_ready": "Revue de %{count} commits dans %{base}..HEAD",
325
325
  "status.review_branch_empty": "Aucun commit dans %{base}..HEAD — rien à revoir.",
326
326
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
@@ -396,7 +396,7 @@
396
396
  "cmd.review_range_desc": "Revisiona un commit o un ramo come un diff appiattito (es. main..HEAD, HEAD~3..HEAD o uno SHA di commit)",
397
397
  "prompt.review_range": "Revisiona (intervallo A..B o SHA di commit): ",
398
398
  "status.review_range_empty": "Nessuna modifica in %{range}",
399
- "prompt.branch_base": "Ref base per il confronto (predefinito: main):",
399
+ "prompt.branch_base": "Ref base per il confronto (predefinito: %{default}):",
400
400
  "status.review_branch_ready": "Revisione di %{count} commit in %{base}..HEAD",
401
401
  "status.review_branch_empty": "Nessun commit in %{base}..HEAD — niente da revisionare.",
402
402
  "panel.review_branch_header": "Commit (%{base}..HEAD)",
@@ -472,7 +472,7 @@
472
472
  "cmd.review_range_desc": "コミットまたはブランチを平坦化した diff としてレビュー (例: main..HEAD、HEAD~3..HEAD、または単一コミットの SHA)",
473
473
  "prompt.review_range": "レビュー (範囲 A..B またはコミット SHA): ",
474
474
  "status.review_range_empty": "%{range} に変更はありません",
475
- "prompt.branch_base": "比較対象のベース ref (デフォルト: main):",
475
+ "prompt.branch_base": "比較対象のベース ref (デフォルト: %{default}):",
476
476
  "status.review_branch_ready": "%{base}..HEAD の %{count} 件のコミットをレビュー中",
477
477
  "status.review_branch_empty": "%{base}..HEAD にコミットはありません — レビューする対象がありません。",
478
478
  "panel.review_branch_header": "コミット (%{base}..HEAD)",
@@ -548,7 +548,7 @@
548
548
  "cmd.review_range_desc": "커밋이나 브랜치를 평탄화된 diff로 리뷰 (예: main..HEAD, HEAD~3..HEAD 또는 단일 커밋 SHA)",
549
549
  "prompt.review_range": "리뷰 (범위 A..B 또는 커밋 SHA): ",
550
550
  "status.review_range_empty": "%{range}에 변경 사항 없음",
551
- "prompt.branch_base": "비교할 기준 ref (기본값: main):",
551
+ "prompt.branch_base": "비교할 기준 ref (기본값: %{default}):",
552
552
  "status.review_branch_ready": "%{base}..HEAD의 %{count}개 커밋을 리뷰 중",
553
553
  "status.review_branch_empty": "%{base}..HEAD에 커밋이 없습니다 — 리뷰할 내용이 없습니다.",
554
554
  "panel.review_branch_header": "커밋 (%{base}..HEAD)",
@@ -624,7 +624,7 @@
624
624
  "cmd.review_range_desc": "Revisar um commit ou uma branch como um diff achatado (ex.: main..HEAD, HEAD~3..HEAD ou um SHA de commit)",
625
625
  "prompt.review_range": "Revisar (intervalo A..B ou SHA de commit): ",
626
626
  "status.review_range_empty": "Sem alterações em %{range}",
627
- "prompt.branch_base": "Ref base para comparação (padrão: main):",
627
+ "prompt.branch_base": "Ref base para comparação (padrão: %{default}):",
628
628
  "status.review_branch_ready": "Revisando %{count} commits em %{base}..HEAD",
629
629
  "status.review_branch_empty": "Sem commits em %{base}..HEAD — nada para revisar.",
630
630
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
@@ -700,7 +700,7 @@
700
700
  "cmd.review_range_desc": "Ревью коммита или ветки как плоского diff (например, main..HEAD, HEAD~3..HEAD или SHA одного коммита)",
701
701
  "prompt.review_range": "Ревью (диапазон A..B или SHA коммита): ",
702
702
  "status.review_range_empty": "Нет изменений в %{range}",
703
- "prompt.branch_base": "Базовый ref для сравнения (по умолчанию: main):",
703
+ "prompt.branch_base": "Базовый ref для сравнения (по умолчанию: %{default}):",
704
704
  "status.review_branch_ready": "Ревью %{count} коммитов в %{base}..HEAD",
705
705
  "status.review_branch_empty": "Нет коммитов в %{base}..HEAD — нечего просматривать.",
706
706
  "panel.review_branch_header": "Коммиты (%{base}..HEAD)",
@@ -776,7 +776,7 @@
776
776
  "cmd.review_range_desc": "ตรวจสอบคอมมิตหรือสาขาเป็น diff แบบรวม (เช่น main..HEAD, HEAD~3..HEAD หรือ SHA ของคอมมิตเดียว)",
777
777
  "prompt.review_range": "ตรวจสอบ (ช่วง A..B หรือ SHA ของคอมมิต): ",
778
778
  "status.review_range_empty": "ไม่มีการเปลี่ยนแปลงใน %{range}",
779
- "prompt.branch_base": "ref ฐานสำหรับเปรียบเทียบ (ค่าเริ่มต้น: main):",
779
+ "prompt.branch_base": "ref ฐานสำหรับเปรียบเทียบ (ค่าเริ่มต้น: %{default}):",
780
780
  "status.review_branch_ready": "กำลังตรวจสอบคอมมิต %{count} รายการใน %{base}..HEAD",
781
781
  "status.review_branch_empty": "ไม่มีคอมมิตใน %{base}..HEAD — ไม่มีอะไรต้องตรวจสอบ",
782
782
  "panel.review_branch_header": "คอมมิต (%{base}..HEAD)",
@@ -852,7 +852,7 @@
852
852
  "cmd.review_range_desc": "Рев'ю коміта або гілки як плаского diff (наприклад main..HEAD, HEAD~3..HEAD або SHA одного коміта)",
853
853
  "prompt.review_range": "Рев'ю (діапазон A..B або SHA коміта): ",
854
854
  "status.review_range_empty": "Немає змін у %{range}",
855
- "prompt.branch_base": "Базовий ref для порівняння (за замовчуванням: main):",
855
+ "prompt.branch_base": "Базовий ref для порівняння (за замовчуванням: %{default}):",
856
856
  "status.review_branch_ready": "Рев'ю %{count} комітів у %{base}..HEAD",
857
857
  "status.review_branch_empty": "Немає комітів у %{base}..HEAD — нічого переглядати.",
858
858
  "panel.review_branch_header": "Коміти (%{base}..HEAD)",
@@ -928,7 +928,7 @@
928
928
  "cmd.review_range_desc": "Xem xét một commit hoặc nhánh dưới dạng diff đã gộp (ví dụ main..HEAD, HEAD~3..HEAD hoặc SHA của một commit)",
929
929
  "prompt.review_range": "Xem xét (khoảng A..B hoặc SHA commit): ",
930
930
  "status.review_range_empty": "Không có thay đổi trong %{range}",
931
- "prompt.branch_base": "Ref cơ sở để so sánh (mặc định: main):",
931
+ "prompt.branch_base": "Ref cơ sở để so sánh (mặc định: %{default}):",
932
932
  "status.review_branch_ready": "Đang xem xét %{count} commit trong %{base}..HEAD",
933
933
  "status.review_branch_empty": "Không có commit nào trong %{base}..HEAD — không có gì để xem xét.",
934
934
  "panel.review_branch_header": "Commit (%{base}..HEAD)",
@@ -1004,7 +1004,7 @@
1004
1004
  "cmd.review_range_desc": "将提交或分支作为合并后的 diff 进行审查(例如 main..HEAD、HEAD~3..HEAD 或单个提交的 SHA)",
1005
1005
  "prompt.review_range": "审查(范围 A..B 或提交 SHA):",
1006
1006
  "status.review_range_empty": "%{range} 中没有更改",
1007
- "prompt.branch_base": "用于比较的基础 ref(默认:main):",
1007
+ "prompt.branch_base": "用于比较的基础 ref(默认:%{default}):",
1008
1008
  "status.review_branch_ready": "正在审查 %{base}..HEAD 中的 %{count} 个提交",
1009
1009
  "status.review_branch_empty": "%{base}..HEAD 中没有提交 — 无需审查。",
1010
1010
  "panel.review_branch_header": "提交 (%{base}..HEAD)",
@@ -1579,7 +1579,7 @@ function on_review_mouse_click(data: {
1579
1579
  else state.collapsedSections.add(cat);
1580
1580
  applyFolds();
1581
1581
  const sectionRow = state.sectionHeaderRows[cat];
1582
- if (sectionRow !== undefined) jumpDiffCursorToRow(sectionRow);
1582
+ if (sectionRow !== undefined) jumpDiffCursorToRow(sectionRow, { recenter: false });
1583
1583
  return;
1584
1584
  }
1585
1585
  }
@@ -1591,7 +1591,7 @@ function on_review_mouse_click(data: {
1591
1591
  else state.collapsedFiles.add(key);
1592
1592
  applyFolds();
1593
1593
  const headerRow = state.fileHeaderRows[key];
1594
- if (headerRow !== undefined) jumpDiffCursorToRow(headerRow);
1594
+ if (headerRow !== undefined) jumpDiffCursorToRow(headerRow, { recenter: false });
1595
1595
  return;
1596
1596
  }
1597
1597
  }
@@ -1602,7 +1602,7 @@ function on_review_mouse_click(data: {
1602
1602
  else state.collapsedHunks.add(hunkId);
1603
1603
  applyFolds();
1604
1604
  const hunkRow = state.hunkRowByHunkId[hunkId];
1605
- if (hunkRow !== undefined) jumpDiffCursorToRow(hunkRow);
1605
+ if (hunkRow !== undefined) jumpDiffCursorToRow(hunkRow, { recenter: false });
1606
1606
  return;
1607
1607
  }
1608
1608
  }
@@ -1812,7 +1812,7 @@ function review_toggle_file_collapse() {
1812
1812
  else state.collapsedSections.add(section);
1813
1813
  applyFolds();
1814
1814
  const sectionRow = state.sectionHeaderRows[section];
1815
- if (sectionRow !== undefined) jumpDiffCursorToRow(sectionRow);
1815
+ if (sectionRow !== undefined) jumpDiffCursorToRow(sectionRow, { recenter: false });
1816
1816
  return;
1817
1817
  }
1818
1818
 
@@ -1824,7 +1824,7 @@ function review_toggle_file_collapse() {
1824
1824
  else state.collapsedFiles.add(key);
1825
1825
  applyFolds();
1826
1826
  const headerRow = state.fileHeaderRows[key];
1827
- if (headerRow !== undefined) jumpDiffCursorToRow(headerRow);
1827
+ if (headerRow !== undefined) jumpDiffCursorToRow(headerRow, { recenter: false });
1828
1828
  return;
1829
1829
  }
1830
1830
 
@@ -1835,7 +1835,7 @@ function review_toggle_file_collapse() {
1835
1835
  else state.collapsedHunks.add(hunk.id);
1836
1836
  applyFolds();
1837
1837
  const hunkRow = state.hunkRowByHunkId[hunk.id];
1838
- if (hunkRow !== undefined) jumpDiffCursorToRow(hunkRow);
1838
+ if (hunkRow !== undefined) jumpDiffCursorToRow(hunkRow, { recenter: false });
1839
1839
  return;
1840
1840
  }
1841
1841
 
@@ -1848,7 +1848,7 @@ function review_toggle_file_collapse() {
1848
1848
  else state.collapsedFiles.add(key);
1849
1849
  applyFolds();
1850
1850
  const headerRow = state.fileHeaderRows[key];
1851
- if (headerRow !== undefined) jumpDiffCursorToRow(headerRow);
1851
+ if (headerRow !== undefined) jumpDiffCursorToRow(headerRow, { recenter: false });
1852
1852
  }
1853
1853
  registerHandler("review_toggle_file_collapse", review_toggle_file_collapse);
1854
1854
 
@@ -3147,23 +3147,31 @@ registerHandler("review_drill_down", review_drill_down);
3147
3147
  // --- Hunk navigation for side-by-side diff view ---
3148
3148
 
3149
3149
  /**
3150
- * Move the diff panel's native cursor to the given 1-indexed row, scrolling
3151
- * the viewport so the row is visible.
3150
+ * Move the diff panel's native cursor to the given 1-indexed row.
3151
+ *
3152
+ * `options.recenter` controls whether the viewport is re-centered on the
3153
+ * target row. The default is `true` for user-initiated navigation (next
3154
+ * hunk, jump-to-comment, jump-to-file) — there the caller wants the
3155
+ * target to land at a predictable position in the viewport. Callers
3156
+ * that merely re-anchor the cursor to a nearby header (e.g. after a
3157
+ * collapse/expand toggle) should pass `recenter: false` so the viewport
3158
+ * stays put; `setBufferCursor` still runs `ensure_cursor_visible`, so
3159
+ * the cursor is scrolled into view only when it would otherwise move
3160
+ * off-screen. Without this opt-out every fold toggle re-centers the
3161
+ * cursor's row at ~1/3 from the top of the viewport, which makes the
3162
+ * diff jump around whenever the user is reading anywhere else.
3152
3163
  */
3153
- function jumpDiffCursorToRow(row: number): void {
3164
+ function jumpDiffCursorToRow(row: number, options?: { recenter?: boolean }): void {
3154
3165
  const diffId = state.panelBuffers["diff"];
3155
3166
  if (diffId === undefined) return;
3156
3167
  const idx = row - 1;
3157
3168
  if (idx < 0 || idx >= state.diffLineByteOffsets.length) return;
3158
3169
 
3159
- // Set the cursor by absolute byte offset + scroll the viewport.
3160
- // Trust setBufferCursor — the previous N × executeAction("move_down")
3161
- // walk was O(target_row) round-trips into the editor and made
3162
- // collapsing big diffs visibly slow (thousands of round trips just
3163
- // to land the cursor on a header).
3164
3170
  const byteOffset = state.diffLineByteOffsets[idx];
3165
3171
  editor.setBufferCursor(diffId, byteOffset);
3166
- editor.scrollBufferToLine(diffId, idx);
3172
+ if (options?.recenter !== false) {
3173
+ editor.scrollBufferToLine(diffId, idx);
3174
+ }
3167
3175
  state.diffCursorRow = row;
3168
3176
  applyCursorLineOverlay('diff');
3169
3177
  refreshStickyHeader(idx);
@@ -4388,12 +4396,47 @@ const branchState: ReviewBranchState = {
4388
4396
  detailBufferId: null,
4389
4397
  commits: [],
4390
4398
  selectedIndex: 0,
4391
- baseRef: "main",
4399
+ // Empty means "not yet detected"; start_review_branch fills this in
4400
+ // from the repo's actual default branch (main, master, or whatever
4401
+ // origin/HEAD points at) before showing the prompt.
4402
+ baseRef: "",
4392
4403
  detailCache: null,
4393
4404
  pendingDetailId: 0,
4394
4405
  logRowByteOffsets: [],
4395
4406
  };
4396
4407
 
4408
+ /**
4409
+ * Best-effort detection of the repo's default branch. Checks, in order:
4410
+ * 1. `origin/HEAD` (the remote's notion of the default branch)
4411
+ * 2. local `main`
4412
+ * 3. local `master`
4413
+ * Falls back to `main` if none match, so the prompt still has a sensible
4414
+ * default in an empty / unusual repo.
4415
+ */
4416
+ async function detectDefaultBranch(): Promise<string> {
4417
+ try {
4418
+ const r = await editor.spawnProcess("git", [
4419
+ "symbolic-ref", "--short", "refs/remotes/origin/HEAD",
4420
+ ]);
4421
+ if (r.exit_code === 0) {
4422
+ const name = r.stdout.trim();
4423
+ // Output looks like "origin/main"; strip the remote prefix.
4424
+ const slash = name.indexOf("/");
4425
+ const branch = slash >= 0 ? name.slice(slash + 1) : name;
4426
+ if (branch) return branch;
4427
+ }
4428
+ } catch { /* fall through */ }
4429
+ for (const candidate of ["main", "master"]) {
4430
+ try {
4431
+ const r = await editor.spawnProcess("git", [
4432
+ "show-ref", "--verify", "--quiet", `refs/heads/${candidate}`,
4433
+ ]);
4434
+ if (r.exit_code === 0) return candidate;
4435
+ } catch { /* fall through */ }
4436
+ }
4437
+ return "main";
4438
+ }
4439
+
4397
4440
  // UTF-8 byte length helper, local copy so audit_mode doesn't pull in the one
4398
4441
  // from git_history (keeps the import list tiny).
4399
4442
  function branchUtf8Len(s: string): number {
@@ -4501,16 +4544,20 @@ async function start_review_branch(): Promise<void> {
4501
4544
  return;
4502
4545
  }
4503
4546
  // Prompt for the base ref so the user can review any PR, not just
4504
- // one branched off main.
4505
- const input = await editor.prompt(
4506
- editor.t("prompt.branch_base") || "Base ref (default: main):",
4507
- branchState.baseRef,
4508
- );
4547
+ // one branched off main. The default offered is either what the user
4548
+ // picked last time in this session, or the repo's actual default
4549
+ // branch (main/master/etc.) on first use.
4550
+ const suggested = branchState.baseRef || await detectDefaultBranch();
4551
+ const rawPromptText = editor.t("prompt.branch_base", { default: suggested });
4552
+ const promptText = (rawPromptText && !rawPromptText.startsWith("prompt."))
4553
+ ? rawPromptText
4554
+ : `Base ref to compare against (default: ${suggested}):`;
4555
+ const input = await editor.prompt(promptText + " ", suggested);
4509
4556
  if (input === null) {
4510
4557
  editor.setStatus(editor.t("status.cancelled") || "Cancelled");
4511
4558
  return;
4512
4559
  }
4513
- const base = input.trim() || "main";
4560
+ const base = input.trim() || suggested;
4514
4561
  branchState.baseRef = base;
4515
4562
 
4516
4563
  editor.setStatus(editor.t("status.loading") || "Loading commits…");
@@ -4637,17 +4684,11 @@ registerHandler("on_review_branch_cursor_moved", on_review_branch_cursor_moved);
4637
4684
  editor.defineMode(
4638
4685
  "review-branch",
4639
4686
  [
4640
- // Mode bindings replace globals, so we re-bind the editor's built-in
4641
- // motion actions here explicitly — without this, j/k and Up/Down
4642
- // do nothing in the commit list.
4643
- ["Up", "move_up"],
4644
- ["Down", "move_down"],
4687
+ // vi-style aliases for Up/Down. Everything else (arrows,
4688
+ // Page{Up,Down}, Home/End, selection motion, …) is inherited
4689
+ // from the Normal keymap via `inheritNormalBindings: true`.
4645
4690
  ["k", "move_up"],
4646
4691
  ["j", "move_down"],
4647
- ["PageUp", "page_up"],
4648
- ["PageDown", "page_down"],
4649
- ["Home", "move_line_start"],
4650
- ["End", "move_line_end"],
4651
4692
  // Enter: focus the right-hand detail panel.
4652
4693
  ["Return", "review_branch_enter"],
4653
4694
  ["Tab", "review_branch_enter"],
@@ -4655,7 +4696,9 @@ editor.defineMode(
4655
4696
  ["q", "review_branch_close_or_back"],
4656
4697
  ["Escape", "review_branch_close_or_back"],
4657
4698
  ],
4658
- true,
4699
+ true, // readOnly
4700
+ false, // allowTextInput — keeps plain letters from inserting into the RO buffer
4701
+ true, // inheritNormalBindings — PageUp/PageDown/arrows/Home/End come from Normal
4659
4702
  );
4660
4703
 
4661
4704
  // Register Modes and Commands