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,98 @@
1
+ import { ILabShell } from '@jupyterlab/application';
2
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
3
+ import { CommandToolbarButton } from '@jupyterlab/ui-components';
4
+ import { leftSidebarIcon, rightSidebarIcon } from './icons';
5
+ const PLUGIN_ID = 'xtralab:top-bar';
6
+ /**
7
+ * Upstream commands from `@jupyterlab/application-extension` that collapse or
8
+ * expand the side areas. We bind the buttons to these rather than
9
+ * reimplementing the toggle, so behavior (and the `isToggled`/`isEnabled`
10
+ * state the buttons reflect) stays in lockstep with the View menu entries
11
+ * and their keyboard shortcuts.
12
+ */
13
+ const TOGGLE_LEFT_AREA = 'application:toggle-left-area';
14
+ const TOGGLE_RIGHT_AREA = 'application:toggle-right-area';
15
+ const BUTTONS = [
16
+ {
17
+ id: 'xtralab-toggle-left-sidebar',
18
+ command: TOGGLE_LEFT_AREA,
19
+ icon: leftSidebarIcon,
20
+ caption: trans => trans.__('Toggle left sidebar'),
21
+ rank: -1,
22
+ sideClass: 'xtralab-TopBarButton-left'
23
+ },
24
+ {
25
+ id: 'xtralab-toggle-right-sidebar',
26
+ command: TOGGLE_RIGHT_AREA,
27
+ icon: rightSidebarIcon,
28
+ caption: trans => trans.__('Toggle right sidebar'),
29
+ rank: 1000,
30
+ sideClass: 'xtralab-TopBarButton-right'
31
+ }
32
+ ];
33
+ /**
34
+ * Add two icon buttons to the top bar that toggle the left and right side
35
+ * areas, mirroring the macOS-style sidebar buttons: a left-sidebar button at
36
+ * the leading edge and a right-sidebar button at the far edge.
37
+ *
38
+ * Each is a `CommandToolbarButton` wrapping an existing
39
+ * `application:toggle-{left,right}-area` command, so clicking it runs the
40
+ * same toggle as the View menu and the button reflects the command's state —
41
+ * pressed while the matching sidebar is open, and disabled when the area is
42
+ * empty. Note xtralab ships nothing in the right area by default (the
43
+ * property inspector, table of contents and debugger are all disabled), so
44
+ * the right button stays disabled until a widget is moved or added there.
45
+ */
46
+ const plugin = {
47
+ id: PLUGIN_ID,
48
+ description: 'Add left and right sidebar toggle buttons to the top bar.',
49
+ autoStart: true,
50
+ requires: [ILabShell],
51
+ optional: [ITranslator],
52
+ activate: (app, labShell, translator) => {
53
+ const { commands } = app;
54
+ const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
55
+ // The upstream toggle-{left,right}-area commands derive `isEnabled` from
56
+ // `!labShell.isEmpty(side)` but don't fire `commandChanged` when widgets
57
+ // move in or out of a side area. `CommandToolbarButton` only re-renders on
58
+ // `commandChanged` for its own id, so the buttons need an explicit notify
59
+ // whenever the shell layout shifts — `layoutModified` covers each side
60
+ // handler's `_updated` (add/remove, expand/collapse), keeping both
61
+ // `isEnabled` and `isToggled` in sync.
62
+ labShell.layoutModified.connect(() => {
63
+ for (const spec of BUTTONS) {
64
+ commands.notifyCommandChanged(spec.command);
65
+ }
66
+ });
67
+ // Defer to `app.restored` so the upstream toggle commands are already in
68
+ // the registry: `CommandToolbarButton` renders nothing until its command
69
+ // exists and only re-renders on `commandChanged` of type `changed` /
70
+ // `many-changed` (not `added`), so mounting earlier could leave a button
71
+ // blank until the next unrelated command change.
72
+ void app.restored.then(() => {
73
+ for (const spec of BUTTONS) {
74
+ const button = new CommandToolbarButton({
75
+ commands,
76
+ id: spec.command,
77
+ icon: spec.icon,
78
+ // Empty label keeps the button icon-only: the override is forwarded
79
+ // verbatim and the component skips the label span when it is falsy.
80
+ label: '',
81
+ caption: spec.caption(trans),
82
+ // Toggle on mousedown without focusing the button. Otherwise
83
+ // ToolbarButtonComponent calls `event.target.focus()` on click,
84
+ // leaving the stealth button showing a focus fill/outline (the
85
+ // stray "border") until you click elsewhere — unwanted chrome for a
86
+ // title-bar control. Keyboard Tab focus still works, so this stays
87
+ // accessible.
88
+ noFocusOnClick: true
89
+ });
90
+ button.id = spec.id;
91
+ button.addClass('xtralab-TopBarButton');
92
+ button.addClass(spec.sideClass);
93
+ labShell.add(button, 'top', { rank: spec.rank });
94
+ }
95
+ });
96
+ }
97
+ };
98
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -30,27 +30,28 @@
30
30
  "type": "git",
