xtralab 0.15.0 → 0.15.2

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 (237) hide show
  1. package/README.md +65 -30
  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 +29 -19
  36. package/lib/fileBrowser/commands.js +83 -97
  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 +70 -222
  42. package/lib/fileBrowser/gitStatus.d.ts +16 -10
  43. package/lib/fileBrowser/gitStatus.js +31 -30
  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 +141 -48
  50. package/lib/fileBrowser/widget.js +102 -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 +45 -5
  57. package/lib/git/commands.js +20 -13
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffPreferences.d.ts +90 -0
  60. package/lib/git/diffPreferences.js +107 -0
  61. package/lib/git/diffProvider.js +10 -2
  62. package/lib/git/diffSurface.d.ts +64 -63
  63. package/lib/git/diffSurface.js +72 -213
  64. package/lib/git/diffTheme.d.ts +7 -0
  65. package/lib/git/diffTheme.js +23 -0
  66. package/lib/git/diffWidget.d.ts +70 -14
  67. package/lib/git/diffWidget.js +100 -94
  68. package/lib/git/imageDiff.d.ts +14 -6
  69. package/lib/git/imageDiff.js +19 -75
  70. package/lib/git/index.d.ts +0 -3
  71. package/lib/git/index.js +44 -10
  72. package/lib/git/notebookDiff.d.ts +110 -33
  73. package/lib/git/notebookDiff.js +55 -197
  74. package/lib/git/tokens.d.ts +73 -31
  75. package/lib/git/tokens.js +2 -3
  76. package/lib/highlight/index.d.ts +4 -10
  77. package/lib/highlight/index.js +10 -41
  78. package/lib/index.d.ts +0 -9
  79. package/lib/index.js +0 -9
  80. package/lib/launcher/agents.d.ts +55 -51
  81. package/lib/launcher/agents.js +15 -57
  82. package/lib/launcher/availability.d.ts +3 -9
  83. package/lib/launcher/availability.js +3 -9
  84. package/lib/launcher/commands.d.ts +7 -32
  85. package/lib/launcher/commands.js +14 -45
  86. package/lib/launcher/dashboard.d.ts +19 -24
  87. package/lib/launcher/dashboard.js +21 -67
  88. package/lib/launcher/editorRegistry.d.ts +10 -22
  89. package/lib/launcher/editorRegistry.js +14 -18
  90. package/lib/launcher/editors.d.ts +39 -45
  91. package/lib/launcher/editors.js +8 -34
  92. package/lib/launcher/icons.d.ts +0 -12
  93. package/lib/launcher/icons.js +5 -39
  94. package/lib/launcher/index.js +12 -55
  95. package/lib/launcher/invocation.d.ts +4 -10
  96. package/lib/launcher/invocation.js +7 -22
  97. package/lib/launcher/registry.d.ts +9 -7
  98. package/lib/launcher/registry.js +9 -7
  99. package/lib/launcher/tokens.d.ts +10 -22
  100. package/lib/launcher/tokens.js +5 -8
  101. package/lib/menuBar/index.d.ts +3 -10
  102. package/lib/menuBar/index.js +11 -28
  103. package/lib/menus/index.d.ts +3 -9
  104. package/lib/menus/index.js +14 -30
  105. package/lib/omnibox/files.d.ts +2 -4
  106. package/lib/omnibox/files.js +5 -11
  107. package/lib/omnibox/index.d.ts +4 -14
  108. package/lib/omnibox/index.js +9 -27
  109. package/lib/omnibox/model.d.ts +38 -10
  110. package/lib/omnibox/model.js +15 -33
  111. package/lib/omnibox/recents.d.ts +15 -17
  112. package/lib/omnibox/recents.js +12 -17
  113. package/lib/omnibox/tokens.d.ts +5 -10
  114. package/lib/omnibox/tokens.js +2 -6
  115. package/lib/omnibox/widget.d.ts +11 -7
  116. package/lib/omnibox/widget.js +8 -16
  117. package/lib/searchReplace/index.d.ts +4 -20
  118. package/lib/searchReplace/index.js +7 -27
  119. package/lib/showOutput/index.d.ts +3 -12
  120. package/lib/showOutput/index.js +6 -22
  121. package/lib/sidebar/index.d.ts +4 -7
  122. package/lib/sidebar/index.js +5 -12
  123. package/lib/terminalNotifications/index.d.ts +4 -10
  124. package/lib/terminalNotifications/index.js +6 -20
  125. package/lib/terminals/agentTerminals.d.ts +12 -5
  126. package/lib/terminals/agentTerminals.js +25 -44
  127. package/lib/terminals/detection.d.ts +4 -8
  128. package/lib/terminals/detection.js +4 -8
  129. package/lib/terminals/index.d.ts +0 -35
  130. package/lib/terminals/index.js +12 -101
  131. package/lib/terminals/model.d.ts +64 -139
  132. package/lib/terminals/model.js +80 -220
  133. package/lib/terminals/tokens.d.ts +11 -22
  134. package/lib/terminals/tokens.js +2 -2
  135. package/lib/terminals/widget.d.ts +43 -13
  136. package/lib/terminals/widget.js +33 -18
  137. package/lib/topBar/icons.d.ts +3 -8
  138. package/lib/topBar/icons.js +3 -8
  139. package/lib/topBar/index.d.ts +4 -11
  140. package/lib/topBar/index.js +10 -38
  141. package/lib/walkthrough/index.d.ts +4 -9
  142. package/lib/walkthrough/index.js +6 -20
  143. package/lib/walkthrough/panel.d.ts +40 -9
  144. package/lib/walkthrough/panel.js +4 -10
  145. package/package.json +5 -3
  146. package/schema/git-diff-preferences.json +42 -0
  147. package/src/about/icons.ts +0 -5
  148. package/src/about/index.tsx +6 -17
  149. package/src/agentSessions.ts +9 -29
  150. package/src/askAgent/editorSelection.ts +22 -29
  151. package/src/askAgent/icons.ts +2 -3
  152. package/src/askAgent/index.ts +54 -158
  153. package/src/askAgent/popup.tsx +23 -45
  154. package/src/askAgent/prompt.ts +6 -19
  155. package/src/askAgent/queue.ts +13 -24
  156. package/src/askAgent/queuePanel.tsx +21 -46
  157. package/src/askAgent/targetPicker.tsx +5 -15
  158. package/src/askAgent/tokens.ts +32 -46
  159. package/src/commandBar/index.ts +24 -39
  160. package/src/customPanel/index.ts +15 -0
  161. package/src/editorBreadcrumbs/index.ts +10 -6
  162. package/src/editorBreadcrumbs/widget.ts +23 -17
  163. package/src/editorIndent/index.ts +17 -23
  164. package/src/fileBrowser/commands.ts +119 -96
  165. package/src/fileBrowser/contents.ts +3 -0
  166. package/src/fileBrowser/dragAndDrop.ts +17 -7
  167. package/src/fileBrowser/fileBrowser.tsx +86 -230
  168. package/src/fileBrowser/gitStatus.ts +36 -33
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +2 -4
  172. package/src/fileBrowser/widget.tsx +171 -50
  173. package/src/fileTypeIcons/index.ts +0 -10
  174. package/src/git/api.ts +10 -36
  175. package/src/git/askRequest.ts +6 -20
  176. package/src/git/commands.ts +83 -11
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffPreferences.ts +192 -0
  179. package/src/git/diffProvider.tsx +11 -2
  180. package/src/git/diffSurface.tsx +165 -286
  181. package/src/git/diffTheme.ts +24 -0
  182. package/src/git/diffWidget.tsx +194 -134
  183. package/src/git/imageDiff.tsx +56 -84
  184. package/src/git/index.ts +54 -10
  185. package/src/git/notebookDiff.tsx +214 -240
  186. package/src/git/tokens.ts +73 -31
  187. package/src/highlight/index.ts +10 -41
  188. package/src/index.ts +0 -9
  189. package/src/launcher/agents.ts +64 -95
  190. package/src/launcher/availability.ts +3 -9
  191. package/src/launcher/commands.ts +14 -51
  192. package/src/launcher/dashboard.tsx +49 -88
  193. package/src/launcher/editorRegistry.ts +21 -29
  194. package/src/launcher/editors.ts +39 -58
  195. package/src/launcher/icons.ts +5 -39
  196. package/src/launcher/index.ts +12 -55
  197. package/src/launcher/invocation.ts +7 -22
  198. package/src/launcher/registry.ts +9 -7
  199. package/src/launcher/tokens.ts +10 -22
  200. package/src/menuBar/index.ts +11 -28
  201. package/src/menus/index.ts +14 -30
  202. package/src/omnibox/files.ts +10 -14
  203. package/src/omnibox/index.ts +9 -27
  204. package/src/omnibox/model.ts +50 -38
  205. package/src/omnibox/recents.ts +17 -22
  206. package/src/omnibox/tokens.ts +5 -10
  207. package/src/omnibox/widget.tsx +14 -19
  208. package/src/searchReplace/index.ts +7 -27
  209. package/src/showOutput/index.ts +6 -22
  210. package/src/sidebar/index.ts +20 -12
  211. package/src/terminalNotifications/index.ts +42 -25
  212. package/src/terminals/agentTerminals.ts +25 -49
  213. package/src/terminals/detection.ts +4 -8
  214. package/src/terminals/index.ts +12 -101
  215. package/src/terminals/model.ts +130 -246
  216. package/src/terminals/tokens.ts +11 -22
  217. package/src/terminals/widget.tsx +48 -31
  218. package/src/topBar/icons.ts +3 -8
  219. package/src/topBar/index.ts +21 -44
  220. package/src/walkthrough/index.ts +6 -20
  221. package/src/walkthrough/panel.ts +40 -10
  222. package/style/about.css +2 -7
  223. package/style/askAgent.css +8 -40
  224. package/style/base.css +9 -45
  225. package/style/chrome.css +108 -308
  226. package/style/commandBar.css +4 -20
  227. package/style/customPanel.css +1 -4
  228. package/style/git.css +26 -186
  229. package/style/highlight.css +1 -3
  230. package/style/launcher.css +6 -70
  231. package/style/omnibox.css +2 -14
  232. package/style/showOutput.css +0 -6
  233. package/style/sidebar.css +29 -9
  234. package/style/tabs.css +5 -13
  235. package/style/terminals.css +6 -53
  236. package/style/topBar.css +2 -22
  237. package/style/walkthrough.css +0 -8
