Compare commits

...

5 Commits

Author SHA1 Message Date
abeatrix 8c4ec94c6b Merge branch 'main' into bee/cli-startup 2026-05-13 11:29:52 -07:00
abeatrix dcdd79bea5 Merge branch 'bee/cli-startup' of https://github.com/cline/cline into bee/cli-startup 2026-05-12 10:01:23 -07:00
abeatrix 5214ec0f72 patches 2026-05-12 10:00:44 -07:00
Saoud Rizwan 3ad5eb7e2d Merge branch 'main' into bee/cli-startup 2026-05-11 20:36:43 -07:00
abeatrix de613c5312 fix(cli): defer heavy imports to speed startup time
Lazy-load hub, update, logging, and wizard modules only when their code paths
run. Use bundled catalog data during default interactive startup to avoid
network-backed model loading and reduce CLI cold-start latency.
2026-05-11 20:31:08 -07:00
15 changed files with 92 additions and 75 deletions
+9 -8
View File
@@ -1,14 +1,6 @@
import { type ChildProcess, spawn } from "node:child_process";
import { realpathSync } from "node:fs";
import {
clearHubDiscovery,
probeHubServer,
readHubDiscovery,
resolveSharedHubOwnerContext,
stopLocalHubServerGracefully,
} from "@cline/core";
import { version } from "../../package.json";
import { ensureCliHubServer } from "../utils/hub-runtime";
import { c, writeErr, writeln } from "../utils/output";
import {
getInstalledKanbanVersion,
@@ -235,6 +227,7 @@ async function waitForHubToStop(
url: string,
timeoutMs: number,
): Promise<boolean> {
const { probeHubServer } = await import("@cline/core");
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const check = await probeHubServer(url).catch(() => undefined);
@@ -250,6 +243,14 @@ async function waitForHubToStop(
* clears stale discovery, then re-ensures a fresh instance is spawned.
*/
async function restartHubServerIfRunning(): Promise<void> {
const {
clearHubDiscovery,
probeHubServer,
readHubDiscovery,
resolveSharedHubOwnerContext,
stopLocalHubServerGracefully,
} = await import("@cline/core");
const { ensureCliHubServer } = await import("../utils/hub-runtime");
const owner = resolveSharedHubOwnerContext();
const discovery = await readHubDiscovery(owner.discoveryPath).catch(
() => undefined,
+6 -6
View File
@@ -28,23 +28,23 @@ if (!isMainThread) {
};
process.on("SIGINT", forwardSignalToRuntime);
process.on("SIGTERM", forwardSignalToRuntime);
const handleFatalProcessError = (kind: string, error: unknown) => {
const handleFatalProcessError = async (kind: string, error: unknown) => {
if (handlingFatalProcessError) {
process.exit(1);
}
handlingFatalProcessError = true;
logCliProcessError(kind, error);
writeErr(
error instanceof Error ? (error.stack ?? error.message) : String(error),
);
cleanupActiveRuntime();
abortActiveRuntime();
void disposeAll().finally(() => {
await logCliProcessError(kind, error);
await disposeAll().finally(() => {
process.exit(1);
});
};
process.on("uncaughtException", (error) => {
handleFatalProcessError("uncaughtException", error);
void handleFatalProcessError("uncaughtException", error);
});
process.on("unhandledRejection", (reason, promise) => {
if (isAbortInProgress()) {
@@ -53,7 +53,7 @@ if (!isMainThread) {
promise.catch(() => {});
return;
}
handleFatalProcessError("unhandledRejection", reason);
void handleFatalProcessError("unhandledRejection", reason);
});
void (async () => {
@@ -67,10 +67,10 @@ if (!isMainThread) {
const { runCli } = await import("./main");
await runCli();
} catch (err) {
logCliProcessError("runCli", err);
writeErr(err instanceof Error ? err.message : String(err));
cleanupActiveRuntime();
abortActiveRuntime();
await logCliProcessError("runCli", err);
exitCode = 1;
} finally {
await disposeAll();
+2 -2
View File
@@ -116,7 +116,7 @@ describe("createCliLoggerAdapter", () => {
}
});
it("writes process-level errors to the CLI log", () => {
it("writes process-level errors to the CLI log", async () => {
const snapshot = withEnvSnapshot();
const dataDir = mkdtempSync(join(tmpdir(), `${commandName}-process-log-`));
process.env.CLINE_DATA_DIR = dataDir;
@@ -126,7 +126,7 @@ describe("createCliLoggerAdapter", () => {
delete process.env.CLINE_LOG_ENABLED;
try {
logCliProcessError(
await logCliProcessError(
"unhandledRejection",
new Error("message schema validation failed"),
);
+7 -2
View File
@@ -1,5 +1,4 @@
import type { BasicLogger } from "@cline/core";
import { createCliLoggerAdapter, flushCliLoggerAdapters } from "./adapter";
export function logCliError(
logger: BasicLogger | undefined,
@@ -16,8 +15,14 @@ export function logCliError(
});
}
export function logCliProcessError(kind: string, error: unknown): void {
export async function logCliProcessError(
kind: string,
error: unknown,
): Promise<void> {
try {
const { createCliLoggerAdapter, flushCliLoggerAdapters } = await import(
"./adapter"
);
const logger = createCliLoggerAdapter({
runtime: "cli",
component: "process",
+5 -5
View File
@@ -444,7 +444,7 @@ describe("runCli lightweight command dispatch", () => {
);
});
it("loads live catalog models for default interactive model selection", async () => {
it("uses bundled catalog models for default interactive startup", async () => {
llmMocks.resolveProviderConfig.mockResolvedValue({
knownModels: {
"live-only-model": {
@@ -461,8 +461,8 @@ describe("runCli lightweight command dispatch", () => {
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
"cline",
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
loadLatestOnInit: false,
loadPrivateOnAuth: false,
failOnError: false,
},
undefined,
@@ -490,8 +490,8 @@ describe("runCli lightweight command dispatch", () => {
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
"cline",
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
loadLatestOnInit: false,
loadPrivateOnAuth: false,
failOnError: false,
},
undefined,
+24 -17
View File
@@ -10,10 +10,6 @@ import {
commanderToParsedArgs,
createProgram,
} from "./commands/program";
import {
autoUpdateOnStartup,
getPreferredKanbanInstaller,
} from "./commands/update";
import { CLI_DEFAULT_CHECKPOINT_CONFIG } from "./runtime/defaults";
import {
buildCliCompactionConfig,
@@ -38,11 +34,7 @@ import {
normalizeProviderId,
} from "./utils/provider-auth";
import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command";
import { captureCliExtensionActivated, getCliTelemetryService } from "./utils/telemetry";
import type { Config } from "./utils/types";
import { runConnectWizard } from "./wizards/connect";
import { runMcpWizard } from "./wizards/mcp";
import { runScheduleWizard } from "./wizards/schedule";
export function stdinHasPipedInput(): boolean {
if (process.stdin.isTTY) return false;
@@ -106,7 +98,12 @@ export function resolveConfigDirArg(argv: string[]): string | undefined {
export async function runCli(): Promise<void> {
installStreamErrorGuards();
autoUpdateOnStartup();
const updateModulePromise = import("./commands/update");
void updateModulePromise
.then(({ autoUpdateOnStartup }) => {
autoUpdateOnStartup();
})
.catch(() => {});
const cliArgs = process.argv.slice(2);
const configDir = resolveConfigDirArg(cliArgs);
@@ -120,6 +117,7 @@ export async function runCli(): Promise<void> {
// has been applied, so the telemetry singleton's persisted distinct-id
// (and any other storage it touches) lands under the user-selected
// `--config <dir>` rather than the default home/config location.
const { captureCliExtensionActivated } = await import("./utils/telemetry");
captureCliExtensionActivated();
let launchConfigView = false;
@@ -310,6 +308,7 @@ export async function runCli(): Promise<void> {
io,
);
} else if (process.stdin.isTTY && process.stdout.isTTY) {
const { runConnectWizard } = await import("./wizards/connect");
ctx.exitCode = await runConnectWizard();
} else {
writeln(`\nAdapters:\n${formatAdapterList()}`);
@@ -322,6 +321,7 @@ export async function runCli(): Promise<void> {
.description("Manage MCP servers")
.action(async () => {
if (process.stdin.isTTY && process.stdout.isTTY) {
const { runMcpWizard } = await import("./wizards/mcp");
ctx.exitCode = await runMcpWizard();
} else {
writeln(
@@ -489,6 +489,7 @@ export async function runCli(): Promise<void> {
process.stdin.isTTY &&
process.stdout.isTTY
) {
const { runScheduleWizard } = await import("./wizards/schedule");
ctx.exitCode = await runScheduleWizard();
return;
}
@@ -534,6 +535,7 @@ export async function runCli(): Promise<void> {
.description("Run the kanban app")
.action(async () => {
const { launchKanban } = await import("./commands/kanban");
const { getPreferredKanbanInstaller } = await updateModulePromise;
ctx.exitCode = await launchKanban({
preferredInstaller: getPreferredKanbanInstaller(),
});
@@ -587,6 +589,7 @@ export async function runCli(): Promise<void> {
return;
}
const { launchKanban } = await import("./commands/kanban");
const { getPreferredKanbanInstaller } = await updateModulePromise;
process.exitCode = await launchKanban({
preferredInstaller: getPreferredKanbanInstaller(),
});
@@ -771,6 +774,13 @@ export async function runCli(): Promise<void> {
apiKey = oauthResult?.apiKey ?? apiKey;
}
const systemPromptPromise = resolveSystemPrompt({
cwd,
explicitSystemPrompt: args.systemPrompt,
providerId: provider,
mode: args.mode ?? "act",
});
systemPromptPromise.catch(() => {});
let knownModels: Config["knownModels"];
try {
const persistedProviderConfig = providerSettingsManager.getProviderConfig(
@@ -781,8 +791,8 @@ export async function runCli(): Promise<void> {
);
const catalogOptions = isInteractive
? {
loadLatestOnInit: true,
loadPrivateOnAuth: true,
loadLatestOnInit: false,
loadPrivateOnAuth: false,
failOnError: false,
}
: undefined;
@@ -823,6 +833,8 @@ export async function runCli(): Promise<void> {
cwd,
});
const { getCliTelemetryService } = await import("./utils/telemetry");
const config: Config = {
providerId: provider,
modelId:
@@ -832,12 +844,7 @@ export async function runCli(): Promise<void> {
"anthropic/claude-sonnet-4.6",
apiKey: apiKey ?? "",
knownModels,
systemPrompt: await resolveSystemPrompt({
cwd,
explicitSystemPrompt: args.systemPrompt,
providerId: provider,
mode: args.mode ?? "act",
}),
systemPrompt: await systemPromptPromise,
execution: {
maxConsecutiveMistakes: args.retries ?? 3,
},
@@ -26,7 +26,6 @@ import {
writeln,
} from "../utils/output";
import { createWorkspaceChatCommandHost } from "../utils/plugin-chat-commands";
import { readRepoStatus } from "../utils/repo-status";
import type { Config } from "../utils/types";
import {
clearAbortInProgress,
@@ -63,7 +62,6 @@ export async function runInteractive(
): Promise<void> {
assertInteractivePreflight(config);
const initialRepoStatus = await readRepoStatus(config.cwd);
const workflowSlashCommands = listInteractiveSlashCommands(
userInstructionService,
);
@@ -338,7 +336,6 @@ export async function runInteractive(
initialNotice: options?.initialNotice,
onInitialNoticeShown: options?.onInitialNoticeShown,
loadDeferredInitialMessages,
initialRepoStatus,
workflowSlashCommands,
loadAdditionalSlashCommands,
loadWelcomeLine: async () =>
@@ -218,23 +218,20 @@ export function SessionProvider(props: {
setLastTotalCost(0);
}, []);
const addUsageDelta = useCallback(
(usage: UsageDelta) => {
const nextTotalTokens = nextUsageTokenDisplay(0, usage);
if (nextTotalTokens > 0) {
setLastTotalTokens((prev) => nextUsageTokenDisplay(prev, usage));
}
const costDelta = usage.cost;
if (
typeof costDelta === "number" &&
Number.isFinite(costDelta) &&
costDelta > 0
) {
setLastTotalCost((prev) => prev + costDelta);
}
},
[],
);
const addUsageDelta = useCallback((usage: UsageDelta) => {
const nextTotalTokens = nextUsageTokenDisplay(0, usage);
if (nextTotalTokens > 0) {
setLastTotalTokens((prev) => nextUsageTokenDisplay(prev, usage));
}
const costDelta = usage.cost;
if (
typeof costDelta === "number" &&
Number.isFinite(costDelta) &&
costDelta > 0
) {
setLastTotalCost((prev) => prev + costDelta);
}
}, []);
const replaceEntries = useCallback((nextEntries: ChatEntry[]) => {
setEntries(
+4
View File
@@ -125,6 +125,10 @@ function App(props: TuiProps) {
.catch(() => {});
}, [props.config.cwd]);
useEffect(() => {
refreshRepoStatus();
}, [refreshRepoStatus]);
const refocusTextareaRef = useRef<() => void>(() => {});
const populateInputRef = useRef<(value: string) => void>(() => {});
const insertSkillCommandRef = useRef<
+3 -2
View File
@@ -1,4 +1,5 @@
import { Llms, type ProviderSettings } from "@cline/core";
import type { ProviderSettings } from "@cline/core";
import { normalizeProviderId as normalizeLlmsProviderId } from "@cline/llms";
import { isOAuthProviderId } from "@cline/shared";
export type OAuthCredentials = {
@@ -11,7 +12,7 @@ export type OAuthCredentials = {
};
export function normalizeProviderId(providerId: string): string {
return Llms.normalizeProviderId(providerId.trim());
return normalizeLlmsProviderId(providerId.trim());
}
export function normalizeAuthProviderId(providerId: string): string {
+1 -1
View File
@@ -1,4 +1,3 @@
import { createTeamName } from "@cline/core";
import { formatUserCommandBlock } from "@cline/shared";
import type { Config } from "./types";
@@ -33,5 +32,6 @@ export async function enableTeamsForPrompt(config: Config): Promise<void> {
return;
}
config.enableAgentTeams = true;
const { createTeamName } = await import("@cline/core");
config.teamName = config.teamName?.trim() || createTeamName();
}
@@ -7,20 +7,15 @@ const hoisted = vi.hoisted(() => ({
getCliTelemetryService: vi.fn(() => undefined),
}));
vi.mock("@cline/core", () => ({
vi.mock("@cline/core/telemetry", () => ({
captureExtensionActivated: hoisted.captureExtensionActivated,
identifyAccount: hoisted.identifyAccount,
// CLI telemetry singleton path normally pulls in
// `createConfiguredTelemetryHandle` and `createClineTelemetryServiceConfig`;
// stub them so the test never spins up a real OpenTelemetry provider.
createClineTelemetryServiceConfig: vi.fn(() => ({})),
createConfiguredTelemetryHandle: vi.fn(() => ({
telemetry: undefined,
provider: undefined,
flush: vi.fn(),
dispose: vi.fn(),
})),
registerDisposable: vi.fn(),
TelemetryLoggerSink: class {},
}));
+7 -5
View File
@@ -1,13 +1,15 @@
import {
captureExtensionActivated,
createConfiguredTelemetryHandle,
identifyAccount,
TelemetryLoggerSink,
} from "@cline/core/telemetry";
import {
type BasicLogger,
captureExtensionActivated,
createClineTelemetryServiceConfig,
createConfiguredTelemetryHandle,
type ITelemetryService,
identifyAccount,
registerDisposable,
TelemetryLoggerSink,
} from "@cline/core";
} from "@cline/shared";
import { getCliBuildInfo } from "./common";
import {
markActivationCaptured,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Agent } from "@cline/sdk";
import { createServer } from "node:http";
import { Agent } from "@cline/sdk";
const PORT = Number(process.env.PORT || 3456);
@@ -20,3 +20,11 @@ export {
OpenTelemetryProvider,
type OpenTelemetryProviderOptions,
} from "./OpenTelemetryProvider";
export {
captureExtensionActivated,
identifyAccount,
} from "./core-events";
export {
TelemetryLoggerSink,
type TelemetryLoggerSinkOptions,
} from "./TelemetryLoggerSink";