31
31
  "url": "https://github.com/jtpio/xtralab.git"
32
32
  },
33
+ "packageManager": "pnpm@10.28.0",
33
34
  "scripts": {
34
- "build": "jlpm build:lib && jlpm build:labextension:dev",
35
- "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
35
+ "build": "pnpm build:lib && pnpm build:labextension:dev",
36
+ "build:prod": "pnpm clean && pnpm build:lib:prod && pnpm build:labextension",
36
37
  "build:labextension": "jupyter-builder build .",
37
38
  "build:labextension:dev": "jupyter-builder build --development True .",
38
39
  "build:lib": "tsc --sourceMap",
39
40
  "build:lib:prod": "tsc",
40
- "clean": "jlpm clean:lib",
41
+ "clean": "pnpm clean:lib",
41
42
  "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
42
43
  "clean:lintcache": "rimraf .eslintcache .stylelintcache",
43
44
  "clean:labextension": "rimraf xtralab/labextension xtralab/_version.py",
44
- "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
45
- "eslint": "jlpm eslint:check --fix",
45
+ "clean:all": "pnpm clean:lib && pnpm clean:labextension && pnpm clean:lintcache",
46
+ "eslint": "pnpm eslint:check --fix",
46
47
  "eslint:check": "eslint . --cache --ext .ts,.tsx",
47
- "install:extension": "jlpm build",
48
- "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
49
- "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
50
- "prettier": "jlpm prettier:base --write --list-different",
48
+ "install:extension": "pnpm build",
49
+ "lint": "pnpm stylelint && pnpm prettier && pnpm eslint",
50
+ "lint:check": "pnpm stylelint:check && pnpm prettier:check && pnpm eslint:check",
51
+ "prettier": "pnpm prettier:base --write --list-different",
51
52
  "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
52
- "prettier:check": "jlpm prettier:base --check",
53
- "stylelint": "jlpm stylelint:check --fix",
53
+ "prettier:check": "pnpm prettier:base --check",
54
+ "stylelint": "pnpm stylelint:check --fix",
54
55
  "stylelint:check": "stylelint --cache \"style/**/*.css\"",
55
56
  "watch": "run-p watch:src watch:labextension",
56
57
  "watch:src": "tsc -w --sourceMap",
@@ -60,25 +61,25 @@
60
61
  "@codemirror/language": "^6.12.1",
61
62
  "@codemirror/state": "^6.5.4",
62
63
  "@codemirror/view": "^6.39.14",
63
- "@jupyterlab/application": "^4.6.0-alpha.5",
64
- "@jupyterlab/apputils": "^4.7.0-alpha.5",
65
- "@jupyterlab/codemirror": "^4.6.0-alpha.5",
66
- "@jupyterlab/coreutils": "^6.6.0-alpha.5",
67
- "@jupyterlab/docmanager": "^4.6.0-alpha.5",
68
- "@jupyterlab/docregistry": "^4.6.0-alpha.5",
69
- "@jupyterlab/git": "^0.54.0-a0",
70
- "@jupyterlab/launcher": "^4.6.0-alpha.5",
71
- "@jupyterlab/mainmenu": "^4.6.0-alpha.5",
72
- "@jupyterlab/nbformat": "^4.6.0-alpha.5",
73
- "@jupyterlab/outputarea": "^4.6.0-alpha.5",
74
- "@jupyterlab/rendermime": "^4.6.0-alpha.5",
75
- "@jupyterlab/services": "^7.6.0-alpha.5",
76
- "@jupyterlab/settingregistry": "^4.6.0-alpha.5",
77
- "@jupyterlab/statusbar": "^4.6.0-alpha.5",
78
- "@jupyterlab/terminal": "^4.6.0-alpha.5",
79
- "@jupyterlab/translation": "^4.6.0-alpha.5",
80
- "@jupyterlab/ui-components": "^4.6.0-alpha.5",
64
+ "@jupyterlab/application": "^4.6.0-beta.0",
65
+ "@jupyterlab/apputils": "^4.7.0-beta.0",
66
+ "@jupyterlab/codemirror": "^4.6.0-beta.0",
67
+ "@jupyterlab/coreutils": "^6.6.0-beta.0",
68
+ "@jupyterlab/docmanager": "^4.6.0-beta.0",
69
+ "@jupyterlab/docregistry": "^4.6.0-beta.0",
70
+ "@jupyterlab/git": "^0.54.0-a1",
71
+ "@jupyterlab/launcher": "^4.6.0-beta.0",
72
+ "@jupyterlab/mainmenu": "^4.6.0-beta.0",
73
+ "@jupyterlab/nbformat": "^4.6.0-beta.0",
74
+ "@jupyterlab/outputarea": "^4.6.0-beta.0",
75
+ "@jupyterlab/rendermime": "^4.6.0-beta.0",
76
+ "@jupyterlab/services": "^7.6.0-beta.0",
77
+ "@jupyterlab/settingregistry": "^4.6.0-beta.0",
78
+ "@jupyterlab/terminal": "^4.6.0-beta.0",
79
+ "@jupyterlab/translation": "^4.6.0-beta.0",
80
+ "@jupyterlab/ui-components": "^4.6.0-beta.0",
81
81
  "@lumino/algorithm": "^2.0.0",
82
+ "@lumino/commands": "^2.0.0",
82
83
  "@lumino/coreutils": "^2.0.0",
83
84
  "@lumino/disposable": "^2.0.0",
84
85
  "@lumino/dragdrop": "^2.0.0",
@@ -94,7 +95,7 @@
94
95
  "react-dom": "^18.2.0"
95
96
  },
