jupyterlab_ai_code_assistants_extension 1.0.10 → 1.0.19

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.
@@ -3,7 +3,7 @@ export declare const assistantsIcon: LabIcon;
3
3
  export declare const starFilledIcon: LabIcon;
4
4
  export declare const refreshIcon: LabIcon;
5
5
  export declare const removeIcon: LabIcon;
6
- export declare const warningIcon: LabIcon;
6
+ export declare const shieldIcon: LabIcon;
7
7
  export declare const addIcon: LabIcon;
8
8
  export declare const branchIcon: LabIcon;
9
9
  export declare const switchIcon: LabIcon;
package/lib/core/icons.js CHANGED
@@ -37,21 +37,23 @@ export const removeIcon = new LabIcon({
37
37
  name: `${ICON_PREFIX}:remove`,
38
38
  svgstr: removeSvgStr
39
39
  });
40
- // Marks the unsafe launch variants. A shield reads as "protected", which is
41
- // the inverse of what these entries do, so the caution triangle carries them
42
- // instead. Material "warning" outline.
40
+ // Marks the unsafe launch variants. The shield is the established marker for
41
+ // an action that touches the permission system (the UAC pattern), and it is
42
+ // what the reference implementation used; Material "shield" half-fill, kept
43
+ // per user direction 2026-08-11 over the earlier caution triangle.
43
44
  //
44
45
  // 16x16 and `jp-icon-warn0`, like every other icon in these menus and unlike
45
46
  // them in colour: authored at 13x13 in `jp-icon3` it rendered smaller than
46
47
  // its neutral siblings AND in the identical grey, measured rgb(97,97,97) in
47
48
  // the light theme and rgb(189,189,189) in the dark one - the one glyph whose
