@fresh-editor/fresh-editor 0.3.6 → 0.3.7

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.
@@ -0,0 +1,72 @@
1
+ {
2
+ "cs": {
3
+ "status.detecting_branch": "Detekuji větev ...",
4
+ "status.not_in_git": "Není v git",
5
+ "status.git_branch": "Git: větev"
6
+ },
7
+ "de": {
8
+ "status.detecting_branch": "Branch erkennen ...",
9
+ "status.not_in_git": "Nicht in git",
10
+ "status.git_branch": "Git: Branch"
11
+ },
12
+ "en": {
13
+ "status.detecting_branch": "Detecting branch ...",
14
+ "status.not_in_git": "Not in git",
15
+ "status.git_branch": "Git: branch"
16
+ },
17
+ "es": {
18
+ "status.detecting_branch": "Detectando rama ...",
19
+ "status.not_in_git": "No está en git",
20
+ "status.git_branch": "Git: rama"
21
+ },
22
+ "fr": {
23
+ "status.detecting_branch": "Détection de la branche ...",
24
+ "status.not_in_git": "Pas dans git",
25
+ "status.git_branch": "Git : branche"
26
+ },
27
+ "it": {
28
+ "status.detecting_branch": "Rilevamento branch ...",
29
+ "status.not_in_git": "Non in git",
30
+ "status.git_branch": "Git: branch"
31
+ },
32
+ "ja": {
33
+ "status.detecting_branch": "ブランチを検出中...",
34
+ "status.not_in_git": "git外",
35
+ "status.git_branch": "Git: ブランチ"
36
+ },
37
+ "ko": {
38
+ "status.detecting_branch": "브랜치 감지 중...",
39
+ "status.not_in_git": "git 아님",
40
+ "status.git_branch": "Git: 브랜치"
41
+ },
42
+ "pt-BR": {
43
+ "status.detecting_branch": "Detectando ramo ...",
44
+ "status.not_in_git": "Não está em git",
45
+ "status.git_branch": "Git: ramo"
46
+ },
47
+ "ru": {
48
+ "status.detecting_branch": "Определение ветки ...",
49
+ "status.not_in_git": "Не в git",
50
+ "status.git_branch": "Git: ветка"
51
+ },
52
+ "th": {
53
+ "status.detecting_branch": "กำลังตรวจจับสาขา ...",
54
+ "status.not_in_git": "ไม่ได้อยู่ใน git",
55
+ "status.git_branch": "Git: สาขา"
56
+ },
57
+ "uk": {
58
+ "status.detecting_branch": "Визначення гілки ...",
59
+ "status.not_in_git": "Не в git",
60
+ "status.git_branch": "Git: гілка"
61
+ },
62
+ "vi": {
63
+ "status.detecting_branch": "Đang phát hiện nhánh ...",
64
+ "status.not_in_git": "Không trong git",
65
+ "status.git_branch": "Git: nhánh"
66
+ },
67
+ "zh-CN": {
68
+ "status.detecting_branch": "正在检测分支...",
69
+ "status.not_in_git": "不在 git 中",
70
+ "status.git_branch": "Git: 分支"
71
+ }
72
+ }
@@ -0,0 +1,133 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+
3
+ const editor = getEditor();
4
+
5
+ const GIT_BRANCH = "branch";
6
+
7
+ let lastDetectedBranch = editor.t("status.detecting_branch");
8
+ let inFlight: Promise<string> | null = null;
9
+
10
+ // HEAD-file watcher. Branch changes correspond exactly to mutations of
11
+ // the `HEAD` file inside the relevant git dir (resolved by
12
+ // `git rev-parse --git-path HEAD` so worktrees / submodules / `--git-dir`
13
+ // setups work). When HEAD changes we re-spawn `git rev-parse --abbrev-ref`
14
+ // and push the new value; otherwise we never spawn git on the hot path.
15
+ let headWatchHandle: number | null = null;
16
+ let watchedCwd: string | null = null;
17
+ let watchedHeadPath: string | null = null;
18
+ let ensureWatchInFlight: Promise<void> | null = null;
19
+
20
+ async function discoverHeadPath(cwd: string): Promise<string | null> {
21
+ const result = await editor.spawnProcess(
22
+ "git",
23
+ ["rev-parse", "--git-path", "HEAD"],
24
+ cwd,
25
+ );
26
+ if (result.exit_code !== 0) return null;
27
+ const headPath = result.stdout.trim();
28
+ if (!headPath) return null;
29
+ // `--git-path` returns a path relative to cwd unless the git dir is
30
+ // outside (e.g. worktree). Make absolute so notify gets a stable target.
31
+ return headPath.startsWith("/") ? headPath : `${cwd}/${headPath}`;
32
+ }
33
+
34
+ async function ensureHeadWatch(): Promise<void> {
35
+ const cwd = editor.getCwd();
36
+ if (watchedCwd === cwd && headWatchHandle !== null) return;
37
+ if (ensureWatchInFlight) return ensureWatchInFlight;
38
+
39
+ ensureWatchInFlight = (async () => {
40
+ try {
41
+ if (headWatchHandle !== null) {
42
+ editor.unwatchPath(headWatchHandle);
43
+ headWatchHandle = null;
44
+ watchedHeadPath = null;
45
+ }
46
+ watchedCwd = cwd;
47
+ const headPath = await discoverHeadPath(cwd);
48
+ if (!headPath) return;
49
+ try {
50
+ headWatchHandle = await editor.watchPath(headPath, false);
51
+ watchedHeadPath = headPath;
52
+ } catch (_e) {
53
+ // Watch registration failed (path missing, kernel limit). Fall
54
+ // back to event-driven refresh — getCurrentGitBranch is still
55
+ // gated by inFlight + the per-event invocation pattern below.
56
+ }
57
+ } finally {
58
+ ensureWatchInFlight = null;
59
+ }
60
+ })();
61
+ return ensureWatchInFlight;
62
+ }
63
+
64
+ async function getCurrentGitBranch(): Promise<string> {
65
+ if (inFlight) return inFlight;
66
+ inFlight = (async () => {
67
+ try {
68
+ const cwd = editor.getCwd();
69
+ const result = await editor.spawnProcess(
70
+ "git",
71
+ ["rev-parse", "--abbrev-ref", "HEAD"],
72
+ cwd,
73
+ );
74
+ if (result.exit_code === 0) {
75
+ const branch = result.stdout.trim();
76
+ lastDetectedBranch = branch || "HEAD";
77
+ } else {
78
+ lastDetectedBranch = editor.t("status.not_in_git");
79
+ }
80
+ return lastDetectedBranch;
81
+ } finally {
82
+ inFlight = null;
83
+ }
84
+ })();
85
+ return inFlight;
86
+ }
87
+
88
+ async function refreshForActiveBuffer(): Promise<void> {
89
+ // Lazy: pick up cwd changes (Orchestrator window switch, etc.) the next
90
+ // time anything triggers us.
91
+ ensureHeadWatch();
92
+ const bufferId = editor.getActiveBufferId();
93
+ if (bufferId === 0) return;
94
+ const branch = await getCurrentGitBranch();
95
+ editor.setStatusBarValue(bufferId, GIT_BRANCH, branch);
96
+ }
97
+
98
+ editor.registerStatusBarElement(GIT_BRANCH, editor.t("status.git_branch"));
99
+
100
+ // Refresh the branch label when:
101
+ // - The user switches to a different buffer (the per-buffer value may not
102
+ // be set yet for that buffer).
103
+ // - A file is freshly opened (same reason).
104
+ // - A file is saved (best-effort UX: the user may have committed via an
105
+ // external terminal between events; the watchPath below catches actual
106
+ // HEAD mutations).
107
+ // - The editor regains focus from another window — covers the case of
108
+ // running `git checkout` in an external terminal while fresh was unfocused.
109
+ //
110
+ // Notably *not* in this list (compared to the legacy version): render_start,
111
+ // cursor_moved, after_insert, after_delete, buffer_deactivated, buffer_closed.
112
+ // None of them can change the current branch, and render_start was being
113
+ // fired ~300/s — see #2009 for the feedback-loop investigation.
114
+ [
115
+ "buffer_activated",
116
+ "after_file_open",
117
+ "after_file_save",
118
+ "focus_gained",
119
+ ].forEach((event) => {
120
+ editor.on(event, async () => {
121
+ await refreshForActiveBuffer();
122
+ });
123
+ });
124
+
125
+ // path_changed → HEAD file mutated → branch may have changed.
126
+ editor.on("path_changed", async (args) => {
127
+ if (args.handle !== headWatchHandle) return;
128
+ await refreshForActiveBuffer();
129
+ });
130
+
131
+ // Kick off the first detection at load time so the status bar populates
132
+ // before any user event fires.
133
+ refreshForActiveBuffer();
@@ -252,6 +252,10 @@ type ViewportInfo = {
252
252
  */
253
253
  height: number;
254
254
  };
