xtralab 0.4.0 → 0.6.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 (96) hide show
  1. package/README.md +110 -73
  2. package/lib/agentSessions.d.ts +50 -0
  3. package/lib/agentSessions.js +45 -0
  4. package/lib/fileBrowser/index.js +5 -1
  5. package/lib/git/commands.d.ts +12 -13
  6. package/lib/git/commands.js +7 -28
  7. package/lib/git/diffProvider.d.ts +8 -0
  8. package/lib/git/diffProvider.js +314 -0
  9. package/lib/git/diffSurface.d.ts +115 -0
  10. package/lib/git/diffSurface.js +342 -0
  11. package/lib/git/diffWidget.d.ts +8 -13
  12. package/lib/git/diffWidget.js +65 -311
  13. package/lib/git/imageDiff.d.ts +23 -0
  14. package/lib/git/imageDiff.js +256 -0
  15. package/lib/git/index.d.ts +10 -8
  16. package/lib/git/index.js +37 -52
  17. package/lib/index.d.ts +4 -2
  18. package/lib/index.js +14 -8
  19. package/lib/launcher/agents.d.ts +1 -1
  20. package/lib/launcher/agents.js +22 -13
  21. package/lib/launcher/commands.d.ts +32 -6
  22. package/lib/launcher/commands.js +81 -57
  23. package/lib/launcher/dashboard.d.ts +15 -0
  24. package/lib/launcher/dashboard.js +119 -5
  25. package/lib/launcher/editorRegistry.d.ts +43 -0
  26. package/lib/launcher/editorRegistry.js +96 -0
  27. package/lib/launcher/editors.d.ts +78 -0
  28. package/lib/launcher/editors.js +110 -0
  29. package/lib/launcher/icons.d.ts +25 -6
  30. package/lib/launcher/icons.js +116 -19
  31. package/lib/launcher/index.d.ts +2 -22
  32. package/lib/launcher/index.js +48 -25
  33. package/lib/launcher/registry.d.ts +21 -0
  34. package/lib/launcher/registry.js +28 -0
  35. package/lib/launcher/tokens.d.ts +41 -0
  36. package/lib/launcher/tokens.js +17 -0
  37. package/lib/terminals/detection.d.ts +11 -0
  38. package/lib/terminals/detection.js +50 -0
  39. package/lib/terminals/index.d.ts +27 -0
  40. package/lib/terminals/index.js +194 -0
  41. package/lib/terminals/model.d.ts +191 -0
  42. package/lib/terminals/model.js +418 -0
  43. package/lib/terminals/widget.d.ts +72 -0
  44. package/lib/terminals/widget.js +93 -0
  45. package/lib/topBar/icons.d.ts +13 -0
  46. package/lib/topBar/icons.js +27 -0
  47. package/lib/topBar/index.d.ts +16 -0
  48. package/lib/topBar/index.js +86 -0
  49. package/package.json +52 -35
  50. package/schema/launcher.json +56 -1
  51. package/src/agentSessions.ts +94 -0
  52. package/src/fileBrowser/index.ts +5 -1
  53. package/src/git/commands.ts +12 -38
  54. package/src/git/diffProvider.tsx +462 -0
  55. package/src/git/diffSurface.tsx +588 -0
  56. package/src/git/diffWidget.tsx +105 -465
  57. package/src/git/imageDiff.tsx +469 -0
  58. package/src/git/index.ts +38 -68
  59. package/src/index.ts +14 -8
  60. package/src/launcher/agents.ts +23 -14
  61. package/src/launcher/commands.ts +98 -60
  62. package/src/launcher/dashboard.tsx +205 -3
  63. package/src/launcher/editorRegistry.ts +149 -0
  64. package/src/launcher/editors.ts +183 -0
  65. package/src/launcher/icons.ts +120 -20
  66. package/src/launcher/index.ts +62 -27
  67. package/src/launcher/registry.ts +33 -0
  68. package/src/launcher/tokens.ts +51 -0
  69. package/src/terminals/detection.ts +65 -0
  70. package/src/terminals/index.ts +222 -0
  71. package/src/terminals/model.ts +497 -0
  72. package/src/terminals/widget.tsx +240 -0
  73. package/src/topBar/icons.ts +30 -0
  74. package/src/topBar/index.ts +121 -0
  75. package/style/git.css +267 -266
  76. package/style/index.css +2 -1
  77. package/style/index.js +2 -1
  78. package/style/launcher.css +124 -2
  79. package/style/terminals.css +235 -0
  80. package/style/topBar.css +38 -0
  81. package/lib/git/icons.d.ts +0 -14
  82. package/lib/git/icons.js +0 -19
  83. package/lib/git/panel.d.ts +0 -29
  84. package/lib/git/panel.js +0 -42
  85. package/lib/git/panelComponent.d.ts +0 -28
  86. package/lib/git/panelComponent.js +0 -320
  87. package/lib/statusBar/index.d.ts +0 -14
  88. package/lib/statusBar/index.js +0 -89
  89. package/lib/statusBar/widget.d.ts +0 -121
  90. package/lib/statusBar/widget.js +0 -237
  91. package/src/git/icons.ts +0 -20
  92. package/src/git/panel.tsx +0 -56
  93. package/src/git/panelComponent.tsx +0 -575
  94. package/src/statusBar/index.ts +0 -115
  95. package/src/statusBar/widget.tsx +0 -318
  96. package/style/statusBar.css +0 -95
