xtralab 0.11.1 → 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 (159) hide show
  1. package/README.md +37 -231
  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.js +3 -4
  57. package/lib/launcher/commands.d.ts +6 -2
  58. package/lib/launcher/dashboard.d.ts +8 -1
  59. package/lib/launcher/dashboard.js +39 -33
  60. package/lib/launcher/editorRegistry.js +5 -3
  61. package/lib/launcher/editors.d.ts +15 -5
  62. package/lib/launcher/icons.d.ts +0 -37
  63. package/lib/launcher/icons.js +11 -12
  64. package/lib/launcher/index.js +2 -1
  65. package/lib/launcher/invocation.d.ts +0 -15
  66. package/lib/launcher/invocation.js +1 -1
  67. package/lib/launcher/schemaDefaults.d.ts +3 -1
  68. package/lib/launcher/schemaDefaults.js +3 -1
  69. package/lib/mimeData.d.ts +7 -0
  70. package/lib/mimeData.js +16 -0
  71. package/lib/omnibox/index.d.ts +4 -0
  72. package/lib/omnibox/index.js +43 -2
  73. package/lib/omnibox/model.d.ts +43 -14
  74. package/lib/omnibox/model.js +117 -14
  75. package/lib/omnibox/recents.d.ts +65 -0
  76. package/lib/omnibox/recents.js +109 -0
  77. package/lib/omnibox/tokens.d.ts +6 -2
  78. package/lib/omnibox/widget.d.ts +23 -6
  79. package/lib/omnibox/widget.js +26 -7
  80. package/lib/showOutput/index.js +15 -18
  81. package/lib/terminals/agentTerminals.d.ts +63 -0
  82. package/lib/terminals/agentTerminals.js +157 -0
  83. package/lib/terminals/index.d.ts +7 -1
  84. package/lib/terminals/index.js +50 -30
  85. package/lib/terminals/model.d.ts +11 -2
  86. package/lib/terminals/model.js +27 -8
  87. package/lib/terminals/tokens.d.ts +66 -0
  88. package/lib/terminals/tokens.js +7 -0
  89. package/lib/terminals/widget.d.ts +3 -6
  90. package/lib/terminals/widget.js +1 -1
  91. package/lib/topBar/index.js +3 -3
  92. package/lib/walkthrough/index.js +14 -3
  93. package/lib/walkthrough/panel.d.ts +12 -4
  94. package/lib/walkthrough/panel.js +9 -3
  95. package/package.json +49 -51
  96. package/schema/command-bar.json +10 -0
  97. package/schema/omnibox.json +15 -0
  98. package/src/agentSessions.ts +6 -2
  99. package/src/askAgent/editorSelection.ts +138 -0
  100. package/src/askAgent/icons.ts +16 -0
  101. package/src/askAgent/index.ts +915 -0
  102. package/src/askAgent/popup.tsx +407 -0
  103. package/src/askAgent/prompt.ts +144 -0
  104. package/src/askAgent/queue.ts +269 -0
  105. package/src/askAgent/queuePanel.tsx +418 -0
  106. package/src/askAgent/targetPicker.tsx +209 -0
  107. package/src/askAgent/tokens.ts +139 -0
  108. package/src/commandBar/index.ts +75 -49
  109. package/src/editorBreadcrumbs/index.ts +14 -4
  110. package/src/editorBreadcrumbs/widget.ts +11 -9
  111. package/src/fileBrowser/commands.ts +45 -31
  112. package/src/fileBrowser/contents.ts +7 -3
  113. package/src/fileBrowser/fileBrowser.tsx +1 -1
  114. package/src/fileBrowser/widget.tsx +5 -3
  115. package/src/fileTypeIcons/index.ts +6 -3
  116. package/src/git/api.ts +2 -61
  117. package/src/git/askRequest.ts +113 -0
  118. package/src/git/commands.ts +41 -14
  119. package/src/git/diffModel.ts +1 -1
  120. package/src/git/diffProvider.tsx +11 -3
  121. package/src/git/diffSurface.tsx +114 -22
  122. package/src/git/diffTheme.ts +1 -1
  123. package/src/git/diffWidget.tsx +54 -7
  124. package/src/git/imageDiff.tsx +169 -58
  125. package/src/git/index.ts +10 -2
  126. package/src/git/notebookDiff.tsx +91 -57
  127. package/src/git/tokens.ts +4 -2
  128. package/src/highlight/index.ts +12 -8
  129. package/src/index.ts +2 -0
  130. package/src/launcher/agents.ts +3 -4
  131. package/src/launcher/commands.ts +6 -2
  132. package/src/launcher/dashboard.tsx +86 -38
  133. package/src/launcher/editorRegistry.ts +10 -4
  134. package/src/launcher/editors.ts +15 -5
  135. package/src/launcher/icons.ts +11 -12
  136. package/src/launcher/index.ts +2 -1
  137. package/src/launcher/invocation.ts +1 -1
  138. package/src/launcher/schemaDefaults.ts +3 -1
  139. package/src/mimeData.ts +20 -0
  140. package/src/omnibox/index.ts +48 -1
  141. package/src/omnibox/model.ts +173 -33
  142. package/src/omnibox/recents.ts +155 -0
  143. package/src/omnibox/tokens.ts +6 -2
  144. package/src/omnibox/widget.tsx +54 -13
  145. package/src/showOutput/index.ts +20 -23
  146. package/src/terminals/agentTerminals.ts +238 -0
  147. package/src/terminals/index.ts +54 -30
  148. package/src/terminals/model.ts +28 -7
  149. package/src/terminals/tokens.ts +77 -0
  150. package/src/terminals/widget.tsx +4 -2
  151. package/src/topBar/index.ts +12 -6
  152. package/src/walkthrough/index.ts +19 -4
  153. package/src/walkthrough/panel.ts +15 -5
  154. package/style/askAgent.css +544 -0
  155. package/style/commandBar.css +12 -33
  156. package/style/git.css +72 -31
  157. package/style/index.css +1 -0
  158. package/style/index.js +2 -0
  159. package/style/topBar.css +4 -4