@@ -7,58 +7,26 @@ import { FileTree, useFileTree } from '@pierre/trees/react';
7
7
  import { ROOT_LOAD_KEY, listDirectory, parentOf, toServerPath } from './contents';
8
8
  import { computeDropMoves, computeRootDropMoves, createTreeDragAndDropConfig, useRootDropZone } from './dragAndDrop';
9
9
  import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
10
- import { loadGitStatusEntries } from './gitStatus';
10
+ import { GIT_REPO_PATH, loadGitChanges, toGitStatusEntries } 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
- /**
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.
45
- */
46
- 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
24
  const FILE_TREE_TAG = 'file-tree-container';
53
25
  /**
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.
26
+ * Injected into the tree's shadow root, where outside CSS cannot reach.
27
+ * The search box is hidden unless the host carries the filter-bridge marker
28
+ * (the library always renders it), and the drag-hover row gets a quiet ring
29
+ * — the library's selection background is illegible with xtralab's colors.
62
30
  */
63
31
  const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
64
32
  '[data-item-section="spacing-item"] {' +
@@ -73,13 +41,9 @@ const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
73
41
  'box-shadow: inset 0 0 0 2px var(--trees-accent);' +
74
42
  '}';
75
43
  /**
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.
44
+ * A `@pierre/trees` file tree backed by the Jupyter contents API. The API
45
+ * returns one directory level per request, so directories load lazily on
46
+ * first expand, detected by diffing the model against a load-state map.
83
47
  */
