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,183 @@
1
+ import { LabIcon, textEditorIcon } from '@jupyterlab/ui-components';
2
+
3
+ import { BUILTIN_EDITOR_ICONS } from './icons';
4
+
5
+ /**
6
+ * A terminal text editor surfaced on the launcher's "Open" section. An editor
7
+ * takes no initial prompt and is launched by typing its command into a fresh
8
+ * terminal (e.g. `nvim`). The launcher shows a single editor tile (the first
9
+ * available candidate; see {@link resolveEditor}); the full list is shared with
10
+ * the terminals panel to badge running editors.
11
+ */
12
+ export interface IEditor {
13
+ /** Stable id; also the default `$PATH` command probed for availability. */
14
+ id: string;
15
+ /** Tile label, e.g. "Neovim". */
16
+ label: string;
17
+ /** Tile tooltip. */
18
+ caption: string;
19
+ /** The literal command typed into the new terminal, e.g. `nvim`. */
20
+ command: string;
21
+ /** Brand icon for the tile. */
22
+ icon: LabIcon;
23
+ /**
24
+ * Preference order for the single launcher tile: the first candidate, by
25
+ * ascending rank, that qualifies wins (Neovim before Vim by default).
26
+ */
27
+ rank: number;
28
+ /**
29
+ * When false, the launcher skips the `which`-based availability check for
30
+ * this entry — useful for a shell alias or function that isn't on PATH but
31
+ * still resolves when typed in a real terminal. Defaults to true.
32
+ */
33
+ requireAvailable: boolean;
34
+ }
35
+
36
+ /**
37
+ * The settings-side shape: every field except `id` is optional, so a user can
38
+ * override a single field on a built-in editor (e.g. swap `vim`'s command for
39
+ * a wrapper) without restating the whole entry. New ids define brand-new
40
+ * editors. Mirrors `IAgentSettings` minus `promptArgs`.
41
+ */
42
+ export interface IEditorSettings {
43
+ id: string;
44
+ label?: string;
45
+ caption?: string;
46
+ command?: string;
47
+ /**
48
+ * Inline SVG for a custom editor's icon. Required for new ids whose icon
49
+ * isn't shipped with xtralab; ignored for built-in ids unless explicitly
50
+ * set (in which case it overrides the built-in).
51
+ */
52
+ iconSvg?: string;
53
+ rank?: number;
54
+ /**
55
+ * When false, the editor is hidden from the launcher's Open section (and no
56
+ * longer recognised by the terminals panel). Disable both built-ins to drop
57
+ * the editor tile entirely. Defaults to true.
58
+ */
59
+ enabled?: boolean;
60
+ /** See {@link IEditor.requireAvailable}. */
61
+ requireAvailable?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Built-in editor candidates in preference order. The launcher shows the first
66
+ * one whose command resolves on the server's `$PATH`, so Neovim wins over Vim
67
+ * when both are installed. Users override, disable, or extend this list
68
+ * through the `editors` setting; see {@link mergeEditors}.
69
+ */
70
+ const DEFAULTS: IEditor[] = [
71
+ {
72
+ id: 'nvim',
73
+ label: 'Neovim',
74
+ caption: 'Open Neovim in a new terminal.',
75
+ command: 'nvim',
76
+ icon: BUILTIN_EDITOR_ICONS.nvim,
77
+ rank: 0,
78
+ requireAvailable: true
79
+ },
80
+ {
81
+ id: 'vim',
82
+ label: 'Vim',
83
+ caption: 'Open Vim in a new terminal.',
84
+ command: 'vim',
85
+ icon: BUILTIN_EDITOR_ICONS.vim,
86
+ rank: 1,
87
+ requireAvailable: true
88
+ }
89
+ ];
90
+
91
+ function resolveEditorIcon(id: string, iconSvg: string | undefined): LabIcon {
92
+ if (iconSvg) {
93
+ return new LabIcon({
94
+ name: `xtralab:editor-custom-${id}`,
95
+ svgstr: iconSvg
96
+ });
97
+ }
98
+ return BUILTIN_EDITOR_ICONS[id] ?? textEditorIcon;
99
+ }
100
+
101
+ /**
102
+ * Merge xtralab's built-in editors with the user's `editors` settings. Built-in
103
+ * entries keep their fields unless explicitly overridden; user-only entries are
104
+ * appended. `enabled: false` filters an entry out of the result entirely (so
105
+ * callers don't need to re-check the flag). The result is sorted by rank, which
106
+ * is also the launcher's tile-preference order. Mirrors `mergeAgents`.
107
+ */
108
+ export function mergeEditors(overrides: IEditorSettings[]): IEditor[] {
109
+ const overrideById = new Map(overrides.map(entry => [entry.id, entry]));
110
+ const merged: IEditor[] = [];
111
+
112
+ for (const base of DEFAULTS) {
113
+ const override = overrideById.get(base.id);
114
+ if (!override) {
115
+ merged.push({ ...base });
116
+ continue;
117
+ }
118
+ overrideById.delete(base.id);
119
+ if (override.enabled === false) {
120
+ continue;
121
+ }
122
+ merged.push({
123
+ id: base.id,
124
+ label: override.label ?? base.label,
125
+ caption: override.caption ?? base.caption,
126
+ command: override.command ?? base.command,
127
+ icon: override.iconSvg
128
+ ? resolveEditorIcon(base.id, override.iconSvg)
129
+ : base.icon,
130
+ rank: override.rank ?? base.rank,
131
+ requireAvailable: override.requireAvailable ?? base.requireAvailable
132
+ });
133
+ }
134
+
135
+ // What remains in `overrideById` are ids that don't match a built-in — treat
136
+ // them as new editors the user is adding. Skip disabled ones; the settings
137
+ // schema validates the shape, so missing fields just fall back here.
138
+ let nextRank =
139
+ merged.reduce((max, editor) => Math.max(max, editor.rank), -1) + 1;
140
+ for (const entry of overrideById.values()) {
141
+ if (entry.enabled === false) {
142
+ continue;
143
+ }
144
+ const label = entry.label ?? entry.id;
145
+ const command = entry.command ?? entry.id;
146
+ merged.push({
147
+ id: entry.id,
148
+ label,
149
+ caption: entry.caption ?? `Open ${label} in a new terminal.`,
150
+ command,
151
+ icon: resolveEditorIcon(entry.id, entry.iconSvg),
152
+ rank: entry.rank ?? nextRank++,
153
+ requireAvailable: entry.requireAvailable ?? true
154
+ });
155
+ }
156
+
157
+ merged.sort((a, b) => a.rank - b.rank);
158
+ return merged;
159
+ }
160
+
161
+ /**
162
+ * Pick the single editor tile to show, given the merged editor list and the
163
+ * set of commands known to be on `$PATH` (from the launcher's availability
164
+ * probe). Returns the first candidate, by rank, that is either available or
165
+ * opts out of the check (`requireAvailable: false`) — Neovim before Vim by
166
+ * default — or `null` when none qualifies.
167
+ *
168
+ * When `available` is `null` (the endpoint couldn't be reached), a
169
+ * `requireAvailable` editor is not shown; an entry with
170
+ * `requireAvailable: false` is shown regardless.
171
+ */
172
+ export function resolveEditor(
173
+ editors: IEditor[],
174
+ available: Set<string> | null
175
+ ): IEditor | null {
176
+ return (
177
+ editors.find(
178
+ editor =>
179
+ !editor.requireAvailable ||
180
+ (available !== null && available.has(editor.command))
181
+ ) ?? null
182
+ );
183
+ }
@@ -1,23 +1,112 @@
1
1
  import { LabIcon } from '@jupyterlab/ui-components';
2
2
 
3
3
  /**
4
- * Brand icons for the agent launcher cards. The artwork is taken verbatim
4
+ * Brand icons for the agent launcher cards. Most marks are taken verbatim
5
5
  * from `jupyter-ai-contrib/jupyter-ai-acp-client`
6
- * (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause), with small
7
- * adjustments to play nicely with JupyterLab themes:
6
+ * (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause); Antigravity post-dates
7
+ * that set, so its mark is the official glyph from `@lobehub/icons-static-svg`
8
+ * (MIT) — we ship the brand-colored `antigravity-color` variant so Antigravity
9
+ * reads as a multi-color Google mark, occupying the slot the Gemini icon used
10
+ * to fill before the upstream Gemini CLI sunset. Small adjustments are applied
11
+ * to play nicely with JupyterLab themes:
8
12
  * - Monochrome marks (Codex, Copilot, Goose) use `currentColor` so they
9
13
  * inherit the surrounding launcher card's text color rather than
10
14
  * hard-coding black, which disappears on the dark theme.
11
15
  * - The Goose mark upstream is rendered on a white rounded rect; we drop
12
16
  * the rect so the silhouette can sit on the launcher card directly.
13
17
  * - SVG ids that would collide if two copies of the same artwork ended up
14
- * on the page (gradient/mask defs) are namespaced under `xtralab-…`.
18
+ * on the page (gradient/mask/filter defs) are namespaced under
19
+ * `xtralab-…`.
15
20
  *
16
- * Brand-colored icons (Claude, Gemini, Kiro, Mistral Vibe, OpenCode) keep
21
+ * Brand-colored icons (Antigravity, Claude, Kiro, Mistral Vibe, OpenCode) keep
17
22
  * their upstream colors; the dark/light contrast of the launcher card behind
18
23
  * them is the same as on `jupyter-ai-acp-client`.
24
+ *
25
+ * The Vim and Neovim marks belong to the launcher's "Open" editor tile rather
26
+ * than the agent grid. They are the brand silhouettes from Simple Icons
27
+ * (https://simpleicons.org, CC0), each filled with its brand green so the mark
28
+ * reads on both the light and dark themes.
19
29
  */
20
30
 
31
+ export const antigravityIcon = new LabIcon({
32
+ name: 'xtralab:agent-antigravity',
33
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
34
+ <mask id="xtralab-antigravity-mask" height="23" maskUnits="userSpaceOnUse" width="24" x="0" y="1">
35
+ <path d="M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z" fill="#fff"/>
36
+ </mask>
37
+ <g mask="url(#xtralab-antigravity-mask)">
38
+ <g filter="url(#xtralab-antigravity-blur-1)">
39
+ <path d="M-1.018-3.992c-.408 3.591 2.686 6.89 6.91 7.37 4.225.48 7.98-2.043 8.387-5.633.408-3.59-2.686-6.89-6.91-7.37-4.225-.479-7.98 2.043-8.387 5.633z" fill="#FFE432"/>
40
+ </g>
41
+ <g filter="url(#xtralab-antigravity-blur-2)">
42
+ <path d="M15.269 7.747c1.058 4.557 5.691 7.374 10.348 6.293 4.657-1.082 7.575-5.653 6.516-10.21-1.058-4.556-5.691-7.374-10.348-6.292-4.657 1.082-7.575 5.653-6.516 10.21z" fill="#FC413D"/>
43
+ </g>
44
+ <g filter="url(#xtralab-antigravity-blur-3)">
45
+ <path d="M-12.443 10.804c1.338 4.703 7.36 7.11 13.453 5.378 6.092-1.733 9.947-6.95 8.61-11.652C8.282-.173 2.26-2.58-3.833-.848-9.925.884-13.78 6.1-12.443 10.804z" fill="#00B95C"/>
46
+ </g>
47
+ <g filter="url(#xtralab-antigravity-blur-4)">
48
+ <path d="M-12.443 10.804c1.338 4.703 7.36 7.11 13.453 5.378 6.092-1.733 9.947-6.95 8.61-11.652C8.282-.173 2.26-2.58-3.833-.848-9.925.884-13.78 6.1-12.443 10.804z" fill="#00B95C"/>
49
+ </g>
50
+ <g filter="url(#xtralab-antigravity-blur-5)">
51
+ <path d="M-7.608 14.703c3.352 3.424 9.126 3.208 12.896-.483 3.77-3.69 4.108-9.459.756-12.883C2.69-2.087-3.083-1.871-6.853 1.82c-3.77 3.69-4.108 9.458-.755 12.883z" fill="#00B95C"/>
52
+ </g>
53
+ <g filter="url(#xtralab-antigravity-blur-6)">
54
+ <path d="M9.932 27.617c1.04 4.482 5.384 7.303 9.7 6.3 4.316-1.002 6.971-5.448 5.93-9.93-1.04-4.483-5.384-7.304-9.7-6.301-4.316 1.002-6.971 5.448-5.93 9.93z" fill="#3186FF"/>
55
+ </g>
56
+ <g filter="url(#xtralab-antigravity-blur-7)">
57
+ <path d="M2.572-8.185C.392-3.329 2.778 2.472 7.9 4.771c5.122 2.3 11.042.227 13.222-4.63 2.18-4.855-.205-10.656-5.327-12.955-5.122-2.3-11.042-.227-13.222 4.63z" fill="#FBBC04"/>
58
+ </g>
59
+ <g filter="url(#xtralab-antigravity-blur-8)">
60
+ <path d="M-3.267 38.686c-5.277-2.072 3.742-19.117 5.984-24.83 2.243-5.712 8.34-8.664 13.616-6.592 5.278 2.071 11.533 13.482 9.29 19.195-2.242 5.713-23.613 14.298-28.89 12.227z" fill="#3186FF"/>
61
+ </g>
62
+ <g filter="url(#xtralab-antigravity-blur-9)">
63
+ <path d="M28.71 17.471c-1.413 1.649-5.1.808-8.236-1.878-3.135-2.687-4.531-6.201-3.118-7.85 1.412-1.649 5.1-.808 8.235 1.878s4.532 6.2 3.119 7.85z" fill="#749BFF"/>
64
+ </g>
65
+ <g filter="url(#xtralab-antigravity-blur-10)">
66
+ <path d="M18.163 9.077c5.81 3.93 12.502 4.19 14.946.577 2.443-3.612-.287-9.727-6.098-13.658-5.81-3.931-12.502-4.19-14.946-.577-2.443 3.612.287 9.727 6.098 13.658z" fill="#FC413D"/>
67
+ </g>
68
+ <g filter="url(#xtralab-antigravity-blur-11)">
69
+ <path d="M-.915 2.684c-1.44 3.473-.97 6.967 1.05 7.804 2.02.837 4.824-1.3 6.264-4.772 1.44-3.473.97-6.967-1.05-7.804-2.02-.837-4.824 1.3-6.264 4.772z" fill="#FFEE48"/>
70
+ </g>
71
+ </g>
72
+ <defs>
73
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="17.587" id="xtralab-antigravity-blur-1" width="19.838" x="-3.288" y="-11.917">
74
+ <feGaussianBlur stdDeviation="1.117"/>
75
+ </filter>
76
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="38.565" id="xtralab-antigravity-blur-2" width="38.9" x="4.251" y="-13.493">
77
+ <feGaussianBlur stdDeviation="5.4"/>
78
+ </filter>
79
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.517" id="xtralab-antigravity-blur-3" width="40.955" x="-21.889" y="-10.592">
80
+ <feGaussianBlur stdDeviation="4.591"/>
81
+ </filter>
82
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.517" id="xtralab-antigravity-blur-4" width="40.955" x="-21.889" y="-10.592">
83
+ <feGaussianBlur stdDeviation="4.591"/>
84
+ </filter>
85
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.595" id="xtralab-antigravity-blur-5" width="36.632" x="-19.099" y="-10.278">
86
+ <feGaussianBlur stdDeviation="4.591"/>
87
+ </filter>
88
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="34.087" id="xtralab-antigravity-blur-6" width="33.533" x=".981" y="8.758">
89
+ <feGaussianBlur stdDeviation="4.363"/>
90
+ </filter>
91
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="35.276" id="xtralab-antigravity-blur-7" width="35.978" x="-6.143" y="-21.659">
92
+ <feGaussianBlur stdDeviation="3.954"/>
93
+ </filter>
94
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="46.523" id="xtralab-antigravity-blur-8" width="45.114" x="-11.96" y="-.46">
95
+ <feGaussianBlur stdDeviation="3.531"/>
96
+ </filter>
97
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="24.054" id="xtralab-antigravity-blur-9" width="25.094" x="10.485" y=".58">
98
+ <feGaussianBlur stdDeviation="3.159"/>
99
+ </filter>
100
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="30.007" id="xtralab-antigravity-blur-10" width="33.508" x="5.833" y="-12.467">
101
+ <feGaussianBlur stdDeviation="2.669"/>
102
+ </filter>
103
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="26.151" id="xtralab-antigravity-blur-11" width="22.194" x="-8.355" y="-8.876">
104
+ <feGaussianBlur stdDeviation="3.303"/>
105
+ </filter>
106
+ </defs>
107
+ </svg>`
108
+ });
109
+
21
110
  export const claudeIcon = new LabIcon({
22
111
  name: 'xtralab:agent-claude',
23
112
  svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
@@ -40,20 +129,6 @@ export const copilotIcon = new LabIcon({
40
129
  </svg>`
41
130
  });
42
131
 
43
- export const geminiIcon = new LabIcon({
44
- name: 'xtralab:agent-gemini',
45
- svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
46
- <path d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z" fill="url(#xtralab-gemini-radial)"/>
47
- <defs>
48
- <radialGradient id="xtralab-gemini-radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)">
49
- <stop offset=".067" stop-color="#9168C0"/>
50
- <stop offset=".343" stop-color="#5684D1"/>
51
- <stop offset=".672" stop-color="#1BA1E3"/>
52
- </radialGradient>
53
- </defs>
54
- </svg>`
55
- });
56
-
57
132
  export const gooseIcon = new LabIcon({
58
133
  name: 'xtralab:agent-goose',
59
134
  svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
@@ -105,6 +180,20 @@ export const opencodeIcon = new LabIcon({
105
180
  </svg>`
106
181
  });
107
182
 
183
+ export const neovimIcon = new LabIcon({
184
+ name: 'xtralab:editor-neovim',
185
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#57A143">
186
+ <path d="M2.214 4.954v13.615L7.655 24V10.314L3.312 3.845 2.214 4.954zm4.999 17.98l-4.557-4.548V5.136l.59-.596 3.967 5.908v12.485zm14.573-4.457l-.862.937-4.24-6.376V0l5.068 5.092.034 13.385zM7.431.001l12.998 19.835-3.637 3.637L3.787 3.683 7.43 0z"/>
187
+ </svg>`
188
+ });
189
+
190
+ export const vimIcon = new LabIcon({
191
+ name: 'xtralab:editor-vim',
192
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#019733">
193
+ <path d="M24 11.986h-.027l-4.318-4.318 4.303-4.414V1.461l-.649-.648h-8.198l-.66.605v1.045L12.015.027V0L12 .014 11.986 0v.027l-1.29 1.291-.538-.539H2.035l-.638.692v1.885l.616.616h.72v5.31L.027 11.987H0L.014 12 0 12.014h.027l2.706 2.706v6.467l.907.523h2.322l1.857-1.904 4.166 4.166V24l.015-.014.014.014v-.028l2.51-2.509h.485c.111 0 .211-.07.25-.179l.146-.426c.028-.084.012-.172-.037-.239l1.462-1.462-.612 1.962c-.043.141.036.289.177.332.025.008.052.012.078.012h1.824c.106-.001.201-.064.243-.163l.165-.394c.025-.065.024-.138-.004-.203-.027-.065-.08-.116-.146-.142-.029-.012-.062-.019-.097-.02h-.075l.84-2.644h1.232l-1.016 3.221c-.043.141.036.289.176.332.025.008.052.012.079.012h2.002c.11 0 .207-.066.248-.17l.164-.428c.051-.138-.021-.29-.158-.341-.029-.011-.06-.017-.091-.017h-.145l1.131-3.673c.027-.082.012-.173-.039-.24l-.375-.504-.003-.005c-.051-.064-.127-.102-.209-.102h-1.436c-.071 0-.141.03-.19.081l-.4.439h-.624l-.042-.046 4.445-4.445H24L23.986 12l.014-.014zM9.838 21.139l1.579-4.509h-.501l.297-.304h1.659l-1.563 4.555h.623l-.079.258H9.838zm3.695-7.516l.15.151-.269.922-.225.226h-.969l-.181-.181.311-.871.288-.247h.895zM5.59 20.829H3.877l-.262-.15V3.091H2.379l-.1-.1V1.815l.143-.154h7.371l.213.214v1.108l-.142.173H8.785v8.688l8.807-8.688h-2.086l-.175-.188V1.805l.121-.111h7.49l.132.133v1.07L12.979 13.25h-.373c-.015-.001-.028 0-.042.001l-.02.003c-.045.01-.086.03-.119.06l-.343.295-.004.003c-.033.031-.059.069-.073.111l-.296.83-6.119 6.276zm14.768-3.952l.474-.519h1.334l.309.415-1.265 4.107h.493l-.08.209H19.84l1.124-3.564h-2.015l-1.077 3.391h.424l-.073.174h-1.605l1.107-3.548h-2.096l-1.062 3.339h.436l-.072.209H13.27l1.514-4.46H14.198l.091-.271h1.65l.519.537h.906l.491-.554h1.061l.489.535h.953z"/>
194
+ </svg>`
195
+ });
196
+
108
197
  /**
109
198
  * Built-in icons keyed by the agent's id, used to resolve the icon for an
110
199
  * agent declared without an inline `iconSvg` override (i.e. one of xtralab's
@@ -112,12 +201,23 @@ export const opencodeIcon = new LabIcon({
112
201
  * `iconSvg` or fall back to the generic terminal icon.
113
202
  */
114
203
  export const BUILTIN_AGENT_ICONS: Record<string, LabIcon> = {
204
+ antigravity: antigravityIcon,
115
205
  claude: claudeIcon,
116
206
  codex: codexIcon,
117
207
  copilot: copilotIcon,
118
- gemini: geminiIcon,
119
208
  goose: gooseIcon,
120
209
  kiro: kiroIcon,
121
210
  'mistral-vibe': mistralVibeIcon,
122
211
  opencode: opencodeIcon
123
212
  };
213
+
214
+ /**
215
+ * Built-in icons keyed by the editor's id, used to resolve the icon for a
216
+ * built-in editor (Neovim, Vim) declared without an inline `iconSvg`. Custom
217
+ * editors added via settings can supply their own `iconSvg` or fall back to
218
+ * the generic text-editor icon.
219
+ */
220
+ export const BUILTIN_EDITOR_ICONS: Record<string, LabIcon> = {
221
+ nvim: neovimIcon,
222
+ vim: vimIcon
223
+ };
@@ -12,10 +12,14 @@ import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
12
12
  import type { IDisposable } from '@lumino/disposable';
13
13
  import type { Widget } from '@lumino/widgets';
14
14
 
15
+ import { IAgentSessions } from '../agentSessions';
15
16
  import { mergeAgents, type IAgent, type IAgentSettings } from './agents';
16
17
  import { fetchAvailableCommands } from './availability';
17
18
  import { CREATE_LAUNCHER_COMMAND, registerAgentCommands } from './commands';
18
19
  import { LauncherDashboard } from './dashboard';
20
+ import { editorRegistryPlugin, IEditorRegistry } from './editorRegistry';
21
+ import { AgentRegistry } from './registry';
22
+ import { IAgentRegistry } from './tokens';
19
23
 
20
24
  const PLUGIN_ID = 'xtralab:launcher';
21
25
 
@@ -23,7 +27,7 @@ const PLUGIN_ID = 'xtralab:launcher';
23
27
  * The xtralab launcher plugin. Replaces the stock JupyterLab launcher
24
28
  * (which is disabled via `package.json`'s `jupyterlab.disabledExtensions`)
25
29
  * with an agent-focused dashboard: an optional initial prompt, a row of
26
- * agent buttons (Claude, Codex, Gemini, …), and a collapsible list of
30
+ * agent buttons (Claude, Codex, Antigravity, …), and a collapsible list of
27
31
  * changed files (clickable into the diff viewer) below them. Clicking an
28
32
  * agent opens a fresh terminal and pipes the agent's command into it; if
29
33
  * the prompt textarea is non-empty, the prompt is shell-quoted and
@@ -39,25 +43,37 @@ const PLUGIN_ID = 'xtralab:launcher';
39
43
  * effect, which would defeat the point of the agent-only launcher. If an
40
44
  * extension needs to surface itself, we'll add it here explicitly.
41
45
  */
42
- const plugin: JupyterFrontEndPlugin<void> = {
46
+ const plugin: JupyterFrontEndPlugin<IAgentRegistry> = {
43
47
  id: PLUGIN_ID,
44
48
  description:
45
49
  'An agent-focused launcher that replaces the default JupyterLab launcher.',
46
50
  autoStart: true,
47
- optional: [ILabShell, ICommandPalette, ISettingRegistry, ITranslator],
51
+ provides: IAgentRegistry,
52
+ optional: [
53
+ ILabShell,
54
+ ICommandPalette,
55
+ ISettingRegistry,
56
+ ITranslator,
57
+ IAgentSessions,
58
+ IEditorRegistry
59
+ ],
48
60
  activate: async (
49
61
  app: JupyterFrontEnd,
50
62
  labShell: ILabShell | null,
51
63
  palette: ICommandPalette | null,
52
64
  settingRegistry: ISettingRegistry | null,
53
- translator: ITranslator | null
54
- ): Promise<void> => {
65
+ translator: ITranslator | null,
66
+ agentSessions: IAgentSessions | null,
67
+ editorRegistry: IEditorRegistry | null
68
+ ): Promise<IAgentRegistry> => {
55
69
  const { commands, shell } = app;
56
70
  const trans = (translator ?? nullTranslator).load('jupyterlab');
57
71
 
58
- // The currently active agent list. Updated whenever settings change so
59
- // every freshly-created launcher widget renders the latest list.
60
- let currentAgents: IAgent[] = [];
72
+ // The shared agent registry this plugin provides on `IAgentRegistry`.
73
+ // It holds the active agent list (updated whenever settings change), so
74
+ // every freshly-created launcher widget — and any other plugin that
75
+ // consumes the token, e.g. the terminals panel — renders the latest list.
76
+ const registry = new AgentRegistry();
61
77
 
62
78
  // Track command registrations so a settings change can wipe them
63
79
  // before re-registering — without this the command palette would
@@ -66,11 +82,24 @@ const plugin: JupyterFrontEndPlugin<void> = {
66
82
 
67
83
  const applyAgents = async (overrides: IAgentSettings[]): Promise<void> => {
68
84
  const agents = mergeAgents(overrides);
69
- const filtered = await filterByAvailability(agents);
85
+
86
+ // Probe the server's `$PATH` so we only surface agents that are actually
87
+ // installed. Agents with `requireAvailable: false` opt out (their command
88
+ // may be a shell alias that `which` can't see) and are kept regardless.
89
+ const probe = Array.from(
90
+ new Set(
91
+ agents
92
+ .filter(agent => agent.requireAvailable)
93
+ .map(agent => agent.command)
94
+ )
95
+ );
96
+ const available = await fetchAvailableCommands(probe);
97
+
98
+ const filtered = filterAgents(agents, available);
70
99
 
71
100
  registered?.dispose();
72
- registered = registerAgentCommands(app, filtered);
73
- currentAgents = filtered;
101
+ registered = registerAgentCommands(app, filtered, agentSessions);
102
+ registry.setAgents(filtered);
74
103
  };
75
104
 
76
105
  const readOverrides = (
@@ -121,7 +150,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
121
150
  };
122
151
  const launcher = new LauncherDashboard({
123
152
  commands,
124
- agents: currentAgents,
153
+ agents: registry.agents,
154
+ editor: editorRegistry?.current ?? null,
155
+ agentSessions,
125
156
  onAgentLaunch,
126
157
  // Empty repoPath/cwd matches the JupyterLab convention used by
127
158
  // the git panel and the stock launcher: let the server resolve
@@ -186,26 +217,25 @@ const plugin: JupyterFrontEndPlugin<void> = {
186
217
  return commands.execute(CREATE_LAUNCHER_COMMAND, { ref });
187
218
  });
188
219
  }
220
+
221
+ return registry;
189
222
  }
190
223
  };
191
224
 
192
225
  /**
193
- * Drop agents whose command isn't on `$PATH`, except for entries that
194
- * explicitly opt out of the check via `requireAvailable: false` (typical
195
- * use case: shell aliases the user wants surfaced regardless).
226
+ * Drop agents whose command isn't on `$PATH`, except entries that opt out via
227
+ * `requireAvailable: false` (e.g. shell aliases the user wants surfaced
228
+ * regardless). `available` is the resolved set from
229
+ * {@link fetchAvailableCommands}.
196
230
  *
197
- * If the availability endpoint can't be reached we fail open and return
198
- * the input unchanged — better to show an unreachable agent than to hide
199
- * the entire launcher because the server extension didn't load.
231
+ * When `available` is `null` (the endpoint couldn't be reached) we fail open
232
+ * and return the input unchanged — better to show an unreachable agent than
233
+ * to hide the entire launcher because the server extension didn't load.
200
234
  */
201
- async function filterByAvailability(agents: IAgent[]): Promise<IAgent[]> {
202
- const commands = agents
203
- .filter(agent => agent.requireAvailable)
204
- .map(agent => agent.command);
205
- if (commands.length === 0) {
206
- return agents;
207
- }
208
- const available = await fetchAvailableCommands(commands);
235
+ function filterAgents(
236
+ agents: IAgent[],
237
+ available: Set<string> | null
238
+ ): IAgent[] {
209
239
  if (!available) {
210
240
  return agents;
211
241
  }
@@ -225,4 +255,9 @@ namespace Private {
225
255
  }
226
256
  }
227
257
 
228
- export default plugin;
258
+ const plugins: JupyterFrontEndPlugin<unknown>[] = [
259
+ plugin,
260
+ editorRegistryPlugin
261
+ ];
262
+
263
+ export default plugins;
@@ -0,0 +1,33 @@
1
+ import { ISignal, Signal } from '@lumino/signaling';
2
+
3
+ import type { IAgent } from './agents';
4
+ import type { IAgentRegistry } from './tokens';
5
+
6
+ /**
7
+ * The concrete {@link IAgentRegistry} the launcher plugin provides on the
8
+ * `IAgentRegistry` token. Holds the active agent list and re-emits `changed`
9
+ * whenever the launcher recomputes it (on a settings change). The launcher
10
+ * is the only writer, so the write side ({@link setAgents}) is kept off the
11
+ * shared token.
12
+ */
13
+ export class AgentRegistry implements IAgentRegistry {
14
+ get agents(): IAgent[] {
15
+ return this._agents;
16
+ }
17
+
18
+ get changed(): ISignal<IAgentRegistry, void> {
19
+ return this._changed;
20
+ }
21
+
22
+ /**
23
+ * Replace the agent list and notify observers. Called by the launcher
24
+ * after merging the user's settings and filtering by availability.
25
+ */
26
+ setAgents(agents: IAgent[]): void {
27
+ this._agents = agents;
28
+ this._changed.emit();
29
+ }
30
+
31
+ private _agents: IAgent[] = [];
32
+ private _changed = new Signal<IAgentRegistry, void>(this);
33
+ }
@@ -0,0 +1,51 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ import type { ISignal } from '@lumino/signaling';
3
+
4
+ import type { IAgent } from './agents';
5
+
6
+ /**
7
+ * A read-only, observable view of the launcher's available agents.
8
+ *
9
+ * The launcher plugin owns the agent list — xtralab's defaults merged with
10
+ * the user's `xtralab:launcher` settings, then filtered by a server-side
11
+ * `which` check — and registers an `xtralab:start-agent:<id>` command for
12
+ * each entry. This token shares that list, and (via {@link agentCommandId})
13
+ * the command id that launches each agent, so other plugins can surface the
14
+ * same agents without re-deriving the list or duplicating the icons. The
15
+ * terminals panel uses it to build its "new terminal" dropdown.
16
+ */
17
+ export interface IAgentRegistry {
18
+ /**
19
+ * The current agents, already filtered by availability and sorted by
20
+ * rank — the same array the launcher renders.
21
+ */
22
+ readonly agents: IAgent[];
23
+
24
+ /**
25
+ * Emitted whenever {@link agents} changes (e.g. the user edits the
26
+ * launcher settings). Consumers that cache the list should re-read it
27
+ * here; consumers that read it on demand can ignore the signal.
28
+ */
29
+ readonly changed: ISignal<IAgentRegistry, void>;
30
+ }
31
+
32
+ /**
33
+ * DI token for {@link IAgentRegistry}. Provided by `xtralab:launcher` and
34
+ * consumed — optionally, so the panel still works when the launcher is
35
+ * disabled — by `xtralab:terminals`.
36
+ */
37
+ export const IAgentRegistry = new Token<IAgentRegistry>(
38
+ 'xtralab:IAgentRegistry',
39
+ 'A read-only, observable view of the launcher agents, shared so other plugins can offer the same agent list and icons.'
40
+ );
41
+
42
+ /**
43
+ * The JupyterLab command id that launches a given agent in a new terminal.
44
+ * Defined here — in the shared contract module rather than the launcher's
45
+ * command-registration internals — so the launcher (which registers the
46
+ * commands) and any consumer (which references them, e.g. in a menu) agree
47
+ * on the id.
48
+ */
49
+ export function agentCommandId(agentId: string): string {
50
+ return `xtralab:start-agent:${agentId}`;
51
+ }