@@ -0,0 +1,65 @@
1
+ import { URLExt } from '@jupyterlab/coreutils';
2
+ import { ServerConnection } from '@jupyterlab/services';
3
+
4
+ /**
5
+ * Ask the xtralab server which of `commands` is currently running inside each
6
+ * open terminal session. Resolves to a map from session name to the matched
7
+ * agent command, or `null` for a session sitting at its prompt.
8
+ *
9
+ * On any failure (endpoint missing on an older server, network error,
10
+ * malformed response) the whole result is `null` — callers should treat that
11
+ * as "detection unavailable" and fall back to the optimistic launch tags
12
+ * rather than clearing any badges.
13
+ */
14
+ export async function fetchRunningAgents(
15
+ commands: string[]
16
+ ): Promise<Record<string, string | null> | null> {
17
+ if (commands.length === 0) {
18
+ return {};
19
+ }
20
+
21
+ const settings = ServerConnection.makeSettings();
22
+ const url = URLExt.join(settings.baseUrl, 'xtralab', 'terminals', 'agents');
23
+
24
+ let response: Response;
25
+ try {
26
+ response = await ServerConnection.makeRequest(
27
+ url,
28
+ {
29
+ method: 'POST',
30
+ body: JSON.stringify({ commands })
31
+ },
32
+ settings
33
+ );
34
+ } catch (error) {
35
+ console.warn('xtralab: running-agent detection failed', error);
36
+ return null;
37
+ }
38
+
39
+ if (!response.ok) {
40
+ console.warn(
41
+ `xtralab: detection endpoint returned HTTP ${response.status}`
42
+ );
43
+ return null;
44
+ }
45
+
46
+ let payload: unknown;
47
+ try {
48
+ payload = await response.json();
49
+ } catch (error) {
50
+ console.warn('xtralab: detection response was not JSON', error);
51
+ return null;
52
+ }
53
+
54
+ if (!payload || typeof payload !== 'object') {
55
+ return null;
56
+ }
57
+
58
+ const result: Record<string, string | null> = {};
59
+ for (const [name, command] of Object.entries(
60
+ payload as Record<string, unknown>
61
+ )) {
62
+ result[name] = typeof command === 'string' && command ? command : null;
63
+ }
64
+ return result;
65
+ }
@@ -0,0 +1,222 @@
1
+ import {
2
+ ILayoutRestorer,
3
+ JupyterFrontEnd,
4
+ JupyterFrontEndPlugin
5
+ } from '@jupyterlab/application';
6
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
7
+ import { ITerminalTracker } from '@jupyterlab/terminal';
8
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9
+ import { LabIcon, MenuSvg, terminalIcon } from '@jupyterlab/ui-components';
10
+
11
+ import { IAgentSessions } from '../agentSessions';
12
+ import { IEditorRegistry } from '../launcher/editorRegistry';
13
+ import { agentCommandId, IAgentRegistry } from '../launcher/tokens';
14
+ import { SessionRegistry } from './model';
15
+ import { RunningTerminals } from './widget';
16
+
17
+ const PLUGIN_ID = 'xtralab:terminals';
18
+
19
+ /**
20
+ * Adds a left-sidebar panel that lists every running terminal session.
21
+ *
22
+ * Each row is labelled with the *real* title the running program
23
+ * published — the launcher's agent name, or whatever title an xterm
24
+ * escape sequence set — which the registry caches so it survives the
25
+ * user closing the tab while the session keeps running on the server.
26
+ * JupyterLab's built-in "Running Terminals and Kernels" panel lists the
27
+ * same sessions but only by their `terminals/<n>` name, so this panel
28
+ * resolves and caches the published title to show something meaningful.
29
+ *
30
+ * Clicking a row activates the existing tab if one is open, or reopens
31
+ * the session in a fresh terminal widget (`terminal:open`). The inline
32
+ * `×` button shuts the session down on the server. The header carries a
33
+ * `+` button and a stop button (the latter shuts every session down at
34
+ * once, after a confirmation).
35
+ *
36
+ * The `+` button drops down a menu — built from the launcher's shared
37
+ * `IAgentRegistry` — listing each available agent (Claude, Codex, …)
38
+ * followed by a plain terminal, so starting an agent session is as
39
+ * consistent here as in the launcher and reuses the very same registered
40
+ * commands and icons. When the launcher is disabled or no agents are
41
+ * installed, the button falls back to opening a plain terminal directly.
42
+ */
43
+ const plugin: JupyterFrontEndPlugin<void> = {
44
+ id: PLUGIN_ID,
45
+ description:
46
+ 'A left-sidebar panel listing every running terminal session by its real (program-published) title, with activate/reopen, shutdown and new-terminal actions.',
47
+ autoStart: true,
48
+ requires: [ITerminalTracker],
49
+ optional: [
50
+ ILayoutRestorer,
51
+ ITranslator,
52
+ IAgentRegistry,
53
+ IEditorRegistry,
54
+ IAgentSessions
55
+ ],
56
+ activate: (
57
+ app: JupyterFrontEnd,
58
+ tracker: ITerminalTracker,
59
+ restorer: ILayoutRestorer | null,
60
+ translator: ITranslator | null,
61
+ agentRegistry: IAgentRegistry | null,
62
+ editorRegistry: IEditorRegistry | null,
63
+ agentSessions: IAgentSessions | null
64
+ ): void => {
65
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
66
+
67
+ // Resolve a session's running command to an icon: the matching agent's
68
+ // logo, the matching editor's logo (Neovim/Vim), or the plain terminal
69
+ // icon when nothing recognised is running. Reads `agentRegistry.agents`
70
+ // live so it tracks settings changes. Used by the panel to badge each row.
71
+ const iconForCommand = (command: string | null): LabIcon => {
72
+ if (!command) {
73
+ return terminalIcon;
74
+ }
75
+ const agent = agentRegistry?.agents.find(a => a.command === command);
76
+ if (agent) {
77
+ return agent.icon;
78
+ }
79
+ // A terminal running an editor (Neovim/Vim, or a user-configured one) is
80
+ // badged with its logo, the same as an agent.
81
+ const editor = editorRegistry?.editors.find(e => e.command === command);
82
+ return editor?.icon ?? terminalIcon;
83
+ };
84
+
85
+ const registry = new SessionRegistry({
86
+ serviceManager: app.serviceManager,
87
+ tracker,
88
+ // The shell tells the registry which widget is current, so the panel can
89
+ // highlight the terminal that is the current main-area widget (and
90
+ // nothing while a notebook or other widget is current instead).
91
+ shell: app.shell,
92
+ agentSessions,
93
+ // The commands the server should look for: the current agent and editor
94
+ // lists, each read live from its registry so they track settings changes
95
+ // — a terminal running an agent or an editor (Neovim/Vim, or a configured
96
+ // one) is badged.
97
+ detectCommands: () => [
98
+ ...(agentRegistry?.agents.map(a => a.command) ?? []),
99
+ ...(editorRegistry?.editors.map(e => e.command) ?? [])
100
+ ]
101
+ });
102
+
103
+ // Mirror the agent/editor detected in each open terminal onto its main-area
104
+ // tab icon, so the tab matches its sidebar row. Re-running on every
105
+ // `stateChanged` is safe: `Title.icon` ignores an unchanged assignment, so
106
+ // the writes neither loop nor churn.
107
+ const syncTabIcons = (): void => {
108
+ tracker.forEach(widget => {
109
+ const session = widget.content?.session;
110
+ if (!session) {
111
+ return;
112
+ }
113
+ widget.title.icon = iconForCommand(
114
+ registry.agentCommandFor(session.name)
115
+ );
116
+ });
117
+ };
118
+ registry.stateChanged.connect(syncTabIcons);
119
+ syncTabIcons();
120
+
121
+ const onActivate = (name: string): void => {
122
+ // `terminal:open` is the upstream entry point that handles both
123
+ // sides of this: if a widget is already attached to the named
124
+ // session it activates that tab, otherwise it spins up a fresh
125
+ // widget connected to the live session.
126
+ void app.commands.execute('terminal:open', { name });
127
+ };
128
+
129
+ const onShutdown = (name: string): void => {
130
+ void app.serviceManager.terminals.shutdown(name).catch(reason => {
131
+ console.error(
132
+ `xtralab: failed to shut down terminal ${name}: ${reason}`
133
+ );
134
+ });
135
+ };
136
+
137
+ const onShutdownAll = (): void => {
138
+ // Tearing down every session at once is hard to undo (each may be a
139
+ // running agent), so confirm before calling the manager's bulk
140
+ // shutdown. `runningChanged` then clears the list and the panel
141
+ // re-renders empty.
142
+ void showDialog({
143
+ title: trans.__('Shut Down All Terminals?'),
144
+ body: trans.__(
145
+ 'Are you sure you want to permanently shut down all running terminals?'
146
+ ),
147
+ buttons: [
148
+ Dialog.cancelButton({ label: trans.__('Cancel') }),
149
+ Dialog.warnButton({ label: trans.__('Shut Down All') })
150
+ ]
151
+ })
152
+ .then(result => {
153
+ if (result.button.accept) {
154
+ return app.serviceManager.terminals.shutdownAll();
155
+ }
156
+ })
157
+ .catch(reason => {
158
+ console.error(
159
+ `xtralab: failed to shut down all terminals: ${reason}`
160
+ );
161
+ });
162
+ };
163
+
164
+ // The header "+" button. With the launcher's agent registry available
165
+ // it drops down a menu of the available agents (reusing their registered
166
+ // `xtralab:start-agent:<id>` commands, so the icons and labels match the
167
+ // launcher exactly) followed by a separator and a plain terminal. Built
168
+ // lazily and repopulated on each open so it always reflects the current,
169
+ // settings-driven agent list. Without the registry (launcher disabled)
170
+ // or with no agents installed, there is nothing to choose between, so
171
+ // the button opens a plain terminal directly in one click.
172
+ //
173
+ // `MenuSvg` (not the bare Lumino `Menu`) is what every JupyterLab menu
174
+ // uses: its renderer applies the `menuItem` LabIcon stylesheet, which
175
+ // sizes the agent icons to 16px and centers them vertically — the bare
176
+ // `Menu` renders the raw, oversized, misaligned SVGs.
177
+ let newMenu: MenuSvg | null = null;
178
+ const onCreate = (anchor: { x: number; y: number }): void => {
179
+ const agents = agentRegistry?.agents ?? [];
180
+ if (agents.length === 0) {
181
+ void app.commands.execute('terminal:create-new');
182
+ return;
183
+ }
184
+ if (!newMenu) {
185
+ newMenu = new MenuSvg({ commands: app.commands });
186
+ }
187
+ newMenu.clearItems();
188
+ for (const agent of agents) {
189
+ newMenu.addItem({ command: agentCommandId(agent.id) });
190
+ }
191
+ newMenu.addItem({ type: 'separator' });
192
+ newMenu.addItem({ command: 'terminal:create-new' });
193
+ newMenu.open(anchor.x, anchor.y);
194
+ };
195
+
196
+ const panel = new RunningTerminals({
197
+ registry,
198
+ trans,
199
+ iconForCommand,
200
+ onActivate,
201
+ onShutdown,
202
+ onShutdownAll,
203
+ onCreate
204
+ });
205
+
206
+ // Added to the left sidebar. The shipped `layout` setting in
207
+ // jupyter-config/labconfig gives each sidebar widget a default rank —
208
+ // this Terminals panel ranks first, so running agents sit one click
209
+ // away at the top. That setting assigns rank only and pins no area, so
210
+ // the "Move Widget" context menu can relocate the panel and the layout
211
+ // restorer remembers where the user puts it. The `rank` here is the
212
+ // in-code fallback used when the setting is absent. Sessions restored
213
+ // from a previous lab run land via `runningChanged` once the terminal
214
+ // manager is ready, which the panel's `UseSignal` picks up.
215
+ app.shell.add(panel, 'left', { rank: 1 });
216
+ if (restorer) {
217
+ restorer.add(panel, panel.id);
218
+ }
219
+ }
220
+ };
221
+
222
+ export default plugin;