96
97
  "devDependencies": {
97
- "@jupyter/builder": "^0.0.8",
98
+ "@jupyter/builder": "^1.0.0-beta.1",
98
99
  "@types/json-schema": "^7.0.11",
99
100
  "@types/react": "^18.0.26",
100
101
  "@types/react-addons-linked-state-mixin": "^0.14.22",
@@ -117,8 +118,10 @@
117
118
  "typescript": "~5.5.4",
118
119
  "yjs": "^13.5.0"
119
120
  },
120
- "resolutions": {
121
- "lib0": "0.2.111"
121
+ "pnpm": {
122
+ "ignoredBuiltDependencies": [
123
+ "@fortawesome/fontawesome-free"
124
+ ]
122
125
  },
123
126
  "sideEffects": [
124
127
  "style/*.css",
@@ -7,7 +7,7 @@
7
7
  "properties": {
8
8
  "agents": {
9
9
  "title": "Agents",
10
- "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, gemini, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Set `requireAvailable: false` for shell aliases that should always show even though they aren't on PATH.",
10
+ "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Set `requireAvailable: false` for shell aliases that should always show even though they aren't on PATH.",
11
11
  "type": "array",
12
12
  "default": [],
13
13
  "items": {
@@ -65,6 +65,61 @@
65
65
  }
66
66
  }
67
67
  }
68
+ },
69
+ "editors": {
70
+ "title": "Editors",
71
+ "description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Set `requireAvailable: false` for a shell alias that should show even though it isn't on PATH.",
72
+ "type": "array",
73
+ "default": [],
74
+ "items": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["id"],
78
+ "properties": {
79
+ "id": {
80
+ "title": "ID",
81
+ "description": "Stable identifier. Match a built-in (nvim, vim) to override, or pick a new id to define a new editor.",
82
+ "type": "string"
83
+ },
84
+ "label": {
85
+ "title": "Label",
86
+ "description": "Tile title shown on the launcher.",
87
+ "type": "string"
88
+ },
89
+ "caption": {
90
+ "title": "Caption",
91
+ "description": "Tooltip shown for the tile.",
92
+ "type": "string"
93
+ },
94
+ "command": {
95
+ "title": "Command",
96
+ "description": "Literal string typed into the new terminal (e.g. `nvim`). Aliases work because shells expand them on interactive lines.",
97
+ "type": "string"
98
+ },
99
+ "iconSvg": {
100
+ "title": "Icon SVG",
101
+ "description": "Inline SVG for the tile icon. Required only for new editors whose icon is not already shipped by xtralab.",
102
+ "type": "string"
103
+ },
104
+ "rank": {
105
+ "title": "Rank",
106
+ "description": "Preference order (lower numbers come first). The launcher shows the first available editor by rank.",
107
+ "type": "number"
108
+ },
109
+ "enabled": {
110
+ "title": "Enabled",
111
+ "description": "When false, hides the editor from the launcher (and the Terminals panel stops badging it).",
112
+ "type": "boolean",
113
+ "default": true
114
+ },
115
+ "requireAvailable": {
116
+ "title": "Require available",
117
+ "description": "When false, the launcher skips the availability check for this entry (useful for a shell alias that isn't on PATH but resolves in an interactive shell).",
118
+ "type": "boolean",
119
+ "default": true
120
+ }
121
+ }
122
+ }
68
123
  }
