jupyterlab_ai_code_assistants_extension 0.6.15

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 (57) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +44 -0
  3. package/lib/core/colour.d.ts +57 -0
  4. package/lib/core/colour.js +190 -0
  5. package/lib/core/icons.d.ts +12 -0
  6. package/lib/core/icons.js +99 -0
  7. package/lib/core/modes.d.ts +13 -0
  8. package/lib/core/modes.js +36 -0
  9. package/lib/core/panel.d.ts +233 -0
  10. package/lib/core/panel.js +1673 -0
  11. package/lib/core/popup.d.ts +40 -0
  12. package/lib/core/popup.js +404 -0
  13. package/lib/core/registry.d.ts +24 -0
  14. package/lib/core/registry.js +48 -0
  15. package/lib/core/request.d.ts +25 -0
  16. package/lib/core/request.js +64 -0
  17. package/lib/core/terminals.d.ts +129 -0
  18. package/lib/core/terminals.js +344 -0
  19. package/lib/core/types.d.ts +269 -0
  20. package/lib/core/types.js +6 -0
  21. package/lib/index.d.ts +3 -0
  22. package/lib/index.js +277 -0
  23. package/lib/providers/claude.d.ts +3 -0
  24. package/lib/providers/claude.js +88 -0
  25. package/lib/providers/codex.d.ts +3 -0
  26. package/lib/providers/codex.js +79 -0
  27. package/lib/providers/gemini.d.ts +2 -0
  28. package/lib/providers/gemini.js +73 -0
  29. package/lib/providers/index.d.ts +3 -0
  30. package/lib/providers/index.js +16 -0
  31. package/lib/providers/kimi.d.ts +23 -0
  32. package/lib/providers/kimi.js +139 -0
  33. package/package.json +169 -0
  34. package/schema/plugin.json +93 -0
  35. package/src/__tests__/colour.spec.ts +153 -0
  36. package/src/__tests__/core-neutrality.spec.ts +86 -0
  37. package/src/__tests__/labels.spec.ts +89 -0
  38. package/src/__tests__/launch-mode.spec.ts +53 -0
  39. package/src/__tests__/registry.spec.ts +178 -0
  40. package/src/core/colour.ts +224 -0
  41. package/src/core/icons.ts +121 -0
  42. package/src/core/modes.ts +45 -0
  43. package/src/core/panel.ts +2020 -0
  44. package/src/core/popup.ts +475 -0
  45. package/src/core/registry.ts +60 -0
  46. package/src/core/request.ts +96 -0
  47. package/src/core/terminals.ts +446 -0
  48. package/src/core/types.ts +305 -0
  49. package/src/index.ts +356 -0
  50. package/src/providers/claude.ts +102 -0
  51. package/src/providers/codex.ts +89 -0
  52. package/src/providers/gemini.ts +80 -0
  53. package/src/providers/index.ts +21 -0
  54. package/src/providers/kimi.ts +155 -0
  55. package/style/base.css +888 -0
  56. package/style/index.css +1 -0
  57. package/style/index.js +1 -0
