Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ async function handleZedAcpIntegration(
return true;
}

function hasExplicitProviderProfileSelector(argv: ParsedCliArgs): boolean {
return [argv.provider, argv.profile, argv.profileLoad].some(
(value) => typeof value === 'string' && value.trim().length > 0,
);
}

/**
* Construct the SINGLE foreground Agent (#2378) and dispatch the interactive or
* non-interactive session. The spinner wraps agent construction, which (via
Expand Down Expand Up @@ -217,6 +223,7 @@ async function constructForegroundAgentAndDispatch(
recording,
hasPipedInput,
readStdinData,
suppressStartupWelcome: hasExplicitProviderProfileSelector(argv),
});
}

Expand Down
14 changes: 12 additions & 2 deletions packages/cli/src/config/cliArgParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface CliArgs {
proxy: string | undefined;
includeDirectories: string[] | undefined;
profileLoad: string | undefined;
profile?: string | undefined;
loadMemoryFromIncludeDirectories: boolean | undefined;
ideMode: string | undefined;
screenReader: boolean | undefined;
Expand Down Expand Up @@ -179,13 +180,14 @@ function mapParsedArgsToCliArgs(result: Record<string, unknown>): CliArgs {
experimentalUi: result['experimentalUi'] as boolean | undefined,
extensions: result['extensions'] as string[] | undefined,
listExtensions: result['listExtensions'] as boolean | undefined,
provider: result['provider'] as string | undefined,
provider: pickLastRepeatedStringOption(result['provider']),
key: result['key'] as string | undefined,
keyfile: result['keyfile'] as string | undefined,
baseurl: result['baseurl'] as string | undefined,
proxy: result['proxy'] as string | undefined,
includeDirectories: result['includeDirectories'] as string[] | undefined,
profileLoad: result['profileLoad'] as string | undefined,
profileLoad: pickLastRepeatedStringOption(result['profileLoad']),
profile: pickLastRepeatedStringOption(result['profile']),
Comment thread
acoliver marked this conversation as resolved.
loadMemoryFromIncludeDirectories: result[
'loadMemoryFromIncludeDirectories'
] as boolean | undefined,
Expand All @@ -206,6 +208,14 @@ function mapParsedArgsToCliArgs(result: Record<string, unknown>): CliArgs {
};
}

function pickLastRepeatedStringOption(value: unknown): string | undefined {
if (Array.isArray(value)) {
const last = value[value.length - 1];
return typeof last === 'string' ? last : undefined;
}
return typeof value === 'string' ? value : undefined;
}

/** Checks for subcommand dispatch (mcp, hooks, extensions) and exits if handled. */
function handleSubcommandExit(result: Record<string, unknown>): void {
const commands = result['_'];
Expand Down
125 changes: 125 additions & 0 deletions packages/cli/src/config/cliArgParser.welcomeSuppression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2026 Vybestack LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { afterEach, describe, expect, it } from 'bun:test';
import process from 'node:process';
import { parseArguments } from './cliArgParser.js';
import type { Settings } from './settings.js';

const emptySettings: Settings = {};

describe('parseArguments selector and non-selector parsing', () => {
const originalArgv = process.argv;

afterEach(() => {
process.argv = originalArgv;
});

it('parses --provider into CliArgs.provider', async () => {
process.argv = ['node', 'llxprt', '--provider', 'gemini'];
const argv = await parseArguments(emptySettings);
expect(argv.provider).toBe('gemini');
expect(argv.profile).toBeUndefined();
expect(argv.profileLoad).toBeUndefined();
});

it('parses --profile into CliArgs.profile', async () => {
const inline = '{"provider":"x"}';
process.argv = ['node', 'llxprt', '--profile', inline];
const argv = await parseArguments(emptySettings);
expect(argv.profile).toBe(inline);
expect(argv.provider).toBeUndefined();
expect(argv.profileLoad).toBeUndefined();
});

it('parses --profile-load into CliArgs.profileLoad', async () => {
process.argv = ['node', 'llxprt', '--profile-load', 'my-profile'];
const argv = await parseArguments(emptySettings);
expect(argv.profileLoad).toBe('my-profile');
expect(argv.provider).toBeUndefined();
expect(argv.profile).toBeUndefined();
});

it('keeps only the last value when --profile is repeated', async () => {
process.argv = [
'node',
'llxprt',
'--profile',
'first',
'--profile',
'last',
];
const argv = await parseArguments(emptySettings);
expect(argv.profile).toBe('last');
});

it('parses no selector when none is supplied', async () => {
process.argv = ['node', 'llxprt'];
const argv = await parseArguments(emptySettings);
expect(argv.provider).toBeUndefined();
expect(argv.profile).toBeUndefined();
expect(argv.profileLoad).toBeUndefined();
});

it('parses --model without populating any selector', async () => {
process.argv = ['node', 'llxprt', '--model', 'gemini-2.5-flash'];
const argv = await parseArguments(emptySettings);
expect(argv.model).toBe('gemini-2.5-flash');
expect(argv.provider).toBeUndefined();
expect(argv.profile).toBeUndefined();
expect(argv.profileLoad).toBeUndefined();
});

it('parses --provider with an empty string value', async () => {
process.argv = ['node', 'llxprt', '--provider', ''];
const argv = await parseArguments(emptySettings);
expect(argv.provider).toBe('');
});

it('parses --provider with a whitespace-only value', async () => {
process.argv = ['node', 'llxprt', '--provider', ' '];
const argv = await parseArguments(emptySettings);
expect(argv.provider).toBe(' ');
});

it('parses --profile with an empty string value', async () => {
process.argv = ['node', 'llxprt', '--profile', ''];
const argv = await parseArguments(emptySettings);
expect(argv.profile).toBe('');
});

it('parses --profile-load with an empty string value', async () => {
process.argv = ['node', 'llxprt', '--profile-load', ''];
const argv = await parseArguments(emptySettings);
expect(argv.profileLoad).toBe('');
});

it('keeps only the last value when --provider is repeated', async () => {
process.argv = [
'node',
'llxprt',
'--provider',
'gemini',
'--provider',
'openai',
];
const argv = await parseArguments(emptySettings);
expect(argv.provider).toBe('openai');
});

it('keeps only the last value when --profile-load is repeated', async () => {
process.argv = [
'node',
'llxprt',
'--profile-load',
'first',
'--profile-load',
'last',
];
const argv = await parseArguments(emptySettings);
expect(argv.profileLoad).toBe('last');
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/session/interactiveUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export async function startInteractiveUI(
resumedHistory?: IContent[],
initialRecordingService?: SessionRecordingService,
initialLockHandle?: LockHandle | null,
suppressStartupWelcome?: boolean,
) {
const version = await getCliVersion();

Expand Down Expand Up @@ -212,6 +213,7 @@ export async function startInteractiveUI(
resumedHistory={resumedHistory}
initialRecordingService={initialRecordingService}
initialLockHandle={initialLockHandle}
suppressStartupWelcome={suppressStartupWelcome}
/>
</SettingsContext.Provider>
</ErrorBoundary>
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/session/nonInteractiveSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface SessionDispatchOptions {
recording: SessionRecordingSetup;
hasPipedInput: boolean;
readStdinData: () => Promise<string>;
suppressStartupWelcome?: boolean;
}

/**
Expand All @@ -80,6 +81,7 @@ export async function dispatchInteractiveOrNonInteractive({
recording,
hasPipedInput,
readStdinData,
suppressStartupWelcome,
}: SessionDispatchOptions): Promise<void> {
const input = config.getQuestion();

Expand Down Expand Up @@ -114,6 +116,7 @@ export async function dispatchInteractiveOrNonInteractive({
recording.resumedHistory ?? undefined,
recording.recordingService,
recording.resumedLockHandle,
suppressStartupWelcome,
);
return;
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface AppProps {
initialRecordingService?: SessionRecordingService;
/** @plan:PLAN-20260214-SESSIONBROWSER.P23 */
initialLockHandle?: LockHandle | null;
suppressStartupWelcome?: boolean;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/ui/AppContainerRuntime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface AppContainerRuntimeProps {
initialRecordingService?: SessionRecordingService;
/** @plan:PLAN-20260214-SESSIONBROWSER.P23 */
initialLockHandle?: LockHandle | null;
suppressStartupWelcome?: boolean;
}

type HookResults = {
Expand Down Expand Up @@ -452,6 +453,7 @@ export const AppContainerRuntime = (props: AppContainerRuntimeProps) => {
runtime: bootstrap.runtime,
consoleMessages: bootstrap.consoleMessages,
setLlxprtMdFileCount: bootstrap.setLlxprtMdFileCount,
suppressStartupWelcome: props.suppressStartupWelcome,
});
const input = useAppInput(
buildInputParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface AppDialogsParams {
runtime: ReturnType<typeof useRuntimeApi>;
consoleMessages: ConsoleMessageItem[];
setLlxprtMdFileCount: (count: number) => void;
suppressStartupWelcome?: boolean;
}

function useDialogsState() {
Expand Down Expand Up @@ -275,6 +276,7 @@ function useDialogsAuth(
settings,
isFolderTrustComplete: !folderTrust.isFolderTrustDialogOpen,
agent: p.agent,
suppressStartup: p.suppressStartupWelcome === true,
});
useIdeTrustEffect(config, st);
const authProviders = useDialogsAuthProviders(
Expand Down
Loading