@fresh-editor/fresh-editor 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +66 -2
  2. package/package.json +1 -1
  3. package/plugins/astro-lsp.ts +6 -12
  4. package/plugins/audit_mode.ts +106 -113
  5. package/plugins/bash-lsp.ts +15 -22
  6. package/plugins/clangd-lsp.ts +15 -24
  7. package/plugins/clojure-lsp.ts +9 -12
  8. package/plugins/cmake-lsp.ts +9 -12
  9. package/plugins/code-tour.ts +15 -16
  10. package/plugins/config-schema.json +10 -0
  11. package/plugins/csharp_support.ts +25 -30
  12. package/plugins/css-lsp.ts +15 -22
  13. package/plugins/dart-lsp.ts +9 -12
  14. package/plugins/dashboard.ts +118 -0
  15. package/plugins/devcontainer.i18n.json +84 -28
  16. package/plugins/devcontainer.ts +897 -170
  17. package/plugins/diagnostics_panel.ts +10 -17
  18. package/plugins/elixir-lsp.ts +9 -12
  19. package/plugins/erlang-lsp.ts +9 -12
  20. package/plugins/examples/bookmarks.ts +10 -16
  21. package/plugins/find_references.ts +5 -9
  22. package/plugins/flash.ts +577 -0
  23. package/plugins/fsharp-lsp.ts +9 -12
  24. package/plugins/git_explorer.ts +16 -20
  25. package/plugins/git_gutter.ts +65 -79
  26. package/plugins/git_log.ts +8 -8
  27. package/plugins/gleam-lsp.ts +9 -12
  28. package/plugins/go-lsp.ts +15 -22
  29. package/plugins/graphql-lsp.ts +9 -12
  30. package/plugins/haskell-lsp.ts +9 -12
  31. package/plugins/html-lsp.ts +15 -24
  32. package/plugins/java-lsp.ts +9 -12
  33. package/plugins/json-lsp.ts +15 -24
  34. package/plugins/julia-lsp.ts +9 -12
  35. package/plugins/kotlin-lsp.ts +15 -22
  36. package/plugins/latex-lsp.ts +9 -12
  37. package/plugins/lib/fresh.d.ts +378 -0
  38. package/plugins/lua-lsp.ts +15 -22
  39. package/plugins/markdown_compose.ts +78 -122
  40. package/plugins/markdown_source.ts +8 -10
  41. package/plugins/marksman-lsp.ts +9 -12
  42. package/plugins/merge_conflict.ts +15 -17
  43. package/plugins/nim-lsp.ts +9 -12
  44. package/plugins/nix-lsp.ts +9 -12
  45. package/plugins/nushell-lsp.ts +9 -12
  46. package/plugins/ocaml-lsp.ts +9 -12
  47. package/plugins/odin-lsp.ts +15 -22
  48. package/plugins/path_complete.ts +5 -6
  49. package/plugins/perl-lsp.ts +9 -12
  50. package/plugins/php-lsp.ts +15 -22
  51. package/plugins/pkg.ts +10 -21
  52. package/plugins/protobuf-lsp.ts +9 -12
  53. package/plugins/python-lsp.ts +15 -24
  54. package/plugins/r-lsp.ts +9 -12
  55. package/plugins/ruby-lsp.ts +15 -22
  56. package/plugins/rust-lsp.ts +18 -28
  57. package/plugins/scala-lsp.ts +9 -12
  58. package/plugins/schemas/theme.schema.json +18 -0
  59. package/plugins/search_replace.ts +10 -13
  60. package/plugins/solidity-lsp.ts +9 -12
  61. package/plugins/sql-lsp.ts +9 -12
  62. package/plugins/svelte-lsp.ts +9 -12
  63. package/plugins/swift-lsp.ts +9 -12
  64. package/plugins/tailwindcss-lsp.ts +9 -12
  65. package/plugins/templ-lsp.ts +9 -12
  66. package/plugins/terraform-lsp.ts +9 -12
  67. package/plugins/theme_editor.i18n.json +70 -14
  68. package/plugins/theme_editor.ts +152 -208
  69. package/plugins/toml-lsp.ts +15 -22
  70. package/plugins/tsconfig.json +100 -0
  71. package/plugins/typescript-lsp.ts +15 -24
  72. package/plugins/typst-lsp.ts +15 -22
  73. package/plugins/vi_mode.ts +77 -290
  74. package/plugins/vue-lsp.ts +9 -12
  75. package/plugins/yaml-lsp.ts +15 -22
  76. package/plugins/zig-lsp.ts +9 -12