255
+ type ScreenSize = {
256
+ width: number;
257
+ height: number;
258
+ };
255
259
  type KeyEventPayload = {
256
260
  /**
257
261
  * Key name (e.g. `"a"`, `"escape"`, `"f1"`).
@@ -319,15 +323,18 @@ type ViewTokenWireKind = {
319
323
  } | "Newline" | "Space" | "Break" | {
320
324
  "BinaryByte": number;
321
325
  };
326
+ type TokenColor = [number, number, number] | string;
322
327
  type ViewTokenStyle = {
323
328
  /**
324
- * Foreground color as RGB tuple
329
+ * Foreground color. Either `[r, g, b]` or a named/theme string —
330
+ * see [`TokenColor`].
325
331
  */
326
- fg: [number, number, number] | null;
332
+ fg: TokenColor | null;
327
333
  /**
328
- * Background color as RGB tuple
334
+ * Background color. Either `[r, g, b]` or a named/theme string —
335
+ * see [`TokenColor`].
329
336
  */
330
- bg: [number, number, number] | null;
337
+ bg: TokenColor | null;
331
338
  /**
332
339
  * Whether to render in bold
333
340
  */
@@ -336,6 +343,10 @@ type ViewTokenStyle = {
336
343
  * Whether to render in italic
337
344
  */
338
345
  italic: boolean;
346
+ /**
347
+ * Whether to render with underline
348
+ */
349
+ underline: boolean;
339
350
  };
340
351
  type PromptSuggestion = {
341
352
  /**
@@ -445,6 +456,23 @@ type WindowInfo = {
445
456
  * Absolute project root.
446
457
  */
447
458
  root: string;
459
+ /**
460
+ * Project this session belongs to — the canonical repo
461
+ * root (or arbitrary directory) the user pointed the
462
+ * new-session form at. `null` for legacy sessions that
463
+ * predate the Project Path field. The Orchestrator Open
464
+ * dialog filters by this so the "this project's sessions"
465
+ * view is one keystroke away from the all-projects view.
466
+ */
467
+ project_path?: string | null;
468
+ /**
469
+ * `true` when the session shares its working tree with
470
+ * other sessions (worktree-creation was off at session
471
+ * time, or the session lives in a non-git directory).
472
+ * Persistence-only field; defaults to `false` and isn't
473
+ * emitted when false.
474
+ */
475
+ shared_worktree?: boolean;
448
476
  };
449
477
  type JsDiagnostic = {
450
478
  /**
@@ -629,6 +657,27 @@ type CreateTerminalOptions = {
629
657
  * target session's membership set rather than the active one's.
630
658
  */
631
659
  windowId?: WindowId;
660
+ /**
661
+ * Argv to spawn directly inside the PTY instead of the host's
662
+ * configured shell. `None` (default) keeps the historical
663
+ * behaviour: spawn the user's shell and let the caller type into
664
+ * it via `sendTerminalInput`. `Some([cmd, ...args])` runs that
665
+ * exact command as the PTY child — no shell middleman, so the
666
+ * process exits cleanly when the agent does and the
667
+ * terminal-buffer's `terminal_exit` plugin hook reflects the
668
+ * agent's real exit status. Used by Orchestrator so a session
669
+ * with agent `python3` is just python3 in the PTY rather than
670
+ * bash-running-python3-as-a-subshell-command.
671
+ */
672
+ command?: Array<string>;
673
+ /**
674
+ * Tab title for the terminal buffer. Defaults to `command[0]`
675
+ * (when `command` is set) or `"Terminal N"` (the historical
676
+ * auto-numbered title). If another terminal in the same window
677
+ * already uses the requested title, the host appends `" (k)"`
678
+ * to disambiguate. Empty string is treated the same as `None`.
679
+ */
680
+ title?: string;
632
681
  };
633
682
  type CursorInfo = {
634
683
  /**
@@ -816,6 +865,15 @@ type WidgetSpec = {
816
865
  focused: boolean;
817
866
  intent: ButtonKind;
818
867
  key?: string | null;
868
+ /**
869
+ * When true, the button renders in a muted style, is dropped
870
+ * from the Tab cycle, and clicks on it are ignored. Use for
871
+ * actions that aren't currently available against the
872
+ * surrounding state (e.g. "Archive" on the base session). The
873
+ * button still occupies its layout cell so the surrounding
874
+ * row doesn't reshuffle when the disabled flag flips.
875
+ */
876
+ disabled: boolean;
819
877
  } | {
820
878
  "kind": "spacer";
821
879
  cols: number;
@@ -923,6 +981,41 @@ type WidgetSpec = {
923
981
  * `LabeledSection` or a flexible row.
924
982
  */
925
983
  fullWidth: boolean;
984
+ /**
985
+ * Optional completion candidates. When non-empty AND
986
+ * `label` is non-empty (the chrome trigger), the
987
+ * renderer paints a popup directly under the input,
988
+ * inside a unified box: the input's normal `╰─...─╯`
989
+ * bottom border becomes a dimmed `┄` separator, the
990
+ * labeled section's side borders extend down through
991
+ * the candidate rows, and a single `╰─...─╯` bottom
992
+ * closes the whole block. Candidates render left-
993
+ * aligned with the input's text (the position right
994
+ * after `[`), with the host-managed selected index
995
+ * highlighted.
996
+ *
997
+ * Smart-key dispatch on a focused Text-with-completions:
998
+ * Up/Down moves selection (host-internal, no event),
999
+ * Tab fires `completion_accept` with the selected
1000
+ * candidate, Enter / Escape fire `completion_dismiss`
1001
+ * (the dispatcher's normal "Enter focus-advance / Esc
1002
+ * close panel" only runs once the popup is closed).
1003
+ *
1004
+ * Plugins push candidates in response to the text
1005
+ * widget's `change` event via
1006
+ * `WidgetMutation::SetCompletions`. An empty `items`
1007
+ * closes the popup.
1008
+ */
1009
+ completions?: Array<string>;
1010
+ /**
1011
+ * How many candidate rows the popup paints at once
1012
+ * when it opens. Excess candidates stay reachable
1013
+ * via Up/Down (host auto-scrolls to keep selection
1014
+ * in view) or the mouse wheel; a thumb glyph paints
1015
+ * in the right edge of the popup whenever there's
1016
+ * more to scroll. `0` (default) falls back to `5`.
1017
+ */
1018
+ completionsVisibleRows: number;
926
1019
  key?: string | null;
927
1020
  } | {
928
1021
  "kind": "labeledSection";
@@ -967,6 +1060,10 @@ type WidgetSpec = {
967
1060
  "kind": "raw";
968
1061
  entries: Array<TextPropertyEntry>;
969
1062
  key?: string | null;
1063
+ } | {
1064
+ "kind": "overlay";
1065
+ child: WidgetSpec;
1066
+ key?: string | null;
970
1067
  };
971
1068
  type WidgetAction = {
972
1069
  "kind": "focusAdvance";
@@ -991,6 +1088,10 @@ type WidgetMutation = {
991
1088
  widgetKey: string;
992
1089
  value: string;
993
1090
  cursorByte?: number | null;
1091
+ } | {
1092
+ "kind": "setCompletions";
1093
+ widgetKey: string;
1094
+ items: Array<string>;
994
1095
  } | {
995
1096
  "kind": "setChecked";
996
1097
  widgetKey: string;
@@ -1013,6 +1114,18 @@ type WidgetMutation = {
1013
1114
  widgetKey: string;
1014
1115
  checked: boolean;
1015
1116
  keys: Array<string>;
1117
+ } | {
1118
+ "kind": "appendTreeNodes";
1119
+ widgetKey: string;
1120
+ newNodes: Array<TreeNode>;
1121
+ newItemKeys: Array<string>;
1122
+ } | {
1123
+ "kind": "setRawEntries";
1124
+ widgetKey: string;
1125
+ entries: Array<TextPropertyEntry>;
1126
+ } | {
1127
+ "kind": "setFocusKey";
1128
+ widgetKey: string;
1016
1129
  };
1017
1130
  type SearchTakeResult = {
1018
1131
  /**
@@ -1442,7 +1555,9 @@ interface EditorAPI {
1442
1555
  * contexts only (e.g. `"tour-active"`, `"review-mode"`), not built-in
1443
1556
  * editor modes.
1444
1557
  */
1445
- registerCommand(name: string, description: string, handlerName: string, context?: string | null): boolean;
1558
+ registerCommand(name: string, description: string, handlerName: string, context?: string | null, options?: {
1559
+ terminalBypass?: boolean;
1560
+ } | null): boolean;
1446
1561
  /**
1447
1562
  * Unregister a command by name
1448
1563
  */
@@ -1456,6 +1571,17 @@ interface EditorAPI {
1456
1571
  */
1457
1572
  executeAction(actionName: string): boolean;
1458
1573
  /**
1574
+ * Register a custom statusbar token.
1575
+ * Token will be named "plugin_name:token_name" where plugin_name is the current plugin.
1576
+ * Returns true if registration succeeded, false if invalid or already registered.
1577
+ */
1578
+ registerStatusBarElement(tokenName: string, title: string): boolean;
1579
+ /**
1580
+ * Set the value of a status-bar token for a specific buffer.
1581
+ * The full token key sent to the editor is "plugin_name:token_name".
1582
+ */
1583
+ setStatusBarValue(bufferId: number, tokenName: string, value: string): boolean;
1584
+ /**
1459
1585
  * Translate a string - reads plugin name from __pluginName__ global
1460
1586
  * Args is optional - can be omitted, undefined, null, or an object
1461
1587
  */
@@ -1502,6 +1628,13 @@ interface EditorAPI {
1502
1628
  */
1503
1629
  getViewport(): ViewportInfo | null;
1504
1630
  /**
1631
+ * Total terminal dimensions in cells. Unlike `getViewport()`
1632
+ * (which reports the active split, shrunk by any vertical
1633
+ * split layout), this reflects the full terminal — what a
1634
+ * floating overlay sized by `heightPct` actually gets.
1635
+ */
1636
+ getScreenSize(): ScreenSize;
1637
+ /**
1505
1638
  * List every split with its active buffer and viewport.
1506
1639
  *
1507
1640
  * Plugins that need to operate on every visible buffer
@@ -1585,6 +1718,27 @@ interface EditorAPI {
1585
1718
  */
1586
1719
  openFileInSplit(splitId: number, path: string, line: number, column: number): boolean;
1587
1720
  /**
1721
+ * Open `path` as a regular buffer in forced large-file (file-backed)
1722
+ * mode. The file is created (empty) if missing — designed for
1723
+ * buffers that will be filled by a concurrent `spawnProcess` with
1724
+ * `stdoutTo`. Resolves with the new buffer's id, or `null` on
1725
+ * failure.
1726
+ *
1727
+ * Pair with `refreshBufferFromDisk` to grow the buffer as the
1728
+ * streaming write advances.
1729
+ */
1730
+ openFileStreaming(path: string): Promise<number | null>;
1731
+ /**
1732
+ * Re-stat the file backing `bufferId` and extend the buffer if the
1733
+ * file has grown. Resolves with the new total byte length, or
1734
+ * `null` if the buffer has no file path or doesn't exist.
1735
+ *
1736
+ * Used to drive a streaming display: while a `spawnProcess` writes
1737
+ * to a temp file, the plugin polls this on a timer so the buffer
1738
+ * length tracks the file length.
1739
+ */
1740
+ refreshBufferFromDisk(bufferId: number): Promise<number | null>;
1741
+ /**
1588
1742
  * Show a buffer in the current split
1589
1743
  */
1590
1744
  showBuffer(bufferId: number): boolean;
@@ -1593,6 +1747,30 @@ interface EditorAPI {
1593
1747
  */
1594
1748
  closeBuffer(bufferId: number): boolean;
1595
1749
  /**
1750
+ * Close other buffers in split
1751
+ */
1752
+ closeOtherBuffersInSplit(bufferId: number, splitId: number): boolean;
1753
+ /**
1754
+ * Close all buffers in split
1755
+ */
1756
+ closeAllBuffersInSplit(splitId: number): boolean;
1757
+ /**
1758
+ * Close buffers to right in split
1759
+ */
1760
+ closeBuffersToRightInSplit(bufferId: number, splitId: number): boolean;
1761
+ /**
1762
+ * Close buffers to left in split
1763
+ */
1764
+ closeBuffersToLeftInSplit(bufferId: number, splitId: number): boolean;
1765
+ /**
1766
+ * Move the active tab to the left in the active split
1767
+ */
1768
+ moveTabToLeft(): boolean;
1769
+ /**
1770
+ * Move the active tab to the right in the active split
1771
+ */
1772
+ moveTabToRight(): boolean;
1773
+ /**
1596
1774
  * Start a frame-buffer animation over an arbitrary screen region.
1597
1775
  * Returns an animation id usable with `cancelAnimation`.
1598
1776
  */
@@ -1750,6 +1928,64 @@ interface EditorAPI {
1750
1928
  */
1751
1929
  getUserConfig(): unknown;
1752
1930
  /**
1931
+ * Declare a boolean config field for the calling plugin.
1932
+ *
1933
+ * Validates `options` synchronously: the JS call throws if any
1934
+ * unknown key is present or if `default` isn't a boolean. The
1935
+ * Settings UI grows a "Plugin Settings → <plugin>" sub-category
1936
+ * containing a toggle for this field. Returns the current value
1937
+ * (user-set if present, otherwise the declared `default`).
1938
+ */
1939
+ defineConfigBoolean(name: string, options: {
1940
+ default: boolean;
1941
+ description?: string;
1942
+ }): boolean;
1943
+ /**
1944
+ * Declare an integer config field for the calling plugin. Throws on
1945
+ * invalid options or if the default falls outside `minimum/maximum`.
1946
+ */
1947
+ defineConfigInteger(name: string, options: {
1948
+ default: number;
1949
+ description?: string;
1950
+ minimum?: number;
1951
+ maximum?: number;
1952
+ }): number;
1953
+ /**
1954
+ * Declare a floating-point number config field. Throws on bad
1955
+ * options or default outside `minimum/maximum`.
1956
+ */
1957
+ defineConfigNumber(name: string, options: {
1958
+ default: number;
1959
+ description?: string;
1960
+ minimum?: number;
1961
+ maximum?: number;
1962
+ }): number;
1963
+ /**
1964
+ * Declare a free-form string config field.
1965
+ */
1966
+ defineConfigString(name: string, options: {
1967
+ default: string;
1968
+ description?: string;
1969
+ }): string;
1970
+ /**
1971
+ * Declare an array-of-strings config field (e.g. a list of
1972
+ * patterns). The Settings UI renders this as a list editor.
1973
+ */
1974
+ defineConfigStringArray(name: string, options: {
1975
+ default: string[];
1976
+ description?: string;
1977
+ }): string[];
1978
+ /**
1979
+ * Get the calling plugin's settings as a JS object.
1980
+ *
1981
+ * Returns the merged value at `config.plugins.<plugin_name>.settings`.
1982
+ * The shape comes from whatever the plugin declared via
1983
+ * `editor.definePluginConfig(...)` (defaults pre-populated by the
1984
+ * host, user overrides on top from the Settings UI). Returns `null`
1985
+ * if the plugin hasn't declared a schema and has no user-set value.
1986
+ */
1987
+ getPluginConfig(): unknown;
1988
+ /**
1753
1989
  * Reload configuration from file
1754
1990
  */
1755
1991
  reloadConfig(): void;
@@ -1972,6 +2208,21 @@ interface EditorAPI {
1972
2208
  */
1973
2209
  clearFolds(bufferId: number): boolean;
1974
2210
  /**
2211
+ * Publish a set of toggleable fold ranges on the buffer. Same
2212
+ * shape an LSP `foldingRange` response would take. Unlike
2213
+ * `addFold`, this does *not* pre-collapse anything — the
2214
+ * standard fold-toggle keybinding finds the range under the
2215
+ * cursor and collapses or expands it on demand. Replacing call
2216
+ * replaces the prior set.
2217
+ *
2218
+ * `ranges` is a JS array of objects shaped
2219
+ * `{ startLine, endLine, kind? }` (lines are 0-indexed).
2220
+ * `kind` is one of `"comment"`, `"imports"`, `"region"` per
2221
+ * the LSP spec; omitted/unknown values are accepted as plain
2222
+ * folds.
2223
+ */
2224
+ setFoldingRanges(bufferId: number, rangesArr: Record<string, unknown>[]): boolean;
2225
+ /**
1975
2226
  * Add a soft break point for marker-based line wrapping
1976
2227
  */
1977
2228
  addSoftBreak(bufferId: number, namespace: string, position: number, indent: number): boolean;
@@ -2046,6 +2297,13 @@ interface EditorAPI {
2046
2297
  * theme-key string (e.g. `"editor.line_number_fg"`). Theme keys
2047
2298
  * are resolved at render time so the line follows theme changes.
2048
2299
  * Both default to `null` (no foreground / transparent background).
2300
+ * * `gutterGlyph` — optional single character (any short string)
2301
+ * rendered in the line-number column on this virtual line's
2302
+ * first visual row. Use to mark e.g. a deletion line with "-"
2303
+ * so the indicator sits next to the deleted content instead
2304
+ * of on the following source line.
2305
+ * * `gutterColor` — color for `gutterGlyph`, same shape as
2306
+ * `fg`/`bg`. Falls back to the theme's line-number fg.
2049
2307
  */
2050
2308
  addVirtualLine(bufferId: number, position: number, text: string, options: Record<string, unknown>, above: boolean, namespace: string, priority: number): boolean;
2051
2309
  /**
@@ -2409,6 +2667,15 @@ interface EditorAPI {
2409
2667
  */
2410
2668
  focusBufferGroupPanel(groupId: number, panelName: string): boolean;
2411
2669
  /**
2670
+ * Re-point a buffer-group's panel at a different buffer id.
2671
+ *
2672
+ * Streaming plugins (e.g. git-log) allocate one file-backed
2673
+ * buffer per item and call this on navigation to swap which
2674
+ * buffer the panel displays — instead of mutating a single
2675
+ * shared buffer's contents. Resolves with `true` on success.
2676
+ */
2677
+ setBufferGroupPanelBuffer(groupId: number, panelName: string, bufferId: number): Promise<boolean>;
2678
+ /**
2412
2679
  * Set virtual buffer content (takes array of entry objects)
2413
2680
  *
2414
2681
  * Note: entries should be TextPropertyEntry[] - uses manual parsing for HashMap support
@@ -2474,8 +2741,13 @@ interface EditorAPI {
2474
2741
  unmountFloatingWidget(panelId: number): boolean;
2475
2742
  /**
2476
2743
  * Spawn a process (async, returns request_id)
2744
+ *
2745
+ * Optional 4th argument `stdoutTo: string` pipes the child's stdout
2746
+ * directly into the named file instead of buffering it. The
2747
+ * resolved `SpawnResult.stdout` is empty in that case; the bytes
2748
+ * land on disk for `openFile` to pick up as a file-backed buffer.
2477
2749
  */
2478
- spawnProcess(command: string, args: string[], cwd?: string): ProcessHandle<SpawnResult>;
2750
+ spawnProcess(command: string, args: string[], cwd?: string, stdoutTo?: string): ProcessHandle<SpawnResult>;
2479
2751
  /**
2480
2752
  * Spawn a process on the host regardless of the active authority.
2481
2753
  *
@@ -2635,6 +2907,33 @@ interface EditorAPI {
2635
2907
  getPluginApi<K extends keyof FreshPluginRegistry>(name: K): FreshPluginRegistry[K] | null;
2636
2908
  }
2637
2909
  /**
2910
+ * Typed overload of `editor.defineConfigEnum`. The macro-generated
2911
+ * signature can't express `<E extends string>` propagating from the
2912
+ * `values` array into the return type, so it's declared here. Use
2913
+ * `as const` on the `values` array to get a literal-union return:
2914
+ *
2915
+ * ```ts
2916
+ * const mode = editor.defineConfigEnum("mode", {
2917
+ * values: ["normal", "insert"] as const,
2918
+ * default: "normal",
2919
+ * });
2920
+ * mode; // typed as "normal" | "insert"
2921
+ * ```
2922
+ *
2923
+ * Typed overload of `editor.getPluginConfig`. Plugins that declared
2924
+ * their fields via `defineConfigX` can pass the shape type explicitly:
2925
+ * `editor.getPluginConfig<{ autoEnable: boolean; ... }>()`. Without
2926
+ * the generic, falls back to `unknown`.
2927
+ */
2928
+ interface EditorAPI {
2929
+ defineConfigEnum<E extends string>(name: string, options: {
2930
+ values: readonly E[];
2931
+ default: NoInfer<E>;
2932
+ description?: string;
2933
+ }): E;
2934
+ getPluginConfig<T = unknown>(): T;
2935
+ }
2936
+ /**
2638
2937
  * Maps every hook event name to its payload type.
2639
2938
  *
2640
2939
  * Payloads match the flat JSON produced by `hook_args_to_json` on the Rust