xtralab 0.15.0 → 0.15.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 (228) hide show
  1. package/README.md +10 -0
  2. package/lib/about/icons.d.ts +0 -5
  3. package/lib/about/icons.js +0 -5
  4. package/lib/about/index.d.ts +4 -8
  5. package/lib/about/index.js +6 -17
  6. package/lib/agentSessions.d.ts +9 -29
  7. package/lib/agentSessions.js +2 -7
  8. package/lib/askAgent/editorSelection.d.ts +20 -26
  9. package/lib/askAgent/editorSelection.js +11 -26
  10. package/lib/askAgent/icons.d.ts +2 -3
  11. package/lib/askAgent/icons.js +2 -3
  12. package/lib/askAgent/index.d.ts +4 -26
  13. package/lib/askAgent/index.js +54 -158
  14. package/lib/askAgent/popup.d.ts +12 -16
  15. package/lib/askAgent/popup.js +18 -37
  16. package/lib/askAgent/prompt.d.ts +3 -5
  17. package/lib/askAgent/prompt.js +6 -19
  18. package/lib/askAgent/queue.d.ts +11 -21
  19. package/lib/askAgent/queue.js +9 -19
  20. package/lib/askAgent/queuePanel.d.ts +13 -18
  21. package/lib/askAgent/queuePanel.js +15 -37
  22. package/lib/askAgent/targetPicker.d.ts +2 -9
  23. package/lib/askAgent/targetPicker.js +6 -11
  24. package/lib/askAgent/tokens.d.ts +32 -46
  25. package/lib/askAgent/tokens.js +4 -6
  26. package/lib/commandBar/index.d.ts +4 -13
  27. package/lib/commandBar/index.js +21 -39
  28. package/lib/customPanel/index.js +15 -0
  29. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  30. package/lib/editorBreadcrumbs/index.js +10 -6
  31. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  32. package/lib/editorBreadcrumbs/widget.js +11 -17
  33. package/lib/editorIndent/index.d.ts +4 -12
  34. package/lib/editorIndent/index.js +8 -23
  35. package/lib/fileBrowser/commands.d.ts +28 -19
  36. package/lib/fileBrowser/commands.js +39 -98
  37. package/lib/fileBrowser/contents.d.ts +3 -0
  38. package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
  39. package/lib/fileBrowser/dragAndDrop.js +2 -7
  40. package/lib/fileBrowser/fileBrowser.d.ts +19 -9
  41. package/lib/fileBrowser/fileBrowser.js +68 -216
  42. package/lib/fileBrowser/gitStatus.d.ts +4 -9
  43. package/lib/fileBrowser/gitStatus.js +7 -17
  44. package/lib/fileBrowser/gitignore.d.ts +4 -17
  45. package/lib/fileBrowser/gitignore.js +4 -25
  46. package/lib/fileBrowser/icons.d.ts +7 -17
  47. package/lib/fileBrowser/icons.js +25 -80
  48. package/lib/fileBrowser/index.js +2 -4
  49. package/lib/fileBrowser/widget.d.ts +126 -48
  50. package/lib/fileBrowser/widget.js +89 -4
  51. package/lib/fileTypeIcons/index.js +0 -10
  52. package/lib/git/api.d.ts +5 -13
  53. package/lib/git/api.js +10 -36
  54. package/lib/git/askRequest.d.ts +4 -8
  55. package/lib/git/askRequest.js +6 -20
  56. package/lib/git/commands.d.ts +40 -5
  57. package/lib/git/commands.js +16 -10
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffSurface.d.ts +41 -55
  60. package/lib/git/diffSurface.js +42 -117
  61. package/lib/git/diffWidget.d.ts +81 -2
  62. package/lib/git/diffWidget.js +62 -12
  63. package/lib/git/imageDiff.d.ts +5 -6
  64. package/lib/git/imageDiff.js +18 -48
  65. package/lib/git/index.d.ts +0 -3
  66. package/lib/git/index.js +3 -8
  67. package/lib/git/notebookDiff.d.ts +110 -33
  68. package/lib/git/notebookDiff.js +52 -195
  69. package/lib/git/tokens.d.ts +73 -31
  70. package/lib/git/tokens.js +2 -3
  71. package/lib/highlight/index.d.ts +4 -10
  72. package/lib/highlight/index.js +10 -41
  73. package/lib/index.d.ts +0 -9
  74. package/lib/index.js +0 -9
  75. package/lib/launcher/agents.d.ts +55 -51
  76. package/lib/launcher/agents.js +15 -57
  77. package/lib/launcher/availability.d.ts +3 -9
  78. package/lib/launcher/availability.js +3 -9
  79. package/lib/launcher/commands.d.ts +7 -32
  80. package/lib/launcher/commands.js +14 -45
  81. package/lib/launcher/dashboard.d.ts +19 -24
  82. package/lib/launcher/dashboard.js +21 -67
  83. package/lib/launcher/editorRegistry.d.ts +10 -22
  84. package/lib/launcher/editorRegistry.js +14 -18
  85. package/lib/launcher/editors.d.ts +39 -45
  86. package/lib/launcher/editors.js +8 -34
  87. package/lib/launcher/icons.d.ts +0 -12
  88. package/lib/launcher/icons.js +5 -39
  89. package/lib/launcher/index.js +12 -55
  90. package/lib/launcher/invocation.d.ts +4 -10
  91. package/lib/launcher/invocation.js +7 -22
  92. package/lib/launcher/registry.d.ts +9 -7
  93. package/lib/launcher/registry.js +9 -7
  94. package/lib/launcher/tokens.d.ts +10 -22
  95. package/lib/launcher/tokens.js +5 -8
  96. package/lib/menuBar/index.d.ts +3 -10
  97. package/lib/menuBar/index.js +11 -28
  98. package/lib/menus/index.d.ts +3 -9
  99. package/lib/menus/index.js +14 -30
  100. package/lib/omnibox/files.d.ts +2 -4
  101. package/lib/omnibox/files.js +5 -11
  102. package/lib/omnibox/index.d.ts +4 -14
  103. package/lib/omnibox/index.js +9 -27
  104. package/lib/omnibox/model.d.ts +38 -10
  105. package/lib/omnibox/model.js +15 -33
  106. package/lib/omnibox/recents.d.ts +15 -17
  107. package/lib/omnibox/recents.js +12 -17
  108. package/lib/omnibox/tokens.d.ts +5 -10
  109. package/lib/omnibox/tokens.js +2 -6
  110. package/lib/omnibox/widget.d.ts +11 -7
  111. package/lib/omnibox/widget.js +8 -16
  112. package/lib/searchReplace/index.d.ts +4 -20
  113. package/lib/searchReplace/index.js +7 -27
  114. package/lib/showOutput/index.d.ts +3 -12
  115. package/lib/showOutput/index.js +6 -22
  116. package/lib/sidebar/index.d.ts +4 -7
  117. package/lib/sidebar/index.js +5 -12
  118. package/lib/terminalNotifications/index.d.ts +4 -10
  119. package/lib/terminalNotifications/index.js +6 -20
  120. package/lib/terminals/agentTerminals.d.ts +12 -5
  121. package/lib/terminals/agentTerminals.js +25 -44
  122. package/lib/terminals/detection.d.ts +4 -8
  123. package/lib/terminals/detection.js +4 -8
  124. package/lib/terminals/index.d.ts +0 -35
  125. package/lib/terminals/index.js +12 -101
  126. package/lib/terminals/model.d.ts +64 -139
  127. package/lib/terminals/model.js +80 -220
  128. package/lib/terminals/tokens.d.ts +11 -22
  129. package/lib/terminals/tokens.js +2 -2
  130. package/lib/terminals/widget.d.ts +43 -13
  131. package/lib/terminals/widget.js +33 -18
  132. package/lib/topBar/icons.d.ts +3 -8
  133. package/lib/topBar/icons.js +3 -8
  134. package/lib/topBar/index.d.ts +4 -11
  135. package/lib/topBar/index.js +10 -38
  136. package/lib/walkthrough/index.d.ts +4 -9
  137. package/lib/walkthrough/index.js +6 -20
  138. package/lib/walkthrough/panel.d.ts +40 -9
  139. package/lib/walkthrough/panel.js +4 -10
  140. package/package.json +2 -2
  141. package/src/about/icons.ts +0 -5
  142. package/src/about/index.tsx +6 -17
  143. package/src/agentSessions.ts +9 -29
  144. package/src/askAgent/editorSelection.ts +22 -29
  145. package/src/askAgent/icons.ts +2 -3
  146. package/src/askAgent/index.ts +54 -158
  147. package/src/askAgent/popup.tsx +23 -45
  148. package/src/askAgent/prompt.ts +6 -19
  149. package/src/askAgent/queue.ts +13 -24
  150. package/src/askAgent/queuePanel.tsx +21 -46
  151. package/src/askAgent/targetPicker.tsx +5 -15
  152. package/src/askAgent/tokens.ts +32 -46
  153. package/src/commandBar/index.ts +24 -39
  154. package/src/customPanel/index.ts +15 -0
  155. package/src/editorBreadcrumbs/index.ts +10 -6
  156. package/src/editorBreadcrumbs/widget.ts +23 -17
  157. package/src/editorIndent/index.ts +17 -23
  158. package/src/fileBrowser/commands.ts +57 -98
  159. package/src/fileBrowser/contents.ts +3 -0
  160. package/src/fileBrowser/dragAndDrop.ts +17 -7
  161. package/src/fileBrowser/fileBrowser.tsx +84 -223
  162. package/src/fileBrowser/gitStatus.ts +7 -17
  163. package/src/fileBrowser/gitignore.ts +4 -25
  164. package/src/fileBrowser/icons.ts +25 -80
  165. package/src/fileBrowser/index.ts +2 -4
  166. package/src/fileBrowser/widget.tsx +149 -50
  167. package/src/fileTypeIcons/index.ts +0 -10
  168. package/src/git/api.ts +10 -36
  169. package/src/git/askRequest.ts +6 -20
  170. package/src/git/commands.ts +70 -10
  171. package/src/git/diffModel.ts +25 -0
  172. package/src/git/diffSurface.tsx +71 -159
  173. package/src/git/diffWidget.tsx +104 -14
  174. package/src/git/imageDiff.tsx +45 -51
  175. package/src/git/index.ts +3 -8
  176. package/src/git/notebookDiff.tsx +211 -238
  177. package/src/git/tokens.ts +73 -31
  178. package/src/highlight/index.ts +10 -41
  179. package/src/index.ts +0 -9
  180. package/src/launcher/agents.ts +64 -95
  181. package/src/launcher/availability.ts +3 -9
  182. package/src/launcher/commands.ts +14 -51
  183. package/src/launcher/dashboard.tsx +49 -88
  184. package/src/launcher/editorRegistry.ts +21 -29
  185. package/src/launcher/editors.ts +39 -58
  186. package/src/launcher/icons.ts +5 -39
  187. package/src/launcher/index.ts +12 -55
  188. package/src/launcher/invocation.ts +7 -22
  189. package/src/launcher/registry.ts +9 -7
  190. package/src/launcher/tokens.ts +10 -22
  191. package/src/menuBar/index.ts +11 -28
  192. package/src/menus/index.ts +14 -30
  193. package/src/omnibox/files.ts +10 -14
  194. package/src/omnibox/index.ts +9 -27
  195. package/src/omnibox/model.ts +50 -38
  196. package/src/omnibox/recents.ts +17 -22
  197. package/src/omnibox/tokens.ts +5 -10
  198. package/src/omnibox/widget.tsx +14 -19
  199. package/src/searchReplace/index.ts +7 -27
  200. package/src/showOutput/index.ts +6 -22
  201. package/src/sidebar/index.ts +20 -12
  202. package/src/terminalNotifications/index.ts +42 -25
  203. package/src/terminals/agentTerminals.ts +25 -49
  204. package/src/terminals/detection.ts +4 -8
  205. package/src/terminals/index.ts +12 -101
  206. package/src/terminals/model.ts +130 -246
  207. package/src/terminals/tokens.ts +11 -22
  208. package/src/terminals/widget.tsx +48 -31
  209. package/src/topBar/icons.ts +3 -8
  210. package/src/topBar/index.ts +21 -44
  211. package/src/walkthrough/index.ts +6 -20
  212. package/src/walkthrough/panel.ts +40 -10
  213. package/style/about.css +2 -7
  214. package/style/askAgent.css +8 -40
  215. package/style/base.css +9 -45
  216. package/style/chrome.css +108 -308
  217. package/style/commandBar.css +4 -20
  218. package/style/customPanel.css +1 -4
  219. package/style/git.css +22 -184
  220. package/style/highlight.css +1 -3
  221. package/style/launcher.css +6 -70
  222. package/style/omnibox.css +2 -14
  223. package/style/showOutput.css +0 -6
  224. package/style/sidebar.css +29 -9
  225. package/style/tabs.css +5 -13
  226. package/style/terminals.css +6 -53
  227. package/style/topBar.css +2 -22
  228. package/style/walkthrough.css +0 -8
