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,102 @@
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
+
12
+ import {
13
+ IBranch,
14
+ IProviderDescriptor,
15
+ IProviderHooks,
16
+ ISession
17
+ } from '../core/types';
18
+
19
+ // Claude's own mark, drawn as a single path group so the theme's `jp-icon3`
20
+ // fill drives it in both light and dark.
21
+ const claudeSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
22
+ <g class="jp-icon3" fill="#616161">
23
+ <path d="m14.375 6.48.49.28v.209l-.14.489-5.937 1.397-.558-1.387zm0 0"/>
24
+ <path d="m12.155 2.373.683.143.182.224.173.535-.072.342-3.983 5.447L7.81 7.737l3.673-4.82z"/>
25
+ <path d="m8.719 1.522.419-.28.349.14.349.49-.957 5.748-.65-.441-.279-.769.49-4.33z"/>
26
+ <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"/>
27
+ <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"/>
28
+ <path d="M1.316 8.296 1 7.946v-.31l.316-.108 3.562.21 3.491.279-.113.695-6.66-.346z"/>
29
+ <path d="M3.411 11.931h-.698l-.278-.32v-.382l1.186-.838 4.82-3.068.487.833z"/>
30
+ <path d="m4.738 13.883-.28.07-.418-.21.07-.35 4.12-5.446.558.768-3.072 4.05z"/>
31
+ <path d="m8.23 14.581-.21.28-.419.14-.349-.28-.21-.42L8.09 8.646l.629.07z"/>
32
+ <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"/>
33
+ <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"/>
34
+ <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"/>
35
+ </g>
36
+ </svg>`;
37
+
38
+ export const descriptor: IProviderDescriptor = {
39
+ id: 'claude',
40
+ label: 'Claude Code',
41
+ panelTitle: 'Claude Code Sessions',
42
+ iconName: 'jupyterlab_ai_code_assistants_extension:claude',
43
+ iconSvg: claudeSvgStr,
44
+ cliBinary: 'claude',
45
+ // `claude --fork-session --session-id <uuid>`: the CLI forks in-process and
46
+ // takes the new id from us, so the fork is known before it exists - and its
47
+ // store entry is written lazily, on the first turn, which is what the core's
48
+ // branch watcher waits for.
49
+ forkStrategy: 'native-flag',
50
+ // `/color` is Claude's own surface for a conversation's colour, recorded in
51
+ // its transcript, so the assistant owns the tint and the extension's
52
+ // write-back store stays out of it.
53
+ colourSource: 'native',
54
+ // `-n <name>` at launch: Claude stamps the name itself and re-stamps it on
55
+ // every turn, which is the only way a fork's name survives - a title written
56
+ // after the fact loses to the parent title the fork inherits.
57
+ namingStrategy: 'launch-flag',
58
+ mintsNewSessionId: true,
59
+ launchModes: [
60
+ {
61
+ id: 'dangerouslySkipPermissions',
62
+ title: 'Dangerously skip permissions',
63
+ description:
64
+ '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.',
65
+ kind: 'boolean',
66
+ default: false,
67
+ menuLabel: 'Skip Permissions'
68
+ }
69
+ ],
70
+ hasRemoteControl: true,
71
+ hasBgAgents: true,
72
+ // Claude records the conversation a process is on, so a terminal is
73
+ // identified exactly; there is no per-project "something is running here"
74
+ // signal to render.
75
+ hasLiveProcess: false,
76
+ // Retired standalone extension this provider replaces.
77
+ legacyPluginId: 'jupyterlab_claude_code_extension'
78
+ };
79
+
80
+ /** The `bg` chip: this conversation belongs to a live background agent, so
81
+ * opening it joins the running agent instead of resuming a dormant transcript.
82
+ * Display only - the server picks the verb at launch, where it cannot be
83
+ * stale. */
84
+ function bgChip(): HTMLElement {
85
+ const chip = document.createElement('span');
86
+ chip.className = 'jp-AiAssistantsPanel-bgBadge';
87
+ chip.textContent = 'bg';
88
+ return chip;
89
+ }
90
+
91
+ export const hooks: IProviderHooks = {
92
+ // Joining a live agent and resuming a dormant conversation are different
93
+ // verbs, so the item says which one this row gets before the click.
94
+ resumeLabel: (session: ISession | null) =>
95
+ session?.bg_id ? 'Attach to Background Agent' : 'Resume',
96
+
97
+ // In the manage-sessions popup the chip replaces the fork badge for an
98
+ // agent-held conversation: both are identity hints, and the one that changes
99
+ // what a click does is the one worth the column. Returning null elsewhere
100
+ // leaves the shared fork badge in place.
101
+ branchBadge: (branch: IBranch) => (branch.bg_id ? bgChip() : null)
102
+ };
@@ -0,0 +1,89 @@
1
+ // The Codex provider: OpenAI's `codex` CLI.
2
+ //
3
+ // Ported from `jupyterlab_codex_extension` v0.6.12. Almost everything that
4
+ // extension did by hand is now shared panel behaviour, so what is left here is
5
+ // the descriptor plus the two tooltip lines Codex has and the others do not.
6
+ //
7
+ // The three divergences that matter are capability flags, not code:
8
+ //
9
+ // * `forkStrategy: 'native-command'` - `codex fork <parent>` mints the fork's
10
+ // id itself, so the panel snapshots the branch list, launches, and diffs to
11
+ // discover the id (the shared 2s x 90 discovery watcher)
12
+ // * `colourSource: 'none'` - Codex has no colour concept at all, so a tab tint
13
+ // only ever comes from the extension's own write-back store
14
+ // * `hasLiveProcess: true` - a project row shows a dot when a `codex` process
15
+ // is running in it, which is the only liveness signal Codex exposes
16
+
17
+ import {
18
+ IHookContext,
19
+ IProviderDescriptor,
20
+ IProviderHooks
21
+ } from '../core/types';
22
+
23
+ // The OpenAI "blossom" mark - Codex is OpenAI's CLI, so the panel wears its
24
+ // vendor's logo. Single path, `jp-icon3` so it picks up the theme's foreground
25
+ // in both light and dark. The box stays 16px like every other icon; the mark is
26
+ // scaled down INSIDE it by padding the viewBox (24 units of art in a 30-unit
27
+ // window, so ~80%), which keeps the sidebar tab and header alignment identical
28
+ // while the dense logo reads less heavy than the simpler glyphs around it.
29
+ const codexSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 30 30" width="16" height="16">
30
+ <path class="jp-icon3" fill="#616161" d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073ZM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494ZM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646ZM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872Zm16.597 3.858-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667Zm2.01-3.024-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66Zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681ZM9.409 12l2.602-1.504L14.618 12v3.008l-2.597 1.5-2.607-1.5Z"/>
31
+ </svg>`;
32
+
33
+ // Codex's own name for its unsafe switch. Kept in the assistant's terminology
34
+ // rather than normalised, because it is both the settings key under
35
+ // `providers.codex.` and the mode token the server maps to the CLI flag
36
+ // `--dangerously-bypass-approvals-and-sandbox`.
37
+ const BYPASS_MODE = 'dangerouslyBypassApprovalsAndSandbox';
38
+
39
+ export const descriptor: IProviderDescriptor = {
40
+ id: 'codex',
41
+ label: 'Codex',
42
+ panelTitle: 'Codex Sessions',
43
+ iconName: 'jupyterlab_ai_code_assistants_extension:codex',
44
+ iconSvg: codexSvgStr,
45
+ cliBinary: 'codex',
46
+ // `codex fork <parent>` owns both the verb and the new id.
47
+ forkStrategy: 'native-command',
48
+ // No `/color` equivalent, and nothing in the thread store records one.
49
+ colourSource: 'none',
50
+ // Forks are unnamed - Codex has no naming flag and no writable title field,
51
+ // so the panel never asks for a name it could not stamp anywhere.
52
+ namingStrategy: 'none',
53
+ // A bare `codex` mints its own thread id; there is no `--session-id`.
54
+ mintsNewSessionId: false,
55
+ launchModes: [
56
+ {
57
+ id: BYPASS_MODE,
58
+ title: 'Bypass Codex approvals and sandbox',
59
+ description:
60
+ 'When enabled, newly spawned and resumed sessions are launched with `--dangerously-bypass-approvals-and-sandbox`. Bypasses every Codex approval prompt and sandbox - only enable in environments you trust.',
61
+ kind: 'boolean',
62
+ default: false,
63
+ menuLabel: 'Bypass Approvals'
64
+ }
65
+ ],
66
+ hasRemoteControl: false,
67
+ hasBgAgents: false,
68
+ // A `codex` process running in the project folder is the only liveness
69
+ // signal Codex exposes - it keeps no per-pid session record.
70
+ hasLiveProcess: true,
71
+ // Retired standalone extension this provider replaces.
72
+ legacyPluginId: 'jupyterlab_codex_extension'
73
+ };
74
+
75
+ export const hooks: IProviderHooks = {
76
+ // Codex's thread store records the model and the token spend of every
77
+ // conversation; no other assistant here does, so they are a provider line
78
+ // rather than a core one.
79
+ tooltipLines: ({ session }: IHookContext): string[] => {
80
+ const lines: string[] = [];
81
+ if (session.model) {
82
+ lines.push(`Model: ${session.model}`);
83
+ }
84
+ if ((session.tokens_used ?? 0) > 0) {
85
+ lines.push(`Tokens: ${session.tokens_used}`);
86
+ }
87
+ return lines;
88
+ }
89
+ };
@@ -0,0 +1,80 @@
1
+ // The Gemini provider - Google's `gemini` CLI.
2
+ //
3
+ // New here: no standalone extension preceded it, so there is nothing to port
4
+ // and nothing to migrate. Everything that makes this assistant different from
5
+ // the base one is descriptor data, so no core file knows it exists:
6
+ //
7
+ // * `forkStrategy: 'server-copy'` - gemini has no fork verb at all, so the
8
+ // server copies the chat file under a fresh id and answers with it
9
+ // * `colourSource: 'none'` - no colour command, and nothing in the chat store
10
+ // records one, so a tab tint only ever comes from the extension's own
11
+ // write-back store
12
+ // * `namingStrategy: 'server-side'` - a fork's name is stamped into the copied
13
+ // chat file's `summary`, which is the field the CLI itself displays a
14
+ // conversation under; there is no naming flag to pass it to
15
+ // * `mintsNewSessionId: true` - `--session-id <uuid>` starts a fresh
16
+ // conversation under an id we chose, so a new session is identifiable from
17
+ // its argv on the first poll instead of being an unknown terminal
18
+ // * two launch modes, gemini's `--yolo` switch and its four-step approval
19
+ // ladder
20
+ //
21
+ // No hooks: gemini rows carry no badge, no extra menu item and no derived
22
+ // colour, so the shared panel renders them unchanged.
23
+
24
+ import { IProviderDescriptor } from '../core/types';
25
+
26
+ // The four-pointed spark of the Gemini mark: four tips on the box edges, each
27
+ // pair joined by a quadratic whose control point is the centre, which is what
28
+ // pinches the sides inwards. Single path, `jp-icon3` so the theme drives the
29
+ // fill in both light and dark; the 16px box matches every other icon in the
30
+ // sidebar.
31
+ const geminiSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
32
+ <path class="jp-icon3" fill="#616161" d="M8 0Q8 8 16 8Q8 8 8 16Q8 8 0 8Q8 8 8 0Z"/>
33
+ </svg>`;
34
+
35
+ // Gemini's own names for its two launch surfaces. Kept in the assistant's
36
+ // terminology rather than normalised, because each is both the settings key
37
+ // under `providers.gemini.` and the mode token the server maps to a CLI flag.
38
+ const YOLO_MODE = 'yoloMode';
39
+ const APPROVAL_MODE = 'approvalMode';
40
+
41
+ export const descriptor: IProviderDescriptor = {
42
+ id: 'gemini',
43
+ label: 'Gemini',
44
+ panelTitle: 'Gemini Sessions',
45
+ iconName: 'jupyterlab_ai_code_assistants_extension:gemini',
46
+ iconSvg: geminiSvgStr,
47
+ cliBinary: 'gemini',
48
+ // No fork flag exists, so the server copies the chat file under a fresh id
49
+ // and the launch resumes that id like any other conversation.
50
+ forkStrategy: 'server-copy',
51
+ colourSource: 'none',
52
+ // The fork's name is written into the copy by the server; the CLI has no
53
+ // naming flag to take it.
54
+ namingStrategy: 'server-side',
55
+ // `--session-id <uuid>` starts a conversation under an id we mint.
56
+ mintsNewSessionId: true,
57
+ launchModes: [
58
+ {
59
+ id: YOLO_MODE,
60
+ title: 'YOLO mode',
61
+ description:
62
+ 'When enabled, newly spawned and resumed sessions are launched with `--yolo`. Gemini auto-approves every tool call without asking - only enable in workspaces you trust.',
63
+ kind: 'boolean',
64
+ default: false,
65
+ menuLabel: 'YOLO'
66
+ },
67
+ {
68
+ id: APPROVAL_MODE,
69
+ title: 'Approval mode',
70
+ description:
71
+ 'Approval ladder passed as `--approval-mode` when set to a value other than the default. A non-default value here takes precedence over the YOLO switch; only the forced YOLO menu action overrides it. `default` prompts for approval, `auto_edit` auto-approves edit tools, `yolo` auto-approves every tool, `plan` is read-only.',
72
+ kind: 'enum',
73
+ values: ['default', 'auto_edit', 'yolo', 'plan'],
74
+ default: 'default'
75
+ }
76
+ ],
77
+ hasRemoteControl: false,
78
+ hasBgAgents: false,
79
+ hasLiveProcess: false
80
+ };
@@ -0,0 +1,21 @@
1
+ // The single registration point for assistants.
2
+ //
3
+ // One line per provider. Adding an assistant means adding its module and its
4
+ // line here; removing one means deleting both. No core file is edited either
5
+ // way, and nothing outside this directory names an assistant.
6
+ //
7
+ // A provider module exports a `descriptor` (pure data - the settings-schema
8
+ // generator reads it under plain Node) and optionally `hooks` (the DOM and
9
+ // JupyterLab-aware parts). Keep module-scope side effects out of both, so the
10
+ // generator can import this barrel without a browser.
11
+
12
+ import { IProviderModule } from '../core/types';
13
+
14
+ import * as claude from './claude';
15
+ import * as codex from './codex';
16
+ import * as kimi from './kimi';
17
+ import * as gemini from './gemini';
18
+
19
+ export const PROVIDERS: IProviderModule[] = [claude, codex, kimi, gemini];
20
+
21
+ export default PROVIDERS;
@@ -0,0 +1,155 @@
1
+ // The Kimi provider - Moonshot AI's `kimi` CLI.
2
+ //
3
+ // Ported from jupyterlab_kimi_code_extension v0.7.8. Everything that made that
4
+ // extension different from the base one is declared here as descriptor data,
5
+ // so no core file knows this assistant exists:
6
+ //
7
+ // * `forkStrategy: 'server-copy'` - kimi has no fork verb at all, so the
8
+ // server copies the session directory and answers with the new id
9
+ // * `colourSource: 'derived'` - kimi has no `/color`, so the default tint is a
10
+ // stable hash of the conversation id (a user-set colour still wins, per the
11
+ // Colour section of the acceptance criteria)
12
+ // * `namingStrategy: 'server-side'` - the name is stamped into the copied
13
+ // `state.json`, not passed to the CLI
14
+ // * one launch mode, `--yolo`, under kimi's own name
15
+ //
16
+ // The only hook is the branch label: kimi's conversation titles are
17
+ // auto-generated by a Chinese-first model, and both the session id shape and
18
+ // the glyph widths of those titles need handling the shared default cannot do.
19
+
20
+ import { IBranch, IProviderDescriptor, IProviderHooks } from '../core/types';
21
+
22
+ // Crescent moon (Kimi is Moonshot AI's CLI) - an outer semicircle on the left,
23
+ // bitten from the right by a wider inner arc.
24
+ const kimiSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
25
+ <g class="jp-icon3" fill="#616161">
26
+ <path d="M8 1.5A6.5 6.5 0 1 0 8 14.5 8 8 0 0 1 8 1.5Z"/>
27
+ </g>
28
+ </svg>`;
29
+
30
+ // ------------------------------------------------------------------- labels
31
+
32
+ // Budget is in COLUMNS, not characters. Han, Kana, Hangul and emoji render at
33
+ // roughly one em against Latin's half, so counting characters bounds nothing:
34
+ // 60 Han glyphs measured 851-862px in a real Lumino submenu, at or above the
35
+ // 850px that filed the defect in the first place. Kimi is Moonshot AI's CLI,
36
+ // so a Chinese auto-generated title is the expected case, not an exotic one.
37
+ export const MENU_TITLE_COLUMNS = 60;
38
+
39
+ // East Asian Wide and Fullwidth ranges, plus emoji. Deliberately coarse: this
40
+ // bounds a menu width, so a handful of mis-classified glyphs cost pixels, not
41
+ // correctness. Anything outside these ranges counts as one column.
42
+ const WIDE_RANGES: ReadonlyArray<readonly [number, number]> = [
43
+ [0x1100, 0x115f], // Hangul Jamo
44
+ [0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols and punctuation
45
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul Compat, CJK Compat
46
+ [0x3400, 0x4dbf], // CJK Unified Extension A
47
+ [0x4e00, 0x9fff], // CJK Unified Ideographs
48
+ [0xa000, 0xa4cf], // Yi
49
+ [0xac00, 0xd7a3], // Hangul syllables
50
+ [0xf900, 0xfaff], // CJK Compatibility Ideographs
51
+ [0xfe30, 0xfe6f], // CJK Compatibility Forms
52
+ [0xff00, 0xff60], // Fullwidth forms
53
+ [0xffe0, 0xffe6], // Fullwidth signs
54
+ [0x1f300, 0x1faff], // Emoji and pictographs
55
+ [0x20000, 0x3fffd] // CJK Unified Extensions B and beyond
56
+ ];
57
+
58
+ function isWide(codePoint: number): boolean {
59
+ return WIDE_RANGES.some(([lo, hi]) => codePoint >= lo && codePoint <= hi);
60
+ }
61
+
62
+ /** Trim `text` to `maxColumns` display columns, appending an ellipsis when
63
+ * anything was cut. Iterates by CODE POINT, so an astral character is never
64
+ * split into a lone surrogate (which chromium paints as the replacement
65
+ * glyph). Returns `text` unchanged when it already fits. */
66
+ export function truncateToColumns(text: string, maxColumns: number): string {
67
+ let columns = 0;
68
+ let kept = '';
69
+ for (const ch of text) {
70
+ const width = isWide(ch.codePointAt(0) as number) ? 2 : 1;
71
+ if (columns + width > maxColumns) {
72
+ // The cut can land just after a space; drop it so the ellipsis hugs the
73
+ // last word. `trimEnd` is ES2019 and this project targets ES2018.
74
+ return `${kept.replace(/\s+$/, '')}…`;
75
+ }
76
+ columns += width;
77
+ kept += ch;
78
+ }
79
+ return text;
80
+ }
81
+
82
+ /** The distinguishing part of a kimi session id.
83
+ *
84
+ * Kimi ids are `session_<uuid>`, so the core's front-slice would render the
85
+ * constant `session_` for every conversation of a project - the exact thing
86
+ * the short id exists to tell apart. Slice past the prefix instead.
87
+ */
88
+ export function shortSessionId(sessionId: string): string {
89
+ return sessionId.startsWith('session_')
90
+ ? sessionId.slice(8, 16)
91
+ : sessionId.slice(0, 8);
92
+ }
93
+
94
+ /** Menu-item label for one branch: the conversation title trimmed to the
95
+ * column budget, followed by the short session id. Only the TITLE is trimmed -
96
+ * branches of one project share a path, so the id is what tells them apart and
97
+ * it must survive. The bracketed id is dropped when the title already IS the
98
+ * id (the server's last-resort fallback), which can never be truncated since a
99
+ * short id is at most 8 characters. */
100
+ export function branchMenuLabel(label: string, shortId: string): string {
101
+ const title = truncateToColumns(label, MENU_TITLE_COLUMNS);
102
+ return label === shortId ? title : `${title} (${shortId})`;
103
+ }
104
+
105
+ // -------------------------------------------------------------- descriptor
106
+
107
+ export const descriptor: IProviderDescriptor = {
108
+ id: 'kimi',
109
+ label: 'Kimi',
110
+ panelTitle: 'Kimi Code Sessions',
111
+ iconName: 'jupyterlab_ai_code_assistants_extension:kimi',
112
+ iconSvg: kimiSvgStr,
113
+ cliBinary: 'kimi',
114
+ // No fork flag exists, so the server copies the session directory under a
115
+ // fresh id and the launch resumes that id like any other conversation.
116
+ forkStrategy: 'server-copy',
117
+ // No `/color` command either: the tint is derived from the conversation id.
118
+ // The core's own FNV-1a is the same hash over the same six ids the retired
119
+ // extension used, so every conversation keeps the colour it already had.
120
+ colourSource: 'derived',
121
+ // The fork's name is written into the copy's `state.json` by the server; the
122
+ // CLI has no naming flag to take it.
123
+ namingStrategy: 'server-side',
124
+ // A bare `kimi` mints its own id and writes it on the first turn, and `-S`
125
+ // only ever resumes - so a new conversation cannot be launched under an id
126
+ // we chose, and is identified from the store on the next poll instead.
127
+ mintsNewSessionId: false,
128
+ launchModes: [
129
+ {
130
+ id: 'yoloMode',
131
+ title: 'YOLO mode',
132
+ description:
133
+ 'When enabled, newly spawned and resumed sessions are launched with `--yolo`. Kimi auto-approves regular tool calls without asking - only enable in workspaces you trust.',
134
+ kind: 'boolean',
135
+ default: false,
136
+ menuLabel: 'YOLO'
137
+ }
138
+ ],
139
+ hasRemoteControl: false,
140
+ hasBgAgents: false,
141
+ hasLiveProcess: false,
142
+ // Retired standalone extension this provider replaces.
143
+ legacyPluginId: 'jupyterlab_kimi_code_extension'
144
+ };
145
+
146
+ // ------------------------------------------------------------------- hooks
147
+
148
+ export const hooks: IProviderHooks = {
149
+ /** Lumino sets no `max-width` on `.lm-Menu-itemLabel`, so an uncapped
150
+ * auto-generated title stretches the submenu across the window. The core's
151
+ * `shortId` argument is a front-slice, which is the constant `session_` for
152
+ * every kimi id, so the id is re-derived here. */
153
+ branchLabel: (branch: IBranch, _shortId: string): string =>
154
+ branchMenuLabel(branch.label, shortSessionId(branch.session_id))
155
+ };