69
124
  }
70
125
  }
@@ -0,0 +1,94 @@
1
+ import type { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { Token } from '@lumino/coreutils';
3
+ import { ISignal, Signal } from '@lumino/signaling';
4
+
5
+ /**
6
+ * A shared map from a terminal session name to the agent command it was
7
+ * *launched* with (e.g. `claude`).
8
+ *
9
+ * This is the optimistic half of the running-agent badge: when the launcher
10
+ * (or the terminals panel's `+` menu) starts an agent via a
11
+ * `xtralab:start-agent:<id>` command, it records the session here so the
12
+ * panel can show the agent's logo immediately, before the server-side
13
+ * process detection has had a chance to confirm it. Server detection is the
14
+ * authoritative source once it reports on a session; this tag only fills the
15
+ * gap right after launch (and serves as a fallback if detection is
16
+ * unavailable).
17
+ *
18
+ * It exists as its own token, provided by a tiny dependency-free plugin,
19
+ * specifically so the launcher (the writer) and the terminals panel (the
20
+ * reader) can share it without depending on each other — a direct
21
+ * launcher↔terminals token pair would form an activation cycle.
22
+ */
23
+ export interface IAgentSessions {
24
+ /**
25
+ * The agent command a session was launched with, or `null` if we never
26
+ * launched an agent into it (e.g. a plain terminal, or one started before
27
+ * this ran).
28
+ */
29
+ get(sessionName: string): string | null;
30
+
31
+ /** Record that a session was launched as the given agent command. */
32
+ set(sessionName: string, command: string): void;
33
+
34
+ /** Forget a session — called when it shuts down so the map stays bounded. */
35
+ delete(sessionName: string): void;
36
+
37
+ /**
38
+ * Emitted with the affected session name whenever a record is added or
39
+ * removed, so readers can re-render.
40
+ */
41
+ readonly changed: ISignal<IAgentSessions, string>;
42
+ }
43
+
44
+ /**
45
+ * DI token for {@link IAgentSessions}. Provided by `xtralab:agent-sessions`;
46
+ * consumed (optionally) by both `xtralab:launcher` and `xtralab:terminals`.
47
+ */
48
+ export const IAgentSessions = new Token<IAgentSessions>(
49
+ 'xtralab:IAgentSessions',
50
+ 'A shared map from terminal session name to the agent command it was launched with.'
51
+ );
52
+
53
+ class AgentSessions implements IAgentSessions {
54
+ get(sessionName: string): string | null {
55
+ return this._byName.get(sessionName) ?? null;
56
+ }
57
+
58
+ set(sessionName: string, command: string): void {
59
+ if (this._byName.get(sessionName) === command) {
60
+ return;
61
+ }
62
+ this._byName.set(sessionName, command);
63
+ this._changed.emit(sessionName);
64
+ }
65
+
66
+ delete(sessionName: string): void {
67
+ if (this._byName.delete(sessionName)) {
68
+ this._changed.emit(sessionName);
69
+ }
70
+ }
71
+
72
+ get changed(): ISignal<IAgentSessions, string> {
73
+ return this._changed;
74
+ }
75
+
76
+ private _byName = new Map<string, string>();
77
+ private _changed = new Signal<IAgentSessions, string>(this);
78
+ }
79
+
80
+ /**
81
+ * Provides {@link IAgentSessions}. Deliberately has no dependencies so it can
82
+ * sit underneath both the launcher and the terminals panel without creating
83
+ * a dependency cycle between them.
84
+ */
85
+ const plugin: JupyterFrontEndPlugin<IAgentSessions> = {
86
+ id: 'xtralab:agent-sessions',
87
+ description:
88
+ 'Shared map from terminal session to the agent command it was launched with, used to badge terminal rows with the running agent.',
89
+ autoStart: true,
90
+ provides: IAgentSessions,
91
+ activate: (): IAgentSessions => new AgentSessions()
92
+ };
93
+
94
+ export default plugin;
@@ -37,7 +37,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
37
37
  void app.commands.execute('docmanager:open', { path: serverPath });
38
38
  }
