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