xtralab 0.14.1 → 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 (236) 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.d.ts +7 -0
  29. package/lib/customPanel/index.js +89 -0
  30. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  31. package/lib/editorBreadcrumbs/index.js +10 -6
  32. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  33. package/lib/editorBreadcrumbs/widget.js +11 -17
  34. package/lib/editorIndent/index.d.ts +4 -12
  35. package/lib/editorIndent/index.js +8 -23
  36. package/lib/fileBrowser/commands.d.ts +28 -19
  37. package/lib/fileBrowser/commands.js +51 -98
  38. package/lib/fileBrowser/contents.d.ts +14 -0
  39. package/lib/fileBrowser/contents.js +28 -0
  40. package/lib/fileBrowser/dragAndDrop.d.ts +79 -0
  41. package/lib/fileBrowser/dragAndDrop.js +110 -0
  42. package/lib/fileBrowser/fileBrowser.d.ts +21 -9
  43. package/lib/fileBrowser/fileBrowser.js +211 -405
  44. package/lib/fileBrowser/gitStatus.d.ts +4 -9
  45. package/lib/fileBrowser/gitStatus.js +7 -17
  46. package/lib/fileBrowser/gitignore.d.ts +4 -17
  47. package/lib/fileBrowser/gitignore.js +4 -25
  48. package/lib/fileBrowser/icons.d.ts +7 -17
  49. package/lib/fileBrowser/icons.js +25 -80
  50. package/lib/fileBrowser/index.js +41 -10
  51. package/lib/fileBrowser/widget.d.ts +170 -53
  52. package/lib/fileBrowser/widget.js +242 -18
  53. package/lib/fileTypeIcons/index.js +0 -10
  54. package/lib/git/api.d.ts +5 -13
  55. package/lib/git/api.js +10 -36
  56. package/lib/git/askRequest.d.ts +4 -8
  57. package/lib/git/askRequest.js +6 -20
  58. package/lib/git/commands.d.ts +40 -5
  59. package/lib/git/commands.js +16 -10
  60. package/lib/git/diffModel.js +4 -0
  61. package/lib/git/diffSurface.d.ts +41 -55
  62. package/lib/git/diffSurface.js +42 -117
  63. package/lib/git/diffWidget.d.ts +81 -2
  64. package/lib/git/diffWidget.js +62 -12
  65. package/lib/git/imageDiff.d.ts +5 -6
  66. package/lib/git/imageDiff.js +18 -48
  67. package/lib/git/index.d.ts +0 -3
  68. package/lib/git/index.js +3 -8
  69. package/lib/git/notebookDiff.d.ts +110 -33
  70. package/lib/git/notebookDiff.js +52 -195
  71. package/lib/git/tokens.d.ts +73 -31
  72. package/lib/git/tokens.js +2 -3
  73. package/lib/highlight/index.d.ts +4 -10
  74. package/lib/highlight/index.js +10 -41
  75. package/lib/index.d.ts +0 -9
  76. package/lib/index.js +2 -9
  77. package/lib/launcher/agents.d.ts +55 -51
  78. package/lib/launcher/agents.js +25 -54
  79. package/lib/launcher/availability.d.ts +3 -9
  80. package/lib/launcher/availability.js +3 -9
  81. package/lib/launcher/commands.d.ts +7 -32
  82. package/lib/launcher/commands.js +14 -45
  83. package/lib/launcher/dashboard.d.ts +19 -24
  84. package/lib/launcher/dashboard.js +21 -67
  85. package/lib/launcher/editorRegistry.d.ts +10 -22
  86. package/lib/launcher/editorRegistry.js +14 -18
  87. package/lib/launcher/editors.d.ts +39 -45
  88. package/lib/launcher/editors.js +8 -34
  89. package/lib/launcher/icons.d.ts +0 -12
  90. package/lib/launcher/icons.js +14 -37
  91. package/lib/launcher/index.js +12 -55
  92. package/lib/launcher/invocation.d.ts +4 -10
  93. package/lib/launcher/invocation.js +7 -22
  94. package/lib/launcher/registry.d.ts +9 -7
  95. package/lib/launcher/registry.js +9 -7
  96. package/lib/launcher/schemaDefaults.d.ts +2 -9
  97. package/lib/launcher/schemaDefaults.js +37 -13
  98. package/lib/launcher/tokens.d.ts +10 -22
  99. package/lib/launcher/tokens.js +5 -8
  100. package/lib/menuBar/index.d.ts +3 -10
  101. package/lib/menuBar/index.js +11 -28
  102. package/lib/menus/index.d.ts +3 -9
  103. package/lib/menus/index.js +14 -30
  104. package/lib/omnibox/files.d.ts +2 -4
  105. package/lib/omnibox/files.js +5 -11
  106. package/lib/omnibox/index.d.ts +4 -14
  107. package/lib/omnibox/index.js +9 -27
  108. package/lib/omnibox/model.d.ts +38 -10
  109. package/lib/omnibox/model.js +15 -33
  110. package/lib/omnibox/recents.d.ts +15 -17
  111. package/lib/omnibox/recents.js +12 -17
  112. package/lib/omnibox/tokens.d.ts +5 -10
  113. package/lib/omnibox/tokens.js +2 -6
  114. package/lib/omnibox/widget.d.ts +11 -7
  115. package/lib/omnibox/widget.js +8 -16
  116. package/lib/searchReplace/index.d.ts +4 -20
  117. package/lib/searchReplace/index.js +7 -27
  118. package/lib/showOutput/index.d.ts +3 -12
  119. package/lib/showOutput/index.js +6 -22
  120. package/lib/sidebar/index.d.ts +4 -13
  121. package/lib/sidebar/index.js +73 -46
  122. package/lib/terminalNotifications/index.d.ts +4 -10
  123. package/lib/terminalNotifications/index.js +6 -20
  124. package/lib/terminals/agentTerminals.d.ts +12 -5
  125. package/lib/terminals/agentTerminals.js +25 -44
  126. package/lib/terminals/detection.d.ts +4 -8
  127. package/lib/terminals/detection.js +4 -8
  128. package/lib/terminals/index.d.ts +0 -33
  129. package/lib/terminals/index.js +24 -102
  130. package/lib/terminals/model.d.ts +64 -139
  131. package/lib/terminals/model.js +80 -220
  132. package/lib/terminals/tokens.d.ts +11 -22
  133. package/lib/terminals/tokens.js +2 -2
  134. package/lib/terminals/widget.d.ts +72 -38
  135. package/lib/terminals/widget.js +173 -73
  136. package/lib/topBar/icons.d.ts +3 -8
  137. package/lib/topBar/icons.js +3 -8
  138. package/lib/topBar/index.d.ts +4 -11
  139. package/lib/topBar/index.js +10 -38
  140. package/lib/walkthrough/index.d.ts +4 -9
  141. package/lib/walkthrough/index.js +6 -20
  142. package/lib/walkthrough/panel.d.ts +40 -9
  143. package/lib/walkthrough/panel.js +4 -10
  144. package/package.json +2 -2
  145. package/schema/launcher.json +2 -2
  146. package/schema/sidebar.json +47 -7
  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 +119 -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 +70 -98
  165. package/src/fileBrowser/contents.ts +33 -0
  166. package/src/fileBrowser/dragAndDrop.ts +188 -0
  167. package/src/fileBrowser/fileBrowser.tsx +259 -440
  168. package/src/fileBrowser/gitStatus.ts +7 -17
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +43 -10
  172. package/src/fileBrowser/widget.tsx +344 -64
  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 +70 -10
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffSurface.tsx +71 -159
  179. package/src/git/diffWidget.tsx +104 -14
  180. package/src/git/imageDiff.tsx +45 -51
  181. package/src/git/index.ts +3 -8
  182. package/src/git/notebookDiff.tsx +211 -238
  183. package/src/git/tokens.ts +73 -31
  184. package/src/highlight/index.ts +10 -41
  185. package/src/index.ts +2 -9
  186. package/src/launcher/agents.ts +74 -92
  187. package/src/launcher/availability.ts +3 -9
  188. package/src/launcher/commands.ts +14 -51
  189. package/src/launcher/dashboard.tsx +49 -88
  190. package/src/launcher/editorRegistry.ts +21 -29
  191. package/src/launcher/editors.ts +39 -58
  192. package/src/launcher/icons.ts +15 -37
  193. package/src/launcher/index.ts +12 -55
  194. package/src/launcher/invocation.ts +7 -22
  195. package/src/launcher/registry.ts +9 -7
  196. package/src/launcher/schemaDefaults.ts +43 -13
  197. package/src/launcher/tokens.ts +10 -22
  198. package/src/menuBar/index.ts +11 -28
  199. package/src/menus/index.ts +14 -30
  200. package/src/omnibox/files.ts +10 -14
  201. package/src/omnibox/index.ts +9 -27
  202. package/src/omnibox/model.ts +50 -38
  203. package/src/omnibox/recents.ts +17 -22
  204. package/src/omnibox/tokens.ts +5 -10
  205. package/src/omnibox/widget.tsx +14 -19
  206. package/src/searchReplace/index.ts +7 -27
  207. package/src/showOutput/index.ts +6 -22
  208. package/src/sidebar/index.ts +103 -52
  209. package/src/terminalNotifications/index.ts +42 -25
  210. package/src/terminals/agentTerminals.ts +25 -49
  211. package/src/terminals/detection.ts +4 -8
  212. package/src/terminals/index.ts +31 -102
  213. package/src/terminals/model.ts +130 -246
  214. package/src/terminals/tokens.ts +11 -22
  215. package/src/terminals/widget.tsx +229 -113
  216. package/src/topBar/icons.ts +3 -8
  217. package/src/topBar/index.ts +21 -44
  218. package/src/walkthrough/index.ts +6 -20
  219. package/src/walkthrough/panel.ts +40 -10
  220. package/style/about.css +2 -7
  221. package/style/askAgent.css +8 -40
  222. package/style/base.css +25 -73
  223. package/style/chrome.css +108 -308
  224. package/style/commandBar.css +4 -20
  225. package/style/customPanel.css +6 -0
  226. package/style/git.css +22 -184
  227. package/style/highlight.css +1 -3
  228. package/style/index.js +1 -0
  229. package/style/launcher.css +6 -70
  230. package/style/omnibox.css +2 -14
  231. package/style/showOutput.css +0 -6
  232. package/style/sidebar.css +29 -9
  233. package/style/tabs.css +5 -13
  234. package/style/terminals.css +18 -123
  235. package/style/topBar.css +2 -22
  236. package/style/walkthrough.css +0 -8
