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,269 @@
|
|
|
1
|
+
import type { ReadonlyPartialJSONValue } from '@lumino/coreutils';
|
|
2
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
3
|
+
|
|
4
|
+
import type { AskAgentTarget, IAskAgentContext } from './tokens';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One queued prompt: a code selection, the user's instruction about it, and
|
|
8
|
+
* where it should go. Every prompt is independent — a queue can mix targets
|
|
9
|
+
* (different agents, new terminals, different running sessions) freely.
|
|
10
|
+
*/
|
|
11
|
+
export interface IQueuedPrompt {
|
|
12
|
+
/**
|
|
13
|
+
* Handle for edits and removal, unique within the page session.
|
|
14
|
+
*/
|
|
15
|
+
id: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The code selection the instruction is about.
|
|
19
|
+
*/
|
|
20
|
+
context: IAskAgentContext;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The user's typed comment.
|
|
24
|
+
*/
|
|
25
|
+
instruction: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The destination picked for this prompt (captured from the popup when it
|
|
29
|
+
* was queued, editable in the panel). `null` when a persisted target no
|
|
30
|
+
* longer parses — the panel then asks for a new pick before sending.
|
|
31
|
+
*/
|
|
32
|
+
target: AskAgentTarget | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let idCounter = 0;
|
|
36
|
+
|
|
37
|
+
function nextId(): string {
|
|
38
|
+
return `queued-${Date.now().toString(36)}-${idCounter++}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The accumulated ask-agent prompts awaiting a batch send.
|
|
43
|
+
*
|
|
44
|
+
* Queueing decouples writing review comments from deciding where they go:
|
|
45
|
+
* the popup appends entries here instead of sending, the side panel lists
|
|
46
|
+
* and edits them, and one batch send flushes the lot, delivering each
|
|
47
|
+
* destination's prompts as one numbered message. The queue itself is a
|
|
48
|
+
* plain in-memory model; the plugin mirrors it into the JupyterLab state
|
|
49
|
+
* database (via {@link serializeQueuedPrompts}) so a page reload does not
|
|
50
|
+
* silently drop typed comments.
|
|
51
|
+
*/
|
|
52
|
+
export class PromptQueue {
|
|
53
|
+
constructor(items: IQueuedPrompt[] = []) {
|
|
54
|
+
this._items = items;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The queued prompts, oldest first.
|
|
59
|
+
*/
|
|
60
|
+
get items(): readonly IQueuedPrompt[] {
|
|
61
|
+
return this._items;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Emitted whenever the queue contents change.
|
|
66
|
+
*/
|
|
67
|
+
get changed(): ISignal<this, void> {
|
|
68
|
+
return this._changed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Append a prompt to the queue.
|
|
73
|
+
*/
|
|
74
|
+
add(
|
|
75
|
+
context: IAskAgentContext,
|
|
76
|
+
instruction: string,
|
|
77
|
+
target: AskAgentTarget
|
|
78
|
+
): void {
|
|
79
|
+
this._items = [
|
|
80
|
+
...this._items,
|
|
81
|
+
{ id: nextId(), context, instruction, target }
|
|
82
|
+
];
|
|
83
|
+
this._changed.emit();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Replace the instruction of the queued prompt `id`, if still present.
|
|
88
|
+
*/
|
|
89
|
+
updateInstruction(id: string, instruction: string): void {
|
|
90
|
+
let touched = false;
|
|
91
|
+
this._items = this._items.map(item => {
|
|
92
|
+
if (item.id !== id || item.instruction === instruction) {
|
|
93
|
+
return item;
|
|
94
|
+
}
|
|
95
|
+
touched = true;
|
|
96
|
+
return { ...item, instruction };
|
|
97
|
+
});
|
|
98
|
+
if (touched) {
|
|
99
|
+
this._changed.emit();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Repoint the queued prompt `id` at `target`, if still present.
|
|
105
|
+
*/
|
|
106
|
+
updateTarget(id: string, target: AskAgentTarget): void {
|
|
107
|
+
let touched = false;
|
|
108
|
+
this._items = this._items.map(item => {
|
|
109
|
+
if (item.id !== id) {
|
|
110
|
+
return item;
|
|
111
|
+
}
|
|
112
|
+
touched = true;
|
|
113
|
+
return { ...item, target };
|
|
114
|
+
});
|
|
115
|
+
if (touched) {
|
|
116
|
+
this._changed.emit();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove the queued prompt `id`, if still present.
|
|
122
|
+
*/
|
|
123
|
+
remove(id: string): void {
|
|
124
|
+
this.removeMany([id]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove several queued prompts at once (one signal emission) — used when
|
|
129
|
+
* a batch send succeeds for one target while other targets' prompts stay.
|
|
130
|
+
*/
|
|
131
|
+
removeMany(ids: readonly string[]): void {
|
|
132
|
+
const drop = new Set(ids);
|
|
133
|
+
const remaining = this._items.filter(item => !drop.has(item.id));
|
|
134
|
+
if (remaining.length !== this._items.length) {
|
|
135
|
+
this._items = remaining;
|
|
136
|
+
this._changed.emit();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Drop every queued prompt.
|
|
142
|
+
*/
|
|
143
|
+
clear(): void {
|
|
144
|
+
if (this._items.length > 0) {
|
|
145
|
+
this._items = [];
|
|
146
|
+
this._changed.emit();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Replace the whole queue (used to restore and to undo a clear).
|
|
152
|
+
*/
|
|
153
|
+
reset(items: readonly IQueuedPrompt[]): void {
|
|
154
|
+
this._items = [...items];
|
|
155
|
+
this._changed.emit();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _items: IQueuedPrompt[];
|
|
159
|
+
private _changed = new Signal<this, void>(this);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate one persisted context. Only fields that still match the
|
|
164
|
+
* `IAskAgentContext` contract are kept, so schema drift (or manual
|
|
165
|
+
* state-database edits) degrades an entry instead of poisoning the queue.
|
|
166
|
+
*/
|
|
167
|
+
function sanitizeContext(value: unknown): IAskAgentContext | null {
|
|
168
|
+
if (value === null || typeof value !== 'object') {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const raw = value as Record<string, unknown>;
|
|
172
|
+
if (typeof raw.path !== 'string' || raw.path.length === 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const context: IAskAgentContext = {
|
|
176
|
+
path: raw.path,
|
|
177
|
+
text: typeof raw.text === 'string' ? raw.text : ''
|
|
178
|
+
};
|
|
179
|
+
if (typeof raw.cwd === 'string') {
|
|
180
|
+
context.cwd = raw.cwd;
|
|
181
|
+
}
|
|
182
|
+
const cell = raw.cell as { index?: unknown; type?: unknown } | null;
|
|
183
|
+
if (
|
|
184
|
+
cell !== null &&
|
|
185
|
+
typeof cell === 'object' &&
|
|
186
|
+
typeof cell.index === 'number' &&
|
|
187
|
+
typeof cell.type === 'string'
|
|
188
|
+
) {
|
|
189
|
+
context.cell = { index: cell.index, type: cell.type };
|
|
190
|
+
}
|
|
191
|
+
if (typeof raw.startLine === 'number') {
|
|
192
|
+
context.startLine = raw.startLine;
|
|
193
|
+
}
|
|
194
|
+
if (typeof raw.endLine === 'number') {
|
|
195
|
+
context.endLine = raw.endLine;
|
|
196
|
+
}
|
|
197
|
+
if (typeof raw.linesInWorkingFile === 'boolean') {
|
|
198
|
+
context.linesInWorkingFile = raw.linesInWorkingFile;
|
|
199
|
+
}
|
|
200
|
+
if (typeof raw.location === 'string') {
|
|
201
|
+
context.location = raw.location;
|
|
202
|
+
}
|
|
203
|
+
if (typeof raw.note === 'string') {
|
|
204
|
+
context.note = raw.note;
|
|
205
|
+
}
|
|
206
|
+
return context;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validate one persisted target; anything that does not parse becomes
|
|
211
|
+
* `null`, which the panel surfaces as "pick where to send this".
|
|
212
|
+
*/
|
|
213
|
+
function sanitizeTarget(value: unknown): AskAgentTarget | null {
|
|
214
|
+
if (value === null || typeof value !== 'object') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const raw = value as { kind?: unknown; agentId?: unknown; name?: unknown };
|
|
218
|
+
if (raw.kind === 'new' && typeof raw.agentId === 'string') {
|
|
219
|
+
return { kind: 'new', agentId: raw.agentId };
|
|
220
|
+
}
|
|
221
|
+
if (raw.kind === 'session' && typeof raw.name === 'string') {
|
|
222
|
+
return { kind: 'session', name: raw.name };
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Rebuild queued prompts from a value read back from the state database.
|
|
229
|
+
* Entries that no longer parse are dropped silently — the queue is a
|
|
230
|
+
* convenience buffer, not a document.
|
|
231
|
+
*/
|
|
232
|
+
export function deserializeQueuedPrompts(value: unknown): IQueuedPrompt[] {
|
|
233
|
+
if (!Array.isArray(value)) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const items: IQueuedPrompt[] = [];
|
|
237
|
+
for (const entry of value) {
|
|
238
|
+
const record = entry as {
|
|
239
|
+
context?: unknown;
|
|
240
|
+
instruction?: unknown;
|
|
241
|
+
target?: unknown;
|
|
242
|
+
} | null;
|
|
243
|
+
const context = sanitizeContext(record?.context);
|
|
244
|
+
if (context !== null && typeof record?.instruction === 'string') {
|
|
245
|
+
items.push({
|
|
246
|
+
id: nextId(),
|
|
247
|
+
context,
|
|
248
|
+
instruction: record.instruction,
|
|
249
|
+
target: sanitizeTarget(record?.target)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return items;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The queue as a JSON value for the state database. Ids are session-scoped
|
|
258
|
+
* handles, so they are not persisted; {@link deserializeQueuedPrompts}
|
|
259
|
+
* assigns fresh ones on load.
|
|
260
|
+
*/
|
|
261
|
+
export function serializeQueuedPrompts(
|
|
262
|
+
items: readonly IQueuedPrompt[]
|
|
263
|
+
): ReadonlyPartialJSONValue {
|
|
264
|
+
return items.map(({ context, instruction, target }) => ({
|
|
265
|
+
context: { ...context },
|
|
266
|
+
instruction,
|
|
267
|
+
target: target === null ? null : { ...target }
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { Notification } from '@jupyterlab/apputils';
|
|
2
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
4
|
+
import {
|
|
5
|
+
closeIcon,
|
|
6
|
+
HTMLSelect,
|
|
7
|
+
LabIcon,
|
|
8
|
+
ReactWidget
|
|
9
|
+
} from '@jupyterlab/ui-components';
|
|
10
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
11
|
+
import type { Message } from '@lumino/messaging';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
import type { IAgent } from '../launcher/agents';
|
|
15
|
+
|
|
16
|
+
import { contextSummary } from './popup';
|
|
17
|
+
import { serverPath } from './prompt';
|
|
18
|
+
import type { IQueuedPrompt, PromptQueue } from './queue';
|
|
19
|
+
import type { ISessionTarget } from './targetPicker';
|
|
20
|
+
import type { AskAgentTarget } from './tokens';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Command the context buttons dispatch; keeps the panel decoupled from the plugin.
|
|
24
|
+
*/
|
|
25
|
+
const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Textarea height that fits `text` without scrolling, within sane bounds:
|
|
29
|
+
* tall enough to look editable, short enough that one long comment cannot
|
|
30
|
+
* crowd out the rest of the queue.
|
|
31
|
+
*/
|
|
32
|
+
function rowsFor(text: string): number {
|
|
33
|
+
return Math.min(8, Math.max(2, text.split('\n').length));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A target as a `<select>` option value: `session:<name>` for a running
|
|
38
|
+
* agent terminal, `new:<agentId>` for a fresh terminal, `''` for a missing
|
|
39
|
+
* target (the placeholder option).
|
|
40
|
+
*/
|
|
41
|
+
function encodeTarget(target: AskAgentTarget | null): string {
|
|
42
|
+
if (target === null) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
return target.kind === 'session'
|
|
46
|
+
? `session:${target.name}`
|
|
47
|
+
: `new:${target.agentId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeTarget(value: string): AskAgentTarget | null {
|
|
51
|
+
if (value.startsWith('session:')) {
|
|
52
|
+
return { kind: 'session', name: value.slice('session:'.length) };
|
|
53
|
+
}
|
|
54
|
+
if (value.startsWith('new:')) {
|
|
55
|
+
return { kind: 'new', agentId: value.slice('new:'.length) };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether `target` can be sent to right now: its session is still running,
|
|
62
|
+
* or its agent still accepts a command-line prompt.
|
|
63
|
+
*/
|
|
64
|
+
function targetIsLive(
|
|
65
|
+
target: AskAgentTarget | null,
|
|
66
|
+
agents: readonly IAgent[],
|
|
67
|
+
targets: readonly ISessionTarget[]
|
|
68
|
+
): boolean {
|
|
69
|
+
if (target === null) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return target.kind === 'session'
|
|
73
|
+
? targets.some(session => session.name === target.name)
|
|
74
|
+
: agents.some(agent => agent.id === target.agentId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function QueuePanelComponent(props: AskAgentQueuePanel.IOptions): JSX.Element {
|
|
78
|
+
const {
|
|
79
|
+
queue,
|
|
80
|
+
commands,
|
|
81
|
+
agents: readAgents,
|
|
82
|
+
targets: readTargets,
|
|
83
|
+
sending: readSending,
|
|
84
|
+
trans,
|
|
85
|
+
onSend,
|
|
86
|
+
onClear
|
|
87
|
+
} = props;
|
|
88
|
+
|
|
89
|
+
// Snapshots for this render; the widget re-renders on every queue, agent
|
|
90
|
+
// registry and terminal change, so these stay current.
|
|
91
|
+
const items = queue.items;
|
|
92
|
+
const agents = readAgents();
|
|
93
|
+
const targets = readTargets();
|
|
94
|
+
const sending = readSending();
|
|
95
|
+
|
|
96
|
+
const complete = items.every(item => item.instruction.trim().length > 0);
|
|
97
|
+
const targeted = items.every(item =>
|
|
98
|
+
targetIsLive(item.target, agents, targets)
|
|
99
|
+
);
|
|
100
|
+
const canSend = items.length > 0 && complete && targeted && !sending;
|
|
101
|
+
|
|
102
|
+
const jumpTo = (item: IQueuedPrompt): void => {
|
|
103
|
+
const { context } = item;
|
|
104
|
+
const path = serverPath(context);
|
|
105
|
+
// The highlighter needs line numbers that index the working file:
|
|
106
|
+
// notebook lines are cell-relative (or raw-JSON offsets), and diff
|
|
107
|
+
// selections may come from another revision — just open the document
|
|
108
|
+
// in those cases.
|
|
109
|
+
const jump =
|
|
110
|
+
context.cell === undefined &&
|
|
111
|
+
context.startLine !== undefined &&
|
|
112
|
+
context.linesInWorkingFile !== false &&
|
|
113
|
+
PathExt.extname(path) !== '.ipynb'
|
|
114
|
+
? commands.execute(HIGHLIGHT_LINES_COMMAND, {
|
|
115
|
+
path,
|
|
116
|
+
line: context.startLine,
|
|
117
|
+
endLine: context.endLine ?? context.startLine
|
|
118
|
+
})
|
|
119
|
+
: commands.execute('docmanager:open', { path });
|
|
120
|
+
jump.catch((error: unknown) => {
|
|
121
|
+
console.error('xtralab: failed to open the queued location', error);
|
|
122
|
+
Notification.error(trans.__('Could not open %1', path), {
|
|
123
|
+
autoClose: 3000
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (items.length === 0) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="jp-xtralab-AskAgentQueue-body">
|
|
131
|
+
<div className="jp-xtralab-AskAgentQueue-blank">
|
|
132
|
+
{trans.__(
|
|
133
|
+
'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.'
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="jp-xtralab-AskAgentQueue-body">
|
|
142
|
+
<ul className="jp-xtralab-AskAgentQueue-list">
|
|
143
|
+
{items.map(item => {
|
|
144
|
+
const summary = contextSummary(item.context, trans);
|
|
145
|
+
const missing = item.instruction.trim().length === 0;
|
|
146
|
+
const target = item.target;
|
|
147
|
+
const live = targetIsLive(target, agents, targets);
|
|
148
|
+
// Say what sending will do for this prompt — paste into a running
|
|
149
|
+
// terminal, or start a fresh one — and badge it with the agent's
|
|
150
|
+
// icon, so a mixed queue can be scanned without opening dropdowns.
|
|
151
|
+
let icon: LabIcon | null = null;
|
|
152
|
+
let targetTitle = trans.__(
|
|
153
|
+
'The destination picked for this prompt is gone — choose another'
|
|
154
|
+
);
|
|
155
|
+
if (live && target !== null) {
|
|
156
|
+
if (target.kind === 'session') {
|
|
157
|
+
icon =
|
|
158
|
+
targets.find(session => session.name === target.name)?.icon ??
|
|
159
|
+
null;
|
|
160
|
+
targetTitle = trans.__(
|
|
161
|
+
'Sends into the agent running in terminal %1',
|
|
162
|
+
target.name
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
const agent = agents.find(entry => entry.id === target.agentId);
|
|
166
|
+
icon = agent?.icon ?? null;
|
|
167
|
+
targetTitle = trans.__(
|
|
168
|
+
'Starts %1 in a new terminal',
|
|
169
|
+
agent?.label ?? target.agentId
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
<li className="jp-xtralab-AskAgentQueue-item" key={item.id}>
|
|
175
|
+
<div className="jp-xtralab-AskAgentQueue-itemHeader">
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className="jp-xtralab-AskAgentQueue-itemContext"
|
|
179
|
+
title={trans.__(
|
|
180
|
+
'Open %1 in the editor',
|
|
181
|
+
serverPath(item.context)
|
|
182
|
+
)}
|
|
183
|
+
onClick={() => jumpTo(item)}
|
|
184
|
+
>
|
|
185
|
+
{summary}
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
className="jp-xtralab-AskAgentQueue-itemRemove"
|
|
190
|
+
title={trans.__('Remove from the queue')}
|
|
191
|
+
aria-label={trans.__('Remove from the queue')}
|
|
192
|
+
onClick={() => queue.remove(item.id)}
|
|
193
|
+
>
|
|
194
|
+
<closeIcon.react
|
|
195
|
+
tag="span"
|
|
196
|
+
className="jp-xtralab-AskAgentQueue-itemRemoveIcon"
|
|
197
|
+
/>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
{item.context.text.length > 0 && (
|
|
201
|
+
<pre className="jp-xtralab-AskAgentQueue-snippet">
|
|
202
|
+
{item.context.text}
|
|
203
|
+
</pre>
|
|
204
|
+
)}
|
|
205
|
+
{/* Uncontrolled on purpose: the queue re-renders through a
|
|
206
|
+
posted Lumino update, and a controlled value fed back that
|
|
207
|
+
late reverts keystrokes and jumps the caret. The `key` on
|
|
208
|
+
the surrounding <li> still remounts it per queue entry. */}
|
|
209
|
+
<textarea
|
|
210
|
+
className="jp-xtralab-AskAgentQueue-instruction"
|
|
211
|
+
rows={rowsFor(item.instruction)}
|
|
212
|
+
spellCheck={false}
|
|
213
|
+
aria-invalid={missing}
|
|
214
|
+
aria-label={trans.__('Instruction about %1', summary)}
|
|
215
|
+
placeholder={trans.__('Describe the change…')}
|
|
216
|
+
defaultValue={item.instruction}
|
|
217
|
+
onChange={event =>
|
|
218
|
+
queue.updateInstruction(item.id, event.target.value)
|
|
219
|
+
}
|
|
220
|
+
/>
|
|
221
|
+
<div className="jp-xtralab-AskAgentQueue-itemTargetRow">
|
|
222
|
+
<span
|
|
223
|
+
className="jp-xtralab-AskAgentQueue-itemTargetIcon"
|
|
224
|
+
aria-hidden="true"
|
|
225
|
+
>
|
|
226
|
+
{icon !== null && <icon.react tag="span" />}
|
|
227
|
+
</span>
|
|
228
|
+
<HTMLSelect
|
|
229
|
+
className="jp-xtralab-AskAgentQueue-itemTarget"
|
|
230
|
+
aria-invalid={!live}
|
|
231
|
+
aria-label={trans.__('Send to')}
|
|
232
|
+
title={targetTitle}
|
|
233
|
+
value={live ? encodeTarget(target) : ''}
|
|
234
|
+
onChange={event => {
|
|
235
|
+
const picked = decodeTarget(event.target.value);
|
|
236
|
+
if (picked !== null) {
|
|
237
|
+
queue.updateTarget(item.id, picked);
|
|
238
|
+
}
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{!live && (
|
|
242
|
+
<option value="" disabled>
|
|
243
|
+
{trans.__('Choose where to send…')}
|
|
244
|
+
</option>
|
|
245
|
+
)}
|
|
246
|
+
{targets.length > 0 && (
|
|
247
|
+
<optgroup label={trans.__('Running agents')}>
|
|
248
|
+
{targets.map(session => (
|
|
249
|
+
<option
|
|
250
|
+
key={session.name}
|
|
251
|
+
value={`session:${session.name}`}
|
|
252
|
+
>
|
|
253
|
+
{trans.__(
|
|
254
|
+
'Terminal %1 · %2',
|
|
255
|
+
session.name,
|
|
256
|
+
session.label
|
|
257
|
+
)}
|
|
258
|
+
</option>
|
|
259
|
+
))}
|
|
260
|
+
</optgroup>
|
|
261
|
+
)}
|
|
262
|
+
{agents.length > 0 && (
|
|
263
|
+
<optgroup label={trans.__('New terminal')}>
|
|
264
|
+
{agents.map(agent => (
|
|
265
|
+
<option key={agent.id} value={`new:${agent.id}`}>
|
|
266
|
+
{trans.__('New terminal · %1', agent.label)}
|
|
267
|
+
</option>
|
|
268
|
+
))}
|
|
269
|
+
</optgroup>
|
|
270
|
+
)}
|
|
271
|
+
</HTMLSelect>
|
|
272
|
+
</div>
|
|
273
|
+
</li>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
</ul>
|
|
277
|
+
<div className="jp-xtralab-AskAgentQueue-footer">
|
|
278
|
+
{agents.length === 0 && targets.length === 0 && (
|
|
279
|
+
<div className="jp-xtralab-AskAgentQueue-noTargets">
|
|
280
|
+
{trans.__(
|
|
281
|
+
'No agent accepts an initial prompt. Configure agents in the launcher settings.'
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
<div className="jp-xtralab-AskAgentQueue-actions">
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
className="jp-xtralab-AskAgentQueue-clear"
|
|
289
|
+
disabled={sending}
|
|
290
|
+
title={trans.__('Remove every queued prompt')}
|
|
291
|
+
onClick={onClear}
|
|
292
|
+
>
|
|
293
|
+
{trans.__('Clear')}
|
|
294
|
+
</button>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
className="jp-xtralab-AskAgentQueue-send"
|
|
298
|
+
disabled={!canSend}
|
|
299
|
+
title={
|
|
300
|
+
sending
|
|
301
|
+
? trans.__('Sending…')
|
|
302
|
+
: !complete
|
|
303
|
+
? trans.__('Every queued prompt needs an instruction')
|
|
304
|
+
: !targeted
|
|
305
|
+
? trans.__('Every queued prompt needs a destination')
|
|
306
|
+
: trans.__(
|
|
307
|
+
'Send each queued prompt to its chosen destination'
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
onClick={() => {
|
|
311
|
+
if (canSend) {
|
|
312
|
+
onSend();
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{sending
|
|
317
|
+
? trans.__('Sending…')
|
|
318
|
+
: trans._n('Send %1 prompt', 'Send %1 prompts', items.length)}
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The right-sidebar review panel for queued ask-agent prompts.
|
|
328
|
+
*
|
|
329
|
+
* Lists every queued comment — jump-to-code context line, snippet preview,
|
|
330
|
+
* editable instruction, its own destination picker, remove button — plus a
|
|
331
|
+
* send button that flushes the whole queue in one go: prompts sharing a
|
|
332
|
+
* destination are combined into one numbered message, and each destination
|
|
333
|
+
* gets its own delivery. The widget re-renders on queue changes itself; the
|
|
334
|
+
* plugin additionally ties it to the agent registry and terminal signals so
|
|
335
|
+
* the destination choices stay current.
|
|
336
|
+
*/
|
|
337
|
+
export class AskAgentQueuePanel extends ReactWidget {
|
|
338
|
+
constructor(options: AskAgentQueuePanel.IOptions) {
|
|
339
|
+
super();
|
|
340
|
+
this._options = options;
|
|
341
|
+
this.addClass('jp-xtralab-AskAgentQueue');
|
|
342
|
+
options.queue.changed.connect(this._onQueueChanged, this);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
render(): JSX.Element {
|
|
346
|
+
return <QueuePanelComponent {...this._options} />;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Dispose on close (e.g. the close button when the panel has been dragged
|
|
351
|
+
* to the main area) rather than leaving a detached, reusable widget
|
|
352
|
+
* behind. The plugin recreates the panel in the side area on demand; the
|
|
353
|
+
* queue itself lives on in the model.
|
|
354
|
+
*/
|
|
355
|
+
protected onCloseRequest(msg: Message): void {
|
|
356
|
+
super.onCloseRequest(msg);
|
|
357
|
+
this.dispose();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _onQueueChanged(): void {
|
|
361
|
+
this.update();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private _options: AskAgentQueuePanel.IOptions;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* A namespace for `AskAgentQueuePanel` statics.
|
|
369
|
+
*/
|
|
370
|
+
export namespace AskAgentQueuePanel {
|
|
371
|
+
/**
|
|
372
|
+
* Construction options for {@link AskAgentQueuePanel}.
|
|
373
|
+
*/
|
|
374
|
+
export interface IOptions {
|
|
375
|
+
/**
|
|
376
|
+
* The queue the panel lists and edits.
|
|
377
|
+
*/
|
|
378
|
+
queue: PromptQueue;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Command registry used to open queued locations in the editor.
|
|
382
|
+
*/
|
|
383
|
+
commands: CommandRegistry;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Live reader of the prompt-capable agents (called on every render).
|
|
387
|
+
*/
|
|
388
|
+
agents: () => IAgent[];
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Live reader of the running agent terminals (called on every render).
|
|
392
|
+
*/
|
|
393
|
+
targets: () => ISessionTarget[];
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Live reader of whether a batch send is in flight (called on every
|
|
397
|
+
* render). While true the send and clear buttons are disabled, so one
|
|
398
|
+
* flush cannot be double-delivered.
|
|
399
|
+
*/
|
|
400
|
+
sending: () => boolean;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Translation bundle for the panel's own labels.
|
|
404
|
+
*/
|
|
405
|
+
trans: TranslationBundle;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Send every queued prompt to its own destination (the panel only calls
|
|
409
|
+
* this while all instructions and destinations are valid).
|
|
410
|
+
*/
|
|
411
|
+
onSend: () => void;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Clear the queue (the plugin offers an undo toast).
|
|
415
|
+
*/
|
|
416
|
+
onClear: () => void;
|
|
417
|
+
}
|
|
418
|
+
}
|