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
@@ -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,
@@ -15,8 +15,17 @@ const PLUGIN_ID = 'xtralab:sidebar';
15
15
  * per-widget options over the ones passed in.
16
16
  */
17
17
  interface ITarget {
18
+ /**
19
+ * The shell widget id of the sidebar tab.
20
+ */
18
21
  id: string;
22
+ /**
23
+ * The sidebar rank used when re-adding the hidden tab.
24
+ */
19
25
  rank: number;
26
+ /**
27
+ * The boolean plugin setting controlling the tab's visibility.
28
+ */
20
29
  settingKey:
21
30
  | 'showTerminals'
22
31
  | 'showFileBrowser'
@@ -24,7 +33,13 @@ interface ITarget {
24
33
  | 'showDefaultFileBrowser'
25
34
  | 'showRunningSessions'
26
35
  | 'showSearchReplace';
36
+ /**
37
+ * The id of the toggle command registered for the tab.
38
+ */
27
39
  command: string;
40
+ /**
41
+ * The localized label of the toggle command.
42
+ */
28
43
  label: (trans: ReturnType<ITranslator['load']>) => string;
29
44
  }
30
45
 
@@ -74,13 +89,10 @@ const TARGETS: ITarget[] = [
74
89
  ];
75
90
 
76
91
  /**
77
- * Toggles individual sidebar tabs from View > Appearance > Sidebars,
78
- * whichever side a tab currently lives on. Hiding detaches the widget
79
- * (`widget.parent = null`) while keeping the instance for a later
80
- * re-add; the preference persists in this plugin's settings. Which side
81
- * a hidden tab came from is only tracked in-memory, so after a reload a
82
- * re-shown tab lands on the left. The menu placement is contributed
83
- * declaratively in `schema/sidebar.json`.
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.
84
96
  */
85
97
  const plugin: JupyterFrontEndPlugin<void> = {
86
98
  id: PLUGIN_ID,
@@ -102,8 +114,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
102
114
  // instances here so hidden tabs can be re-added.
103
115
  const widgetCache = new Map<string, Widget>();
104
116
 
105
- // Side each hidden widget was removed from, so re-showing restores it
106
- // there.
107
117
  const hiddenFrom = new Map<string, 'left' | 'right'>();
108
118
 
109
119
  const findInSidebars = (
@@ -147,7 +157,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
147
157
  const apply = (target: ITarget): void => {
148
158
  const widget = captureWidget(target.id);
149
159
  if (!widget) {
150
- // Not seen yet (extension disabled or not added) — nothing to do.
151
160
  return;
152
161
  }
153
162
  const area = locate(target.id);
@@ -202,8 +211,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
202
211
  );
203
212
  }
204
213
  }
205
- // No settings registry (or the write failed): apply in-memory
206
- // for the current session only.
214
+ // No settings (or the write failed): apply in-memory for this session.
207
215
  apply(target);
208
216
  commands.notifyCommandChanged(target.command);
209
217
  }
@@ -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 =>
@@ -8,35 +8,23 @@ import type { SessionRegistry, TerminalWidget } from './model';
8
8
  import type { IAgentTerminalSession, IAgentTerminals } from './tokens';
9
9
 
10
10
  /**
11
- * Pause between pasting the prompt and pressing Enter, so the two reach the
12
- * agent as separate PTY reads — the same shape as a human pasting and then
13
- * submitting. Sent back to back, the server can coalesce both websocket
14
- * messages into one read and the TUI would have to split the paste from the
15
- * trailing Enter inside a single chunk.
11
+ * Pause between pasting the prompt and pressing Enter so the server cannot
12
+ * coalesce both websocket messages into one PTY read.
16
13
  */
17
14
  const PASTE_SUBMIT_DELAY_MS = 150;
18
15
 
19
16
  /**
20
- * Bracketed-paste markers (`CSI 200~` / `CSI 201~`). The prompt is wrapped
21
- * in these so the agent's TUI treats it as one pasted block — newlines
22
- * insert into its input box instead of submitting the message at each line
23
- * break. Every send target is a terminal where detection just confirmed a
24
- * running coding agent, and the agent TUIs keep bracketed-paste mode on.
17
+ * Bracketed-paste markers (`CSI 200~` / `CSI 201~`): a wrapped prompt arrives
18
+ * as one pasted block, so newlines insert instead of submitting at each break.
25
19
  */