48
- // whole job is to say "this differs" was the least conspicuous in the menu.
49
- const warningSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
50
- <path class="jp-icon-warn0" fill="#f57c00" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
49
+ // whole job is to say "this differs" was the least conspicuous in the menu
50
+ // (DEF-40, and the reason the reference's 13x13 grey is not copied verbatim).
51
+ const shieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
52
+ <path class="jp-icon-warn0" fill="#f57c00" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
51
53
  </svg>`;
52
- export const warningIcon = new LabIcon({
53
- name: `${ICON_PREFIX}:warning`,
54
- svgstr: warningSvgStr
54
+ export const shieldIcon = new LabIcon({
55
+ name: `${ICON_PREFIX}:shield`,
56
+ svgstr: shieldSvgStr
55
57
  });
56
58
  const addSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
57
59
  <path class="jp-icon3" fill="#616161" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
@@ -0,0 +1,12 @@
1
+ /** The ONE home for the Recent section's size bounds. `src/core/panel.ts`
2
+ * clamps with them, `src/index.ts` defaults with them, and
3
+ * `scripts/generate-schema.mjs` writes them into `schema/plugin.json`, so a
4
+ * change here is the only change needed.
5
+ *
6
+ * Keep this module import-free. The schema generator loads the compiled
7
+ * `lib/core/limits.js` under plain Node, exactly as it loads the provider
8
+ * barrel; a single browser-only import would break that.
9
+ */
10
+ export declare const DEFAULT_RECENT_LIMIT = 10;
11
+ export declare const MIN_RECENT_LIMIT = 1;
12
+ export declare const MAX_RECENT_LIMIT = 100;
@@ -0,0 +1,12 @@
1
+ /** The ONE home for the Recent section's size bounds. `src/core/panel.ts`
2
+ * clamps with them, `src/index.ts` defaults with them, and
3
+ * `scripts/generate-schema.mjs` writes them into `schema/plugin.json`, so a
4
+ * change here is the only change needed.
5
+ *
6
+ * Keep this module import-free. The schema generator loads the compiled
7
+ * `lib/core/limits.js` under plain Node, exactly as it loads the provider
8
+ * barrel; a single browser-only import would break that.
9
+ */
10
+ export const DEFAULT_RECENT_LIMIT = 10;
11
+ export const MIN_RECENT_LIMIT = 1;
12
+ export const MAX_RECENT_LIMIT = 100;
@@ -5,13 +5,6 @@ import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
5
5
  import { Message } from '@lumino/messaging';
6
6
  import { Widget } from '@lumino/widgets';
7
7
  import { IProviderDescriptor, IProviderHooks } from './types';
8
- /** Default and bounds for the Recent section's size. Exported because
9
- * `src/index.ts` reads the same setting and used to restate all three as
10
- * literals; `schema/plugin.json` declares the identical default, minimum and
11
- * maximum, so a change here is a change there. */
12
- export declare const DEFAULT_RECENT_LIMIT = 10;
13
- export declare const MIN_RECENT_LIMIT = 1;
14
- export declare const MAX_RECENT_LIMIT = 100;
15
8
  export type PresentationMode = 'name' | 'path';
16
9
  export declare const DEFAULT_PRESENTATION_MODE: PresentationMode;
17
10
  /** Widget id for one provider's panel - what the layout restorer keys on. */
@@ -68,6 +61,10 @@ export declare class AssistantSessionsPanel extends Widget {
68
61
  /** Inline, per-panel error. One provider's unreachable server must leave the
69
62
  * other panels alone, so nothing here is modal. */
70
63
  private _setInlineError;
64
+ /** A polled or refreshed fetch failed. Only these two paths own a retry, so
65
+ * only they may promise one. DEF-81: a client-side timeout means the server
66
+ * was REACHED and did not answer in time, which is a different thing to
67
+ * report than a server that is gone. */
71
68
  private _showError;
72
69
  /** A one-shot action failed. Nothing retries it, so the copy names what did
73
70
  * not happen instead of promising a retry the poll loop alone performs. */
package/lib/core/panel.js CHANGED
@@ -4,11 +4,12 @@ import { CommandRegistry } from '@lumino/commands';
4
4
  import { UUID } from '@lumino/coreutils';
5
5
  import { Widget } from '@lumino/widgets';
6
6
  import { ColourStore } from './colour';
7
- import { addIcon, branchIcon, filterIcon, providerIcon, refreshIcon, removeIcon, starFilledIcon, switchIcon, warningIcon } from './icons';
7
+ import { addIcon, branchIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, switchIcon } from './icons';
8
+ import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
8
9
  import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
9
10
  import { showManageSessionsPopup } from './popup';
10
11
  import { LOG_PREFIX } from './registry';
11
- import { isResponseStatus, requestProvider, withQuery } from './request';
12
+ import { isRequestTimeout, isResponseStatus, requestProvider, withQuery } from './request';
12
13
  import { TerminalManager } from './terminals';
13
14
  // Rows refresh on this cadence. It is also what refreshes whatever roster the
14
15
  // server caches for the context menu, so raising it past the server's cache
@@ -33,13 +34,6 @@ const REFRESH_SPIN_FLOOR_MS = 500;
33
34
  // faster cadence (bounded) so a new branch surfaces in seconds.
34
35
  const BRANCH_WATCH_INTERVAL_MS = 2000;
35
36
  const BRANCH_WATCH_MAX_ATTEMPTS = 90; // ~3 minutes
36
- /** Default and bounds for the Recent section's size. Exported because
37
- * `src/index.ts` reads the same setting and used to restate all three as
38
- * literals; `schema/plugin.json` declares the identical default, minimum and
39
- * maximum, so a change here is a change there. */
40
- export const DEFAULT_RECENT_LIMIT = 10;
41
- export const MIN_RECENT_LIMIT = 1;
42
- export const MAX_RECENT_LIMIT = 100;
43
37
  /** Conversations listed inline in a branch submenu before the popup takes over. */
44
38
  const INLINE_BRANCH_LIMIT = 5;
45
39
  export const DEFAULT_PRESENTATION_MODE = 'name';
@@ -528,10 +522,17 @@ export class AssistantSessionsPanel extends Widget {
528
522
  this._errorEl.textContent = message !== null && message !== void 0 ? message : '';
529
523
  this._errorEl.hidden = !message;
530
524
  }
525
+ /** A polled or refreshed fetch failed. Only these two paths own a retry, so
526
+ * only they may promise one. DEF-81: a client-side timeout means the server
527
+ * was REACHED and did not answer in time, which is a different thing to
528
+ * report than a server that is gone. */
531
529
  _showError(err) {
532
530
  const message = err instanceof Error ? err.message : String(err);
533
531
  this._logError(message);
534
- this._setInlineError(`Could not reach the server - retrying. ${message}`);
532
+ const copy = isRequestTimeout(err)
533
+ ? 'The server is not answering - retrying.'
534
+ : 'Could not reach the server - retrying.';
535
+ this._setInlineError(`${copy} ${message}`);
535
536
  }
536
537
  /** A one-shot action failed. Nothing retries it, so the copy names what did
537
538
  * not happen instead of promising a retry the poll loop alone performs. */
@@ -704,7 +705,6 @@ export class AssistantSessionsPanel extends Widget {
704
705
  bar.remove();
705
706
  message.classList.add('jp-AiAssistantsPanel-cleanupError');
706
707
  message.textContent = `Cleanup failed: ${err instanceof Error ? err.message : String(err)}`;
707
- this._showError(err);
708
708
  }
709
709
  }
710
710
  // ---------------------------------------------------------------- launch
@@ -790,7 +790,7 @@ export class AssistantSessionsPanel extends Widget {
790
790
  }
791
791
  const parentColour = this._terminals.colourFor(session);
792
792
  let name;
793
- if (this._descriptor.namingStrategy !== 'none') {
793
+ if (this._descriptor.promptsForBranchName) {
794
794
  const named = await InputDialog.getText({
795
795
  title: 'Branch Session',
796
796
  label: 'Name for the new session',
@@ -872,6 +872,18 @@ export class AssistantSessionsPanel extends Widget {
872
872
  * answers with the new id, so the branch exists before the terminal opens
873
873
  * and one refresh surfaces it - no watcher needed. */
874
874
  async _branchByServerCopy(session, name, force) {
875
+ // Let the parent's colour settle before the server reads it (DEF-74). The
876
+ // server copies the store entry while minting the fork, so a colour picked
877
+ // on the parent's tab moments earlier - still queued in this store's
878
+ // request chain, unsent - is not in the server's store yet and the branch
879
+ // is born wearing the PREVIOUS tint. Same gate as `_inheritColour`
880
+ // (DEF-31): a reload is a queued request like every other of this store's
881
+ // (DEF-30), so it is not sent until the capture ahead of it has been
882
+ // answered, and awaiting it is what makes the server's read fresh. Only
883
+ // inside the capture window, so an ordinary fork still costs no request.
884
+ if (this._colours.isPending(session.session_id)) {
885
+ await this._colours.load();
886
+ }
875
887
  try {
876
888
  const fork = await requestProvider(this._descriptor.id, 'branch', this._serverSettings, {
877
889
  method: 'POST',
@@ -1679,7 +1691,7 @@ export class AssistantSessionsPanel extends Widget {
1679
1691
  /** The glyph, only where the mode WIDENS what runs without asking. */
1680
1692
  _variantIcon() {
1681
1693
  var _a;
1682
- return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? warningIcon : undefined;
1694
+ return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
1683
1695
  }
1684
1696
  _setupCommands() {
1685
1697
  this._commands = new CommandRegistry();
@@ -1704,7 +1716,7 @@ export class AssistantSessionsPanel extends Widget {
1704
1716
  const verb = (_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, active())) !== null && _c !== void 0 ? _c : 'Resume';
1705
1717
  return variant ? `${verb} (${variant.label})` : verb;
1706
1718
  },
1707
- icon: () => { var _a; return (((_a = this._resumeVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? warningIcon : undefined); },
1719
+ icon: () => { var _a; return (((_a = this._resumeVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined); },
1708
1720
  execute: () => {
1709
1721
  const s = active();
1710
1722
  if (s) {
@@ -1715,7 +1727,7 @@ export class AssistantSessionsPanel extends Widget {
1715
1727
  for (const mode of this._variantModes) {
1716
1728
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
1717
1729
  label: `Resume (${mode.menuLabel})`,
1718
- icon: warningIcon,
1730
+ icon: shieldIcon,
1719
1731
  // Absent while the plain item already resolves to this same mode - it
1720
1732
  // would be a second entry building an identical launch.
1721
1733
  isVisible: () => !this._buildsSameLaunch(mode),
@@ -1732,13 +1744,13 @@ export class AssistantSessionsPanel extends Widget {
1732
1744
  });
1733
1745
  this._commands.addCommand(this._cmd(`new-session-${mode.id}`), {
1734
1746
  label: `New ${this._descriptor.label} Session (${mode.menuLabel})`,
1735
- icon: warningIcon,
1747
+ icon: shieldIcon,
1736
1748
  isVisible: () => !this._buildsSameLaunch(mode),
1737
1749
  execute: () => void this._newSession(mode.id)
1738
1750
  });
1739
1751
  this._commands.addCommand(this._cmd(`branch-session-${mode.id}`), {
1740
1752
  label: mode.menuLabel,
1741
- icon: warningIcon,
1753
+ icon: shieldIcon,
1742
1754
  isVisible: () => !this._buildsSameLaunch(mode),
1743
1755
  execute: () => void this._branchSession(mode.id)
1744
1756
  });
@@ -1761,7 +1773,7 @@ export class AssistantSessionsPanel extends Widget {
1761
1773
  }
1762
1774
  this._app.commands
1763
1775
  .execute('terminal:create-new', { cwd: rel })
1764
- .catch(err => this._showError(err));
1776
+ .catch(err => this._showActionError('Could not open a terminal for this session - try again.', err));
1765
1777
  }
1766
1778
  });
1767
1779
  this._commands.addCommand(this._cmd('show-in-filebrowser'), {
@@ -1779,7 +1791,7 @@ export class AssistantSessionsPanel extends Widget {
1779
1791
  }
1780
1792
  this._app.commands
1781
1793
  .execute('filebrowser:go-to-path', { path: rel })
1782
- .catch(err => this._showError(err));
1794
+ .catch(err => this._showActionError('Could not open the project folder - try again.', err));
1783
1795
  }
1784
1796
  });
1785
1797
  this._commands.addCommand(this._cmd('copy-path'), {
@@ -1828,7 +1840,7 @@ export class AssistantSessionsPanel extends Widget {
1828
1840
  // separator was added to protect, and both delete history that cannot be
1829
1841
  // recovered; leaving this one bare made the pair read as one dangerous
1830
1842
  // item beside an ordinary one (DEF-45).
1831
- icon: warningIcon,
1843
+ icon: shieldIcon,
1832
1844
  isVisible: () => { var _a, _b; return ((_b = (_a = active()) === null || _a === void 0 ? void 0 : _a.extra_sessions) !== null && _b !== void 0 ? _b : 0) > 0; },
1833
1845
  execute: () => {
1834
1846
  const s = active();
@@ -1897,7 +1909,7 @@ export class AssistantSessionsPanel extends Widget {
1897
1909
  // danger everywhere else in this menu - spending it on a branch glyph
1898
1910
  // left `Branch Session (Skip Permissions)` looking exactly as safe as
1899
1911
  // Show in File Browser while `Resume (Skip Permissions)` beside it wore
1900
- // the triangle. The branch glyph is the fallback, not the winner.
1912
+ // the shield. The branch glyph is the fallback, not the winner.
1901
1913
  icon: () => {
1902
1914
  var _a;
1903
1915
  return (_a = this._variantIcon()) !== null && _a !== void 0 ? _a : (this._visibleVariantCount() === 0 ? branchIcon : undefined);
@@ -18,13 +18,6 @@ export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
18
18
  /** Where a conversation's DEFAULT tab tint comes from. A user-set colour in
19
19
  * the extension's own write-back store always wins over all three. */
20
20
  export type ColourSource = 'native' | 'derived' | 'none';
21
- /** Who owns a branched conversation's name.
22
- *
23
- * - `launch-flag` - the CLI is told the name at launch and stamps it itself
24
- * - `server-side` - the server writes the name into the copied store entry
25
- * - `none` - the assistant has no naming surface, so no name is asked for
26
- */
27
- export type NamingStrategy = 'launch-flag' | 'server-side' | 'none';
28
21
  /** One launch-behaviour setting of an assistant, in that assistant's own
29
22
  * terminology. `boolean` modes are the unsafe/skip-approval switches: they get
30
23
  * a settings toggle (off by default) AND a context-menu launch variant.
@@ -76,7 +69,9 @@ export interface IProviderDescriptor {
76
69
  cliBinary: string;
77
70
  forkStrategy: ForkStrategy;
78
71
  colourSource: ColourSource;
79
- namingStrategy: NamingStrategy;
72
+ /** Whether the branch flow asks the user to name the new conversation.
73
+ * Where that name then GOES is `forkStrategy`'s job. */
74
+ promptsForBranchName: boolean;
80
75
  /** The CLI starts a brand-new conversation under an id we give it, so a
81
76
  * fresh session is identifiable from its argv on the first poll instead of
82
77
  * being an unknown terminal until it writes its store entry. Independent of
@@ -211,7 +206,7 @@ export interface ILaunchRequest {
211
206
  fork_session_id?: string;
212
207
  /** Parent conversation to fork from (`forkStrategy: 'native-command'`). */
213
208
  fork_from?: string;
214
- /** Name to stamp on the new conversation (`namingStrategy: 'launch-flag'`). */
209
+ /** Name to stamp on the new conversation (`promptsForBranchName: true`). */
215
210
  name?: string;
216
211
  /** The launch mode this action runs under, as one token in the assistant's
217
212
  * own terminology: a boolean mode is its bare `ILaunchMode.id`, an enum mode
package/lib/index.js CHANGED
@@ -4,7 +4,8 @@ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
4
4
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
5
5
  import { ITerminalTracker } from '@jupyterlab/terminal';
6
6
  import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
7
- import { AssistantSessionsPanel, DEFAULT_PRESENTATION_MODE, DEFAULT_RECENT_LIMIT, commandId, panelWidgetId } from './core/panel';
7
+ import { DEFAULT_RECENT_LIMIT } from './core/limits';
8
+ import { AssistantSessionsPanel, DEFAULT_PRESENTATION_MODE, commandId, panelWidgetId } from './core/panel';
8
9
  import { LOG_PREFIX, ProviderRegistry } from './core/registry';
9
10
  import { requestAPI } from './core/request';
10
11
  import { PROVIDERS } from './providers';
@@ -45,7 +45,7 @@ export const descriptor = {
45
45
  // `-n <name>` at launch: Claude stamps the name itself and re-stamps it on
46
46
  // every turn, which is the only way a fork's name survives - a title written
47
47
  // after the fact loses to the parent title the fork inherits.
48
- namingStrategy: 'launch-flag',
48
+ promptsForBranchName: true,
49
49
  mintsNewSessionId: true,
50
50
  launchModes: [
51
51
  {
@@ -40,7 +40,7 @@ export const descriptor = {
40
40
  colourSource: 'none',
41
41
  // Forks are unnamed - Codex has no naming flag and no writable title field,
42
42
  // so the panel never asks for a name it could not stamp anywhere.
43
- namingStrategy: 'none',
43
+ promptsForBranchName: false,
44
44
  // A bare `codex` mints its own thread id; there is no `--session-id`.
45
45
  mintsNewSessionId: false,
46
46
  launchModes: [
@@ -9,9 +9,9 @@
9
9
  // * `colourSource: 'none'` - no colour command, and nothing in the chat store
10
10
  // records one, so a tab tint only ever comes from the extension's own
11
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
12
+ // * `promptsForBranchName: true` - the branch flow asks for a name, which the
13
+ // server stamps into the copied chat file's `summary`, the field the CLI
14
+ // itself displays a conversation under; there is no naming flag to pass it to
15
15
  // * `mintsNewSessionId: true` - `--session-id <uuid>` starts a fresh
16
16
  // conversation under an id we chose, so a new session is identifiable from
17
17
  // its argv on the first poll instead of being an unknown terminal
@@ -46,7 +46,7 @@ export const descriptor = {
46
46
  colourSource: 'none',
47
47
  // The fork's name is written into the copy by the server; the CLI has no
48
48
  // naming flag to take it.
49
- namingStrategy: 'server-side',
49
+ promptsForBranchName: true,
50
50
  // `--session-id <uuid>` starts a conversation under an id we mint.
51
51
  mintsNewSessionId: true,
52
52
  launchModes: [
@@ -9,8 +9,8 @@
9
9
  // * `colourSource: 'derived'` - kimi has no `/color`, so the default tint is a
10
10
  // stable hash of the conversation id (a user-set colour still wins, per the
11
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
12
+ // * `promptsForBranchName: true` - the branch flow asks for a name, which the
13
+ // server stamps into the copied `state.json`, not passed to the CLI
14
14
  // * one launch mode, `--yolo`, under kimi's own name
15
15
  //
16
16
  // The only hook is the branch label: kimi's conversation titles are
@@ -112,7 +112,7 @@ export const descriptor = {
112
112
  colourSource: 'derived',
113
113
  // The fork's name is written into the copy's `state.json` by the server; the
114
114
  // CLI has no naming flag to take it.
115
- namingStrategy: 'server-side',
115
+ promptsForBranchName: true,
116
116
  // A bare `kimi` mints its own id and writes it on the first turn, and `-S`
117
117
  // only ever resumes - so a new conversation cannot be launched under an id
118
118
  // we chose, and is identified from the store on the next poll instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_ai_code_assistants_extension",
3
- "version": "1.0.10",
3
+ "version": "1.0.19",
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",
@@ -84,7 +84,12 @@ const CORE_DIR = path.resolve(__dirname, '..', 'core');
84
84
  /** Drop block and line comments, leaving code and string literals. */
85
85
  function stripComments(source: string): string {
86
86
  return source
87
- .replace(/\/\*[\s\S]*?\*\//g, '')
87
+ .replace(/\/\*[\s\S]*?\*\//g, match =>
88
+ // DEF-80: keep the comment's newlines, or every line after a block
89
+ // comment is reported at the wrong number - the licence header alone
90
+ // shifts a real violation by the length of the header.
91
+ '\n'.repeat((match.match(/\n/g) ?? []).length)
92
+ )
88
93
  .replace(/^[^\n]*?\/\/.*$/gm, line =>
89
94
  // Keep the part before a trailing `//`, so code on a commented line is
