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.
- package/LICENSE +29 -0
- package/README.md +44 -0
- package/lib/core/colour.d.ts +57 -0
- package/lib/core/colour.js +190 -0
- package/lib/core/icons.d.ts +12 -0
- package/lib/core/icons.js +99 -0
- package/lib/core/modes.d.ts +13 -0
- package/lib/core/modes.js +36 -0
- package/lib/core/panel.d.ts +233 -0
- package/lib/core/panel.js +1673 -0
- package/lib/core/popup.d.ts +40 -0
- package/lib/core/popup.js +404 -0
- package/lib/core/registry.d.ts +24 -0
- package/lib/core/registry.js +48 -0
- package/lib/core/request.d.ts +25 -0
- package/lib/core/request.js +64 -0
- package/lib/core/terminals.d.ts +129 -0
- package/lib/core/terminals.js +344 -0
- package/lib/core/types.d.ts +269 -0
- package/lib/core/types.js +6 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +277 -0
- package/lib/providers/claude.d.ts +3 -0
- package/lib/providers/claude.js +88 -0
- package/lib/providers/codex.d.ts +3 -0
- package/lib/providers/codex.js +79 -0
- package/lib/providers/gemini.d.ts +2 -0
- package/lib/providers/gemini.js +73 -0
- package/lib/providers/index.d.ts +3 -0
- package/lib/providers/index.js +16 -0
- package/lib/providers/kimi.d.ts +23 -0
- package/lib/providers/kimi.js +139 -0
- package/package.json +169 -0
- package/schema/plugin.json +93 -0
- package/src/__tests__/colour.spec.ts +153 -0
- package/src/__tests__/core-neutrality.spec.ts +86 -0
- package/src/__tests__/labels.spec.ts +89 -0
- package/src/__tests__/launch-mode.spec.ts +53 -0
- package/src/__tests__/registry.spec.ts +178 -0
- package/src/core/colour.ts +224 -0
- package/src/core/icons.ts +121 -0
- package/src/core/modes.ts +45 -0
- package/src/core/panel.ts +2020 -0
- package/src/core/popup.ts +475 -0
- package/src/core/registry.ts +60 -0
- package/src/core/request.ts +96 -0
- package/src/core/terminals.ts +446 -0
- package/src/core/types.ts +305 -0
- package/src/index.ts +356 -0
- package/src/providers/claude.ts +102 -0
- package/src/providers/codex.ts +89 -0
- package/src/providers/gemini.ts +80 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/kimi.ts +155 -0
- package/style/base.css +888 -0
- package/style/index.css +1 -0
- 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
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,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
|
+
};
|