@@ -11,21 +11,15 @@ import { computeSections, IOmniboxItem } from './model';
11
11
  import { loadWorkspaceFiles } from './files';
12
12
  import type { OmniboxRecents } from './recents';
13
13
 
14
- /**
15
- * DOM id of the results listbox, referenced by the input's `aria-controls`.
16
- */
17
14
  const LIST_ID = 'jp-xtralab-Omnibox-list';
18
15
 
19
- /**
20
- * Stable DOM id for the result row at `index`, for `aria-activedescendant`.
21
- */
22
16
  function optionId(index: number): string {
23
17
  return `jp-xtralab-Omnibox-option-${index}`;
24
18
  }
25
19
 
26
20
  /**
27
21
  * Render `text` with the fuzzy-matched characters at `indices` wrapped in a
28
- * highlight span, leaving the rest as plain text.
22
+ * highlight span.
29
23
  */
30
24
  function renderHighlight(
31
25
  text: string,
@@ -125,13 +119,10 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
125
119
  );
126
120
  const hasResults = flat.length > 0;
127
121
 
128
- // Reset the highlighted row whenever the result set changes, and keep the
129
- // index in range when results shrink.
130
122
  React.useEffect(() => {
131
123
  setActive(current => (current < flat.length ? current : 0));
132
124
  }, [flat]);
