@fresh-editor/fresh-editor 0.3.7 → 0.3.9

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.
@@ -5,7 +5,7 @@ import {
5
5
  buildCommitLogEntries,
6
6
  fetchGitLog,
7
7
  } from "./lib/git_history.ts";
8
- import { button, flexSpacer, key, list, row, WidgetPanel } from "./lib/index.ts";
8
+ import { button, flexSpacer, list, row, WidgetPanel } from "./lib/index.ts";
9
9
 
10
10
  const editor = getEditor();
11
11
 
@@ -105,20 +105,23 @@ const SELECT_DEBOUNCE_MS = 60;
105
105
  // the log, and opens the file at the cursor when pressed in the detail).
106
106
  // =============================================================================
107
107
 
108
- // j/k/Up/Down/PageUp/PageDown route to the log List widget so the host
109
- // owns selection + scroll + auto-scroll. The List's `select` event then
110
- // fires back into the plugin's `widget_event` handler for detail-pane
111
- // refresh. Other plugin actions (q/r/y/Tab/Return) stay as direct
112
- // bindings they don't depend on which row is highlighted.
108
+ // The log pane is cursor-driven: j/k/Up/Down/PageUp/PageDown move the
109
+ // pane's real buffer cursor (normal editor movement), which scrolls via
110
+ // the standard `ensure_cursor_visible` wheel only when the cursor
111
+ // crosses the top/bottom edge. The cursor is the source of truth for
112
+ // which commit is selected; a `cursor_moved` subscription mirrors its
113
+ // line into the List highlight + detail pane. On the detail pane the
114
+ // same keys scroll the diff. Other actions (q/r/y/Tab/Return) are direct
115
+ // bindings — they don't depend on the cursor row.
113
116
  editor.defineMode(
114
117
  "git-log",
115
118
  [
116
- ["k", "git_log_select_up"],
117
- ["j", "git_log_select_down"],
118
- ["Up", "git_log_select_up"],
119
- ["Down", "git_log_select_down"],
120
- ["PageUp", "git_log_select_page_up"],
121
- ["PageDown", "git_log_select_page_down"],
119
+ ["k", "move_up"],
120
+ ["j", "move_down"],
121
+ ["Up", "move_up"],
122
+ ["Down", "move_down"],
123
+ ["PageUp", "move_page_up"],
124
+ ["PageDown", "move_page_down"],
122
125
  ["Return", "git_log_enter"],
123
126
  ["Tab", "git_log_tab"],
124
127
  ["q", "git_log_q"],
@@ -130,52 +133,6 @@ editor.defineMode(
130
133
  true, // inherit Normal-context bindings for unbound keys
131
134
  );
132
135
 
133
- function git_log_select_up(): void {
134
- if (isLogPanelActive()) {
135
- state.logPanel?.command(key("Up"));
136
- } else {
137
- editor.executeAction("move_up");
138
- }
139
- }
140
- function git_log_select_down(): void {
141
- if (isLogPanelActive()) {
142
- state.logPanel?.command(key("Down"));
143
- } else {
144
- editor.executeAction("move_down");
145
- }
146
- }
147
- function git_log_select_page_up(): void {
148
- if (isLogPanelActive()) {
149
- state.logPanel?.command(key("PageUp"));
150
- } else {
151
- editor.executeAction("move_page_up");
152
- }
153
- }
154
- function git_log_select_page_down(): void {
155
- if (isLogPanelActive()) {
156
- state.logPanel?.command(key("PageDown"));
157
- } else {
158
- editor.executeAction("move_page_down");
159
- }
160
- }
161
-
162
- /** True iff the log panel is the focused buffer in the group. The
163
- * group's bindings (j/k/Up/Down/PageUp/PageDown) apply to all panels
164
- * uniformly; we only want navigation to drive the List widget when
165
- * the user is *on* the log panel. From the detail panel, the same
166
- * keys must move the buffer cursor (so users can scroll the diff
167
- * before pressing Enter on a diff line to open the file view). */
168
- function isLogPanelActive(): boolean {
169
- return (
170
- state.logBufferId !== null &&
171
- editor.getActiveBufferId() === state.logBufferId
172
- );
173
- }
174
- registerHandler("git_log_select_up", git_log_select_up);
175
- registerHandler("git_log_select_down", git_log_select_down);
176
- registerHandler("git_log_select_page_up", git_log_select_page_up);
177
- registerHandler("git_log_select_page_down", git_log_select_page_down);
178
-
179
136
  // =============================================================================
180
137
  // Panel layout
181
138
  // =============================================================================
@@ -268,19 +225,13 @@ editor.on("widget_event", (data) => {
268
225
  }
269
226
  return;
270
227
  }
271
- // Log pane (List of commit rows) `select` fires on j/k/Up/Down/
272
- // PageUp/PageDown navigation and on row clicks; `activate` fires on
273
- // Enter or double-click.
228
+ // Log pane (List of commit rows). Selection is cursor-driven (see the
229
+ // `cursor_moved` handler), so the List's `select` event is ignored —
230
+ // a row click places the buffer cursor, and `cursor_moved` mirrors it
231
+ // into the selection. `activate` (Enter / double-click) still opens.
274
232
  if (state.logPanel !== null && data.panel_id === state.logPanel.id()) {
275
- if (data.event_type === "select") {
276
- const idx =
277
- typeof data.payload?.index === "number" ? data.payload.index : -1;
278
- if (idx >= 0) void on_log_select(idx);
279
- return;
280
- }
281
233
  if (data.event_type === "activate") {
282
234
  void git_log_enter();
283
- return;
284
235
  }
285
236
  return;
286
237
  }
@@ -300,6 +251,11 @@ function detailFooter(hash: string): string {
300
251
  return editor.t("status.commit_ready", { hash });
301
252
  }
302
253
 
254
+ /** Stable widget key for the log List. The host keys selection +
255
+ * scroll instance state off this; the plugin re-pins selection
256
+ * through it after click/keyboard `select` events. */
257
+ const LOG_LIST_KEY = "git-log-list";
258
+
303
259
  function renderLog(): void {
304
260
  if (state.logPanel === null) return;
305
261
  // List takes the per-row entries directly. selectedIndex: -1 on the
@@ -321,7 +277,7 @@ function renderLog(): void {
321
277
  // scroll handle viewport. Revisit if commit lists grow into the
322
278
  // tens of thousands.
323
279
  visibleRows: Math.max(1, state.commits.length),
324
- key: "git-log-list",
280
+ key: LOG_LIST_KEY,
325
281
  }),
326
282
  );
327
283
  }