26
20
  const PASTE_OPEN = '\x1b[200~';
27
21
  const PASTE_CLOSE = '\x1b[201~';
28
22
 
29
- /**
30
- * How long to wait for the session's websocket to reach `connected` before
31
- * failing the send, so a server hiccup surfaces as an error notification
32
- * rather than a silent hang.
33
- */
34
23
  const CONNECT_TIMEOUT_MS = 10_000;
35
24
 
36
25
  /**
37
26
  * Resolve once `session` reports `connected`; reject with `timeoutMessage`
38
- * after {@link CONNECT_TIMEOUT_MS}. Matches how the launcher waits before
39
- * typing into a fresh terminal — writes sent while the websocket is still
27
+ * after {@link CONNECT_TIMEOUT_MS} — writes sent while the websocket is still
40
28
  * connecting disappear silently.
41
29
  */
42
30
  function waitUntilConnected(
@@ -67,11 +55,8 @@ function waitUntilConnected(
67
55
 
68
56
  /**
69
57
  * The {@link IAgentTerminals} implementation: a read-only view over the
70
- * terminals panel's {@link SessionRegistry} narrowed to sessions with a
71
- * detection-confirmed coding agent, plus the "send a prompt into one"
72
- * action. Lives in the `xtralab:terminals` plugin so the ask-agent popup
73
- * (and any future caller) can target running agents without duplicating the
74
- * detection plumbing.
58
+ * panel's {@link SessionRegistry} narrowed to sessions with a
59
+ * detection-confirmed coding agent, plus the "send a prompt into one" action.
75
60
  */
76
61
  export class AgentTerminals implements IAgentTerminals {
77
62
  constructor(options: AgentTerminals.IOptions) {
@@ -84,6 +69,9 @@ export class AgentTerminals implements IAgentTerminals {
84
69
  this._registry.stateChanged.connect(this._onRegistryStateChanged, this);
85
70
  }
86
71
 
72
+ /**
73
+ * Snapshot of the sessions with a detection-confirmed running agent.
74
+ */
87
75
  sessions(): IAgentTerminalSession[] {
88
76
  const result: IAgentTerminalSession[] = [];
89
77
  for (const name of this._registry.sessionNames()) {
@@ -101,15 +89,20 @@ export class AgentTerminals implements IAgentTerminals {
101
89
  return result;
102
90
  }
103
91
 
92
+ /**
93
+ * A signal emitted whenever the {@link sessions} snapshot may have changed.
94
+ */
104
95
  get changed(): ISignal<IAgentTerminals, void> {
105
96
  return this._changed;
106
97
  }
107
98
 
99
+ /**
100
+ * Paste the prompt into the named session and press Enter, re-validating
101
+ * with the server first that an agent still runs there.
102
+ */
108
103
  async sendPrompt(name: string, prompt: string): Promise<void> {
109
- // Re-validate against the server rather than trusting the caller's
110
- // (possibly seconds-old) snapshot: the prompt is prose, and prose pasted
111
- // into the shell prompt left behind by an exited agent would be
112
- // *executed* on the Enter below.
104
+ // Re-validate against the server: prose pasted into the shell prompt an
105
+ // exited agent left behind would be *executed* on the Enter below.
113
106
  await this._terminals.refreshRunning();
114
107
  const alive = Array.from(this._terminals.running()).some(
115
108
  model => model.name === name
@@ -119,9 +112,8 @@ export class AgentTerminals implements IAgentTerminals {
119
112
  }
120
113
  const detected = await fetchRunningAgents(this._detectCommands());
121
114
  if (detected === null) {
122
- // Detection being unavailable (older server, transient failure) is
123
- // not proof the agent is gone; refuse the write with a message that
124
- // invites the retry that would succeed.
115
+ // Detection unavailable (older server, transient failure) is not proof
116
+ // the agent is gone; refuse the write and invite a retry.
125
117
  throw new Error(
126
118
  this._trans.__('Could not verify the terminal — try sending again.')
127
119
  );
@@ -133,11 +125,6 @@ export class AgentTerminals implements IAgentTerminals {
133
125
  );
134
126
  }
135
127
 
136
- // Deliver in the background: write straight to the session over its
137
- // websocket instead of revealing its tab, so focus stays wherever the
138
- // user is asking from. An open tab's live connection is reused; a
139
- // closed tab gets a short-lived client connection to the running server
140
- // session, without creating any widget.
141
128
  const widget = this._findWidget(name);
142
129
  const session =
143
130
  widget !== null
@@ -148,12 +135,8 @@ export class AgentTerminals implements IAgentTerminals {
148
135
  session,
149
136
  this._trans.__('Timed out connecting to the terminal.')
150
137
  );
151
- // Wrapped and newline-normalised exactly like xterm's own paste
152
- // handling. Writing the markers directly (rather than through an open
153
- // tab's `Terminal.paste`) also sidesteps the one case paste() gets
154
- // wrong: a freshly reconnected xterm whose replayed scrollback no
155
- // longer holds the agent's original mode-setting escape, which makes
156
- // paste() skip the wrapping the application still expects.
138
+ // Writing the markers directly (not `Terminal.paste`) sidesteps paste()
139
+ // skipping them on a reconnected xterm that lost the mode-setting escape.
157
140
  session.send({
158
141
  type: 'stdin',
159
142
  content: [PASTE_OPEN + prompt.replace(/\r?\n/g, '\r') + PASTE_CLOSE]
@@ -162,16 +145,14 @@ export class AgentTerminals implements IAgentTerminals {
162
145
  window.setTimeout(resolve, PASTE_SUBMIT_DELAY_MS)
163
146
  );
164
147
  if (session.isDisposed) {
165
- // The tab closed during the pause; the pasted text already reached
166
- // the agent's input box but Enter can no longer be delivered here.
167
148
  throw new Error(
168
149
  this._trans.__('The terminal closed before the prompt was submitted.')
169
150
  );
170
151
  }
171
152
  session.send({ type: 'stdin', content: ['\r'] });
172
153
  } finally {
173
- // Close a connection we opened just for this send. Disposing the
174
- // client connection leaves the server session (and its agent) running.
154
+ // Disposing an ad-hoc client connection leaves the server session
155
+ // (and its agent) running.
175
156
  if (widget === null && !session.isDisposed) {
176
157
  session.dispose();
177
158
  }
@@ -209,27 +190,22 @@ export namespace AgentTerminals {
209
190
  * The panel's session registry — the source of the session snapshot.
210
191
  */
211
192
  registry: SessionRegistry;
212
-
213
193
  /**
214
194
  * Tracker of open terminal widgets, to reuse an open tab's connection.
215
195
  */
216
196
  tracker: ITerminalTracker;
217
-
218
197
  /**
219
198
  * The terminal session manager, for validation and ad-hoc connections.
220
199
  */
221
200
  terminals: Terminal.IManager;
222
-
223
201
  /**
224
202
  * Names to detect — same list the registry polls with.
225
203
  */
226
204
  detectCommands: () => string[];
227
-
228
205
  /**
229
206
  * Whether a detected command belongs to a coding agent (not an editor).
230
207
  */
231
208
  isAgentCommand: (command: string) => boolean;
232
-
233
209
  /**
234
210
  * Translation bundle for the error messages thrown to callers.
235
211
  */
@@ -2,14 +2,10 @@ import { URLExt } from '@jupyterlab/coreutils';
2
2
  import { ServerConnection } from '@jupyterlab/services';
3
3
 
4
4
  /**
5
- * Ask the xtralab server which of `commands` is currently running inside each
6
- * open terminal session. Resolves to a map from session name to the matched
7
- * agent command, or `null` for a session sitting at its prompt.
8
- *
9
- * On any failure (endpoint missing on an older server, network error,
10
- * malformed response) the whole result is `null` — callers should treat that
11
- * as "detection unavailable" and fall back to the optimistic launch tags
12
- * rather than clearing any badges.
5
+ * Ask the xtralab server which of `commands` runs in each open terminal
6
+ * session: a map from session name to matched command (`null` = idle). Any
7
+ * failure resolves the whole result to `null` — "detection unavailable",
8
+ * not "no agents"; callers must not clear badges on it.
13
9
  */
14
10
  export async function fetchRunningAgents(
15
11
  commands: string[]