jupyterlab_ai_code_assistants_extension 1.0.21 → 1.0.23

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/README.md CHANGED
@@ -32,7 +32,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
32
32
  - **One-click resume** - click a row to jump back into that session in a terminal; an open terminal for the project is reused instead of duplicated
33
33
  - **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete and per-row open and copy-id buttons
34
34
  - **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's `codex fork`, server-side copies for Kimi and Gemini) behind the same menu item
35
- - **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO and approval modes; unsafe variants carry a warning glyph
35
+ - **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO; unsafe variants carry a shield glyph in the launch menus
36
36
  - **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically). Claude's own `/color` supplies its default, Kimi derives a stable colour from the conversation id; Codex and Gemini have no colour of their own until you set one, and a branched session inherits its parent's colour, which it keeps even if the parent's colour later changes or is reset
37
37
  - **Your own tab colour wins** - set a colour on a terminal tab and the extension remembers it for that conversation, overriding whatever the assistant chose. `Reset Tab Colour (n)` in the session's right-click menu hands back every colour you set by hand on that project's conversations, and appears once there is one to hand back. Use it rather than the tab menu's own Clear, which will not release a stored colour. A Codex or Kimi conversation that has never been resumed cannot be tracked yet, so a colour set on its tab is not kept
38
38
  - **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
@@ -2,7 +2,7 @@ import { ILaunchMode } from './types';
2
2
  /** A resolved launch mode, with what to call it and whether to warn. */
