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,240 @@
1
+ import {
2
+ type TranslationBundle,
3
+ nullTranslator
4
+ } from '@jupyterlab/translation';
5
+ import {
6
+ addIcon,
7
+ closeIcon,
8
+ LabIcon,
9
+ ReactWidget,
10
+ stopIcon,
11
+ terminalIcon,
12
+ UseSignal
13
+ } from '@jupyterlab/ui-components';
14
+ import * as React from 'react';
15
+
16
+ import { SessionRegistry } from './model';
17
+
18
+ /**
19
+ * Id of the panel widget. Used for layout restoration and as the handle
20
+ * the sidebar visibility toggle would target.
21
+ */
22
+ export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
23
+
24
+ /**
25
+ * Left-sidebar panel listing every running terminal session. It is the
26
+ * sidebar counterpart of the launcher's terminal tiles: where the
27
+ * launcher *starts* sessions, this panel surfaces the ones already
28
+ * running so the user can jump back to a backgrounded agent — including
29
+ * sessions whose tab has been closed but which are still alive on the
30
+ * server.
31
+ *
32
+ * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
33
+ * whenever the registry emits `stateChanged`, so it tracks
34
+ * `runningChanged` and the agent-title cache without any local state.
35
+ * Click behaviour (activate vs. reopen, shutdown, new terminal) is
36
+ * delegated to plugin-supplied callbacks so the widget never imports
37
+ * `app`.
38
+ */
39
+ export class RunningTerminals extends ReactWidget {
40
+ constructor(options: RunningTerminals.IOptions) {
41
+ super();
42
+ this._registry = options.registry;
43
+ this._trans = options.trans ?? nullTranslator.load('jupyterlab');
44
+ this._iconForCommand = options.iconForCommand;
45
+ this._onActivate = options.onActivate;
46
+ this._onShutdown = options.onShutdown;
47
+ this._onShutdownAll = options.onShutdownAll;
48
+ this._onCreate = options.onCreate;
49
+
50
+ this.id = RUNNING_TERMINALS_ID;
51
+ this.title.icon = terminalIcon;
52
+ this.title.caption = this._trans.__('Running Terminals');
53
+ this.addClass('jp-xtralab-Terminals');
54
+ }
55
+
56
+ dispose(): void {
57
+ if (this.isDisposed) {
58
+ return;
59
+ }
60
+ // The panel owns the registry, so tear down its upstream
61
+ // subscriptions before the React tree goes away.
62
+ this._registry.dispose();
63
+ super.dispose();
64
+ }
65
+
66
+ protected render(): React.ReactElement {
67
+ return (
68
+ <UseSignal signal={this._registry.stateChanged}>
69
+ {() => (
70
+ <RunningTerminalsComponent
71
+ registry={this._registry}
72
+ trans={this._trans}
73
+ iconForCommand={this._iconForCommand}
74
+ onActivate={this._onActivate}
75
+ onShutdown={this._onShutdown}
76
+ onShutdownAll={this._onShutdownAll}
77
+ onCreate={this._onCreate}
78
+ />
79
+ )}
80
+ </UseSignal>
81
+ );
82
+ }
83
+
84
+ private _registry: SessionRegistry;
85
+ private _trans: TranslationBundle;
86
+ private _iconForCommand: (command: string | null) => LabIcon;
87
+ private _onActivate: (sessionName: string) => void;
88
+ private _onShutdown: (sessionName: string) => void;
89
+ private _onShutdownAll: () => void;
90
+ private _onCreate: (anchor: { x: number; y: number }) => void;
91
+ }
92
+
93
+ export namespace RunningTerminals {
94
+ export interface IOptions {
95
+ registry: SessionRegistry;
96
+ trans?: TranslationBundle;
97
+ /**
98
+ * Resolve the running-agent command for a row (from
99
+ * `registry.agentCommandFor`) to the icon to show before its label —
100
+ * the agent's logo, or the plain terminal icon. Supplied by the plugin
101
+ * so the widget never imports the agent list.
102
+ */
103
+ iconForCommand: (command: string | null) => LabIcon;
104
+ /**
105
+ * Activate the named session's open tab, or reopen it in a fresh
106
+ * terminal widget if no tab is currently attached.
107
+ */
108
+ onActivate: (sessionName: string) => void;
109
+ /** Shut the named session down on the server. */
110
+ onShutdown: (sessionName: string) => void;
111
+ /**
112
+ * Shut down every running terminal at once. The plugin is expected to
113
+ * confirm with the user first, since it tears down all live sessions.
114
+ */
115
+ onShutdownAll: () => void;
116
+ /**
117
+ * Activate the "+" button, anchored at the given viewport coordinates
118
+ * (the bottom-left of the button). The plugin decides what to show
119
+ * there — a menu of agents plus a plain terminal, or just a new
120
+ * terminal when no agents are available — so the panel only reports
121
+ * where the button is and never imports the command/menu machinery.
122
+ */
123
+ onCreate: (anchor: { x: number; y: number }) => void;
124
+ }
125
+ }
126
+
127
+ function RunningTerminalsComponent(props: {
128
+ registry: SessionRegistry;
129
+ trans: TranslationBundle;
130
+ iconForCommand: (command: string | null) => LabIcon;
131
+ onActivate: (sessionName: string) => void;
132
+ onShutdown: (sessionName: string) => void;
133
+ onShutdownAll: () => void;
134
+ onCreate: (anchor: { x: number; y: number }) => void;
135
+ }): React.ReactElement {
136
+ const {
137
+ registry,
138
+ trans,
139
+ iconForCommand,
140
+ onActivate,
141
+ onShutdown,
142
+ onShutdownAll,
143
+ onCreate
144
+ } = props;
145
+ const names = registry.sessionNames();
146
+ // The session whose terminal is the current widget in the main area, so its
147
+ // row can be highlighted. `null` when the current tab is a notebook or any
148
+ // other non-terminal widget.
149
+ const currentName = registry.currentSessionName();
150
+
151
+ return (
152
+ <div className="jp-xtralab-Terminals-body">
153
+ <div className="jp-xtralab-Terminals-header">
154
+ <h2 className="jp-xtralab-Terminals-title">{trans.__('Terminals')}</h2>
155
+ <div className="jp-xtralab-Terminals-actions">
156
+ <button
157
+ type="button"
158
+ className="jp-xtralab-Terminals-action jp-xtralab-Terminals-shutdown-all"
159
+ onClick={onShutdownAll}
160
+ disabled={names.length === 0}
161
+ title={trans.__('Shut Down All Terminals')}
162
+ aria-label={trans.__('Shut Down All Terminals')}
163
+ >
164
+ <stopIcon.react tag="span" verticalAlign="middle" />
165
+ </button>
166
+ <button
167
+ type="button"
168
+ className="jp-xtralab-Terminals-action jp-xtralab-Terminals-new"
169
+ onClick={event => {
170
+ // Anchor whatever the plugin shows (usually an agent menu) to
171
+ // the button's bottom-left, in viewport coordinates — what
172
+ // `Menu.open(x, y)` expects.
173
+ const rect = event.currentTarget.getBoundingClientRect();
174
+ onCreate({ x: rect.left, y: rect.bottom });
175
+ }}
176
+ title={trans.__('New Terminal')}
177
+ aria-label={trans.__('New Terminal')}
178
+ aria-haspopup="menu"
179
+ >
180
+ <addIcon.react tag="span" verticalAlign="middle" />
181
+ </button>
182
+ </div>
183
+ </div>
184
+ {names.length === 0 ? (
185
+ <p className="jp-xtralab-Terminals-empty">
186
+ {trans.__('No running terminals.')}
187
+ </p>
188
+ ) : (
189
+ <ul className="jp-xtralab-Terminals-list">
190
+ {names.map(name => {
191
+ const label = registry.labelFor(name);
192
+ const hasWidget = registry.widgetFor(name) !== null;
193
+ const tooltip = hasWidget
194
+ ? trans.__('Activate %1', label)
195
+ : trans.__('Reopen %1', label);
196
+ // The running agent's logo (e.g. Claude), or the plain terminal
197
+ // icon when nothing recognised is running in the session.
198
+ const RowIcon = iconForCommand(
199
+ registry.agentCommandFor(name)
200
+ ).react;
201
+ const isCurrent = name === currentName;
202
+ return (
203
+ <li
204
+ key={name}
205
+ className={
206
+ isCurrent
207
+ ? 'jp-xtralab-Terminals-item jp-mod-current'
208
+ : 'jp-xtralab-Terminals-item'
209
+ }
210
+ >
211
+ <button
212
+ type="button"
213
+ className="jp-xtralab-Terminals-item-activate"
214
+ onClick={() => onActivate(name)}
215
+ title={tooltip}
216
+ aria-label={tooltip}
217
+ aria-current={isCurrent || undefined}
218
+ >
219
+ <RowIcon tag="span" verticalAlign="middle" />
220
+ <span className="jp-xtralab-Terminals-item-label">
221
+ {label}
222
+ </span>
223
+ </button>
224
+ <button
225
+ type="button"
226
+ className="jp-xtralab-Terminals-item-close"
227
+ onClick={() => onShutdown(name)}
228
+ title={trans.__('Shut down %1', label)}
229
+ aria-label={trans.__('Shut down %1', label)}
230
+ >
231
+ <closeIcon.react tag="span" verticalAlign="middle" />
232
+ </button>
233
+ </li>
234
+ );
235
+ })}
236
+ </ul>
237
+ )}
238
+ </div>
239
+ );
240
+ }
@@ -0,0 +1,30 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+
3
+ /**
4
+ * Mirror-symmetric "toggle sidebar" marks for the top-bar buttons, in the
5
+ * same visual language as the macOS / VS Code / svgrepo sidebar icons
6
+ * (https://www.svgrepo.com/vectors/sidebar/): a rounded window outline split
7
+ * by a divider, with the controlled side shaded so left vs. right reads at a
8
+ * glance. The artwork is hand-authored from basic geometry (rounded rect +
9
+ * divider + a shaded panel) so there is no third-party licensing to track,
10
+ * and every stroke/fill uses `currentColor` so the icons inherit the top
11
+ * bar's text color across the light and dark themes.
12
+ */
13
+
14
+ export const leftSidebarIcon = new LabIcon({
15
+ name: 'xtralab:sidebar-left',
16
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
17
+ <path d="M9 3H5.5A2.5 2.5 0 0 0 3 5.5V18.5A2.5 2.5 0 0 0 5.5 21H9Z" fill="currentColor" fill-opacity="0.3"/>
18
+ <rect x="3" y="3" width="18" height="18" rx="2.5" stroke="currentColor" stroke-width="1.7"/>
19
+ <line x1="9" y1="3" x2="9" y2="21" stroke="currentColor" stroke-width="1.7"/>
20
+ </svg>`
21
+ });
22
+
23
+ export const rightSidebarIcon = new LabIcon({
24
+ name: 'xtralab:sidebar-right',
25
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
26
+ <path d="M15 3H18.5A2.5 2.5 0 0 1 21 5.5V18.5A2.5 2.5 0 0 1 18.5 21H15Z" fill="currentColor" fill-opacity="0.3"/>
27
+ <rect x="3" y="3" width="18" height="18" rx="2.5" stroke="currentColor" stroke-width="1.7"/>
28
+ <line x1="15" y1="3" x2="15" y2="21" stroke="currentColor" stroke-width="1.7"/>
29
+ </svg>`
30
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ ILabShell,
3
+ JupyterFrontEnd,
4
+ JupyterFrontEndPlugin
5
+ } from '@jupyterlab/application';
6
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
7
+ import { CommandToolbarButton, LabIcon } from '@jupyterlab/ui-components';
8
+
9
+ import { leftSidebarIcon, rightSidebarIcon } from './icons';
10
+
11
+ const PLUGIN_ID = 'xtralab:top-bar';
12
+
13
+ /**
14
+ * Upstream commands from `@jupyterlab/application-extension` that collapse or
15
+ * expand the side areas. We bind the buttons to these rather than
16
+ * reimplementing the toggle, so behavior (and the `isToggled`/`isEnabled`
17
+ * state the buttons reflect) stays in lockstep with the View menu entries
18
+ * and their keyboard shortcuts.
19
+ */
20
+ const TOGGLE_LEFT_AREA = 'application:toggle-left-area';
21
+ const TOGGLE_RIGHT_AREA = 'application:toggle-right-area';
22
+
23
+ interface IButtonSpec {
24
+ /** Stable widget id (required by `LabShell.add`). */
25
+ id: string;
26
+ /** Command the button triggers and mirrors the state of. */
27
+ command: string;
28
+ icon: LabIcon;
29
+ caption: (trans: ReturnType<ITranslator['load']>) => string;
30
+ /**
31
+ * Rank within the `top` area. The main menu bar is added at rank 100, so a
32
+ * negative rank lands the left button at the leading edge before it; a large
33
+ * rank puts the right button after the menu, where `margin-left: auto`
34
+ * (style/topBar.css) floats it to the far edge. xtralab's shipped config
35
+ * disables the upstream Jupyter logo that otherwise occupies rank 0, so the
36
+ * leading edge is free.
37
+ */
38
+ rank: number;
39
+ /** Side-specific class, used by the stylesheet for placement. */
40
+ sideClass: string;
41
+ }
42
+
43
+ const BUTTONS: IButtonSpec[] = [
44
+ {
45
+ id: 'xtralab-toggle-left-sidebar',
46
+ command: TOGGLE_LEFT_AREA,
47
+ icon: leftSidebarIcon,
48
+ caption: trans => trans.__('Toggle left sidebar'),
49
+ rank: -1,
50
+ sideClass: 'xtralab-TopBarButton-left'
51
+ },
52
+ {
53
+ id: 'xtralab-toggle-right-sidebar',
54
+ command: TOGGLE_RIGHT_AREA,
55
+ icon: rightSidebarIcon,
56
+ caption: trans => trans.__('Toggle right sidebar'),
57
+ rank: 1000,
58
+ sideClass: 'xtralab-TopBarButton-right'
59
+ }
60
+ ];
61
+
62
+ /**
63
+ * Add two icon buttons to the top bar that toggle the left and right side
64
+ * areas, mirroring the macOS-style sidebar buttons: a left-sidebar button at
65
+ * the leading edge and a right-sidebar button at the far edge.
66
+ *
67
+ * Each is a `CommandToolbarButton` wrapping an existing
68
+ * `application:toggle-{left,right}-area` command, so clicking it runs the
69
+ * same toggle as the View menu and the button reflects the command's state —
70
+ * pressed while the matching sidebar is open, and disabled when the area is
71
+ * empty. Note xtralab ships nothing in the right area by default (the
72
+ * property inspector, table of contents and debugger are all disabled), so
73
+ * the right button stays disabled until a widget is moved or added there.
74
+ */
75
+ const plugin: JupyterFrontEndPlugin<void> = {
76
+ id: PLUGIN_ID,
77
+ description: 'Add left and right sidebar toggle buttons to the top bar.',
78
+ autoStart: true,
79
+ requires: [ILabShell],
80
+ optional: [ITranslator],
81
+ activate: (
82
+ app: JupyterFrontEnd,
83
+ labShell: ILabShell,
84
+ translator: ITranslator | null
85
+ ): void => {
86
+ const { commands } = app;
87
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
88
+
89
+ // Defer to `app.restored` so the upstream toggle commands are already in
90
+ // the registry: `CommandToolbarButton` renders nothing until its command
91
+ // exists and only re-renders on `commandChanged` of type `changed` /
92
+ // `many-changed` (not `added`), so mounting earlier could leave a button
93
+ // blank until the next unrelated command change.
94
+ void app.restored.then(() => {
95
+ for (const spec of BUTTONS) {
96
+ const button = new CommandToolbarButton({
97
+ commands,
98
+ id: spec.command,
99
+ icon: spec.icon,
100
+ // Empty label keeps the button icon-only: the override is forwarded
101
+ // verbatim and the component skips the label span when it is falsy.
102
+ label: '',
103
+ caption: spec.caption(trans),
104
+ // Toggle on mousedown without focusing the button. Otherwise
105
+ // ToolbarButtonComponent calls `event.target.focus()` on click,
106
+ // leaving the stealth button showing a focus fill/outline (the
107
+ // stray "border") until you click elsewhere — unwanted chrome for a
108
+ // title-bar control. Keyboard Tab focus still works, so this stays
109
+ // accessible.
110
+ noFocusOnClick: true
111
+ });
112
+ button.id = spec.id;
113
+ button.addClass('xtralab-TopBarButton');
114
+ button.addClass(spec.sideClass);
115
+ labShell.add(button, 'top', { rank: spec.rank });
116
+ }
117
+ });
118
+ }
119
+ };
120
+
121
+ export default plugin;