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
package/lib/showOutput/index.js
CHANGED
|
@@ -3,25 +3,12 @@ import { ICommandPalette } from '@jupyterlab/apputils';
|
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
5
5
|
import { Panel } from '@lumino/widgets';
|
|
6
|
+
import { coerceData } from '../mimeData';
|
|
6
7
|
const PLUGIN_ID = 'xtralab:show-output';
|
|
7
|
-
/** Render arbitrary rich content into a panel, with no notebook and no kernel. */
|
|
8
|
-
const SHOW_COMMAND = 'xtralab:show';
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
* MCP bridge sometimes serialize a spec (e.g. a Vega-Lite object) to a string;
|
|
12
|
-
* for `application/...json` MIME types we accept either and parse the string.
|
|
9
|
+
* Render arbitrary rich content into a panel, with no notebook and no kernel.
|
|
13
10
|
*/
|
|
14
|
-
|
|
15
|
-
if (typeof data === 'string' && /\+?json$/.test(mimeType)) {
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(data);
|
|
18
|
-
}
|
|
19
|
-
catch (_a) {
|
|
20
|
-
return data;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return data;
|
|
24
|
-
}
|
|
11
|
+
const SHOW_COMMAND = 'xtralab:show';
|
|
25
12
|
/**
|
|
26
13
|
* Contribute `xtralab:show`: render a single MIME bundle into a main-area
|
|
27
14
|
* panel using the application's render-mime registry.
|
|
@@ -92,7 +79,9 @@ const plugin = {
|
|
|
92
79
|
: trans.__('Output');
|
|
93
80
|
const id = typeof args['id'] === 'string' ? args['id'] : 'default';
|
|
94
81
|
const area = args['area'] === 'main' ? 'main' : 'right';
|
|
95
|
-
const mode = typeof args['mode'] === 'string'
|
|
82
|
+
const mode = typeof args['mode'] === 'string' && isDocumentMode(args['mode'])
|
|
83
|
+
? args['mode']
|
|
84
|
+
: 'split-right';
|
|
96
85
|
const bundle = { [mimeType]: data };
|
|
97
86
|
const chosen = rendermime.preferredMimeType(bundle, 'any');
|
|
98
87
|
if (!chosen) {
|
|
@@ -125,7 +114,7 @@ const plugin = {
|
|
|
125
114
|
// only split the document area when `area` is "main".
|
|
126
115
|
if (area === 'main') {
|
|
127
116
|
labShell.add(widget, 'main', {
|
|
128
|
-
mode
|
|
117
|
+
mode,
|
|
129
118
|
activate: true
|
|
130
119
|
});
|
|
131
120
|
}
|
|
@@ -151,4 +140,12 @@ const plugin = {
|
|
|
151
140
|
}
|
|
152
141
|
}
|
|
153
142
|
};
|
|
143
|
+
function isDocumentMode(value) {
|
|
144
|
+
return (value === 'split-top' ||
|
|
145
|
+
value === 'split-left' ||
|
|
146
|
+
value === 'split-right' ||
|
|
147
|
+
value === 'split-bottom' ||
|
|
148
|
+
value === 'tab-before' ||
|
|
149
|
+
value === 'tab-after');
|
|
150
|
+
}
|
|
154
151
|
export default plugin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Terminal } from '@jupyterlab/services';
|
|
2
|
+
import type { ITerminalTracker } from '@jupyterlab/terminal';
|
|
3
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
4
|
+
import { ISignal } from '@lumino/signaling';
|
|
5
|
+
import type { SessionRegistry } from './model';
|
|
6
|
+
import type { IAgentTerminalSession, IAgentTerminals } from './tokens';
|
|
7
|
+
/**
|
|
8
|
+
* The {@link IAgentTerminals} implementation: a read-only view over the
|
|
9
|
+
* terminals panel's {@link SessionRegistry} narrowed to sessions with a
|
|
10
|
+
* detection-confirmed coding agent, plus the "send a prompt into one"
|
|
11
|
+
* action. Lives in the `xtralab:terminals` plugin so the ask-agent popup
|
|
12
|
+
* (and any future caller) can target running agents without duplicating the
|
|
13
|
+
* detection plumbing.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AgentTerminals implements IAgentTerminals {
|
|
16
|
+
constructor(options: AgentTerminals.IOptions);
|
|
17
|
+
sessions(): IAgentTerminalSession[];
|
|
18
|
+
get changed(): ISignal<IAgentTerminals, void>;
|
|
19
|
+
sendPrompt(name: string, prompt: string): Promise<void>;
|
|
20
|
+
private _findWidget;
|
|
21
|
+
private _onRegistryStateChanged;
|
|
22
|
+
private _registry;
|
|
23
|
+
private _tracker;
|
|
24
|
+
private _terminals;
|
|
25
|
+
private _detectCommands;
|
|
26
|
+
private _isAgentCommand;
|
|
27
|
+
private _trans;
|
|
28
|
+
private _changed;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A namespace for `AgentTerminals` statics.
|
|
32
|
+
*/
|
|
33
|
+
export declare namespace AgentTerminals {
|
|
34
|
+
/**
|
|
35
|
+
* Construction options for {@link AgentTerminals}.
|
|
36
|
+
*/
|
|
37
|
+
interface IOptions {
|
|
38
|
+
/**
|
|
39
|
+
* The panel's session registry — the source of the session snapshot.
|
|
40
|
+
*/
|
|
41
|
+
registry: SessionRegistry;
|
|
42
|
+
/**
|
|
43
|
+
* Tracker of open terminal widgets, to reuse an open tab's connection.
|
|
44
|
+
*/
|
|
45
|
+
tracker: ITerminalTracker;
|
|
46
|
+
/**
|
|
47
|
+
* The terminal session manager, for validation and ad-hoc connections.
|
|
48
|
+
*/
|
|
49
|
+
terminals: Terminal.IManager;
|
|
50
|
+
/**
|
|
51
|
+
* Names to detect — same list the registry polls with.
|
|
52
|
+
*/
|
|
53
|
+
detectCommands: () => string[];
|
|
54
|
+
/**
|
|
55
|
+
* Whether a detected command belongs to a coding agent (not an editor).
|
|
56
|
+
*/
|
|
57
|
+
isAgentCommand: (command: string) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Translation bundle for the error messages thrown to callers.
|
|
60
|
+
*/
|
|
61
|
+
trans: TranslationBundle;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Signal } from '@lumino/signaling';
|
|
2
|
+
import { fetchRunningAgents } from './detection';
|
|
3
|
+
/**
|
|
4
|
+
* Pause between pasting the prompt and pressing Enter, so the two reach the
|
|
5
|
+
* agent as separate PTY reads — the same shape as a human pasting and then
|
|
6
|
+
* submitting. Sent back to back, the server can coalesce both websocket
|
|
7
|
+
* messages into one read and the TUI would have to split the paste from the
|
|
8
|
+
* trailing Enter inside a single chunk.
|
|
9
|
+
*/
|
|
10
|
+
const PASTE_SUBMIT_DELAY_MS = 150;
|
|
11
|
+
/**
|
|
12
|
+
* Bracketed-paste markers (`CSI 200~` / `CSI 201~`). The prompt is wrapped
|
|
13
|
+
* in these so the agent's TUI treats it as one pasted block — newlines
|
|
14
|
+
* insert into its input box instead of submitting the message at each line
|
|
15
|
+
* break. Every send target is a terminal where detection just confirmed a
|
|
16
|
+
* running coding agent, and the agent TUIs keep bracketed-paste mode on.
|
|
17
|
+
*/
|
|
18
|
+
const PASTE_OPEN = '\x1b[200~';
|
|
19
|
+
const PASTE_CLOSE = '\x1b[201~';
|
|
20
|
+
/**
|
|
21
|
+
* How long to wait for the session's websocket to reach `connected` before
|
|
22
|
+
* failing the send, so a server hiccup surfaces as an error notification
|
|
23
|
+
* rather than a silent hang.
|
|
24
|
+
*/
|
|
25
|
+
const CONNECT_TIMEOUT_MS = 10000;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve once `session` reports `connected`; reject with `timeoutMessage`
|
|
28
|
+
* after {@link CONNECT_TIMEOUT_MS}. Matches how the launcher waits before
|
|
29
|
+
* typing into a fresh terminal — writes sent while the websocket is still
|
|
30
|
+
* connecting disappear silently.
|
|
31
|
+
*/
|
|
32
|
+
function waitUntilConnected(session, timeoutMessage) {
|
|
33
|
+
if (session.connectionStatus === 'connected') {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
window.clearTimeout(timer);
|
|
39
|
+
session.connectionStatusChanged.disconnect(onStatus);
|
|
40
|
+
};
|
|
41
|
+
const onStatus = () => {
|
|
42
|
+
if (session.connectionStatus === 'connected') {
|
|
43
|
+
cleanup();
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const timer = window.setTimeout(() => {
|
|
48
|
+
cleanup();
|
|
49
|
+
reject(new Error(timeoutMessage));
|
|
50
|
+
}, CONNECT_TIMEOUT_MS);
|
|
51
|
+
session.connectionStatusChanged.connect(onStatus);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The {@link IAgentTerminals} implementation: a read-only view over the
|
|
56
|
+
* terminals panel's {@link SessionRegistry} narrowed to sessions with a
|
|
57
|
+
* detection-confirmed coding agent, plus the "send a prompt into one"
|
|
58
|
+
* action. Lives in the `xtralab:terminals` plugin so the ask-agent popup
|
|
59
|
+
* (and any future caller) can target running agents without duplicating the
|
|
60
|
+
* detection plumbing.
|
|
61
|
+
*/
|
|
62
|
+
export class AgentTerminals {
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this._changed = new Signal(this);
|
|
65
|
+
this._registry = options.registry;
|
|
66
|
+
this._tracker = options.tracker;
|
|
67
|
+
this._terminals = options.terminals;
|
|
68
|
+
this._detectCommands = options.detectCommands;
|
|
69
|
+
this._isAgentCommand = options.isAgentCommand;
|
|
70
|
+
this._trans = options.trans;
|
|
71
|
+
this._registry.stateChanged.connect(this._onRegistryStateChanged, this);
|
|
72
|
+
}
|
|
73
|
+
sessions() {
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const name of this._registry.sessionNames()) {
|
|
76
|
+
const command = this._registry.detectedCommandFor(name);
|
|
77
|
+
if (command === null || !this._isAgentCommand(command)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
result.push({
|
|
81
|
+
name,
|
|
82
|
+
command,
|
|
83
|
+
label: this._registry.labelFor(name),
|
|
84
|
+
activity: this._registry.activityFor(name)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
get changed() {
|
|
90
|
+
return this._changed;
|
|
91
|
+
}
|
|
92
|
+
async sendPrompt(name, prompt) {
|
|
93
|
+
// Re-validate against the server rather than trusting the caller's
|
|
94
|
+
// (possibly seconds-old) snapshot: the prompt is prose, and prose pasted
|
|
95
|
+
// into the shell prompt left behind by an exited agent would be
|
|
96
|
+
// *executed* on the Enter below.
|
|
97
|
+
await this._terminals.refreshRunning();
|
|
98
|
+
const alive = Array.from(this._terminals.running()).some(model => model.name === name);
|
|
99
|
+
if (!alive) {
|
|
100
|
+
throw new Error(this._trans.__('The terminal is no longer running.'));
|
|
101
|
+
}
|
|
102
|
+
const detected = await fetchRunningAgents(this._detectCommands());
|
|
103
|
+
if (detected === null) {
|
|
104
|
+
// Detection being unavailable (older server, transient failure) is
|
|
105
|
+
// not proof the agent is gone; refuse the write with a message that
|
|
106
|
+
// invites the retry that would succeed.
|
|
107
|
+
throw new Error(this._trans.__('Could not verify the terminal — try sending again.'));
|
|
108
|
+
}
|
|
109
|
+
const command = detected[name];
|
|
110
|
+
if (typeof command !== 'string' || !this._isAgentCommand(command)) {
|
|
111
|
+
throw new Error(this._trans.__('No agent is running in that terminal anymore.'));
|
|
112
|
+
}
|
|
113
|
+
// Deliver in the background: write straight to the session over its
|
|
114
|
+
// websocket instead of revealing its tab, so focus stays wherever the
|
|
115
|
+
// user is asking from. An open tab's live connection is reused; a
|
|
116
|
+
// closed tab gets a short-lived client connection to the running server
|
|
117
|
+
// session, without creating any widget.
|
|
118
|
+
const widget = this._findWidget(name);
|
|
119
|
+
const session = widget !== null
|
|
120
|
+
? widget.content.session
|
|
121
|
+
: this._terminals.connectTo({ model: { name } });
|
|
122
|
+
try {
|
|
123
|
+
await waitUntilConnected(session, this._trans.__('Timed out connecting to the terminal.'));
|
|
124
|
+
// Wrapped and newline-normalised exactly like xterm's own paste
|
|
125
|
+
// handling. Writing the markers directly (rather than through an open
|
|
126
|
+
// tab's `Terminal.paste`) also sidesteps the one case paste() gets
|
|
127
|
+
// wrong: a freshly reconnected xterm whose replayed scrollback no
|
|
128
|
+
// longer holds the agent's original mode-setting escape, which makes
|
|
129
|
+
// paste() skip the wrapping the application still expects.
|
|
130
|
+
session.send({
|
|
131
|
+
type: 'stdin',
|
|
132
|
+
content: [PASTE_OPEN + prompt.replace(/\r?\n/g, '\r') + PASTE_CLOSE]
|
|
133
|
+
});
|
|
134
|
+
await new Promise(resolve => window.setTimeout(resolve, PASTE_SUBMIT_DELAY_MS));
|
|
135
|
+
if (session.isDisposed) {
|
|
136
|
+
// The tab closed during the pause; the pasted text already reached
|
|
137
|
+
// the agent's input box but Enter can no longer be delivered here.
|
|
138
|
+
throw new Error(this._trans.__('The terminal closed before the prompt was submitted.'));
|
|
139
|
+
}
|
|
140
|
+
session.send({ type: 'stdin', content: ['\r'] });
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
// Close a connection we opened just for this send. Disposing the
|
|
144
|
+
// client connection leaves the server session (and its agent) running.
|
|
145
|
+
if (widget === null && !session.isDisposed) {
|
|
146
|
+
session.dispose();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
_findWidget(name) {
|
|
151
|
+
var _a;
|
|
152
|
+
return ((_a = this._tracker.find(widget => widget.content.session.name === name)) !== null && _a !== void 0 ? _a : null);
|
|
153
|
+
}
|
|
154
|
+
_onRegistryStateChanged() {
|
|
155
|
+
this._changed.emit();
|
|
156
|
+
}
|
|
157
|
+
}
|
package/lib/terminals/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
import { IAgentTerminals } from './tokens';
|
|
2
3
|
/**
|
|
3
4
|
* Adds a left-sidebar panel that lists every running terminal session.
|
|
4
5
|
*
|
|
@@ -26,6 +27,11 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
|
26
27
|
* consistent here as in the launcher and reuses the very same registered
|
|
27
28
|
* commands and icons. When the launcher is disabled or no agents are
|
|
28
29
|
* installed, the button falls back to opening a plain terminal directly.
|
|
30
|
+
*
|
|
31
|
+
* The plugin also provides {@link IAgentTerminals} — the sessions with a
|
|
32
|
+
* detection-confirmed running agent, plus a "paste a prompt into one"
|
|
33
|
+
* action — which the ask-agent popup uses to offer running agents as
|
|
34
|
+
* targets next to "new terminal".
|
|
29
35
|
*/
|
|
30
|
-
declare const plugin: JupyterFrontEndPlugin<
|
|
36
|
+
declare const plugin: JupyterFrontEndPlugin<IAgentTerminals>;
|
|
31
37
|
export default plugin;
|
package/lib/terminals/index.js
CHANGED
|
@@ -7,7 +7,9 @@ import { MenuSvg, terminalIcon } from '@jupyterlab/ui-components';
|
|
|
7
7
|
import { IAgentSessions } from '../agentSessions';
|
|
8
8
|
import { IEditorRegistry } from '../launcher/editorRegistry';
|
|
9
9
|
import { agentCommandId, IAgentRegistry } from '../launcher/tokens';
|
|
10
|
+
import { AgentTerminals } from './agentTerminals';
|
|
10
11
|
import { SessionRegistry } from './model';
|
|
12
|
+
import { IAgentTerminals } from './tokens';
|
|
11
13
|
import { RunningTerminals } from './widget';
|
|
12
14
|
const PLUGIN_ID = 'xtralab:terminals';
|
|
13
15
|
/**
|
|
@@ -37,11 +39,17 @@ const PLUGIN_ID = 'xtralab:terminals';
|
|
|
37
39
|
* consistent here as in the launcher and reuses the very same registered
|
|
38
40
|
* commands and icons. When the launcher is disabled or no agents are
|
|
39
41
|
* installed, the button falls back to opening a plain terminal directly.
|
|
42
|
+
*
|
|
43
|
+
* The plugin also provides {@link IAgentTerminals} — the sessions with a
|
|
44
|
+
* detection-confirmed running agent, plus a "paste a prompt into one"
|
|
45
|
+
* action — which the ask-agent popup uses to offer running agents as
|
|
46
|
+
* targets next to "new terminal".
|
|
40
47
|
*/
|
|
41
48
|
const plugin = {
|
|
42
49
|
id: PLUGIN_ID,
|
|
43
50
|
description: 'A left-sidebar panel listing every running terminal session by its real (program-published) title, with activate/reopen, shutdown and new-terminal actions.',
|
|
44
51
|
autoStart: true,
|
|
52
|
+
provides: IAgentTerminals,
|
|
45
53
|
requires: [ITerminalTracker],
|
|
46
54
|
optional: [
|
|
47
55
|
ILayoutRestorer,
|
|
@@ -74,6 +82,34 @@ const plugin = {
|
|
|
74
82
|
const editor = editorRegistry === null || editorRegistry === void 0 ? void 0 : editorRegistry.editors.find(e => e.command === command || e.id === command);
|
|
75
83
|
return (_a = editor === null || editor === void 0 ? void 0 : editor.icon) !== null && _a !== void 0 ? _a : terminalIcon;
|
|
76
84
|
};
|
|
85
|
+
// The names the server should look for in each terminal's process tree:
|
|
86
|
+
// for every agent and editor, its configured `command` plus its canonical
|
|
87
|
+
// `id`. The id is the real CLI name (e.g. `claude`, `nvim`), so an agent
|
|
88
|
+
// whose command points at an alias — e.g. `ccm` running `claude` — is
|
|
89
|
+
// still recognised by the process it actually spawns. Read live so the
|
|
90
|
+
// list tracks settings changes; deduplicated since command and id usually
|
|
91
|
+
// coincide.
|
|
92
|
+
const detectCommands = () => {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
const names = new Set();
|
|
95
|
+
for (const agent of (_a = agentRegistry === null || agentRegistry === void 0 ? void 0 : agentRegistry.agents) !== null && _a !== void 0 ? _a : []) {
|
|
96
|
+
names.add(agent.command);
|
|
97
|
+
names.add(agent.id);
|
|
98
|
+
}
|
|
99
|
+
for (const editor of (_b = editorRegistry === null || editorRegistry === void 0 ? void 0 : editorRegistry.editors) !== null && _b !== void 0 ? _b : []) {
|
|
100
|
+
names.add(editor.command);
|
|
101
|
+
names.add(editor.id);
|
|
102
|
+
}
|
|
103
|
+
return Array.from(names);
|
|
104
|
+
};
|
|
105
|
+
// Whether a detected command/id belongs to a coding agent rather than an
|
|
106
|
+
// editor. Only agent sessions get a latest-activity line (editor
|
|
107
|
+
// terminals stay badge-only), and only they are offered as prompt
|
|
108
|
+
// targets through `IAgentTerminals`.
|
|
109
|
+
const isAgentCommand = (command) => {
|
|
110
|
+
var _a;
|
|
111
|
+
return (_a = agentRegistry === null || agentRegistry === void 0 ? void 0 : agentRegistry.agents.some(a => a.command === command || a.id === command)) !== null && _a !== void 0 ? _a : false;
|
|
112
|
+
};
|
|
77
113
|
const registry = new SessionRegistry({
|
|
78
114
|
serviceManager: app.serviceManager,
|
|
79
115
|
tracker,
|
|
@@ -82,33 +118,8 @@ const plugin = {
|
|
|
82
118
|
// nothing while a notebook or other widget is current instead).
|
|
83
119
|
shell: app.shell,
|
|
84
120
|
agentSessions,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// `id`. The id is the real CLI name (e.g. `claude`, `nvim`), so an agent
|
|
88
|
-
// whose command points at an alias — e.g. `ccm` running `claude` — is
|
|
89
|
-
// still recognised by the process it actually spawns. Read live so the
|
|
90
|
-
// list tracks settings changes; deduplicated since command and id usually
|
|
91
|
-
// coincide.
|
|
92
|
-
detectCommands: () => {
|
|
93
|
-
var _a, _b;
|
|
94
|
-
const names = new Set();
|
|
95
|
-
for (const agent of (_a = agentRegistry === null || agentRegistry === void 0 ? void 0 : agentRegistry.agents) !== null && _a !== void 0 ? _a : []) {
|
|
96
|
-
names.add(agent.command);
|
|
97
|
-
names.add(agent.id);
|
|
98
|
-
}
|
|
99
|
-
for (const editor of (_b = editorRegistry === null || editorRegistry === void 0 ? void 0 : editorRegistry.editors) !== null && _b !== void 0 ? _b : []) {
|
|
100
|
-
names.add(editor.command);
|
|
101
|
-
names.add(editor.id);
|
|
102
|
-
}
|
|
103
|
-
return Array.from(names);
|
|
104
|
-
},
|
|
105
|
-
// Only coding agents (not editors) get a latest-activity line, so the
|
|
106
|
-
// registry asks this whether a detected command/id is an agent before
|
|
107
|
-
// reading its output buffer — editor terminals stay badge-only.
|
|
108
|
-
isAgentCommand: (command) => {
|
|
109
|
-
var _a;
|
|
110
|
-
return (_a = agentRegistry === null || agentRegistry === void 0 ? void 0 : agentRegistry.agents.some(a => a.command === command || a.id === command)) !== null && _a !== void 0 ? _a : false;
|
|
111
|
-
}
|
|
121
|
+
detectCommands,
|
|
122
|
+
isAgentCommand
|
|
112
123
|
});
|
|
113
124
|
// Mirror the agent/editor detected in each open terminal onto its main-area
|
|
114
125
|
// tab icon, so the tab matches its sidebar row. Re-running on every
|
|
@@ -116,8 +127,7 @@ const plugin = {
|
|
|
116
127
|
// the writes neither loop nor churn.
|
|
117
128
|
const syncTabIcons = () => {
|
|
118
129
|
tracker.forEach(widget => {
|
|
119
|
-
|
|
120
|
-
const session = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.session;
|
|
130
|
+
const session = widget.content.session;
|
|
121
131
|
if (!session) {
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
@@ -234,9 +244,19 @@ const plugin = {
|
|
|
234
244
|
console.error(`xtralab: failed to load ${PLUGIN_ID} settings`, reason);
|
|
235
245
|
});
|
|
236
246
|
}
|
|
247
|
+
return new AgentTerminals({
|
|
248
|
+
registry,
|
|
249
|
+
tracker,
|
|
250
|
+
terminals: app.serviceManager.terminals,
|
|
251
|
+
detectCommands,
|
|
252
|
+
isAgentCommand,
|
|
253
|
+
trans
|
|
254
|
+
});
|
|
237
255
|
}
|
|
238
256
|
};
|
|
239
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Read a boolean setting value, falling back when it is missing or invalid.
|
|
259
|
+
*/
|
|
240
260
|
function boolOption(value, fallback) {
|
|
241
261
|
return typeof value === 'boolean' ? value : fallback;
|
|
242
262
|
}
|
package/lib/terminals/model.d.ts
CHANGED
|
@@ -88,6 +88,16 @@ export declare class SessionRegistry implements IDisposable {
|
|
|
88
88
|
* exits.
|
|
89
89
|
*/
|
|
90
90
|
agentCommandFor(name: string): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* The agent command server-side detection currently reports for the
|
|
93
|
+
* session, or `null` when it has not confirmed one (the session is idle,
|
|
94
|
+
* not yet covered by a poll, or detection is unavailable). Unlike
|
|
95
|
+
* {@link agentCommandFor} this never falls back to the optimistic launch
|
|
96
|
+
* tags: callers about to *write into* a session use it, and text must only
|
|
97
|
+
* ever be sent to a confirmed running agent — pasted into a shell prompt it
|
|
98
|
+
* would be executed.
|
|
99
|
+
*/
|
|
100
|
+
detectedCommandFor(name: string): string | null;
|
|
91
101
|
/**
|
|
92
102
|
* The most recent meaningful line of output from the session's terminal, or
|
|
93
103
|
* `null` when there is nothing to surface — no coding agent is running in the
|
|
@@ -177,8 +187,7 @@ export declare class SessionRegistry implements IDisposable {
|
|
|
177
187
|
* `_initialConnection` fires). Without the filter, briefly
|
|
178
188
|
* reopening a tab while an agent has yet to re-emit its xterm
|
|
179
189
|
* title escape sequence would clobber the cached agent name with
|
|
180
|
-
* `Terminal 1
|
|
181
|
-
* against. Live widget titles still display whatever the widget
|
|
190
|
+
* `Terminal 1`. Live widget titles still display whatever the widget
|
|
182
191
|
* currently holds because `labelFor` consults the widget first;
|
|
183
192
|
* the filter only affects what survives a tab close.
|
|
184
193
|
*/
|
package/lib/terminals/model.js
CHANGED
|
@@ -202,6 +202,19 @@ export class SessionRegistry {
|
|
|
202
202
|
}
|
|
203
203
|
return null;
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The agent command server-side detection currently reports for the
|
|
207
|
+
* session, or `null` when it has not confirmed one (the session is idle,
|
|
208
|
+
* not yet covered by a poll, or detection is unavailable). Unlike
|
|
209
|
+
* {@link agentCommandFor} this never falls back to the optimistic launch
|
|
210
|
+
* tags: callers about to *write into* a session use it, and text must only
|
|
211
|
+
* ever be sent to a confirmed running agent — pasted into a shell prompt it
|
|
212
|
+
* would be executed.
|
|
213
|
+
*/
|
|
214
|
+
detectedCommandFor(name) {
|
|
215
|
+
const detected = this._detected.get(name);
|
|
216
|
+
return typeof detected === 'string' ? detected : null;
|
|
217
|
+
}
|
|
205
218
|
/**
|
|
206
219
|
* The most recent meaningful line of output from the session's terminal, or
|
|
207
220
|
* `null` when there is nothing to surface — no coding agent is running in the
|
|
@@ -484,9 +497,8 @@ export class SessionRegistry {
|
|
|
484
497
|
widget.disposed.disconnect(this._onWidgetDisposed, this);
|
|
485
498
|
}
|
|
486
499
|
_onTitleChanged(title) {
|
|
487
|
-
var _a;
|
|
488
500
|
const owner = title.owner;
|
|
489
|
-
const session =
|
|
501
|
+
const session = owner.content.session;
|
|
490
502
|
if (!session) {
|
|
491
503
|
return;
|
|
492
504
|
}
|
|
@@ -501,8 +513,7 @@ export class SessionRegistry {
|
|
|
501
513
|
* `_initialConnection` fires). Without the filter, briefly
|
|
502
514
|
* reopening a tab while an agent has yet to re-emit its xterm
|
|
503
515
|
* title escape sequence would clobber the cached agent name with
|
|
504
|
-
* `Terminal 1
|
|
505
|
-
* against. Live widget titles still display whatever the widget
|
|
516
|
+
* `Terminal 1`. Live widget titles still display whatever the widget
|
|
506
517
|
* currently holds because `labelFor` consults the widget first;
|
|
507
518
|
* the filter only affects what survives a tab close.
|
|
508
519
|
*/
|
|
@@ -543,9 +554,13 @@ var Private;
|
|
|
543
554
|
return true;
|
|
544
555
|
}
|
|
545
556
|
Private.detectedEqual = detectedEqual;
|
|
546
|
-
/**
|
|
557
|
+
/**
|
|
558
|
+
* Rows above the cursor to scan when looking for the latest output.
|
|
559
|
+
*/
|
|
547
560
|
const ACTIVITY_SCAN_ROWS = 64;
|
|
548
|
-
/**
|
|
561
|
+
/**
|
|
562
|
+
* Clamp for the activity string so one runaway block can't bloat a row.
|
|
563
|
+
*/
|
|
549
564
|
const ACTIVITY_MAX_LENGTH = 160;
|
|
550
565
|
/**
|
|
551
566
|
* Horizontal box-drawing / rule characters, treated as blanks when
|
|
@@ -591,7 +606,9 @@ var Private;
|
|
|
591
606
|
return null;
|
|
592
607
|
}
|
|
593
608
|
Private.readActivity = readActivity;
|
|
594
|
-
/**
|
|
609
|
+
/**
|
|
610
|
+
* Sanitized text of one buffer row.
|
|
611
|
+
*/
|
|
595
612
|
function lineText(buffer, row) {
|
|
596
613
|
var _a, _b;
|
|
597
614
|
return sanitizeActivity((_b = (_a = buffer.getLine(row)) === null || _a === void 0 ? void 0 : _a.translateToString(true)) !== null && _b !== void 0 ? _b : '');
|
|
@@ -620,7 +637,9 @@ var Private;
|
|
|
620
637
|
return result;
|
|
621
638
|
}
|
|
622
639
|
Private.joinBlock = joinBlock;
|
|
623
|
-
/**
|
|
640
|
+
/**
|
|
641
|
+
* Truncate by code point (never splitting an emoji) with an ellipsis.
|
|
642
|
+
*/
|
|
624
643
|
function clampActivity(text) {
|
|
625
644
|
const points = Array.from(text);
|
|
626
645
|
return points.length > ACTIVITY_MAX_LENGTH
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Token } from '@lumino/coreutils';
|
|
2
|
+
import type { ISignal } from '@lumino/signaling';
|
|
3
|
+
/**
|
|
4
|
+
* One running terminal session with a confirmed coding agent inside — an
|
|
5
|
+
* entry of {@link IAgentTerminals.sessions}.
|
|
6
|
+
*/
|
|
7
|
+
export interface IAgentTerminalSession {
|
|
8
|
+
/**
|
|
9
|
+
* The terminal session name (terminado's, e.g. `1`).
|
|
10
|
+
*/
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* The agent identifier server-side detection reported for the session:
|
|
14
|
+
* the configured `command`, or the canonical agent `id` when the command
|
|
15
|
+
* is an alias (e.g. `ccm` spawning `claude`). Matches an `IAgent` by
|
|
16
|
+
* either field.
|
|
17
|
+
*/
|
|
18
|
+
command: string;
|
|
19
|
+
/**
|
|
20
|
+
* Display label — the session's real title (the launcher's agent name, or
|
|
21
|
+
* whatever the running program published via an xterm escape sequence).
|
|
22
|
+
*/
|
|
23
|
+
label: string;
|
|
24
|
+
/**
|
|
25
|
+
* The latest meaningful line of the agent's output, when its tab is open
|
|
26
|
+
* and the activity line is enabled; `null` otherwise.
|
|
27
|
+
*/
|
|
28
|
+
activity: string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Running agent terminals, and a way to send a prompt into one.
|
|
32
|
+
*
|
|
33
|
+
* `sessions` only lists sessions where server-side process detection has
|
|
34
|
+
* *confirmed* a running coding agent (editors and plain shells are excluded,
|
|
35
|
+
* as are optimistic just-launched tags): every listed session is one whose
|
|
36
|
+
* TUI can meaningfully receive pasted prompt text. `sendPrompt` re-validates
|
|
37
|
+
* against the server before writing, since pasting prose into a shell prompt
|
|
38
|
+
* would execute it.
|
|
39
|
+
*/
|
|
40
|
+
export interface IAgentTerminals {
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot of the sessions with a detection-confirmed running agent.
|
|
43
|
+
*/
|
|
44
|
+
sessions(): IAgentTerminalSession[];
|
|
45
|
+
/**
|
|
46
|
+
* Emitted whenever the {@link sessions} snapshot may have changed.
|
|
47
|
+
*/
|
|
48
|
+
readonly changed: ISignal<IAgentTerminals, void>;
|
|
49
|
+
/**
|
|
50
|
+
* Paste `prompt` into the named session's terminal and press Enter.
|
|
51
|
+
*
|
|
52
|
+
* Delivery is deliberately background: the text is written straight to
|
|
53
|
+
* the session's websocket, bracketed-paste-wrapped so a multi-line prompt
|
|
54
|
+
* arrives as one block in the agent's input box, without revealing or
|
|
55
|
+
* focusing the terminal's tab — the user keeps working where they are,
|
|
56
|
+
* and the agent CLIs queue the prompt themselves when they are busy.
|
|
57
|
+
* Rejects when the session is gone or no agent is running in it anymore.
|
|
58
|
+
*/
|
|
59
|
+
sendPrompt(name: string, prompt: string): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* DI token for {@link IAgentTerminals}. Provided by `xtralab:terminals`;
|
|
63
|
+
* consumed — optionally, so each side works without the other — by
|
|
64
|
+
* `xtralab:ask-agent` to offer running agent terminals as prompt targets.
|
|
65
|
+
*/
|
|
66
|
+
export declare const IAgentTerminals: Token<IAgentTerminals>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Token } from '@lumino/coreutils';
|
|
2
|
+
/**
|
|
3
|
+
* DI token for {@link IAgentTerminals}. Provided by `xtralab:terminals`;
|
|
4
|
+
* consumed — optionally, so each side works without the other — by
|
|
5
|
+
* `xtralab:ask-agent` to offer running agent terminals as prompt targets.
|
|
6
|
+
*/
|
|
7
|
+
export const IAgentTerminals = new Token('xtralab:IAgentTerminals', 'The running terminal sessions with a detected coding agent, and a way to send a prompt into one.');
|
|
@@ -2,11 +2,6 @@ import { type TranslationBundle } from '@jupyterlab/translation';
|
|
|
2
2
|
import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { SessionRegistry } from './model';
|
|
5
|
-
/**
|
|
6
|
-
* Id of the panel widget. Used for layout restoration and as the handle
|
|
7
|
-
* the sidebar visibility toggle would target.
|
|
8
|
-
*/
|
|
9
|
-
export declare const RUNNING_TERMINALS_ID = "xtralab-running-terminals";
|
|
10
5
|
/**
|
|
11
6
|
* Left-sidebar panel listing every running terminal session. It is the
|
|
12
7
|
* sidebar counterpart of the launcher's terminal tiles: where the
|
|
@@ -52,7 +47,9 @@ export declare namespace RunningTerminals {
|
|
|
52
47
|
* terminal widget if no tab is currently attached.
|
|
53
48
|
*/
|
|
54
49
|
onActivate: (sessionName: string) => void;
|
|
55
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Shut the named session down on the server.
|
|
52
|
+
*/
|
|
56
53
|
onShutdown: (sessionName: string) => void;
|
|
57
54
|
/**
|
|
58
55
|
* Shut down every running terminal at once. The plugin is expected to
|
package/lib/terminals/widget.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
* Id of the panel widget. Used for layout restoration and as the handle
|
|
6
6
|
* the sidebar visibility toggle would target.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
|
|
9
9
|
/**
|
|
10
10
|
* Left-sidebar panel listing every running terminal session. It is the
|
|
11
11
|
* sidebar counterpart of the launcher's terminal tiles: where the
|