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,224 @@
1
+ // Terminal tab tint per conversation.
2
+ //
3
+ // Effective colour, highest wins:
4
+ // 1. user-set - the write-back store this module keeps, per provider
5
+ // 2. native - the conversation's own colour, when the assistant has one
6
+ // 3. derived - a stable hash of the conversation id
7
+ // 4. none - no tint
8
+ //
9
+ // Every step of that ladder is pure and exported on its own, because the tint
10
+ // is the part of this panel that regressed twice behind a green suite that
11
+ // never ran the logic.
12
+
13
+ import { ServerConnection } from '@jupyterlab/services';
14
+
15
+ import { requestProvider } from './request';
16
+ import { ColourSource, IColourStoreResponse, ISession } from './types';
17
+
18
+ /** The colour vocabulary of `jupyterlab_colourful_tab_extension`, in its own
19
+ * order - that extension owns the tab CSS and these six ids; this module only
20
+ * feeds it one of them. The order is load-bearing: its localStorage records a
21
+ * user's menu choice as an INDEX into this list. */
22
+ export const TAB_COLOUR_IDS: readonly string[] = [
23
+ 'rose',
24
+ 'peach',
25
+ 'lemon',
26
+ 'mint',
27
+ 'sky',
28
+ 'lavender'
29
+ ];
30
+
31
+ /** Key `jupyterlab_colourful_tab_extension` persists menu-set tab colours
32
+ * under, and the `terminal:<session name>` shape of its entries. Read only -
33
+ * this module never writes into another extension's storage. */
34
+ const TAB_COLOUR_STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
35
+
36
+ /** Map a conversation id onto one of the six colour ids. FNV-1a (32-bit) - a
37
+ * stable string hash with good avalanche on short hex-ish ids; `Math.imul`
38
+ * keeps the multiplication in 32-bit integer semantics. The same id always
39
+ * yields the same colour, on every reload and in every browser. */
40
+ export function fnv1aColour(sessionId: string): string {
41
+ let hash = 0x811c9dc5; // FNV offset basis
42
+ for (let i = 0; i < sessionId.length; i++) {
43
+ hash ^= sessionId.charCodeAt(i);
44
+ hash = Math.imul(hash, 0x01000193); // FNV prime
45
+ }
46
+ return TAB_COLOUR_IDS[(hash >>> 0) % TAB_COLOUR_IDS.length];
47
+ }
48
+
49
+ /** Colour the user set by hand on a terminal tab, or null.
50
+ *
51
+ * The colourful-tab extension stores its right-click menu choices as
52
+ * `{ "terminal:<session name>": <index into TAB_COLOUR_IDS> }`. Reading it is
53
+ * what makes "set the colour on the tab" register as the conversation's
54
+ * colour for assistants whose CLI has no colour concept at all. */
55
+ export function readUserSetTabColour(terminalName: string): string | null {
56
+ try {
57
+ const raw = window.localStorage.getItem(TAB_COLOUR_STORAGE_KEY);
58
+ if (!raw) {
59
+ return null;
60
+ }
61
+ const parsed = JSON.parse(raw) as Record<string, number>;
62
+ const index = parsed[`terminal:${terminalName}`];
63
+ if (typeof index !== 'number') {
64
+ return null;
65
+ }
66
+ return TAB_COLOUR_IDS[index] ?? null;
67
+ } catch (_err) {
68
+ // localStorage unavailable, or another extension's schema changed under
69
+ // us - no user colour is a valid answer, a thrown error is not.
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /** Resolve the ladder for one conversation. `userSet` is this extension's own
75
+ * store, `native` the conversation's own colour (only ever populated by a
76
+ * `native` provider). */
77
+ export function effectiveColour(
78
+ sessionId: string | null,
79
+ source: ColourSource,
80
+ userSet: string | null,
81
+ native: string | null | undefined
82
+ ): string | null {
83
+ if (userSet) {
84
+ return userSet;
85
+ }
86
+ if (source === 'native') {
87
+ return native ?? null;
88
+ }
89
+ if (source === 'derived' && sessionId) {
90
+ return fnv1aColour(sessionId);
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /** Fall back from an unreadable conversation to the project a terminal sits
96
+ * in: longest matching path wins, so a nested project beats its parent.
97
+ * Never the first resort - a row carries only its representative
98
+ * conversation, so resolving colour by row tints every terminal of a project
99
+ * with the representative's colour. */
100
+ export function sessionForCwds(
101
+ cwds: string[],
102
+ sessions: ISession[]
103
+ ): ISession | null {
104
+ let best: ISession | null = null;
105
+ let bestLen = -1;
106
+ for (const raw of cwds) {
107
+ const cwd = raw.replace(/\/+$/, '');
108
+ for (const s of sessions) {
109
+ const p = s.project_path.replace(/\/+$/, '');
110
+ if ((cwd === p || cwd.startsWith(p + '/')) && p.length > bestLen) {
111
+ best = s;
112
+ bestLen = p.length;
113
+ }
114
+ }
115
+ }
116
+ return best;
117
+ }
118
+
119
+ /**
120
+ * Client for one provider's user-set colour store.
121
+ *
122
+ * The store is server-side and per provider (`providers/<id>/colours`), so
123
+ * colours survive a reload and one assistant's colours can never be read or
124
+ * written through another's routes. Reads are cached in memory and refreshed
125
+ * with the session poll; writes go straight through and update the cache.
126
+ */
127
+ export class ColourStore {
128
+ constructor(providerId: string, serverSettings: ServerConnection.ISettings) {
129
+ this._providerId = providerId;
130
+ this._serverSettings = serverSettings;
131
+ }
132
+
133
+ /** User-set colour for a conversation, from the in-memory cache. */
134
+ get(sessionId: string | null | undefined): string | null {
135
+ if (!sessionId) {
136
+ return null;
137
+ }
138
+ return this._colours.get(sessionId) ?? null;
139
+ }
140
+
141
+ /** Reload the whole store. Failures leave the previous cache in place - a
142
+ * momentarily unreachable server must not clear every user colour. */
143
+ async load(): Promise<void> {
144
+ try {
145
+ const data = await requestProvider<IColourStoreResponse>(
146
+ this._providerId,
147
+ 'colours',
148
+ this._serverSettings,
149
+ { cache: 'no-store' }
150
+ );
151
+ const next = new Map<string, string>();
152
+ for (const [id, colour] of Object.entries(data.colours ?? {})) {
153
+ if (typeof colour === 'string' && colour) {
154
+ next.set(id, colour);
155
+ }
156
+ }
157
+ this._colours = next;
158
+ } catch (_err) {
159
+ // Keep the cache; the next poll retries.
160
+ }
161
+ }
162
+
163
+ /** Record a colour as this conversation's own. Passing null drops the entry,
164
+ * which is also how a deleted conversation leaves no orphan key behind. */
165
+ async set(sessionId: string, colour: string | null): Promise<void> {
166
+ if (!sessionId) {
167
+ return;
168
+ }
169
+ if (colour) {
170
+ this._colours.set(sessionId, colour);
171
+ } else {
172
+ this._colours.delete(sessionId);
173
+ }
174
+ try {
175
+ await requestProvider<IColourStoreResponse>(
176
+ this._providerId,
177
+ 'colours',
178
+ this._serverSettings,
179
+ {
180
+ method: 'POST',
181
+ body: JSON.stringify({ session_id: sessionId, colour })
182
+ }
183
+ );
184
+ } catch (_err) {
185
+ // The cache already reflects the intent; the next load reconciles.
186
+ }
187
+ }
188
+
189
+ /** Drop stored colours for conversations that no longer exist. */
190
+ async forget(sessionIds: string[]): Promise<void> {
191
+ const present = sessionIds.filter(id => this._colours.has(id));
192
+ if (present.length === 0) {
193
+ return;
194
+ }
195
+ present.forEach(id => this._colours.delete(id));
196
+ try {
197
+ await requestProvider<IColourStoreResponse>(
198
+ this._providerId,
199
+ 'colours',
200
+ this._serverSettings,
201
+ {
202
+ method: 'DELETE',
203
+ body: JSON.stringify({ session_ids: present })
204
+ }
205
+ );
206
+ } catch (_err) {
207
+ // Same as `set`: the cache is authoritative until the next load.
208
+ }
209
+ }
210
+
211
+ /** Copy the parent's EFFECTIVE colour onto a fresh branch, so a branch of a
212
+ * branch inherits whatever its immediate parent actually shows, user-set
213
+ * override included. Called at the fork sites, once the new id is known. */
214
+ async inherit(parentColour: string | null, childId: string): Promise<void> {
215
+ if (!parentColour || !childId) {
216
+ return;
217
+ }
218
+ await this.set(childId, parentColour);
219
+ }
220
+
221
+ private readonly _providerId: string;
222
+ private readonly _serverSettings: ServerConnection.ISettings;
223
+ private _colours: Map<string, string> = new Map();
224
+ }
@@ -0,0 +1,121 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+
3
+ // Shared panel chrome icons. Provider icons are NOT here - each provider
4
+ // carries its own SVG on its descriptor and the registry builds the LabIcon
5
+ // from it, so adding an assistant touches no core file.
6
+ //
7
+ // Material-style glyphs matched to the rest of the sidebar so header and menu
8
+ // icons render at identical size and theme.
9
+
10
+ const ICON_PREFIX = 'jupyterlab_ai_code_assistants_extension';
11
+
12
+ // The extension's own identity, used by the settings entry - one section for
13
+ // all assistants, so it cannot borrow any one assistant's glyph. Material
14
+ // "smart_toy" outline.
15
+ const assistantsSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
16
+ <path class="jp-icon3" fill="#616161" d="M20 9V7c0-1.1-.9-2-2-2h-3c0-1.66-1.34-3-3-3S9 3.34 9 5H6c-1.1 0-2 .9-2 2v2c-1.66 0-3 1.34-3 3s1.34 3 3 3v4c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-4c1.66 0 3-1.34 3-3s-1.34-3-3-3zm-2 10H6V7h12v12zM9 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-1.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5.67-1.5 1.5-1.5 1.5.67 1.5 1.5zM8 15h8v2H8v-2z"/>
17
+ </svg>`;
18
+
19
+ export const assistantsIcon = new LabIcon({
20
+ name: `${ICON_PREFIX}:assistants`,
21
+ svgstr: assistantsSvgStr
22
+ });
23
+
24
+ const starFilledSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
25
+ <path class="jp-icon3" fill="#616161" d="M12 2.5l2.9 6.4 7 .7-5.3 4.7 1.6 6.9L12 17.7l-6.2 3.5 1.6-6.9-5.3-4.7 7-.7z"/>
26
+ </svg>`;
27
+
28
+ export const starFilledIcon = new LabIcon({
29
+ name: `${ICON_PREFIX}:star-filled`,
30
+ svgstr: starFilledSvgStr
31
+ });
32
+
33
+ const refreshSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
34
+ <path class="jp-icon3" fill="#616161" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
35
+ </svg>`;
36
+
37
+ export const refreshIcon = new LabIcon({
38
+ name: `${ICON_PREFIX}:refresh`,
39
+ svgstr: refreshSvgStr
40
+ });
41
+
42
+ const removeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
43
+ <path class="jp-icon3" fill="#616161" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4h-3.5z"/>
44
+ </svg>`;
45
+
46
+ export const removeIcon = new LabIcon({
47
+ name: `${ICON_PREFIX}:remove`,
48
+ svgstr: removeSvgStr
49
+ });
50
+
51
+ // Marks the unsafe launch variants. A shield reads as "protected", which is
52
+ // the inverse of what these entries do, so the caution triangle carries them
53
+ // instead. Material "warning" outline.
54
+ const warningSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="13" height="13">
55
+ <path class="jp-icon3" fill="#616161" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
56
+ </svg>`;
57
+
58
+ export const warningIcon = new LabIcon({
59
+ name: `${ICON_PREFIX}:warning`,
60
+ svgstr: warningSvgStr
61
+ });
62
+
63
+ const addSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
64
+ <path class="jp-icon3" fill="#616161" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
65
+ </svg>`;
66
+
67
+ export const addIcon = new LabIcon({
68
+ name: `${ICON_PREFIX}:add`,
69
+ svgstr: addSvgStr
70
+ });
71
+
72
+ // Git-branch glyph (Octicons git-branch-16, MIT) - marks rows carrying
73
+ // parallel conversations and the Branch Session entries.
74
+ const branchSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
75
+ <path class="jp-icon3" fill="#616161" d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/>
76
+ </svg>`;
77
+
78
+ export const branchIcon = new LabIcon({
79
+ name: `${ICON_PREFIX}:branch`,
80
+ svgstr: branchSvgStr
81
+ });
82
+
83
+ // Arrow-switch glyph (Octicons arrow-switch-16, MIT) - the switch-and-manage
84
+ // submenu.
85
+ const switchSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
86
+ <path class="jp-icon3" fill="#616161" d="M5.22 14.78a.75.75 0 0 0 1.06-1.06L4.56 12h8.69a.75.75 0 0 0 0-1.5H4.56l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3a.75.75 0 0 0 0 1.06l3 3Zm5.56-6.5a.75.75 0 1 1-1.06-1.06l1.72-1.72H2.75a.75.75 0 0 1 0-1.5h8.69L9.72 2.28a.75.75 0 0 1 1.06-1.06l3 3a.75.75 0 0 1 0 1.06l-3 3Z"/>
87
+ </svg>`;
88
+
89
+ export const switchIcon = new LabIcon({
90
+ name: `${ICON_PREFIX}:switch`,
91
+ svgstr: switchSvgStr
92
+ });
93
+
94
+ // Funnel copied verbatim from @jupyterlab/ui-components' `search/filter.svg` -
95
+ // the same image the file browser's filter toggle uses. `class="jp-icon3"`
96
+ // lets the theme drive the fill, so the source `fill="#FFF"` is inert under
97
+ // the standard light/dark themes.
98
+ const filterSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 24 24">
99
+ <path fill="#FFF" d="M14 12v7.88c.04.3-.06.62-.29.83a.996.996 0 0 1-1.41 0l-2.01-2.01a.99.99 0 0 1-.29-.83V12h-.03L4.21 4.62a1 1 0 0 1 .17-1.4c.19-.14.4-.22.62-.22h14c.22 0 .43.08.62.22a1 1 0 0 1 .17 1.4L14.03 12z" class="jp-icon3"/>
100
+ </svg>`;
101
+
102
+ export const filterIcon = new LabIcon({
103
+ name: `${ICON_PREFIX}:filter`,
104
+ svgstr: filterSvgStr
105
+ });
106
+
107
+ // Provider glyphs already built, by icon name. LabIcon warns and returns the
108
+ // first registration for a duplicate name, so a provider re-registered by a
109
+ // live settings toggle must resolve to the icon it built the first time
110
+ // rather than construct a second one on every toggle.
111
+ const _providerIcons = new Map<string, LabIcon>();
112
+
113
+ /** Build (or reuse) the LabIcon for a provider's own glyph. */
114
+ export function providerIcon(name: string, svgstr: string): LabIcon {
115
+ let icon = _providerIcons.get(name);
116
+ if (!icon) {
117
+ icon = new LabIcon({ name, svgstr });
118
+ _providerIcons.set(name, icon);
119
+ }
120
+ return icon;
121
+ }
@@ -0,0 +1,45 @@
1
+ // Launch-mode resolution: which single mode token, if any, one launch carries.
2
+ //
3
+ // Its own module because the panel is the only caller but not the only reader -
4
+ // the precedence between a boolean switch and an enum ladder is a rule worth
5
+ // asserting on its own, and this file pulls in no JupyterLab surface.
6
+
7
+ import { ILaunchMode } from './types';
8
+
9
+ /**
10
+ * The single launch-mode token for one action, or undefined for a plain launch.
11
+ *
12
+ * The payload carries at most one mode, because the server maps one token to
13
+ * one flag. `force` wins outright - that is what the menu's variant entries
14
+ * do. Otherwise an enum mode moved off its default takes precedence over any
15
+ * boolean switch: the enum is the explicit statement of intent, so a boolean
16
+ * left on must not silently invert it. A boolean fires only while every enum
17
+ * mode sits at its default. An enum at its default sends nothing - it is what
18
+ * the CLI would do unasked.
19
+ */
20
+ export function resolveLaunchMode(
21
+ launchModes: ILaunchMode[],
22
+ values: Record<string, boolean | string>,
23
+ force?: string
24
+ ): string | undefined {
25
+ if (force) {
26
+ return force;
27
+ }
28
+ for (const mode of launchModes) {
29
+ const value = values[mode.id];
30
+ if (
31
+ mode.kind !== 'boolean' &&
32
+ typeof value === 'string' &&
33
+ value &&
34
+ value !== mode.default
35
+ ) {
36
+ return `${mode.id}=${value}`;
37
+ }
38
+ }
39
+ for (const mode of launchModes) {
40
+ if (mode.kind === 'boolean' && values[mode.id] === true) {
41
+ return mode.id;
42
+ }
43
+ }
44
+ return undefined;
45
+ }