@@ -49,7 +49,8 @@ let cmakeLspError: { serverCommand: string; message: string } | null = null;
49
49
  /**
50
50
  * Handle LSP server errors for CMake
51
51
  */
52
- function on_cmake_lsp_server_error(data: LspServerErrorData): void {
52
+
53
+ editor.on("lsp_server_error", (data) => {
53
54
  if (data.language !== "cmake") {
54
55
  return;
55
56
  }
@@ -68,14 +69,13 @@ function on_cmake_lsp_server_error(data: LspServerErrorData): void {
68
69
  } else {
69
70
  editor.setStatus(`CMake LSP error: ${data.message}`);
70
71
  }
71
- }
72
- registerHandler("on_cmake_lsp_server_error", on_cmake_lsp_server_error);
73
- editor.on("lsp_server_error", "on_cmake_lsp_server_error");
72
+ });
74
73
 
75
74
  /**
76
75
  * Handle status bar click when there's a CMake LSP error
77
76
  */
78
- function on_cmake_lsp_status_clicked(data: LspStatusClickedData): void {
77
+
78
+ editor.on("lsp_status_clicked", (data) => {
79
79
  if (data.language !== "cmake" || !cmakeLspError) {
80
80
  return;
81
81
  }
@@ -93,14 +93,13 @@ function on_cmake_lsp_status_clicked(data: LspStatusClickedData): void {
93
93
  { id: "dismiss", label: "Dismiss (ESC)" },
94
94
  ],
95
95
  });
96
- }
97
- registerHandler("on_cmake_lsp_status_clicked", on_cmake_lsp_status_clicked);
98
- editor.on("lsp_status_clicked", "on_cmake_lsp_status_clicked");
96
+ });
99
97
 
100
98
  /**
101
99
  * Handle action popup results for CMake LSP help
102
100
  */
103
- function on_cmake_lsp_action_result(data: ActionPopupResultData): void {
101
+
102
+ editor.on("action_popup_result", (data) => {
104
103
  if (data.popup_id !== "cmake-lsp-help") {
105
104
  return;
106
105
  }
@@ -131,8 +130,6 @@ function on_cmake_lsp_action_result(data: ActionPopupResultData): void {
131
130
  default:
132
131
  editor.debug(`cmake-lsp: Unknown action: ${data.action_id}`);
133
132
  }
134
- }
135
- registerHandler("on_cmake_lsp_action_result", on_cmake_lsp_action_result);
136
- editor.on("action_popup_result", "on_cmake_lsp_action_result");
133
+ });
137
134
 
138
135
  editor.debug("cmake-lsp: Plugin loaded");
@@ -123,22 +123,7 @@ interface ActionPopupResultData {
123
123
  action_id: string;
124
124
  }
125
125
 
126
- function tour_on_action_popup_result(data: ActionPopupResultData) : void {
127
- if (data.popup_id !== TOUR_POPUP_ID) return;
128
126
 
129
- switch (data.action_id) {
130
- case "next":
131
- nextStep();
132
- break;
133
- case "prev":
134
- prevStep();
135
- break;
136
- case "exit":
137
- exitTour();
138
- break;
139
- }
140
- }
141
- registerHandler("tour_on_action_popup_result", tour_on_action_popup_result);
142
127
 
143
128
  // ============================================================================
144
129
  // Overlay Rendering
@@ -397,6 +382,20 @@ editor.registerCommand(
397
382
  );
398
383
 
399
384
  // Subscribe to action popup results for navigation buttons
400
- editor.on("action_popup_result", "tour_on_action_popup_result");
385
+ editor.on("action_popup_result", (data) => {
386
+ if (data.popup_id !== TOUR_POPUP_ID) return;
387
+
388
+ switch (data.action_id) {
389
+ case "next":
390
+ nextStep();
391
+ break;
392
+ case "prev":
393
+ prevStep();
394
+ break;
395
+ case "exit":
396
+ exitTour();
397
+ break;
398
+ }
399
+ });
401
400
 
