xtralab 0.5.0 → 0.7.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 (71) hide show
  1. package/README.md +138 -76
  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/imageDiff.js +80 -8
  6. package/lib/index.d.ts +3 -2
  7. package/lib/index.js +11 -6
  8. package/lib/launcher/agents.d.ts +1 -1
  9. package/lib/launcher/agents.js +22 -13
  10. package/lib/launcher/commands.d.ts +32 -6
  11. package/lib/launcher/commands.js +81 -57
  12. package/lib/launcher/dashboard.d.ts +15 -0
  13. package/lib/launcher/dashboard.js +139 -10
  14. package/lib/launcher/editorRegistry.d.ts +43 -0
  15. package/lib/launcher/editorRegistry.js +96 -0
  16. package/lib/launcher/editors.d.ts +78 -0
  17. package/lib/launcher/editors.js +110 -0
  18. package/lib/launcher/icons.d.ts +25 -6
  19. package/lib/launcher/icons.js +116 -19
  20. package/lib/launcher/index.d.ts +2 -22
  21. package/lib/launcher/index.js +48 -25
  22. package/lib/launcher/registry.d.ts +21 -0
  23. package/lib/launcher/registry.js +28 -0
  24. package/lib/launcher/tokens.d.ts +41 -0
  25. package/lib/launcher/tokens.js +17 -0
  26. package/lib/terminals/detection.d.ts +11 -0
  27. package/lib/terminals/detection.js +50 -0
  28. package/lib/terminals/index.d.ts +27 -0
  29. package/lib/terminals/index.js +194 -0
  30. package/lib/terminals/model.d.ts +191 -0
  31. package/lib/terminals/model.js +418 -0
  32. package/lib/terminals/widget.d.ts +72 -0
  33. package/lib/terminals/widget.js +93 -0
  34. package/lib/topBar/icons.d.ts +13 -0
  35. package/lib/topBar/icons.js +27 -0
  36. package/lib/topBar/index.d.ts +16 -0
  37. package/lib/topBar/index.js +98 -0
  38. package/package.json +36 -33
  39. package/schema/launcher.json +56 -1
  40. package/src/agentSessions.ts +94 -0
  41. package/src/fileBrowser/index.ts +5 -1
  42. package/src/git/imageDiff.tsx +127 -26
  43. package/src/index.ts +11 -6
  44. package/src/launcher/agents.ts +23 -14
  45. package/src/launcher/commands.ts +98 -60
  46. package/src/launcher/dashboard.tsx +227 -9
  47. package/src/launcher/editorRegistry.ts +149 -0
  48. package/src/launcher/editors.ts +183 -0
  49. package/src/launcher/icons.ts +120 -20
  50. package/src/launcher/index.ts +62 -27
  51. package/src/launcher/registry.ts +33 -0
  52. package/src/launcher/tokens.ts +51 -0
  53. package/src/terminals/detection.ts +65 -0
  54. package/src/terminals/index.ts +222 -0
  55. package/src/terminals/model.ts +497 -0
  56. package/src/terminals/widget.tsx +240 -0
  57. package/src/topBar/icons.ts +30 -0
  58. package/src/topBar/index.ts +134 -0
  59. package/style/git.css +109 -0
  60. package/style/index.css +2 -1
  61. package/style/index.js +2 -1
  62. package/style/launcher.css +124 -2
  63. package/style/terminals.css +235 -0
  64. package/style/topBar.css +38 -0
  65. package/lib/statusBar/index.d.ts +0 -14
  66. package/lib/statusBar/index.js +0 -89
  67. package/lib/statusBar/widget.d.ts +0 -121
  68. package/lib/statusBar/widget.js +0 -237
  69. package/src/statusBar/index.ts +0 -115
  70. package/src/statusBar/widget.tsx +0 -318
  71. 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,134 @@
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
+ // The upstream toggle-{left,right}-area commands derive `isEnabled` from
90
+ // `!labShell.isEmpty(side)` but don't fire `commandChanged` when widgets
91
+ // move in or out of a side area. `CommandToolbarButton` only re-renders on
92
+ // `commandChanged` for its own id, so the buttons need an explicit notify
93
+ // whenever the shell layout shifts — `layoutModified` covers each side
94
+ // handler's `_updated` (add/remove, expand/collapse), keeping both
95
+ // `isEnabled` and `isToggled` in sync.
96
+ labShell.layoutModified.connect(() => {
97
+ for (const spec of BUTTONS) {
98
+ commands.notifyCommandChanged(spec.command);
99
+ }
100
+ });
101
+
102
+ // Defer to `app.restored` so the upstream toggle commands are already in
103
+ // the registry: `CommandToolbarButton` renders nothing until its command
104
+ // exists and only re-renders on `commandChanged` of type `changed` /
105
+ // `many-changed` (not `added`), so mounting earlier could leave a button
106
+ // blank until the next unrelated command change.
107
+ void app.restored.then(() => {
108
+ for (const spec of BUTTONS) {
109
+ const button = new CommandToolbarButton({
110
+ commands,
111
+ id: spec.command,
112
+ icon: spec.icon,
113
+ // Empty label keeps the button icon-only: the override is forwarded
114
+ // verbatim and the component skips the label span when it is falsy.
115
+ label: '',
116
+ caption: spec.caption(trans),
117
+ // Toggle on mousedown without focusing the button. Otherwise
118
+ // ToolbarButtonComponent calls `event.target.focus()` on click,
119
+ // leaving the stealth button showing a focus fill/outline (the
120
+ // stray "border") until you click elsewhere — unwanted chrome for a
121
+ // title-bar control. Keyboard Tab focus still works, so this stays
122
+ // accessible.
123
+ noFocusOnClick: true
124
+ });
125
+ button.id = spec.id;
126
+ button.addClass('xtralab-TopBarButton');
127
+ button.addClass(spec.sideClass);
128
+ labShell.add(button, 'top', { rank: spec.rank });
129
+ }
130
+ });
131
+ }
132
+ };
133
+
134
+ export default plugin;
package/style/git.css CHANGED
@@ -633,6 +633,7 @@
633
633
  .jp-xtralab-ImageDiff-stack {
634
634
  flex: 1 1 auto;
635
635
  min-height: 0;
636
+ min-width: 0;
636
637
  width: 100%;
637
638
  display: grid;
638
639
  place-items: center;
@@ -649,6 +650,114 @@
649
650
  object-fit: contain;
650
651
  }