133
125
 
134
- // Keep the highlighted row scrolled into view as it moves.
135
126
  React.useEffect(() => {
136
127
  listRef.current
137
128
  ?.querySelector('.jp-mod-active')
@@ -139,8 +130,8 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
139
130
  }, [active, flat]);
140
131
 
141
132
  const dismiss = React.useCallback(() => {
142
- // Defer so unmounting this React root (via onClose) never happens
143
- // synchronously inside the event handler that triggered it.
133
+ // Defer so unmounting this React root never happens synchronously inside
134
+ // the event handler that triggered it.
144
135
  window.setTimeout(onClose, 0);
145
136
  }, [onClose]);
146
137
 
@@ -307,10 +298,8 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
307
298
  }
308
299
 
309
300
  /**
310
- * The omnibox overlay widget. Hosts {@link OmniboxComponent} in a React root
311
- * attached to `document.body`. It carries `jp-ThemedContainer` so JupyterLab's
312
- * theme CSS variables resolve outside the shell, and its own `jp-xtralab-Omnibox`
313
- * class for the overlay/panel styling (style/omnibox.css).
301
+ * The omnibox overlay widget, attached to `document.body`; `jp-ThemedContainer`
302
+ * makes JupyterLab's theme CSS variables resolve outside the shell.
314
303
  */
