xtralab 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +10 -0
  2. package/lib/about/icons.d.ts +0 -5
  3. package/lib/about/icons.js +0 -5
  4. package/lib/about/index.d.ts +4 -8
  5. package/lib/about/index.js +6 -17
  6. package/lib/agentSessions.d.ts +9 -29
  7. package/lib/agentSessions.js +2 -7
  8. package/lib/askAgent/editorSelection.d.ts +20 -26
  9. package/lib/askAgent/editorSelection.js +11 -26
  10. package/lib/askAgent/icons.d.ts +2 -3
  11. package/lib/askAgent/icons.js +2 -3
  12. package/lib/askAgent/index.d.ts +4 -26
  13. package/lib/askAgent/index.js +54 -158
  14. package/lib/askAgent/popup.d.ts +12 -16
  15. package/lib/askAgent/popup.js +18 -37
  16. package/lib/askAgent/prompt.d.ts +3 -5
  17. package/lib/askAgent/prompt.js +6 -19
  18. package/lib/askAgent/queue.d.ts +11 -21
  19. package/lib/askAgent/queue.js +9 -19
  20. package/lib/askAgent/queuePanel.d.ts +13 -18
  21. package/lib/askAgent/queuePanel.js +15 -37
  22. package/lib/askAgent/targetPicker.d.ts +2 -9
  23. package/lib/askAgent/targetPicker.js +6 -11
  24. package/lib/askAgent/tokens.d.ts +32 -46
  25. package/lib/askAgent/tokens.js +4 -6
  26. package/lib/commandBar/index.d.ts +4 -13
  27. package/lib/commandBar/index.js +21 -39
  28. package/lib/customPanel/index.js +15 -0
  29. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  30. package/lib/editorBreadcrumbs/index.js +10 -6
  31. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  32. package/lib/editorBreadcrumbs/widget.js +11 -17
  33. package/lib/editorIndent/index.d.ts +4 -12
  34. package/lib/editorIndent/index.js +8 -23
  35. package/lib/fileBrowser/commands.d.ts +28 -19
  36. package/lib/fileBrowser/commands.js +39 -98
  37. package/lib/fileBrowser/contents.d.ts +3 -0
  38. package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
  39. package/lib/fileBrowser/dragAndDrop.js +2 -7
  40. package/lib/fileBrowser/fileBrowser.d.ts +19 -9
  41. package/lib/fileBrowser/fileBrowser.js +68 -216
  42. package/lib/fileBrowser/gitStatus.d.ts +4 -9
  43. package/lib/fileBrowser/gitStatus.js +7 -17
  44. package/lib/fileBrowser/gitignore.d.ts +4 -17
  45. package/lib/fileBrowser/gitignore.js +4 -25
  46. package/lib/fileBrowser/icons.d.ts +7 -17
  47. package/lib/fileBrowser/icons.js +25 -80
  48. package/lib/fileBrowser/index.js +2 -4
  49. package/lib/fileBrowser/widget.d.ts +126 -48
  50. package/lib/fileBrowser/widget.js +89 -4
  51. package/lib/fileTypeIcons/index.js +0 -10
  52. package/lib/git/api.d.ts +5 -13
  53. package/lib/git/api.js +10 -36
  54. package/lib/git/askRequest.d.ts +4 -8
  55. package/lib/git/askRequest.js +6 -20
  56. package/lib/git/commands.d.ts +40 -5
  57. package/lib/git/commands.js +16 -10
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffSurface.d.ts +41 -55
  60. package/lib/git/diffSurface.js +42 -117
  61. package/lib/git/diffWidget.d.ts +81 -2
  62. package/lib/git/diffWidget.js +62 -12
  63. package/lib/git/imageDiff.d.ts +5 -6
  64. package/lib/git/imageDiff.js +18 -48
  65. package/lib/git/index.d.ts +0 -3
  66. package/lib/git/index.js +3 -8
  67. package/lib/git/notebookDiff.d.ts +110 -33
  68. package/lib/git/notebookDiff.js +52 -195
  69. package/lib/git/tokens.d.ts +73 -31
  70. package/lib/git/tokens.js +2 -3
  71. package/lib/highlight/index.d.ts +4 -10
  72. package/lib/highlight/index.js +10 -41
  73. package/lib/index.d.ts +0 -9
  74. package/lib/index.js +0 -9
  75. package/lib/launcher/agents.d.ts +55 -51
  76. package/lib/launcher/agents.js +15 -57
  77. package/lib/launcher/availability.d.ts +3 -9
  78. package/lib/launcher/availability.js +3 -9
  79. package/lib/launcher/commands.d.ts +7 -32
  80. package/lib/launcher/commands.js +14 -45
  81. package/lib/launcher/dashboard.d.ts +19 -24
  82. package/lib/launcher/dashboard.js +21 -67
  83. package/lib/launcher/editorRegistry.d.ts +10 -22
  84. package/lib/launcher/editorRegistry.js +14 -18
  85. package/lib/launcher/editors.d.ts +39 -45
  86. package/lib/launcher/editors.js +8 -34
  87. package/lib/launcher/icons.d.ts +0 -12
  88. package/lib/launcher/icons.js +5 -39
  89. package/lib/launcher/index.js +12 -55
  90. package/lib/launcher/invocation.d.ts +4 -10
  91. package/lib/launcher/invocation.js +7 -22
  92. package/lib/launcher/registry.d.ts +9 -7
  93. package/lib/launcher/registry.js +9 -7
  94. package/lib/launcher/tokens.d.ts +10 -22
  95. package/lib/launcher/tokens.js +5 -8
  96. package/lib/menuBar/index.d.ts +3 -10
  97. package/lib/menuBar/index.js +11 -28
  98. package/lib/menus/index.d.ts +3 -9
  99. package/lib/menus/index.js +14 -30
  100. package/lib/omnibox/files.d.ts +2 -4
  101. package/lib/omnibox/files.js +5 -11
  102. package/lib/omnibox/index.d.ts +4 -14
  103. package/lib/omnibox/index.js +9 -27
  104. package/lib/omnibox/model.d.ts +38 -10
  105. package/lib/omnibox/model.js +15 -33
  106. package/lib/omnibox/recents.d.ts +15 -17
  107. package/lib/omnibox/recents.js +12 -17
  108. package/lib/omnibox/tokens.d.ts +5 -10
  109. package/lib/omnibox/tokens.js +2 -6
  110. package/lib/omnibox/widget.d.ts +11 -7
  111. package/lib/omnibox/widget.js +8 -16
  112. package/lib/searchReplace/index.d.ts +4 -20
  113. package/lib/searchReplace/index.js +7 -27
  114. package/lib/showOutput/index.d.ts +3 -12
  115. package/lib/showOutput/index.js +6 -22
  116. package/lib/sidebar/index.d.ts +4 -7
  117. package/lib/sidebar/index.js +5 -12
  118. package/lib/terminalNotifications/index.d.ts +4 -10
  119. package/lib/terminalNotifications/index.js +6 -20
  120. package/lib/terminals/agentTerminals.d.ts +12 -5
  121. package/lib/terminals/agentTerminals.js +25 -44
  122. package/lib/terminals/detection.d.ts +4 -8
  123. package/lib/terminals/detection.js +4 -8
  124. package/lib/terminals/index.d.ts +0 -35
  125. package/lib/terminals/index.js +12 -101
  126. package/lib/terminals/model.d.ts +64 -139
  127. package/lib/terminals/model.js +80 -220
  128. package/lib/terminals/tokens.d.ts +11 -22
  129. package/lib/terminals/tokens.js +2 -2
  130. package/lib/terminals/widget.d.ts +43 -13
  131. package/lib/terminals/widget.js +33 -18
  132. package/lib/topBar/icons.d.ts +3 -8
  133. package/lib/topBar/icons.js +3 -8
  134. package/lib/topBar/index.d.ts +4 -11
  135. package/lib/topBar/index.js +10 -38
  136. package/lib/walkthrough/index.d.ts +4 -9
  137. package/lib/walkthrough/index.js +6 -20
  138. package/lib/walkthrough/panel.d.ts +40 -9
  139. package/lib/walkthrough/panel.js +4 -10
  140. package/package.json +2 -2
  141. package/src/about/icons.ts +0 -5
  142. package/src/about/index.tsx +6 -17
  143. package/src/agentSessions.ts +9 -29
  144. package/src/askAgent/editorSelection.ts +22 -29
  145. package/src/askAgent/icons.ts +2 -3
  146. package/src/askAgent/index.ts +54 -158
  147. package/src/askAgent/popup.tsx +23 -45
  148. package/src/askAgent/prompt.ts +6 -19
  149. package/src/askAgent/queue.ts +13 -24
  150. package/src/askAgent/queuePanel.tsx +21 -46
  151. package/src/askAgent/targetPicker.tsx +5 -15
  152. package/src/askAgent/tokens.ts +32 -46
  153. package/src/commandBar/index.ts +24 -39
  154. package/src/customPanel/index.ts +15 -0
  155. package/src/editorBreadcrumbs/index.ts +10 -6
  156. package/src/editorBreadcrumbs/widget.ts +23 -17
  157. package/src/editorIndent/index.ts +17 -23
  158. package/src/fileBrowser/commands.ts +57 -98
  159. package/src/fileBrowser/contents.ts +3 -0
  160. package/src/fileBrowser/dragAndDrop.ts +17 -7
  161. package/src/fileBrowser/fileBrowser.tsx +84 -223
  162. package/src/fileBrowser/gitStatus.ts +7 -17
  163. package/src/fileBrowser/gitignore.ts +4 -25
  164. package/src/fileBrowser/icons.ts +25 -80
  165. package/src/fileBrowser/index.ts +2 -4
  166. package/src/fileBrowser/widget.tsx +149 -50
  167. package/src/fileTypeIcons/index.ts +0 -10
  168. package/src/git/api.ts +10 -36
  169. package/src/git/askRequest.ts +6 -20
  170. package/src/git/commands.ts +70 -10
  171. package/src/git/diffModel.ts +25 -0
  172. package/src/git/diffSurface.tsx +71 -159
  173. package/src/git/diffWidget.tsx +104 -14
  174. package/src/git/imageDiff.tsx +45 -51
  175. package/src/git/index.ts +3 -8
  176. package/src/git/notebookDiff.tsx +211 -238
  177. package/src/git/tokens.ts +73 -31
  178. package/src/highlight/index.ts +10 -41
  179. package/src/index.ts +0 -9
  180. package/src/launcher/agents.ts +64 -95
  181. package/src/launcher/availability.ts +3 -9
  182. package/src/launcher/commands.ts +14 -51
  183. package/src/launcher/dashboard.tsx +49 -88
  184. package/src/launcher/editorRegistry.ts +21 -29
  185. package/src/launcher/editors.ts +39 -58
  186. package/src/launcher/icons.ts +5 -39
  187. package/src/launcher/index.ts +12 -55
  188. package/src/launcher/invocation.ts +7 -22
  189. package/src/launcher/registry.ts +9 -7
  190. package/src/launcher/tokens.ts +10 -22
  191. package/src/menuBar/index.ts +11 -28
  192. package/src/menus/index.ts +14 -30
  193. package/src/omnibox/files.ts +10 -14
  194. package/src/omnibox/index.ts +9 -27
  195. package/src/omnibox/model.ts +50 -38
  196. package/src/omnibox/recents.ts +17 -22
  197. package/src/omnibox/tokens.ts +5 -10
  198. package/src/omnibox/widget.tsx +14 -19
  199. package/src/searchReplace/index.ts +7 -27
  200. package/src/showOutput/index.ts +6 -22
  201. package/src/sidebar/index.ts +20 -12
  202. package/src/terminalNotifications/index.ts +42 -25
  203. package/src/terminals/agentTerminals.ts +25 -49
  204. package/src/terminals/detection.ts +4 -8
  205. package/src/terminals/index.ts +12 -101
  206. package/src/terminals/model.ts +130 -246
  207. package/src/terminals/tokens.ts +11 -22
  208. package/src/terminals/widget.tsx +48 -31
  209. package/src/topBar/icons.ts +3 -8
  210. package/src/topBar/index.ts +21 -44
  211. package/src/walkthrough/index.ts +6 -20
  212. package/src/walkthrough/panel.ts +40 -10
  213. package/style/about.css +2 -7
  214. package/style/askAgent.css +8 -40
  215. package/style/base.css +9 -45
  216. package/style/chrome.css +108 -308
  217. package/style/commandBar.css +4 -20
  218. package/style/customPanel.css +1 -4
  219. package/style/git.css +22 -184
  220. package/style/highlight.css +1 -3
  221. package/style/launcher.css +6 -70
  222. package/style/omnibox.css +2 -14
  223. package/style/showOutput.css +0 -6
  224. package/style/sidebar.css +29 -9
  225. package/style/tabs.css +5 -13
  226. package/style/terminals.css +6 -53
  227. package/style/topBar.css +2 -22
  228. package/style/walkthrough.css +0 -8