@@ -10,55 +10,28 @@ import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
10
10
  import { loadGitStatusEntries } from './gitStatus';
11
11
  import { FILE_BROWSER_ICONS } from './icons';
12
12
  /**
13
- * Polling cadence for the git status decoration. Out-of-band changes (a
14
- * terminal `git add`, a `git pull`, a file edited outside the JupyterLab
15
- * editor) become visible within this interval without a manual refresh.
16
- * Aligned with the git panel's own polling so both views update on the
17
- * same rhythm.
13
+ * Git status poll cadence, aligned with the git panel's polling so both
14
+ * views update on the same rhythm.
18
15
  */
19
16
  const GIT_STATUS_POLL_INTERVAL_MS = 5000;
20
- /**
21
- * Upper bound on the git status poll's exponential backoff. Matches the
22
- * other polls in the plugin so behavior is consistent across views.
23
- */
24
17
  const GIT_STATUS_POLL_MAX_MS = 300000;
25
18
  /**
26
- * Auto-refresh cadence for the file listing itself. Matches the default
27
- * JupyterLab file browser (`DEFAULT_REFRESH_INTERVAL` in
28
- * `@jupyterlab/filebrowser`) so the tree picks up files created outside
29
- * JupyterLab (terminal commands, external editors, `git pull`, …) within
30
- * the same window as the stock browser.
19
+ * Listing auto-refresh cadence; matches the default file browser's
20
+ * `DEFAULT_REFRESH_INTERVAL` for picking up out-of-band file changes.
31
21
  */