39
39
  });
40
- app.shell.add(browser, 'left', { rank: 50 });
40
+ // Ranks just under the Terminals panel in the left sidebar. The shipped
41
+ // `layout` setting assigns the default ranks (rank only, no area pin),
42
+ // so this browser stays movable via the "Move Widget" context menu; the
43
+ // `rank` here is the in-code fallback for when that setting is absent.
44
+ app.shell.add(browser, 'left', { rank: 2 });
41
45
  if (restorer) {
42
46
  restorer.add(browser, browser.id);
43
47
  }
@@ -301,35 +301,136 @@ function RangeSlider(props: {
301
301
 
302
302
  function Swipe({ reference, challenger }: ISideViewProps): React.ReactElement {
303
303
  const [position, setPosition] = React.useState(50);
304
+ const [dragging, setDragging] = React.useState(false);
305
+ const frameRef = React.useRef<HTMLDivElement>(null);
306
+
307
+ const setFromClientX = React.useCallback((clientX: number) => {
308
+ const frame = frameRef.current;
309
+ if (frame === null) {
310
+ return;
311
+ }
312
+ const rect = frame.getBoundingClientRect();
313
+ if (rect.width <= 0) {
314
+ return;
315
+ }
316
+ const next = ((clientX - rect.left) / rect.width) * 100;
317
+ setPosition(Math.max(0, Math.min(100, next)));
318
+ }, []);
319
+
320
+ const onPointerDown = React.useCallback(
321
+ (event: React.PointerEvent<HTMLDivElement>) => {
322
+ // Only react to the primary pointer (left click / touch / pen tip).
323
+ if (event.button !== 0) {
324
+ return;
325
+ }
326
+ event.preventDefault();
327
+ setDragging(true);
328
+ setFromClientX(event.clientX);
329
+ },
330
+ [setFromClientX]
331
+ );
332
+
333
+ React.useEffect(() => {
334
+ if (!dragging) {
335
+ return;
336
+ }
337
+ const onMove = (event: PointerEvent) => {
338
+ setFromClientX(event.clientX);
339
+ };
340
+ const onUp = () => {
341
+ setDragging(false);
342
+ };
343
+ window.addEventListener('pointermove', onMove);
344
+ window.addEventListener('pointerup', onUp);
345
+ window.addEventListener('pointercancel', onUp);
346
+ return () => {
347
+ window.removeEventListener('pointermove', onMove);
348
+ window.removeEventListener('pointerup', onUp);
349
+ window.removeEventListener('pointercancel', onUp);
350
+ };
351
+ }, [dragging, setFromClientX]);
352
+
353
+ const onKeyDown = React.useCallback(
354
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
355
+ const step = event.shiftKey ? 10 : 1;
356
+ let next: number | null = null;
357
+ switch (event.key) {
358
+ case 'ArrowLeft':
359
+ next = position - step;
360
+ break;
361
+ case 'ArrowRight':
362
+ next = position + step;
363
+ break;
364
+ case 'Home':
365
+ next = 0;
366
+ break;
367
+ case 'End':
368
+ next = 100;
369
+ break;
370
+ }
371
+ if (next === null) {
372
+ return;
373
+ }
374
+ event.preventDefault();
375
+ setPosition(Math.max(0, Math.min(100, next)));
376
+ },
377
+ [position]
378
+ );
379
+
304
380
  return (
305
381
  <div className="jp-xtralab-ImageDiff-interactive">
306
- <div className="jp-xtralab-ImageDiff-stack">
307
- {reference.uri !== null ? (
308
- <img
309
- className="jp-xtralab-ImageDiff-stackImg"
310
- src={reference.uri}
311
- alt="Reference revision"
312
- style={{
313
- clipPath: `inset(0 ${100 - position}% 0 0)`
314
- }}
315
- />
316
- ) : null}
317
- {challenger.uri !== null ? (
318
- <img
319
- className="jp-xtralab-ImageDiff-stackImg"
320
- src={challenger.uri}
321
- alt="Challenger revision"
322
- style={{
323
- clipPath: `inset(0 0 0 ${position}%)`
324
- }}
325
- />
326
- ) : null}
382
+ <div
383
+ className="jp-xtralab-ImageDiff-swipeFrame"
384
+ ref={frameRef}
385
+ onPointerDown={onPointerDown}
386
+ data-dragging={dragging ? 'true' : undefined}
387
+ >
388
+ <div className="jp-xtralab-ImageDiff-stack">
389
+ {reference.uri !== null ? (
390
+ <img
391
+ className="jp-xtralab-ImageDiff-stackImg"
392
+ src={reference.uri}
393
+ alt="Reference revision"
394
+ draggable={false}
395
+ style={{
396
+ clipPath: `inset(0 ${100 - position}% 0 0)`
397
+ }}
398
+ />
399
+ ) : null}
400
+ {challenger.uri !== null ? (
401
+ <img
402
+ className="jp-xtralab-ImageDiff-stackImg"
403
+ src={challenger.uri}
404
+ alt="Challenger revision"
405
+ draggable={false}
406
+ style={{
407
+ clipPath: `inset(0 0 0 ${position}%)`
408
+ }}
409
+ />
410
+ ) : null}
411
+ </div>
412
+ <div
413
+ className="jp-xtralab-ImageDiff-swipeDivider"
414
+ style={{ left: `${position}%` }}
415
+ role="slider"
416
+ aria-label="Swipe between reference and challenger"
417
+ aria-valuemin={0}
418
+ aria-valuemax={100}
419
+ aria-valuenow={Math.round(position)}
420
+ aria-orientation="vertical"
421
+ tabIndex={0}
422
+ onKeyDown={onKeyDown}
423
+ >
424
+ <div className="jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-top">
425
+ <div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" />
426
+ <div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" />
427
+ </div>
428
+ <div className="jp-xtralab-ImageDiff-swipeDividerHandle jp-mod-bottom">
429
+ <div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-left" />
430
+ <div className="jp-xtralab-ImageDiff-swipeDividerArrow jp-mod-right" />
431
+ </div>
432
+ </div>
327
433
  </div>
328
- <RangeSlider
329
- value={position}
330
- onChange={setPosition}
331
- ariaLabel="Swipe between reference and challenger"
332
- />
333
434
  </div>
334
435
  );
335
436
  }
package/src/index.ts CHANGED
@@ -1,31 +1,36 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
+ import agentSessionsPlugin from './agentSessions';
3
4
  import fileBrowserPlugin from './fileBrowser';
4
5
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
5
6
  import editorIndentPlugin from './editorIndent';
6
7
  import gitPlugins from './git';
7
- import launcherPlugin from './launcher';
8
+ import launcherPlugins from './launcher';
8
9
  import menusPlugin from './menus';
9
10
  import sidebarPlugin from './sidebar';
10
- import statusBarPlugin from './statusBar';
11
+ import terminalsPlugin from './terminals';
12
+ import topBarPlugin from './topBar';
11
13
 
12
14
  /**
13
15
  * Every plugin contributed by `xtralab`. The entry point of the
14
16
  * labextension is an array because the package bundles several independent
15
17
  * enhancements (file browser, git diff providers, …) — JupyterLab activates
16
18
  * each plugin individually and only the ones whose required tokens are
17
- * available end up running. `gitPlugins` is itself an array (the launcher
18
- * diff command + the jupyterlab-git diff providers), so it is spread in.
19
+ * available end up running. `gitPlugins` and `launcherPlugins` are themselves
20
+ * arrays (the git diff providers; the launcher plus its editor registry), so
21
+ * they are spread in.
19
22
  */
20
23
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
24
+ agentSessionsPlugin,
21
25
  editorBreadcrumbsPlugin,
22
26
  editorIndentPlugin,
23
27
  fileBrowserPlugin,
24
28
  ...gitPlugins,
25
- launcherPlugin,
29
+ ...launcherPlugins,
26
30
  menusPlugin,
27
31
  sidebarPlugin,
28
- statusBarPlugin
32
+ terminalsPlugin,
33
+ topBarPlugin
29
34
  ];
