xtralab 0.5.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.
- package/README.md +110 -78
- package/lib/agentSessions.d.ts +50 -0
- package/lib/agentSessions.js +45 -0
- package/lib/fileBrowser/index.js +5 -1
- package/lib/git/imageDiff.js +80 -8
- package/lib/index.d.ts +3 -2
- package/lib/index.js +11 -6
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +22 -13
- package/lib/launcher/commands.d.ts +32 -6
- package/lib/launcher/commands.js +81 -57
- package/lib/launcher/dashboard.d.ts +15 -0
- package/lib/launcher/dashboard.js +119 -5
- package/lib/launcher/editorRegistry.d.ts +43 -0
- package/lib/launcher/editorRegistry.js +96 -0
- package/lib/launcher/editors.d.ts +78 -0
- package/lib/launcher/editors.js +110 -0
- package/lib/launcher/icons.d.ts +25 -6
- package/lib/launcher/icons.js +116 -19
- package/lib/launcher/index.d.ts +2 -22
- package/lib/launcher/index.js +48 -25
- package/lib/launcher/registry.d.ts +21 -0
- package/lib/launcher/registry.js +28 -0
- package/lib/launcher/tokens.d.ts +41 -0
- package/lib/launcher/tokens.js +17 -0
- package/lib/terminals/detection.d.ts +11 -0
- package/lib/terminals/detection.js +50 -0
- package/lib/terminals/index.d.ts +27 -0
- package/lib/terminals/index.js +194 -0
- package/lib/terminals/model.d.ts +191 -0
- package/lib/terminals/model.js +418 -0
- package/lib/terminals/widget.d.ts +72 -0
- package/lib/terminals/widget.js +93 -0
- package/lib/topBar/icons.d.ts +13 -0
- package/lib/topBar/icons.js +27 -0
- package/lib/topBar/index.d.ts +16 -0
- package/lib/topBar/index.js +86 -0
- package/package.json +36 -33
- package/schema/launcher.json +56 -1
- package/src/agentSessions.ts +94 -0
- package/src/fileBrowser/index.ts +5 -1
- package/src/git/imageDiff.tsx +127 -26
- package/src/index.ts +11 -6
- package/src/launcher/agents.ts +23 -14
- package/src/launcher/commands.ts +98 -60
- package/src/launcher/dashboard.tsx +205 -3
- package/src/launcher/editorRegistry.ts +149 -0
- package/src/launcher/editors.ts +183 -0
- package/src/launcher/icons.ts +120 -20
- package/src/launcher/index.ts +62 -27
- package/src/launcher/registry.ts +33 -0
- package/src/launcher/tokens.ts +51 -0
- package/src/terminals/detection.ts +65 -0
- package/src/terminals/index.ts +222 -0
- package/src/terminals/model.ts +497 -0
- package/src/terminals/widget.tsx +240 -0
- package/src/topBar/icons.ts +30 -0
- package/src/topBar/index.ts +121 -0
- package/style/git.css +109 -0
- package/style/index.css +2 -1
- package/style/index.js +2 -1
- package/style/launcher.css +124 -2
- package/style/terminals.css +235 -0
- package/style/topBar.css +38 -0
- package/lib/statusBar/index.d.ts +0 -14
- package/lib/statusBar/index.js +0 -89
- package/lib/statusBar/widget.d.ts +0 -121
- package/lib/statusBar/widget.js +0 -237
- package/src/statusBar/index.ts +0 -115
- package/src/statusBar/widget.tsx +0 -318
- 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;
|
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
package/style/index.js
CHANGED
package/style/launcher.css
CHANGED
|
@@ -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
|
|
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
|
+
}
|