3
3
  export interface IResolvedLaunchMode {
4
4
  mode: ILaunchMode;
5
- /** What the menu should call it - the value, or the mode's own label. */
5
+ /** What the menu should call it - the mode's own label. */
6
6
  label: string;
7
7
  /** Whether this widens what the assistant may do without asking. */
8
8
  unsafe: boolean;
@@ -12,11 +12,8 @@ export interface IResolvedLaunchMode {
12
12
  *
13
13
  * The payload carries at most one mode, because the server maps one token to
14
14
  * one flag. `force` wins outright - that is what the menu's variant entries
15
- * do. Otherwise an enum mode moved off its default takes precedence over any
16
- * boolean switch: the enum is the explicit statement of intent, so a boolean
17
- * left on must not silently invert it. A boolean fires only while every enum
18
- * mode sits at its default. An enum at its default sends nothing - it is what
19
- * the CLI would do unasked.
15
+ * do. Otherwise the first switch that is on fires. Every mode is a boolean
16
+ * skip-approval switch, so "on" always means the same thing.
20
17
  */
21
18
  export declare function resolveLaunchMode(launchModes: ILaunchMode[], values: Record<string, boolean | string>, force?: string): string | undefined;
22
19
  /**
package/lib/core/modes.js CHANGED
@@ -1,34 +1,22 @@
1
1
  // Launch-mode resolution: which single mode token, if any, one launch carries.
2
2
  //
3
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.
4
+ // the rule that one launch carries at most one mode is worth asserting on its
5
+ // own, and this file pulls in no JupyterLab surface.
6
6
  /**
7
7
  * The single launch-mode token for one action, or undefined for a plain launch.
8
8
  *
9
9
  * The payload carries at most one mode, because the server maps one token to
10
10
  * one flag. `force` wins outright - that is what the menu's variant entries
11
- * do. Otherwise an enum mode moved off its default takes precedence over any
12
- * boolean switch: the enum is the explicit statement of intent, so a boolean
13
- * left on must not silently invert it. A boolean fires only while every enum
14
- * mode sits at its default. An enum at its default sends nothing - it is what
15
- * the CLI would do unasked.
11
+ * do. Otherwise the first switch that is on fires. Every mode is a boolean
12
+ * skip-approval switch, so "on" always means the same thing.
16
13
  */
17
14
  export function resolveLaunchMode(launchModes, values, force) {
18
15
  if (force) {
19
16
  return force;
20
17
  }
21
18
  for (const mode of launchModes) {
22
- const value = values[mode.id];
23
- if (mode.kind !== 'boolean' &&
24
- typeof value === 'string' &&
25
- value &&
26
- value !== mode.default) {
27
- return `${mode.id}=${value}`;
28
- }
29
- }
30
- for (const mode of launchModes) {
31
- if (mode.kind === 'boolean' && values[mode.id] === true) {
19
+ if (values[mode.id] === true) {
32
20
  return mode.id;
33
21
  }
34
22
  }
@@ -45,28 +33,20 @@ export function resolveLaunchMode(launchModes, values, force) {
45
33
  * assistant's own wording.
46
34
  */
47
35
  export function resolvedLaunchModeEntry(launchModes, values) {
48
- var _a, _b;
36
+ var _a;
49
37
  const token = resolveLaunchMode(launchModes, values);
50
38
  if (!token) {
51
39
  return null;
52
40
  }
53
- const [id, value] = token.split('=');
54
- const mode = launchModes.find(m => m.id === id);
41
+ const mode = launchModes.find(m => m.id === token);
55
42
  if (!mode) {
56
43
  return null;
57
44
  }
58
- // A boolean mode with a menu label IS the assistant's skip-approval switch;
59
- // an enum only widens on the values its provider declared. `plan` resolves
60
- // to a mode and is stricter than the default, so "resolved at all" is the
61
- // wrong question to ask about danger.
62
- const unsafe = value
63
- ? ((_a = mode.unsafeValues) !== null && _a !== void 0 ? _a : []).indexOf(value) !== -1
64
- : !!mode.menuLabel;
65
- // Name the VALUE where there is one - three enum values sharing the mode's
66
- // title render as one indistinguishable label.
45
+ // Carrying a menu label IS a boolean mode's declaration that it skips
46
+ // approval - it is what puts a variant in the menu.
67
47
  return {
68
48
  mode,
69
- label: (_b = value !== null && value !== void 0 ? value : mode.menuLabel) !== null && _b !== void 0 ? _b : mode.title,
70
- unsafe
49
+ label: (_a = mode.menuLabel) !== null && _a !== void 0 ? _a : mode.title,
50
+ unsafe: !!mode.menuLabel
71
51
  };
72
52
  }
@@ -188,7 +188,7 @@ export declare class AssistantSessionsPanel extends Widget {
188
188
  private _setRefreshSpinning;
189
189
  private _setActiveRow;
190
190
  private _cmd;
191
- /** Boolean launch modes that produce a menu variant - the assistant's own
191
+ /** Launch modes that produce a menu variant - the assistant's own
192
192
  * skip-approval switch, under the assistant's own name. */
193
193
  private get _variantModes();
194
194
  /** The launch mode a PLAIN action resolves to right now, or null.
@@ -298,8 +298,6 @@ export declare class AssistantSessionsPanel extends Widget {
298
298
  private _newSessionMenu;
299
299
  /** Re-writes the `+` button's title. Set once the shell exists. */
300
300
  private _renameNewButton;
301
- /** Re-draws the `+` button's glyph. Set once the shell exists. */
302
- private _repaintNewIcon;
303
301
  private _lastBranches;
304
302
  private _lastBranchesCurrent;
305
303
  private _colouredTabs;
package/lib/core/panel.js CHANGED
@@ -107,8 +107,6 @@ export class AssistantSessionsPanel extends Widget {
107
107
  this._roving = {};
108
108
  /** Re-writes the `+` button's title. Set once the shell exists. */
109
109
  this._renameNewButton = null;
110
- /** Re-draws the `+` button's glyph. Set once the shell exists. */
111
- this._repaintNewIcon = null;
112
110
  this._lastBranches = [];
113
111
  this._lastBranchesCurrent = '';
114
112
  this._colouredTabs = true;
@@ -220,14 +218,12 @@ export class AssistantSessionsPanel extends Widget {
220
218
  /** Current values of this provider's launch modes, read from settings. Each
221
219
  * key is an `ILaunchMode.id` in the assistant's own terminology. */
222
220
  setModes(modes) {
223
- var _a, _b;
221
+ var _a;
224
222
  this._modes = { ...modes };
225
223
  // Menu labels are lazy and re-read this on every open; the button's title
226
- // is a written string, so it has to be rewritten here.
224
+ // is a written string, so it has to be rewritten here. The glyph needs no
225
+ // such hook - it is always "+", whatever mode is armed (DEF-112).
227
226
  (_a = this._renameNewButton) === null || _a === void 0 ? void 0 : _a.call(this);
228
- // Same for its glyph - a menu item re-reads its icon on every open, a
229
- // painted button does not.
230
- (_b = this._repaintNewIcon) === null || _b === void 0 ? void 0 : _b.call(this);
231
227
  }
232
228
  setColouredTabs(on) {
233
229
  this._colouredTabs = on;
@@ -282,25 +278,11 @@ export class AssistantSessionsPanel extends Widget {
282
278
  // after turning an approval-free mode on (DEF-38).
283
279
  this._renameNewButton = nameNewButton;
284
280
  nameNewButton();
285
- // The glyph follows the same rule the menu entries follow: where a mode
286
- // WIDENS what runs without asking, the mode's warning glyph wins over the
287
- // action's own (DEF-35). It matters most here - with a mode in force the
288
- // variants suppress themselves and this button launches on click, so a
289
- // plain "+" would be the only one-click approval-free launch in the panel
290
- // wearing no warning at all.
291
- const paintNewIcon = () => {
292
- var _a;
293
- const icon = (_a = (this._visibleVariantCount() === 0 ? this._variantIcon() : undefined)) !== null && _a !== void 0 ? _a : addIcon;
294
- // `LabIcon.element` appends into the container, so the previous glyph
295
- // has to go or the button would collect one svg per settings change.
296
- newBtn.replaceChildren();
297
- icon.element({ container: newBtn });
298
- };
299
- // Held for the same reason the title is: the shell is built before
300
- // settings arrive, so what is drawn here can only say "no mode in force"
301
- // and `setModes` has to repaint it (DEF-38).
302
- this._repaintNewIcon = paintNewIcon;
303
- paintNewIcon();
281
+ // The glyph is always "+", whatever mode is armed (DEF-112): the shield
282
+ // marks the menu ENTRIES that skip approval, while the button that offers
283
+ // or launches stays neutral - an armed mode is still named in the title
284
+ // above, so the launch never goes unnamed.
285
+ addIcon.element({ container: newBtn });
304
286
  newBtn.addEventListener('click', () => {
305
287
  // With a mode in force the variant items suppress themselves, leaving a
306
288
  // dropdown with one entry - two clicks for its only outcome. Do it.
@@ -1637,10 +1619,10 @@ export class AssistantSessionsPanel extends Widget {
1637
1619
  _cmd(action) {
1638
1620
  return commandId(this._descriptor.id, action);
1639
1621
  }
1640
- /** Boolean launch modes that produce a menu variant - the assistant's own
1622
+ /** Launch modes that produce a menu variant - the assistant's own
1641
1623
  * skip-approval switch, under the assistant's own name. */
1642
1624
  get _variantModes() {
1643
- return this._descriptor.launchModes.filter(m => m.kind === 'boolean' && !!m.menuLabel);
1625
+ return this._descriptor.launchModes.filter(m => !!m.menuLabel);
1644
1626
  }
1645
1627
  /** The launch mode a PLAIN action resolves to right now, or null.
1646
1628
  *
@@ -19,9 +19,8 @@ export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
19
19
  * the extension's own write-back store always wins over all three. */
20
20
  export type ColourSource = 'native' | 'derived' | 'none';
21
21
  /** One launch-behaviour setting of an assistant, in that assistant's own
22
- * terminology. `boolean` modes are the unsafe/skip-approval switches: they get
23
- * a settings toggle (off by default) AND a context-menu launch variant.
24
- * `enum` modes are settings-only choices passed through on every launch. */
22
+ * terminology. A launch mode is a boolean skip-approval switch: it gets a
23
+ * settings toggle (off by default) AND a context-menu launch variant. */
25
24
  export interface ILaunchMode {
26
25
  /** Settings key under `providers.<id>.` and launch-payload key. Uses the
27
26
  * assistant's own terminology, never a normalised core name. */
@@ -30,24 +29,12 @@ export interface ILaunchMode {
30
29
  title: string;
31
30
  /** Settings description. */
32
31
  description: string;
33
- kind: 'boolean' | 'enum';
34
- /** Allowed values, `enum` modes only. */
35
- values?: string[];
36
- /** Default value. Boolean modes default to `false` without exception. */
37
- default: boolean | string;
32
+ /** Default value - `false` without exception. */
33
+ default: boolean;
38
34
  /** Menu label for the launch variant this mode produces, e.g.
39
- * `Skip Permissions`. Boolean modes only; absent means settings-only. */
35
+ * `Skip Permissions`. Carrying one IS the mode's declaration that it skips
36
+ * approval - it is what puts a variant in the menu. */
40
37
  menuLabel?: string;
41
- /** `enum` modes only: the values that WIDEN what the assistant may do
42
- * without asking. The panel warns on these and names the value; every other
43
- * value is a settings choice and is marked as one.
44
- *
45
- * Declared per provider rather than inferred, because "a mode resolved at
46
- * all" is not the same question: Gemini's `plan` is read-only - stricter
47
- * than the default - and marking it the same as `yolo` says nothing about
48
- * either. A boolean mode needs no entry here; carrying a `menuLabel` is
49
- * already its declaration that it skips approval. */
50
- unsafeValues?: string[];
51
38
  }
52
39
  /** Everything the core needs to know about one assistant. Pure data - no
53
40
  * JupyterLab imports - so the settings-schema generator can read it under
@@ -52,7 +52,6 @@ export const descriptor = {
52
52
  id: 'dangerouslySkipPermissions',
53
53
  title: 'Dangerously skip permissions',
54
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
55
  default: false,
57
56
  menuLabel: 'Skip Permissions'
58
57
  }
@@ -48,7 +48,6 @@ export const descriptor = {
48
48
  id: BYPASS_MODE,
49
49
  title: 'Bypass Codex approvals and sandbox',
50
50
  description: '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.',
51
- kind: 'boolean',
52
51
  default: false,
53
52
  menuLabel: 'Bypass Approvals'
54
53
  }
@@ -28,11 +28,10 @@
28
28
  const geminiSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 65 65" width="16" height="16">
29
29
  <path class="jp-icon3" fill="#616161" d="M32.447 0c.68 0 1.273.465 1.439 1.125a38.904 38.904 0 001.999 5.905c2.152 5 5.105 9.376 8.854 13.125 3.751 3.75 8.126 6.703 13.125 8.855a38.98 38.98 0 005.906 1.999c.66.166 1.124.758 1.124 1.438 0 .68-.464 1.273-1.125 1.439a38.902 38.902 0 00-5.905 1.999c-5 2.152-9.375 5.105-13.125 8.854-3.749 3.751-6.702 8.126-8.854 13.125a38.973 38.973 0 00-2 5.906 1.485 1.485 0 01-1.438 1.124c-.68 0-1.272-.464-1.438-1.125a38.913 38.913 0 00-2-5.905c-2.151-5-5.103-9.375-8.854-13.125-3.75-3.749-8.125-6.702-13.125-8.854a38.973 38.973 0 00-5.905-2A1.485 1.485 0 010 32.448c0-.68.465-1.272 1.125-1.438a38.903 38.903 0 005.905-2c5-2.151 9.376-5.104 13.125-8.854 3.75-3.749 6.703-8.125 8.855-13.125a38.972 38.972 0 001.999-5.905A1.485 1.485 0 0132.447 0z"/>
30
30
  </svg>`;
31
- // Gemini's own names for its two launch surfaces. Kept in the assistant's
32
- // terminology rather than normalised, because each is both the settings key
31
+ // Gemini's own name for its approval switch. Kept in the assistant's
32
+ // terminology rather than normalised, because it is both the settings key
33
33
  // under `providers.gemini.` and the mode token the server maps to a CLI flag.
34
34
  const YOLO_MODE = 'yoloMode';
35
- const APPROVAL_MODE = 'approvalMode';
36
35
  export const descriptor = {
37
36
  id: 'gemini',
38
37
  label: 'Gemini',
@@ -54,19 +53,8 @@ export const descriptor = {
54
53
  id: YOLO_MODE,
55
54
  title: 'YOLO mode',
56
55
  description: '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.',
57
- kind: 'boolean',
58
56
  default: false,
59
57
  menuLabel: 'YOLO'
60
- },
61
- {
62
- id: APPROVAL_MODE,
63
- title: 'Approval mode',
64
- description: '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.',
65
- kind: 'enum',
66
- values: ['default', 'auto_edit', 'yolo', 'plan'],
67
- default: 'default',
68
- // `plan` is read-only and `default` prompts, so neither widens anything.
69
- unsafeValues: ['auto_edit', 'yolo']
70
58
  }
71
59
  ],
72
60
  hasRemoteControl: false,
@@ -122,7 +122,6 @@ export const descriptor = {
122
122
  id: 'yoloMode',
123
123
  title: 'YOLO mode',
124
124
  description: '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.',
125
- kind: 'boolean',
126
125
  default: false,
127
126
  menuLabel: 'YOLO'
128
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_ai_code_assistants_extension",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Jupyterlab extension to replace individual jupyterlab_*_code_extension and integrate into one, that offers joint capability, joint settings page and individual right toolbar panels per code assistant, with the ability in settings to decide which code assistant support is on (default - all are enabled)",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -69,13 +69,6 @@
69
69
  "description": "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.",
70
70
  "default": false
71
71
  },
72
- "providers.gemini.approvalMode": {
73
- "type": "string",
74
- "title": "Gemini: Approval mode",
75
- "description": "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.",
76
- "default": "default",
77
- "enum": ["default", "auto_edit", "yolo", "plan"]
78
- },
79
72
  "providers.kimi.enabled": {
80
73
  "type": "boolean",
81
74
  "title": "Kimi: enabled",
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Launch-mode precedence.
2
+ * Launch-mode resolution.
3
3
  *
4
- * A provider can expose both a boolean switch and an enum ladder over the same
5
- * approval surface, and the payload carries only one token. An enum moved off
6
- * its default is the user's explicit statement, so it has to beat a boolean
7
- * left on - otherwise a read-only ladder setting launches wide open.
4
+ * Every mode is a boolean skip-approval switch in the assistant's own
5
+ * terminology (DEF-111 retired the one enum ladder, Gemini's `approvalMode`),
6
+ * and the payload carries only one token: `force` wins outright, otherwise
7
+ * the first switch that is on fires.
8
8
  */
9
9
 
10
10
  import { resolveLaunchMode, resolvedLaunchModeEntry } from '../core/modes';
@@ -15,44 +15,33 @@ const MODES: ILaunchMode[] = [
15
15
  id: 'yoloMode',
16
16
  title: 'YOLO',
17
17
  description: 'Boolean switch.',
18
- kind: 'boolean',
19
18
  default: false,
20
19
  // Carrying a menu label IS a boolean mode's declaration that it skips
21
20
  // approval - it is what puts a variant in the menu.
22
21
  menuLabel: 'YOLO'
23
- },
24
- {
25
- id: 'approvalMode',
26
- title: 'Approval',
27
- description: 'Enum ladder.',
28
- kind: 'enum',
29
- values: ['default', 'plan', 'yolo'],
30
- default: 'default',
31
- unsafeValues: ['yolo']
32
22
  }
33
23
  ];
34
24
 
35
25
  describe('resolveLaunchMode', () => {
36
- it('lets a non-default enum beat a boolean that is on', () => {
37
- expect(
38
- resolveLaunchMode(MODES, { yoloMode: true, approvalMode: 'plan' })
39
- ).toEqual('approvalMode=plan');
40
- });
41
-
42
- it('sends the boolean while every enum sits at its default', () => {
43
- expect(
44
- resolveLaunchMode(MODES, { yoloMode: true, approvalMode: 'default' })
45
- ).toEqual('yoloMode');
26
+ it('sends the boolean that is on', () => {
27
+ expect(resolveLaunchMode(MODES, { yoloMode: true })).toEqual('yoloMode');
46
28
  });
47
29
 
48
30
  it('sends nothing when no mode is set', () => {
49
31
  expect(resolveLaunchMode(MODES, {})).toBeUndefined();
32
+ expect(resolveLaunchMode(MODES, { yoloMode: false })).toBeUndefined();
50
33
  });
51
34
 
52
35
  it('forces the requested mode whatever the settings say', () => {
36
+ expect(resolveLaunchMode(MODES, {}, 'yoloMode')).toEqual('yoloMode');
37
+ });
38
+
39
+ it('ignores a token from a retired setting', () => {
40
+ // A stale `approvalMode` value in saved settings is not a boolean that is
41
+ // on - it must never become a launch token (DEF-111).
53
42
  expect(
54
- resolveLaunchMode(MODES, { approvalMode: 'plan' }, 'yoloMode')
55
- ).toEqual('yoloMode');
43
+ resolveLaunchMode(MODES, { approvalMode: 'plan' } as any)
44
+ ).toBeUndefined();
56
45
  });
57
46
  });
58
47
 
@@ -60,8 +49,7 @@ describe('resolvedLaunchModeEntry', () => {
60
49
  // What the panel marks its PLAIN menu items from. Deciding the warning glyph
61
50
  // from the menu's `force` argument instead leaves the entry the user
62
51
  // actually clicks unmarked, while marking a forced duplicate that builds the
63
- // identical launch. Deciding it from "a mode resolved at all" is the same
64
- // error one level down: it warns on Gemini's `plan`, which is read-only.
52
+ // identical launch.
65
53
  it('warns on a boolean skip-approval switch and names it', () => {
66
54
  const r = resolvedLaunchModeEntry(MODES, { yoloMode: true });
67
55
  expect(r?.mode.id).toEqual('yoloMode');
@@ -69,32 +57,8 @@ describe('resolvedLaunchModeEntry', () => {
69
57
  expect(r?.unsafe).toBe(true);
70
58
  });
71
59
 
72
- it('warns on an enum value the provider declared as widening', () => {
73
- const r = resolvedLaunchModeEntry(MODES, { approvalMode: 'yolo' });
74
- expect(r?.mode.id).toEqual('approvalMode');
75
- expect(r?.label).toEqual('yolo');
76
- expect(r?.unsafe).toBe(true);
77
- });
78
-
79
- it('does NOT warn on an enum value that is stricter than the default', () => {
80
- // `plan` is read-only. Marking it like `yolo` says nothing about either.
81
- const r = resolvedLaunchModeEntry(MODES, { approvalMode: 'plan' });
82
- expect(r?.mode.id).toEqual('approvalMode');
83
- expect(r?.label).toEqual('plan');
84
- expect(r?.unsafe).toBe(false);
85
- });
86
-
87
- it('names the mode that actually wins, not both', () => {
88
- expect(
89
- resolvedLaunchModeEntry(MODES, { yoloMode: true, approvalMode: 'plan' })
90
- ?.mode.id
91
- ).toEqual('approvalMode');
92
- });
93
-
94
60
  it('names nothing for a launch that carries no mode', () => {
95
61
  expect(resolvedLaunchModeEntry(MODES, {})).toBeNull();
96
- expect(
97
- resolvedLaunchModeEntry(MODES, { approvalMode: 'default' })
98
- ).toBeNull();
62
+ expect(resolvedLaunchModeEntry(MODES, { yoloMode: false })).toBeNull();
99
63
  });
100
64
  });
@@ -66,7 +66,6 @@ const DESCRIPTOR: IProviderDescriptor = {
66
66
  id: 'skip',
67
67
  title: 'Skip approvals',
68
68
  description: 'Run without asking.',
69
- kind: 'boolean',
70
69
  default: false,
71
70
  menuLabel: 'Skip Permissions'
72
71
  }
@@ -277,16 +276,14 @@ describe('the + button names and marks the launch it performs', () => {
277
276
  expect(newBtn().title).not.toContain('(Skip Permissions)');
278
277
  });
279
278
 
280
- it('DEF-35 / DEF-38 - the glyph follows the mode, one svg at a time', () => {
281
- // With the mode in force every variant suppresses itself, so this button
282
- // stops dropping a menu and launches on click - a plain + would be the one
283
- // one-click approval-free launch in the panel wearing no warning.
279
+ it('DEF-112 - the glyph is always +, one svg at a time', () => {
280
+ // The shield marks the menu entries that skip approval; the button that
281
+ // offers or launches stays neutral in every mode state. The mode is still
282
+ // named in the title, per DEF-36 above.
284
283
  expect(glyphs()).toEqual([addIcon.name]);
285
284
  panel.setModes({ skip: true });
286
285
  expect((panel as any)._visibleVariantCount()).toEqual(0);
287
- expect(glyphs()).toEqual([shieldIcon.name]);
288
- // `LabIcon.element` appends, so a button that is not cleared collects one
289
- // svg per settings change.
286
+ expect(glyphs()).toEqual([addIcon.name]);
290
287
  panel.setModes({ skip: false });
291
288
  expect(glyphs()).toEqual([addIcon.name]);
292
289
  });
@@ -79,13 +79,8 @@ describe('the providers barrel', () => {
79
79
  expect(mode.id.trim()).toBeTruthy();
80
80
  expect(mode.title.trim()).toBeTruthy();
81
81
  expect(mode.description.trim()).toBeTruthy();
82
- if (mode.kind === 'boolean') {
83
- // Every unsafe switch is off by default, without exception.
84
- expect(mode.default).toBe(false);
85
- } else {
86
- expect(mode.values).toBeDefined();
87
- expect(mode.values).toContain(mode.default as string);
88
- }
82
+ // Every unsafe switch is off by default, without exception.
83
+ expect(mode.default).toBe(false);
89
84
  }
90
85
  const ids = descriptor.launchModes.map(m => m.id);
91
86
  expect(new Set(ids).size).toEqual(ids.length);
package/src/core/modes.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  // Launch-mode resolution: which single mode token, if any, one launch carries.
2
2
  //
3
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.
4
+ // the rule that one launch carries at most one mode is worth asserting on its
5
+ // own, and this file pulls in no JupyterLab surface.
6
6
 
7
7
  import { ILaunchMode } from './types';
8
8
 
9
9
  /** A resolved launch mode, with what to call it and whether to warn. */
10
10
  export interface IResolvedLaunchMode {
11
11
  mode: ILaunchMode;
12
- /** What the menu should call it - the value, or the mode's own label. */
12
+ /** What the menu should call it - the mode's own label. */
13
13
  label: string;
14
14
  /** Whether this widens what the assistant may do without asking. */
15
15
  unsafe: boolean;
@@ -20,11 +20,8 @@ export interface IResolvedLaunchMode {
20
20
  *
21
21
  * The payload carries at most one mode, because the server maps one token to
22
22
  * one flag. `force` wins outright - that is what the menu's variant entries
23
- * do. Otherwise an enum mode moved off its default takes precedence over any
24
- * boolean switch: the enum is the explicit statement of intent, so a boolean
25
- * left on must not silently invert it. A boolean fires only while every enum
26
- * mode sits at its default. An enum at its default sends nothing - it is what
27
- * the CLI would do unasked.
23
+ * do. Otherwise the first switch that is on fires. Every mode is a boolean
24
+ * skip-approval switch, so "on" always means the same thing.
28
25
  */
29
26
  export function resolveLaunchMode(
30
27
  launchModes: ILaunchMode[],
@@ -35,18 +32,7 @@ export function resolveLaunchMode(
35
32
  return force;
36
33
  }
37
34
  for (const mode of launchModes) {
38
- const value = values[mode.id];
39
- if (
40
- mode.kind !== 'boolean' &&
41
- typeof value === 'string' &&
42
- value &&
43
- value !== mode.default
44
- ) {
45
- return `${mode.id}=${value}`;
46
- }
47
- }
48
- for (const mode of launchModes) {
49
- if (mode.kind === 'boolean' && values[mode.id] === true) {
35
+ if (values[mode.id] === true) {
50
36
  return mode.id;
51
37
  }
52
38
  }
@@ -71,23 +57,15 @@ export function resolvedLaunchModeEntry(
71
57
  if (!token) {
72
58
  return null;
73
59
  }
74
- const [id, value] = token.split('=');
75
- const mode = launchModes.find(m => m.id === id);
60
+ const mode = launchModes.find(m => m.id === token);
76
61
  if (!mode) {
77
62
  return null;
78
63
  }
79
- // A boolean mode with a menu label IS the assistant's skip-approval switch;
80
- // an enum only widens on the values its provider declared. `plan` resolves
81
- // to a mode and is stricter than the default, so "resolved at all" is the
82
- // wrong question to ask about danger.
83
- const unsafe = value
84
- ? (mode.unsafeValues ?? []).indexOf(value) !== -1
85
- : !!mode.menuLabel;
86
- // Name the VALUE where there is one - three enum values sharing the mode's
87
- // title render as one indistinguishable label.
64
+ // Carrying a menu label IS a boolean mode's declaration that it skips
65
+ // approval - it is what puts a variant in the menu.
88
66
  return {
89
67
  mode,
90
- label: value ?? mode.menuLabel ?? mode.title,
91
- unsafe
68
+ label: mode.menuLabel ?? mode.title,
69
+ unsafe: !!mode.menuLabel
92
70
  };
93
71
  }
package/src/core/panel.ts CHANGED
@@ -276,11 +276,9 @@ export class AssistantSessionsPanel extends Widget {
276
276
  setModes(modes: Record<string, boolean | string>): void {
277
277
  this._modes = { ...modes };
278
278
  // Menu labels are lazy and re-read this on every open; the button's title
279
- // is a written string, so it has to be rewritten here.
279
+ // is a written string, so it has to be rewritten here. The glyph needs no
280
+ // such hook - it is always "+", whatever mode is armed (DEF-112).
280
281
  this._renameNewButton?.();
281
- // Same for its glyph - a menu item re-reads its icon on every open, a
282
- // painted button does not.
283
- this._repaintNewIcon?.();
284
282
  }
285
283
 
286
284
  setColouredTabs(on: boolean): void {
@@ -344,26 +342,11 @@ export class AssistantSessionsPanel extends Widget {
344
342
  // after turning an approval-free mode on (DEF-38).
345
343
  this._renameNewButton = nameNewButton;
346
344
  nameNewButton();
347
- // The glyph follows the same rule the menu entries follow: where a mode
348
- // WIDENS what runs without asking, the mode's warning glyph wins over the
349
- // action's own (DEF-35). It matters most here - with a mode in force the
350
- // variants suppress themselves and this button launches on click, so a
351
- // plain "+" would be the only one-click approval-free launch in the panel
352
- // wearing no warning at all.
353
- const paintNewIcon = (): void => {
354
- const icon =
355
- (this._visibleVariantCount() === 0 ? this._variantIcon() : undefined) ??
356
- addIcon;
357
- // `LabIcon.element` appends into the container, so the previous glyph
358
- // has to go or the button would collect one svg per settings change.
359
- newBtn.replaceChildren();
360
- icon.element({ container: newBtn });
361
- };
362
- // Held for the same reason the title is: the shell is built before
363
- // settings arrive, so what is drawn here can only say "no mode in force"
364
- // and `setModes` has to repaint it (DEF-38).
365
- this._repaintNewIcon = paintNewIcon;
366
- paintNewIcon();
345
+ // The glyph is always "+", whatever mode is armed (DEF-112): the shield
346
+ // marks the menu ENTRIES that skip approval, while the button that offers
347
+ // or launches stays neutral - an armed mode is still named in the title
348
+ // above, so the launch never goes unnamed.
349
+ addIcon.element({ container: newBtn });
367
350
  newBtn.addEventListener('click', () => {
368
351
  // With a mode in force the variant items suppress themselves, leaving a
369
352
  // dropdown with one entry - two clicks for its only outcome. Do it.
@@ -1928,12 +1911,10 @@ export class AssistantSessionsPanel extends Widget {
1928
1911
  return commandId(this._descriptor.id, action);
1929
1912
  }
1930
1913
 
1931
- /** Boolean launch modes that produce a menu variant - the assistant's own
1914
+ /** Launch modes that produce a menu variant - the assistant's own
1932
1915
  * skip-approval switch, under the assistant's own name. */
1933
1916
  private get _variantModes(): ILaunchMode[] {
1934
- return this._descriptor.launchModes.filter(
1935
- m => m.kind === 'boolean' && !!m.menuLabel
1936
- );
1917
+ return this._descriptor.launchModes.filter(m => !!m.menuLabel);
1937
1918
  }
1938
1919
 
1939
1920
  /** The launch mode a PLAIN action resolves to right now, or null.
@@ -2656,8 +2637,6 @@ export class AssistantSessionsPanel extends Widget {
2656
2637
  private _newSessionMenu!: Menu;
2657
2638
  /** Re-writes the `+` button's title. Set once the shell exists. */
2658
2639
  private _renameNewButton: (() => void) | null = null;
2659
- /** Re-draws the `+` button's glyph. Set once the shell exists. */
2660
- private _repaintNewIcon: (() => void) | null = null;
2661
2640
  private _lastBranches: IBranch[] = [];
2662
2641
  private _lastBranchesCurrent = '';
2663
2642
  private _colouredTabs = true;
package/src/core/types.ts CHANGED
@@ -27,9 +27,8 @@ export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
27
27
  export type ColourSource = 'native' | 'derived' | 'none';
28
28
 
29
29
  /** One launch-behaviour setting of an assistant, in that assistant's own
30
- * terminology. `boolean` modes are the unsafe/skip-approval switches: they get
31
- * a settings toggle (off by default) AND a context-menu launch variant.
32
- * `enum` modes are settings-only choices passed through on every launch. */
30
+ * terminology. A launch mode is a boolean skip-approval switch: it gets a
31
+ * settings toggle (off by default) AND a context-menu launch variant. */
33
32
  export interface ILaunchMode {
34
33
  /** Settings key under `providers.<id>.` and launch-payload key. Uses the
35
34
  * assistant's own terminology, never a normalised core name. */
@@ -38,24 +37,12 @@ export interface ILaunchMode {
38
37
  title: string;
39
38
  /** Settings description. */
40
39
  description: string;
41
- kind: 'boolean' | 'enum';
42
- /** Allowed values, `enum` modes only. */
43
- values?: string[];
44
- /** Default value. Boolean modes default to `false` without exception. */
45
- default: boolean | string;
40
+ /** Default value - `false` without exception. */
41
+ default: boolean;
46
42
  /** Menu label for the launch variant this mode produces, e.g.
47
- * `Skip Permissions`. Boolean modes only; absent means settings-only. */
43
+ * `Skip Permissions`. Carrying one IS the mode's declaration that it skips
44
+ * approval - it is what puts a variant in the menu. */
48
45
  menuLabel?: string;
49
- /** `enum` modes only: the values that WIDEN what the assistant may do
50
- * without asking. The panel warns on these and names the value; every other
51
- * value is a settings choice and is marked as one.
52
- *
53
- * Declared per provider rather than inferred, because "a mode resolved at
54
- * all" is not the same question: Gemini's `plan` is read-only - stricter
55
- * than the default - and marking it the same as `yolo` says nothing about
56
- * either. A boolean mode needs no entry here; carrying a `menuLabel` is
57
- * already its declaration that it skips approval. */
58
- unsafeValues?: string[];
59
46
  }
60
47
 
61
48
  /** Everything the core needs to know about one assistant. Pure data - no
@@ -57,7 +57,6 @@ export const descriptor: IProviderDescriptor = {
57
57
  title: 'Dangerously skip permissions',
58
58
  description:
59
59
  '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.',
60
- kind: 'boolean',
61
60
  default: false,
62
61
  menuLabel: 'Skip Permissions'
63
62
  }
@@ -54,7 +54,6 @@ export const descriptor: IProviderDescriptor = {
54
54
  title: 'Bypass Codex approvals and sandbox',
55
55
  description:
56
56
  '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.',
57
- kind: 'boolean',
58
57
  default: false,
59
58
  menuLabel: 'Bypass Approvals'
60
59
  }
@@ -32,11 +32,10 @@ const geminiSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 65 65
32
32
  <path class="jp-icon3" fill="#616161" d="M32.447 0c.68 0 1.273.465 1.439 1.125a38.904 38.904 0 001.999 5.905c2.152 5 5.105 9.376 8.854 13.125 3.751 3.75 8.126 6.703 13.125 8.855a38.98 38.98 0 005.906 1.999c.66.166 1.124.758 1.124 1.438 0 .68-.464 1.273-1.125 1.439a38.902 38.902 0 00-5.905 1.999c-5 2.152-9.375 5.105-13.125 8.854-3.749 3.751-6.702 8.126-8.854 13.125a38.973 38.973 0 00-2 5.906 1.485 1.485 0 01-1.438 1.124c-.68 0-1.272-.464-1.438-1.125a38.913 38.913 0 00-2-5.905c-2.151-5-5.103-9.375-8.854-13.125-3.75-3.749-8.125-6.702-13.125-8.854a38.973 38.973 0 00-5.905-2A1.485 1.485 0 010 32.448c0-.68.465-1.272 1.125-1.438a38.903 38.903 0 005.905-2c5-2.151 9.376-5.104 13.125-8.854 3.75-3.749 6.703-8.125 8.855-13.125a38.972 38.972 0 001.999-5.905A1.485 1.485 0 0132.447 0z"/>
33
33
  </svg>`;
34
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
35
+ // Gemini's own name for its approval switch. Kept in the assistant's
36
+ // terminology rather than normalised, because it is both the settings key
37
37
  // under `providers.gemini.` and the mode token the server maps to a CLI flag.
38
38
  const YOLO_MODE = 'yoloMode';
39
- const APPROVAL_MODE = 'approvalMode';
40
39
 
41
40
  export const descriptor: IProviderDescriptor = {
42
41
  id: 'gemini',
@@ -60,20 +59,8 @@ export const descriptor: IProviderDescriptor = {
60
59
  title: 'YOLO mode',
61
60
  description:
62
61
  '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
62
  default: false,
65
63
  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
- // `plan` is read-only and `default` prompts, so neither widens anything.
76
- unsafeValues: ['auto_edit', 'yolo']
77
64
  }
78
65
  ],
79
66
  hasRemoteControl: false,
@@ -135,7 +135,6 @@ export const descriptor: IProviderDescriptor = {
135
135
  title: 'YOLO mode',
136
136
  description:
137
137
  '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.',
138
- kind: 'boolean',
139
138
  default: false,
140
139
  menuLabel: 'YOLO'
141
140
  }