@@ -3,28 +3,14 @@ import { Token } from '@lumino/coreutils';
3
3
  import { ISignal, Signal } from '@lumino/signaling';
4
4
 
5
5
  /**
6
- * A shared map from a terminal session name to the agent command it was
7
- * *launched* with (e.g. `claude`).
8
- *
9
- * This is the optimistic half of the running-agent badge: when the launcher
10
- * (or the terminals panel's `+` menu) starts an agent via a
11
- * `xtralab:start-agent:<id>` command, it records the session here so the
12
- * panel can show the agent's logo immediately, before the server-side
13
- * process detection has had a chance to confirm it. Server detection is the
14
- * authoritative source once it reports on a session; this tag only fills the
15
- * gap right after launch (and serves as a fallback if detection is
16
- * unavailable).
17
- *
18
- * It exists as its own token, provided by a tiny dependency-free plugin,
19
- * specifically so the launcher (the writer) and the terminals panel (the
20
- * reader) can share it without depending on each other — a direct
21
- * launcher↔terminals token pair would form an activation cycle.
6
+ * Shared map from terminal session name to the agent command it was launched
7
+ * with — the optimistic half of the running-agent badge, shown until server
8
+ * detection confirms. Its own token so the launcher (writer) and terminals
9
+ * panel (reader) can share it without an activation cycle.
22
10
  */
