@fresh-editor/fresh-editor 0.3.8 → 0.3.10

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.
@@ -82,28 +82,26 @@ function isTrusted(): boolean {
82
82
  /** Activate (or, when already active, reload) the detected environment. */
83
83
  function activate(): void {
84
84
  if (!isTrusted()) {
85
- editor.setStatus(
86
- "Workspace not trusted — run “Workspace Trust: Trust This Folder” to activate the environment",
87
- );
85
+ editor.setStatus(editor.t("status.not_trusted"));
88
86
  return;
89
87
  }
90
88
  const det = detect();
91
89
  if (!det) {
92
- editor.setStatus("No environment manager detected in this project");
90
+ editor.setStatus(editor.t("status.no_env_detected"));
93
91
  return;
94
92
  }
95
93
  // Core captures `snippet` on the active backend and applies it to every
96
94
  // spawn; it restarts so language servers re-spawn under the fresh env.
97
95
  editor.setEnv(det.snippet, editor.getCwd());
98
96
  editor.setStatus(
99
- `${editor.envActive() ? "Reloading" : "Activating"} ${det.name} environment…`,
97
+ editor.t(editor.envActive() ? "status.reloading" : "status.activating", { name: det.name }),
100
98
  );
101
99
  }
102
100
  registerHandler("env_activate_handler", activate);
103
101
 
104
102
  function useSystem(): void {
105
103
  editor.clearEnv();
106
- editor.setStatus("Environment deactivated — using the system environment");
104
+ editor.setStatus(editor.t("status.deactivated"));
107
105
  }
108
106
  registerHandler("env_use_system_handler", useSystem);
109
107
 
@@ -111,37 +109,23 @@ function showStatus(): void {
111
109
  const det = detect();
112
110
  const trust = editor.workspaceTrustLevel() || "unavailable";
113
111
  if (editor.envActive()) {
114
- editor.setStatus(`Environment active${det ? ` (${det.name})` : ""}`);
115
- } else if (det) {
116
112
  editor.setStatus(
117
- `Detected ${det.name} (trust: ${trust}). Run “Env: Activate” to use it.`,
113
+ det
114
+ ? editor.t("status.env_active_named", { name: det.name })
115
+ : editor.t("status.env_active"),
118
116
  );
117
+ } else if (det) {
118
+ editor.setStatus(editor.t("status.env_detected", { name: det.name, trust }));
119
119
  } else {
120
- editor.setStatus(`No environment detected (trust: ${trust})`);
120
+ editor.setStatus(editor.t("status.no_env", { trust }));
121
121
  }
122
122
  }
123
123
  registerHandler("env_status_handler", showStatus);
124
124
 
125
- editor.registerCommand(
126
- "env_activate",
127
- "Env: Activate Detected Environment (venv / direnv / mise)",
128
- "env_activate_handler",
129
- );
130
- editor.registerCommand(
131
- "env_reload",
132
- "Env: Reload Environment (re-capture after .envrc/mise.toml change)",
133
- "env_activate_handler",
134
- );
135
- editor.registerCommand(
136
- "env_use_system",
137
- "Env: Use System (Deactivate Environment)",
138
- "env_use_system_handler",
139
- );
140
- editor.registerCommand(
141
- "env_status",
142
- "Env: Show Environment Status",
143
- "env_status_handler",
144
- );
125
+ editor.registerCommand("%cmd.activate", "%cmd.activate_desc", "env_activate_handler");
126
+ editor.registerCommand("%cmd.reload", "%cmd.reload_desc", "env_activate_handler");
127
+ editor.registerCommand("%cmd.use_system", "%cmd.use_system_desc", "env_use_system_handler");
128
+ editor.registerCommand("%cmd.status", "%cmd.status_desc", "env_status_handler");
145
129
 
146
130
  // === Status pill (opt-in to a user's status-bar layout) ===
147
131
 
@@ -151,14 +135,20 @@ function refreshStatus(): void {
151
135
  const det = detect();
152
136
  let value: string;
153
137
  if (editor.envActive()) {
154
- value = det ? `${det.name} ✓` : "active";
138
+ value = det
139
+ ? editor.t("statusbar.active", { name: det.name })
140
+ : editor.t("statusbar.active_unknown");
141
+ } else if (det) {
142
+ value = isTrusted()
143
+ ? det.name
144
+ : editor.t("statusbar.locked", { name: det.name });
155
145
  } else {
156
- value = det ? `${det.name}${isTrusted() ? "" : " (locked)"}` : "system";
146
+ value = editor.t("statusbar.system");
157
147
  }
158
148
  editor.setStatusBarValue(bufferId, STATUS_TOKEN, value);
159
149
  }
160
150
 
161
- editor.registerStatusBarElement(STATUS_TOKEN, "Environment");
151
+ editor.registerStatusBarElement(STATUS_TOKEN, editor.t("statusbar.label"));
162
152
 
163
153
  registerHandler("env_refresh_status", refreshStatus);
164
154
  for (const event of ["buffer_activated", "after_file_open", "focus_gained"]) {
@@ -46,8 +46,9 @@ function getCurrentLocation(): {
46
46
 
47
47
  // Helper: Get actual line number using the API
48
48
  function getCurrentLineCol(): { line: number; column: number } {
49
- // Use the actual getCursorLine API for accurate line number
50
- const lineNumber = editor.getCursorLine();
49
+ // Read the primary cursor's line from the snapshot. `line` is null when the
50
+ // buffer has no line index yet (huge files); fall back to 0 there.
51
+ const lineNumber = editor.getPrimaryCursor()?.line ?? 0;
51
52
 
52
53
  // Get cursor position within the line by reading buffer content
53
54
  const bufferId = editor.getActiveBufferId();
@@ -116,9 +116,37 @@ export interface FinderConfig<T> {
116
116
  /** Panel-specific: navigate source split when cursor moves (preview without focus change) */
117
117
  navigateOnCursorMove?: boolean;
118
118
 
119
+ /**
120
+ * Panel-specific: whether selecting an entry (Enter) closes the
121
+ * panel. Defaults to `true` — the panel dismisses as it jumps, which
122
+ * suits one-shot lists like Find References. Set `false` to keep the
123
+ * panel docked so the user can step through entries (Vim quickfix /
124
+ * VS Code results list). Only consulted by the default selection
125
+ * handler — a custom `onSelect` owns close behaviour itself.
126
+ *
127
+ * Note: leaving the panel open while `navigateOnCursorMove` is also
128
+ * on re-introduces the focus race the close was guarding against
129
+ * (a `focusSplit` queued after the jump can re-grab the panel), so
130
+ * the two shouldn't be combined without care.
131
+ */
132
+ closeOnSelect?: boolean;
133
+
119
134
  /** Called when the panel or prompt is closed (e.g. via Escape) */
120
135
  onClose?: () => void;
121
136
 
137
+ /**
138
+ * Panel-specific: extra key bindings active while the panel buffer
139
+ * is focused, as `[key, command]` pairs (e.g. `["q", "my_close"]`).
140
+ * `command` is dispatched as a plugin action, so it should name a
141
+ * handler registered via `registerHandler`. These augment the
142
+ * built-in `Return` (select) and `Escape` (close) bindings.
143
+ *
144
+ * Without this, keys a plugin advertises in its panel UI (e.g.
145
+ * "q: close") fall through to the read-only text layer and trip
146
+ * "Editing disabled in this buffer" (issue #2125).
147
+ */
148
+ panelKeys?: Array<[string, string]>;
149
+
122
150
  /**
123
151
  * When true, panels created by this Finder are routed into the
124
152
  * shared Utility Dock (issue #1796 / Section 2 of
@@ -447,6 +475,33 @@ export class Finder<T> {
447
475
  // Mode flags
448
476
  private isPromptMode = false;
449
477
  private isPanelMode = false;
478
+ /** True when the active prompt is a centred floating overlay. Search
479
+ * status then goes to the overlay's own footer (visible inside the frame)
480
+ * rather than the editor status bar (off at the bottom, easy to miss). */
481
+ private isOverlay = false;
482
+
483
+ /** Present a search-status message where the user is actually looking: on
484
+ * the overlay's input row (right-aligned by the match count) for a
485
+ * floating overlay, else the editor status bar. */
486
+ private setSearchStatus(message: string): void {
487
+ if (this.isOverlay) {
488
+ this.editor.setPromptStatus(message);
489
+ } else {
490
+ this.editor.setStatus(message);
491
+ }
492
+ }
493
+
494
+ /** Report a successful search with `count` matches. In overlay mode the
495
+ * "N / total" count on the input row already conveys this, so the status
496
+ * is cleared to avoid duplicating it; the status bar (non-overlay) still
497
+ * shows "Found N matches". */
498
+ private reportFound(count: number): void {
499
+ if (this.isOverlay) {
500
+ this.editor.setPromptStatus("");
501
+ } else {
502
+ this.editor.setStatus(`Found ${count} matches`);
503
+ }
504
+ }
450
505
 
451
506
  // Handler names (for cleanup)
452
507
  private handlerPrefix: string;
@@ -513,6 +568,7 @@ export class Finder<T> {
513
568
 
514
569
  // Start the prompt
515
570
  const overlay = options.floatingOverlay === true;
571
+ this.isOverlay = overlay;
516
572
  if (options.initialQuery) {
517
573
  this.editor.startPromptWithInitial(
518
574
  options.title,
@@ -525,7 +581,7 @@ export class Finder<T> {
525
581
  const result = this.editor.startPrompt(options.title, this.config.id, overlay);
526
582
  this.editor.debug(`[Finder] startPrompt returned: ${result}`);
527
583
  }
528
- this.editor.setStatus("Type to search...");
584
+ this.setSearchStatus("Type to search");
529
585
  }
530
586
 
531
587
  /**
@@ -545,6 +601,11 @@ export class Finder<T> {
545
601
  // unchanged query.
546
602
  this.promptState.lastQuery = "";
547
603
  if (query.length === 0) return;
604
+ // The backend (or scope set) changed, so the on-screen results are now
605
+ // stale. Clear them and show progress immediately rather than leaving the
606
+ // previous output up while the (possibly slow) new search runs.
607
+ this.updatePromptResults([]);
608
+ this.setSearchStatus("Searching…");
548
609
  await this.runSearch(query, this.currentSource);
549
610
  }
550
611
 
@@ -713,9 +774,9 @@ export class Finder<T> {
713
774
  this.updatePromptResults(filtered);
714
775
 
715
776
  if (filtered.length > 0) {
716
- this.editor.setStatus(`Found ${filtered.length} matches`);
777
+ this.reportFound(filtered.length);
717
778
  } else {
718
- this.editor.setStatus("No matches");
779
+ this.setSearchStatus("No matches");
719
780
  }
720
781
  } else {
721
782
  // Search mode: run external search
@@ -745,6 +806,7 @@ export class Finder<T> {
745
806
  }
746
807
  this.editor.setPromptSuggestions([]);
747
808
  this.promptState.results = [];
809
+ this.setSearchStatus("");
748
810
  return;
749
811
  }
750
812
 
@@ -768,6 +830,11 @@ export class Finder<T> {
768
830
  }
769
831
  this.promptState.lastQuery = query;
770
832
 
833
+ // A search is now actually starting (every query change that gets here —
834
+ // typing, deleting, provider/scope refresh). Show pending status so the
835
+ // user sees the re-scan in progress rather than a stale result count.
836
+ this.setSearchStatus("Searching…");
837
+
771
838
  try {
772
839
  const searchResult = source.search(query);
773
840
 
@@ -793,21 +860,21 @@ export class Finder<T> {
793
860
  this.updatePromptResults(parsed);
794
861
 
795
862
  if (parsed.length > 0) {
796
- this.editor.setStatus(`Found ${parsed.length} matches`);
863
+ this.reportFound(parsed.length);
797
864
  // Show preview of first result
798
865
  if (this.shouldShowPreview()) {
799
866
  await this.updatePreview(this.promptState.entries[0]);
800
867
  }
801
868
  } else {
802
- this.editor.setStatus("No matches");
869
+ this.setSearchStatus("No matches");
803
870
  }
804
871
  } else if (result.exit_code === 1) {
805
872
  // No matches
806
873
  this.updatePromptResults([]);
807
- this.editor.setStatus("No matches");
874
+ this.setSearchStatus("No matches");
808
875
  } else if (result.exit_code !== -1) {
809
876
  // Error (ignore -1 which means killed)
810
- this.editor.setStatus(`Search error: ${result.stderr}`);
877
+ this.setSearchStatus(`Search error: ${result.stderr}`);
811
878
  }
812
879
  } else {
813
880
  // Promise<T[]>
@@ -821,12 +888,12 @@ export class Finder<T> {
821
888
  this.updatePromptResults(results);
822
889
 
823
890
  if (results.length > 0) {
824
- this.editor.setStatus(`Found ${results.length} matches`);
891
+ this.reportFound(results.length);
825
892
  if (this.shouldShowPreview()) {
826
893
  await this.updatePreview(this.promptState.entries[0]);
827
894
  }
828
895
  } else {
829
- this.editor.setStatus("No matches");
896
+ this.setSearchStatus("No matches");
830
897
  }
831
898
  }
832
899
  } catch (e) {
@@ -864,7 +931,15 @@ export class Finder<T> {
864
931
  (entry, i) => ({
865
932
  text: entry.label,
866
933
  description: entry.description,
867
- value: `${i}`,
934
+ // The preview pane uses `value` as the authoritative
935
+ // `path:line:col` for the result. We must not rely on parsing the
936
+ // user-facing label, which may carry source badges (e.g. "[term]")
937
+ // that make the label unparseable as a path.
938
+ value: entry.location
939
+ ? `${entry.location.file}:${entry.location.line}:${
940
+ entry.location.column ?? 1
941
+ }`
942
+ : `${i}`,
868
943
  disabled: false,
869
944
  })
870
945
  );
@@ -1064,12 +1139,17 @@ export class Finder<T> {
1064
1139
  private registerPanelHandlers(): void {
1065
1140
  const self = this;
1066
1141
 
1067
- // Define panel mode
1142
+ // Define panel mode. The built-in Return/Escape bindings are
1143
+ // augmented with any caller-supplied `panelKeys` so plugin-specific
1144
+ // shortcuts (e.g. Diagnostics' "q: close | a: toggle filter") resolve
1145
+ // to their handlers instead of falling through to the read-only text
1146
+ // layer (issue #2125).
1068
1147
  this.editor.defineMode(
1069
1148
  this.modeName,
1070
1149
  [
1071
1150
  ["Return", `${this.handlerPrefix}_panel_select`],
1072
1151
  ["Escape", `${this.handlerPrefix}_panel_close`],
1152
+ ...(this.config.panelKeys ?? []),
1073
1153
  ],
1074
1154
  true
1075
1155
  );
@@ -1351,13 +1431,17 @@ export class Finder<T> {
1351
1431
  } else if (entry.location) {
1352
1432
  const loc = entry.location;
1353
1433
 
1354
- // Close the panel first. This is necessary because
1355
- // navigateOnCursorMove's focusSplit(panelSplitId) can interfere with
1356
- // the jump it queues a FocusSplit that runs after OpenFileInSplit
1357
- // and restores the panel as the active split.
1358
- this.closePanel();
1434
+ // Close the panel first (unless the consumer opted to keep it
1435
+ // docked via `closeOnSelect: false`). Closing is the default
1436
+ // because navigateOnCursorMove's focusSplit(panelSplitId) can
1437
+ // interfere with the jump it queues a FocusSplit that runs after
1438
+ // OpenFileInSplit and restores the panel as the active split.
1439
+ if (this.config.closeOnSelect !== false) {
1440
+ this.closePanel();
1441
+ }
1359
1442
 
1360
- // Now navigate with the panel gone only one split remains
1443
+ // openFile routes away from the dock leaf, so with the panel kept
1444
+ // open the file lands in the editor pane and the list stays put.
1361
1445
  this.editor.openFile(loc.file, loc.line, loc.column);
1362
1446
  this.editor.setStatus(`Jumped to ${loc.file}:${loc.line}`);
1363
1447
  }
@@ -735,6 +735,13 @@ type CursorInfo = {
735
735
  start: number;
736
736
  end: number;
737
737
  } | null;
738
+ /**
739
+ * 0-indexed line number of the cursor. `null` when the line index is
740
+ * unavailable — e.g. a huge file whose line scan hasn't completed, where
741
+ * the editor positions purely by byte offset. Plugins must treat `null`
742
+ * as "unknown", never as line 0.
743
+ */
744
+ line: number | null;
738
745
  };
739
746
  type OverlayOptions = {
740
747
  /**
@@ -896,6 +903,15 @@ type WidgetSpec = {
896
903
  "kind": "row";
897
904
  children: Array<WidgetSpec>;
898
905
  key?: string | null;
906
+ /**
907
+ * When true, children that don't fit on one line reflow onto
908
+ * additional lines (growing the row's height) instead of being
909
+ * truncated. Children are never split — wrap happens at child
910
+ * boundaries — so wrap a logical group (e.g. a toggle + its
911
+ * accelerator) in a nested non-wrapping `Row` to keep it intact.
912
+ * Ignored when the row contains multi-line (block) children.
913
+ */
914
+ wrap: boolean;
899
915
  } | {
900
916
  "kind": "col";
901
917
  children: Array<WidgetSpec>;
@@ -1206,6 +1222,16 @@ interface SearchHandle {
1206
1222
  take(): SearchTakeResult;
1207
1223
  cancel(): void;
1208
1224
  }
1225
+ type ReplaceResult = {
1226
+ /**
1227
+ * Number of replacements made
1228
+ */
1229
+ replacements: number;
1230
+ /**
1231
+ * Buffer ID of the edited buffer
1232
+ */
1233
+ bufferId: number;
1234
+ };
1209
1235
  type AuthorityFilesystem = {
1210
1236
  kind: "local";
1211
1237
  };
@@ -1489,16 +1515,6 @@ type RemoteIndicatorStatePayload = {
1489
1515
  kind: "disconnected";
1490
1516
  label?: string | null;
1491
1517
  };
1492
- type ReplaceResult = {
1493
- /**
1494
- * Number of replacements made
1495
- */
1496
- replacements: number;
1497
- /**
1498
- * Buffer ID of the edited buffer
1499
- */
1500
- bufferId: number;
1501
- };
1502
1518
  type SpawnResult = {
1503
1519
  /**
1504
1520
  * Complete stdout as string
@@ -1622,6 +1638,13 @@ interface EditorAPI {
1622
1638
  */
1623
1639
  executeAction(actionName: string): boolean;
1624
1640
  /**
1641
+ * Cancel the active prompt / overlay — the same teardown the
1642
+ * Escape key triggers. Lets a plugin dismiss a prompt it opened
1643
+ * (e.g. exporting Live Grep results to a dock panel) without
1644
+ * routing a synthetic keypress.
1645
+ */
1646
+ cancelPrompt(): boolean;
1647
+ /**
1625
1648
  * Register a custom statusbar token.
1626
1649
  * Token will be named "plugin_name:token_name" where plugin_name is the current plugin.
1627
1650
  * Returns true if registration succeeded, false if invalid or already registered.
@@ -1695,7 +1718,13 @@ interface EditorAPI {
1695
1718
  */
1696
1719
  listSplits(): SplitSnapshot[];
1697
1720
  /**
1698
- * Get the line number (0-indexed) of the primary cursor
1721
+ * Get the line number (0-indexed) of the primary cursor.
1722
+ *
1723
+ * @deprecated Use `getPrimaryCursor()?.line` instead. This accessor cannot
1724
+ * represent "line index unavailable" (huge files before their line scan) —
1725
+ * it returns `0` in that case, indistinguishable from a real first line.
1726
+ * `getPrimaryCursor().line` is `number | null` and also covers every cursor
1727
+ * via `getAllCursors()`.
1699
1728
  */
1700
1729
  getCursorLine(): number;
1701
1730
  /**
@@ -1864,38 +1893,17 @@ interface EditorAPI {
1864
1893
  */
1865
1894
  getAuthorityLabel(): string;
1866
1895
  /**
1867
- * Current Workspace Trust level for the active project:
1868
- * `"restricted"`, `"trusted"`, or `"blocked"` (empty `""` when trust
1869
- * state is unavailable, e.g. the default local authority).
1870
- *
1871
- * Trust is a per-project, user-granted decision. Plugins that run
1872
- * repo-controlled work (env activation, project tooling, repo-local
1873
- * binaries) MUST gate on this and treat anything other than
1874
- * `"trusted"` as "do not execute".
1875
- */
1876
- workspaceTrustLevel(): "restricted" | "trusted" | "blocked" | "";
1877
- /**
1878
- * Activate an environment by setting the live env recipe: an activation
1879
- * shell `snippet` (e.g. `eval "$(direnv export bash)"`,
1880
- * `source .venv/bin/activate`, or `""` for a pure login shell) run in
1881
- * `dir` (defaults to the workspace). It is re-evaluated on demand on the
1882
- * active backend and applied to every spawn — language servers,
1883
- * formatters, `spawnProcess` — so they see the project environment. No
1884
- * authority rebuild; the LSP is restarted to pick it up.
1885
- *
1886
- * Honored only when `workspaceTrustLevel() === "trusted"` (it runs
1887
- * repo-controlled code). Call `clearEnv()` to deactivate.
1888
- */
1889
- setEnv(snippet: string, dir?: string): void;
1890
- /**
1891
- * Deactivate the environment set by `setEnv` — spawns return to the
1892
- * inherited environment.
1896
+ * Current Workspace Trust level for the active project: `"restricted"`,
1897
+ * `"trusted"`, or `"blocked"` (empty when unavailable). Exposed to JS as
1898
+ * `editor.workspaceTrustLevel()`. Plugins that run repo-controlled work
1899
+ * should treat anything other than `"trusted"` as "do not execute".
1893
1900
  */
1894
- clearEnv(): void;
1901
+ workspaceTrustLevel(): string;
1895
1902
  /**
1896
- * Whether an environment is currently active (a recipe was set via
1897
- * `setEnv`). Survives the restart `setEnv` triggers, so a plugin can
1898
- * re-establish its file watch and reflect activation after reloading.
1903
+ * Whether an environment is currently active (set via `editor.setEnv`).
1904
+ * Exposed to JS as `editor.envActive()`. Lets the env-manager plugin
1905
+ * reflect activation and re-establish its file watch after the restart
1906
+ * that `setEnv` triggers.
1899
1907
  */
1900
1908
  envActive(): boolean;
1901
1909
  /**
@@ -2134,6 +2142,21 @@ interface EditorAPI {
2134
2142
  */
2135
2143
  getDataDir(): string;
2136
2144
  /**
2145
+ * Directory holding terminal scrollback backing files for the current
2146
+ * working directory. Each project root / worktree has its own subdir, so
2147
+ * Universal Search's terminal scope can stay scoped to the active
2148
+ * project rather than spanning every project's terminals.
2149
+ */
2150
+ getTerminalDir(): string;
2151
+ /**
2152
+ * Per-working-directory data root for plugin state scoped to the current
2153
+ * project root / worktree (`<data_dir>/workdirs/<encoded-cwd>/`). Use
2154
+ * instead of `getDataDir()` for state that should not be shared across
2155
+ * worktrees. The directory is not created here — callers create what
2156
+ * they need under it.
2157
+ */
2158
+ getWorkingDataDir(): string;
2159
+ /**
2137
2160
  * Get themes directory path
2138
2161
  */
2139
2162
  getThemesDir(): string;
@@ -2267,6 +2290,12 @@ interface EditorAPI {
2267
2290
  */
2268
2291
  clearOverlaysInRange(bufferId: number, start: number, end: number): boolean;
2269
2292
  /**
2293
+ * Clear overlays in a single namespace that overlap with a byte range.
2294
+ * Unlike clearOverlaysInRange, overlays in other namespaces (e.g.
2295
+ * editor-owned LSP diagnostics) are left untouched.
2296
+ */
2297
+ clearOverlaysInRangeForNamespace(bufferId: number, namespace: string, start: number, end: number): boolean;
2298
+ /**
2270
2299
  * Remove an overlay by its handle
2271
2300
  */
2272
2301
  removeOverlay(bufferId: number, handle: string): boolean;
@@ -2472,6 +2501,24 @@ interface EditorAPI {
2472
2501
  */
2473
2502
  setPromptFooter(footer: StyledText[]): boolean;
2474
2503
  /**
2504
+ * Set the floating-overlay prompt's input-row status text (right-aligned,
2505
+ * left of the match count). Empty string clears it.
2506
+ */
2507
+ setPromptStatus(status: string): boolean;
2508
+ /**
2509
+ * Set the floating-overlay prompt's toolbar as a `WidgetSpec` (real,
2510
+ * clickable `Toggle`/`Button` widgets rendered in the header band, in
2511
+ * place of the styled-text title). Pass `null`/`undefined` to clear it.
2512
+ */
2513
+ setPromptToolbar(specObj: unknown): boolean;
2514
+ /**
2515
+ * Toggle a floating-overlay toolbar control by its widget `key`. The host
2516
+ * owns the toggle's checked state, flips it, and emits a `widget_event`
2517
+ * the plugin can listen for. Lets a plugin route its own Alt+… shortcut
2518
+ * through the same host path as a click / Space on the toggle.
2519
+ */
2520
+ toggleOverlayToolbarWidget(key: string): boolean;
2521
+ /**
2475
2522
  * Override the currently-highlighted suggestion row in the
2476
2523
  * open prompt. The editor clamps `index` to the suggestion
2477
2524
  * list's bounds and the renderer scrolls it into view on
@@ -2860,6 +2907,16 @@ interface EditorAPI {
2860
2907
  */
2861
2908
  clearAuthority(): void;
2862
2909
  /**
2910
+ * Activate an environment: set the live env recipe (`snippet` run in
2911
+ * `dir`). Applied to every spawn, re-evaluated on demand — no restart.
2912
+ * Honored only when the workspace is Trusted.
2913
+ */
2914
+ setEnv(snippet: string, dir: string | null): void;
2915
+ /**
2916
+ * Deactivate the environment — spawns return to the inherited env.
2917
+ */
2918
+ clearEnv(): void;
2919
+ /**
2863
2920
  * Override the Remote Indicator's displayed state. Plugins call
2864
2921
  * this to surface lifecycle transitions that the authority layer
2865
2922
  * doesn't know about yet — "Connecting" while `devcontainer up`
@@ -2926,13 +2983,14 @@ interface EditorAPI {
2926
2983
  caseSensitive?: boolean;
2927
2984
  maxResults?: number;
2928
2985
  wholeWords?: boolean;
2986
+ sourceBufferId?: number;
2929
2987
  }): SearchHandle;
2930
2988
  /**
2931
2989
  * Replace matches in a file's buffer (async)
2932
2990
  * Opens the file if not already in a buffer, applies edits via the buffer model,
2933
2991
  * and saves. All edits are grouped as a single undo action.
2934
2992
  */
2935
- replaceInFile(filePath: string, matches: number[][], replacement: string): Promise<ReplaceResult>;
2993
+ replaceInFile(filePath: string, matches: number[][], replacement: string, bufferId?: number): Promise<ReplaceResult>;
2936
2994
  /**
2937
2995
  * Send LSP request (async, returns request_id)
2938
2996
  */
@@ -60,6 +60,14 @@ export function row(...children: WidgetSpec[]): WidgetSpec {
60
60
  return { kind: "row", children };
61
61
  }
62
62
 
63
+ /** Horizontal layout that **wraps**: children that don't fit on one line
64
+ * reflow onto additional lines (growing the row's height) instead of being
65
+ * truncated. Children are never split, so wrap a logical group (e.g. a
66
+ * toggle + its accelerator) in a nested `row(...)` to keep it intact. */
67
+ export function wrappingRow(...children: WidgetSpec[]): WidgetSpec {
68
+ return { kind: "row", children, wrap: true };
69
+ }
70
+
63
71
  /** Vertical layout. Children stacked top-to-bottom. */
64
72
  export function col(...children: WidgetSpec[]): WidgetSpec {
65
73
  return { kind: "col", children };
@@ -978,7 +978,18 @@ async function recompute(bufferId: number): Promise<void> {
978
978
  }
979
979
 
980
980
  const length = editor.getBufferLength(bufferId);
981
- const newText = await editor.getBufferText(bufferId, 0, length);
981
+ let newText: string;
982
+ try {
983
+ newText = await editor.getBufferText(bufferId, 0, length);
984
+ } catch (e) {
985
+ // The buffer can close between the awaits above (the git `show`
986
+ // for the reference is slow) and this fetch — e.g. the user
987
+ // closes the file, or an external process churns it while it's
988
+ // open. `buffer_closed` deletes our state, so if it's gone the
989
+ // recompute is moot: bail quietly instead of logging an error.
990
+ if (!states.has(bufferId)) return;
991
+ throw e;
992
+ }
982
993
 
983
994
  // Skip 1: same buffer text as last recompute. `lines_changed` fires
984
995
  // on viewport scrolls (cursor up/down past the visible area), and