mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
feat: 添加工具类命令(teleport、recap、break-cache、env、tui 等)
- /teleport: 从 claude.ai 恢复会话 - /recap: 生成会话摘要 - /break-cache: 提示缓存管理(once/always/off/status) - /env: 环境信息展示(含密钥脱敏) - /tui: 无闪烁 TUI 模式管理 - /onboarding: 引导流程 - /perf-issue: 性能问题诊断 - /debug-tool-call: 工具调用调试 - /usage: 用量统计(合并 /cost 和 /stats 别名) Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
288
src/commands/onboarding/__tests__/onboarding.test.tsx
Normal file
288
src/commands/onboarding/__tests__/onboarding.test.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
import * as React from 'react';
|
||||
import { logMock } from '../../../../tests/mocks/log';
|
||||
import { debugMock } from '../../../../tests/mocks/debug';
|
||||
|
||||
// Pre-import real ink so we can fall through after this suite. Bun's
|
||||
// mock.module is process-global / last-write-wins; without delegation the
|
||||
// stub Box/Pane/Text/useTheme leak into other test files (e.g.
|
||||
// AgentsPlatformView.test.tsx) that need real ink components.
|
||||
const _realOnboardingInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||
let _useStubInkForOnboarding = true;
|
||||
afterAll(() => {
|
||||
_useStubInkForOnboarding = false;
|
||||
});
|
||||
|
||||
mock.module('bun:bundle', () => ({
|
||||
feature: (_name: string) => false,
|
||||
}));
|
||||
|
||||
mock.module('src/utils/log.ts', logMock);
|
||||
mock.module('src/utils/debug.ts', debugMock);
|
||||
|
||||
const loggedEvents: Array<{ name: string; payload: unknown }> = [];
|
||||
mock.module('src/services/analytics/index.js', () => ({
|
||||
logEvent: (name: string, payload: unknown) => {
|
||||
loggedEvents.push({ name, payload });
|
||||
},
|
||||
}));
|
||||
|
||||
// In-memory config used by the global/project config helpers so the
|
||||
// command's persistence path is exercised without touching disk.
|
||||
const fakeGlobalConfig: {
|
||||
theme?: string;
|
||||
hasCompletedOnboarding?: boolean;
|
||||
lastOnboardingVersion?: string;
|
||||
} = {};
|
||||
const fakeProjectConfig: { hasTrustDialogAccepted?: boolean } = {};
|
||||
|
||||
mock.module('src/utils/config.js', () => ({
|
||||
getGlobalConfig: () => ({ ...fakeGlobalConfig }),
|
||||
saveGlobalConfig: (updater: (cur: typeof fakeGlobalConfig) => typeof fakeGlobalConfig) => {
|
||||
Object.assign(fakeGlobalConfig, updater({ ...fakeGlobalConfig }));
|
||||
},
|
||||
saveCurrentProjectConfig: (updater: (cur: typeof fakeProjectConfig) => typeof fakeProjectConfig) => {
|
||||
Object.assign(fakeProjectConfig, updater({ ...fakeProjectConfig }));
|
||||
},
|
||||
}));
|
||||
|
||||
// Stub heavy theme + ink imports — the launcher only references them for
|
||||
// the `theme` subcommand JSX render path. Spread real ink so when the flag
|
||||
// flips off in afterAll, later test files see real components.
|
||||
mock.module('@anthropic/ink', () => {
|
||||
if (_useStubInkForOnboarding) {
|
||||
return {
|
||||
..._realOnboardingInkMod,
|
||||
Box: ({ children }: { children?: React.ReactNode }) => React.createElement('box', null, children),
|
||||
Pane: ({ children }: { children?: React.ReactNode }) => React.createElement('pane', null, children),
|
||||
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('text', null, children),
|
||||
useTheme: () => ['dark', (_t: string) => undefined],
|
||||
};
|
||||
}
|
||||
return _realOnboardingInkMod;
|
||||
});
|
||||
|
||||
mock.module('src/components/ThemePicker.js', () => ({
|
||||
ThemePicker: () => React.createElement('theme-picker'),
|
||||
}));
|
||||
|
||||
import { callOnboarding, parseSubcommand, type OnboardingSubcommand } from '../launchOnboarding.js';
|
||||
import onboardingCommand from '../index.js';
|
||||
import type { LocalJSXCommandContext } from '../../../types/command.js';
|
||||
|
||||
type DoneCall = { msg?: string; opts?: { display?: string } };
|
||||
|
||||
function makeContext(): LocalJSXCommandContext {
|
||||
return {} as unknown as LocalJSXCommandContext;
|
||||
}
|
||||
|
||||
function makeOnDone(): {
|
||||
fn: (msg?: string, opts?: { display?: string }) => void;
|
||||
calls: DoneCall[];
|
||||
} {
|
||||
const calls: DoneCall[] = [];
|
||||
return {
|
||||
fn: (msg, opts) => {
|
||||
calls.push({ msg, opts });
|
||||
},
|
||||
calls,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
loggedEvents.length = 0;
|
||||
for (const k of Object.keys(fakeGlobalConfig)) delete (fakeGlobalConfig as Record<string, unknown>)[k];
|
||||
for (const k of Object.keys(fakeProjectConfig)) delete (fakeProjectConfig as Record<string, unknown>)[k];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loggedEvents.length = 0;
|
||||
});
|
||||
|
||||
describe('onboarding command metadata', () => {
|
||||
test('has correct name and description', () => {
|
||||
expect(onboardingCommand.name).toBe('onboarding');
|
||||
expect(onboardingCommand.description).toContain('first-run setup');
|
||||
});
|
||||
|
||||
test('is local-jsx, enabled, visible, not bridge-safe', () => {
|
||||
expect(onboardingCommand.type).toBe('local-jsx');
|
||||
expect(onboardingCommand.isEnabled?.()).toBe(true);
|
||||
expect(onboardingCommand.isHidden).toBe(false);
|
||||
expect(onboardingCommand.bridgeSafe).toBe(false);
|
||||
});
|
||||
|
||||
test('bridge invocation always rejected with an explanation', () => {
|
||||
const reason = onboardingCommand.getBridgeInvocationError?.('full');
|
||||
expect(reason).toBeTruthy();
|
||||
expect(reason).toContain('bridge');
|
||||
});
|
||||
|
||||
test('has descriptive argumentHint listing subcommands', () => {
|
||||
expect(onboardingCommand.argumentHint).toBe('[full|theme|trust|model|mcp|status]');
|
||||
});
|
||||
|
||||
test('load() returns a module with a call() function', async () => {
|
||||
if (onboardingCommand.type !== 'local-jsx') {
|
||||
throw new Error('expected local-jsx command');
|
||||
}
|
||||
const mod = await onboardingCommand.load();
|
||||
expect(typeof mod.call).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSubcommand', () => {
|
||||
test.each<[string, OnboardingSubcommand]>([
|
||||
['', 'full'],
|
||||
[' ', 'full'],
|
||||
['full', 'full'],
|
||||
['FULL', 'full'],
|
||||
['reset', 'full'],
|
||||
['theme', 'theme'],
|
||||
['trust', 'trust'],
|
||||
['model', 'model'],
|
||||
['mcp', 'mcp'],
|
||||
['status', 'status'],
|
||||
])('parses %p → %p', (input, expected) => {
|
||||
expect(parseSubcommand(input)).toEqual({ sub: expected });
|
||||
});
|
||||
|
||||
test('unknown arg returns full + unknownArg', () => {
|
||||
expect(parseSubcommand('garbage')).toEqual({
|
||||
sub: 'full',
|
||||
unknownArg: 'garbage',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('callOnboarding behavior', () => {
|
||||
test('full (no args) clears hasCompletedOnboarding and emits system message', async () => {
|
||||
fakeGlobalConfig.hasCompletedOnboarding = true;
|
||||
const { fn, calls } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), '');
|
||||
expect(result).toBeNull();
|
||||
expect(fakeGlobalConfig.hasCompletedOnboarding).toBe(false);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.opts?.display).toBe('system');
|
||||
expect(calls[0]?.msg).toContain('Onboarding flag cleared');
|
||||
expect(loggedEvents.some(e => e.name === 'tengu_onboarding_step')).toBe(true);
|
||||
});
|
||||
|
||||
test('reset alias also runs the full path', async () => {
|
||||
fakeGlobalConfig.hasCompletedOnboarding = true;
|
||||
const { fn } = makeOnDone();
|
||||
await callOnboarding(fn, makeContext(), 'reset');
|
||||
expect(fakeGlobalConfig.hasCompletedOnboarding).toBe(false);
|
||||
});
|
||||
|
||||
test('theme subcommand returns a React element (theme picker)', async () => {
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'theme');
|
||||
expect(React.isValidElement(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('trust subcommand clears project trust and notifies', async () => {
|
||||
fakeProjectConfig.hasTrustDialogAccepted = true;
|
||||
const { fn, calls } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'trust');
|
||||
expect(result).toBeNull();
|
||||
expect(fakeProjectConfig.hasTrustDialogAccepted).toBe(false);
|
||||
expect(calls[0]?.msg).toContain('trust cleared');
|
||||
});
|
||||
|
||||
test('model subcommand prints /model deferral hint', async () => {
|
||||
const { fn, calls } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'model');
|
||||
expect(result).toBeNull();
|
||||
expect(calls[0]?.msg).toContain('/model');
|
||||
});
|
||||
|
||||
test('mcp subcommand prints MCP setup hints', async () => {
|
||||
const { fn, calls } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'mcp');
|
||||
expect(result).toBeNull();
|
||||
expect(calls[0]?.msg).toContain('mcp add');
|
||||
expect(calls[0]?.msg).toContain('.mcp.json');
|
||||
});
|
||||
|
||||
test('status subcommand renders state view (React element)', async () => {
|
||||
fakeGlobalConfig.theme = 'dark';
|
||||
fakeGlobalConfig.hasCompletedOnboarding = true;
|
||||
fakeGlobalConfig.lastOnboardingVersion = '2.1.888';
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'status');
|
||||
expect(React.isValidElement(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('status subcommand falls back to (unset) for missing values', async () => {
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'status');
|
||||
expect(React.isValidElement(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('status JSX exposes theme/version values via props', async () => {
|
||||
fakeGlobalConfig.theme = 'light';
|
||||
fakeGlobalConfig.hasCompletedOnboarding = true;
|
||||
fakeGlobalConfig.lastOnboardingVersion = '1.2.3';
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'status');
|
||||
if (!React.isValidElement(result)) throw new Error('expected element');
|
||||
const el = result as React.ReactElement<{
|
||||
theme: string;
|
||||
hasCompletedOnboarding: boolean;
|
||||
lastOnboardingVersion: string;
|
||||
}>;
|
||||
expect(el.props.theme).toBe('light');
|
||||
expect(el.props.hasCompletedOnboarding).toBe(true);
|
||||
expect(el.props.lastOnboardingVersion).toBe('1.2.3');
|
||||
});
|
||||
|
||||
test('theme JSX wires onDone callback through ThemeSubcommand props', async () => {
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'theme');
|
||||
if (!React.isValidElement(result)) throw new Error('expected element');
|
||||
const el = result as React.ReactElement<{ onDone: (msg: string) => void }>;
|
||||
expect(typeof el.props.onDone).toBe('function');
|
||||
});
|
||||
|
||||
test('rendering ThemeSubcommand executes its body once', () => {
|
||||
// Pull the ThemeSubcommand render path through React.createElement so its
|
||||
// body (useTheme + ThemePicker JSX) executes under coverage.
|
||||
const result = callOnboarding(() => undefined, makeContext(), 'theme');
|
||||
return result.then(node => {
|
||||
if (!React.isValidElement(node)) throw new Error('not element');
|
||||
// Render the inner element by invoking its component function once.
|
||||
const Comp = (node as React.ReactElement).type as (p: unknown) => React.ReactNode;
|
||||
const rendered = Comp((node as React.ReactElement).props);
|
||||
expect(rendered).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('rendering StatusView executes its body once', async () => {
|
||||
const { fn } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'status');
|
||||
if (!React.isValidElement(result)) throw new Error('not element');
|
||||
const Comp = (result as React.ReactElement).type as (p: unknown) => React.ReactNode;
|
||||
const rendered = Comp((result as React.ReactElement).props);
|
||||
expect(rendered).toBeDefined();
|
||||
});
|
||||
|
||||
test('unknown subcommand reports error and does not mutate config', async () => {
|
||||
fakeGlobalConfig.hasCompletedOnboarding = true;
|
||||
const { fn, calls } = makeOnDone();
|
||||
const result = await callOnboarding(fn, makeContext(), 'bogus');
|
||||
expect(result).toBeNull();
|
||||
expect(calls[0]?.msg).toContain('Unknown');
|
||||
expect(calls[0]?.msg).toContain('bogus');
|
||||
expect(fakeGlobalConfig.hasCompletedOnboarding).toBe(true);
|
||||
});
|
||||
|
||||
test('every invocation logs a tengu_onboarding_step event', async () => {
|
||||
const { fn } = makeOnDone();
|
||||
for (const arg of ['full', 'theme', 'trust', 'model', 'mcp', 'status']) {
|
||||
loggedEvents.length = 0;
|
||||
await callOnboarding(fn, makeContext(), arg);
|
||||
expect(loggedEvents.find(e => e.name === 'tengu_onboarding_step')).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
3
src/commands/onboarding/index.d.ts
vendored
3
src/commands/onboarding/index.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import type { Command } from '../../types/command.js'
|
||||
declare const _default: Command
|
||||
export default _default
|
||||
@@ -1 +0,0 @@
|
||||
export default { isEnabled: () => false, isHidden: true, name: 'stub' }
|
||||
30
src/commands/onboarding/index.ts
Normal file
30
src/commands/onboarding/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Command } from '../../types/command.js'
|
||||
|
||||
// Subcommands supported by `/onboarding`.
|
||||
// - (no args) | full — re-run the complete first-run flow
|
||||
// - theme — re-pick the terminal theme
|
||||
// - trust — re-confirm the workspace trust dialog
|
||||
// - model — open the model picker (delegates to /model)
|
||||
// - mcp — show MCP server setup instructions
|
||||
// - status — print current onboarding state
|
||||
//
|
||||
// `/onboarding` exists in official v2.1.123 (string + telemetry confirmed:
|
||||
// `tengu_onboarding_step`, `hasCompletedOnboarding`, `lastOnboardingVersion`).
|
||||
// We expose the user-facing entry point so subscribers can re-run any step.
|
||||
const onboarding: Command = {
|
||||
type: 'local-jsx',
|
||||
name: 'onboarding',
|
||||
description: 'Re-run the first-run setup (theme, trust, model, MCP)',
|
||||
argumentHint: '[full|theme|trust|model|mcp|status]',
|
||||
isEnabled: () => true,
|
||||
isHidden: false,
|
||||
bridgeSafe: false,
|
||||
getBridgeInvocationError: () =>
|
||||
'onboarding requires the local interactive UI and is not bridge-safe',
|
||||
load: async () => {
|
||||
const m = await import('./launchOnboarding.js')
|
||||
return { call: m.callOnboarding }
|
||||
},
|
||||
}
|
||||
|
||||
export default onboarding
|
||||
190
src/commands/onboarding/launchOnboarding.tsx
Normal file
190
src/commands/onboarding/launchOnboarding.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Pane, Text, useTheme } from '@anthropic/ink';
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { ThemePicker } from '../../components/ThemePicker.js';
|
||||
import { getGlobalConfig, saveCurrentProjectConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import type { ThemeSetting } from '../../utils/theme.js';
|
||||
|
||||
/**
|
||||
* /onboarding [subcommand]
|
||||
*
|
||||
* User-facing slash command that re-runs the first-run setup flow. The
|
||||
* official v2.1.123 binary advertises `/onboarding` and emits
|
||||
* `tengu_onboarding_step` telemetry; this command exposes a clean entry
|
||||
* point for re-running individual steps after initial setup.
|
||||
*
|
||||
* Subcommands:
|
||||
* (none) | full | reset — clear `hasCompletedOnboarding` so the next
|
||||
* REPL launch re-runs the full flow, then exit
|
||||
* with instructions.
|
||||
* theme — render the theme picker inline.
|
||||
* trust — clear the workspace trust acceptance and
|
||||
* instruct the user to restart.
|
||||
* model — defer to /model (cannot mid-call suspend
|
||||
* into a separate command's Ink picker; print
|
||||
* instructions instead).
|
||||
* mcp — print MCP setup hints (delegates to /mcp).
|
||||
* status — show current onboarding state (theme,
|
||||
* completion flag, trust, last version).
|
||||
*/
|
||||
export type OnboardingSubcommand = 'full' | 'theme' | 'trust' | 'model' | 'mcp' | 'status';
|
||||
|
||||
const SUBCOMMANDS: ReadonlySet<OnboardingSubcommand> = new Set(['full', 'theme', 'trust', 'model', 'mcp', 'status']);
|
||||
|
||||
function meta(s: string): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
||||
return s as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
}
|
||||
|
||||
export function parseSubcommand(args: string): {
|
||||
sub: OnboardingSubcommand;
|
||||
unknownArg?: string;
|
||||
} {
|
||||
const trimmed = args.trim().toLowerCase();
|
||||
if (trimmed === '' || trimmed === 'reset') {
|
||||
return { sub: 'full' };
|
||||
}
|
||||
if (SUBCOMMANDS.has(trimmed as OnboardingSubcommand)) {
|
||||
return { sub: trimmed as OnboardingSubcommand };
|
||||
}
|
||||
return { sub: 'full', unknownArg: trimmed };
|
||||
}
|
||||
|
||||
function ThemeSubcommand({ onDone }: { onDone: (msg: string) => void }): React.ReactNode {
|
||||
const [, setTheme] = useTheme();
|
||||
return (
|
||||
<Pane color="permission">
|
||||
<ThemePicker
|
||||
onThemeSelect={(setting: ThemeSetting) => {
|
||||
setTheme(setting);
|
||||
logEvent('tengu_onboarding_step', { stepId: meta('theme') });
|
||||
onDone(`Theme set to ${setting}.`);
|
||||
}}
|
||||
onCancel={() => onDone('Theme picker dismissed.')}
|
||||
skipExitHandling={true}
|
||||
/>
|
||||
</Pane>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusView({
|
||||
theme,
|
||||
hasCompletedOnboarding,
|
||||
lastOnboardingVersion,
|
||||
}: {
|
||||
theme: string;
|
||||
hasCompletedOnboarding: boolean;
|
||||
lastOnboardingVersion: string;
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text bold>Onboarding status</Text>
|
||||
<Text>
|
||||
- Theme: <Text bold>{theme}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
- Onboarding completed:{' '}
|
||||
<Text bold color={hasCompletedOnboarding ? 'success' : 'warning'}>
|
||||
{hasCompletedOnboarding ? 'yes' : 'no'}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
- Last onboarding version: <Text bold>{lastOnboardingVersion}</Text>
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
Run /onboarding (no args) to re-run the full flow, or /onboarding theme | trust | model | mcp for a specific
|
||||
step.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const callOnboarding: LocalJSXCommandCall = async (onDone, _context, args) => {
|
||||
const { sub, unknownArg } = parseSubcommand(args);
|
||||
logEvent('tengu_onboarding_step', { stepId: meta(`slash_${sub}`) });
|
||||
|
||||
if (unknownArg !== undefined) {
|
||||
onDone(
|
||||
`Unknown /onboarding subcommand: \`${unknownArg}\`.\n` + `Valid: full | theme | trust | model | mcp | status`,
|
||||
{ display: 'system' },
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sub === 'theme') {
|
||||
return <ThemeSubcommand onDone={msg => onDone(msg)} />;
|
||||
}
|
||||
|
||||
if (sub === 'trust') {
|
||||
saveCurrentProjectConfig(current => ({
|
||||
...current,
|
||||
hasTrustDialogAccepted: false,
|
||||
}));
|
||||
onDone(
|
||||
'Workspace trust cleared for the current project. ' + 'The trust dialog will appear on the next `claude` launch.',
|
||||
{ display: 'system' },
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sub === 'model') {
|
||||
onDone(
|
||||
'Run `/model` to pick the AI model. ' +
|
||||
'Onboarding does not own the model picker; this entry exists for ' +
|
||||
'discoverability only.',
|
||||
{ display: 'system' },
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sub === 'mcp') {
|
||||
onDone(
|
||||
'MCP server setup:\n' +
|
||||
' - `/mcp` — list configured MCP servers\n' +
|
||||
' - `claude mcp add <name> <command>` — add a server (in your shell)\n' +
|
||||
' - `claude mcp remove <name>` — remove a server\n' +
|
||||
'Servers also load from `.mcp.json` in the workspace and from ' +
|
||||
'`~/.claude.json` globally.',
|
||||
{ display: 'system' },
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sub === 'status') {
|
||||
const cfg = getGlobalConfig();
|
||||
return (
|
||||
<StatusView
|
||||
theme={cfg.theme ?? '(unset)'}
|
||||
hasCompletedOnboarding={cfg.hasCompletedOnboarding === true}
|
||||
lastOnboardingVersion={cfg.lastOnboardingVersion ?? '(unset)'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// sub === 'full'
|
||||
// Clearing `hasCompletedOnboarding` causes `showSetupScreens()` (in
|
||||
// src/interactiveHelpers.tsx) to render the full Onboarding component
|
||||
// on the next launch. We cannot render <Onboarding /> mid-REPL because
|
||||
// it owns terminal-setup detection, OAuth flow, and final redirect to
|
||||
// the prompt — not safe to mount inside an active REPL session.
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasCompletedOnboarding: false,
|
||||
}));
|
||||
onDone(
|
||||
'Onboarding flag cleared. The full first-run setup ' +
|
||||
'(theme, OAuth/API key, security notes, terminal-setup) ' +
|
||||
'will run on the next `claude` launch.\n\n' +
|
||||
'For individual steps in this session, use:\n' +
|
||||
' /onboarding theme — re-pick theme inline\n' +
|
||||
' /onboarding trust — re-confirm workspace trust on next launch\n' +
|
||||
' /onboarding model — open /model picker\n' +
|
||||
' /onboarding mcp — show MCP setup hints\n' +
|
||||
' /onboarding status — show current onboarding state',
|
||||
{ display: 'system' },
|
||||
);
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user