23
11
  export interface IAgentSessions {
24
12
  /**
25
- * The agent command a session was launched with, or `null` if we never
26
- * launched an agent into it (e.g. a plain terminal, or one started before
27
- * this ran).
13
+ * The agent command a session was launched with, or `null` if unknown.
28
14
  */
29
15
  get(sessionName: string): string | null;
30
16
 
@@ -34,21 +20,16 @@ export interface IAgentSessions {
34
20
  set(sessionName: string, command: string): void;
35
21
 
36
22
  /**
37
- * Forget a session — called when it shuts down so the map stays bounded.
23
+ * Forget a session so the map stays bounded.
38
24
  */
39
25
  delete(sessionName: string): void;
40
26
 
41
27
  /**
42
- * Emitted with the affected session name whenever a record is added or
43
- * removed, so readers can re-render.
28
+ * Emitted with the session name whenever a record is added or removed.
44
29
  */
45
30
  readonly changed: ISignal<IAgentSessions, string>;
46
31
  }
47
32
 
48
- /**
49
- * DI token for {@link IAgentSessions}. Provided by `xtralab:agent-sessions`;
50
- * consumed (optionally) by both `xtralab:launcher` and `xtralab:terminals`.
51
- */
52
33
  export const IAgentSessions = new Token<IAgentSessions>(
53
34
  'xtralab:IAgentSessions',
54
35
  'A shared map from terminal session name to the agent command it was launched with.'
@@ -82,9 +63,8 @@ class AgentSessions implements IAgentSessions {
82
63
  }
83
64
 
84
65
  /**
85
- * Provides {@link IAgentSessions}. Deliberately has no dependencies so it can
86
- * sit underneath both the launcher and the terminals panel without creating
87
- * a dependency cycle between them.
66
+ * Provides {@link IAgentSessions}; dependency-free so it can sit underneath
67
+ * both the launcher and the terminals panel.
88
68
  */
89
69
  const plugin: JupyterFrontEndPlugin<IAgentSessions> = {
90
70
  id: 'xtralab:agent-sessions',
@@ -8,13 +8,21 @@ import type { Widget } from '@lumino/widgets';
8
8
  import type { IAskAgentContext, IAskAgentRequest } from './tokens';
9
9
 
10
10
  /**
11
- * A CodeMirror view a selection can be asked about, plus the context needed
12
- * to describe it to an agent: the document path and, for notebooks, which
13
- * cell the view belongs to.
11
+ * A CodeMirror view a selection can be asked about, plus the document path
12
+ * and, for notebooks, the owning cell.
14
13
  */
15
14
  interface IEditorTarget {
15
+ /**
16
+ * The CodeMirror view holding the selection.
17
+ */
16
18
  view: EditorView;
19
+ /**
20
+ * The path of the document the view belongs to.
21
+ */
17
22
  path: string;
23
+ /**
24
+ * The index and type of the owning notebook cell; absent for file editors.
25
+ */
18
26
  cell?: {
19
27
  index: number;
20
28
  type: string;
@@ -22,19 +30,10 @@ interface IEditorTarget {
22
30
  }
23
31
 
24
32
  /**
25
- * Resolve the CodeMirror view a selection in `widget` would live in, or
26
- * `null` when the widget holds no such editor. Two widget shapes are
27
- * recognized:
28
- *
29
- * - a notebook panel, where the active cell carries the editor and
30
- * `activeCellIndex` names its 0-based position in the nbformat `cells`
31
- * array. A rendered markdown cell keeps its (hidden) editor, but a
32
- * selection in the rendered HTML never passes {@link domSelectionInView},
33
- * so no pill appears there; and
34
- * - a document widget whose content is a `FileEditor`. The wrapper is not
35
- * checked with `instanceof DocumentWidget`: `@jupyterlab/docregistry` is
36
- * not a core singleton, so another copy of the class could be in play.
37
- * `FileEditor` and `NotebookPanel` come from singleton packages.
33
+ * Resolve the CodeMirror view a selection in `widget` would live in — the
34
+ * active notebook cell or a `FileEditor` document — or `null`. The wrapper is
35
+ * duck-typed, not `instanceof DocumentWidget`: `@jupyterlab/docregistry` is
36
+ * not a core singleton, unlike `FileEditor` and `NotebookPanel`.
38
37
  */
39
38
  export function resolveEditorTarget(
40
39
  widget: Widget | null
@@ -63,10 +62,8 @@ export function resolveEditorTarget(
63
62
  }
64
63
 
65
64
  /**
66
- * Whether the current DOM selection lives inside `view`. Guards the
67
- * selection-change listener against selections made elsewhere (a terminal,
68
- * a sidebar, another notebook cell) while the widget owning `view` happens
69
- * to be the shell's current widget.
65
+ * Whether the current DOM selection lives inside `view`; guards against
66
+ * selections made elsewhere while the owning widget is current.
70
67
  */
71
68
  export function domSelectionInView(
72
69
  view: EditorView,
@@ -80,12 +77,9 @@ export function domSelectionInView(
80
77
 
81
78
  /**
82
79
  * Build an ask-agent request from the current selection in the shell's
83
- * current widget — a file editor or the active notebook cell — or return
84
- * `null` when that widget holds no CodeMirror document editor.
85
- *
86
- * With `allowEmpty`, a collapsed selection falls back to the cursor's line
87
- * (used by the command/shortcut path so it works without a mouse
88
- * selection); otherwise an empty selection yields `null`.
80
+ * current widget, or `null` when it holds no CodeMirror document editor.
81
+ * With `allowEmpty`, a collapsed selection falls back to the cursor's line;
82
+ * otherwise it yields `null`.
89
83
  */
90
84
  export function editorAskRequest(
91
85
  app: JupyterFrontEnd,
@@ -110,15 +104,14 @@ export function editorAskRequest(
110
104
  endLine = startLine;
111
105
  text = state.doc.lineAt(main.from).text;
112
106
  } else {
113
- // A selection that ends exactly at the start of a line (the common
114
- // "drag over whole lines" gesture) should not count that line.
107
+ // A selection ending exactly at a line start (the whole-line drag
108
+ // gesture) should not count that line.
115
109
  const endPos =
116
110
  main.to === state.doc.lineAt(main.to).from ? main.to - 1 : main.to;
117
111
  endLine = state.doc.lineAt(endPos).number;
118
112
  text = state.sliceDoc(main.from, main.to);
119
113
  }
120
114
 
121
- // Anchor the popup at the selection head (where the cursor ended up).
122
115
  // `coordsAtPos` returns `null` for positions scrolled out of view.
123
116
  const coords = view.coordsAtPos(main.head);
124
117
  const anchor = coords
@@ -1,9 +1,8 @@
1
1
  import { LabIcon } from '@jupyterlab/ui-components';
2
2
 
3
3
  /**
4
- * Four-point sparkle used by the ask-agent affordances (selection pill,
5
- * command palette entry). Drawn in-repo; the `jp-icon3` class lets the
6
- * active theme recolor the glyph like other JupyterLab UI icons.
4
+ * Four-point sparkle used by the ask-agent affordances; the `jp-icon3`
5
+ * class lets the active theme recolor the glyph.
7
6
  */
8
7
  export const askAgentIcon = new LabIcon({
9
8
  name: 'xtralab:ask-agent',
@@ -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,