xtralab 0.11.1 → 0.12.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 +38 -231
- package/lib/agentSessions.d.ts +6 -2
- package/lib/askAgent/editorSelection.d.ts +53 -0
- package/lib/askAgent/editorSelection.js +95 -0
- package/lib/askAgent/icons.d.ts +7 -0
- package/lib/askAgent/icons.js +15 -0
- package/lib/askAgent/index.d.ts +32 -0
- package/lib/askAgent/index.js +741 -0
- package/lib/askAgent/popup.d.ts +83 -0
- package/lib/askAgent/popup.js +221 -0
- package/lib/askAgent/prompt.d.ts +24 -0
- package/lib/askAgent/prompt.js +116 -0
- package/lib/askAgent/queue.d.ts +93 -0
- package/lib/askAgent/queue.js +207 -0
- package/lib/askAgent/queuePanel.d.ts +76 -0
- package/lib/askAgent/queuePanel.js +188 -0
- package/lib/askAgent/targetPicker.d.ts +58 -0
- package/lib/askAgent/targetPicker.js +74 -0
- package/lib/askAgent/tokens.d.ts +124 -0
- package/lib/askAgent/tokens.js +13 -0
- package/lib/commandBar/index.d.ts +12 -8
- package/lib/commandBar/index.js +58 -41
- package/lib/editorBreadcrumbs/index.js +9 -4
- package/lib/editorBreadcrumbs/widget.d.ts +7 -7
- package/lib/editorBreadcrumbs/widget.js +6 -8
- package/lib/fileBrowser/commands.d.ts +2 -8
- package/lib/fileBrowser/commands.js +39 -30
- package/lib/fileBrowser/contents.d.ts +8 -3
- package/lib/fileBrowser/fileBrowser.d.ts +2 -1
- package/lib/fileBrowser/widget.d.ts +5 -9
- package/lib/fileBrowser/widget.js +1 -1
- package/lib/fileTypeIcons/index.js +5 -2
- package/lib/git/api.d.ts +0 -26
- package/lib/git/api.js +2 -44
- package/lib/git/askRequest.d.ts +23 -0
- package/lib/git/askRequest.js +90 -0
- package/lib/git/commands.d.ts +14 -2
- package/lib/git/commands.js +16 -13
- package/lib/git/diffModel.d.ts +1 -9
- package/lib/git/diffModel.js +1 -1
- package/lib/git/diffProvider.js +8 -3
- package/lib/git/diffSurface.d.ts +19 -2
- package/lib/git/diffSurface.js +64 -18
- package/lib/git/diffTheme.d.ts +0 -4
- package/lib/git/diffTheme.js +1 -1
- package/lib/git/diffWidget.d.ts +9 -0
- package/lib/git/diffWidget.js +38 -10
- package/lib/git/imageDiff.d.ts +17 -4
- package/lib/git/imageDiff.js +90 -30
- package/lib/git/index.js +7 -2
- package/lib/git/notebookDiff.d.ts +10 -67
- package/lib/git/notebookDiff.js +62 -69
- package/lib/git/tokens.d.ts +5 -2
- package/lib/highlight/index.js +12 -8
- package/lib/index.js +2 -0
- package/lib/launcher/agents.js +3 -4
- package/lib/launcher/commands.d.ts +6 -2
- package/lib/launcher/dashboard.d.ts +8 -1
- package/lib/launcher/dashboard.js +39 -33
- package/lib/launcher/editorRegistry.js +5 -3
- package/lib/launcher/editors.d.ts +15 -5
- package/lib/launcher/icons.d.ts +0 -37
- package/lib/launcher/icons.js +11 -12
- package/lib/launcher/index.js +2 -1
- package/lib/launcher/invocation.d.ts +0 -15
- package/lib/launcher/invocation.js +1 -1
- package/lib/launcher/schemaDefaults.d.ts +3 -1
- package/lib/launcher/schemaDefaults.js +3 -1
- package/lib/mimeData.d.ts +7 -0
- package/lib/mimeData.js +16 -0
- package/lib/omnibox/index.d.ts +4 -0
- package/lib/omnibox/index.js +43 -2
- package/lib/omnibox/model.d.ts +43 -14
- package/lib/omnibox/model.js +117 -14
- package/lib/omnibox/recents.d.ts +65 -0
- package/lib/omnibox/recents.js +109 -0
- package/lib/omnibox/tokens.d.ts +6 -2
- package/lib/omnibox/widget.d.ts +23 -6
- package/lib/omnibox/widget.js +26 -7
- package/lib/showOutput/index.js +15 -18
- package/lib/terminals/agentTerminals.d.ts +63 -0
- package/lib/terminals/agentTerminals.js +157 -0
- package/lib/terminals/index.d.ts +7 -1
- package/lib/terminals/index.js +50 -30
- package/lib/terminals/model.d.ts +11 -2
- package/lib/terminals/model.js +27 -8
- package/lib/terminals/tokens.d.ts +66 -0
- package/lib/terminals/tokens.js +7 -0
- package/lib/terminals/widget.d.ts +3 -6
- package/lib/terminals/widget.js +1 -1
- package/lib/topBar/index.js +3 -3
- package/lib/walkthrough/index.js +14 -3
- package/lib/walkthrough/panel.d.ts +12 -4
- package/lib/walkthrough/panel.js +9 -3
- package/package.json +49 -51
- package/schema/command-bar.json +10 -0
- package/schema/omnibox.json +15 -0
- package/src/agentSessions.ts +6 -2
- package/src/askAgent/editorSelection.ts +138 -0
- package/src/askAgent/icons.ts +16 -0
- package/src/askAgent/index.ts +915 -0
- package/src/askAgent/popup.tsx +407 -0
- package/src/askAgent/prompt.ts +144 -0
- package/src/askAgent/queue.ts +269 -0
- package/src/askAgent/queuePanel.tsx +418 -0
- package/src/askAgent/targetPicker.tsx +209 -0
- package/src/askAgent/tokens.ts +139 -0
- package/src/commandBar/index.ts +75 -49
- package/src/editorBreadcrumbs/index.ts +14 -4
- package/src/editorBreadcrumbs/widget.ts +11 -9
- package/src/fileBrowser/commands.ts +45 -31
- package/src/fileBrowser/contents.ts +7 -3
- package/src/fileBrowser/fileBrowser.tsx +1 -1
- package/src/fileBrowser/widget.tsx +5 -3
- package/src/fileTypeIcons/index.ts +6 -3
- package/src/git/api.ts +2 -61
- package/src/git/askRequest.ts +113 -0
- package/src/git/commands.ts +41 -14
- package/src/git/diffModel.ts +1 -1
- package/src/git/diffProvider.tsx +11 -3
- package/src/git/diffSurface.tsx +114 -22
- package/src/git/diffTheme.ts +1 -1
- package/src/git/diffWidget.tsx +54 -7
- package/src/git/imageDiff.tsx +169 -58
- package/src/git/index.ts +10 -2
- package/src/git/notebookDiff.tsx +91 -57
- package/src/git/tokens.ts +4 -2
- package/src/highlight/index.ts +12 -8
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +3 -4
- package/src/launcher/commands.ts +6 -2
- package/src/launcher/dashboard.tsx +86 -38
- package/src/launcher/editorRegistry.ts +10 -4
- package/src/launcher/editors.ts +15 -5
- package/src/launcher/icons.ts +11 -12
- package/src/launcher/index.ts +2 -1
- package/src/launcher/invocation.ts +1 -1
- package/src/launcher/schemaDefaults.ts +3 -1
- package/src/mimeData.ts +20 -0
- package/src/omnibox/index.ts +48 -1
- package/src/omnibox/model.ts +173 -33
- package/src/omnibox/recents.ts +155 -0
- package/src/omnibox/tokens.ts +6 -2
- package/src/omnibox/widget.tsx +54 -13
- package/src/showOutput/index.ts +20 -23
- package/src/terminals/agentTerminals.ts +238 -0
- package/src/terminals/index.ts +54 -30
- package/src/terminals/model.ts +28 -7
- package/src/terminals/tokens.ts +77 -0
- package/src/terminals/widget.tsx +4 -2
- package/src/topBar/index.ts +12 -6
- package/src/walkthrough/index.ts +19 -4
- package/src/walkthrough/panel.ts +15 -5
- package/style/askAgent.css +544 -0
- package/style/commandBar.css +12 -33
- package/style/git.css +72 -31
- package/style/index.css +1 -0
- package/style/index.js +2 -0
- package/style/topBar.css +4 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import type { IAgent } from '../launcher/agents';
|
|
4
|
+
import { ISessionTarget } from './targetPicker';
|
|
5
|
+
import type { AskAgentTarget, IAskAgentContext } from './tokens';
|
|
6
|
+
/**
|
|
7
|
+
* Short human-readable descriptor of the prompted range, shown in the popup
|
|
8
|
+
* header and the queue panel: file name, notebook cell (when applicable),
|
|
9
|
+
* line range and, for ranges that are not current file content (such as the
|
|
10
|
+
* old side of a diff), a clarifying tag.
|
|
11
|
+
*/
|
|
12
|
+
export declare function contextSummary(context: IAskAgentContext, trans: TranslationBundle): string;
|
|
13
|
+
/**
|
|
14
|
+
* The floating ask-agent prompt box. Hosts {@link AskAgentPopupComponent} in
|
|
15
|
+
* a React root attached to `document.body`; `jp-ThemedContainer` makes
|
|
16
|
+
* JupyterLab's theme variables resolve outside the shell (the omnibox uses
|
|
17
|
+
* the same arrangement).
|
|
18
|
+
*/
|
|
19
|
+
export declare class AskAgentPopup extends ReactWidget {
|
|
20
|
+
constructor(options: AskAgentPopup.IOptions);
|
|
21
|
+
render(): JSX.Element;
|
|
22
|
+
private _options;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A namespace for `AskAgentPopup` statics.
|
|
26
|
+
*/
|
|
27
|
+
export declare namespace AskAgentPopup {
|
|
28
|
+
/**
|
|
29
|
+
* Construction options for {@link AskAgentPopup}.
|
|
30
|
+
*/
|
|
31
|
+
interface IOptions {
|
|
32
|
+
/**
|
|
33
|
+
* The code selection the prompt is about.
|
|
34
|
+
*/
|
|
35
|
+
context: IAskAgentContext;
|
|
36
|
+
/**
|
|
37
|
+
* Viewport rectangle to anchor to; `null` centers near the top.
|
|
38
|
+
*/
|
|
39
|
+
anchor: DOMRect | null;
|
|
40
|
+
/**
|
|
41
|
+
* Snapshot of the prompt-capable agents, taken when the popup opens.
|
|
42
|
+
*/
|
|
43
|
+
agents: IAgent[];
|
|
44
|
+
/**
|
|
45
|
+
* Snapshot of the running agent terminals, taken when the popup opens.
|
|
46
|
+
*/
|
|
47
|
+
targets: ISessionTarget[];
|
|
48
|
+
/**
|
|
49
|
+
* Preferred agent id (the last one used), when still available.
|
|
50
|
+
*/
|
|
51
|
+
initialAgentId: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Session to preselect as the target, or `null` to preselect the new
|
|
54
|
+
* terminal. Must be `null` or the name of an entry in {@link targets}.
|
|
55
|
+
*/
|
|
56
|
+
initialTargetName: string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Number of prompts already queued, shown on the queue button. Only
|
|
59
|
+
* meaningful together with {@link onQueue}.
|
|
60
|
+
*/
|
|
61
|
+
queueCount?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Translation bundle for the popup's own labels.
|
|
64
|
+
*/
|
|
65
|
+
trans: TranslationBundle;
|
|
66
|
+
/**
|
|
67
|
+
* Send the instruction to the chosen target (the plugin closes the popup).
|
|
68
|
+
*/
|
|
69
|
+
onSubmit: (target: AskAgentTarget, instruction: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Add the instruction to the prompt queue instead of sending it, keyed
|
|
72
|
+
* to the same chosen target (the plugin closes the popup). The queue
|
|
73
|
+
* button is hidden when omitted.
|
|
74
|
+
*/
|
|
75
|
+
onQueue?: (target: AskAgentTarget, instruction: string) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Dismiss the popup (the plugin disposes the widget). `restoreFocus` is
|
|
78
|
+
* true when the dismissal placed no focus of its own (Escape), asking
|
|
79
|
+
* the plugin to hand focus back to the widget the ask came from.
|
|
80
|
+
*/
|
|
81
|
+
onCancel: (restoreFocus: boolean) => void;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { AgentChoices, TargetChips } from './targetPicker';
|
|
6
|
+
/**
|
|
7
|
+
* Gap between the popup and its anchor rectangle, in pixels.
|
|
8
|
+
*/
|
|
9
|
+
const ANCHOR_GAP = 6;
|
|
10
|
+
/**
|
|
11
|
+
* Minimum distance kept between the popup and the viewport edges.
|
|
12
|
+
*/
|
|
13
|
+
const VIEWPORT_MARGIN = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Short human-readable descriptor of the prompted range, shown in the popup
|
|
16
|
+
* header and the queue panel: file name, notebook cell (when applicable),
|
|
17
|
+
* line range and, for ranges that are not current file content (such as the
|
|
18
|
+
* old side of a diff), a clarifying tag.
|
|
19
|
+
*/
|
|
20
|
+
export function contextSummary(context, trans) {
|
|
21
|
+
var _a;
|
|
22
|
+
const parts = [PathExt.basename(context.path)];
|
|
23
|
+
if (context.cell !== undefined) {
|
|
24
|
+
parts.push(trans.__('cell %1', context.cell.index + 1));
|
|
25
|
+
}
|
|
26
|
+
if (context.startLine !== undefined) {
|
|
27
|
+
const endLine = (_a = context.endLine) !== null && _a !== void 0 ? _a : context.startLine;
|
|
28
|
+
parts.push(endLine > context.startLine
|
|
29
|
+
? trans.__('L%1–%2', context.startLine, endLine)
|
|
30
|
+
: trans.__('L%1', context.startLine));
|
|
31
|
+
}
|
|
32
|
+
if (context.note !== undefined && context.note.length > 0) {
|
|
33
|
+
parts.push(context.note);
|
|
34
|
+
}
|
|
35
|
+
return parts.join(' · ');
|
|
36
|
+
}
|
|
37
|
+
function AskAgentPopupComponent(props) {
|
|
38
|
+
var _a, _b;
|
|
39
|
+
const { context, anchor, agents, targets, initialAgentId, initialTargetName, queueCount, trans, onSubmit, onQueue, onCancel } = props;
|
|
40
|
+
const [instruction, setInstruction] = React.useState('');
|
|
41
|
+
const [agentId, setAgentId] = React.useState(() => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const known = agents.find(agent => agent.id === initialAgentId);
|
|
44
|
+
return (_b = (_a = (known !== null && known !== void 0 ? known : agents[0])) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '';
|
|
45
|
+
});
|
|
46
|
+
// The running session the prompt goes to, or `null` for a new terminal.
|
|
47
|
+
const [targetName, setTargetName] = React.useState(initialTargetName);
|
|
48
|
+
const [position, setPosition] = React.useState(null);
|
|
49
|
+
const popupRef = React.useRef(null);
|
|
50
|
+
const textareaRef = React.useRef(null);
|
|
51
|
+
// Place the popup once its size is known: below the anchor, flipped above
|
|
52
|
+
// when there is no room, clamped into the viewport. Hidden until placed so
|
|
53
|
+
// the measurement never flashes at the wrong position.
|
|
54
|
+
React.useLayoutEffect(() => {
|
|
55
|
+
const node = popupRef.current;
|
|
56
|
+
if (node === null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const rect = node.getBoundingClientRect();
|
|
60
|
+
const viewportWidth = window.innerWidth;
|
|
61
|
+
const viewportHeight = window.innerHeight;
|
|
62
|
+
let left;
|
|
63
|
+
let top;
|
|
64
|
+
if (anchor) {
|
|
65
|
+
left = anchor.left;
|
|
66
|
+
top = anchor.bottom + ANCHOR_GAP;
|
|
67
|
+
if (top + rect.height > viewportHeight - VIEWPORT_MARGIN) {
|
|
68
|
+
top = anchor.top - rect.height - ANCHOR_GAP;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
left = (viewportWidth - rect.width) / 2;
|
|
73
|
+
top = viewportHeight * 0.2;
|
|
74
|
+
}
|
|
75
|
+
left = Math.max(VIEWPORT_MARGIN, Math.min(left, viewportWidth - rect.width - VIEWPORT_MARGIN));
|
|
76
|
+
top = Math.max(VIEWPORT_MARGIN, Math.min(top, viewportHeight - rect.height - VIEWPORT_MARGIN));
|
|
77
|
+
setPosition({ left, top });
|
|
78
|
+
}, [anchor]);
|
|
79
|
+
// Focus the input only once the popup is placed: while it is still
|
|
80
|
+
// unpositioned it sits `visibility: hidden`, and hidden elements silently
|
|
81
|
+
// refuse focus. The empty state renders no textarea; focus the dialog
|
|
82
|
+
// root itself so it is announced and Escape works without a pointer.
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
if (position !== null) {
|
|
86
|
+
(_b = ((_a = textareaRef.current) !== null && _a !== void 0 ? _a : popupRef.current)) === null || _b === void 0 ? void 0 : _b.focus();
|
|
87
|
+
}
|
|
88
|
+
}, [position]);
|
|
89
|
+
const dismiss = React.useCallback((restoreFocus) => {
|
|
90
|
+
// Defer so unmounting this React root never happens synchronously
|
|
91
|
+
// inside the event handler that triggered it (same pattern as the
|
|
92
|
+
// omnibox).
|
|
93
|
+
window.setTimeout(() => onCancel(restoreFocus), 0);
|
|
94
|
+
}, [onCancel]);
|
|
95
|
+
// Close when the user clicks anywhere outside the popup. Capture phase so
|
|
96
|
+
// a click into widgets that swallow events still dismisses it. The click
|
|
97
|
+
// places focus itself, so none is restored.
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
const onPointerDown = (event) => {
|
|
100
|
+
const node = popupRef.current;
|
|
101
|
+
if (node &&
|
|
102
|
+
event.target instanceof Node &&
|
|
103
|
+
!node.contains(event.target)) {
|
|
104
|
+
dismiss(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
110
|
+
};
|
|
111
|
+
}, [dismiss]);
|
|
112
|
+
const selectedAgent = agents.find(agent => agent.id === agentId);
|
|
113
|
+
const selectedSession = targets.find(target => target.name === targetName);
|
|
114
|
+
const canSubmit = instruction.trim().length > 0 &&
|
|
115
|
+
(selectedSession !== undefined || selectedAgent !== undefined);
|
|
116
|
+
// The destination both actions use: send delivers to it now, queue stamps
|
|
117
|
+
// it on the queued prompt (still editable in the panel later).
|
|
118
|
+
const resolveTarget = React.useCallback(() => {
|
|
119
|
+
const session = targets.find(entry => entry.name === targetName);
|
|
120
|
+
if (session !== undefined) {
|
|
121
|
+
return { kind: 'session', name: session.name };
|
|
122
|
+
}
|
|
123
|
+
const agent = agents.find(entry => entry.id === agentId);
|
|
124
|
+
if (agent !== undefined) {
|
|
125
|
+
return { kind: 'new', agentId: agent.id };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}, [agents, agentId, targets, targetName]);
|
|
129
|
+
const submit = React.useCallback(() => {
|
|
130
|
+
const trimmed = instruction.trim();
|
|
131
|
+
const target = resolveTarget();
|
|
132
|
+
if (trimmed.length === 0 || target === null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Defer so unmounting this React root never happens synchronously
|
|
136
|
+
// inside the event handler that triggered it.
|
|
137
|
+
window.setTimeout(() => {
|
|
138
|
+
onSubmit(target, trimmed);
|
|
139
|
+
}, 0);
|
|
140
|
+
}, [instruction, resolveTarget, onSubmit]);
|
|
141
|
+
const queueInstruction = React.useCallback(() => {
|
|
142
|
+
const trimmed = instruction.trim();
|
|
143
|
+
const target = resolveTarget();
|
|
144
|
+
if (trimmed.length === 0 || target === null || onQueue === undefined) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Same deferred pattern as submit.
|
|
148
|
+
window.setTimeout(() => {
|
|
149
|
+
onQueue(target, trimmed);
|
|
150
|
+
}, 0);
|
|
151
|
+
}, [instruction, resolveTarget, onQueue]);
|
|
152
|
+
const onRootKeyDown = (event) => {
|
|
153
|
+
if (event.key === 'Escape') {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
// A keyboard dismissal places no focus of its own; ask the plugin to
|
|
157
|
+
// hand it back to the widget the ask came from.
|
|
158
|
+
dismiss(true);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const onTextareaKeyDown = (event) => {
|
|
162
|
+
if (event.key !== 'Enter' || event.nativeEvent.isComposing) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if ((event.metaKey || event.ctrlKey) && onQueue !== undefined) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
queueInstruction();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!event.shiftKey) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
submit();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const summary = contextSummary(context, trans);
|
|
178
|
+
const placeholder = trans.__('Describe the change…');
|
|
179
|
+
return (React.createElement("div", { ref: popupRef, className: "jp-xtralab-AskAgent-popup", role: "dialog", "aria-label": trans.__('Ask an agent about %1', summary),
|
|
180
|
+
// Focusable so clicks on non-interactive popup content keep focus
|
|
181
|
+
// inside the dialog (Escape keeps working) and so the empty state
|
|
182
|
+
// can be focused at all.
|
|
183
|
+
tabIndex: -1, style: {
|
|
184
|
+
left: (_a = position === null || position === void 0 ? void 0 : position.left) !== null && _a !== void 0 ? _a : 0,
|
|
185
|
+
top: (_b = position === null || position === void 0 ? void 0 : position.top) !== null && _b !== void 0 ? _b : 0,
|
|
186
|
+
visibility: position ? 'visible' : 'hidden'
|
|
187
|
+
}, onKeyDown: onRootKeyDown },
|
|
188
|
+
React.createElement("div", { className: "jp-xtralab-AskAgent-context", title: context.path }, summary),
|
|
189
|
+
agents.length === 0 && targets.length === 0 ? (React.createElement("div", { className: "jp-xtralab-AskAgent-empty" }, trans.__('No agent accepts an initial prompt. Configure agents in the launcher settings.'))) : (React.createElement(React.Fragment, null,
|
|
190
|
+
React.createElement("textarea", { ref: textareaRef, className: "jp-xtralab-AskAgent-input", rows: 3, spellCheck: false, placeholder: placeholder, "aria-label": placeholder, value: instruction, onChange: event => setInstruction(event.target.value), onKeyDown: onTextareaKeyDown }),
|
|
191
|
+
React.createElement(TargetChips, { agents: agents, targets: targets, targetName: targetName, trans: trans, onSelect: setTargetName }),
|
|
192
|
+
React.createElement("div", { className: "jp-xtralab-AskAgent-footer" },
|
|
193
|
+
selectedSession === undefined && (React.createElement(AgentChoices, { agents: agents, agentId: agentId, trans: trans, onSelect: setAgentId })),
|
|
194
|
+
React.createElement("div", { className: "jp-xtralab-AskAgent-actions" },
|
|
195
|
+
onQueue !== undefined && (React.createElement("button", { type: "button", className: "jp-xtralab-AskAgent-queueButton", disabled: !canSubmit, title: trans.__('Queue for this destination instead of sending now — review the queue and send everything later (%1)', CommandRegistry.formatKeystroke('Accel Enter')), onMouseDown: event => event.preventDefault(), onClick: queueInstruction }, queueCount !== undefined && queueCount > 0
|
|
196
|
+
? trans.__('Queue (%1)', queueCount)
|
|
197
|
+
: trans.__('Queue'))),
|
|
198
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-AskAgent-submit", disabled: !canSubmit, title: trans.__('Enter to send, Esc to close'), onMouseDown: event => event.preventDefault(), onClick: submit }, selectedSession
|
|
199
|
+
? trans.__('Send to %1', selectedSession.label)
|
|
200
|
+
: selectedAgent
|
|
201
|
+
? trans.__('Ask %1', selectedAgent.label)
|
|
202
|
+
: trans.__('Ask agent'))))))));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The floating ask-agent prompt box. Hosts {@link AskAgentPopupComponent} in
|
|
206
|
+
* a React root attached to `document.body`; `jp-ThemedContainer` makes
|
|
207
|
+
* JupyterLab's theme variables resolve outside the shell (the omnibox uses
|
|
208
|
+
* the same arrangement).
|
|
209
|
+
*/
|
|
210
|
+
export class AskAgentPopup extends ReactWidget {
|
|
211
|
+
constructor(options) {
|
|
212
|
+
super();
|
|
213
|
+
this._options = options;
|
|
214
|
+
this.id = 'xtralab-ask-agent-popup';
|
|
215
|
+
this.addClass('jp-xtralab-AskAgent');
|
|
216
|
+
this.addClass('jp-ThemedContainer');
|
|
217
|
+
}
|
|
218
|
+
render() {
|
|
219
|
+
return React.createElement(AskAgentPopupComponent, { ...this._options });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IAskAgentContext } from './tokens';
|
|
2
|
+
/**
|
|
3
|
+
* The path a context points at, resolved against the server root. Contexts
|
|
4
|
+
* from git diffs carry repository-relative paths plus the repository root in
|
|
5
|
+
* `cwd`; contexts from plain editors are already server-relative.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serverPath(context: IAskAgentContext): string;
|
|
8
|
+
/**
|
|
9
|
+
* Compose the prompt handed to the agent CLI: a one-line locator, the
|
|
10
|
+
* selected snippet in a code fence, then the user's instruction.
|
|
11
|
+
*
|
|
12
|
+
* The scaffold is written in English on purpose — it is consumed by the
|
|
13
|
+
* agent, not shown in the UI, and the agent CLIs are English-first.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildPrompt(context: IAskAgentContext, instruction: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Compose one prompt out of every queued comment, numbered and in queue
|
|
18
|
+
* order, so a whole review pass reaches the agent as a single task list. A
|
|
19
|
+
* queue of one degrades to the plain {@link buildPrompt} wording.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildBatchPrompt(items: ReadonlyArray<{
|
|
22
|
+
context: IAskAgentContext;
|
|
23
|
+
instruction: string;
|
|
24
|
+
}>): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
2
|
+
/**
|
|
3
|
+
* The path a context points at, resolved against the server root. Contexts
|
|
4
|
+
* from git diffs carry repository-relative paths plus the repository root in
|
|
5
|
+
* `cwd`; contexts from plain editors are already server-relative.
|
|
6
|
+
*/
|
|
7
|
+
export function serverPath(context) {
|
|
8
|
+
return context.cwd !== undefined && context.cwd.length > 0
|
|
9
|
+
? PathExt.join(context.cwd, context.path)
|
|
10
|
+
: context.path;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Cap on the snippet embedded in the prompt, in characters. The path and
|
|
14
|
+
* line range are the authoritative pointers — agents open the file
|
|
15
|
+
* themselves — so a very long selection is trimmed instead of being typed
|
|
16
|
+
* into the terminal wholesale.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_SNIPPET_CHARS = 2000;
|
|
19
|
+
/**
|
|
20
|
+
* A backtick fence strictly longer than any backtick run inside `text`, so
|
|
21
|
+
* the embedded snippet can never terminate the fence early.
|
|
22
|
+
*/
|
|
23
|
+
function fenceFor(text) {
|
|
24
|
+
var _a;
|
|
25
|
+
let longest = 0;
|
|
26
|
+
for (const run of (_a = text.match(/`+/g)) !== null && _a !== void 0 ? _a : []) {
|
|
27
|
+
longest = Math.max(longest, run.length);
|
|
28
|
+
}
|
|
29
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Trim `text` to {@link MAX_SNIPPET_CHARS}, cutting at a line boundary.
|
|
33
|
+
* Returns the (possibly shortened) snippet and whether anything was dropped.
|
|
34
|
+
*/
|
|
35
|
+
function clampSnippet(text) {
|
|
36
|
+
if (text.length <= MAX_SNIPPET_CHARS) {
|
|
37
|
+
return { snippet: text, truncated: false };
|
|
38
|
+
}
|
|
39
|
+
const head = text.slice(0, MAX_SNIPPET_CHARS);
|
|
40
|
+
const lastBreak = head.lastIndexOf('\n');
|
|
41
|
+
return {
|
|
42
|
+
snippet: lastBreak > 0 ? head.slice(0, lastBreak) : head,
|
|
43
|
+
truncated: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Agent-facing description of where `context` points: path, notebook cell,
|
|
48
|
+
* line range and any extra locator, e.g.
|
|
49
|
+
* `` `src/app.ts`, lines 3-9, the old side (HEAD) of the current git diff ``.
|
|
50
|
+
*/
|
|
51
|
+
function locatorFor(context) {
|
|
52
|
+
var _a;
|
|
53
|
+
const where = [`\`${context.path}\``];
|
|
54
|
+
if (context.cell !== undefined) {
|
|
55
|
+
// Both orderings so the agent can count cells or index the nbformat
|
|
56
|
+
// `cells` array, whichever its notebook tooling prefers.
|
|
57
|
+
where.push(`${context.cell.type} cell ${context.cell.index + 1} ` +
|
|
58
|
+
`(0-based index ${context.cell.index})`);
|
|
59
|
+
}
|
|
60
|
+
if (context.startLine !== undefined) {
|
|
61
|
+
const endLine = (_a = context.endLine) !== null && _a !== void 0 ? _a : context.startLine;
|
|
62
|
+
const lines = endLine > context.startLine
|
|
63
|
+
? `lines ${context.startLine}-${endLine}`
|
|
64
|
+
: `line ${context.startLine}`;
|
|
65
|
+
where.push(context.cell !== undefined ? `${lines} of the cell` : lines);
|
|
66
|
+
}
|
|
67
|
+
if (context.location !== undefined && context.location.length > 0) {
|
|
68
|
+
where.push(context.location);
|
|
69
|
+
}
|
|
70
|
+
return where.join(', ');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The blocks describing one comment: `headline`, the selected snippet in a
|
|
74
|
+
* code fence (when there is one), then the user's instruction. Blocks are
|
|
75
|
+
* joined with blank lines by the callers.
|
|
76
|
+
*/
|
|
77
|
+
function commentParts(context, instruction, headline) {
|
|
78
|
+
const parts = [headline];
|
|
79
|
+
if (context.text.length > 0) {
|
|
80
|
+
const { snippet, truncated } = clampSnippet(context.text);
|
|
81
|
+
const fence = fenceFor(snippet);
|
|
82
|
+
parts.push(`${fence}\n${snippet}\n${fence}`);
|
|
83
|
+
if (truncated) {
|
|
84
|
+
parts.push('(snippet truncated — read the file for the full range)');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
parts.push(instruction);
|
|
88
|
+
return parts;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compose the prompt handed to the agent CLI: a one-line locator, the
|
|
92
|
+
* selected snippet in a code fence, then the user's instruction.
|
|
93
|
+
*
|
|
94
|
+
* The scaffold is written in English on purpose — it is consumed by the
|
|
95
|
+
* agent, not shown in the UI, and the agent CLIs are English-first.
|
|
96
|
+
*/
|
|
97
|
+
export function buildPrompt(context, instruction) {
|
|
98
|
+
return commentParts(context, instruction, `In ${locatorFor(context)}:`).join('\n\n');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Compose one prompt out of every queued comment, numbered and in queue
|
|
102
|
+
* order, so a whole review pass reaches the agent as a single task list. A
|
|
103
|
+
* queue of one degrades to the plain {@link buildPrompt} wording.
|
|
104
|
+
*/
|
|
105
|
+
export function buildBatchPrompt(items) {
|
|
106
|
+
if (items.length === 1) {
|
|
107
|
+
return buildPrompt(items[0].context, items[0].instruction);
|
|
108
|
+
}
|
|
109
|
+
const parts = [
|
|
110
|
+
`Please address the following ${items.length} comments.`
|
|
111
|
+
];
|
|
112
|
+
items.forEach((item, index) => {
|
|
113
|
+
parts.push(...commentParts(item.context, item.instruction, `Comment ${index + 1} — in ${locatorFor(item.context)}:`));
|
|
114
|
+
});
|
|
115
|
+
return parts.join('\n\n');
|
|
116
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ReadonlyPartialJSONValue } from '@lumino/coreutils';
|
|
2
|
+
import { ISignal } from '@lumino/signaling';
|
|
3
|
+
import type { AskAgentTarget, IAskAgentContext } from './tokens';
|
|
4
|
+
/**
|
|
5
|
+
* One queued prompt: a code selection, the user's instruction about it, and
|
|
6
|
+
* where it should go. Every prompt is independent — a queue can mix targets
|
|
7
|
+
* (different agents, new terminals, different running sessions) freely.
|
|
8
|
+
*/
|
|
9
|
+
export interface IQueuedPrompt {
|
|
10
|
+
/**
|
|
11
|
+
* Handle for edits and removal, unique within the page session.
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
/**
|
|
15
|
+
* The code selection the instruction is about.
|
|
16
|
+
*/
|
|
17
|
+
context: IAskAgentContext;
|
|
18
|
+
/**
|
|
19
|
+
* The user's typed comment.
|
|
20
|
+
*/
|
|
21
|
+
instruction: string;
|
|
22
|
+
/**
|
|
23
|
+
* The destination picked for this prompt (captured from the popup when it
|
|
24
|
+
* was queued, editable in the panel). `null` when a persisted target no
|
|
25
|
+
* longer parses — the panel then asks for a new pick before sending.
|
|
26
|
+
*/
|
|
27
|
+
target: AskAgentTarget | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The accumulated ask-agent prompts awaiting a batch send.
|
|
31
|
+
*
|
|
32
|
+
* Queueing decouples writing review comments from deciding where they go:
|
|
33
|
+
* the popup appends entries here instead of sending, the side panel lists
|
|
34
|
+
* and edits them, and one batch send flushes the lot, delivering each
|
|
35
|
+
* destination's prompts as one numbered message. The queue itself is a
|
|
36
|
+
* plain in-memory model; the plugin mirrors it into the JupyterLab state
|
|
37
|
+
* database (via {@link serializeQueuedPrompts}) so a page reload does not
|
|
38
|
+
* silently drop typed comments.
|
|
39
|
+
*/
|
|
40
|
+
export declare class PromptQueue {
|
|
41
|
+
constructor(items?: IQueuedPrompt[]);
|
|
42
|
+
/**
|
|
43
|
+
* The queued prompts, oldest first.
|
|
44
|
+
*/
|
|
45
|
+
get items(): readonly IQueuedPrompt[];
|
|
46
|
+
/**
|
|
47
|
+
* Emitted whenever the queue contents change.
|
|
48
|
+
*/
|
|
49
|
+
get changed(): ISignal<this, void>;
|
|
50
|
+
/**
|
|
51
|
+
* Append a prompt to the queue.
|
|
52
|
+
*/
|
|
53
|
+
add(context: IAskAgentContext, instruction: string, target: AskAgentTarget): void;
|
|
54
|
+
/**
|
|
55
|
+
* Replace the instruction of the queued prompt `id`, if still present.
|
|
56
|
+
*/
|
|
57
|
+
updateInstruction(id: string, instruction: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Repoint the queued prompt `id` at `target`, if still present.
|
|
60
|
+
*/
|
|
61
|
+
updateTarget(id: string, target: AskAgentTarget): void;
|
|
62
|
+
/**
|
|
63
|
+
* Remove the queued prompt `id`, if still present.
|
|
64
|
+
*/
|
|
65
|
+
remove(id: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Remove several queued prompts at once (one signal emission) — used when
|
|
68
|
+
* a batch send succeeds for one target while other targets' prompts stay.
|
|
69
|
+
*/
|
|
70
|
+
removeMany(ids: readonly string[]): void;
|
|
71
|
+
/**
|
|
72
|
+
* Drop every queued prompt.
|
|
73
|
+
*/
|
|
74
|
+
clear(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Replace the whole queue (used to restore and to undo a clear).
|
|
77
|
+
*/
|
|
78
|
+
reset(items: readonly IQueuedPrompt[]): void;
|
|
79
|
+
private _items;
|
|
80
|
+
private _changed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Rebuild queued prompts from a value read back from the state database.
|
|
84
|
+
* Entries that no longer parse are dropped silently — the queue is a
|
|
85
|
+
* convenience buffer, not a document.
|
|
86
|
+
*/
|
|
87
|
+
export declare function deserializeQueuedPrompts(value: unknown): IQueuedPrompt[];
|
|
88
|
+
/**
|
|
89
|
+
* The queue as a JSON value for the state database. Ids are session-scoped
|
|
90
|
+
* handles, so they are not persisted; {@link deserializeQueuedPrompts}
|
|
91
|
+
* assigns fresh ones on load.
|
|
92
|
+
*/
|
|
93
|
+
export declare function serializeQueuedPrompts(items: readonly IQueuedPrompt[]): ReadonlyPartialJSONValue;
|