@@ -652,13 +608,20 @@ async function show_git_log(): Promise<void> {
652
608
 
653
609
  renderToolbar();
654
610
  renderLog();
655
- // List widget's instance state is the source of truth for selection;
656
- // no buffer-cursor positioning needed (the renderer auto-scrolls so
657
- // the selected row stays visible).
611
+ // Cursor-driven selection: give the log pane a real, visible cursor and
612
+ // take ownership of it (`setBufferShowCursors` locks it so the widget
613
+ // runtime won't clear it on repaint). The cursor's line is the selected
614
+ // commit; `cursor_moved` mirrors it into the List highlight + detail.
615
+ // Start on HEAD (line 0). Scrolling is the normal cursor-follow wheel.
616
+ if (state.logBufferId !== null) {
617
+ editor.setBufferShowCursors(state.logBufferId, true);
618
+ editor.setBufferCursor(state.logBufferId, 0);
619
+ }
658
620
  await refreshDetail();
659
621
 
660
622
  editor.on("resize", on_git_log_resize);
661
623
  editor.on("buffer_closed", on_git_log_buffer_closed);
624
+ editor.on("cursor_moved", on_git_log_cursor_moved);
662
625
 
663
626
  editor.setStatus(
664
627
  editor.t("status.log_ready", { count: String(state.commits.length) })
@@ -673,6 +636,7 @@ function git_log_cleanup(): void {
673
636
  if (!state.isOpen) return;
674
637
  editor.off("resize", on_git_log_resize);
675
638
  editor.off("buffer_closed", on_git_log_buffer_closed);
639
+ editor.off("cursor_moved", on_git_log_cursor_moved);
676
640
  // Kill any still-running `git show` spawns — we no longer care.
677
641
  for (const [, handle] of state.inFlightSpawns) {
678
642
  handle.kill?.();
@@ -1114,15 +1078,34 @@ function git_log_file_view_close(): void {
1114
1078
  registerHandler("git_log_file_view_close", git_log_file_view_close);
1115
1079
 
1116
1080
  // =============================================================================
1117
- // Selection tracking — live-update the detail panel as the user
1118
- // navigates the List. Driven by `widget_event "select"` from the host.
1081
+ // Selection tracking — the log pane is cursor-driven. The buffer cursor's
1082
+ // line (set by arrow-key movement or a click) is the selected commit; this
1083
+ // `cursor_moved` subscription mirrors it into the List highlight and the
1084
+ // detail pane. Scrolling is handled by the normal cursor-follow wheel, so
1085
+ // the viewport only moves when the cursor crosses the top/bottom edge.
1119
1086
  // =============================================================================
1120
1087
 
1121
- async function on_log_select(idx: number): Promise<void> {
1088
+ function on_git_log_cursor_moved(data: { buffer_id: number; line: number }): void {
1089
+ if (!state.isOpen || state.logBufferId === null) return;
1090
+ if (data.buffer_id !== state.logBufferId) return;
1091
+ // `cursor_moved.line` is 1-based; commit rows are 0-based (no header),
1092
+ // so the selected commit index is `line - 1`.
1093
+ const idx = data.line - 1;
1094
+ if (idx < 0 || idx >= state.commits.length) return;
1095
+ void selectCommitLine(idx);
1096
+ }
1097
+
1098
+ async function selectCommitLine(idx: number): Promise<void> {
1122
1099
  if (!state.isOpen) return;
1123
1100
  if (idx === state.selectedIndex) return;
1124
1101
  state.selectedIndex = idx;
1125
1102
 
1103
+ // Move the List's highlight bar to the cursor's row. The cursor itself
1104
+ // is the real (plugin-owned) buffer cursor, so it stays exactly where
1105
+ // the user moved or clicked it — this only repaints the row styling,
1106
+ // and the repaint preserves the cursor position.
1107
+ state.logPanel?.setSelectedIndex(LOG_LIST_KEY, idx);
1108
+
1126
1109
  const commit = state.commits[state.selectedIndex];
1127
1110
  if (commit) {
1128
1111
  editor.setStatus(
@@ -447,6 +447,33 @@ export class Finder<T> {
447
447
  // Mode flags
448
448
  private isPromptMode = false;
449
449
  private isPanelMode = false;
450
+ /** True when the active prompt is a centred floating overlay. Search
451
+ * status then goes to the overlay's own footer (visible inside the frame)
452
+ * rather than the editor status bar (off at the bottom, easy to miss). */
453
+ private isOverlay = false;
454
+
455
+ /** Present a search-status message where the user is actually looking: on
456
+ * the overlay's input row (right-aligned by the match count) for a
457
+ * floating overlay, else the editor status bar. */
458
+ private setSearchStatus(message: string): void {
459
+ if (this.isOverlay) {
460
+ this.editor.setPromptStatus(message);
461
+ } else {
462
+ this.editor.setStatus(message);
463
+ }
464
+ }
465
+
466
+ /** Report a successful search with `count` matches. In overlay mode the
467
+ * "N / total" count on the input row already conveys this, so the status
468
+ * is cleared to avoid duplicating it; the status bar (non-overlay) still
469
+ * shows "Found N matches". */
470
+ private reportFound(count: number): void {
471
+ if (this.isOverlay) {
472
+ this.editor.setPromptStatus("");
473
+ } else {
474
+ this.editor.setStatus(`Found ${count} matches`);
475
+ }
476
+ }
450
477
 
451
478
  // Handler names (for cleanup)
452
479
  private handlerPrefix: string;
@@ -513,6 +540,7 @@ export class Finder<T> {
513
540
 
514
541
  // Start the prompt
515
542
  const overlay = options.floatingOverlay === true;
543
+ this.isOverlay = overlay;
516
544
  if (options.initialQuery) {
517
545
  this.editor.startPromptWithInitial(
518
546
  options.title,
@@ -525,7 +553,7 @@ export class Finder<T> {
525
553
  const result = this.editor.startPrompt(options.title, this.config.id, overlay);
526
554
  this.editor.debug(`[Finder] startPrompt returned: ${result}`);
527
555
  }
528
- this.editor.setStatus("Type to search...");
556
+ this.setSearchStatus("Type to search");
529
557
  }
530
558
 
531
559
  /**
@@ -545,6 +573,11 @@ export class Finder<T> {
545
573
  // unchanged query.
546
574
  this.promptState.lastQuery = "";
547
575
  if (query.length === 0) return;
576
+ // The backend (or scope set) changed, so the on-screen results are now
577
+ // stale. Clear them and show progress immediately rather than leaving the
578
+ // previous output up while the (possibly slow) new search runs.
579
+ this.updatePromptResults([]);
580
+ this.setSearchStatus("Searching…");
548
581
  await this.runSearch(query, this.currentSource);
549
582
  }
550
583
 
@@ -713,9 +746,9 @@ export class Finder<T> {
713
746
  this.updatePromptResults(filtered);
714
747
 
715
748
  if (filtered.length > 0) {
716
- this.editor.setStatus(`Found ${filtered.length} matches`);
749
+ this.reportFound(filtered.length);
717
750
  } else {
718
- this.editor.setStatus("No matches");
751
+ this.setSearchStatus("No matches");
719
752
  }
720
753
  } else {
721
754
  // Search mode: run external search
@@ -745,6 +778,7 @@ export class Finder<T> {
745
778
  }
746
779
  this.editor.setPromptSuggestions([]);
747
780
  this.promptState.results = [];
781
+ this.setSearchStatus("");
748
782
  return;
749
783
  }
750
784
 
@@ -768,6 +802,11 @@ export class Finder<T> {
768
802
  }
769
803
  this.promptState.lastQuery = query;
770
804
 
805
+ // A search is now actually starting (every query change that gets here —
806
+ // typing, deleting, provider/scope refresh). Show pending status so the
807
+ // user sees the re-scan in progress rather than a stale result count.
808
+ this.setSearchStatus("Searching…");
809
+
771
810
  try {
772
811
  const searchResult = source.search(query);
773
812
 
@@ -793,21 +832,21 @@ export class Finder<T> {
793
832
  this.updatePromptResults(parsed);
794
833
 
795
834
  if (parsed.length > 0) {
796
- this.editor.setStatus(`Found ${parsed.length} matches`);
835
+ this.reportFound(parsed.length);
797
836
  // Show preview of first result
798
837
  if (this.shouldShowPreview()) {
799
838
  await this.updatePreview(this.promptState.entries[0]);
800
839
  }
801
840
  } else {
802
- this.editor.setStatus("No matches");
841
+ this.setSearchStatus("No matches");
803
842
  }
804
843
  } else if (result.exit_code === 1) {
805
844
  // No matches
806
845
  this.updatePromptResults([]);
807
- this.editor.setStatus("No matches");
846
+ this.setSearchStatus("No matches");
808
847
  } else if (result.exit_code !== -1) {
809
848
  // Error (ignore -1 which means killed)
810
- this.editor.setStatus(`Search error: ${result.stderr}`);
849
+ this.setSearchStatus(`Search error: ${result.stderr}`);
811
850
  }
812
851
  } else {
813
852
  // Promise<T[]>
@@ -821,12 +860,12 @@ export class Finder<T> {
821
860
  this.updatePromptResults(results);
822
861
 
823
862
  if (results.length > 0) {
824
- this.editor.setStatus(`Found ${results.length} matches`);
863
+ this.reportFound(results.length);
825
864
  if (this.shouldShowPreview()) {
826
865
  await this.updatePreview(this.promptState.entries[0]);
827
866
  }
828
867
  } else {
829
- this.editor.setStatus("No matches");
868
+ this.setSearchStatus("No matches");
830
869
  }
831
870
  }
832
871
  } catch (e) {
@@ -864,7 +903,15 @@ export class Finder<T> {
864
903
  (entry, i) => ({
865
904
  text: entry.label,
866
905
  description: entry.description,
867
- value: `${i}`,
906
+ // The preview pane uses `value` as the authoritative
907
+ // `path:line:col` for the result. We must not rely on parsing the
908
+ // user-facing label, which may carry source badges (e.g. "[term]")
909
+ // that make the label unparseable as a path.
910
+ value: entry.location
911
+ ? `${entry.location.file}:${entry.location.line}:${
912
+ entry.location.column ?? 1
913
+ }`
914
+ : `${i}`,
868
915
  disabled: false,
869
916
  })
870
917
  );
@@ -621,6 +621,50 @@ type TerminalResult = {
621
621
  */
622
622
  splitId: number | null;
623
623
  };
624
+ type CreateWindowWithTerminalOptions = {
625
+ /**
626
+ * Absolute path to the new session's worktree / project
627
+ * root. Relative paths are rejected (logged, no window
628
+ * created).
629
+ */
630
+ root: string;
631
+ /**
632
+ * Human-readable label for the new session. When empty,
633
+ * defaults to the basename of `root`.
634
+ */
635
+ label: string;
636
+ /**
637
+ * Working directory for the spawned terminal. Defaults to
638
+ * `root` when omitted.
639
+ */
640
+ cwd?: string;
641
+ /**
642
+ * Argv to spawn directly inside the PTY. `None` keeps the
643
+ * shell-and-type behaviour; `Some([cmd, ...args])` runs the
644
+ * command as the PTY child (used by Orchestrator so the
645
+ * agent process is the PTY's direct child).
646
+ */
647
+ command?: Array<string>;
648
+ /**
649
+ * Tab title override. Defaults to `command[0]`'s basename
650
+ * when `command` is set, or "Terminal N" otherwise.
651
+ */
652
+ title?: string;
653
+ };
654
+ type SessionWithTerminalResult = {
655
+ /**
656
+ * The new window's id.
657
+ */
658
+ windowId: number;
659
+ /**
660
+ * The seeded terminal's id (for `sendTerminalInput`, etc.).
661
+ */
662
+ terminalId: number;
663
+ /**
664
+ * The seeded terminal buffer's id.
665
+ */
666
+ bufferId: number;
667
+ };
624
668
  type CreateTerminalOptions = {
625
669
  /**
626
670
  * Working directory for the terminal (defaults to editor cwd)
@@ -691,6 +735,13 @@ type CursorInfo = {
691
735
  start: number;
692
736
  end: number;
693
737
  } | null;
738
+ /**
739
+ * 0-indexed line number of the cursor. `None` 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;
694
745
  };
695
746
  type OverlayOptions = {
696
747
  /**
@@ -722,6 +773,13 @@ type OverlayOptions = {
722
773
  */
723
774
  extendToLineEnd: boolean;
724
775
  /**
776
+ * When `true`, `fg` is applied only on cells whose existing fg
777
+ * matches this overlay's resolved bg — i.e. a same-colour fg/bg
778
+ * collision. Lets a row-wide overlay stay legible on tokens that
779
+ * share the bg's colour without repainting unrelated tokens.
780
+ */
781
+ fgOnCollisionOnly: boolean;
782
+ /**
725
783
  * Optional URL for OSC 8 terminal hyperlinks.
726
784
  * When set, the overlay text becomes a clickable hyperlink in terminals
727
785
  * that support OSC 8 escape sequences.
@@ -1006,7 +1064,7 @@ type WidgetSpec = {
1006
1064
  * `WidgetMutation::SetCompletions`. An empty `items`
1007
1065
  * closes the popup.
1008
1066
  */
1009
- completions?: Array<string>;
1067
+ completions?: Array<string | CompletionItem>;
1010
1068
  /**
1011
1069
  * How many candidate rows the popup paints at once
1012
1070
  * when it opens. Excess candidates stay reachable
@@ -1091,7 +1149,7 @@ type WidgetMutation = {
1091
1149
  } | {
1092
1150
  "kind": "setCompletions";
1093
1151
  widgetKey: string;
1094
- items: Array<string>;
1152
+ items: Array<string | CompletionItem>;
1095
1153
  } | {
1096
1154
  "kind": "setChecked";
1097
1155
  widgetKey: string;
@@ -1644,7 +1702,13 @@ interface EditorAPI {
1644
1702
  */
1645
1703
  listSplits(): SplitSnapshot[];
1646
1704
  /**
1647
- * Get the line number (0-indexed) of the primary cursor
1705
+ * Get the line number (0-indexed) of the primary cursor.
1706
+ *
1707
+ * @deprecated Use `getPrimaryCursor()?.line` instead. This accessor cannot
1708
+ * represent "line index unavailable" (huge files before their line scan) —
1709
+ * it returns `0` in that case, indistinguishable from a real first line.
1710
+ * `getPrimaryCursor().line` is `number | null` and also covers every cursor
1711
+ * via `getAllCursors()`.
1648
1712
  */
1649
1713
  getCursorLine(): number;
1650
1714
  /**
@@ -1813,6 +1877,20 @@ interface EditorAPI {
1813
1877
  */
1814
1878
  getAuthorityLabel(): string;
1815
1879
  /**
1880
+ * Current Workspace Trust level for the active project: `"restricted"`,
1881
+ * `"trusted"`, or `"blocked"` (empty when unavailable). Exposed to JS as
1882
+ * `editor.workspaceTrustLevel()`. Plugins that run repo-controlled work
1883
+ * should treat anything other than `"trusted"` as "do not execute".
1884
+ */
1885
+ workspaceTrustLevel(): string;
1886
+ /**
1887
+ * Whether an environment is currently active (set via `editor.setEnv`).
1888
+ * Exposed to JS as `editor.envActive()`. Lets the env-manager plugin
1889
+ * reflect activation and re-establish its file watch after the restart
1890
+ * that `setEnv` triggers.
1891
+ */
1892
+ envActive(): boolean;
1893
+ /**
1816
1894
  * Join path components (variadic - accepts multiple string arguments)
1817
1895
  * Always uses forward slashes for cross-platform consistency (like Node.js path.posix.join)
1818
1896
  *
@@ -2048,6 +2126,21 @@ interface EditorAPI {
2048
2126
  */
2049
2127
  getDataDir(): string;
2050
2128
  /**
2129
+ * Per-working-directory data root for plugin state that should be scoped
2130
+ * to the current project root / worktree rather than shared across all of
2131
+ * them (`<data_dir>/workdirs/<encoded-cwd>/`). Prefer this over
2132
+ * `getDataDir()` for per-project state; the directory is not created for
2133
+ * you. Note: terminal scrollback and orchestrator state use their own
2134
+ * dedicated layouts (see `getTerminalDir()`), not this root.
2135
+ */
2136
+ getWorkingDataDir(): string;
2137
+ /**
2138
+ * Directory holding terminal scrollback backing files for the current
2139
+ * working directory. Each project root / worktree has its own subdir, so
2140
+ * a search can stay scoped to the active project's terminals.
2141
+ */
2142
+ getTerminalDir(): string;
2143
+ /**
2051
2144
  * Get themes directory path
2052
2145
  */
2053
2146
  getThemesDir(): string;
@@ -2386,6 +2479,29 @@ interface EditorAPI {
2386
2479
  */
2387
2480
  setPromptFooter(footer: StyledText[]): boolean;
2388
2481
  /**
2482
+ * Set the floating-overlay prompt's header toolbar as a `WidgetSpec`
2483
+ * (real, clickable `Toggle`/`Button` widgets), rendered in place of the
2484
+ * styled-text title. Give each control a `key` equal to the action it
2485
+ * should fire on click (e.g. `"live_grep_toggle_files"`). Pass `null` to
2486
+ * clear it. No visible effect on non-overlay prompts.
2487
+ */
2488
+ setPromptToolbar(spec: WidgetSpec | null): boolean;
2489
+ /**
2490
+ * Set the floating-overlay prompt's input-row status text, shown
2491
+ * right-aligned just left of the `selected / total` count (e.g.
2492
+ * "Searching…", "No matches"). Empty string clears it. No effect on
2493
+ * non-overlay prompts.
2494
+ */
2495
+ setPromptStatus(status: string): boolean;
2496
+ /**
2497
+ * Toggle a floating-overlay toolbar control by its widget `key`. The host
2498
+ * owns the toggle's checked state, flips it in place, and emits a
2499
+ * `widget_event` (`event_type: "toggle"`, payload `{ checked }`). Use this
2500
+ * to route a plugin's own keyboard shortcut through the same host path as
2501
+ * a click or Space on the toggle, then react in your `widget_event` handler.
2502
+ */
2503
+ toggleOverlayToolbarWidget(key: string): boolean;
2504
+ /**
2389
2505
  * Override the currently-highlighted suggestion row in the
2390
2506
  * open prompt. The editor clamps `index` to the suggestion
2391
2507
  * list's bounds and the renderer scrolls it into view on
@@ -2774,6 +2890,16 @@ interface EditorAPI {
2774
2890
  */
2775
2891
  clearAuthority(): void;
2776
2892
  /**
2893
+ * Activate an environment: set the live env recipe (`snippet` run in
2894
+ * `dir`). Applied to every spawn, re-evaluated on demand — no restart.
2895
+ * Honored only when the workspace is Trusted.
2896
+ */
2897
+ setEnv(snippet: string, dir: string | null): void;
2898
+ /**
2899
+ * Deactivate the environment — spawns return to the inherited env.
2900
+ */
2901
+ clearEnv(): void;
2902
+ /**
2777
2903
  * Override the Remote Indicator's displayed state. Plugins call
2778
2904
  * this to surface lifecycle transitions that the authority layer
2779
2905
  * doesn't know about yet — "Connecting" while `devcontainer up`
@@ -2799,6 +2925,19 @@ interface EditorAPI {
2799
2925
  */
2800
2926
  clearRemoteIndicatorState(): void;
2801
2927
  /**
2928
+ * Fetch a URL over HTTP(S) and stream the response body into `target_path`.
2929
+ *
2930
+ * Resolves with a `SpawnResult`-shaped value: `exit_code` is `0` on a
2931
+ * 2xx response (file written), the HTTP status code on non-2xx
2932
+ * (target file untouched), and `-1` on transport errors. `stderr`
2933
+ * carries an error message in the non-success cases; `stdout` is
2934
+ * always empty.
2935
+ *
2936
+ * This uses the editor's built-in HTTP client (`ureq`), so plugins
2937
+ * don't need `curl`/`wget` on PATH.
2938
+ */
2939
+ httpFetch(url: string, targetPath: string): ProcessHandle<SpawnResult>;
2940
+ /**
2802
2941
  * Wait for a process to complete and get its result (async)
2803
2942
  */
2804
2943
  spawnProcessWait(processId: number): Promise<SpawnResult>;
@@ -2851,6 +2990,14 @@ interface EditorAPI {
2851
2990
  */
2852
2991
  createTerminal(opts?: CreateTerminalOptions): Promise<TerminalResult>;
2853
2992
  /**
2993
+ * Create a new editor window seeded with an agent terminal as
2994
+ * its only buffer. Atomic — replaces the legacy
2995
+ * `createWindow` + `setActiveWindow` + `createTerminal`
2996
+ * chain that left a transient `[No Name]` tab alongside the
2997
+ * agent terminal.
2998
+ */
2999
+ createWindowWithTerminal(opts: CreateWindowWithTerminalOptions): Promise<SessionWithTerminalResult>;
3000
+ /**
2854
3001
  * Send input data to a terminal
2855
3002
  */
2856
3003
  sendTerminalInput(terminalId: number, data: string): boolean;
@@ -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 };
@@ -38,19 +38,18 @@ const PRIORITY = 9;
38
38
 
39
39
  // Theme keys for backgrounds and "on top of bg" foregrounds. These
40
40
  // are resolved at render time by the editor, so the diff colors track
41
- // the active theme automatically. The `editor.diff_*_fg` keys are
42
- // purpose-built for "text drawn on top of the matching diff bg" —
43
- // they default to `ui.file_status_*_fg` so themes that haven't been
44
- // updated still work, but themes whose `file_status_*_fg` collides
45
- // with `diff_*_bg` (e.g. `terminal`, where both resolve to ANSI Red)
46
- // override `editor.diff_*_fg` to a contrasting color.
41
+ // the active theme automatically. The `editor.diff_*_collision_fg`
42
+ // keys pair with `fgOnCollisionOnly: true` below: themes that define
43
+ // them get a contrasting fg painted only on cells whose syntax fg
44
+ // happens to match the diff bg (e.g. ANSI Green-on-Green); every
45
+ // other cell keeps its syntax colour.
47
46
  const THEME = {
48
47
  addedBg: "editor.diff_add_bg",
49
- addedFg: "editor.diff_add_fg",
48
+ addedFg: "editor.diff_add_collision_fg",
50
49
  modifiedBg: "editor.diff_modify_bg",
51
- modifiedFg: "editor.diff_modify_fg",
50
+ modifiedFg: "editor.diff_modify_collision_fg",
52
51
  removedBg: "editor.diff_remove_bg",
53
- removedFg: "editor.diff_remove_fg",
52
+ removedFg: "editor.diff_remove_collision_fg",
54
53
  };
55
54
 
56
55
  // `setLineIndicator` only accepts RGB triples (not theme keys), so the
@@ -845,14 +844,9 @@ function renderHunks(state: BufferDiffState, newLines: string[]): void {
845
844
  for (const h of state.hunks) {
846
845
  if (h.kind === "added" || h.kind === "modified") {
847
846
  const bg = h.kind === "added" ? THEME.addedBg : THEME.modifiedBg;
848
- // Passing `fg` as a theme key lets each theme decide whether to
849
- // override the cell's existing fg: themes that DEFINE
850
- // `editor.diff_*_fg` (e.g. `terminal`, where the ANSI bg would
851
- // otherwise collide with same-named syntax colors) get a
852
- // contrasting fg painted on; themes that don't define the key
853
- // resolve to `None` in `OverlayFace::ThemedStyle`, so the
854
- // overlay leaves the cell's fg alone and syntax highlighting
855
- // shows through unchanged.
847
+ // `fgOnCollisionOnly` keeps syntax highlighting intact on the
848
+ // rest of the line and only paints `fg` where a token's colour
849
+ // would otherwise be invisible against the diff bg.
856
850
  const fg = h.kind === "added" ? THEME.addedFg : THEME.modifiedFg;
857
851
  for (let i = 0; i < h.newCount; i++) {
858
852
  const line = h.newStart + i;
@@ -871,6 +865,7 @@ function renderHunks(state: BufferDiffState, newLines: string[]): void {
871
865
  editor.addOverlay(bid, NS_OVERLAY, start, end, {
872
866
  bg,
873
867
  fg,
868
+ fgOnCollisionOnly: true,
874
869
  underline: false,
875
870
  bold: false,
876
871
  italic: false,