84
48
  export function FileBrowserComponent(props) {
85
49
  const { contentsManager, docManager, onOpenFile, translator, widget } = props;
@@ -96,25 +60,16 @@ export function FileBrowserComponent(props) {
96
60
  });
97
61
  React.useEffect(() => {
98
62
  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.
63
+ // Mirror of the loaded canonical paths — the model has no path-iteration
64
+ // API, and the gitignore matcher must re-test every loaded path.
104
65
  const loadedPaths = new Set();
105
66
  let gitignoreMatcher = null;
106
67
  let gitStatusEntries = [];
107
68
  let cancelled = false;
108
69
  /**
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.
70
+ * Push the combined gitignore + porcelain entries into the tree.
71
+ * Ignored entries go first so porcelain entries win on overlap —
72
+ * `@pierre/trees` lets later entries overwrite earlier ones.
118
73
  */
119
74
  const syncGitStatus = () => {
120
75
  const entries = [];
@@ -128,12 +83,6 @@ export function FileBrowserComponent(props) {
128
83
  }
129
84
  model.setGitStatus(entries);
130
85
  };
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
86
  const refreshGitignoreMatcher = async () => {
138
87
  let next = null;
139
88
  try {
@@ -148,18 +97,13 @@ export function FileBrowserComponent(props) {
148
97
  gitignoreMatcher = next;
149
98
  syncGitStatus();
150
99
  };
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
100
  const refreshGitStatus = async () => {
158
- const next = await loadGitStatusEntries(GIT_REPO_PATH);
101
+ const changes = await loadGitChanges(GIT_REPO_PATH);
159
102
  if (cancelled) {
160
103
  return;
161
104
  }
162
- gitStatusEntries = next;
105
+ gitStatusEntries = toGitStatusEntries(changes);
106
+ widget === null || widget === void 0 ? void 0 : widget.updateGitChanges(changes);
163
107
  syncGitStatus();
164
108
  };
165
109
  const fetchDirectory = async (canonicalPath) => {
@@ -182,21 +126,16 @@ export function FileBrowserComponent(props) {
182
126
  }
183
127
  }
184
128
  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.
129
+ // The path-store throws when a path is added twice, and entries
130
+ // can arrive out-of-band (e.g. `notifyPathAdded`), so skip those.
189
131
  const operations = paths
190
132
  .filter(path => model.getItem(path) === null)
191
133
  .map(path => ({ type: 'add', path }));
192
134
  if (operations.length > 0) {
193
135
  model.batch(operations);
194
136
  }
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.
137
+ // Paths skipped above are already in the model from a prior add,
138
+ // so they belong in the mirror too.
200
139
  for (const path of paths) {
201
140
  loadedPaths.add(path);
202
141
  }
@@ -215,10 +154,8 @@ export function FileBrowserComponent(props) {
215
154
  }
216
155
  };
217
156
  /**
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.
157
+ * Re-fetch the root and every previously-expanded directory so the tree
158
+ * mirrors the disk while preserving the user's expansion state.
222
159
  */
223
160
  const refreshAll = async () => {
224
161
  const expandedPaths = new Set();
@@ -251,9 +188,8 @@ export function FileBrowserComponent(props) {
251
188
  return;
252
189
  }
253
190
  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.
191
+ // Fetch only the previously-expanded subtree so the refresh doesn't
192
+ // walk the entire workspace.
257
193
  const subdirsByParent = new Map();
258
194
  subdirsByParent.set(ROOT_LOAD_KEY, rootSubdirs);
259
195
  const queue = rootSubdirs.filter(s => expandedPaths.has(s));
@@ -277,8 +213,6 @@ export function FileBrowserComponent(props) {
277
213
  console.error(`xtralab: refresh failed for "${dir}"`, err);
278
214
  }
279
215
  }
280
- // Reset the tree contents and restore the load-state map so future
281
- // expansions know which directories still need fetching.
282
216
  model.resetPaths(allPaths);
283
217
  loadedPaths.clear();
284
218
  for (const path of allPaths) {
@@ -290,18 +224,10 @@ export function FileBrowserComponent(props) {
290
224
  knownDirs.set(subdir, expandedPaths.has(subdir) ? 'loaded' : 'unloaded');
291
225
  }
292
226
  });
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.
227
+ // Each re-applies the statuses itself when it completes.
300
228
  void refreshGitignoreMatcher();
301
229
  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.
230
+ // `resetPaths` starts every directory collapsed, so re-expand after it.
305
231
  for (const path of expandedPaths) {
306
232
  const item = model.getItem(path);
307
233
  if (item !== null && item.isDirectory()) {
@@ -310,20 +236,10 @@ export function FileBrowserComponent(props) {
310
236
  }
311
237
  };
312
238
  /**
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.
239
+ * Auto-refresh tick: diff every loaded directory and apply batched
240
+ * mutations without `resetPaths`, so expansion, selection, and scroll
241
+ * state survive. A failed fetch is skipped as transient; a deleted
242
+ * directory surfaces through its parent's diff and is removed recursively.
327
243
  */
328
244
  const quietRefresh = async () => {
329
245
  if (cancelled) {
@@ -343,10 +259,8 @@ export function FileBrowserComponent(props) {
343
259
  if (cancelled) {
344
260
  return;
345
261
  }
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.
262
+ // The cascade cleanup below can prune subtrees mid-tick, so a dir
263
+ // captured at the start may already be gone.
350
264
  if (knownDirs.get(dir) !== 'loaded') {
351
265
  continue;
352
266
  }
@@ -373,8 +287,6 @@ export function FileBrowserComponent(props) {
373
287
  if (newChildren.has(lp)) {
374
288
  continue;
375
289
  }
376
- // Disappeared since the last tick. Remove recursively so any
377
- // descendants that were also being tracked go with it.
378
290
  ops.push({ type: 'remove', path: lp, recursive: true });
379
291
  removals.push(lp);
380
292
  if (lp.endsWith('/')) {
@@ -394,15 +306,12 @@ export function FileBrowserComponent(props) {
394
306
  }
395
307
  catch (err) {
396
308
  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.
309
+ // Leave the mirrors untouched so the next tick retries.
399
310
  continue;
400
311
  }
401
312
  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`.
313
+ // Mirror updates only after the batch lands; the cascade below
314
+ // mirrors the `recursive: true` removal semantics.
406
315
  for (const r of removals) {
407
316
  loadedPaths.delete(r);
408
317
  }
@@ -431,8 +340,7 @@ export function FileBrowserComponent(props) {
431
340
  loadedPaths.add(a);
432
341
  }
433
342
  }
434
- // Register newly-observed subdirectories so the next user expand
435
- // triggers a fetch instead of being ignored.
343
+ // Untracked subdirectories would ignore their first expand.
436
344
  for (const subdir of fetched.subdirectories) {
437
345
  if (!knownDirs.has(subdir)) {
438
346
  knownDirs.set(subdir, 'unloaded');
@@ -444,24 +352,8 @@ export function FileBrowserComponent(props) {
444
352
  }
445
353
  };
446
354
  /**
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.
355
+ * Clear the selection and scroll to the top; gives the home-crumb
356
+ * gesture visible feedback even when the sidebar is already focused.
465
357
  */
466
358
  const goToRoot = () => {
467
359
  var _a;
@@ -476,13 +368,6 @@ export function FileBrowserComponent(props) {
476
368
  container.scrollTop = 0;
477
369
  }
478
370
  };
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
371
  const collapseAll = () => {
487
372
  if (cancelled) {
488
373
  return;
@@ -501,6 +386,12 @@ export function FileBrowserComponent(props) {
501
386
  }
502
387
  });
503
388
  };
389
+ /**
390
+ * Reveal `canonicalPath`: fetch and expand ancestors, then select and
391
+ * scroll to the target. Each ancestor is fetched *before* expanding —
392
+ * expansion starts the subscribe callback's own fetch, and awaiting that
393
+ * in-flight fetch resolves with the children still unloaded.
394
+ */
504
395
  const revealPath = async (canonicalPath) => {
505
396
  if (cancelled || canonicalPath.length === 0) {
506
397
  return;
@@ -511,9 +402,7 @@ export function FileBrowserComponent(props) {
511
402
  if (segments.length === 0) {
512
403
  return;
513
404
  }
514
- // Build the ordered list of ancestor directory canonical paths.
515
- // For "foo/bar/baz.txt" → ["foo/", "foo/bar/"].
516
- // For "foo/bar/" → ["foo/"].
405
+ // "foo/bar/baz.txt" → ["foo/", "foo/bar/"].
517
406
  const ancestors = [];
518
407
  let cumulative = '';
519
408
  for (let i = 0; i < segments.length - 1; i++) {
@@ -560,15 +449,13 @@ export function FileBrowserComponent(props) {
560
449
  previous === null || previous === void 0 ? void 0 : previous.deselect();
561
450
  }
562
451
  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.
452
+ // `scrollToPath` focuses and scrolls even when the row is virtualized
453
+ // out of the rendered window.
566
454
  model.scrollToPath(canonicalPath, { focus: true, offset: 'center' });
567
455
  };
568
456
  /**
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.
457
+ * Insert a newly-created path without a full refresh and expand its
458
+ * parent so the entry is visible immediately.
572
459
  */
573
460
  const handlePathAdded = (canonicalPath) => {
574
461
  if (model.getItem(canonicalPath) === null) {
@@ -582,9 +469,8 @@ export function FileBrowserComponent(props) {
582
469
  }
583
470
  loadedPaths.add(canonicalPath);
584
471
  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.
472
+ // A new directory has no children; mark it loaded so a stale
473
+ // "unloaded" entry doesn't trigger a fetch on the next expand.
588
474
  knownDirs.set(canonicalPath, 'loaded');
589
475
  }
590
476
  syncGitStatus();
@@ -685,10 +571,6 @@ export function FileBrowserComponent(props) {
685
571
  }
686
572
  }
687
573
  };
688
- /**
689
- * The tree refused the drop and left its model untouched — almost
690
- * always a name collision at the destination.
691
- */
692
574
  const failedDrop = (error, context) => {
693
575
  if (cancelled) {
694
576
  return;
@@ -744,13 +626,8 @@ export function FileBrowserComponent(props) {
744
626
  },
745
627
  standby: 'when-hidden'
746
628
  });
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.
629
+ // `auto: false` keeps the first tick from racing the initial root
630
+ // fetch above; the poll is started explicitly below.
754
631
  const listingPoll = new Poll({
755
632
  auto: false,
756
633
  name: '@xtralab/fileBrowser:listing',
@@ -763,26 +640,16 @@ export function FileBrowserComponent(props) {
763
640
  standby: 'when-hidden'
764
641
  });
765
642
  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.
643
+ // In-app contents changes (save, rename, delete) nudge the poll; the
644
+ // signal doesn't say whether a shown path is affected, the diff does.
773
645
  const onContentsFileChanged = () => {
774
646
  void listingPoll.refresh();
775
647
  };
776
648
  contentsManager.fileChanged.connect(onContentsFileChanged);
777
649
  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.
650
+ // Search auto-expands every matching directory — fetching those would
651
+ // walk whole subtrees (node_modules included); real expansions still
652
+ // fetch once the search session closes.
786
653
  if (model.isSearchOpen()) {
787
654
  return;
788
655
  }
@@ -854,11 +721,8 @@ export function FileBrowserComponent(props) {
854
721
  }
855
722
  };
856
723
  }, [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.
724
+ // Bridge the tree selection up to the widget. `subscribe` fires on every
725
+ // mutation, so diff against a snapshot before notifying.
862
726
  React.useEffect(() => {
863
727
  if (widget === undefined) {
864
728
  return;
@@ -879,13 +743,9 @@ export function FileBrowserComponent(props) {
879
743
  unsubscribe();
880
744
  };
881
745
  }, [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.
746
+ // Applying the widget's filter flag stamps the marker the unsafeCSS rule
747
+ // keys on and syncs the search session; the subscription surfaces the box
748
+ // when the tree opens a session itself (typing while focused).
889
749
  React.useEffect(() => {
890
750
  if (widget === undefined) {
891
751
  return;
@@ -952,21 +812,9 @@ export function FileBrowserComponent(props) {
952
812
  wrapper.removeEventListener('dblclick', handleDoubleClick);
953
813
  };
954
814
  }, [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.
815
+ // Lumino never enters the tree's shadow DOM, so the `[data-type="item"]`
816
+ // selectors can't match rows; mirror the right-clicked row's data
817
+ // attributes onto the host (capture phase, cleared on a miss).
970
818
  React.useEffect(() => {
971
819
  const wrapper = wrapperRef.current;
972
820
  if (wrapper === null) {
@@ -1,13 +1,19 @@
1
1
  import type { GitStatusEntry } from '@pierre/trees';
2
+ import type { IFileChange } from '../git/tokens';
2
3
  /**
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.
4
+ * Repo path for `/git/*` calls; empty means the server root, letting git
5
+ * resolve the enclosing repo (same convention as the git panel).
12
6
  */
13
- export declare function loadGitStatusEntries(repoPath: string): Promise<GitStatusEntry[]>;
7
+ export declare const GIT_REPO_PATH = "";
8
+ /**
9
+ * Fetch the porcelain status for `repoPath`, one entry per staged or
10
+ * unstaged change. Any failure yields an empty array — the file browser
11
+ * must work without git.
12
+ */
13
+ export declare function loadGitChanges(repoPath: string): Promise<IFileChange[]>;
14
+ /**
15
+ * Collapse changes into the tree's `GitStatusEntry` values. A file both
16
+ * staged and unstaged gets one entry, the unstaged status winning (mirrors
17
+ * what is on disk, matching VS Code).
18
+ */
19
+ export declare function toGitStatusEntries(changes: readonly IFileChange[]): GitStatusEntry[];
@@ -1,10 +1,13 @@
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
+ * Repo path for `/git/*` calls; empty means the server root, letting git
4
+ * resolve the enclosing repo (same convention as the git panel).
5
+ */
6
+ export const GIT_REPO_PATH = '';
7
+ /**
8
+ * Translate a `jupyterlab_git` porcelain status into a `@pierre/trees`
9
+ * `GitStatus`. The tree only supports the six common statuses, so the rare
10
+ * ones (unmerged, typechange, unknown) collapse onto `modified`.
8
11
  */
9
12
  function toTreeStatus(value) {
10
13
  switch (value) {
@@ -22,39 +25,37 @@ function toTreeStatus(value) {
22
25
  }
23
26
  }
24
27
  /**
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.
28
+ * Fetch the porcelain status for `repoPath`, one entry per staged or
29
+ * unstaged change. Any failure yields an empty array — the file browser
30
+ * must work without git.
34
31
  */
35
- export async function loadGitStatusEntries(repoPath) {
32
+ export async function loadGitChanges(repoPath) {
36
33
  try {
37
34
  const result = await status(repoPath);
38
35
  if (result.code !== 0) {
39
36
  return [];
40
37
  }
41
- 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
- const byPath = new Map();
46
- for (const change of changes) {
47
- if (change.group === 'unstaged' || !byPath.has(change.path)) {
48
- byPath.set(change.path, change.status);
49
- }
50
- }
51
- const entries = [];
52
- byPath.forEach((value, path) => {
53
- entries.push({ path, status: toTreeStatus(value) });
54
- });
55
- return entries;
38
+ return expandStatusFiles(result.files);
56
39
  }
57
40
  catch (_a) {
58
41
  return [];
59
42
  }
60
43
  }
44
+ /**
45
+ * Collapse changes into the tree's `GitStatusEntry` values. A file both
46
+ * staged and unstaged gets one entry, the unstaged status winning (mirrors
47
+ * what is on disk, matching VS Code).
48
+ */
49
+ export function toGitStatusEntries(changes) {
50
+ const byPath = new Map();
51
+ for (const change of changes) {
52
+ if (change.group === 'unstaged' || !byPath.has(change.path)) {
53
+ byPath.set(change.path, change.status);
54
+ }
55
+ }
56
+ const entries = [];
57
+ byPath.forEach((value, path) => {
58
+ entries.push({ path, status: toTreeStatus(value) });
59
+ });
60
+ return entries;
61
+ }