651
652
 
653
+ /*
654
+ * Swipe view wraps the stack in a positioning context so the divider
655
+ * can be overlaid without joining the grid. Keeping the divider as a
656
+ * sibling of `.jp-xtralab-ImageDiff-stack` (rather than a child)
657
+ * preserves the stack's existing flex sizing — it stays the only flex
658
+ * item the interactive column needs to shrink, so `max-height: 100%`
659
+ * on the image still keeps big revisions inside the available area.
660
+ *
661
+ * The whole frame is the drag target: pressing anywhere over the image
662
+ * sets the divider to that x position and drag-tracks the pointer. The
663
+ * dedicated bottom slider was removed because its thumb sat on a
664
+ * narrow centered track that drifted out of sync with the wide
665
+ * divider, which felt off. Dragging directly on the image (GitHub /
666
+ * Pierre style) keeps cursor and divider visually aligned.
667
+ */
668
+ .jp-xtralab-ImageDiff-swipeFrame {
669
+ flex: 1 1 auto;
670
+ min-height: 0;
671
+ min-width: 0;
672
+ width: 100%;
673
+ position: relative;
674
+ display: flex;
675
+ user-select: none;
676
+ touch-action: none;
677
+ }
678
+
679
+ /*
680
+ * Swipe divider: a vertical line marking the boundary between the two
681
+ * clipped revisions. The natural seam between two clipped images is
682
+ * barely visible — especially on light content where it disappears into
683
+ * the background — so an explicit divider with high-contrast styling is
684
+ * drawn on top. White core with dark outline + soft shadow keeps it
685
+ * legible against any image, light or dark.
686
+ *
687
+ * The divider and its handles are the only elements that show the
688
+ * `ew-resize` cursor — clicks anywhere over the image still bubble up
689
+ * to the frame's drag handler, but the cursor only signals "drag me"
690
+ * over the actual control affordances.
691
+ */
692
+ .jp-xtralab-ImageDiff-swipeDivider {
693
+ position: absolute;
694
+ top: 0;
695
+ bottom: 0;
696
+ width: 2px;
697
+ margin-left: -1px;
698
+ background-color: #fff;
699
+ box-shadow:
700
+ 0 0 0 1px rgb(0 0 0 / 55%),
701
+ 0 0 6px rgb(0 0 0 / 45%);
702
+ cursor: ew-resize;
703
+ outline: none;
704
+ }
705
+
706
+ .jp-xtralab-ImageDiff-swipeDivider:focus-visible {
707
+ box-shadow:
708
+ 0 0 0 1px var(--jp-brand-color1),
709
+ 0 0 0 3px rgb(33 150 243 / 35%),
710
+ 0 0 6px rgb(0 0 0 / 45%);
711
+ }
712
+
713
+ /*
714
+ * Handle pucks at the top and bottom of the divider. The two arrows
715
+ * inside (left + right chevrons) signal that the line can be dragged
716
+ * sideways — matching GitHub's image diff swipe affordance.
717
+ */
718
+ .jp-xtralab-ImageDiff-swipeDividerHandle {
719
+ position: absolute;
720
+ left: 50%;
721
+ width: 28px;
722
+ height: 28px;
723
+ margin-left: -14px;
724
+ border-radius: 50%;
725
+ background-color: #fff;
726
+ box-shadow:
727
+ 0 0 0 1px rgb(0 0 0 / 55%),
728
+ 0 2px 6px rgb(0 0 0 / 40%);
729
+ display: flex;
730
+ align-items: center;
731
+ justify-content: center;
732
+ gap: 3px;
733
+ cursor: ew-resize;
734
+ }
735
+
736
+ .jp-xtralab-ImageDiff-swipeDividerHandle.jp-mod-top {
737
+ top: -14px;
738
+ }
739
+
740
+ .jp-xtralab-ImageDiff-swipeDividerHandle.jp-mod-bottom {
741
+ bottom: -14px;
742
+ }
743
+
744
+ /* Chevron triangles drawn with borders so no asset is needed. */
745
+ .jp-xtralab-ImageDiff-swipeDividerArrow {
746
+ width: 0;
747
+ height: 0;
748
+ border-style: solid;
749
+ }
750
+
751
+ .jp-xtralab-ImageDiff-swipeDividerArrow.jp-mod-left {
752
+ border-width: 5px 5px 5px 0;
753
+ border-color: transparent #333 transparent transparent;
754
+ }
755
+
756
+ .jp-xtralab-ImageDiff-swipeDividerArrow.jp-mod-right {
757
+ border-width: 5px 0 5px 5px;
758
+ border-color: transparent transparent transparent #333;
759
+ }
760
+
652
761
  .jp-xtralab-ImageDiff-slider {
653
762
  flex: 0 0 auto;
654
763
  width: min(480px, 100%);
package/style/index.css CHANGED
@@ -2,5 +2,6 @@
2
2
  @import url('editorBreadcrumbs.css');
3
3
  @import url('git.css');
4
4
  @import url('launcher.css');
5
- @import url('statusBar.css');
6
5
  @import url('tabs.css');
6
+ @import url('terminals.css');
7
+ @import url('topBar.css');
package/style/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import './base.css';
2
2
  import './git.css';
3
3
  import './launcher.css';
4
- import './statusBar.css';
5
4
  import './tabs.css';
5
+ import './terminals.css';
6
+ import './topBar.css';
@@ -58,7 +58,8 @@
58
58
  * that hosts the title, count badge, and refresh action; the disclosure
59
59
  * triangle is the browser default — it rotates when the section opens.
60
60
  */
61
- .jp-xtralab-Launcher-changes-section {
61
+ .jp-xtralab-Launcher-changes-section,
62
+ .jp-xtralab-Launcher-mcp-section {
62
63
  /* Reset the default `<details>` block so its contents inherit the
63
64
  section's gap-aligned column layout instead of stacking flush. */
64
65
  display: flex;
@@ -70,7 +71,7 @@
70
71
  display: flex;
71
72
  align-items: center;
72
73
  gap: 8px;
73
- list-style-position: outside;
74
+ list-style: none;
74
75
  cursor: pointer;
75
76
  user-select: none;
76
77
  padding: 2px 0;
@@ -86,6 +87,31 @@
86
87
  outline-offset: 1px;
87
88
  }
88
89
 
90
+ /*
91
+ * Disclosure caret. `display: flex` on the `<summary>` drops the browser's
92
+ * native triangle, so draw one (a CSS border triangle in `currentColor`) and
93
+ * rotate it from pointing right to pointing down when the section is open.
94
+ */
95
+ .jp-xtralab-Launcher-section-summary::-webkit-details-marker {
96
+ display: none;
97
+ }
98
+
99
+ .jp-xtralab-Launcher-section-summary::before {
100
+ content: '';
101
+ flex: 0 0 auto;
102
+ width: 0;
103
+ height: 0;
104
+ border-style: solid;
105
+ border-width: 4px 0 4px 6px;
106
+ border-color: transparent transparent transparent currentcolor;
107
+ transition: transform 120ms ease;
108
+ }
109
+
110
+ .jp-xtralab-Launcher-section[open]
111
+ > .jp-xtralab-Launcher-section-summary::before {
112
+ transform: rotate(90deg);
113
+ }
114
+
89
115
  .jp-xtralab-Launcher-section-count {
90
116
  display: inline-flex;
91
117
  align-items: center;
@@ -316,3 +342,99 @@
316
342
  font-weight: 500;
317
343
  text-align: center;
318
344
  }
345
+
346
+ /*
347
+ * MCP registration hint. A collapsed-by-default `<details>` listing the
348
+ * one-line `mcp add` command for each installed agent that supports one,
349
+ * each paired with a Copy button. The command sits in a read-only `<code>`
350
+ * box — the user copies it and runs it in their own terminal — so the row
351
+ * is not itself a button.
352
+ */
353
+
354
+ /*
355
+ * The MCP hint is an optional helper, not a primary section, so its summary
356
+ * is rendered lighter and in sentence case rather than reusing the bold,
357
+ * uppercase treatment of the `START AN AGENT` / `OPEN` headers — otherwise a
358
+ * collapsed disclosure reads as a misplaced top-level section.
359
+ */
360
+ .jp-xtralab-Launcher-mcp-section .jp-xtralab-Launcher-section-title {
361
+ font-weight: 500;
362
+ text-transform: none;
363
+ letter-spacing: normal;
364
+ }
365
+
366
+ .jp-xtralab-Launcher-mcp-list {
367
+ list-style: none;
368
+ margin: 0;
369
+ padding: 0;
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: 6px;
373
+ }
374
+
375
+ .jp-xtralab-Launcher-mcp-item {
376
+ display: flex;
377
+ align-items: center;
378
+ gap: 8px;
379
+ }
380
+
381
+ .jp-xtralab-Launcher-mcp-command {
382
+ flex: 1 1 auto;
383
+ min-width: 0;
384
+ padding: 6px 10px;
385
+ background-color: var(--jp-layout-color2);
386
+ border: 1px solid var(--jp-border-color2);
387
+ border-radius: 4px;
388
+ font-family: var(--jp-code-font-family);
389
+ font-size: var(--jp-code-font-size);
390
+ white-space: pre;
391
+ overflow-x: auto;
392
+ }
393
+
394
+ /*
395
+ * Copy buttons are sized to the longest label ("Copied!") and centered, so
396
+ * swapping "Copy" ↔ "Copied!" never resizes the button or ragged-aligns the
397
+ * column.
398
+ */
399
+ .jp-xtralab-Launcher-mcp-copy {
400
+ flex: 0 0 auto;
401
+ min-width: 4.5rem;
402
+ padding: 4px 12px;
403
+ text-align: center;
404
+ background-color: var(--jp-layout-color1);
405
+ color: var(--jp-ui-font-color1);
406
+ border: 1px solid var(--jp-border-color2);
407
+ border-radius: 4px;
408
+ cursor: pointer;
409
+ font: inherit;
410
+ font-size: var(--jp-ui-font-size0);
411
+ }
412
+
413
+ .jp-xtralab-Launcher-mcp-copy:hover {
414
+ background-color: var(--jp-layout-color2);
415
+ border-color: var(--jp-border-color1);
416
+ }
417
+
418
+ .jp-xtralab-Launcher-mcp-copy:focus-visible {
419
+ outline: 1px solid var(--jp-brand-color1);
420
+ outline-offset: 1px;
421
+ }
422
+
423
+ /* Confirmation state after a successful copy — a clear color shift, paired
424
+ with the button's "Copied!" label, so the action is unmistakable. */
425
+ .jp-xtralab-Launcher-mcp-copy.jp-mod-copied {
426
+ color: var(--jp-success-color1, var(--jp-ui-font-color1));
427
+ border-color: var(--jp-success-color1, var(--jp-border-color1));
428
+ }
429
+
430
+ .jp-xtralab-Launcher-mcp-note {
431
+ margin: 4px 0 0;
432
+ color: var(--jp-ui-font-color3);
433
+ font-size: var(--jp-ui-font-size0);
434
+ line-height: 1.4;
435
+ }
436
+
437
+ .jp-xtralab-Launcher-mcp-note code {
438
+ font-family: var(--jp-code-font-family);
439
+ font-size: var(--jp-code-font-size);
440
+ }