402
401
  editor.debug("Code Tour plugin loaded");
@@ -30,6 +30,7 @@
30
30
  "description": "Editor behavior settings (indentation, line numbers, wrapping, etc.)",
31
31
  "$ref": "#/$defs/EditorConfig",
32
32
  "default": {
33
+ "animations": true,
33
34
  "line_numbers": true,
34
35
  "relative_line_numbers": false,
35
36
  "highlight_current_line": true,
@@ -45,6 +46,7 @@
45
46
  "show_status_bar": true,
46
47
  "status_bar": {
47
48
  "left": [
49
+ "{remote}",
48
50
  "{filename}",
49
51
  "{cursor}",
50
52
  "{diagnostics}",
@@ -268,6 +270,12 @@
268
270
  "description": "Editor behavior configuration",
269
271
  "type": "object",
270
272
  "properties": {
273
+ "animations": {
274
+ "description": "Enable frame-buffer animations (tab-switch slides, dashboard\nbringup, plugin-driven effects). When `false`, every animation\ncall is a no-op: the UI is fully static and each render lands\nthe final frame immediately. Useful on slow terminals, over\nSSH, or for users who prefer no motion.",
275
+ "type": "boolean",
276
+ "default": true,
277
+ "x-section": "Display"
278
+ },
271
279
  "line_numbers": {
272
280
  "description": "Show line numbers in the gutter (default for new buffers)",
273
281
  "type": "boolean",
@@ -361,6 +369,7 @@
361
369
  "$ref": "#/$defs/StatusBarConfig",
362
370
  "default": {
363
371
  "left": [
372
+ "{remote}",
364
373
  "{filename}",
365
374
  "{cursor}",
366
375
  "{diagnostics}",
@@ -751,6 +760,7 @@
751
760
  "$ref": "#/$defs/StatusBarElement"
752
761
  },
753
762
  "default": [
763
+ "{remote}",
754
764
  "{filename}",
755
765
  "{cursor}",
756
766
  "{diagnostics}",
@@ -169,7 +169,10 @@ async function restoreProject(projectPath: string): Promise<void> {
169
169
  /**
170
170
  * Handle file open - set project root and restore packages
171
171
  */
172
- async function on_csharp_file_open(data: AfterFileOpenData) : Promise<void> {
172
+
173
+
174
+ // Register hook for file open
175
+ editor.on("after_file_open", async (data) => {
173
176
  // Only handle .cs files
174
177
  if (!data.path.endsWith(".cs")) {
175
178
  return;
@@ -206,16 +209,15 @@ async function on_csharp_file_open(data: AfterFileOpenData) : Promise<void> {
206
209
  await restoreProject(dir);
207
210
  }
208
211
  }
209
- }
210
- registerHandler("on_csharp_file_open", on_csharp_file_open);
211
-
212
- // Register hook for file open
213
- editor.on("after_file_open", "on_csharp_file_open");
212
+ });
214
213
 
215
214
  /**
216
215
  * Handle LSP server requests from C# language servers (Roslyn-based)
217
216
  */
218
- function on_csharp_lsp_server_request(data: LspServerRequestData) : void {
217
+
218
+
219
+ // Register hook for LSP server requests
220
+ editor.on("lsp_server_request", (data) => {
219
221
  // Only handle requests from C# language servers
220
222
  if (data.server_command !== "csharp-ls" && data.server_command !== "csharp-language-server") {
221
223
  return;
@@ -243,16 +245,15 @@ function on_csharp_lsp_server_request(data: LspServerRequestData) : void {
243
245
  // Log unhandled requests for debugging
244
246
  editor.debug(`csharp_support: Unhandled LSP request: ${data.method}`);
245
247
  }
246
- }
247
- registerHandler("on_csharp_lsp_server_request", on_csharp_lsp_server_request);
248
-
249
- // Register hook for LSP server requests
250
- editor.on("lsp_server_request", "on_csharp_lsp_server_request");
248
+ });
251
249
 
252
250
  /**
253
251
  * Handle LSP server errors for C#
254
252
  */
255
- function on_csharp_lsp_server_error(data: LspServerErrorData) : void {
253
+
254
+
255
+ // Register hook for LSP server errors
256
+ editor.on("lsp_server_error", (data) => {
256
257
  // Only handle C# language errors
257
258
  if (data.language !== "csharp") {
258
259
  return;
@@ -274,16 +275,15 @@ function on_csharp_lsp_server_error(data: LspServerErrorData) : void {
274
275
  } else {
275
276
  editor.setStatus(`C# LSP error: ${data.message}`);
276
277
  }
277
- }
278
- registerHandler("on_csharp_lsp_server_error", on_csharp_lsp_server_error);
279
-
280
- // Register hook for LSP server errors
281
- editor.on("lsp_server_error", "on_csharp_lsp_server_error");
278
+ });
282
279
 
283
280
  /**
284
281
  * Handle status bar click when there's a C# LSP error
285
282
  */
286
- function on_csharp_lsp_status_clicked(data: LspStatusClickedData) : void {
283
+
284
+
285
+ // Register hook for status bar clicks
286
+ editor.on("lsp_status_clicked", (data) => {
287
287
  // Only handle C# language clicks when there's an error
288
288
  if (data.language !== "csharp" || !csharpLspError) {
289
289
  return;
@@ -302,16 +302,15 @@ function on_csharp_lsp_status_clicked(data: LspStatusClickedData) : void {
302
302
  { id: "dismiss", label: "Dismiss (ESC)" },
303
303
  ],
304
304
  });
305
- }
306
- registerHandler("on_csharp_lsp_status_clicked", on_csharp_lsp_status_clicked);
307
-
308
- // Register hook for status bar clicks
309
- editor.on("lsp_status_clicked", "on_csharp_lsp_status_clicked");
305
+ });
310
306
 
311
307
  /**
312
308
  * Handle action popup results for C# LSP help
313
309
  */
314
- function on_csharp_lsp_action_result(data: ActionPopupResultData) : void {
310
+
311
+
312
+ // Register hook for action popup results
313
+ editor.on("action_popup_result", (data) => {
315
314
  // Only handle our popup
316
315
  if (data.popup_id !== "csharp-lsp-help") {
317
316
  return;
@@ -339,10 +338,6 @@ function on_csharp_lsp_action_result(data: ActionPopupResultData) : void {
339
338
  default:
340
339
  editor.debug(`csharp_support: Unknown action: ${data.action_id}`);
341
340
  }
342
- }
343
- registerHandler("on_csharp_lsp_action_result", on_csharp_lsp_action_result);
344
-
345
- // Register hook for action popup results
346
- editor.on("action_popup_result", "on_csharp_lsp_action_result");
341
+ });
347
342
 
348
343
  editor.debug("csharp_support: Plugin loaded");
@@ -46,7 +46,10 @@ let cssLspError: { serverCommand: string; message: string } | null = null;
46
46
  /**
47
47
  * Handle LSP server errors for CSS
48
48
  */
49
- function on_css_lsp_server_error(data: LspServerErrorData) : void {
49
+
50
+
51
+ // Register hook for LSP server errors
52
+ editor.on("lsp_server_error", (data) => {
50
53
  // Only handle CSS language errors
51
54
  if (data.language !== "css") {
52
55
  return;
@@ -68,18 +71,15 @@ function on_css_lsp_server_error(data: LspServerErrorData) : void {
68
71
  } else {
69
72
  editor.setStatus(`CSS LSP error: ${data.message}`);
70
73
  }
71
- }
72
- registerHandler("on_css_lsp_server_error", on_css_lsp_server_error);
73
-
74
- // Register hook for LSP server errors
75
- editor.on("lsp_server_error", "on_css_lsp_server_error");
74
+ });
76
75
 
77
76
  /**
78
77
  * Handle status bar click when there's a CSS LSP error
79
78
  */
80
- function on_css_lsp_status_clicked(
81
- data: LspStatusClickedData
82
- ): void {
79
+
80
+
81
+ // Register hook for status bar clicks
82
+ editor.on("lsp_status_clicked", (data) => {
83
83
  // Only handle CSS language clicks when there's an error
84
84
  if (data.language !== "css" || !cssLspError) {
85
85
  return;
@@ -98,18 +98,15 @@ function on_css_lsp_status_clicked(
98
98
  { id: "dismiss", label: "Dismiss (ESC)" },
99
99
  ],
100
100
  });
101
- }
102
- registerHandler("on_css_lsp_status_clicked", on_css_lsp_status_clicked);
103
-
104
- // Register hook for status bar clicks
105
- editor.on("lsp_status_clicked", "on_css_lsp_status_clicked");
101
+ });
106
102
 
107
103
  /**
108
104
  * Handle action popup results for CSS LSP help
109
105
  */
110
- function on_css_lsp_action_result(
111
- data: ActionPopupResultData
112
- ): void {
106
+
107
+
108
+ // Register hook for action popup results
109
+ editor.on("action_popup_result", (data) => {
113
110
  // Only handle our popup
114
111
  if (data.popup_id !== "css-lsp-help") {
115
112
  return;
@@ -137,10 +134,6 @@ function on_css_lsp_action_result(
137
134
  default:
138
135
  editor.debug(`css-lsp: Unknown action: ${data.action_id}`);
139
136
  }
140
- }
141
- registerHandler("on_css_lsp_action_result", on_css_lsp_action_result);
142
-
143
- // Register hook for action popup results
144
- editor.on("action_popup_result", "on_css_lsp_action_result");
137
+ });
145
138
 
146
139
  editor.debug("css-lsp: Plugin loaded");
@@ -49,7 +49,8 @@ let dartLspError: { serverCommand: string; message: string } | null = null;
49
49
  /**
50
50
  * Handle LSP server errors for Dart
51
51
  */
52
- function on_dart_lsp_server_error(data: LspServerErrorData): void {
52
+
53
+ editor.on("lsp_server_error", (data) => {
53
54
  if (data.language !== "dart") {
54
55
  return;
55
56
  }
@@ -68,14 +69,13 @@ function on_dart_lsp_server_error(data: LspServerErrorData): void {
68
69
  } else {
69
70
  editor.setStatus(`Dart LSP error: ${data.message}`);
70
71
  }
71
- }
72
- registerHandler("on_dart_lsp_server_error", on_dart_lsp_server_error);
73
- editor.on("lsp_server_error", "on_dart_lsp_server_error");
72
+ });
74
73
 
75
74
  /**
76
75
  * Handle status bar click when there's a Dart LSP error
77
76
  */
78
- function on_dart_lsp_status_clicked(data: LspStatusClickedData): void {
77
+
78
+ editor.on("lsp_status_clicked", (data) => {
79
79
  if (data.language !== "dart" || !dartLspError) {
80
80
  return;
81
81
  }
@@ -94,14 +94,13 @@ function on_dart_lsp_status_clicked(data: LspStatusClickedData): void {
94
94
  { id: "dismiss", label: "Dismiss (ESC)" },
95
95
  ],
96
96
  });
97
- }
98
- registerHandler("on_dart_lsp_status_clicked", on_dart_lsp_status_clicked);
99
- editor.on("lsp_status_clicked", "on_dart_lsp_status_clicked");
97
+ });
100
98
 
101
99
  /**
102
100
  * Handle action popup results for Dart LSP help
103
101
  */
104
- function on_dart_lsp_action_result(data: ActionPopupResultData): void {
102
+
103
+ editor.on("action_popup_result", (data) => {
105
104
  if (data.popup_id !== "dart-lsp-help") {
106
105
  return;
107
106
  }
@@ -137,8 +136,6 @@ function on_dart_lsp_action_result(data: ActionPopupResultData): void {
137
136
  default:
138
137
  editor.debug(`dart-lsp: Unknown action: ${data.action_id}`);
139
138
  }
140
- }
141
- registerHandler("on_dart_lsp_action_result", on_dart_lsp_action_result);
142
- editor.on("action_popup_result", "on_dart_lsp_action_result");
139
+ });
143
140
 
144
141
  editor.debug("dart-lsp: Plugin loaded");
@@ -201,6 +201,55 @@ type RegisteredSection = {
201
201
  let dashboardBufferId: number | null = null;
202
202
  let fetchToken = 0; // bumped each open; late fetches from a prior open no-op.
203
203
 
204
+ // Id of the in-flight slide-in, so we can cancel it when starting a
205
+ // new one (on content change) or when the dashboard is closed
206
+ // mid-slide. Null once the animation settles or is cleared.
207
+ let activeAnimationId: number | null = null;
208
+
209
+ // Hash of all entries at the last paint (post-focus-highlight too —
210
+ // it's what ultimately lands in the virtual buffer). Used to decide
211
+ // whether setVirtualBufferContent needs to run at all: identical
212
+ // full hash AND identical focus → nothing changed, skip the VB
213
+ // round-trip entirely.
214
+ let lastPaintedFullKey: string | null = null;
215
+
216
+ // Hash of the entries with the clock stamp stripped. Animations only
217
+ // fire when THIS hash changes, so the 1 Hz clock tick on the top
218
+ // frame updates in place without re-sliding the whole dashboard.
219
+ // Keyboard focus changes don't move this hash either (the hash is
220
+ // taken before the focus overlay is laid on top), so Tab/Shift-Tab
221
+ // pan the highlight without re-animating.
222
+ let lastPaintedStructuralKey: string | null = null;
223
+
224
+ // focusedIndex the last successful setVirtualBufferContent ran with.
225
+ // Paired with the keys above so we can tell "focus moved but section
226
+ // data is the same" (update VB for the highlight, no animation).
227
+ let lastPaintedFocusedIndex = -1;
228
+
229
+ // Matches an HH:MM:SS clock stamp. Anything shaped like that is
230
+ // stripped from the structural hash so clock ticks don't animate.
231
+ // The frame renderer is the only dashboard author that emits such a
232
+ // string; if a third-party section happens to show a value in the
233
+ // same shape, the worst case is "we don't re-animate when that
234
+ // value changes" — acceptable noise floor.
235
+ const CLOCK_RE = /\d\d:\d\d:\d\d/g;
236
+
237
+ // Edge the slide-in enters from. Maps 1:1 to the plugin API's `from`
238
+ // field and is resolved from config (plugins.dashboard.slide_from) on
239
+ // each paint() so hot-reload of the setting Just Works. Defaults to
240
+ // "right" (new content pushes in from the right, old exits left).
241
+ type SlideFrom = "top" | "bottom" | "left" | "right";
242
+ function resolveSlideFrom(): SlideFrom {
243
+ const config = editor.getConfig() as Record<string, unknown> | null;
244
+ const plugins = config?.plugins as Record<string, unknown> | undefined;
245
+ const dashCfg = plugins?.dashboard as Record<string, unknown> | undefined;
246
+ const raw = dashCfg?.slide_from;
247
+ if (raw === "top" || raw === "bottom" || raw === "left" || raw === "right") {
248
+ return raw;
249
+ }
250
+ return "right";
251
+ }
252
+
204
253
  // Registered sections, in render order. Built-ins are registered at
205
254
  // plugin load (see the bottom of this file); third-party plugins
206
255
  // append via the exported `registerSection` API.
@@ -726,6 +775,36 @@ function paint(dims?: { width: number; height: number }) {
726
775
  ((focusedIndex % targets.length) + targets.length) % targets.length;
727
776
  }
728
777
 
778
+ // Two hashes, taken BEFORE the focus highlight goes on top:
779
+ // fullKey — everything including the clock. Drives the
780
+ // setVirtualBufferContent skip check, so the
781
+ // clock still redraws in place every second.
782
+ // structuralKey — clock stamps stripped. Drives the animation.
783
+ // A clock tick alone does not flip this, so it
784
+ // updates silently; a real section data change
785
+ // does, and the slide fires.
786
+ const fullKey = JSON.stringify(entries);
787
+ const structuralKey = fullKey.replace(CLOCK_RE, "##:##:##");
788
+ const fullChanged = fullKey !== lastPaintedFullKey;
789
+ const structuralChanged = structuralKey !== lastPaintedStructuralKey;
790
+ const focusChanged = focusedIndex !== lastPaintedFocusedIndex;
791
+ // A resize / viewport-shape change reshapes frame padding, dash
792
+ // runs, and centering, which flips the structural hash even when
793
+ // section data is unchanged. We repaint so the new layout takes
794
+ // effect but skip the slide — nothing NEW showed up, the user is
795
+ // just resizing a window. openDashboard clears lastPaintedW/H to
796
+ // -1 so the first paint after open doesn't trip this guard.
797
+ const dimsChanged =
798
+ lastPaintedW !== -1 &&
799
+ lastPaintedH !== -1 &&
800
+ (width !== lastPaintedW || height !== lastPaintedH);
801
+
802
+ // Identical render → short-circuit. Nothing to push to the
803
+ // buffer, nothing to animate.
804
+ if (!fullChanged && !focusChanged) {
805
+ return;
806
+ }
807
+
729
808
  // Paint the focus highlight by mutating the entry for the focused
730
809
  // row: translate its visual col range into a byte range and push an
731
810
  // inline overlay on top of whatever foreground/underline spans the
@@ -758,6 +837,26 @@ function paint(dims?: { width: number; height: number }) {
758
837
  editor.setVirtualBufferContent(bufferId, entries);
759
838
  lastPaintedW = width;
760
839
  lastPaintedH = height;
840
+ lastPaintedFullKey = fullKey;
841
+ lastPaintedStructuralKey = structuralKey;
842
+ lastPaintedFocusedIndex = focusedIndex;
843
+
844
+ // Structural-change-driven re-animation: fire only when the
845
+ // section payload actually differs AND the dashboard isn't just
846
+ // reshaping in place (clock tick, focus move, and resize all
847
+ // land here without animating). Cancel any in-flight slide
848
+ // first so the new one snapshots the fresh content.
849
+ if (structuralChanged && !dimsChanged) {
850
+ if (activeAnimationId !== null) {
851
+ editor.cancelAnimation(activeAnimationId);
852
+ }
853
+ activeAnimationId = editor.animateVirtualBuffer(bufferId, {
854
+ kind: "slideIn",
855
+ from: resolveSlideFrom(),
856
+ durationMs: 520,
857
+ delayMs: 0,
858
+ });
859
+ }
761
860
  }
762
861
 
763
862
  // Open a URL in the user's browser via the platform's "open" helper.
@@ -1513,6 +1612,17 @@ async function openDashboard() {
1513
1612
  }
1514
1613
  }
1515
1614
 
1615
+ // Clear the content/focus keys and dims so the first paint after
1616
+ // open is treated as a content change and the slide-in fires.
1617
+ // Dim reset is needed because open is the one case where we DO
1618
+ // want the animation despite "dims changed" (there was no prior
1619
+ // dimension, so the change is really "buffer just appeared").
1620
+ lastPaintedFullKey = null;
1621
+ lastPaintedStructuralKey = null;
1622
+ lastPaintedFocusedIndex = -1;
1623
+ lastPaintedW = -1;
1624
+ lastPaintedH = -1;
1625
+
1516
1626
  bootstrapDashboard(dashboardBufferId);
1517
1627
  }
1518
1628
 
@@ -1584,6 +1694,10 @@ registerHandler(
1584
1694
  // If the dashboard itself was closed, clear our handle so we'll
1585
1695
  // re-open on the next "last tab closed" event.
1586
1696
  if (dashboardBufferId !== null && e.buffer_id === dashboardBufferId) {
1697
+ if (activeAnimationId !== null) {
1698
+ editor.cancelAnimation(activeAnimationId);
1699
+ activeAnimationId = null;
1700
+ }
1587
1701
  dashboardBufferId = null;
1588
1702
  return;
1589
1703
  }
@@ -1603,6 +1717,10 @@ registerHandler(
1603
1717
  "dashboardOnAfterFileOpen",
1604
1718
  (_e: { buffer_id: number; path: string }) => {
1605
1719
  if (dashboardBufferId === null) return;
1720
+ if (activeAnimationId !== null) {
1721
+ editor.cancelAnimation(activeAnimationId);
1722
+ activeAnimationId = null;
1723
+ }
1606
1724
  editor.closeBuffer(dashboardBufferId);
1607
1725
  dashboardBufferId = null;
1608
1726
  },