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.
- package/README.md +65 -30
- package/lib/about/icons.d.ts +0 -5
- package/lib/about/icons.js +0 -5
- package/lib/about/index.d.ts +4 -8
- package/lib/about/index.js +6 -17
- package/lib/agentSessions.d.ts +9 -29
- package/lib/agentSessions.js +2 -7
- package/lib/askAgent/editorSelection.d.ts +20 -26
- package/lib/askAgent/editorSelection.js +11 -26
- package/lib/askAgent/icons.d.ts +2 -3
- package/lib/askAgent/icons.js +2 -3
- package/lib/askAgent/index.d.ts +4 -26
- package/lib/askAgent/index.js +54 -158
- package/lib/askAgent/popup.d.ts +12 -16
- package/lib/askAgent/popup.js +18 -37
- package/lib/askAgent/prompt.d.ts +3 -5
- package/lib/askAgent/prompt.js +6 -19
- package/lib/askAgent/queue.d.ts +11 -21
- package/lib/askAgent/queue.js +9 -19
- package/lib/askAgent/queuePanel.d.ts +13 -18
- package/lib/askAgent/queuePanel.js +15 -37
- package/lib/askAgent/targetPicker.d.ts +2 -9
- package/lib/askAgent/targetPicker.js +6 -11
- package/lib/askAgent/tokens.d.ts +32 -46
- package/lib/askAgent/tokens.js +4 -6
- package/lib/commandBar/index.d.ts +4 -13
- package/lib/commandBar/index.js +21 -39
- package/lib/customPanel/index.js +15 -0
- package/lib/editorBreadcrumbs/index.d.ts +3 -6
- package/lib/editorBreadcrumbs/index.js +10 -6
- package/lib/editorBreadcrumbs/widget.d.ts +17 -4
- package/lib/editorBreadcrumbs/widget.js +11 -17
- package/lib/editorIndent/index.d.ts +4 -12
- package/lib/editorIndent/index.js +8 -23
- package/lib/fileBrowser/commands.d.ts +29 -19
- package/lib/fileBrowser/commands.js +83 -97
- package/lib/fileBrowser/contents.d.ts +3 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
- package/lib/fileBrowser/dragAndDrop.js +2 -7
- package/lib/fileBrowser/fileBrowser.d.ts +19 -9
- package/lib/fileBrowser/fileBrowser.js +70 -222
- package/lib/fileBrowser/gitStatus.d.ts +16 -10
- package/lib/fileBrowser/gitStatus.js +31 -30
- package/lib/fileBrowser/gitignore.d.ts +4 -17
- package/lib/fileBrowser/gitignore.js +4 -25
- package/lib/fileBrowser/icons.d.ts +7 -17
- package/lib/fileBrowser/icons.js +25 -80
- package/lib/fileBrowser/index.js +2 -4
- package/lib/fileBrowser/widget.d.ts +141 -48
- package/lib/fileBrowser/widget.js +102 -4
- package/lib/fileTypeIcons/index.js +0 -10
- package/lib/git/api.d.ts +5 -13
- package/lib/git/api.js +10 -36
- package/lib/git/askRequest.d.ts +4 -8
- package/lib/git/askRequest.js +6 -20
- package/lib/git/commands.d.ts +45 -5
- package/lib/git/commands.js +20 -13
- package/lib/git/diffModel.js +4 -0
- package/lib/git/diffPreferences.d.ts +90 -0
- package/lib/git/diffPreferences.js +107 -0
- package/lib/git/diffProvider.js +10 -2
- package/lib/git/diffSurface.d.ts +64 -63
- package/lib/git/diffSurface.js +72 -213
- package/lib/git/diffTheme.d.ts +7 -0
- package/lib/git/diffTheme.js +23 -0
- package/lib/git/diffWidget.d.ts +70 -14
- package/lib/git/diffWidget.js +100 -94
- package/lib/git/imageDiff.d.ts +14 -6
- package/lib/git/imageDiff.js +19 -75
- package/lib/git/index.d.ts +0 -3
- package/lib/git/index.js +44 -10
- package/lib/git/notebookDiff.d.ts +110 -33
- package/lib/git/notebookDiff.js +55 -197
- package/lib/git/tokens.d.ts +73 -31
- package/lib/git/tokens.js +2 -3
- package/lib/highlight/index.d.ts +4 -10
- package/lib/highlight/index.js +10 -41
- package/lib/index.d.ts +0 -9
- package/lib/index.js +0 -9
- package/lib/launcher/agents.d.ts +55 -51
- package/lib/launcher/agents.js +15 -57
- package/lib/launcher/availability.d.ts +3 -9
- package/lib/launcher/availability.js +3 -9
- package/lib/launcher/commands.d.ts +7 -32
- package/lib/launcher/commands.js +14 -45
- package/lib/launcher/dashboard.d.ts +19 -24
- package/lib/launcher/dashboard.js +21 -67
- package/lib/launcher/editorRegistry.d.ts +10 -22
- package/lib/launcher/editorRegistry.js +14 -18
- package/lib/launcher/editors.d.ts +39 -45
- package/lib/launcher/editors.js +8 -34
- package/lib/launcher/icons.d.ts +0 -12
- package/lib/launcher/icons.js +5 -39
- package/lib/launcher/index.js +12 -55
- package/lib/launcher/invocation.d.ts +4 -10
- package/lib/launcher/invocation.js +7 -22
- package/lib/launcher/registry.d.ts +9 -7
- package/lib/launcher/registry.js +9 -7
- package/lib/launcher/tokens.d.ts +10 -22
- package/lib/launcher/tokens.js +5 -8
- package/lib/menuBar/index.d.ts +3 -10
- package/lib/menuBar/index.js +11 -28
- package/lib/menus/index.d.ts +3 -9
- package/lib/menus/index.js +14 -30
- package/lib/omnibox/files.d.ts +2 -4
- package/lib/omnibox/files.js +5 -11
- package/lib/omnibox/index.d.ts +4 -14
- package/lib/omnibox/index.js +9 -27
- package/lib/omnibox/model.d.ts +38 -10
- package/lib/omnibox/model.js +15 -33
- package/lib/omnibox/recents.d.ts +15 -17
- package/lib/omnibox/recents.js +12 -17
- package/lib/omnibox/tokens.d.ts +5 -10
- package/lib/omnibox/tokens.js +2 -6
- package/lib/omnibox/widget.d.ts +11 -7
- package/lib/omnibox/widget.js +8 -16
- package/lib/searchReplace/index.d.ts +4 -20
- package/lib/searchReplace/index.js +7 -27
- package/lib/showOutput/index.d.ts +3 -12
- package/lib/showOutput/index.js +6 -22
- package/lib/sidebar/index.d.ts +4 -7
- package/lib/sidebar/index.js +5 -12
- package/lib/terminalNotifications/index.d.ts +4 -10
- package/lib/terminalNotifications/index.js +6 -20
- package/lib/terminals/agentTerminals.d.ts +12 -5
- package/lib/terminals/agentTerminals.js +25 -44
- package/lib/terminals/detection.d.ts +4 -8
- package/lib/terminals/detection.js +4 -8
- package/lib/terminals/index.d.ts +0 -35
- package/lib/terminals/index.js +12 -101
- package/lib/terminals/model.d.ts +64 -139
- package/lib/terminals/model.js +80 -220
- package/lib/terminals/tokens.d.ts +11 -22
- package/lib/terminals/tokens.js +2 -2
- package/lib/terminals/widget.d.ts +43 -13
- package/lib/terminals/widget.js +33 -18
- package/lib/topBar/icons.d.ts +3 -8
- package/lib/topBar/icons.js +3 -8
- package/lib/topBar/index.d.ts +4 -11
- package/lib/topBar/index.js +10 -38
- package/lib/walkthrough/index.d.ts +4 -9
- package/lib/walkthrough/index.js +6 -20
- package/lib/walkthrough/panel.d.ts +40 -9
- package/lib/walkthrough/panel.js +4 -10
- package/package.json +5 -3
- package/schema/git-diff-preferences.json +42 -0
- package/src/about/icons.ts +0 -5
- package/src/about/index.tsx +6 -17
- package/src/agentSessions.ts +9 -29
- package/src/askAgent/editorSelection.ts +22 -29
- package/src/askAgent/icons.ts +2 -3
- package/src/askAgent/index.ts +54 -158
- package/src/askAgent/popup.tsx +23 -45
- package/src/askAgent/prompt.ts +6 -19
- package/src/askAgent/queue.ts +13 -24
- package/src/askAgent/queuePanel.tsx +21 -46
- package/src/askAgent/targetPicker.tsx +5 -15
- package/src/askAgent/tokens.ts +32 -46
- package/src/commandBar/index.ts +24 -39
- package/src/customPanel/index.ts +15 -0
- package/src/editorBreadcrumbs/index.ts +10 -6
- package/src/editorBreadcrumbs/widget.ts +23 -17
- package/src/editorIndent/index.ts +17 -23
- package/src/fileBrowser/commands.ts +119 -96
- package/src/fileBrowser/contents.ts +3 -0
- package/src/fileBrowser/dragAndDrop.ts +17 -7
- package/src/fileBrowser/fileBrowser.tsx +86 -230
- package/src/fileBrowser/gitStatus.ts +36 -33
- package/src/fileBrowser/gitignore.ts +4 -25
- package/src/fileBrowser/icons.ts +25 -80
- package/src/fileBrowser/index.ts +2 -4
- package/src/fileBrowser/widget.tsx +171 -50
- package/src/fileTypeIcons/index.ts +0 -10
- package/src/git/api.ts +10 -36
- package/src/git/askRequest.ts +6 -20
- package/src/git/commands.ts +83 -11
- package/src/git/diffModel.ts +25 -0
- package/src/git/diffPreferences.ts +192 -0
- package/src/git/diffProvider.tsx +11 -2
- package/src/git/diffSurface.tsx +165 -286
- package/src/git/diffTheme.ts +24 -0
- package/src/git/diffWidget.tsx +194 -134
- package/src/git/imageDiff.tsx +56 -84
- package/src/git/index.ts +54 -10
- package/src/git/notebookDiff.tsx +214 -240
- package/src/git/tokens.ts +73 -31
- package/src/highlight/index.ts +10 -41
- package/src/index.ts +0 -9
- package/src/launcher/agents.ts +64 -95
- package/src/launcher/availability.ts +3 -9
- package/src/launcher/commands.ts +14 -51
- package/src/launcher/dashboard.tsx +49 -88
- package/src/launcher/editorRegistry.ts +21 -29
- package/src/launcher/editors.ts +39 -58
- package/src/launcher/icons.ts +5 -39
- package/src/launcher/index.ts +12 -55
- package/src/launcher/invocation.ts +7 -22
- package/src/launcher/registry.ts +9 -7
- package/src/launcher/tokens.ts +10 -22
- package/src/menuBar/index.ts +11 -28
- package/src/menus/index.ts +14 -30
- package/src/omnibox/files.ts +10 -14
- package/src/omnibox/index.ts +9 -27
- package/src/omnibox/model.ts +50 -38
- package/src/omnibox/recents.ts +17 -22
- package/src/omnibox/tokens.ts +5 -10
- package/src/omnibox/widget.tsx +14 -19
- package/src/searchReplace/index.ts +7 -27
- package/src/showOutput/index.ts +6 -22
- package/src/sidebar/index.ts +20 -12
- package/src/terminalNotifications/index.ts +42 -25
- package/src/terminals/agentTerminals.ts +25 -49
- package/src/terminals/detection.ts +4 -8
- package/src/terminals/index.ts +12 -101
- package/src/terminals/model.ts +130 -246
- package/src/terminals/tokens.ts +11 -22
- package/src/terminals/widget.tsx +48 -31
- package/src/topBar/icons.ts +3 -8
- package/src/topBar/index.ts +21 -44
- package/src/walkthrough/index.ts +6 -20
- package/src/walkthrough/panel.ts +40 -10
- package/style/about.css +2 -7
- package/style/askAgent.css +8 -40
- package/style/base.css +9 -45
- package/style/chrome.css +108 -308
- package/style/commandBar.css +4 -20
- package/style/customPanel.css +1 -4
- package/style/git.css +26 -186
- package/style/highlight.css +1 -3
- package/style/launcher.css +6 -70
- package/style/omnibox.css +2 -14
- package/style/showOutput.css +0 -6
- package/style/sidebar.css +29 -9
- package/style/tabs.css +5 -13
- package/style/terminals.css +6 -53
- package/style/topBar.css +2 -22
- package/style/walkthrough.css +0 -8
package/src/askAgent/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
|
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
|
-
//
|
|
178
|
-
//
|
|
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,
|
|
219
|
-
*
|
|
220
|
-
*
|
|
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
|
|
266
|
-
* background
|
|
267
|
-
*
|
|
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
|
|
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
|
|
331
|
-
//
|
|
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
|
-
//
|
|
349
|
-
//
|
|
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
|
|
359
|
-
*
|
|
360
|
-
*
|
|
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
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
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
|
|
407
|
-
//
|
|
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
|
-
*
|
|
423
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
|
524
|
-
//
|
|
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
|
-
//
|
|
568
|
-
//
|
|
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
|
-
//
|
|
596
|
-
//
|
|
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
|
|
621
|
-
//
|
|
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
|
|
668
|
-
//
|
|
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
|
|
742
|
-
//
|
|
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
|
-
//
|
|
795
|
-
//
|
|
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
|
|
806
|
-
//
|
|
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
|
-
//
|
|
849
|
-
//
|
|
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,
|
package/src/askAgent/popup.tsx
CHANGED
|
@@ -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
|
|
24
|
-
*
|
|
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
|
|
81
|
-
//
|
|
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
|
-
//
|
|
115
|
-
//
|
|
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
|
|
127
|
-
//
|
|
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
|
-
//
|
|
135
|
-
//
|
|
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
|
-
//
|
|
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
|
|
238
|
-
//
|
|
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
|
|
328
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
396
|
-
*
|
|
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
|
|
402
|
-
*
|
|
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
|
}
|