@@ -0,0 +1,269 @@
1
+ /** How a conversation is branched.
2
+ *
3
+ * - `native-flag` - the CLI forks in-process from a flag pair, and the new
4
+ * conversation id is minted by the frontend, so the launch carries it and
5
+ * the branch is watched for until its store entry materialises
6
+ * - `native-command` - the CLI owns the fork verb AND mints the id itself, so
7
+ * the id has to be discovered by diffing the branch list after launch
8
+ * - `server-copy` - the CLI has no fork at all; the server copies the store
9
+ * entry, answers with the new id, and the launch resumes that id
10
+ */
11
+ export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
12
+ /** Where a conversation's DEFAULT tab tint comes from. A user-set colour in
13
+ * the extension's own write-back store always wins over all three. */
14
+ export type ColourSource = 'native' | 'derived' | 'none';
15
+ /** Who owns a branched conversation's name.
16
+ *
17
+ * - `launch-flag` - the CLI is told the name at launch and stamps it itself
18
+ * - `server-side` - the server writes the name into the copied store entry
19
+ * - `none` - the assistant has no naming surface, so no name is asked for
20
+ */
21
+ export type NamingStrategy = 'launch-flag' | 'server-side' | 'none';
22
+ /** One launch-behaviour setting of an assistant, in that assistant's own
23
+ * terminology. `boolean` modes are the unsafe/skip-approval switches: they get
24
+ * a settings toggle (off by default) AND a context-menu launch variant.
25
+ * `enum` modes are settings-only choices passed through on every launch. */
26
+ export interface ILaunchMode {
27
+ /** Settings key under `providers.<id>.` and launch-payload key. Uses the
28
+ * assistant's own terminology, never a normalised core name. */
29
+ id: string;
30
+ /** Settings title shown in the JupyterLab settings editor. */
31
+ title: string;
32
+ /** Settings description. */
33
+ description: string;
34
+ kind: 'boolean' | 'enum';
35
+ /** Allowed values, `enum` modes only. */
36
+ values?: string[];
37
+ /** Default value. Boolean modes default to `false` without exception. */
38
+ default: boolean | string;
39
+ /** Menu label for the launch variant this mode produces, e.g.
40
+ * `Skip Permissions`. Boolean modes only; absent means settings-only. */
41
+ menuLabel?: string;
42
+ }
43
+ /** Everything the core needs to know about one assistant. Pure data - no
44
+ * JupyterLab imports - so the settings-schema generator can read it under
45
+ * plain Node from the compiled barrel. */
46
+ export interface IProviderDescriptor {
47
+ /** Stable id. Drives the route namespace, widget id, command namespace,
48
+ * settings keys and localStorage keys. */
49
+ id: string;
50
+ /** Display label, e.g. used in settings and dialogs. */
51
+ label: string;
52
+ /** Panel title and tab caption. */
53
+ panelTitle: string;
54
+ /** LabIcon name for the panel icon. Registered by the core registry. */
55
+ iconName: string;
56
+ /** SVG source for that icon. Kept as a string so the descriptor carries no
57
+ * JupyterLab dependency; the registry builds the LabIcon. */
58
+ iconSvg: string;
59
+ /** Binary probed on PATH server-side. Absence disables this provider only. */
60
+ cliBinary: string;
61
+ forkStrategy: ForkStrategy;
62
+ colourSource: ColourSource;
63
+ namingStrategy: NamingStrategy;
64
+ /** The CLI starts a brand-new conversation under an id we give it, so a
65
+ * fresh session is identifiable from its argv on the first poll instead of
66
+ * being an unknown terminal until it writes its store entry. Independent of
67
+ * `forkStrategy` - an assistant can accept an id for a new session and still
68
+ * have no fork verb at all. */
69
+ mintsNewSessionId: boolean;
70
+ launchModes: ILaunchMode[];
71
+ /** Sessions can be held under remote control - renders the indicator dot. */
72
+ hasRemoteControl: boolean;
73
+ /** Sessions can be owned by a live background agent - renders the chip and
74
+ * asks the branch endpoint for agent state. */
75
+ hasBgAgents: boolean;
76
+ /** Projects can show a live-process indicator. */
77
+ hasLiveProcess: boolean;
78
+ /** JupyterLab plugin id of the retired standalone extension this provider
79
+ * replaces, without the `:plugin` suffix. Present only for a provider that
80
+ * had one: if that plugin is still installed the core suppresses this
81
+ * provider's panel rather than docking a second one for the same assistant.
82
+ */
83
+ legacyPluginId?: string;
84
+ }
85
+ /** One project row. Fields past `extra_sessions` are capability-gated: a
86
+ * provider whose descriptor does not claim the capability simply omits them,
87
+ * and the panel renders nothing for them. */
88
+ export interface ISession {
89
+ project_path: string;
90
+ /** The provider's own project key. Opaque to the core - it is only ever
91
+ * echoed back to that provider's routes. */
92
+ encoded_path: string;
93
+ session_id: string;
94
+ name: string;
95
+ name_source: 'session' | 'basename';
96
+ message_count: number;
97
+ /** Last activity, epoch milliseconds. */
98
+ file_mtime: number;
99
+ git_branch: string | null;
100
+ favourite: boolean;
101
+ /** Conversations of this project beyond the current one. */
102
+ extra_sessions: number;
103
+ summary?: string;
104
+ first_prompt?: string;
105
+ created?: string | number | null;
106
+ modified?: string | number | null;
107
+ /** The conversation's own colour, for `colourSource: 'native'` providers.
108
+ * One of the six colour ids, or null. */
109
+ colour?: string | null;
110
+ /** Remote control is active on this conversation (`hasRemoteControl`). */
111
+ remote_control?: boolean;
112
+ /** Short id of the live background agent holding this conversation
113
+ * (`hasBgAgents`), or null. Display only - the server picks the launch verb
114
+ * itself, where it cannot be stale. */
115
+ bg_id?: string | null;
116
+ /** A process of this assistant is running in this project
117
+ * (`hasLiveProcess`). */
118
+ live?: boolean;
119
+ /** Model the conversation last ran on, when the store records one. */
120
+ model?: string | null;
121
+ tokens_used?: number;
122
+ }
123
+ export interface ISessionsListResponse {
124
+ sessions: ISession[];
125
+ }
126
+ /** One registered provider as the server sees it. */
127
+ export interface IProviderStatus {
128
+ id: string;
129
+ label: string;
130
+ /** The `providers.<id>.enabled` setting as the server last read it. */
131
+ enabled: boolean;
132
+ cli_path: string | null;
133
+ /** Enabled AND binary present - the only state that docks a panel. */
134
+ available: boolean;
135
+ }
136
+ export interface IStatusResponse {
137
+ root_dir: string;
138
+ providers: IProviderStatus[];
139
+ /** JupyterLab's own move-to-trash setting, read server-side, so dialogs say
140
+ * which of the two a click means rather than reciting both. */
141
+ delete_to_trash?: boolean;
142
+ }
143
+ export interface IFavouriteRequest {
144
+ project_path: string;
145
+ favourite: boolean;
146
+ }
147
+ export interface IFavouriteResponse {
148
+ favourites: string[];
149
+ }
150
+ /** Destructive endpoints answer with the count AND the ids that actually went,
151
+ * because a shortfall is not a failure: whatever was disposed of is gone, so
152
+ * the count has to reach the user - and only the ids named here may have their
153
+ * stored colour dropped, or a conversation whose deletion was refused loses its
154
+ * tint. */
155
+ export interface IDisposalReport {
156
+ removed_count: number;
157
+ removed_ids: string[];
158
+ }
159
+ export type IRemoveResponse = IDisposalReport;
160
+ export type ICleanupResponse = IDisposalReport;
161
+ export type IDeleteSessionsResponse = IDisposalReport;
162
+ export interface IBranch {
163
+ session_id: string;
164
+ file_mtime: number;
165
+ label: string;
166
+ /** Live background agent owning this branch (`hasBgAgents`), or null.
167
+ * Populated only when the fetch asked for it. */
168
+ bg_id?: string | null;
169
+ }
170
+ export interface IBranchesResponse {
171
+ current: string;
172
+ total: number;
173
+ branches: IBranch[];
174
+ }
175
+ export interface ISwitchResponse {
176
+ requested: string;
177
+ current: string | null;
178
+ }
179
+ export interface IForkRequest {
180
+ encoded_path: string;
181
+ session_id: string;
182
+ name?: string;
183
+ }
184
+ export interface IForkResponse {
185
+ session_id: string;
186
+ }
187
+ export interface ILaunchRequest {
188
+ project_path: string;
189
+ /** The project's store key. Carrying it arms the server's launch-time pin
190
+ * bookkeeping (a native-flag fork pins itself current, a new session clears
191
+ * a stale pin) and the session_not_found pre-flight - without it both are
192
+ * silently skipped. Send it whenever the session row is in hand. */
193
+ encoded_path?: string;
194
+ /** Conversation to open. Omit to start a brand-new one. */
195
+ session_id?: string;
196
+ /** Frontend-minted id for a brand-new conversation, when the CLI accepts
197
+ * one, so the terminal is identifiable from its argv on the first poll. */
198
+ new_session_id?: string;
199
+ /** Frontend-minted id for the fork (`forkStrategy: 'native-flag'`). */
200
+ fork_session_id?: string;
201
+ /** Parent conversation to fork from (`forkStrategy: 'native-command'`). */
202
+ fork_from?: string;
203
+ /** Name to stamp on the new conversation (`namingStrategy: 'launch-flag'`). */
204
+ name?: string;
205
+ /** The launch mode this action runs under, as one token in the assistant's
206
+ * own terminology: a boolean mode is its bare `ILaunchMode.id`, an enum mode
207
+ * is `<id>=<value>`. One token, not a set - the server maps a single mode to
208
+ * a single flag, and refuses any token the descriptor does not declare with
209
+ * 400 `mode_unsupported`. */
210
+ mode?: string;
211
+ }
212
+ export interface ILaunchResponse {
213
+ terminal_name: string;
214
+ }
215
+ /** What a terminal is running, as the server resolved it from `/proc`. */
216
+ export interface ITerminalProbeResponse {
217
+ terminal_name: string;
218
+ /** The assistant's binary is the pty's process. */
219
+ running: boolean;
220
+ /** Conversation the running process holds, or null when unreadable. */
221
+ session_id?: string | null;
222
+ /** That conversation's own colour (`colourSource: 'native'`). */
223
+ colour?: string | null;
224
+ /** Working directories read from `/proc`, never from `PWD`. */
225
+ cwds?: string[];
226
+ }
227
+ /** What one provider carried over from its retired standalone extension.
228
+ * `keys` are settings this extension is to apply through the settings registry,
229
+ * which the server has no writable handle on. */
230
+ export interface IMigratedProvider {
231
+ provider_id: string;
232
+ keys: Record<string, boolean | number | string>;
233
+ favourites: string[];
234
+ }
235
+ /** `POST migrate`. Idempotent - a second call reports an empty list. */
236
+ export interface IMigrateResponse {
237
+ migrated: IMigratedProvider[];
238
+ }
239
+ /** The extension's own per-provider colour store: user-set tab colours keyed
240
+ * by conversation id. */
241
+ export interface IColourStoreResponse {
242
+ colours: Record<string, string>;
243
+ }
244
+ /** Everything a provider hook is handed about the row it is describing. */
245
+ export interface IHookContext {
246
+ session: ISession;
247
+ descriptor: IProviderDescriptor;
248
+ }
249
+ /** Optional behaviour a provider module supplies on top of its descriptor.
250
+ * Every hook is optional - a provider that supplies none renders the plain
251
+ * shared panel. */
252
+ export interface IProviderHooks {
253
+ /** Label for the primary open action, when the provider distinguishes verbs
254
+ * (joining a live worker is not the same as resuming a dormant store entry). */
255
+ resumeLabel?: (session: ISession | null) => string;
256
+ /** Extra lines for the row tooltip. */
257
+ tooltipLines?: (ctx: IHookContext) => string[];
258
+ /** Menu label for one branch. Defaults to `<label> (<short id>) - <time>`;
259
+ * a provider whose titles are written in wide scripts overrides it to bound
260
+ * the menu by display columns rather than by code units. */
261
+ branchLabel?: (branch: IBranch, shortId: string) => string;
262
+ /** Chip appended to a branch row in the menu and the popup. */
263
+ branchBadge?: (branch: IBranch) => HTMLElement | null;
264
+ }
265
+ /** What `providers/index.ts` registers: one descriptor, optionally with hooks. */
266
+ export interface IProviderModule {
267
+ descriptor: IProviderDescriptor;
268
+ hooks?: IProviderHooks;
269
+ }
@@ -0,0 +1,6 @@
1
+ // Shared vocabulary of the provider core. Nothing here names an assistant:
2
+ // every divergence between the registered providers is expressed as a
3
+ // capability flag on the descriptor, or as an optional hook the provider
4
+ // module supplies. Adding an assistant adds a descriptor, never a branch in
5
+ // core code.
6
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ declare const plugin: JupyterFrontEndPlugin<void>;
3
+ export default plugin;
package/lib/index.js ADDED
@@ -0,0 +1,277 @@
1
+ import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
2
+ import { Notification } from '@jupyterlab/apputils';
3
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
4
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
5
+ import { ITerminalTracker } from '@jupyterlab/terminal';
6
+ import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
7
+ import { AssistantSessionsPanel, commandId, panelWidgetId } from './core/panel';
8
+ import { LOG_PREFIX, ProviderRegistry } from './core/registry';
9
+ import { requestAPI } from './core/request';
10
+ import { PROVIDERS } from './providers';
11
+ const PLUGIN_ID = 'jupyterlab_ai_code_assistants_extension:plugin';
12
+ /** Sidebar rank, shared by every panel so they dock together. */
13
+ const PANEL_RANK = 600;
14
+ /** How often the server is re-asked which assistants are available, so a CLI
15
+ * installed while JupyterLab runs surfaces without a reload. */
16
+ const STATUS_INTERVAL_MS = 60000;
17
+ const plugin = {
18
+ id: PLUGIN_ID,
19
+ description: 'One side panel per AI code assistant, over a shared provider registry: sessions, favourites, branching and one-click resume in a terminal.',
20
+ autoStart: true,
21
+ requires: [ILabShell],
22
+ optional: [
23
+ ILayoutRestorer,
24
+ ISettingRegistry,
25
+ ITerminalTracker,
26
+ IDefaultFileBrowser,
27
+ IColourfulTabs
28
+ ],
29
+ activate: async (app, labShell, restorer, settingRegistry, terminalTracker, fileBrowser, colourfulTabs) => {
30
+ console.log('JupyterLab extension jupyterlab_ai_code_assistants_extension is activated!');
31
+ // A duplicate id throws here, at registration, rather than surfacing later
32
+ // as two panels quietly fighting over one widget id.
33
+ const registry = new ProviderRegistry(PROVIDERS);
34
+ const serverSettings = app.serviceManager.serverSettings;
35
+ const live = new Map();
36
+ const warnedUnknown = new Set();
37
+ const warnedUnavailable = new Set();
38
+ // Assistants whose retired standalone extension is STILL installed. This
39
+ // extension supersedes those three, and running both would give the user
40
+ // two panels and two command sets for one assistant - so the duplicate is
41
+ // resolved in favour of the standalone one already on screen and this
42
+ // provider stays dark: never started, so it never polls and never registers
43
+ // a command. Resolved once, at activation: a plugin cannot be installed
44
+ // into a running JupyterLab.
45
+ const suppressed = new Set();
46
+ for (const descriptor of registry.descriptors) {
47
+ if (descriptor.legacyPluginId &&
48
+ app.hasPlugin(`${descriptor.legacyPluginId}:plugin`)) {
49
+ suppressed.add(descriptor.id);
50
+ }
51
+ }
52
+ if (suppressed.size > 0) {
53
+ const packages = registry.descriptors
54
+ .filter(d => suppressed.has(d.id))
55
+ .map(d => d.legacyPluginId)
56
+ .join(', ');
57
+ console.warn(`${LOG_PREFIX} superseded extension(s) still installed: ${packages}. Their assistants are suppressed here.`);
58
+ // Once, not on every refresh: this runs a single time per page load.
59
+ Notification.warning(`AI Code Assistants: uninstall ${packages} - it is superseded by this extension, and until it goes its assistant has no panel here.`, { autoClose: false });
60
+ }
61
+ let status = null;
62
+ let settings = null;
63
+ let sidebar = 'right';
64
+ /** Ask the server which providers exist and which of their binaries are on
65
+ * PATH. A failure leaves the extension activated with no panels - never a
66
+ * dialog, and never a thrown activation. */
67
+ const probeStatus = async () => {
68
+ try {
69
+ status = await requestAPI('status', serverSettings, {
70
+ cache: 'no-store'
71
+ });
72
+ }
73
+ catch (err) {
74
+ console.warn(`${LOG_PREFIX} status probe failed; no assistant panels will be docked.`, err);
75
+ status = null;
76
+ }
77
+ };
78
+ const statusFor = (id) => { var _a, _b; return (_b = (_a = status === null || status === void 0 ? void 0 : status.providers) === null || _a === void 0 ? void 0 : _a.find(p => p.id === id)) !== null && _b !== void 0 ? _b : null; };
79
+ /** Settings value, or the fallback when settings are unavailable or the
80
+ * key is absent. An absent enable key reads as enabled - only an explicit
81
+ * `false` turns a provider off. */
82
+ const setting = (key, fallback) => {
83
+ if (!settings) {
84
+ return fallback;
85
+ }
86
+ const value = settings.get(key).composite;
87
+ if (value === undefined || value === null) {
88
+ return fallback;
89
+ }
90
+ return value;
91
+ };
92
+ /** Launch-mode values for one provider, keyed by the mode ids the
93
+ * assistant itself uses. */
94
+ const modesFor = (id) => {
95
+ var _a, _b;
96
+ const descriptor = (_a = registry.get(id)) === null || _a === void 0 ? void 0 : _a.descriptor;
97
+ const modes = {};
98
+ for (const mode of (_b = descriptor === null || descriptor === void 0 ? void 0 : descriptor.launchModes) !== null && _b !== void 0 ? _b : []) {
99
+ modes[mode.id] = setting(`providers.${id}.${mode.id}`, mode.default);
100
+ }
101
+ return modes;
102
+ };
103
+ /** Push the shared settings into one panel. */
104
+ const applySharedSettings = (panel) => {
105
+ // Any value other than `path` is treated as `name`, so an older saved
106
+ // value still maps onto a live mode.
107
+ const mode = setting('presentationMode', 'name') === 'path'
108
+ ? 'path'
109
+ : 'name';
110
+ panel.setPresentationMode(mode);
111
+ const limit = setting('recentLimit', 10);
112
+ if (typeof limit === 'number') {
113
+ panel.setRecentLimit(limit);
114
+ }
115
+ // Defaults ON, so only an explicit `false` turns it off.
116
+ panel.setColouredTabs(setting('colouredTabs', true) !== false);
117
+ panel.setModes(modesFor(panel.descriptor.id));
118
+ };
119
+ const dock = (panel) => {
120
+ // Lumino re-parents cleanly when add() is called against a different
121
+ // area, so the same call both docks and moves.
122
+ labShell.add(panel, sidebar, { rank: PANEL_RANK });
123
+ };
124
+ const start = (id) => {
125
+ var _a;
126
+ const module = registry.get(id);
127
+ if (!module || live.has(id)) {
128
+ return;
129
+ }
130
+ try {
131
+ const panel = new AssistantSessionsPanel({
132
+ app,
133
+ descriptor: module.descriptor,
134
+ hooks: module.hooks,
135
+ rootDir: (_a = status === null || status === void 0 ? void 0 : status.root_dir) !== null && _a !== void 0 ? _a : '',
136
+ deleteToTrash: (status === null || status === void 0 ? void 0 : status.delete_to_trash) !== false,
137
+ terminalTracker,
138
+ fileBrowser,
139
+ colourfulTabs
140
+ });
141
+ applySharedSettings(panel);
142
+ dock(panel);
143
+ // Each panel registers under its own name, so its open/closed state
144
+ // survives a reload independently of the others.
145
+ restorer === null || restorer === void 0 ? void 0 : restorer.add(panel, panelWidgetId(id));
146
+ const command = app.commands.addCommand(commandId(id, 'refresh'), {
147
+ label: `Refresh ${module.descriptor.panelTitle}`,
148
+ execute: () => panel.refresh()
149
+ });
150
+ live.set(id, { panel, command });
151
+ }
152
+ catch (err) {
153
+ // One provider failing to start must leave the others working.
154
+ console.error(`${LOG_PREFIX} provider "${id}" failed to start.`, err);
155
+ }
156
+ };
157
+ const stop = (id) => {
158
+ const entry = live.get(id);
159
+ if (!entry) {
160
+ return;
161
+ }
162
+ live.delete(id);
163
+ // Disposing stops the polling and clears this provider's tab tints; the
164
+ // terminals it launched keep running, untouched.
165
+ entry.command.dispose();
166
+ entry.panel.dispose();
167
+ };
168
+ /** Bring the docked panels in line with settings and CLI availability.
169
+ * Both must hold for a panel to be present. */
170
+ const reconcile = () => {
171
+ var _a, _b;
172
+ for (const descriptor of registry.descriptors) {
173
+ const id = descriptor.id;
174
+ if (suppressed.has(id)) {
175
+ continue;
176
+ }
177
+ const enabled = setting(`providers.${id}.enabled`, true) !== false;
178
+ const probe = statusFor(id);
179
+ const available = probe ? probe.available !== false : false;
180
+ // Enabled but binary absent must not be a silent state: the user sees
181
+ // a missing panel with no clue that PATH under the Jupyter server
182
+ // differs from their shell. Once per id, not per reconcile.
183
+ if (enabled && !available && !warnedUnavailable.has(id)) {
184
+ warnedUnavailable.add(id);
185
+ const binary = (_b = (_a = registry.get(id)) === null || _a === void 0 ? void 0 : _a.descriptor.cliBinary) !== null && _b !== void 0 ? _b : id;
186
+ console.info(`${LOG_PREFIX} "${id}" is enabled but its \`${binary}\` binary was not found on the server's PATH; panel not shown.`);
187
+ }
188
+ if (enabled && available) {
189
+ if (live.has(id)) {
190
+ applySharedSettings(live.get(id).panel);
191
+ }
192
+ else {
193
+ start(id);
194
+ }
195
+ }
196
+ else {
197
+ stop(id);
198
+ }
199
+ }
200
+ // A provider still named in saved settings but no longer registered is
201
+ // inert. Warned once per id, not once per reconcile - this runs on every
202
+ // status poll, and a warning that repeats forever is noise.
203
+ for (const key of settings ? Object.keys(settings.composite) : []) {
204
+ const match = /^providers\.([^.]+)\.enabled$/.exec(key);
205
+ if (match && !registry.has(match[1]) && !warnedUnknown.has(match[1])) {
206
+ warnedUnknown.add(match[1]);
207
+ console.warn(`${LOG_PREFIX} settings name unknown assistant "${match[1]}" - ignored.`);
208
+ }
209
+ }
210
+ };
211
+ /** Carry favourites and settings over from the retired standalone
212
+ * extensions, once, before the first panel is docked.
213
+ *
214
+ * The server writes the favourites itself and hands back the mapped
215
+ * settings keys, because it has no writable handle on the settings
216
+ * registry. It is idempotent - a provider is marked migrated and skipped
217
+ * forever after - so calling it on every activation costs one request and
218
+ * migrates nothing the second time. A failure is a warning and nothing
219
+ * more: losing a carried-over favourite must never cost the user a panel.
220
+ *
221
+ * Skipped entirely while the settings registry is unavailable: the server
222
+ * marks a provider migrated in the same call that HANDS BACK its settings
223
+ * keys, so migrating with nowhere to apply them would drop a carried-over
224
+ * value permanently - the marker blocks every retry.
225
+ */
226
+ const migrate = async () => {
227
+ var _a, _b;
228
+ if (!settings) {
229
+ return;
230
+ }
231
+ try {
232
+ const result = await requestAPI('migrate', serverSettings, { method: 'POST', body: '{}' });
233
+ for (const entry of (_a = result.migrated) !== null && _a !== void 0 ? _a : []) {
234
+ for (const [key, value] of Object.entries((_b = entry.keys) !== null && _b !== void 0 ? _b : {})) {
235
+ await (settings === null || settings === void 0 ? void 0 : settings.set(key, value));
236
+ }
237
+ }
238
+ }
239
+ catch (err) {
240
+ console.warn(`${LOG_PREFIX} migration from the retired extensions failed; nothing was carried over.`, err);
241
+ }
242
+ };
243
+ const applySidebar = () => {
244
+ const next = setting('sidebar', 'right');
245
+ if ((next === 'left' || next === 'right') && next !== sidebar) {
246
+ sidebar = next;
247
+ live.forEach(entry => dock(entry.panel));
248
+ }
249
+ };
250
+ await probeStatus();
251
+ if (settingRegistry) {
252
+ try {
253
+ settings = await settingRegistry.load(PLUGIN_ID);
254
+ sidebar =
255
+ setting('sidebar', 'right') === 'left' ? 'left' : 'right';
256
+ settings.changed.connect(() => {
257
+ applySidebar();
258
+ reconcile();
259
+ });
260
+ }
261
+ catch (err) {
262
+ // Defaults apply: every provider enabled, right sidebar.
263
+ console.warn(`${LOG_PREFIX} failed to load settings; using defaults.`, err);
264
+ settings = null;
265
+ }
266
+ }
267
+ await migrate();
268
+ reconcile();
269
+ // Re-probe on a slow cadence so a binary installed (or removed) while
270
+ // JupyterLab runs takes effect on the next refresh, without a reload.
271
+ window.setInterval(() => {
272
+ void probeStatus().then(reconcile);
273
+ }, STATUS_INTERVAL_MS);
274
+ console.log(`${LOG_PREFIX} ${live.size} of ${registry.ids.length} assistant panel(s) docked.`);
275
+ }
276
+ };
277
+ export default plugin;
@@ -0,0 +1,3 @@
1
+ import { IProviderDescriptor, IProviderHooks } from '../core/types';
2
+ export declare const descriptor: IProviderDescriptor;
3
+ export declare const hooks: IProviderHooks;
@@ -0,0 +1,88 @@
1
+ // The Claude Code provider.
2
+ //
3
+ // Ported from `jupyterlab_claude_code_extension` v1.2.73, the architectural
4
+ // base. Almost all of that extension's behaviour is now the shared core, and
5
+ // what is left here is what only Claude has: the capability flags on the
6
+ // descriptor, and the two places where "a live background agent holds this
7
+ // conversation" changes what the user is offered.
8
+ //
9
+ // Nothing here touches the DOM outside its own badge element, and nothing here
10
+ // reaches into core internals - the hooks are the whole seam.
11
+ // Claude's own mark, drawn as a single path group so the theme's `jp-icon3`
12
+ // fill drives it in both light and dark.
13
+ const claudeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
14
+ <g class="jp-icon3" fill="#616161">
15
+ <path d="m14.375 6.48.49.28v.209l-.14.489-5.937 1.397-.558-1.387zm0 0"/>
16
+ <path d="m12.155 2.373.683.143.182.224.173.535-.072.342-3.983 5.447L7.81 7.737l3.673-4.82z"/>
17
+ <path d="m8.719 1.522.419-.28.349.14.349.49-.957 5.748-.65-.441-.279-.769.49-4.33z"/>
18
+ <path d="m4.239 1.614.43-.55L4.95 1l.558.081.275.216 2.004 4.442.724 2.11-.848.471-3.231-5.864z"/>
19
+ <path d="m2.154 4.665-.14-.56.42-.488.488.07h.14l2.933 2.165.908.698 1.257.978-.698 1.187-.629-.489-.419-.419-4.05-2.863z"/>
20
+ <path d="M1.316 8.296 1 7.946v-.31l.316-.108 3.562.21 3.491.279-.113.695-6.66-.346z"/>
21
+ <path d="M3.411 11.931h-.698l-.278-.32v-.382l1.186-.838 4.82-3.068.487.833z"/>
22
+ <path d="m4.738 13.883-.28.07-.418-.21.07-.35 4.12-5.446.558.768-3.072 4.05z"/>
23
+ <path d="m8.23 14.581-.21.28-.419.14-.349-.28-.21-.42L8.09 8.646l.629.07z"/>
24
+ <path d="M11.791 13.045v.558l-.07.21-.279.14-.489-.066-3.356-4.996 1.331-1.014 1.117 2.025.105.733z"/>
25
+ <path d="m13.398 12.207.07.349-.21.279-.21-.07-1.187-.838-1.815-1.606-1.397-.978.419-1.326.698.419.42.768z"/>
26
+ <path d="m12.49 8.645 1.746.14.419.28.279.418v.302l-.768.327-3.911-.978-1.606-.07.419-1.466 1.117.838z"/>
27
+ </g>
28
+ </svg>`;
29
+ export const descriptor = {
30
+ id: 'claude',
31
+ label: 'Claude Code',
32
+ panelTitle: 'Claude Code Sessions',
33
+ iconName: 'jupyterlab_ai_code_assistants_extension:claude',
34
+ iconSvg: claudeSvgStr,
35
+ cliBinary: 'claude',
36
+ // `claude --fork-session --session-id <uuid>`: the CLI forks in-process and
37
+ // takes the new id from us, so the fork is known before it exists - and its
38
+ // store entry is written lazily, on the first turn, which is what the core's
39
+ // branch watcher waits for.
40
+ forkStrategy: 'native-flag',
41
+ // `/color` is Claude's own surface for a conversation's colour, recorded in
42
+ // its transcript, so the assistant owns the tint and the extension's
43
+ // write-back store stays out of it.
44
+ colourSource: 'native',
45
+ // `-n <name>` at launch: Claude stamps the name itself and re-stamps it on
46
+ // every turn, which is the only way a fork's name survives - a title written
47
+ // after the fact loses to the parent title the fork inherits.
48
+ namingStrategy: 'launch-flag',
49
+ mintsNewSessionId: true,
50
+ launchModes: [
51
+ {
52
+ id: 'dangerouslySkipPermissions',
53
+ title: 'Dangerously skip permissions',
54
+ description: 'When enabled, newly spawned and resumed sessions are launched with `--dangerously-skip-permissions`. Bypasses every Claude Code permission prompt - only enable in sandboxes you trust.',
55
+ kind: 'boolean',
56
+ default: false,
57
+ menuLabel: 'Skip Permissions'
58
+ }
59
+ ],
60
+ hasRemoteControl: true,
61
+ hasBgAgents: true,
62
+ // Claude records the conversation a process is on, so a terminal is
63
+ // identified exactly; there is no per-project "something is running here"
64
+ // signal to render.
65
+ hasLiveProcess: false,
66
+ // Retired standalone extension this provider replaces.
67
+ legacyPluginId: 'jupyterlab_claude_code_extension'
68
+ };
69
+ /** The `bg` chip: this conversation belongs to a live background agent, so
70
+ * opening it joins the running agent instead of resuming a dormant transcript.
71
+ * Display only - the server picks the verb at launch, where it cannot be
72
+ * stale. */
73
+ function bgChip() {
74
+ const chip = document.createElement('span');
75
+ chip.className = 'jp-AiAssistantsPanel-bgBadge';
76
+ chip.textContent = 'bg';
77
+ return chip;
78
+ }
79
+ export const hooks = {
80
+ // Joining a live agent and resuming a dormant conversation are different
81
+ // verbs, so the item says which one this row gets before the click.
82
+ resumeLabel: (session) => (session === null || session === void 0 ? void 0 : session.bg_id) ? 'Attach to Background Agent' : 'Resume',
83
+ // In the manage-sessions popup the chip replaces the fork badge for an
84
+ // agent-held conversation: both are identity hints, and the one that changes
85
+ // what a click does is the one worth the column. Returning null elsewhere
86
+ // leaves the shared fork badge in place.
87
+ branchBadge: (branch) => (branch.bg_id ? bgChip() : null)
88
+ };
@@ -0,0 +1,3 @@
1
+ import { IProviderDescriptor, IProviderHooks } from '../core/types';
2
+ export declare const descriptor: IProviderDescriptor;
3
+ export declare const hooks: IProviderHooks;