315
304
  export class OmniboxWidget extends ReactWidget {
316
305
  constructor(options: OmniboxWidget.IOptions) {
@@ -321,6 +310,9 @@ export class OmniboxWidget extends ReactWidget {
321
310
  this.addClass('jp-ThemedContainer');
322
311
  }
323
312
 
313
+ /**
314
+ * Render the omnibox overlay content.
315
+ */
324
316
  render(): JSX.Element {
325
317
  return <OmniboxComponent {...this._options} />;
326
318
  }
@@ -328,20 +320,23 @@ export class OmniboxWidget extends ReactWidget {
328
320
  private _options: OmniboxWidget.IOptions;
329
321
  }
330
322
 
331
- /**
332
- * A namespace for `OmniboxWidget` statics.
333
- */
334
323
  export namespace OmniboxWidget {
335
324
  /**
336
325
  * Construction options for {@link OmniboxWidget}.
337
326
  */
338
327
  export interface IOptions {
328
+ /**
329
+ * The command registry used to look up and execute commands.
330
+ */
339
331
  commands: CommandRegistry;
340
332
  /**
341
333
  * Snapshot of the command palette's items, read once when the overlay
342
334
  * opens; empty when the palette isn't available.
343
335
  */
344
336
  paletteItems: ReadonlyArray<CommandPalette.IItem>;
337
+ /**
338
+ * The document registry used to pick file icons.
339
+ */
345
340
  docRegistry: DocumentRegistry;
346
341
  /**
347
342
  * Snapshot of the available agents, read once when the overlay opens.
@@ -11,34 +11,16 @@ const PLUGIN_ID = 'xtralab:search-replace';
11
11
  const COMMAND_ID = 'xtralab:activate-search-replace';
12
12
 
13
13
  /**
14
- * Widget id of the panel contributed by `jupyterlab-search-replace`. The
15
- * extension adds it to the left sidebar but registers no command of its own,
16
- * so there is nothing to bind a shortcut to out of the box — this plugin adds
17
- * the missing command.
14
+ * Widget id of the `jupyterlab-search-replace` left-sidebar panel; the
15
+ * extension registers no command of its own to bind a shortcut to.
18
16
  */
19
17
  const SEARCH_REPLACE_WIDGET_ID = 'jp-search-replace';
20
18
 
21
19
  /**
22
- * Bind Accel+Shift+F (Cmd+Shift+F on macOS, Ctrl+Shift+F elsewhere) to the
23
- * project-wide Search and Replace panel, matching the equivalent shortcut in
24
- * editors like VS Code.
25
- *
26
- * Two pieces are needed because that chord is already taken: JupyterLab core
27
- * binds Accel+Shift+F to `filebrowser:toggle-main`.
28
- *
29
- * 1. The chord is freed declaratively. `SettingRegistry.reconcileShortcuts`
30
- * keys collisions and disables on (keys, selector) — not on command — so the
31
- * `@jupyterlab/shortcuts-extension:shortcuts` override in
32
- * `default_setting_overrides.d/00-xtralab.json` disables the file-browser
33
- * binding. The file browser stays reachable from its sidebar tab and the
34
- * View menu; only the keyboard shortcut moves.
35
- *
36
- * 2. The new binding is added imperatively below rather than as another shipped
37
- * default. A `disabled` default and a re-binding default land on the same
38
- * (keys, selector) slot, and the disable suppresses every default on that
39
- * slot — including the re-binding. `commands.addKeyBinding` runs outside the
40
- * reconcile pass, so it is unaffected and wins cleanly once the file-browser
41
- * binding is gone.
20
+ * Bind Accel+Shift+F to the Search and Replace panel (VS Code parity). Core
21
+ * binds the chord to `filebrowser:toggle-main`; a `default_setting_overrides.d`
22
+ * override disables it. The re-binding is imperative: `reconcileShortcuts` keys
23
+ * on (keys, selector), so the disable would suppress a shipped default too.
42
24
  */
43
25
  const plugin: JupyterFrontEndPlugin<void> = {
44
26
  id: PLUGIN_ID,
@@ -60,9 +42,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
60
42
  label: trans.__('Search and Replace'),
61
43
  caption: trans.__('Show the Search and Replace panel'),
62
44
  execute: () => {
63
- // `activateById` expands the left sidebar when collapsed, selects the
64
- // tab, and the widget focuses its search input from `onAfterShow`, so
65
- // the user can start typing a query immediately. It no-ops if
45
+ // The widget focuses its search input from `onAfterShow`; no-ops if
66
46
  // `jupyterlab-search-replace` is unavailable.
67
47
  labShell.activateById(SEARCH_REPLACE_WIDGET_ID);
68
48
  }
@@ -12,24 +12,12 @@ import { coerceData } from '../mimeData';
12
12
 
13
13
  const PLUGIN_ID = 'xtralab:show-output';
14
14
 
15
- /**
16
- * Render arbitrary rich content into a panel, with no notebook and no kernel.
17
- */
18
15
  const SHOW_COMMAND = 'xtralab:show';
19
16
 
20
17
  /**
21
- * Contribute `xtralab:show`: render a single MIME bundle into a main-area
22
- * panel using the application's render-mime registry.
23
- *
24
- * This is the kernel-free counterpart to running a notebook. A coding agent
25
- * generates the content itself (a Vega-Lite spec, a Markdown explainer, an
26
- * HTML fragment, an SVG, a base64 image) and hands it to this command, which
27
- * renders it with the same renderers JupyterLab uses for cell output and docks
28
- * the result beside the code. Nothing is written to disk and no kernel is
29
- * required, so it works even where the kernel has no plotting libraries.
30
- *
31
- * Repeated calls with the same `id` reuse one panel, so a walkthrough can
32
- * refresh the view in place instead of piling up tabs.
18
+ * Contributes `xtralab:show`: render a single MIME bundle into a panel with
19
+ * the same renderers JupyterLab uses for cell output — no kernel, no file on
20
+ * disk. Repeated calls with the same `id` reuse one panel.
33
21
  */
34
22
  const plugin: JupyterFrontEndPlugin<void> = {
35
23
  id: PLUGIN_ID,
@@ -48,7 +36,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
48
36
  const { commands } = app;
49
37
  const trans = (translator ?? nullTranslator).load('jupyterlab');
50
38
 
51
- // One reusable panel per `id`, so repeated shows refresh in place.
52
39
  const panels = new Map<string, Panel>();
53
40
 
54
41
  commands.addCommand(SHOW_COMMAND, {
@@ -120,9 +107,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
120
107
 
121
108
  const model = rendermime.createModel({ data: bundle, trusted: true });
122
109
  const renderer = rendermime.createRenderer(chosen);
123
- // A scrollable Panel, not a MainAreaWidget: MainAreaWidget does not lay
124
- // out its content in the side area, and the panel needs to scroll when
125
- // the content is taller than it (style/showOutput.css).
110
+ // Not a MainAreaWidget: it does not lay out its content in the side
111
+ // area; the panel scrolls via style/showOutput.css.
126
112
  const widget = new Panel();
127
113
  widget.addClass('jp-xtralab-ShowPanel');
128
114
  widget.id = `xtralab-show-${id}`;
@@ -130,7 +116,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
130
116
  widget.title.closable = true;
131
117
  widget.addWidget(renderer);
132
118
 
133
- // Replace any existing panel with this id so shows refresh in place.
134
119
  const existing = panels.get(id);
135
120
  if (existing && !existing.isDisposed) {
136
121
  existing.dispose();
@@ -143,8 +128,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
143
128
  });
144
129
 
145
130
  // Attach before rendering so renderers that need layout (Vega) size
146
- // correctly. Default to the side area so the editor keeps full width;
147
- // only split the document area when `area` is "main".
131
+ // correctly.
148
132
  if (area === 'main') {
149
133
  labShell.add(widget, 'main', {
150
134
  mode,
@@ -10,21 +10,61 @@ import type { Widget } from '@lumino/widgets';
10
10
  const PLUGIN_ID = 'xtralab:sidebar';
11
11
 
12
12
  /**
13
- * Each entry pairs an upstream sidebar widget id with the rank it was
14
- * originally added at. The ranks come from the upstream extensions —
15
- * `filebrowser-extension` adds the default browser at rank 100 and
16
- * `running-extension` adds the running panel at rank 200 — and we reuse
17
- * them when re-adding so the tab lands back in its usual position.
13
+ * `rank` is the fallback used when re-adding a hidden tab; the shipped
14
+ * `layout` setting normally wins because `LabShell.add` merges its
15
+ * per-widget options over the ones passed in.
18
16
  */
19
17
  interface ITarget {
18
+ /**
19
+ * The shell widget id of the sidebar tab.
20
+ */
20
21
  id: string;
22
+ /**
23
+ * The sidebar rank used when re-adding the hidden tab.
24
+ */
21
25
  rank: number;
22
- settingKey: 'showDefaultFileBrowser' | 'showRunningSessions';
26
+ /**
27
+ * The boolean plugin setting controlling the tab's visibility.
28
+ */
29
+ settingKey:
30
+ | 'showTerminals'
31
+ | 'showFileBrowser'
32
+ | 'showGitPanel'
33
+ | 'showDefaultFileBrowser'
34
+ | 'showRunningSessions'
35
+ | 'showSearchReplace';
36
+ /**
37
+ * The id of the toggle command registered for the tab.
38
+ */
23
39
  command: string;
40
+ /**
41
+ * The localized label of the toggle command.
42
+ */
24
43
  label: (trans: ReturnType<ITranslator['load']>) => string;
25
44
  }
26
45
 
27
46
  const TARGETS: ITarget[] = [
47
+ {
48
+ id: 'xtralab-running-terminals',
49
+ rank: 1,
50
+ settingKey: 'showTerminals',
51
+ command: 'xtralab:toggle-terminals',
52
+ label: trans => trans.__('Terminals')
53
+ },
54
+ {
55
+ id: 'xtralab:file-browser',
56
+ rank: 2,
57
+ settingKey: 'showFileBrowser',
58
+ command: 'xtralab:toggle-file-browser',
59
+ label: trans => trans.__('File Browser')
60
+ },
61
+ {
62
+ id: 'jp-git-sessions',
63
+ rank: 200,
64
+ settingKey: 'showGitPanel',
65
+ command: 'xtralab:toggle-git-panel',
66
+ label: trans => trans.__('Git')
67
+ },
28
68
  {
29
69
  id: 'filebrowser',
30
70
  rank: 100,
@@ -38,28 +78,26 @@ const TARGETS: ITarget[] = [
38
78
  settingKey: 'showRunningSessions',
39
79
  command: 'xtralab:toggle-running-sessions',
40
80
  label: trans => trans.__('Running Terminals and Kernels')
81
+ },
82
+ {
83
+ id: 'jp-search-replace',
84
+ rank: 900,
85
+ settingKey: 'showSearchReplace',
86
+ command: 'xtralab:toggle-search-replace',
87
+ label: trans => trans.__('Search and Replace')
41
88
  }
42
89
  ];
43
90
 
44
91
  /**
45
- * Lets the user toggle individual left-sidebar tabs from
46
- * View > Appearance > Left Sidebar. Removing a widget from a `LabShell`
47
- * side area is done by setting `widget.parent = null`: the StackedPanel
48
- * emits `widgetRemoved`, which the SideBarHandler responds to by removing
49
- * the matching tab. The widget instance is preserved, so we can re-add it
50
- * later via `labShell.add` with its original rank.
51
- *
52
- * The menu placement is contributed declaratively in
53
- * `schema/sidebar.json`, so this plugin only registers commands and
54
- * applies state — it does not touch `IMainMenu` directly.
55
- *
56
- * The preference is persisted to `xtralab:sidebar` settings so the choice
57
- * survives reloads.
92
+ * Toggles individual sidebar tabs from View > Appearance > Sidebars. Hiding
93
+ * detaches the widget while keeping the instance for a later re-add; which
94
+ * side it came from is only tracked in-memory, so after a reload a re-shown
95
+ * tab lands on the left. Menu placement is declared in schema/sidebar.json.
58
96
  */
59
97
  const plugin: JupyterFrontEndPlugin<void> = {
60
98
  id: PLUGIN_ID,
61
99
  description:
62
- 'Toggle visibility of selected sidebar tabs (default file browser, running sessions) from the View menu.',
100
+ 'Toggle visibility of individual sidebar tabs from the View menu.',
63
101
  autoStart: true,
64
102
  requires: [ILabShell],
65
103
  optional: [ISettingRegistry, ITranslator],
@@ -72,31 +110,38 @@ const plugin: JupyterFrontEndPlugin<void> = {
72
110
  const trans = (translator ?? nullTranslator).load('jupyterlab');
73
111
  const { commands } = app;
74
112
 
75
- // Cache widget instances so we can re-add them after `widget.parent =
76
- // null` has detached them from the sidebar. The widget is no longer
77
- // reachable through `labShell.widgets('left')` once removed, so a
78
- // missing cache entry would leave us unable to bring the tab back.
113
+ // Detached widgets are unreachable via `labShell.widgets(...)`; keep
114
+ // instances here so hidden tabs can be re-added.
79
115
  const widgetCache = new Map<string, Widget>();
80
116
 
81
- const findInSidebar = (id: string): Widget | null => {
82
- for (const w of labShell.widgets('left')) {
83
- if (w.id === id) {
84
- return w;
117
+ const hiddenFrom = new Map<string, 'left' | 'right'>();
118
+
119
+ const findInSidebars = (
120
+ id: string
121
+ ): { widget: Widget; area: 'left' | 'right' } | null => {
122
+ for (const area of ['left', 'right'] as const) {
123
+ for (const widget of labShell.widgets(area)) {
124
+ if (widget.id === id) {
125
+ return { widget, area };
126
+ }
85
127
  }
86
128
  }
87
129
  return null;
88
130
  };
89
131
 
132
+ const locate = (id: string): 'left' | 'right' | null =>
133
+ findInSidebars(id)?.area ?? null;
134
+
90
135
  const captureWidget = (id: string): Widget | null => {
91
136
  const cached = widgetCache.get(id);
92
137
  if (cached && !cached.isDisposed) {
93
138
  return cached;
94
139
  }
95
- const found = findInSidebar(id);
140
+ const found = findInSidebars(id);
96
141
  if (found) {
97
- widgetCache.set(id, found);
142
+ widgetCache.set(id, found.widget);
98
143
  }
99
- return found;
144
+ return found?.widget ?? null;
100
145
  };
101
146
 
102
147
  let settings: ISettingRegistry.ISettings | null = null;
@@ -112,19 +157,23 @@ const plugin: JupyterFrontEndPlugin<void> = {
112
157
  const apply = (target: ITarget): void => {
113
158
  const widget = captureWidget(target.id);
114
159
  if (!widget) {
115
- // The upstream widget hasn't been added yet (or the extension is
116
- // disabled). Nothing to do — the next time `apply` runs and finds
117
- // it, the preference will take effect.
118
160
  return;
119
161
  }
120
- const present = findInSidebar(target.id) !== null;
162
+ const area = locate(target.id);
163
+ if (!area && widget.parent !== null) {
164
+ // Attached outside the sidebars — leave it alone.
165
+ return;
166
+ }
121
167
  const wantPresent = readPreference(target);
122
- if (wantPresent && !present) {
123
- labShell.add(widget, 'left', { rank: target.rank });
124
- } else if (!wantPresent && present) {
125
- // Setting `parent = null` triggers SideBarHandler's
126
- // `_onWidgetRemoved`, which strips the tab while leaving the
127
- // widget instance alive for a later re-add.
168
+ if (wantPresent && !area) {
169
+ labShell.add(widget, hiddenFrom.get(target.id) ?? 'left', {
170
+ rank: target.rank
171
+ });
172
+ hiddenFrom.delete(target.id);
173
+ } else if (!wantPresent && area) {
174
+ hiddenFrom.set(target.id, area);
175
+ // Detaching makes the SideBarHandler remove the tab while the
176
+ // widget instance stays alive for a later re-add.
128
177
  widget.parent = null;
129
178
  }
130
179
  };
@@ -139,14 +188,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
139
188
  commands.addCommand(target.command, {
140
189
  label: target.label(trans),
141
190
  isToggled: () => readPreference(target),
191
+ isVisible: () => {
192
+ // List only tabs this plugin can manage: in a sidebar, or
193
+ // hidden by this plugin (`parent === null`).
194
+ const widget = captureWidget(target.id);
195
+ return (
196
+ widget !== null &&
197
+ (widget.parent === null || locate(target.id) !== null)
198
+ );
199
+ },
142
200
  execute: async () => {
143
201
  const next = !readPreference(target);
144
202
  if (settings) {
145
203
  try {
146
204
  await settings.set(target.settingKey, next);
147
- // `settings.changed` will fire and drive `apply` + the
148
- // command-state refresh. Returning here keeps the toggle's
149
- // single source of truth in the settings registry.
205
+ // `settings.changed` drives `apply` and the state refresh.
150
206
  return;
151
207
  } catch (reason) {
152
208
  console.error(
@@ -155,9 +211,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
155
211
  );
156
212
  }
157
213
  }
158
- // No settings registry available (or the write failed): fall
159
- // back to applying the change in-memory so the toggle still
160
- // works for the current session.
214
+ // No settings (or the write failed): apply in-memory for this session.
161
215
  apply(target);
162
216
  commands.notifyCommandChanged(target.command);
163
217
  }
@@ -179,11 +233,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
179
233
  }
180
234
 
181
235
  void app.restored.then(() => {
182
- // Capture references first so a hidden-on-startup widget is still
183
- // recoverable later. `apply` would also do the lookup, but only on
184
- // the targets that need to stay visible — by capturing every target
185
- // here we keep the cache populated for both directions of the
186
- // toggle.
236
+ // Capture every widget before applying, so tabs hidden on startup
237
+ // stay recoverable.
187
238
  for (const target of TARGETS) {
188
239
  captureWidget(target.id);
189
240
  }
@@ -22,16 +22,28 @@ const OSC_RXVT_NOTIFY = 777;
22
22
 
23
23
  const MAX_TEXT_LENGTH = 256;
24
24
 
25
- // The bits of an xterm.js `Terminal` we reach into. JupyterLab's terminal
26
- // widget keeps its xterm in a private `_term` field and does not expose these
27
- // hooks, so a structural type uses them without depending on `@xterm/xterm`. If
28
- // the field is ever renamed, `_term` is `undefined` and notifications stop, with
29
- // a console warning per terminal.
25
+ // JupyterLab's terminal keeps its xterm in a private `_term` field; structural
26
+ // types use its hooks without `@xterm/xterm`, going inert (warning) if renamed.
27
+ /**
28
+ * A handle returned by an xterm listener registration.
29
+ */
30
30
  interface IXtermDisposable {
31
+ /**
32
+ * Unregister the listener.
33
+ */
31
34
  dispose(): void;
32
35
  }
36
+ /**
37
+ * The subset of the xterm.js API used to observe bells and OSC sequences.
38
+ */
33
39
  interface IXtermTerminal {
40
+ /**
41
+ * Register a handler invoked when the terminal bell rings.
42
+ */
34
43
  onBell(handler: () => void): IXtermDisposable;
44
+ /**
45
+ * The escape-sequence parser used to register OSC handlers.
46
+ */
35
47
  parser: {
36
48
  registerOscHandler(
37
49
  ident: number,
@@ -39,8 +51,17 @@ interface IXtermTerminal {
39
51
  ): IXtermDisposable;
40
52
  };
41
53
  }
54
+ /**
55
+ * A JupyterLab terminal widget content with its private xterm exposed.
56
+ */
42
57
  interface ITerminalContentInternals extends ITerminal.ITerminal {
58
+ /**
59
+ * A promise resolving once the underlying xterm exists.
60
+ */
43
61
  ready: Promise<void>;
62
+ /**
63
+ * The private xterm instance; `undefined` if upstream renames the field.
64
+ */
44
65
  _term?: IXtermTerminal;
45
66
  }
46
67
 
@@ -48,26 +69,30 @@ type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
48
69
 
49
70
  // The renderer→main bridge the desktop shell injects on the lab window. Absent
50
71
  // for pip-install users in a browser, who fall back to web Notifications.
72
+ /**
73
+ * The desktop notification API exposed on `window.xtralab`.
74
+ */
51
75
  interface IDesktopBridge {
76
+ /**
77
+ * Show a native notification, tagged with the emitting terminal session.
78
+ */
52
79
  notify?: (
53
80
  title: string,
54
81
  body: string,
55
82
  session?: string
56
83
  ) => Promise<void> | void;
84
+ /**
85
+ * Register a callback invoked with the tagged terminal session name when
86
+ * the user clicks a notification.
87
+ */
57
88
  onFocusTerminal?: (callback: (session: string) => void) => void;
58
89
  }
59
90
 
60
91
  /**
61
- * Turns the notifications coding agents already emit (OSC 9, OSC 777, the bell)
62
- * into desktop notifications. JupyterLab's xterm renders these sequences but
63
- * never forwards them to the OS, so this plugin hooks each terminal and bridges
64
- * them through `window.xtralab.notify` (desktop) or the web Notifications API.
65
- *
66
- * A notification is suppressed while its terminal is the focused, active tab,
67
- * and throttled per terminal. The desktop shell advertises
68
- * `TERM_PROGRAM=iTerm.app` so agents emit OSC 9 in the first place, and the
69
- * session name is forwarded so clicking a notification focuses the terminal
70
- * that fired it.
92
+ * Turns the OSC 9 / OSC 777 / bell sequences agents emit into desktop
93
+ * notifications via `window.xtralab.notify` (desktop) or the web Notifications
94
+ * API — JupyterLab's xterm renders them but never forwards them to the OS.
95
+ * The desktop shell advertises `TERM_PROGRAM=iTerm.app` so agents emit OSC 9.
71
96
  */
72
97
  const plugin: JupyterFrontEndPlugin<void> = {
73
98
  id: PLUGIN_ID,
@@ -87,13 +112,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
87
112
  let enabled = true;
88
113
  let notifyOnBell = true;
89
114
 
90
- // Per-widget throttle timestamps and xterm hooks. WeakMaps let disposed
91
- // widgets be collected; hooks are torn down when a tab closes.
92
115
  const lastNotified = new WeakMap<TerminalWidget, number>();
93
116
  const hooks = new WeakMap<TerminalWidget, IXtermDisposable[]>();
94
117
 
95
- // True when the user is already looking at this terminal, so a banner would
96
- // just be noise.
97
118
  const isActivelyViewing = (widget: TerminalWidget): boolean =>
98
119
  document.hasFocus() && app.shell.currentWidget === widget;
99
120
 
@@ -107,7 +128,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
107
128
  );
108
129
  return;
109
130
  }
110
- // Plain-browser fallback (pip install): the Web Notifications API.
111
131
  if (typeof Notification === 'undefined') {
112
132
  return;
113
133
  }
@@ -146,9 +166,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
146
166
  widget.title.label || trans.__('Terminal');
147
167
 
148
168
  const onOsc9 = (widget: TerminalWidget, data: string): boolean => {
149
- // OSC 9 is overloaded: ConEmu/Windows Terminal use `9 ; <n> ; …`
150
- // subcommands (e.g. `9 ; 4` progress), so skip a numeric-subcommand
151
- // payload and leave it unconsumed rather than treat it as a notification.
169
+ // OSC 9 is overloaded: ConEmu/Windows Terminal use numeric subcommands
170
+ // (e.g. `9 ; 4` progress); leave those unconsumed.
152
171
  if (/^\d+(;|$)/.test(data)) {
153
172
  return false;
154
173
  }
@@ -197,8 +216,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
197
216
  );
198
217
  return;
199
218
  }
200
- // Handlers are wrapped so a parse slip cannot break xterm's parser; an
201
- // OSC handler returns whether it consumed the sequence.
202
219
  const disposables: IXtermDisposable[] = [
203
220
  term.onBell(() => guard(() => onBell(widget))),
204
221
  term.parser.registerOscHandler(OSC_ITERM2_GROWL, data =>