32
22
  const FILE_LISTING_REFRESH_INTERVAL_MS = 10000;
33
- /**
34
- * Upper bound on the auto-refresh backoff when polls fail repeatedly.
35
- * Matches the default file browser's `max: 300 * 1000` — five minutes is
36
- * long enough that a server-side outage stops hammering the API, but
37
- * short enough that a transient failure heals on its own.
38
- */
39
23
  const FILE_LISTING_REFRESH_MAX_MS = 300000;
40
24
  /**
41
- * Server-relative repository path used for `/git/*` calls. Empty string
42
- * means "use the JupyterLab server's root and let git resolve the
43
- * enclosing repo" — same convention as the git panel and the launcher
44
- * dashboard.
25
+ * Repo path for `/git/*` calls; empty means the server root, letting git
26
+ * resolve the enclosing repo (same convention as the git panel).
45
27
  */
46
28
  const GIT_REPO_PATH = '';
47
- /**
48
- * The custom-element tag used by `@pierre/trees` for its shadow host. Kept
49
- * as a constant rather than imported so we don't pay the `@pierre/trees`
50
- * resolution cost just for one string.
51
- */
52
29
  const FILE_TREE_TAG = 'file-tree-container';
53
30
  /**
54
- * Injected into the tree's shadow root. The second rule hides the search
55
- * box unless the host carries the visibility marker set by the filter
56
- * bridge effect below — `@pierre/trees` always renders the box when
57
- * `search` is enabled, and its stylesheet lives in the shadow root where
58
- * outside CSS cannot reach. The third rule restyles the drag-hover row:
59
- * the library paints it with the selection background, which xtralab
60
- * maps to a full-strength brand color that is illegible without the
61
- * inverted selection foreground — hence the quieter ring.
31
+ * Injected into the tree's shadow root, where outside CSS cannot reach.
32
+ * The search box is hidden unless the host carries the filter-bridge marker
33
+ * (the library always renders it), and the drag-hover row gets a quiet ring
34
+ * — the library's selection background is illegible with xtralab's colors.
62
35
  */