90
95
  // still scanned - but drop a whole-line comment entirely.
@@ -200,4 +205,14 @@ describe('the comment-stripping the scan relies on', () => {
200
205
  ASSISTANT_NAMES
201
206
  );
202
207
  });
208
+
209
+ it('reports a violation at its real line number, past a block comment', () => {
210
+ // DEF-80: the block comment used to be replaced by nothing, so every line
211
+ // after it was reported short by the comment's height - a violation under
212
+ // a header comment pointed at unrelated code.
213
+ const source = `/**\n * ported\n * from a comment\n */\nconst id = 'claude';`;
214
+ expect(offenders(source, ASSISTANT_NAMES)).toEqual([
215
+ [5, "const id = 'claude';"]
216
+ ]);
217
+ });
203
218
  });
@@ -42,7 +42,7 @@ jest.mock('../core/request', () => ({
42
42
  }));
43
43
 
44
44
  import { TAB_COLOUR_IDS, fnv1aColour } from '../core/colour';
45
- import { addIcon, branchIcon, warningIcon } from '../core/icons';
45
+ import { addIcon, branchIcon, shieldIcon } from '../core/icons';
46
46
  import { AssistantSessionsPanel, commandId } from '../core/panel';
47
47
  import { requestProvider } from '../core/request';
