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.
- package/README.md +10 -0
- 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 +28 -19
- package/lib/fileBrowser/commands.js +39 -98
- 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 +68 -216
- package/lib/fileBrowser/gitStatus.d.ts +4 -9
- package/lib/fileBrowser/gitStatus.js +7 -17
- 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 +126 -48
- package/lib/fileBrowser/widget.js +89 -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 +40 -5
- package/lib/git/commands.js +16 -10
- package/lib/git/diffModel.js +4 -0
- package/lib/git/diffSurface.d.ts +41 -55
- package/lib/git/diffSurface.js +42 -117
- package/lib/git/diffWidget.d.ts +81 -2
- package/lib/git/diffWidget.js +62 -12
- package/lib/git/imageDiff.d.ts +5 -6
- package/lib/git/imageDiff.js +18 -48
- package/lib/git/index.d.ts +0 -3
- package/lib/git/index.js +3 -8
- package/lib/git/notebookDiff.d.ts +110 -33
- package/lib/git/notebookDiff.js +52 -195
- 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 +2 -2
- 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 +57 -98
- package/src/fileBrowser/contents.ts +3 -0
- package/src/fileBrowser/dragAndDrop.ts +17 -7
- package/src/fileBrowser/fileBrowser.tsx +84 -223
- package/src/fileBrowser/gitStatus.ts +7 -17
- 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 +149 -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 +70 -10
- package/src/git/diffModel.ts +25 -0
- package/src/git/diffSurface.tsx +71 -159
- package/src/git/diffWidget.tsx +104 -14
- package/src/git/imageDiff.tsx +45 -51
- package/src/git/index.ts +3 -8
- package/src/git/notebookDiff.tsx +211 -238
- 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 +22 -184
- 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/agentSessions.ts
CHANGED
|
@@ -3,28 +3,14 @@ import { Token } from '@lumino/coreutils';
|
|
|
3
3
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
86
|
-
*
|
|
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
|
|
12
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
67
|
-
*
|
|
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
|
|
84
|
-
* `
|
|
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
|
|
114
|
-
//
|
|
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
|
package/src/askAgent/icons.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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',
|
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,
|