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.
Files changed (162) hide show
  1. package/README.md +37 -229
  2. package/lib/agentSessions.d.ts +6 -2
  3. package/lib/askAgent/editorSelection.d.ts +53 -0
  4. package/lib/askAgent/editorSelection.js +95 -0
  5. package/lib/askAgent/icons.d.ts +7 -0
  6. package/lib/askAgent/icons.js +15 -0
  7. package/lib/askAgent/index.d.ts +32 -0
  8. package/lib/askAgent/index.js +741 -0
  9. package/lib/askAgent/popup.d.ts +83 -0
  10. package/lib/askAgent/popup.js +221 -0
  11. package/lib/askAgent/prompt.d.ts +24 -0
  12. package/lib/askAgent/prompt.js +116 -0
  13. package/lib/askAgent/queue.d.ts +93 -0
  14. package/lib/askAgent/queue.js +207 -0
  15. package/lib/askAgent/queuePanel.d.ts +76 -0
  16. package/lib/askAgent/queuePanel.js +188 -0
  17. package/lib/askAgent/targetPicker.d.ts +58 -0
  18. package/lib/askAgent/targetPicker.js +74 -0
  19. package/lib/askAgent/tokens.d.ts +124 -0
  20. package/lib/askAgent/tokens.js +13 -0
  21. package/lib/commandBar/index.d.ts +12 -8
  22. package/lib/commandBar/index.js +58 -41
  23. package/lib/editorBreadcrumbs/index.js +9 -4
  24. package/lib/editorBreadcrumbs/widget.d.ts +7 -7
  25. package/lib/editorBreadcrumbs/widget.js +6 -8
  26. package/lib/fileBrowser/commands.d.ts +2 -8
  27. package/lib/fileBrowser/commands.js +39 -30
  28. package/lib/fileBrowser/contents.d.ts +8 -3
  29. package/lib/fileBrowser/fileBrowser.d.ts +2 -1
  30. package/lib/fileBrowser/widget.d.ts +5 -9
  31. package/lib/fileBrowser/widget.js +1 -1
  32. package/lib/fileTypeIcons/index.js +5 -2
  33. package/lib/git/api.d.ts +0 -26
  34. package/lib/git/api.js +2 -44
  35. package/lib/git/askRequest.d.ts +23 -0
  36. package/lib/git/askRequest.js +90 -0
  37. package/lib/git/commands.d.ts +14 -2
  38. package/lib/git/commands.js +16 -13
  39. package/lib/git/diffModel.d.ts +1 -9
  40. package/lib/git/diffModel.js +1 -1
  41. package/lib/git/diffProvider.js +8 -3
  42. package/lib/git/diffSurface.d.ts +19 -2
  43. package/lib/git/diffSurface.js +64 -18
  44. package/lib/git/diffTheme.d.ts +0 -4
  45. package/lib/git/diffTheme.js +1 -1
  46. package/lib/git/diffWidget.d.ts +9 -0
  47. package/lib/git/diffWidget.js +38 -10
  48. package/lib/git/imageDiff.d.ts +17 -4
  49. package/lib/git/imageDiff.js +90 -30
  50. package/lib/git/index.js +7 -2
  51. package/lib/git/notebookDiff.d.ts +10 -67
  52. package/lib/git/notebookDiff.js +62 -69
  53. package/lib/git/tokens.d.ts +5 -2
  54. package/lib/highlight/index.js +12 -8
  55. package/lib/index.js +2 -0
  56. package/lib/launcher/agents.d.ts +11 -1
  57. package/lib/launcher/agents.js +26 -8
  58. package/lib/launcher/commands.d.ts +6 -2
  59. package/lib/launcher/dashboard.d.ts +8 -1
  60. package/lib/launcher/dashboard.js +39 -33
  61. package/lib/launcher/editorRegistry.js +5 -3
  62. package/lib/launcher/editors.d.ts +24 -7
  63. package/lib/launcher/editors.js +14 -5
  64. package/lib/launcher/icons.d.ts +0 -37
  65. package/lib/launcher/icons.js +11 -12
  66. package/lib/launcher/index.js +5 -2
  67. package/lib/launcher/invocation.d.ts +0 -15
  68. package/lib/launcher/invocation.js +1 -1
  69. package/lib/launcher/schemaDefaults.d.ts +3 -1
  70. package/lib/launcher/schemaDefaults.js +3 -1
  71. package/lib/mimeData.d.ts +7 -0
  72. package/lib/mimeData.js +16 -0
  73. package/lib/omnibox/index.d.ts +4 -0
  74. package/lib/omnibox/index.js +43 -2
  75. package/lib/omnibox/model.d.ts +43 -14
  76. package/lib/omnibox/model.js +117 -14
  77. package/lib/omnibox/recents.d.ts +65 -0
  78. package/lib/omnibox/recents.js +109 -0
  79. package/lib/omnibox/tokens.d.ts +6 -2
  80. package/lib/omnibox/widget.d.ts +23 -6
  81. package/lib/omnibox/widget.js +26 -7
  82. package/lib/showOutput/index.js +15 -18
  83. package/lib/terminals/agentTerminals.d.ts +63 -0
  84. package/lib/terminals/agentTerminals.js +157 -0
  85. package/lib/terminals/index.d.ts +7 -1
  86. package/lib/terminals/index.js +50 -30
  87. package/lib/terminals/model.d.ts +11 -2
  88. package/lib/terminals/model.js +27 -8
  89. package/lib/terminals/tokens.d.ts +66 -0
  90. package/lib/terminals/tokens.js +7 -0
  91. package/lib/terminals/widget.d.ts +3 -6
  92. package/lib/terminals/widget.js +1 -1
  93. package/lib/topBar/index.js +3 -3
  94. package/lib/walkthrough/index.js +14 -3
  95. package/lib/walkthrough/panel.d.ts +12 -4
  96. package/lib/walkthrough/panel.js +9 -3
  97. package/package.json +49 -51
  98. package/schema/command-bar.json +10 -0
  99. package/schema/launcher.json +4 -4
  100. package/schema/omnibox.json +15 -0
  101. package/src/agentSessions.ts +6 -2
  102. package/src/askAgent/editorSelection.ts +138 -0
  103. package/src/askAgent/icons.ts +16 -0
  104. package/src/askAgent/index.ts +915 -0
  105. package/src/askAgent/popup.tsx +407 -0
  106. package/src/askAgent/prompt.ts +144 -0
  107. package/src/askAgent/queue.ts +269 -0
  108. package/src/askAgent/queuePanel.tsx +418 -0
  109. package/src/askAgent/targetPicker.tsx +209 -0
  110. package/src/askAgent/tokens.ts +139 -0
  111. package/src/commandBar/index.ts +75 -49
  112. package/src/editorBreadcrumbs/index.ts +14 -4
  113. package/src/editorBreadcrumbs/widget.ts +11 -9
  114. package/src/fileBrowser/commands.ts +45 -31
  115. package/src/fileBrowser/contents.ts +7 -3
  116. package/src/fileBrowser/fileBrowser.tsx +1 -1
  117. package/src/fileBrowser/widget.tsx +5 -3
  118. package/src/fileTypeIcons/index.ts +6 -3
  119. package/src/git/api.ts +2 -61
  120. package/src/git/askRequest.ts +113 -0
  121. package/src/git/commands.ts +41 -14
  122. package/src/git/diffModel.ts +1 -1
  123. package/src/git/diffProvider.tsx +11 -3
  124. package/src/git/diffSurface.tsx +114 -22
  125. package/src/git/diffTheme.ts +1 -1
  126. package/src/git/diffWidget.tsx +54 -7
  127. package/src/git/imageDiff.tsx +169 -58
  128. package/src/git/index.ts +10 -2
  129. package/src/git/notebookDiff.tsx +91 -57
  130. package/src/git/tokens.ts +4 -2
  131. package/src/highlight/index.ts +12 -8
  132. package/src/index.ts +2 -0
  133. package/src/launcher/agents.ts +31 -7
  134. package/src/launcher/commands.ts +6 -2
  135. package/src/launcher/dashboard.tsx +86 -38
  136. package/src/launcher/editorRegistry.ts +10 -4
  137. package/src/launcher/editors.ts +34 -9
  138. package/src/launcher/icons.ts +11 -12
  139. package/src/launcher/index.ts +5 -2
  140. package/src/launcher/invocation.ts +1 -1
  141. package/src/launcher/schemaDefaults.ts +3 -1
  142. package/src/mimeData.ts +20 -0
  143. package/src/omnibox/index.ts +48 -1
  144. package/src/omnibox/model.ts +173 -33
  145. package/src/omnibox/recents.ts +155 -0
  146. package/src/omnibox/tokens.ts +6 -2
  147. package/src/omnibox/widget.tsx +54 -13
  148. package/src/showOutput/index.ts +20 -23
  149. package/src/terminals/agentTerminals.ts +238 -0
  150. package/src/terminals/index.ts +54 -30
  151. package/src/terminals/model.ts +28 -7
  152. package/src/terminals/tokens.ts +77 -0
  153. package/src/terminals/widget.tsx +4 -2
  154. package/src/topBar/index.ts +12 -6
  155. package/src/walkthrough/index.ts +19 -4
  156. package/src/walkthrough/panel.ts +15 -5
  157. package/style/askAgent.css +544 -0
  158. package/style/commandBar.css +12 -33
  159. package/style/git.css +72 -31
  160. package/style/index.css +1 -0
  161. package/style/index.js +2 -0
  162. 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
+ }