48
48
  import { IProviderDescriptor, ISession } from '../core/types';
@@ -59,7 +59,7 @@ const DESCRIPTOR: IProviderDescriptor = {
59
59
  cliBinary: 'testbed',
60
60
  forkStrategy: 'native-flag',
61
61
  colourSource: 'derived',
62
- namingStrategy: 'launch-flag',
62
+ promptsForBranchName: true,
63
63
  mintsNewSessionId: true,
64
64
  launchModes: [
65
65
  {
@@ -284,7 +284,7 @@ describe('the + button names and marks the launch it performs', () => {
284
284
  expect(glyphs()).toEqual([addIcon.name]);
285
285
  panel.setModes({ skip: true });
286
286
  expect((panel as any)._visibleVariantCount()).toEqual(0);
287
- expect(glyphs()).toEqual([warningIcon.name]);
287
+ expect(glyphs()).toEqual([shieldIcon.name]);
288
288
  // `LabIcon.element` appends, so a button that is not cleared collects one
289
289
  // svg per settings change.
290
290
  panel.setModes({ skip: false });
@@ -314,7 +314,7 @@ describe('DEF-35 / DEF-39 - the branch entry with no visible variants', () => {
314
314
  panel.setModes({ skip: true });
315
315
  // The suffix names the mode in words, but the icon slot is what marks
316
316
  // danger everywhere else in this menu.
317
- expect(cmds(panel).icon(id(panel, 'branch-session'), {})).toBe(warningIcon);
317
+ expect(cmds(panel).icon(id(panel, 'branch-session'), {})).toBe(shieldIcon);
318
318
  });
319
319
 
320
320
  it('goes back to naming the launch alone while the variants render', () => {
@@ -354,7 +354,7 @@ describe('DEF-40 - the warning glyph is sized and coloured to stand out', () =>
354
354
  it('renders at 16x16, like its neutral siblings', () => {
355
355
  // Authored at 13x13 it rendered SMALLER than every icon beside it - the one
356
356
  // glyph whose job is to say "this differs" was also the least visible.
357
- for (const icon of [warningIcon, addIcon, branchIcon]) {
357
+ for (const icon of [shieldIcon, addIcon, branchIcon]) {
358
358
  const el = svg(icon.svgstr);
359
359
  expect([
360
360
  icon.name,
@@ -365,11 +365,11 @@ describe('DEF-40 - the warning glyph is sized and coloured to stand out', () =>
365
365
  });
366
366
 
367
367
  it('paints in jp-icon-warn0 and not the neutral grey', () => {
368
- // `jp-icon3` is the theme's neutral: in it the triangle measured the same
368
+ // `jp-icon3` is the theme's neutral: in it the glyph measured the same
369
369
  // rgb(97,97,97) as the icons it was supposed to differ from.
370
- const path = svg(warningIcon.svgstr).querySelector('path')!;
370
+ const path = svg(shieldIcon.svgstr).querySelector('path')!;
371
371
  expect(path.getAttribute('class')).toEqual('jp-icon-warn0');
372
- expect(warningIcon.svgstr).not.toContain('jp-icon3');
372
+ expect(shieldIcon.svgstr).not.toContain('jp-icon3');
373
373
  });
374
374
  });
375
375
 
@@ -380,7 +380,7 @@ describe('DEF-45 - both destructive menu entries are marked alike', () => {
380
380
  const commands = (panel as any)._commands;
381
381
  expect(
382
382
  commands.icon(commandId(DESCRIPTOR.id, 'cleanup-parallel'), {})
383
- ).toBe(warningIcon);
383
+ ).toBe(shieldIcon);
384
384
  expect(commands.icon(commandId(DESCRIPTOR.id, 'remove'), {})).toBeDefined();
385
385
  });
386
386
  });
@@ -17,7 +17,6 @@ const EXPECTED_IDS = ['claude', 'codex', 'kimi', 'gemini'];
17
17
 
18
18
  const FORK_STRATEGIES = ['native-flag', 'native-command', 'server-copy'];
19
19
  const COLOUR_SOURCES = ['native', 'derived', 'none'];
20
- const NAMING_STRATEGIES = ['launch-flag', 'server-side', 'none'];
21
20
 
22
21
  function stub(id: string): IProviderModule {
23
22
  return {
@@ -30,7 +29,7 @@ function stub(id: string): IProviderModule {
30
29
  cliBinary: id,
31
30
  forkStrategy: 'server-copy',
32
31
  colourSource: 'none',
33
- namingStrategy: 'none',
32
+ promptsForBranchName: false,
34
33
  mintsNewSessionId: false,
35
34
  launchModes: [],
36
35
  hasRemoteControl: false,
@@ -65,7 +64,7 @@ describe('the providers barrel', () => {
65
64
  expect(descriptor.iconSvg).toContain('<svg');
66
65
  expect(FORK_STRATEGIES).toContain(descriptor.forkStrategy);
67
66
  expect(COLOUR_SOURCES).toContain(descriptor.colourSource);
68
- expect(NAMING_STRATEGIES).toContain(descriptor.namingStrategy);
67
+ expect(typeof descriptor.promptsForBranchName).toBe('boolean');
69
68
  expect(typeof descriptor.mintsNewSessionId).toBe('boolean');
70
69
  expect(typeof descriptor.hasRemoteControl).toBe('boolean');
71
70
  expect(typeof descriptor.hasBgAgents).toBe('boolean');
package/src/core/icons.ts CHANGED
@@ -48,22 +48,24 @@ export const removeIcon = new LabIcon({
48
48
  svgstr: removeSvgStr
49
49
  });
50
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.
51
+ // Marks the unsafe launch variants. The shield is the established marker for
52
+ // an action that touches the permission system (the UAC pattern), and it is
53
+ // what the reference implementation used; Material "shield" half-fill, kept
54
+ // per user direction 2026-08-11 over the earlier caution triangle.
54
55
  //
55
56
  // 16x16 and `jp-icon-warn0`, like every other icon in these menus and unlike
56
57
  // them in colour: authored at 13x13 in `jp-icon3` it rendered smaller than
57
58
  // its neutral siblings AND in the identical grey, measured rgb(97,97,97) in
58
59
  // the light theme and rgb(189,189,189) in the dark one - the one glyph whose
59
- // whole job is to say "this differs" was the least conspicuous in the menu.
60
- const warningSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
61
- <path class="jp-icon-warn0" fill="#f57c00" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
60
+ // whole job is to say "this differs" was the least conspicuous in the menu
61
+ // (DEF-40, and the reason the reference's 13x13 grey is not copied verbatim).
62
+ const shieldSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
63
+ <path class="jp-icon-warn0" fill="#f57c00" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
62
64
  </svg>`;
63
65
 
64
- export const warningIcon = new LabIcon({
65
- name: `${ICON_PREFIX}:warning`,
66
- svgstr: warningSvgStr
66
+ export const shieldIcon = new LabIcon({
67
+ name: `${ICON_PREFIX}:shield`,
68
+ svgstr: shieldSvgStr
67
69
  });
68
70
 
69
71
  const addSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
@@ -0,0 +1,12 @@
1
+ /** The ONE home for the Recent section's size bounds. `src/core/panel.ts`
2
+ * clamps with them, `src/index.ts` defaults with them, and
3
+ * `scripts/generate-schema.mjs` writes them into `schema/plugin.json`, so a
4
+ * change here is the only change needed.
5
+ *
6
+ * Keep this module import-free. The schema generator loads the compiled
7
+ * `lib/core/limits.js` under plain Node, exactly as it loads the provider
8
+ * barrel; a single browser-only import would break that.
9
+ */
10
+ export const DEFAULT_RECENT_LIMIT = 10;
11
+ export const MIN_RECENT_LIMIT = 1;
12
+ export const MAX_RECENT_LIMIT = 100;
package/src/core/panel.ts CHANGED
@@ -30,10 +30,15 @@ import {
30
30
  providerIcon,
31
31
  refreshIcon,
32
32
  removeIcon,
33
+ shieldIcon,
33
34
  starFilledIcon,
34
- switchIcon,
35
- warningIcon
35
+ switchIcon
36
36
  } from './icons';
37
+ import {
38
+ DEFAULT_RECENT_LIMIT,
39
+ MAX_RECENT_LIMIT,
40
+ MIN_RECENT_LIMIT
41
+ } from './limits';
37
42
  import {
38
43
  IResolvedLaunchMode,
39
44
  resolveLaunchMode,
@@ -41,7 +46,12 @@ import {
41
46
  } from './modes';
42
47
  import { showManageSessionsPopup } from './popup';
43
48
  import { LOG_PREFIX } from './registry';
44
- import { isResponseStatus, requestProvider, withQuery } from './request';
49
+ import {
50
+ isRequestTimeout,
51
+ isResponseStatus,
52
+ requestProvider,
53
+ withQuery
54
+ } from './request';
45
55
  import { TerminalManager } from './terminals';
46
56
  import {
47
57
  IBranch,
@@ -83,13 +93,6 @@ const REFRESH_SPIN_FLOOR_MS = 500;
83
93
  // faster cadence (bounded) so a new branch surfaces in seconds.
84
94
  const BRANCH_WATCH_INTERVAL_MS = 2_000;
85
95
  const BRANCH_WATCH_MAX_ATTEMPTS = 90; // ~3 minutes
86
- /** Default and bounds for the Recent section's size. Exported because
87
- * `src/index.ts` reads the same setting and used to restate all three as
88
- * literals; `schema/plugin.json` declares the identical default, minimum and
89
- * maximum, so a change here is a change there. */
90
- export const DEFAULT_RECENT_LIMIT = 10;
91
- export const MIN_RECENT_LIMIT = 1;
92
- export const MAX_RECENT_LIMIT = 100;
93
96
  /** Conversations listed inline in a branch submenu before the popup takes over. */
94
97
  const INLINE_BRANCH_LIMIT = 5;
95
98
 
@@ -601,10 +604,17 @@ export class AssistantSessionsPanel extends Widget {
601
604
  this._errorEl.hidden = !message;
602
605
  }
603
606
 
607
+ /** A polled or refreshed fetch failed. Only these two paths own a retry, so
608
+ * only they may promise one. DEF-81: a client-side timeout means the server
609
+ * was REACHED and did not answer in time, which is a different thing to
610
+ * report than a server that is gone. */
604
611
  private _showError(err: unknown): void {
605
612
  const message = err instanceof Error ? err.message : String(err);
606
613
  this._logError(message);
607
- this._setInlineError(`Could not reach the server - retrying. ${message}`);
614
+ const copy = isRequestTimeout(err)
615
+ ? 'The server is not answering - retrying.'
616
+ : 'Could not reach the server - retrying.';
617
+ this._setInlineError(`${copy} ${message}`);
608
618
  }
609
619
 
610
620
  /** A one-shot action failed. Nothing retries it, so the copy names what did
@@ -815,7 +825,6 @@ export class AssistantSessionsPanel extends Widget {
815
825
  message.textContent = `Cleanup failed: ${
816
826
  err instanceof Error ? err.message : String(err)
817
827
  }`;
818
- this._showError(err);
819
828
  }
820
829
  }
821
830
 
@@ -916,7 +925,7 @@ export class AssistantSessionsPanel extends Widget {
916
925
  const parentColour = this._terminals.colourFor(session);
917
926
 
918
927
  let name: string | undefined;
919
- if (this._descriptor.namingStrategy !== 'none') {
928
+ if (this._descriptor.promptsForBranchName) {
920
929
  const named = await InputDialog.getText({
921
930
  title: 'Branch Session',
922
931
  label: 'Name for the new session',
@@ -1019,6 +1028,18 @@ export class AssistantSessionsPanel extends Widget {
1019
1028
  name: string | undefined,
1020
1029
  force: string | undefined
1021
1030
  ): Promise<void> {
1031
+ // Let the parent's colour settle before the server reads it (DEF-74). The
1032
+ // server copies the store entry while minting the fork, so a colour picked
1033
+ // on the parent's tab moments earlier - still queued in this store's
1034
+ // request chain, unsent - is not in the server's store yet and the branch
1035
+ // is born wearing the PREVIOUS tint. Same gate as `_inheritColour`
1036
+ // (DEF-31): a reload is a queued request like every other of this store's
1037
+ // (DEF-30), so it is not sent until the capture ahead of it has been
1038
+ // answered, and awaiting it is what makes the server's read fresh. Only
1039
+ // inside the capture window, so an ordinary fork still costs no request.
1040
+ if (this._colours.isPending(session.session_id)) {
1041
+ await this._colours.load();
1042
+ }
1022
1043
  try {
1023
1044
  const fork = await requestProvider<IForkResponse>(
1024
1045
  this._descriptor.id,
@@ -1975,7 +1996,7 @@ export class AssistantSessionsPanel extends Widget {
1975
1996
 
1976
1997
  /** The glyph, only where the mode WIDENS what runs without asking. */
1977
1998
  private _variantIcon(): LabIcon | undefined {
1978
- return this._resolvedVariant()?.unsafe ? warningIcon : undefined;
1999
+ return this._resolvedVariant()?.unsafe ? shieldIcon : undefined;
1979
2000
  }
1980
2001
 
1981
2002
  private _setupCommands(): void {
@@ -2003,7 +2024,7 @@ export class AssistantSessionsPanel extends Widget {
2003
2024
  const verb = this._hooks.resumeLabel?.(active()) ?? 'Resume';
2004
2025
  return variant ? `${verb} (${variant.label})` : verb;
2005
2026
  },
2006
- icon: () => (this._resumeVariant()?.unsafe ? warningIcon : undefined),
2027
+ icon: () => (this._resumeVariant()?.unsafe ? shieldIcon : undefined),
2007
2028
  execute: () => {
2008
2029
  const s = active();
2009
2030
  if (s) {
@@ -2015,7 +2036,7 @@ export class AssistantSessionsPanel extends Widget {
2015
2036
  for (const mode of this._variantModes) {
2016
2037
  this._commands.addCommand(this._cmd(`resume-${mode.id}`), {
2017
2038
  label: `Resume (${mode.menuLabel})`,
2018
- icon: warningIcon,
2039
+ icon: shieldIcon,
2019
2040
  // Absent while the plain item already resolves to this same mode - it
2020
2041
  // would be a second entry building an identical launch.
2021
2042
  isVisible: () => !this._buildsSameLaunch(mode),
@@ -2032,13 +2053,13 @@ export class AssistantSessionsPanel extends Widget {
2032
2053
  });
2033
2054
  this._commands.addCommand(this._cmd(`new-session-${mode.id}`), {
2034
2055
  label: `New ${this._descriptor.label} Session (${mode.menuLabel})`,
2035
- icon: warningIcon,
2056
+ icon: shieldIcon,
2036
2057
  isVisible: () => !this._buildsSameLaunch(mode),
2037
2058
  execute: () => void this._newSession(mode.id)
2038
2059
  });
2039
2060
  this._commands.addCommand(this._cmd(`branch-session-${mode.id}`), {
2040
2061
  label: mode.menuLabel as string,
2041
- icon: warningIcon,
2062
+ icon: shieldIcon,
2042
2063
  isVisible: () => !this._buildsSameLaunch(mode),
2043
2064
  execute: () => void this._branchSession(mode.id)
2044
2065
  });
@@ -2065,7 +2086,12 @@ export class AssistantSessionsPanel extends Widget {
2065
2086
  }
2066
2087
  this._app.commands
2067
2088
  .execute('terminal:create-new', { cwd: rel })
2068
- .catch(err => this._showError(err));
2089
+ .catch(err =>
2090
+ this._showActionError(
2091
+ 'Could not open a terminal for this session - try again.',
2092
+ err
2093
+ )
2094
+ );
2069
2095
  }
2070
2096
  });
2071
2097
 
@@ -2087,7 +2113,12 @@ export class AssistantSessionsPanel extends Widget {
2087
2113
  }
2088
2114
  this._app.commands
2089
2115
  .execute('filebrowser:go-to-path', { path: rel })
2090
- .catch(err => this._showError(err));
2116
+ .catch(err =>
2117
+ this._showActionError(
2118
+ 'Could not open the project folder - try again.',
2119
+ err
2120
+ )
2121
+ );
2091
2122
  }
2092
2123
  });
2093
2124
 
@@ -2140,7 +2171,7 @@ export class AssistantSessionsPanel extends Widget {
2140
2171
  // separator was added to protect, and both delete history that cannot be
2141
2172
  // recovered; leaving this one bare made the pair read as one dangerous
2142
2173
  // item beside an ordinary one (DEF-45).
2143
- icon: warningIcon,
2174
+ icon: shieldIcon,
2144
2175
  isVisible: () => (active()?.extra_sessions ?? 0) > 0,
2145
2176
  execute: () => {
2146
2177
  const s = active();
@@ -2213,7 +2244,7 @@ export class AssistantSessionsPanel extends Widget {
2213
2244
  // danger everywhere else in this menu - spending it on a branch glyph
2214
2245
  // left `Branch Session (Skip Permissions)` looking exactly as safe as
2215
2246
  // Show in File Browser while `Resume (Skip Permissions)` beside it wore
2216
- // the triangle. The branch glyph is the fallback, not the winner.
2247
+ // the shield. The branch glyph is the fallback, not the winner.
2217
2248
  icon: () =>
2218
2249
  this._variantIcon() ??
2219
2250
  (this._visibleVariantCount() === 0 ? branchIcon : undefined),
package/src/core/types.ts CHANGED
@@ -26,14 +26,6 @@ export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
26
26
  * the extension's own write-back store always wins over all three. */
27
27
  export type ColourSource = 'native' | 'derived' | 'none';
28
28
 
29
- /** Who owns a branched conversation's name.
30
- *
31
- * - `launch-flag` - the CLI is told the name at launch and stamps it itself
32
- * - `server-side` - the server writes the name into the copied store entry
33
- * - `none` - the assistant has no naming surface, so no name is asked for
34
- */
35
- export type NamingStrategy = 'launch-flag' | 'server-side' | 'none';
36
-
37
29
  /** One launch-behaviour setting of an assistant, in that assistant's own
38
30
  * terminology. `boolean` modes are the unsafe/skip-approval switches: they get
39
31
  * a settings toggle (off by default) AND a context-menu launch variant.
@@ -86,7 +78,9 @@ export interface IProviderDescriptor {
86
78
  cliBinary: string;
87
79
  forkStrategy: ForkStrategy;
88
80
  colourSource: ColourSource;
89
- namingStrategy: NamingStrategy;
81
+ /** Whether the branch flow asks the user to name the new conversation.
82
+ * Where that name then GOES is `forkStrategy`'s job. */
83
+ promptsForBranchName: boolean;
90
84
  /** The CLI starts a brand-new conversation under an id we give it, so a
91
85
  * fresh session is identifiable from its argv on the first poll instead of
92
86
  * being an unknown terminal until it writes its store entry. Independent of
@@ -235,7 +229,7 @@ export interface ILaunchRequest {
235
229
  fork_session_id?: string;
236
230
  /** Parent conversation to fork from (`forkStrategy: 'native-command'`). */
237
231
  fork_from?: string;
238
- /** Name to stamp on the new conversation (`namingStrategy: 'launch-flag'`). */
232
+ /** Name to stamp on the new conversation (`promptsForBranchName: true`). */
239
233
  name?: string;
240
234
  /** The launch mode this action runs under, as one token in the assistant's
241
235
  * own terminology: a boolean mode is its bare `ILaunchMode.id`, an enum mode
package/src/index.ts CHANGED
@@ -11,10 +11,10 @@ import { ITerminalTracker } from '@jupyterlab/terminal';
11
11
  import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
12
12
  import { IDisposable } from '@lumino/disposable';
13
13
 
14
+ import { DEFAULT_RECENT_LIMIT } from './core/limits';
14
15
  import {
15
16
  AssistantSessionsPanel,
16
17
  DEFAULT_PRESENTATION_MODE,
17
- DEFAULT_RECENT_LIMIT,
18
18
  PresentationMode,
19
19
  commandId,
20
20
  panelWidgetId
@@ -49,7 +49,7 @@ export const descriptor: IProviderDescriptor = {
49
49
  // `-n <name>` at launch: Claude stamps the name itself and re-stamps it on
50
50
  // every turn, which is the only way a fork's name survives - a title written
51
51
  // after the fact loses to the parent title the fork inherits.
52
- namingStrategy: 'launch-flag',
52
+ promptsForBranchName: true,
53
53
  mintsNewSessionId: true,
54
54
  launchModes: [
55
55
  {
@@ -45,7 +45,7 @@ export const descriptor: IProviderDescriptor = {
45
45
  colourSource: 'none',
46
46
  // Forks are unnamed - Codex has no naming flag and no writable title field,
47
47
  // so the panel never asks for a name it could not stamp anywhere.
48
- namingStrategy: 'none',
48
+ promptsForBranchName: false,
49
49
  // A bare `codex` mints its own thread id; there is no `--session-id`.
50
50
  mintsNewSessionId: false,
51
51
  launchModes: [
@@ -9,9 +9,9 @@
9
9
  // * `colourSource: 'none'` - no colour command, and nothing in the chat store
10
10
  // records one, so a tab tint only ever comes from the extension's own
11
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
12
+ // * `promptsForBranchName: true` - the branch flow asks for a name, which the
13
+ // server stamps into the copied chat file's `summary`, the field the CLI
14
+ // itself displays a conversation under; there is no naming flag to pass it to
15
15
  // * `mintsNewSessionId: true` - `--session-id <uuid>` starts a fresh
16
16
  // conversation under an id we chose, so a new session is identifiable from
17
17
  // its argv on the first poll instead of being an unknown terminal
@@ -51,7 +51,7 @@ export const descriptor: IProviderDescriptor = {
51
51
  colourSource: 'none',
52
52
  // The fork's name is written into the copy by the server; the CLI has no
53
53
  // naming flag to take it.
54
- namingStrategy: 'server-side',
54
+ promptsForBranchName: true,
55
55
  // `--session-id <uuid>` starts a conversation under an id we mint.
56
56
  mintsNewSessionId: true,
57
57
  launchModes: [
@@ -9,8 +9,8 @@
9
9
  // * `colourSource: 'derived'` - kimi has no `/color`, so the default tint is a
10
10
  // stable hash of the conversation id (a user-set colour still wins, per the
11
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
12
+ // * `promptsForBranchName: true` - the branch flow asks for a name, which the
13
+ // server stamps into the copied `state.json`, not passed to the CLI
14
14
  // * one launch mode, `--yolo`, under kimi's own name
15
15
  //
16
16
  // The only hook is the branch label: kimi's conversation titles are
@@ -124,7 +124,7 @@ export const descriptor: IProviderDescriptor = {
124
124
  colourSource: 'derived',
125
125
  // The fork's name is written into the copy's `state.json` by the server; the
126
126
  // CLI has no naming flag to take it.
127
- namingStrategy: 'server-side',
127
+ promptsForBranchName: true,
128
128
  // A bare `kimi` mints its own id and writes it on the first turn, and `-S`
129
129
  // only ever resumes - so a new conversation cannot be launched under an id
130
130
  // we chose, and is identified from the store on the next poll instead.