@@ -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
+ }
@@ -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<void>;
36
+ declare const plugin: JupyterFrontEndPlugin<IAgentTerminals>;
31
37
  export default plugin;
@@ -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
- // 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
- 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
- var _a;
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
- /** Read a boolean setting value, falling back when it is missing or invalid. */
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
  }
@@ -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`, which is exactly the regression we are guarding
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
  */
@@ -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 = (_a = owner.content) === null || _a === void 0 ? void 0 : _a.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`, which is exactly the regression we are guarding
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
- /** Rows above the cursor to scan when looking for the latest output. */
557
+ /**
558
+ * Rows above the cursor to scan when looking for the latest output.
559
+ */
547
560
  const ACTIVITY_SCAN_ROWS = 64;
548
- /** Clamp for the activity string so one runaway block can't bloat a row. */
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
- /** Sanitized text of one buffer row. */
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
- /** Truncate by code point (never splitting an emoji) with an ellipsis. */
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
- /** Shut the named session down on the server. */
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
@@ -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
- export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
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
@@ -19,7 +19,7 @@ const BUTTONS = [
19
19
  icon: leftSidebarIcon,
20
20
  caption: trans => trans.__('Toggle left sidebar'),
21
21
  rank: -1,
22
- sideClass: 'xtralab-TopBarButton-left'
22
+ sideClass: 'jp-xtralab-TopBarButton-left'
23
23
  },
24
24
  {
25
25
  id: 'xtralab-toggle-right-sidebar',
@@ -27,7 +27,7 @@ const BUTTONS = [
27
27
  icon: rightSidebarIcon,
28
28
  caption: trans => trans.__('Toggle right sidebar'),
29
29
  rank: 1000,
30
- sideClass: 'xtralab-TopBarButton-right'
30
+ sideClass: 'jp-xtralab-TopBarButton-right'
31
31
  }
32
32
  ];
33
33
  /**
@@ -88,7 +88,7 @@ const plugin = {
88
88
  noFocusOnClick: true
89
89
  });
90
90
  button.id = spec.id;
91
- button.addClass('xtralab-TopBarButton');
91
+ button.addClass('jp-xtralab-TopBarButton');
92
92
  button.addClass(spec.sideClass);
93
93
  labShell.add(button, 'top', { rank: spec.rank });
94
94
  }
@@ -3,20 +3,31 @@ import { ICommandPalette } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
5
5
  import { tocIcon } from '@jupyterlab/ui-components';
6
+ import { coerceData } from '../mimeData';
6
7
  import { WalkthroughPanel } from './panel';
7
8
  const PLUGIN_ID = 'xtralab:walkthrough';
8
- /** Append a step to the read-only walkthrough panel. */
9
+ /**
10
+ * Append a step to the read-only walkthrough panel.
11
+ */
9
12
  const WALKTHROUGH_COMMAND = 'xtralab:walkthrough';
10
13
  const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
11
14
  const PANEL_ID = 'xtralab-walkthrough';
12
- /** Pull a `{ mimeType, data }` media object out of a JSON command argument. */
15
+ /**
16
+ * Pull a `{ mimeType, data }` media object out of a JSON command argument.
17
+ */
13
18
  function readMedia(value) {
14
19
  if (value &&
15
20
  typeof value === 'object' &&
16
21
  !Array.isArray(value) &&
17
22
  typeof value.mimeType === 'string') {
18
23
  const media = value;
19
- return { mimeType: media.mimeType, data: media.data };
24
+ if (media.data === undefined) {
25
+ return undefined;
26
+ }
27
+ return {
28
+ mimeType: media.mimeType,
29
+ data: coerceData(media.mimeType, media.data)
30
+ };
20
31
  }
21
32
  return undefined;
22
33
  }