63
36
  const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
64
37
  '[data-item-section="spacing-item"] {' +
@@ -73,13 +46,9 @@ const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
73
46
  'box-shadow: inset 0 0 0 2px var(--trees-accent);' +
74
47
  '}';
75
48
  /**
76
- * Renders a `@pierre/trees` file tree backed by the Jupyter contents API.
77
- *
78
- * The Jupyter contents API only returns one directory level per request, so
79
- * the tree is populated lazily: the root is fetched on mount, and each
80
- * directory is fetched the first time the user expands it. Expansion is
81
- * detected by subscribing to the model and diffing against an in-memory load
82
- * state map.
49
+ * A `@pierre/trees` file tree backed by the Jupyter contents API. The API
50
+ * returns one directory level per request, so directories load lazily on
51
+ * first expand, detected by diffing the model against a load-state map.
83
52
  */
84
53
  export function FileBrowserComponent(props) {
85
54
  const { contentsManager, docManager, onOpenFile, translator, widget } = props;
@@ -96,25 +65,16 @@ export function FileBrowserComponent(props) {
96
65
  });
97
66
  React.useEffect(() => {
98
67
  const knownDirs = new Map();
99
- // Mirror of the canonical paths currently loaded into the tree. Kept in
100
- // sync with `model.resetPaths`/`model.batch`/`model.add` so we can
101
- // re-test every loaded path against the gitignore matcher whenever
102
- // either side changes — the model itself does not expose a
103
- // path-iteration API.
68
+ // Mirror of the loaded canonical paths — the model has no path-iteration
69
+ // API, and the gitignore matcher must re-test every loaded path.
104
70
  const loadedPaths = new Set();
105
71
  let gitignoreMatcher = null;
106
72
  let gitStatusEntries = [];
107
73
  let cancelled = false;
108
74
  /**
109
- * Recompute the combined `GitStatusEntry` list from the current
110
- * gitignore matcher and the latest porcelain status, then push it into
111
- * the tree. Safe to call at any time: empty inputs result in an empty
112
- * payload, which clears any statuses applied previously.
113
- *
114
- * Ignored entries go first so the porcelain entries win in the
115
- * unlikely event of overlap (a tracked path that also matches a
116
- * `.gitignore` rule). `@pierre/trees` lets later entries overwrite
117
- * earlier ones in its internal `statusByPath` map.
75
+ * Push the combined gitignore + porcelain entries into the tree.
76
+ * Ignored entries go first so porcelain entries win on overlap —
77
+ * `@pierre/trees` lets later entries overwrite earlier ones.
118
78
  */
119
79
  const syncGitStatus = () => {
120
80
  const entries = [];
@@ -128,12 +88,6 @@ export function FileBrowserComponent(props) {
128
88
  }
129
89
  model.setGitStatus(entries);
130
90
  };
131
- /**
132
- * Reload the workspace `.gitignore`, then re-apply the resulting
133
- * ignored statuses. Called on initial mount and whenever the user
134
- * triggers a refresh — the file may have been edited or created in
135
- * between.
136
- */
137
91
  const refreshGitignoreMatcher = async () => {
138
92
  let next = null;
139
93
  try {
@@ -148,12 +102,6 @@ export function FileBrowserComponent(props) {
148
102
  gitignoreMatcher = next;
149
103
  syncGitStatus();
150
104
  };
151
- /**
152
- * Refresh the porcelain-derived git status entries and re-apply them
153
- * to the tree. Runs on mount, on every refresh, and on a periodic
154
- * poll so out-of-band changes (terminal `git add`, file edits saved
155
- * outside the editor, …) become visible without explicit user action.
156
- */
157
105
  const refreshGitStatus = async () => {
158
106
  const next = await loadGitStatusEntries(GIT_REPO_PATH);
159
107
  if (cancelled) {
@@ -182,21 +130,16 @@ export function FileBrowserComponent(props) {
182
130
  }
183
131
  }
184
132
  else {
185
- // Filter out paths already in the model. The path-store throws
186
- // when an explicit directory is added a second time, so any
187
- // entry created out-of-band (e.g. by the "new folder" command's
188
- // `notifyPathAdded` callback) must be skipped here.
133
+ // The path-store throws when a path is added twice, and entries
134
+ // can arrive out-of-band (e.g. `notifyPathAdded`), so skip those.
189
135
  const operations = paths
190
136
  .filter(path => model.getItem(path) === null)
191
137
  .map(path => ({ type: 'add', path }));
192
138
  if (operations.length > 0) {
193
139
  model.batch(operations);
194
140
  }
195
- // Mirror every directory child into `loadedPaths` regardless of
196
- // whether it was just added or skipped above — paths skipped by
197
- // the filter are already in the model from a prior add and so
198
- // belong in the mirror too. `Set.add` is idempotent, so the
199
- // duplicate adds are a no-op.
141
+ // Paths skipped above are already in the model from a prior add,
142
+ // so they belong in the mirror too.
200
143
  for (const path of paths) {
201
144
  loadedPaths.add(path);
202
145
  }
@@ -215,10 +158,8 @@ export function FileBrowserComponent(props) {
215
158
  }
216
159
  };
217
160
  /**
218
- * Refresh every directory currently loaded into the tree. Walks down
219
- * from the root through the previously-expanded subdirectories so the
220
- * tree state mirrors what's on disk while preserving the user's
221
- * expansion state.
161
+ * Re-fetch the root and every previously-expanded directory so the tree
162
+ * mirrors the disk while preserving the user's expansion state.
222
163
  */
223
164
  const refreshAll = async () => {
224
165
  const expandedPaths = new Set();
@@ -251,9 +192,8 @@ export function FileBrowserComponent(props) {
251
192
  return;
252
193
  }
253
194
  const allPaths = [...rootPaths];
254
- // BFS through the previously-expanded subtree, fetching only the
255
- // directories the user had opened so the refresh doesn't walk the
256
- // entire workspace.
195
+ // Fetch only the previously-expanded subtree so the refresh doesn't
196
+ // walk the entire workspace.
257
197
  const subdirsByParent = new Map();
258
198
  subdirsByParent.set(ROOT_LOAD_KEY, rootSubdirs);
259
199
  const queue = rootSubdirs.filter(s => expandedPaths.has(s));
@@ -277,8 +217,6 @@ export function FileBrowserComponent(props) {
277
217
  console.error(`xtralab: refresh failed for "${dir}"`, err);
278
218
  }
279
219
  }
280
- // Reset the tree contents and restore the load-state map so future
281
- // expansions know which directories still need fetching.
282
220
  model.resetPaths(allPaths);
283
221
  loadedPaths.clear();
284
222
  for (const path of allPaths) {
@@ -290,18 +228,10 @@ export function FileBrowserComponent(props) {
290
228
  knownDirs.set(subdir, expandedPaths.has(subdir) ? 'loaded' : 'unloaded');
291
229
  }
292
230
  });
293
- // Refresh the `.gitignore` matcher in case the file was edited
294
- // since the last load, then re-apply the resulting statuses to the
295
- // newly-loaded paths. The reload runs in parallel with the rest of
296
- // the refresh — `refreshGitignoreMatcher` calls `syncGitStatus`
297
- // itself when it completes. The porcelain status is also re-fetched
298
- // here so a user-triggered refresh picks up out-of-band git changes
299
- // immediately instead of waiting for the next poll tick.
231
+ // Each re-applies the statuses itself when it completes.
300
232
  void refreshGitignoreMatcher();
301
233
  void refreshGitStatus();
302
- // Re-expand the directories that were expanded before the refresh.
303
- // We have to do this after `resetPaths` because the reset starts
304
- // every directory in its initial collapsed state.
234
+ // `resetPaths` starts every directory collapsed, so re-expand after it.
305
235
  for (const path of expandedPaths) {
306
236
  const item = model.getItem(path);
307
237
  if (item !== null && item.isDirectory()) {
@@ -310,20 +240,10 @@ export function FileBrowserComponent(props) {
310
240
  }
311
241
  };
312
242
  /**
313
- * Auto-refresh tick: walk every directory currently loaded into the
314
- * tree, fetch its children, and apply the per-directory diff as a
315
- * single batched mutation. Unlike {@link refreshAll} this never calls
316
- * `model.resetPaths`, so the user's expansion, selection, and scroll
317
- * state survive every poll. Mirrors what the default JupyterLab file
318
- * browser does for its single-directory view.
319
- *
320
- * A failed fetch for a single directory is treated as transient and
321
- * is skipped without touching that directory's children — they may
322
- * still be valid even if this one fetch lost the race with a server
323
- * restart. A deleted directory eventually surfaces through its
324
- * parent's diff: when the parent is re-fetched and no longer lists
325
- * the missing child, the child is removed recursively from the tree
326
- * and from the load-state tracking maps.
243
+ * Auto-refresh tick: diff every loaded directory and apply batched
244
+ * mutations without `resetPaths`, so expansion, selection, and scroll
245
+ * state survive. A failed fetch is skipped as transient; a deleted
246
+ * directory surfaces through its parent's diff and is removed recursively.
327
247
  */
328
248
  const quietRefresh = async () => {
329
249
  if (cancelled) {
@@ -343,10 +263,8 @@ export function FileBrowserComponent(props) {
343
263
  if (cancelled) {
344
264
  return;
345
265
  }
346
- // Skip directories that were removed from `knownDirs` while we
347
- // were processing an earlier sibling — the cascade cleanup below
348
- // can prune deep subtrees, so a path captured at the start of
349
- // the tick may already be gone.
266
+ // The cascade cleanup below can prune subtrees mid-tick, so a dir
267
+ // captured at the start may already be gone.
350
268
  if (knownDirs.get(dir) !== 'loaded') {
351
269
  continue;
352
270
  }
@@ -373,8 +291,6 @@ export function FileBrowserComponent(props) {
373
291
  if (newChildren.has(lp)) {
374
292
  continue;
375
293
  }
376
- // Disappeared since the last tick. Remove recursively so any
377
- // descendants that were also being tracked go with it.
378
294
  ops.push({ type: 'remove', path: lp, recursive: true });
379
295
  removals.push(lp);
380
296
  if (lp.endsWith('/')) {
@@ -394,15 +310,12 @@ export function FileBrowserComponent(props) {
394
310
  }
395
311
  catch (err) {
396
312
  console.error(`xtralab: auto-refresh batch failed for "${dir}"`, err);
397
- // Leave the bookkeeping mirrors untouched so the next tick
398
- // sees the same starting state and tries again.
313
+ // Leave the mirrors untouched so the next tick retries.
399
314
  continue;
400
315
  }
401
316
  mutated = true;
402
- // Apply the matching mirror updates only after the batch lands
403
- // in the model. The cascade cleanup below mirrors the
404
- // `recursive: true` removal semantics so descendants we were
405
- // tracking don't linger in `loadedPaths` or `knownDirs`.
317
+ // Mirror updates only after the batch lands; the cascade below
318
+ // mirrors the `recursive: true` removal semantics.
406
319
  for (const r of removals) {
407
320
  loadedPaths.delete(r);
408
321
  }
@@ -431,8 +344,7 @@ export function FileBrowserComponent(props) {
431
344
  loadedPaths.add(a);
432
345
  }
433
346
  }
434
- // Register newly-observed subdirectories so the next user expand
435
- // triggers a fetch instead of being ignored.
347
+ // Untracked subdirectories would ignore their first expand.
436
348
  for (const subdir of fetched.subdirectories) {
437
349
  if (!knownDirs.has(subdir)) {
438
350
  knownDirs.set(subdir, 'unloaded');
@@ -444,24 +356,8 @@ export function FileBrowserComponent(props) {
444
356
  }
445
357
  };
446
358
  /**
447
- * Reveal {@link canonicalPath} in the tree: load any unloaded
448
- * ancestor directories, expand them, and select the target so it is
449
- * scrolled into view. Tolerant of partially-loaded state so the
450
- * editor breadcrumbs can call it on any path at any time without
451
- * caring about what the tree has already fetched.
452
- *
453
- * Each ancestor is awaited *before* it is expanded — expanding a
454
- * directory triggers the model's subscribe callback, which
455
- * synchronously sets the directory's load state to `loading` and
456
- * kicks off its own fetch in parallel. Awaiting that in-flight
457
- * fetch would return immediately on the second await, leaving the
458
- * children unloaded when we move to the next iteration.
459
- */
460
- /**
461
- * Clear the tree selection and scroll back to the top. Invoked by
462
- * the file browser widget's `scrollToRoot` method when the home
463
- * crumb is clicked; gives that gesture visible feedback even when
464
- * the sidebar is already focused on the file browser.
359
+ * Clear the selection and scroll to the top; gives the home-crumb
360
+ * gesture visible feedback even when the sidebar is already focused.
465
361
  */
466
362
  const goToRoot = () => {
467
363
  var _a;
@@ -476,13 +372,6 @@ export function FileBrowserComponent(props) {
476
372
  container.scrollTop = 0;
477
373
  }
478
374
  };
479
- /**
480
- * Collapse every currently expanded directory in the tree. Walks the
481
- * `knownDirs` map (the canonical record of which directories have been
482
- * observed in the tree) and calls `.collapse()` on each loaded
483
- * directory whose handle reports `isExpanded()`. Unloaded directories
484
- * are not expanded by definition, so they're skipped.
485
- */
486
375
  const collapseAll = () => {
487
376
  if (cancelled) {
488
377
  return;
@@ -501,6 +390,12 @@ export function FileBrowserComponent(props) {
501
390
  }
502
391
  });
503
392
  };
393
+ /**
394
+ * Reveal `canonicalPath`: fetch and expand ancestors, then select and
395
+ * scroll to the target. Each ancestor is fetched *before* expanding —
396
+ * expansion starts the subscribe callback's own fetch, and awaiting that
397
+ * in-flight fetch resolves with the children still unloaded.
398
+ */
504
399
  const revealPath = async (canonicalPath) => {
505
400
  if (cancelled || canonicalPath.length === 0) {
506
401
  return;
@@ -511,9 +406,7 @@ export function FileBrowserComponent(props) {
511
406
  if (segments.length === 0) {
512
407
  return;
513
408
  }
514
- // Build the ordered list of ancestor directory canonical paths.
515
- // For "foo/bar/baz.txt" → ["foo/", "foo/bar/"].
516
- // For "foo/bar/" → ["foo/"].
409
+ // "foo/bar/baz.txt" → ["foo/", "foo/bar/"].
517
410
  const ancestors = [];
518
411
  let cumulative = '';
519
412
  for (let i = 0; i < segments.length - 1; i++) {
@@ -560,15 +453,13 @@ export function FileBrowserComponent(props) {
560
453
  previous === null || previous === void 0 ? void 0 : previous.deselect();
561
454
  }
562
455
  target.select();
563
- // `select` highlights the row; `scrollToPath` focuses it and scrolls it
564
- // to the middle of the viewport, even when the row is virtualized out of
565
- // the rendered window.
456
+ // `scrollToPath` focuses and scrolls even when the row is virtualized
457
+ // out of the rendered window.
566
458
  model.scrollToPath(canonicalPath, { focus: true, offset: 'center' });
567
459
  };
568
460
  /**
569
- * Insert a newly-created path (typically from "new folder" or
570
- * "duplicate") into the tree without doing a full refresh. Expands
571
- * the parent so the user sees the newly created entry immediately.
461
+ * Insert a newly-created path without a full refresh and expand its
462
+ * parent so the entry is visible immediately.
572
463
  */
573
464
  const handlePathAdded = (canonicalPath) => {
574
465
  if (model.getItem(canonicalPath) === null) {
@@ -582,9 +473,8 @@ export function FileBrowserComponent(props) {
582
473
  }
583
474
  loadedPaths.add(canonicalPath);
584
475
  if (canonicalPath.endsWith('/') && !knownDirs.has(canonicalPath)) {
585
- // The new directory has no children yet, so mark it as already
586
- // loaded — there's nothing to fetch and we don't want a stale
587
- // "unloaded" entry to trigger a fetch on the next expand.
476
+ // A new directory has no children; mark it loaded so a stale
477
+ // "unloaded" entry doesn't trigger a fetch on the next expand.
588
478
  knownDirs.set(canonicalPath, 'loaded');
589
479
  }
590
480
  syncGitStatus();
@@ -685,10 +575,6 @@ export function FileBrowserComponent(props) {
685
575
  }
686
576
  }
687
577
  };
688
- /**
689
- * The tree refused the drop and left its model untouched — almost
690
- * always a name collision at the destination.
691
- */
692
578
  const failedDrop = (error, context) => {
693
579
  if (cancelled) {
694
580
  return;
@@ -744,13 +630,8 @@ export function FileBrowserComponent(props) {
744
630
  },
745
631
  standby: 'when-hidden'
746
632
  });
747
- // Auto-refresh the file listing on the same cadence as the default
748
- // JupyterLab file browser. Backoff on failures so a server-side
749
- // outage doesn't hammer the API, and stand by when the tab is
750
- // hidden so we don't run the polling loop while the user is in
751
- // another tab. `auto: false` keeps the first tick from racing the
752
- // initial `fetchDirectory(ROOT_LOAD_KEY)` above — the poll is
753
- // started explicitly once the initial load is in flight.
633
+ // `auto: false` keeps the first tick from racing the initial root
634
+ // fetch above; the poll is started explicitly below.
754
635
  const listingPoll = new Poll({
755
636
  auto: false,
756
637
  name: '@xtralab/fileBrowser:listing',
@@ -763,26 +644,16 @@ export function FileBrowserComponent(props) {
763
644
  standby: 'when-hidden'
764
645
  });
765
646
  void listingPoll.start();
766
- // Surface contents changes that happen inside JupyterLab (file save,
767
- // rename, delete) without waiting for the next poll tick. The
768
- // default file browser uses the same `fileChanged` signal for the
769
- // same reason. We can't tell from the signal alone whether the
770
- // change affects a path we're showing, so we just nudge the poll —
771
- // it diffs the loaded directories and emits no batch ops when
772
- // nothing relevant changed.
647
+ // In-app contents changes (save, rename, delete) nudge the poll; the
648
+ // signal doesn't say whether a shown path is affected, the diff does.
773
649
  const onContentsFileChanged = () => {
774
650
  void listingPoll.refresh();
775
651
  };
776
652
  contentsManager.fileChanged.connect(onContentsFileChanged);
777
653
  const unsubscribe = model.subscribe(() => {
778
- // Search-driven expansion must not trigger fetches: the tree
779
- // auto-expands every directory whose path matches the query, and
780
- // treating those as user expansions would recursively fetch every
781
- // matching subtree — a single common letter can walk the whole
782
- // workspace, node_modules included. Closing the search restores
783
- // the pre-search expansion state, and a directory clicked in the
784
- // results is toggled after the session closes, so real expansions
785
- // are still fetched the moment the session ends.
654
+ // Search auto-expands every matching directory — fetching those would
655
+ // walk whole subtrees (node_modules included); real expansions still
656
+ // fetch once the search session closes.
786
657
  if (model.isSearchOpen()) {
787
658
  return;
788
659
  }
@@ -854,11 +725,8 @@ export function FileBrowserComponent(props) {
854
725
  }
855
726
  };
856
727
  }, [model, contentsManager, widget, docManager, trans]);
857
- // Bridge the tree's selection state up to the widget so command handlers
858
- // can read it without depending on React internals. The tree exposes its
859
- // selection through `getSelectedPaths()` and emits a generic notification
860
- // through `subscribe`, so we diff against a snapshot to avoid spamming the
861
- // widget on every unrelated mutation.
728
+ // Bridge the tree selection up to the widget. `subscribe` fires on every
729
+ // mutation, so diff against a snapshot before notifying.
862
730
  React.useEffect(() => {
863
731
  if (widget === undefined) {
864
732
  return;
@@ -879,13 +747,9 @@ export function FileBrowserComponent(props) {
879
747
  unsubscribe();
880
748
  };
881
749
  }, [model, widget]);
882
- // Bridge the filter-box visibility between the widget and the tree. The
883
- // widget owns the flag; applying it stamps the marker attribute the
884
- // unsafeCSS rule keys on and opens or closes the model's search session
885
- // (opening focuses the input, closing clears any active filter). The
886
- // model subscription covers the reverse direction: typing a printable
887
- // character while the tree has focus opens a session on the tree's own
888
- // initiative, and the box must surface for that session to be usable.
750
+ // Applying the widget's filter flag stamps the marker the unsafeCSS rule
751
+ // keys on and syncs the search session; the subscription surfaces the box
752
+ // when the tree opens a session itself (typing while focused).
889
753
  React.useEffect(() => {
890
754
  if (widget === undefined) {
891
755
  return;
@@ -952,21 +816,9 @@ export function FileBrowserComponent(props) {
952
816
  wrapper.removeEventListener('dblclick', handleDoubleClick);
953
817
  };
954
818
  }, [onOpenFile]);
955
- // Bridge contextmenu events out of the `<file-tree-container>` shadow DOM.
956
- //
957
- // `@pierre/trees` mounts the tree under an open shadow root attached to the
958
- // `<file-tree-container>` custom element. When the user right-clicks a row
959
- // inside the shadow tree, the event is retargeted to the host element when
960
- // observed from the light DOM, and `app.contextMenu` walks via
961
- // `parentElement` — it never enters the shadow tree, so the `[data-type=
962
- // "item"]` selectors registered in `schema/plugin.json` never match.
963
- //
964
- // We listen in the capture phase (so we run before the application's
965
- // document-level handler) and copy the right-clicked row's data attributes
966
- // onto the host. Lumino then matches the host as if it were the row, and
967
- // `app.contextMenuHitTest` from command handlers reads the same attributes
968
- // back to recover the path. When the right-click misses any row we clear
969
- // the attributes so empty-area clicks don't show stale per-item entries.
819
+ // Lumino never enters the tree's shadow DOM, so the `[data-type="item"]`
820
+ // selectors can't match rows; mirror the right-clicked row's data
821
+ // attributes onto the host (capture phase, cleared on a miss).
970
822
  React.useEffect(() => {
971
823
  const wrapper = wrapperRef.current;
972
824
  if (wrapper === null) {
@@ -1,13 +1,8 @@
1
1
  import type { GitStatusEntry } from '@pierre/trees';
2
2
  /**
3
- * Fetch the porcelain status for `repoPath` and convert it into a flat list
4
- * of `GitStatusEntry` values consumable by `FileTree.setGitStatus`. Returns
5
- * an empty array when the path is not in a git repo, the request fails, or
6
- * the server reports a non-zero exit — the file browser must still work
7
- * without git, so any error here is a no-op rather than a thrown exception.
8
- *
9
- * Files that have both a staged and an unstaged change are collapsed into a
10
- * single entry: the unstaged status wins because that mirrors what is
11
- * actually different on disk and matches VS Code's tree decoration behavior.
3
+ * Fetch the porcelain status for `repoPath` as `GitStatusEntry` values.
4
+ * Any failure yields an empty array — the file browser must work without
5
+ * git. A file both staged and unstaged gets one entry, the unstaged status
6
+ * winning (mirrors what is on disk, matching VS Code).
12
7
  */
13
8
  export declare function loadGitStatusEntries(repoPath: string): Promise<GitStatusEntry[]>;
@@ -1,10 +1,8 @@
1
1
  import { expandStatusFiles, status } from '../git/api';
2
2
  /**
3
- * Translate a `jupyterlab_git` porcelain status into the closest
4
- * `@pierre/trees` `GitStatus` enum value. The tree library only supports the
5
- * six common statuses (added, deleted, ignored, modified, renamed, untracked)
6
- * so the rare ones (unmerged, typechange, unknown) collapse onto `modified`
7
- * — they all denote a pending change the user should see decorated.
3
+ * Translate a `jupyterlab_git` porcelain status into a `@pierre/trees`
4
+ * `GitStatus`. The tree only supports the six common statuses, so the rare
5
+ * ones (unmerged, typechange, unknown) collapse onto `modified`.
8
6
  */
9
7
  function toTreeStatus(value) {
10
8
  switch (value) {
@@ -22,15 +20,10 @@ function toTreeStatus(value) {
22
20
  }
23
21
  }
24
22
  /**
25
- * Fetch the porcelain status for `repoPath` and convert it into a flat list
26
- * of `GitStatusEntry` values consumable by `FileTree.setGitStatus`. Returns
27
- * an empty array when the path is not in a git repo, the request fails, or
28
- * the server reports a non-zero exit — the file browser must still work
29
- * without git, so any error here is a no-op rather than a thrown exception.
30
- *
31
- * Files that have both a staged and an unstaged change are collapsed into a
32
- * single entry: the unstaged status wins because that mirrors what is
33
- * actually different on disk and matches VS Code's tree decoration behavior.
23
+ * Fetch the porcelain status for `repoPath` as `GitStatusEntry` values.
24
+ * Any failure yields an empty array — the file browser must work without
25
+ * git. A file both staged and unstaged gets one entry, the unstaged status
26
+ * winning (mirrors what is on disk, matching VS Code).
34
27
  */
35
28
  export async function loadGitStatusEntries(repoPath) {
36
29
  try {
@@ -39,9 +32,6 @@ export async function loadGitStatusEntries(repoPath) {
39
32
  return [];
40
33
  }
41
34
  const changes = expandStatusFiles(result.files);
42
- // Collapse staged + unstaged into one decoration per file. Prefer the
43
- // unstaged group when both exist; otherwise keep whichever group is
44
- // present.
45
35
  const byPath = new Map();
46
36
  for (const change of changes) {
47
37
  if (change.group === 'unstaged' || !byPath.has(change.path)) {
@@ -8,22 +8,9 @@ import type { GitStatusEntry } from '@pierre/trees';
8
8
  */
9
9
  export declare function loadGitignoreMatcher(contentsManager: Contents.IManager): Promise<Ignore | null>;
10
10
  /**
11
- * Build `GitStatusEntry`s for the canonical paths that match the given
12
- * gitignore matcher.
13
- *
14
- * `@pierre/trees` propagates the `ignored` style from a directory entry
15
- * down to every descendant via its internal `ignoredDirectoryPaths` set, so
16
- * we only emit one entry per ignored subtree root: emitting descendants
17
- * would also pollute the library's `directoriesWithChanges` set, causing
18
- * parent directories of nested ignored items to render a "contains git
19
- * status items" dot.
20
- *
21
- * Paths are sorted lexicographically so a directory always precedes its
22
- * descendants, allowing a simple ancestor-suppression check via
23
- * `path.startsWith(dir)`. The trailing slash on canonical directory paths
24
- * (required by `@pierre/trees`, also recognized by `ignore` as the
25
- * "directory" indicator) keeps the prefix check from accidentally matching
26
- * sibling paths that happen to share a name prefix (e.g. `node_modules_old`
27
- * vs `node_modules/`).
11
+ * One `GitStatusEntry` per ignored subtree root: descendant entries would
12
+ * pollute the tree's `directoriesWithChanges` set (bogus dots on parents),
13
+ * and `ignored` already propagates downward. The trailing slash on directory
14
+ * paths keeps the sorted `startsWith` ancestor check off name-prefix siblings.
28
15
  */
29
16
  export declare function buildIgnoredEntries(matcher: Ignore, canonicalPaths: Iterable<string>): GitStatusEntry[];