xtralab 0.11.0 → 0.12.0
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 +37 -229
- 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.d.ts +11 -1
- package/lib/launcher/agents.js +26 -8
- 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 +24 -7
- package/lib/launcher/editors.js +14 -5
- package/lib/launcher/icons.d.ts +0 -37
- package/lib/launcher/icons.js +11 -12
- package/lib/launcher/index.js +5 -2
- 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/launcher.json +4 -4
- 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 +31 -7
- 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 +34 -9
- package/src/launcher/icons.ts +11 -12
- package/src/launcher/index.ts +5 -2
- 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,207 @@
|
|
|
1
|
+
import { Signal } from '@lumino/signaling';
|
|
2
|
+
let idCounter = 0;
|
|
3
|
+
function nextId() {
|
|
4
|
+
return `queued-${Date.now().toString(36)}-${idCounter++}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The accumulated ask-agent prompts awaiting a batch send.
|
|
8
|
+
*
|
|
9
|
+
* Queueing decouples writing review comments from deciding where they go:
|
|
10
|
+
* the popup appends entries here instead of sending, the side panel lists
|
|
11
|
+
* and edits them, and one batch send flushes the lot, delivering each
|
|
12
|
+
* destination's prompts as one numbered message. The queue itself is a
|
|
13
|
+
* plain in-memory model; the plugin mirrors it into the JupyterLab state
|
|
14
|
+
* database (via {@link serializeQueuedPrompts}) so a page reload does not
|
|
15
|
+
* silently drop typed comments.
|
|
16
|
+
*/
|
|
17
|
+
export class PromptQueue {
|
|
18
|
+
constructor(items = []) {
|
|
19
|
+
this._changed = new Signal(this);
|
|
20
|
+
this._items = items;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The queued prompts, oldest first.
|
|
24
|
+
*/
|
|
25
|
+
get items() {
|
|
26
|
+
return this._items;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Emitted whenever the queue contents change.
|
|
30
|
+
*/
|
|
31
|
+
get changed() {
|
|
32
|
+
return this._changed;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Append a prompt to the queue.
|
|
36
|
+
*/
|
|
37
|
+
add(context, instruction, target) {
|
|
38
|
+
this._items = [
|
|
39
|
+
...this._items,
|
|
40
|
+
{ id: nextId(), context, instruction, target }
|
|
41
|
+
];
|
|
42
|
+
this._changed.emit();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Replace the instruction of the queued prompt `id`, if still present.
|
|
46
|
+
*/
|
|
47
|
+
updateInstruction(id, instruction) {
|
|
48
|
+
let touched = false;
|
|
49
|
+
this._items = this._items.map(item => {
|
|
50
|
+
if (item.id !== id || item.instruction === instruction) {
|
|
51
|
+
return item;
|
|
52
|
+
}
|
|
53
|
+
touched = true;
|
|
54
|
+
return { ...item, instruction };
|
|
55
|
+
});
|
|
56
|
+
if (touched) {
|
|
57
|
+
this._changed.emit();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Repoint the queued prompt `id` at `target`, if still present.
|
|
62
|
+
*/
|
|
63
|
+
updateTarget(id, target) {
|
|
64
|
+
let touched = false;
|
|
65
|
+
this._items = this._items.map(item => {
|
|
66
|
+
if (item.id !== id) {
|
|
67
|
+
return item;
|
|
68
|
+
}
|
|
69
|
+
touched = true;
|
|
70
|
+
return { ...item, target };
|
|
71
|
+
});
|
|
72
|
+
if (touched) {
|
|
73
|
+
this._changed.emit();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove the queued prompt `id`, if still present.
|
|
78
|
+
*/
|
|
79
|
+
remove(id) {
|
|
80
|
+
this.removeMany([id]);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Remove several queued prompts at once (one signal emission) — used when
|
|
84
|
+
* a batch send succeeds for one target while other targets' prompts stay.
|
|
85
|
+
*/
|
|
86
|
+
removeMany(ids) {
|
|
87
|
+
const drop = new Set(ids);
|
|
88
|
+
const remaining = this._items.filter(item => !drop.has(item.id));
|
|
89
|
+
if (remaining.length !== this._items.length) {
|
|
90
|
+
this._items = remaining;
|
|
91
|
+
this._changed.emit();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Drop every queued prompt.
|
|
96
|
+
*/
|
|
97
|
+
clear() {
|
|
98
|
+
if (this._items.length > 0) {
|
|
99
|
+
this._items = [];
|
|
100
|
+
this._changed.emit();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Replace the whole queue (used to restore and to undo a clear).
|
|
105
|
+
*/
|
|
106
|
+
reset(items) {
|
|
107
|
+
this._items = [...items];
|
|
108
|
+
this._changed.emit();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validate one persisted context. Only fields that still match the
|
|
113
|
+
* `IAskAgentContext` contract are kept, so schema drift (or manual
|
|
114
|
+
* state-database edits) degrades an entry instead of poisoning the queue.
|
|
115
|
+
*/
|
|
116
|
+
function sanitizeContext(value) {
|
|
117
|
+
if (value === null || typeof value !== 'object') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const raw = value;
|
|
121
|
+
if (typeof raw.path !== 'string' || raw.path.length === 0) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const context = {
|
|
125
|
+
path: raw.path,
|
|
126
|
+
text: typeof raw.text === 'string' ? raw.text : ''
|
|
127
|
+
};
|
|
128
|
+
if (typeof raw.cwd === 'string') {
|
|
129
|
+
context.cwd = raw.cwd;
|
|
130
|
+
}
|
|
131
|
+
const cell = raw.cell;
|
|
132
|
+
if (cell !== null &&
|
|
133
|
+
typeof cell === 'object' &&
|
|
134
|
+
typeof cell.index === 'number' &&
|
|
135
|
+
typeof cell.type === 'string') {
|
|
136
|
+
context.cell = { index: cell.index, type: cell.type };
|
|
137
|
+
}
|
|
138
|
+
if (typeof raw.startLine === 'number') {
|
|
139
|
+
context.startLine = raw.startLine;
|
|
140
|
+
}
|
|
141
|
+
if (typeof raw.endLine === 'number') {
|
|
142
|
+
context.endLine = raw.endLine;
|
|
143
|
+
}
|
|
144
|
+
if (typeof raw.linesInWorkingFile === 'boolean') {
|
|
145
|
+
context.linesInWorkingFile = raw.linesInWorkingFile;
|
|
146
|
+
}
|
|
147
|
+
if (typeof raw.location === 'string') {
|
|
148
|
+
context.location = raw.location;
|
|
149
|
+
}
|
|
150
|
+
if (typeof raw.note === 'string') {
|
|
151
|
+
context.note = raw.note;
|
|
152
|
+
}
|
|
153
|
+
return context;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate one persisted target; anything that does not parse becomes
|
|
157
|
+
* `null`, which the panel surfaces as "pick where to send this".
|
|
158
|
+
*/
|
|
159
|
+
function sanitizeTarget(value) {
|
|
160
|
+
if (value === null || typeof value !== 'object') {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const raw = value;
|
|
164
|
+
if (raw.kind === 'new' && typeof raw.agentId === 'string') {
|
|
165
|
+
return { kind: 'new', agentId: raw.agentId };
|
|
166
|
+
}
|
|
167
|
+
if (raw.kind === 'session' && typeof raw.name === 'string') {
|
|
168
|
+
return { kind: 'session', name: raw.name };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Rebuild queued prompts from a value read back from the state database.
|
|
174
|
+
* Entries that no longer parse are dropped silently — the queue is a
|
|
175
|
+
* convenience buffer, not a document.
|
|
176
|
+
*/
|
|
177
|
+
export function deserializeQueuedPrompts(value) {
|
|
178
|
+
if (!Array.isArray(value)) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const items = [];
|
|
182
|
+
for (const entry of value) {
|
|
183
|
+
const record = entry;
|
|
184
|
+
const context = sanitizeContext(record === null || record === void 0 ? void 0 : record.context);
|
|
185
|
+
if (context !== null && typeof (record === null || record === void 0 ? void 0 : record.instruction) === 'string') {
|
|
186
|
+
items.push({
|
|
187
|
+
id: nextId(),
|
|
188
|
+
context,
|
|
189
|
+
instruction: record.instruction,
|
|
190
|
+
target: sanitizeTarget(record === null || record === void 0 ? void 0 : record.target)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return items;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The queue as a JSON value for the state database. Ids are session-scoped
|
|
198
|
+
* handles, so they are not persisted; {@link deserializeQueuedPrompts}
|
|
199
|
+
* assigns fresh ones on load.
|
|
200
|
+
*/
|
|
201
|
+
export function serializeQueuedPrompts(items) {
|
|
202
|
+
return items.map(({ context, instruction, target }) => ({
|
|
203
|
+
context: { ...context },
|
|
204
|
+
instruction,
|
|
205
|
+
target: target === null ? null : { ...target }
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
4
|
+
import type { Message } from '@lumino/messaging';
|
|
5
|
+
import type { IAgent } from '../launcher/agents';
|
|
6
|
+
import type { PromptQueue } from './queue';
|
|
7
|
+
import type { ISessionTarget } from './targetPicker';
|
|
8
|
+
/**
|
|
9
|
+
* The right-sidebar review panel for queued ask-agent prompts.
|
|
10
|
+
*
|
|
11
|
+
* Lists every queued comment — jump-to-code context line, snippet preview,
|
|
12
|
+
* editable instruction, its own destination picker, remove button — plus a
|
|
13
|
+
* send button that flushes the whole queue in one go: prompts sharing a
|
|
14
|
+
* destination are combined into one numbered message, and each destination
|
|
15
|
+
* gets its own delivery. The widget re-renders on queue changes itself; the
|
|
16
|
+
* plugin additionally ties it to the agent registry and terminal signals so
|
|
17
|
+
* the destination choices stay current.
|
|
18
|
+
*/
|
|
19
|
+
export declare class AskAgentQueuePanel extends ReactWidget {
|
|
20
|
+
constructor(options: AskAgentQueuePanel.IOptions);
|
|
21
|
+
render(): JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Dispose on close (e.g. the close button when the panel has been dragged
|
|
24
|
+
* to the main area) rather than leaving a detached, reusable widget
|
|
25
|
+
* behind. The plugin recreates the panel in the side area on demand; the
|
|
26
|
+
* queue itself lives on in the model.
|
|
27
|
+
*/
|
|
28
|
+
protected onCloseRequest(msg: Message): void;
|
|
29
|
+
private _onQueueChanged;
|
|
30
|
+
private _options;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A namespace for `AskAgentQueuePanel` statics.
|
|
34
|
+
*/
|
|
35
|
+
export declare namespace AskAgentQueuePanel {
|
|
36
|
+
/**
|
|
37
|
+
* Construction options for {@link AskAgentQueuePanel}.
|
|
38
|
+
*/
|
|
39
|
+
interface IOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The queue the panel lists and edits.
|
|
42
|
+
*/
|
|
43
|
+
queue: PromptQueue;
|
|
44
|
+
/**
|
|
45
|
+
* Command registry used to open queued locations in the editor.
|
|
46
|
+
*/
|
|
47
|
+
commands: CommandRegistry;
|
|
48
|
+
/**
|
|
49
|
+
* Live reader of the prompt-capable agents (called on every render).
|
|
50
|
+
*/
|
|
51
|
+
agents: () => IAgent[];
|
|
52
|
+
/**
|
|
53
|
+
* Live reader of the running agent terminals (called on every render).
|
|
54
|
+
*/
|
|
55
|
+
targets: () => ISessionTarget[];
|
|
56
|
+
/**
|
|
57
|
+
* Live reader of whether a batch send is in flight (called on every
|
|
58
|
+
* render). While true the send and clear buttons are disabled, so one
|
|
59
|
+
* flush cannot be double-delivered.
|
|
60
|
+
*/
|
|
61
|
+
sending: () => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Translation bundle for the panel's own labels.
|
|
64
|
+
*/
|
|
65
|
+
trans: TranslationBundle;
|
|
66
|
+
/**
|
|
67
|
+
* Send every queued prompt to its own destination (the panel only calls
|
|
68
|
+
* this while all instructions and destinations are valid).
|
|
69
|
+
*/
|
|
70
|
+
onSend: () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Clear the queue (the plugin offers an undo toast).
|
|
73
|
+
*/
|
|
74
|
+
onClear: () => void;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Notification } from '@jupyterlab/apputils';
|
|
2
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
|
+
import { closeIcon, HTMLSelect, ReactWidget } from '@jupyterlab/ui-components';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { contextSummary } from './popup';
|
|
6
|
+
import { serverPath } from './prompt';
|
|
7
|
+
/**
|
|
8
|
+
* Command the context buttons dispatch; keeps the panel decoupled from the plugin.
|
|
9
|
+
*/
|
|
10
|
+
const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
|
|
11
|
+
/**
|
|
12
|
+
* Textarea height that fits `text` without scrolling, within sane bounds:
|
|
13
|
+
* tall enough to look editable, short enough that one long comment cannot
|
|
14
|
+
* crowd out the rest of the queue.
|
|
15
|
+
*/
|
|
16
|
+
function rowsFor(text) {
|
|
17
|
+
return Math.min(8, Math.max(2, text.split('\n').length));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A target as a `<select>` option value: `session:<name>` for a running
|
|
21
|
+
* agent terminal, `new:<agentId>` for a fresh terminal, `''` for a missing
|
|
22
|
+
* target (the placeholder option).
|
|
23
|
+
*/
|
|
24
|
+
function encodeTarget(target) {
|
|
25
|
+
if (target === null) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return target.kind === 'session'
|
|
29
|
+
? `session:${target.name}`
|
|
30
|
+
: `new:${target.agentId}`;
|
|
31
|
+
}
|
|
32
|
+
function decodeTarget(value) {
|
|
33
|
+
if (value.startsWith('session:')) {
|
|
34
|
+
return { kind: 'session', name: value.slice('session:'.length) };
|
|
35
|
+
}
|
|
36
|
+
if (value.startsWith('new:')) {
|
|
37
|
+
return { kind: 'new', agentId: value.slice('new:'.length) };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether `target` can be sent to right now: its session is still running,
|
|
43
|
+
* or its agent still accepts a command-line prompt.
|
|
44
|
+
*/
|
|
45
|
+
function targetIsLive(target, agents, targets) {
|
|
46
|
+
if (target === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return target.kind === 'session'
|
|
50
|
+
? targets.some(session => session.name === target.name)
|
|
51
|
+
: agents.some(agent => agent.id === target.agentId);
|
|
52
|
+
}
|
|
53
|
+
function QueuePanelComponent(props) {
|
|
54
|
+
const { queue, commands, agents: readAgents, targets: readTargets, sending: readSending, trans, onSend, onClear } = props;
|
|
55
|
+
// Snapshots for this render; the widget re-renders on every queue, agent
|
|
56
|
+
// registry and terminal change, so these stay current.
|
|
57
|
+
const items = queue.items;
|
|
58
|
+
const agents = readAgents();
|
|
59
|
+
const targets = readTargets();
|
|
60
|
+
const sending = readSending();
|
|
61
|
+
const complete = items.every(item => item.instruction.trim().length > 0);
|
|
62
|
+
const targeted = items.every(item => targetIsLive(item.target, agents, targets));
|
|
63
|
+
const canSend = items.length > 0 && complete && targeted && !sending;
|
|
64
|
+
const jumpTo = (item) => {
|
|
65
|
+
var _a;
|
|
66
|
+
const { context } = item;
|
|
67
|
+
const path = serverPath(context);
|
|
68
|
+
// The highlighter needs line numbers that index the working file:
|
|
69
|
+
// notebook lines are cell-relative (or raw-JSON offsets), and diff
|
|
70
|
+
// selections may come from another revision — just open the document
|
|
71
|
+
// in those cases.
|
|
72
|
+
const jump = context.cell === undefined &&
|
|
73
|
+
context.startLine !== undefined &&
|
|
74
|
+
context.linesInWorkingFile !== false &&
|
|
75
|
+
PathExt.extname(path) !== '.ipynb'
|
|
76
|
+
? commands.execute(HIGHLIGHT_LINES_COMMAND, {
|
|
77
|
+
path,
|
|
78
|
+
line: context.startLine,
|
|
79
|
+
endLine: (_a = context.endLine) !== null && _a !== void 0 ? _a : context.startLine
|
|
80
|
+
})
|
|
81
|
+
: commands.execute('docmanager:open', { path });
|
|
82
|
+
jump.catch((error) => {
|
|
83
|
+
console.error('xtralab: failed to open the queued location', error);
|
|
84
|
+
Notification.error(trans.__('Could not open %1', path), {
|
|
85
|
+
autoClose: 3000
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
if (items.length === 0) {
|
|
90
|
+
return (React.createElement("div", { className: "jp-xtralab-AskAgentQueue-body" },
|
|
91
|
+
React.createElement("div", { className: "jp-xtralab-AskAgentQueue-blank" }, trans.__('No queued prompts. Select code in an editor, notebook or git diff and choose “Queue” in the ask-agent popup; each prompt keeps its own destination and everything is sent in one go.'))));
|
|
92
|
+
}
|
|
93
|
+
return (React.createElement("div", { className: "jp-xtralab-AskAgentQueue-body" },
|
|
94
|
+
React.createElement("ul", { className: "jp-xtralab-AskAgentQueue-list" }, items.map(item => {
|
|
95
|
+
var _a, _b, _c, _d;
|
|
96
|
+
const summary = contextSummary(item.context, trans);
|
|
97
|
+
const missing = item.instruction.trim().length === 0;
|
|
98
|
+
const target = item.target;
|
|
99
|
+
const live = targetIsLive(target, agents, targets);
|
|
100
|
+
// Say what sending will do for this prompt — paste into a running
|
|
101
|
+
// terminal, or start a fresh one — and badge it with the agent's
|
|
102
|
+
// icon, so a mixed queue can be scanned without opening dropdowns.
|
|
103
|
+
let icon = null;
|
|
104
|
+
let targetTitle = trans.__('The destination picked for this prompt is gone — choose another');
|
|
105
|
+
if (live && target !== null) {
|
|
106
|
+
if (target.kind === 'session') {
|
|
107
|
+
icon =
|
|
108
|
+
(_b = (_a = targets.find(session => session.name === target.name)) === null || _a === void 0 ? void 0 : _a.icon) !== null && _b !== void 0 ? _b : null;
|
|
109
|
+
targetTitle = trans.__('Sends into the agent running in terminal %1', target.name);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const agent = agents.find(entry => entry.id === target.agentId);
|
|
113
|
+
icon = (_c = agent === null || agent === void 0 ? void 0 : agent.icon) !== null && _c !== void 0 ? _c : null;
|
|
114
|
+
targetTitle = trans.__('Starts %1 in a new terminal', (_d = agent === null || agent === void 0 ? void 0 : agent.label) !== null && _d !== void 0 ? _d : target.agentId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return (React.createElement("li", { className: "jp-xtralab-AskAgentQueue-item", key: item.id },
|
|
118
|
+
React.createElement("div", { className: "jp-xtralab-AskAgentQueue-itemHeader" },
|
|
119
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-AskAgentQueue-itemContext", title: trans.__('Open %1 in the editor', serverPath(item.context)), onClick: () => jumpTo(item) }, summary),
|
|
120
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-AskAgentQueue-itemRemove", title: trans.__('Remove from the queue'), "aria-label": trans.__('Remove from the queue'), onClick: () => queue.remove(item.id) },
|
|
121
|
+
React.createElement(closeIcon.react, { tag: "span", className: "jp-xtralab-AskAgentQueue-itemRemoveIcon" }))),
|
|
122
|
+
item.context.text.length > 0 && (React.createElement("pre", { className: "jp-xtralab-AskAgentQueue-snippet" }, item.context.text)),
|
|
123
|
+
React.createElement("textarea", { className: "jp-xtralab-AskAgentQueue-instruction", rows: rowsFor(item.instruction), spellCheck: false, "aria-invalid": missing, "aria-label": trans.__('Instruction about %1', summary), placeholder: trans.__('Describe the change…'), defaultValue: item.instruction, onChange: event => queue.updateInstruction(item.id, event.target.value) }),
|
|
124
|
+
React.createElement("div", { className: "jp-xtralab-AskAgentQueue-itemTargetRow" },
|
|
125
|
+
React.createElement("span", { className: "jp-xtralab-AskAgentQueue-itemTargetIcon", "aria-hidden": "true" }, icon !== null && React.createElement(icon.react, { tag: "span" })),
|
|
126
|
+
React.createElement(HTMLSelect, { className: "jp-xtralab-AskAgentQueue-itemTarget", "aria-invalid": !live, "aria-label": trans.__('Send to'), title: targetTitle, value: live ? encodeTarget(target) : '', onChange: event => {
|
|
127
|
+
const picked = decodeTarget(event.target.value);
|
|
128
|
+
if (picked !== null) {
|
|
129
|
+
queue.updateTarget(item.id, picked);
|
|
130
|
+
}
|
|
131
|
+
} },
|
|
132
|
+
!live && (React.createElement("option", { value: "", disabled: true }, trans.__('Choose where to send…'))),
|
|
133
|
+
targets.length > 0 && (React.createElement("optgroup", { label: trans.__('Running agents') }, targets.map(session => (React.createElement("option", { key: session.name, value: `session:${session.name}` }, trans.__('Terminal %1 · %2', session.name, session.label)))))),
|
|
134
|
+
agents.length > 0 && (React.createElement("optgroup", { label: trans.__('New terminal') }, agents.map(agent => (React.createElement("option", { key: agent.id, value: `new:${agent.id}` }, trans.__('New terminal · %1', agent.label))))))))));
|
|
135
|
+
})),
|
|
136
|
+
React.createElement("div", { className: "jp-xtralab-AskAgentQueue-footer" },
|
|
137
|
+
agents.length === 0 && targets.length === 0 && (React.createElement("div", { className: "jp-xtralab-AskAgentQueue-noTargets" }, trans.__('No agent accepts an initial prompt. Configure agents in the launcher settings.'))),
|
|
138
|
+
React.createElement("div", { className: "jp-xtralab-AskAgentQueue-actions" },
|
|
139
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-AskAgentQueue-clear", disabled: sending, title: trans.__('Remove every queued prompt'), onClick: onClear }, trans.__('Clear')),
|
|
140
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-AskAgentQueue-send", disabled: !canSend, title: sending
|
|
141
|
+
? trans.__('Sending…')
|
|
142
|
+
: !complete
|
|
143
|
+
? trans.__('Every queued prompt needs an instruction')
|
|
144
|
+
: !targeted
|
|
145
|
+
? trans.__('Every queued prompt needs a destination')
|
|
146
|
+
: trans.__('Send each queued prompt to its chosen destination'), onClick: () => {
|
|
147
|
+
if (canSend) {
|
|
148
|
+
onSend();
|
|
149
|
+
}
|
|
150
|
+
} }, sending
|
|
151
|
+
? trans.__('Sending…')
|
|
152
|
+
: trans._n('Send %1 prompt', 'Send %1 prompts', items.length))))));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The right-sidebar review panel for queued ask-agent prompts.
|
|
156
|
+
*
|
|
157
|
+
* Lists every queued comment — jump-to-code context line, snippet preview,
|
|
158
|
+
* editable instruction, its own destination picker, remove button — plus a
|
|
159
|
+
* send button that flushes the whole queue in one go: prompts sharing a
|
|
160
|
+
* destination are combined into one numbered message, and each destination
|
|
161
|
+
* gets its own delivery. The widget re-renders on queue changes itself; the
|
|
162
|
+
* plugin additionally ties it to the agent registry and terminal signals so
|
|
163
|
+
* the destination choices stay current.
|
|
164
|
+
*/
|
|
165
|
+
export class AskAgentQueuePanel extends ReactWidget {
|
|
166
|
+
constructor(options) {
|
|
167
|
+
super();
|
|
168
|
+
this._options = options;
|
|
169
|
+
this.addClass('jp-xtralab-AskAgentQueue');
|
|
170
|
+
options.queue.changed.connect(this._onQueueChanged, this);
|
|
171
|
+
}
|
|
172
|
+
render() {
|
|
173
|
+
return React.createElement(QueuePanelComponent, { ...this._options });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Dispose on close (e.g. the close button when the panel has been dragged
|
|
177
|
+
* to the main area) rather than leaving a detached, reusable widget
|
|
178
|
+
* behind. The plugin recreates the panel in the side area on demand; the
|
|
179
|
+
* queue itself lives on in the model.
|
|
180
|
+
*/
|
|
181
|
+
onCloseRequest(msg) {
|
|
182
|
+
super.onCloseRequest(msg);
|
|
183
|
+
this.dispose();
|
|
184
|
+
}
|
|
185
|
+
_onQueueChanged() {
|
|
186
|
+
this.update();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import type { IAgent } from '../launcher/agents';
|
|
4
|
+
/**
|
|
5
|
+
* A running agent terminal offered as a prompt target: the session data
|
|
6
|
+
* from `IAgentTerminals` plus the matching agent's icon, resolved by the
|
|
7
|
+
* plugin so these components stay presentation-only.
|
|
8
|
+
*/
|
|
9
|
+
export interface ISessionTarget {
|
|
10
|
+
/**
|
|
11
|
+
* The terminal session name.
|
|
12
|
+
*/
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* The session's display label (usually the agent's name).
|
|
16
|
+
*/
|
|
17
|
+
label: string;
|
|
18
|
+
/**
|
|
19
|
+
* The agent's latest activity line, when one is available.
|
|
20
|
+
*/
|
|
21
|
+
activity: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* The running agent's icon.
|
|
24
|
+
*/
|
|
25
|
+
icon: LabIcon;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The chip row picking where a prompt goes: "New terminal" plus one chip
|
|
29
|
+
* per running agent session. Renders nothing while no agent session is
|
|
30
|
+
* running (a new terminal is then the only possibility and the row would
|
|
31
|
+
* be noise).
|
|
32
|
+
*/
|
|
33
|
+
export declare function TargetChips(props: {
|
|
34
|
+
/**
|
|
35
|
+
* Agents that could start in a new terminal; gates the "New terminal" chip.
|
|
36
|
+
*/
|
|
37
|
+
agents: IAgent[];
|
|
38
|
+
/**
|
|
39
|
+
* The running agent sessions.
|
|
40
|
+
*/
|
|
41
|
+
targets: ISessionTarget[];
|
|
42
|
+
/**
|
|
43
|
+
* The selected session name, or `null` for a new terminal.
|
|
44
|
+
*/
|
|
45
|
+
targetName: string | null;
|
|
46
|
+
trans: TranslationBundle;
|
|
47
|
+
onSelect: (targetName: string | null) => void;
|
|
48
|
+
}): JSX.Element | null;
|
|
49
|
+
/**
|
|
50
|
+
* The icon radiogroup picking which agent a new terminal starts with.
|
|
51
|
+
* Renders nothing when no agent accepts an initial prompt.
|
|
52
|
+
*/
|
|
53
|
+
export declare function AgentChoices(props: {
|
|
54
|
+
agents: IAgent[];
|
|
55
|
+
agentId: string;
|
|
56
|
+
trans: TranslationBundle;
|
|
57
|
+
onSelect: (agentId: string) => void;
|
|
58
|
+
}): JSX.Element | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { addIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard support shared by the two radiogroups below, following the ARIA
|
|
5
|
+
* radio-group pattern: the group is one tab stop (only the checked radio is
|
|
6
|
+
* tabbable) and the arrow keys move the selection to the neighbouring
|
|
7
|
+
* radio, wrapping around. Selection follows focus, so the handler clicks
|
|
8
|
+
* the neighbour and the re-render moves the roving tabindex along.
|
|
9
|
+
*/
|
|
10
|
+
function onRadioGroupKeyDown(event) {
|
|
11
|
+
let delta;
|
|
12
|
+
switch (event.key) {
|
|
13
|
+
case 'ArrowRight':
|
|
14
|
+
case 'ArrowDown':
|
|
15
|
+
delta = 1;
|
|
16
|
+
break;
|
|
17
|
+
case 'ArrowLeft':
|
|
18
|
+
case 'ArrowUp':
|
|
19
|
+
delta = -1;
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const radios = Array.from(event.currentTarget.querySelectorAll('[role="radio"]'));
|
|
25
|
+
const index = radios.findIndex(radio => radio === event.target);
|
|
26
|
+
if (index === -1) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
const next = radios[(index + delta + radios.length) % radios.length];
|
|
31
|
+
next.focus();
|
|
32
|
+
next.click();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The chip row picking where a prompt goes: "New terminal" plus one chip
|
|
36
|
+
* per running agent session. Renders nothing while no agent session is
|
|
37
|
+
* running (a new terminal is then the only possibility and the row would
|
|
38
|
+
* be noise).
|
|
39
|
+
*/
|
|
40
|
+
export function TargetChips(props) {
|
|
41
|
+
const { agents, targets, targetName, trans, onSelect } = props;
|
|
42
|
+
if (targets.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return (React.createElement("div", { className: "jp-xtralab-AskAgent-targets", role: "radiogroup", "aria-label": trans.__('Send to'), onKeyDown: onRadioGroupKeyDown },
|
|
46
|
+
agents.length > 0 && (React.createElement("button", { type: "button", role: "radio", "aria-checked": targetName === null, tabIndex: targetName === null ? 0 : -1, title: trans.__('Start the agent in a new terminal'), className: 'jp-xtralab-AskAgent-targetButton' +
|
|
47
|
+
(targetName === null ? ' jp-mod-selected' : ''),
|
|
48
|
+
// Keep focus where the user is typing while picking a target.
|
|
49
|
+
onMouseDown: event => event.preventDefault(), onClick: () => onSelect(null) },
|
|
50
|
+
React.createElement(addIcon.react, { tag: "span", className: "jp-xtralab-AskAgent-targetIcon" }),
|
|
51
|
+
React.createElement("span", { className: "jp-xtralab-AskAgent-targetLabel" }, trans.__('New terminal')))),
|
|
52
|
+
targets.map(target => (React.createElement("button", { key: target.name, type: "button", role: "radio", "aria-checked": target.name === targetName, tabIndex: target.name === targetName ? 0 : -1, title: target.activity
|
|
53
|
+
? trans.__('%1 · %2 — %3', target.label, target.name, target.activity)
|
|
54
|
+
: trans.__('%1 · %2', target.label, target.name), className: 'jp-xtralab-AskAgent-targetButton' +
|
|
55
|
+
(target.name === targetName ? ' jp-mod-selected' : ''), onMouseDown: event => event.preventDefault(), onClick: () => onSelect(target.name) },
|
|
56
|
+
React.createElement(target.icon.react, { tag: "span", className: "jp-xtralab-AskAgent-targetIcon" }),
|
|
57
|
+
React.createElement("span", { className: "jp-xtralab-AskAgent-targetLabel" }, target.label),
|
|
58
|
+
React.createElement("span", { className: "jp-xtralab-AskAgent-targetName" }, target.name))))));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The icon radiogroup picking which agent a new terminal starts with.
|
|
62
|
+
* Renders nothing when no agent accepts an initial prompt.
|
|
63
|
+
*/
|
|
64
|
+
export function AgentChoices(props) {
|
|
65
|
+
const { agents, agentId, trans, onSelect } = props;
|
|
66
|
+
if (agents.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return (React.createElement("div", { className: "jp-xtralab-AskAgent-agents", role: "radiogroup", "aria-label": trans.__('Agent'), onKeyDown: onRadioGroupKeyDown }, agents.map(agent => (React.createElement("button", { key: agent.id, type: "button", role: "radio", "aria-checked": agent.id === agentId, tabIndex: agent.id === agentId ? 0 : -1, title: agent.label, "aria-label": agent.label, className: 'jp-xtralab-AskAgent-agentButton' +
|
|
70
|
+
(agent.id === agentId ? ' jp-mod-selected' : ''),
|
|
71
|
+
// Keep focus where the user is typing while picking an agent.
|
|
72
|
+
onMouseDown: event => event.preventDefault(), onClick: () => onSelect(agent.id) },
|
|
73
|
+
React.createElement(agent.icon.react, { tag: "span", className: "jp-xtralab-AskAgent-agentIcon" }))))));
|
|
74
|
+
}
|