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
@@ -43,99 +43,43 @@ import {
43
43
 
44
44
  const PLUGIN_ID = 'xtralab:ask-agent';
45
45
 
46
- /**
47
- * Command id that reveals the queued-prompts panel.
48
- */
49
46
  const QUEUE_PANEL_COMMAND = 'xtralab:ask-agent-queue';
50
47
 
51
- /**
52
- * Widget id of the queued-prompts side panel.
53
- */
54
48
  const QUEUE_PANEL_ID = 'xtralab-ask-agent-queue';
55
49
 
56
- /**
57
- * State-database key remembering the last agent picked in the popup.
58
- */
59
50
  const LAST_AGENT_STATE_KEY = 'xtralab:ask-agent:agent';
60
51
 
61
- /**
62
- * State-database key remembering the last target picked in the popup.
63
- */
64
52
  const LAST_TARGET_STATE_KEY = 'xtralab:ask-agent:target';
65
53
 
66
- /**
67
- * State-database key holding the queued prompts across page loads.
68
- */
69
54
  const QUEUE_STATE_KEY = 'xtralab:ask-agent:queue';
70
55
 
71
- /**
72
- * Debounce for mirroring the queue into the state database, so typing in
73
- * the panel's textareas does not issue a write per keystroke.
74
- */
75
56
  const QUEUE_PERSIST_MS = 500;
76
57
 
77
- /**
78
- * Stored target value for "start the agent in a new terminal".
79
- */
80
58
  const NEW_TARGET_VALUE = 'new';
81
59
 
82
- /**
83
- * Stored-target prefix for an existing session; the session name follows.
84
- */
85
60
  const SESSION_TARGET_PREFIX = 'session:';
86
61
 
87
62
  /**
88
- * Delay between a selection change and showing the pill, so the affordance
89
- * appears once the selection settles rather than flickering along a drag.
63
+ * Delay before showing the pill, so it does not flicker along a drag.
90
64
  */
91
65
  const SELECTION_SETTLE_MS = 250;
92
66
 
93
67
  /**
94
- * Keystroke that opens the popup in a file editor. `Accel I` would be the
95
- * familiar "inline chat" chord but CodeMirror's default keymap owns `Mod-i`
96
- * (selectParentSyntax) inside editors and prevents-default before Lumino
97
- * sees it; `Accel .` — the "quick fix" chord elsewhere — is free in the
98
- * editor, the browser and JupyterLab.
68
+ * `Accel I` would be the familiar "inline chat" chord but CodeMirror's
69
+ * default keymap owns `Mod-i` and prevents-default before Lumino sees it;
70
+ * `Accel .` is free in the editor, the browser and JupyterLab.
99
71
  */
100
72
  const ASK_AGENT_KEYS = 'Accel .';
101
73
 
102
- /**
103
- * Gap between the pill and the selection anchor, in pixels.
104
- */
105
74
  const PILL_GAP = 6;
106
75
 
107
- /**
108
- * Minimum distance kept between the pill and the viewport edges.
109
- */
110
76
  const PILL_VIEWPORT_MARGIN = 8;
111
77
 
112
78
  /**
113
- * Select code in a file editor (or pick diff lines) and prompt a coding
114
- * agent about it.
115
- *
116
- * The plugin watches the document selection: a non-empty selection inside a
117
- * CodeMirror file editor or notebook cell grows a small floating "Ask agent"
118
- * pill next to the selection. Clicking it (or running `xtralab:ask-agent`,
119
- * bound to Accel+. in editors and notebooks) opens a popup where the user
120
- * types an instruction, picks one of the launcher's agents and picks where
121
- * the prompt goes; submitting either starts that agent in a fresh terminal
122
- * via `xtralab:start-agent:<id>`, or pastes the prompt into an agent already
123
- * running in one of the open terminal sessions (via `IAgentTerminals`; the
124
- * agent CLIs queue prompts that arrive while they are busy). Sends to a
125
- * running agent stay in the background — focus never leaves the editor, and
126
- * a success toast offers to open the terminal. Either way the prompt embeds
127
- * the file path, cell index for notebooks, line range and selected snippet.
128
- *
129
- * The popup's Queue button (or Accel+Enter) defers the send instead: the
130
- * comment lands in a persistent queue, stamped with the popup's selected
131
- * destination — every queued prompt is independent and may aim at a
132
- * different agent or session. A right-sidebar panel reviews the queue
133
- * (edit, retarget, remove) and flushes it in one go, combining prompts that
134
- * share a destination into a single numbered message. The queue survives
135
- * page reloads.
136
- *
137
- * The same popup is provided on the `IAskAgent` token so the git diff
138
- * viewers can open it for a selected diff line range.
79
+ * Prompt a coding agent about selected code: a pill/popup over editor,
80
+ * notebook-cell and git-diff selections sends the instruction to a fresh
81
+ * agent terminal or into a running one, or defers it into a persistent
82
+ * queue reviewed in a right-sidebar panel and flushed as one batch.
139
83
  */
140
84
  const plugin: JupyterFrontEndPlugin<IAskAgent> = {
141
85
  id: PLUGIN_ID,
@@ -165,8 +109,7 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
165
109
  const trans = (translator ?? nullTranslator).load('jupyterlab');
166
110
 
167
111
  /**
168
- * Best-effort write to the state database; a failure only loses the
169
- * convenience of restoring this value on the next page load.
112
+ * Best-effort state-database write; a failure only loses a restored default.
170
113
  */
171
114
  const saveState = (key: string, value: ReadonlyPartialJSONValue): void => {
172
115
  state?.save(key, value).catch(error => {
@@ -174,8 +117,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
174
117
  });
175
118
  };
176
119
 
177
- // The last-used picks, restored asynchronously from the state database
178
- // and kept in memory so the popup can read them synchronously.
120
+ // Last-used picks, restored asynchronously but read synchronously by
121
+ // the popup.
179
122
  let lastAgentId: string | null = null;
180
123
  let lastTarget: string | null = null;
181
124
  if (state !== null) {
@@ -206,21 +149,15 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
206
149
  saveState(LAST_TARGET_STATE_KEY, value);
207
150
  };
208
151
 
209
- /**
210
- * Agents that can receive an initial prompt on their command line.
211
- */
212
152
  const promptAgents = (): IAgent[] =>
213
153
  (agentRegistry?.agents ?? []).filter(
214
154
  agent => agent.promptArgs !== undefined
215
155
  );
216
156
 
217
157
  /**
218
- * Running agent terminals offered as prompt targets, each resolved to
219
- * its agent's icon the same way the terminals panel badges its rows
220
- * (matching either the configured command or the canonical id, falling
221
- * back to the plain terminal icon when the agent has since been removed
222
- * from the settings). Unlike {@link promptAgents} this does not require
223
- * `promptArgs`: pasting into a running TUI needs no command-line recipe.
158
+ * Running agent terminals offered as prompt targets, badged with the
159
+ * matching agent's icon like the terminals panel (configured command or
160
+ * canonical id). Pasting into a running TUI needs no `promptArgs`.
224
161
  */
225
162
  const sessionTargets = (): ISessionTarget[] =>
226
163
  (agentTerminals?.sessions() ?? []).map(session => ({
@@ -252,9 +189,6 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
252
189
  }
253
190
  };
254
191
 
255
- /**
256
- * Open the named terminal's tab (the success toast's call to action).
257
- */
258
192
  const openTerminal = (name: string): void => {
259
193
  app.commands.execute('terminal:open', { name }).catch(reason => {
260
194
  console.error('xtralab: failed to open the terminal', reason);
@@ -262,10 +196,9 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
262
196
  };
263
197
 
264
198
  /**
265
- * Paste `prompt` into the running agent in session `name`. Delivery is
266
- * background — focus stays where it is — so `successMessage` is toasted
267
- * with an "Open" action instead. Failures are toasted too; the promise
268
- * still rejects so callers can chain their own cleanup to success only.
199
+ * Paste `prompt` into the running agent in session `name`, in the
200
+ * background. Failures are toasted but still reject, so callers can
201
+ * chain their own cleanup to success only.
269
202
  */
270
203
  const deliverToSession = async (
271
204
  name: string,
@@ -273,8 +206,6 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
273
206
  successMessage: string
274
207
  ): Promise<void> => {
275
208
  if (agentTerminals === null) {
276
- // Session targets are only offered when the terminals plugin is
277
- // present; fail loudly rather than resolving as a delivered send.
278
209
  throw new Error('xtralab: agent terminals are unavailable');
279
210
  }
280
211
  try {
@@ -303,8 +234,7 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
303
234
 
304
235
  /**
305
236
  * Start `agentId` in a fresh terminal with `prompt` on its command
306
- * line. Failures are toasted; the promise still rejects so callers can
307
- * chain their own cleanup to success only.
237
+ * line. Failures are toasted but still reject.
308
238
  */
309
239
  const startAgentTerminal = async (
310
240
  agentId: string,
@@ -327,9 +257,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
327
257
  }
328
258
  };
329
259
 
330
- // The queue feature needs a side area to dock the review panel into, so
331
- // everything below stays off (and the popup hides its Queue button)
332
- // without the full Lab shell.
260
+ // The queue needs a side area for its review panel, so it stays off
261
+ // (and the popup hides its Queue button) without the full Lab shell.
333
262
  let queue: PromptQueue | null = null;
334
263
  let queuePrompt:
335
264
  | ((
@@ -345,8 +274,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
345
274
  let panel: AskAgentQueuePanel | null = null;
346
275
  let flushing = false;
347
276
 
348
- // Mirror every queue change into the state database, debounced so
349
- // typing in the panel's textareas does not write on each keystroke.
277
+ // Debounced so typing in the panel's textareas does not write on
278
+ // each keystroke.
350
279
  const persistQueue = new Debouncer(() => {
351
280
  saveState(QUEUE_STATE_KEY, serializeQueuedPrompts(promptQueue.items));
352
281
  }, QUEUE_PERSIST_MS);
@@ -355,12 +284,9 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
355
284
  });
356
285
 
357
286
  /**
358
- * The prompt (and working directory) for a batch sent to a new
359
- * terminal. When every item agrees on a repository root the terminal
360
- * starts there and the item paths stay relative to it; a mixed batch
361
- * starts at the server root instead, with every locator rewritten to
362
- * a server-relative path so none of them depends on the terminal's
363
- * cwd.
287
+ * The prompt (and cwd) for a batch sent to a new terminal: a shared
288
+ * repository root becomes the cwd; a mixed batch starts at the server
289
+ * root with every locator rewritten server-relative instead.
364
290
  */
365
291
  const newTerminalBatch = (
366
292
  items: readonly IQueuedPrompt[]
@@ -385,13 +311,10 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
385
311
  };
386
312
 
387
313
  /**
388
- * Flush the queue: prompts are grouped by destination — every prompt
389
- * is independent and may target a different agent or session — and
390
- * each group goes out as one numbered message. A group's prompts
391
- * leave the queue only when its delivery succeeds, so one dead
392
- * session never loses the comments aimed elsewhere. One flush runs
393
- * at a time — deliveries span server round trips, and a second click
394
- * meanwhile would re-send every still-queued group.
314
+ * Flush the queue: prompts are grouped by destination and each group
315
+ * goes out as one numbered message, leaving the queue only when its
316
+ * delivery succeeds. One flush at a time — a second click mid-flight
317
+ * would re-send every still-queued group.
395
318
  */
396
319
  const sendQueue = (): void => {
397
320
  if (flushing) {
@@ -403,8 +326,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
403
326
  >();
404
327
  for (const item of promptQueue.items) {
405
328
  if (item.target === null) {
406
- // The panel disables sending while a target is missing; skip
407
- // defensively if a send races a target loss.
329
+ // The panel disables sending without a target; skip if a send
330
+ // races a target loss.
408
331
  continue;
409
332
  }
410
333
  const key =
@@ -419,9 +342,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
419
342
  for (const { target, items } of groups.values()) {
420
343
  /**
421
344
  * Remove the delivered prompts — except any edited or retargeted
422
- * while the send was in flight: an edit makes a new object, so
423
- * identity is the "unchanged" check, and the edited prompt stays
424
- * queued for the next flush instead of being dropped unsent.
345
+ * in flight: an edit makes a new object, so identity is the
346
+ * "unchanged" check and the edited prompt stays queued.
425
347
  */
426
348
  const delivered = (): void => {
427
349
  promptQueue.removeMany(
@@ -466,17 +388,13 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
466
388
  }
467
389
  flushing = true;
468
390
  panel?.update();
469
- // Every delivery caught its own failure above, so this resolves
470
- // once all groups have settled either way.
391
+ // Every delivery caught its own failure, so this settles either way.
471
392
  void Promise.all(deliveries).then(() => {
472
393
  flushing = false;
473
394
  panel?.update();
474
395
  });
475
396
  };
476
397
 
477
- /**
478
- * Empty the queue, with an undo toast (typed comments are work).
479
- */
480
398
  const clearQueue = (): void => {
481
399
  const removed = promptQueue.items;
482
400
  if (removed.length === 0) {
@@ -494,8 +412,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
494
412
  actions: [
495
413
  {
496
414
  label: trans.__('Undo'),
497
- // Prompts queued after the clear are newer; keep them,
498
- // after the restored ones.
415
+ // Prompts queued after the clear are newer; keep them after
416
+ // the restored ones.
499
417
  callback: () =>
500
418
  promptQueue.reset([...removed, ...promptQueue.items])
501
419
  }
@@ -520,12 +438,9 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
520
438
  created.title.icon = askAgentIcon;
521
439
  created.title.label = trans.__('Prompt Queue');
522
440
  created.title.caption = trans.__('Queued ask-agent prompts');
523
- // Closable so it gets a close button if the user drags it out of
524
- // the side area; closing disposes it, and it is recreated on
525
- // demand (same arrangement as the walkthrough panel).
441
+ // Closable so it gets a close button when dragged out of the side
442
+ // area; closing disposes it and it is recreated on demand.
526
443
  created.title.closable = true;
527
- // The panel re-renders on queue changes itself; the chips also
528
- // mirror the live agent list and terminal sessions.
529
444
  agentRegistry?.changed.connect(created.update, created);
530
445
  agentTerminals?.changed.connect(created.update, created);
531
446
  labShell.add(created, 'right', { rank: 900 });
@@ -546,8 +461,6 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
546
461
 
547
462
  queuePrompt = (request, target, instruction) => {
548
463
  const wasEmpty = promptQueue.items.length === 0;
549
- // Queueing is a target choice like sending: remember it for the
550
- // next popup's preselection.
551
464
  if (target.kind === 'session') {
552
465
  rememberTarget(SESSION_TARGET_PREFIX + target.name);
553
466
  } else {
@@ -555,18 +468,14 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
555
468
  rememberTarget(NEW_TARGET_VALUE);
556
469
  }
557
470
  promptQueue.add(request.context, instruction, target);
558
- // Queueing is background like a session send: hand focus straight
559
- // back to the widget the ask came from.
560
471
  app.shell.currentWidget?.activate();
561
472
  const queuePanel = ensurePanel();
562
473
  if (queuePanel.isVisible) {
563
- // The list visibly grows; no extra feedback needed.
564
474
  return;
565
475
  }
566
476
  if (wasEmpty) {
567
- // First prompt of a batch: reveal the panel once, so the user
568
- // sees where queued prompts accumulate. Later additions respect
569
- // a deliberately closed sidebar and toast instead.
477
+ // Reveal the panel once for the first prompt; later additions
478
+ // respect a deliberately closed sidebar and toast instead.
570
479
  labShell.activateById(queuePanel.id);
571
480
  return;
572
481
  }
@@ -592,10 +501,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
592
501
  execute: revealPanel
593
502
  });
594
503
 
595
- // Restore the queue persisted by a previous page load. Anything
596
- // queued before the fetch resolves is newer and stays, after the
597
- // restored prompts. A restored queue should be discoverable without
598
- // queueing anything new, so its sidebar tab is recreated (collapsed).
504
+ // Prompts queued before the fetch resolves are newer and stay, after
505
+ // the restored ones; the tab is recreated so the restore is discoverable.
599
506
  if (state !== null) {
600
507
  void state
601
508
  .fetch(QUEUE_STATE_KEY)
@@ -617,11 +524,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
617
524
  closePopup();
618
525
  const agents = promptAgents();
619
526
  const targets = sessionTargets();
620
- // Preselect where the prompt goes. Queueing into a running agent is
621
- // the common follow-up loop, so a live session wins by default — the
622
- // last-used one when it is still running, otherwise the first — unless
623
- // the user explicitly chose a new terminal last time (and one can
624
- // still be started).
527
+ // Preselect: a live session wins (last-used if still running, else the
528
+ // first) unless a new terminal was last chosen and can still start.
625
529
  const storedTarget = lastTarget;
626
530
  let initialTargetName: string | null =
627
531
  targets.length > 0 ? targets[0].name : null;
@@ -664,9 +568,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
664
568
  const prompt = buildPrompt(request.context, instruction);
665
569
  if (target.kind === 'session') {
666
570
  rememberTarget(SESSION_TARGET_PREFIX + target.name);
667
- // The send is background, so hand focus straight back to the
668
- // widget the ask came from (the editor or diff under the popup);
669
- // disposing the popup alone would drop it on `document.body`.
571
+ // The send is background: hand focus back to the widget the ask
572
+ // came from — disposing the popup alone drops it on `document.body`.
670
573
  app.shell.currentWidget?.activate();
671
574
  const label =
672
575
  targets.find(entry => entry.name === target.name)?.label ??
@@ -738,9 +641,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
738
641
  const button = ensurePill();
739
642
  pillRequest = request;
740
643
  button.style.display = 'flex';
741
- // Measure after display so the rect is real, then sit the pill just
742
- // above the selection head (below it when that would leave the
743
- // viewport).
644
+ // Measure after display so the rect is real; the pill sits above the
645
+ // selection head, below it when that would leave the viewport.
744
646
  const rect = button.getBoundingClientRect();
745
647
  let left = anchor.left;
746
648
  let top = anchor.top - rect.height - PILL_GAP;
@@ -773,9 +675,6 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
773
675
  hidePill();
774
676
  return;
775
677
  }
776
- // The popup can send somewhere as long as an agent takes a prompt on
777
- // its command line (new terminal) or one is already running (existing
778
- // session) — only with neither is there nothing to offer.
779
678
  if (
780
679
  promptAgents().length === 0 &&
781
680
  (agentTerminals?.sessions() ?? []).length === 0
@@ -791,9 +690,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
791
690
  showPill(request);
792
691
  };
793
692
 
794
- // Each invocation resets the timer, so evaluation runs once the
795
- // selection has settled. Mid-drag the selection is still moving; the
796
- // pointerup listener invokes it again once the drag ends.
693
+ // Mid-drag the selection is still moving; the pointerup listener
694
+ // re-invokes once the drag ends.
797
695
  const settled = new Debouncer(() => {
798
696
  if (!pointerIsDown) {
799
697
  evaluateSelection();
@@ -802,8 +700,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
802
700
 
803
701
  document.addEventListener('selectionchange', () => {
804
702
  if (popup !== null) {
805
- // Typing in the popup's textarea moves the document selection; the
806
- // pill must not resurface underneath the open popup.
703
+ // Typing in the popup moves the document selection; the pill must
704
+ // not resurface underneath it.
807
705
  return;
808
706
  }
809
707
  hidePill();
@@ -845,9 +743,8 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
845
743
  return;
846
744
  }
847
745
  if (popup !== null) {
848
- // Focus is outside the popup — its own key handler consumes Escape
849
- // when focus is inside — so close it here and hand focus back to
850
- // the widget the ask came from.
746
+ // The popup's own handler consumes Escape while focus is inside it;
747
+ // here focus is elsewhere, so close and restore focus.
851
748
  closePopup();
852
749
  app.shell.currentWidget?.activate();
853
750
  return;
@@ -875,7 +772,6 @@ const plugin: JupyterFrontEndPlugin<IAskAgent> = {
875
772
  }
876
773
  });
877
774
 
878
- // One binding per editing surface: file editors and notebooks.
879
775
  for (const selector of ['.jp-FileEditor', '.jp-Notebook']) {
880
776
  app.commands.addKeyBinding({
881
777
  command: ASK_AGENT_COMMAND,
@@ -9,21 +9,13 @@ import type { IAgent } from '../launcher/agents';
9
9
  import { AgentChoices, ISessionTarget, TargetChips } from './targetPicker';
10
10
  import type { AskAgentTarget, IAskAgentContext } from './tokens';
11
11
 
12
- /**
13
- * Gap between the popup and its anchor rectangle, in pixels.
14
- */
15
12
  const ANCHOR_GAP = 6;
16
13
 
17
- /**
18
- * Minimum distance kept between the popup and the viewport edges.
19
- */
20
14
  const VIEWPORT_MARGIN = 8;
21
15
 
22
16
  /**
23
- * Short human-readable descriptor of the prompted range, shown in the popup
24
- * header and the queue panel: file name, notebook cell (when applicable),
25
- * line range and, for ranges that are not current file content (such as the
26
- * old side of a diff), a clarifying tag.
17
+ * Short descriptor of the prompted range for the popup header and the queue
18
+ * panel: file name, notebook cell, line range and any clarifying tag.
27
19
  */
28
20
  export function contextSummary(
29
21
  context: IAskAgentContext,
@@ -66,7 +58,6 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
66
58
  const known = agents.find(agent => agent.id === initialAgentId);
67
59
  return (known ?? agents[0])?.id ?? '';
68
60
  });
69
- // The running session the prompt goes to, or `null` for a new terminal.
70
61
  const [targetName, setTargetName] = React.useState<string | null>(
71
62
  initialTargetName
72
63
  );
@@ -77,9 +68,8 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
77
68
  const popupRef = React.useRef<HTMLDivElement>(null);
78
69
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
79
70
 
80
- // Place the popup once its size is known: below the anchor, flipped above
81
- // when there is no room, clamped into the viewport. Hidden until placed so
82
- // the measurement never flashes at the wrong position.
71
+ // Place the popup once its size is known; hidden until placed so the
72
+ // measurement never flashes at the wrong position.
83
73
  React.useLayoutEffect(() => {
84
74
  const node = popupRef.current;
85
75
  if (node === null) {
@@ -111,10 +101,8 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
111
101
  setPosition({ left, top });
112
102
  }, [anchor]);
113
103
 
114
- // Focus the input only once the popup is placed: while it is still
115
- // unpositioned it sits `visibility: hidden`, and hidden elements silently
116
- // refuse focus. The empty state renders no textarea; focus the dialog
117
- // root itself so it is announced and Escape works without a pointer.
104
+ // Unpositioned means `visibility: hidden`, and hidden elements refuse focus.
105
+ // With no textarea (empty state) the dialog root takes focus for Escape.
118
106
  React.useEffect(() => {
119
107
  if (position !== null) {
120
108
  (textareaRef.current ?? popupRef.current)?.focus();
@@ -123,17 +111,15 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
123
111
 
124
112
  const dismiss = React.useCallback(
125
113
  (restoreFocus: boolean) => {
126
- // Defer so unmounting this React root never happens synchronously
127
- // inside the event handler that triggered it (same pattern as the
128
- // omnibox).
114
+ // Defer so this React root never unmounts synchronously inside its
115
+ // own event handler (same pattern as the omnibox).
129
116
  window.setTimeout(() => onCancel(restoreFocus), 0);
130
117
  },
131
118
  [onCancel]
132
119
  );
133
120
 
134
- // Close when the user clicks anywhere outside the popup. Capture phase so
135
- // a click into widgets that swallow events still dismisses it. The click
136
- // places focus itself, so none is restored.
121
+ // Capture phase so a click into event-swallowing widgets still dismisses;
122
+ // the click places focus itself, so none is restored.
137
123
  React.useEffect(() => {
138
124
  const onPointerDown = (event: PointerEvent): void => {
139
125
  const node = popupRef.current;
@@ -157,8 +143,6 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
157
143
  instruction.trim().length > 0 &&
158
144
  (selectedSession !== undefined || selectedAgent !== undefined);
159
145
 
160
- // The destination both actions use: send delivers to it now, queue stamps
161
- // it on the queued prompt (still editable in the panel later).
162
146
  const resolveTarget = React.useCallback((): AskAgentTarget | null => {
163
147
  const session = targets.find(entry => entry.name === targetName);
164
148
  if (session !== undefined) {
@@ -177,8 +161,7 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
177
161
  if (trimmed.length === 0 || target === null) {
178
162
  return;
179
163
  }
180
- // Defer so unmounting this React root never happens synchronously
181
- // inside the event handler that triggered it.
164
+ // Same deferred pattern as dismiss.
182
165
  window.setTimeout(() => {
183
166
  onSubmit(target, trimmed);
184
167
  }, 0);
@@ -200,8 +183,6 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
200
183
  if (event.key === 'Escape') {
201
184
  event.preventDefault();
202
185
  event.stopPropagation();
203
- // A keyboard dismissal places no focus of its own; ask the plugin to
204
- // hand it back to the widget the ask came from.
205
186
  dismiss(true);
206
187
  }
207
188
  };
@@ -234,9 +215,8 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
234
215
  className="jp-xtralab-AskAgent-popup"
235
216
  role="dialog"
236
217
  aria-label={trans.__('Ask an agent about %1', summary)}
237
- // Focusable so clicks on non-interactive popup content keep focus
238
- // inside the dialog (Escape keeps working) and so the empty state
239
- // can be focused at all.
218
+ // Focusable so clicks on non-interactive content keep Escape working
219
+ // and the empty state can take focus at all.
240
220
  tabIndex={-1}
241
221
  style={{
242
222
  left: position?.left ?? 0,
@@ -324,10 +304,8 @@ function AskAgentPopupComponent(props: AskAgentPopup.IOptions): JSX.Element {
324
304
  }
325
305
 
326
306
  /**
327
- * The floating ask-agent prompt box. Hosts {@link AskAgentPopupComponent} in
328
- * a React root attached to `document.body`; `jp-ThemedContainer` makes
329
- * JupyterLab's theme variables resolve outside the shell (the omnibox uses
330
- * the same arrangement).
307
+ * The floating ask-agent prompt box, attached to `document.body`;
308
+ * `jp-ThemedContainer` makes theme variables resolve outside the shell.
331
309
  */
332
310
  export class AskAgentPopup extends ReactWidget {
333
311
  constructor(options: AskAgentPopup.IOptions) {
@@ -338,6 +316,9 @@ export class AskAgentPopup extends ReactWidget {
338
316
  this.addClass('jp-ThemedContainer');
339
317
  }
340
318
 
319
+ /**
320
+ * Render the popup content.
321
+ */
341
322
  render(): JSX.Element {
342
323
  return <AskAgentPopupComponent {...this._options} />;
343
324
  }
@@ -379,8 +360,7 @@ export namespace AskAgentPopup {
379
360
  */
380
361
  initialTargetName: string | null;
381
362
  /**
382
- * Number of prompts already queued, shown on the queue button. Only
383
- * meaningful together with {@link onQueue}.
363
+ * Number of prompts already queued, shown on the queue button.
384
364
  */
385
365
  queueCount?: number;
386
366
  /**
@@ -392,15 +372,13 @@ export namespace AskAgentPopup {
392
372
  */
393
373
  onSubmit: (target: AskAgentTarget, instruction: string) => void;
394
374
  /**
395
- * Add the instruction to the prompt queue instead of sending it, keyed
396
- * to the same chosen target (the plugin closes the popup). The queue
397
- * button is hidden when omitted.
375
+ * Queue the instruction for the chosen target instead of sending it.
376
+ * The queue button is hidden when omitted.
398
377
  */
399
378
  onQueue?: (target: AskAgentTarget, instruction: string) => void;
400
379
  /**
401
- * Dismiss the popup (the plugin disposes the widget). `restoreFocus` is
402
- * true when the dismissal placed no focus of its own (Escape), asking
403
- * the plugin to hand focus back to the widget the ask came from.
380
+ * Dismiss the popup. `restoreFocus` is true when the dismissal placed
381
+ * no focus of its own (Escape).
404
382
  */
405
383
  onCancel: (restoreFocus: boolean) => void;
406
384
  }