30
35
 
31
36
  export default plugins;
@@ -26,7 +26,7 @@ export interface IAgent {
26
26
  * the user types one into the launcher's prompt box. The semantics:
27
27
  *
28
28
  * - `[]` → prompt is appended as a positional argument:
29
- * `<command> 'PROMPT'`. (Used by claude, codex, gemini, vibe.)
29
+ * `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
30
30
  * - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
31
31
  * `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
32
32
  * - `undefined` → the agent does not accept an initial prompt. The
@@ -75,12 +75,15 @@ export interface IAgentSettings {
75
75
  }
76
76
 
77
77
  /**
78
- * Default agent cards shipped with xtralab. The list mirrors the eight
79
- * first-class personas in `jupyter-ai-contrib/jupyter-ai-acp-client` (which
80
- * is also where the icons come from), but uses the bare CLI names a user
81
- * would type in a terminal rather than the ACP wrapper binaries that
82
- * project spawns. Anything not on the user's `$PATH` is filtered out at
83
- * activation time, so the wider list is harmless.
78
+ * Default agent cards shipped with xtralab. Seven of these (Claude, Codex,
79
+ * Copilot, Goose, OpenCode, Kiro, Mistral Vibe) mirror first-class personas
80
+ * in `jupyter-ai-contrib/jupyter-ai-acp-client` (which is also where their
81
+ * icons come from), but use the bare CLI names a user would type in a
82
+ * terminal rather than the ACP wrapper binaries that project spawns.
83
+ * Antigravity (Google's agent-first IDE/CLI, `agy`) takes the Google slot —
84
+ * Gemini CLI was dropped because Google is sunsetting it for consumer
85
+ * accounts on 2026-06-18. Anything not on the user's `$PATH` is filtered
86
+ * out at activation time, so the wider list is harmless.
84
87
  */
85
88
  const DEFAULTS: IAgent[] = [
86
89
  {
@@ -104,14 +107,20 @@ const DEFAULTS: IAgent[] = [
104
107
  promptArgs: []
105
108
  },
106
109
  {
107
- id: 'gemini',
108
- label: 'Gemini',
109
- caption: 'Start the Gemini CLI in a new terminal.',
110
- command: 'gemini',
111
- icon: BUILTIN_AGENT_ICONS.gemini,
110
+ id: 'antigravity',
111
+ label: 'Antigravity',
112
+ caption: 'Open Google Antigravity from a new terminal.',
113
+ command: 'agy',
114
+ icon: BUILTIN_AGENT_ICONS.antigravity,
112
115
  rank: 2,
113
- requireAvailable: true,
114
- promptArgs: []
116
+ requireAvailable: true
117
+ // `agy` is the Antigravity launcher binary — a VS Code-derived editor
118
+ // CLI that opens the Agent Manager / workspace, not a terminal REPL.
119
+ // Its positional args are file/folder paths, so there is no inline
120
+ // natural-language prompt form; like goose/kiro we omit promptArgs so a
121
+ // typed prompt dims the button instead of being mangled into a path.
122
+ // The command stays the bare binary (no `agy .`) so the server-side
123
+ // `shutil.which` availability check still resolves it.
115
124
  },
116
125
  {
117
126
  id: 'copilot',