Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions apps/cli/src/bootstrap/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface StepExtensionFactoryDeps {
feedbackIdentity: StepExtensionOptions["feedbackIdentity"];
/** Initial Step permission policy parsed from CLI/runtime options. */
permission: StepExtensionOptions["permission"];
usageHost?: StepExtensionOptions["usageHost"];
/** Optional StepCode provider extension, present only when configured. */
stepCodeProviderExtension: InlineExtension | undefined;
}
Expand All @@ -45,6 +46,7 @@ export function createStepExtensionFactories(deps: StepExtensionFactoryDeps): In
stepSettings: deps.stepSettings,
feedbackIdentity: deps.feedbackIdentity,
permission: deps.permission,
usageHost: deps.usageHost,
traceHeaderPolicy: deps.traceHeaderPolicy,
}),
createStepCapabilitiesExtensionInline({ telemetry: deps.telemetry }),
Expand Down
105 changes: 91 additions & 14 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ambiguousPluginServerNames,
applyStepCodeConfigDefaults,
buildStepSystemPromptAppendix,
checkStepPlanBeforeInteractive,
configureHttpDispatcher,
createStepCode,
createStepCodeProviderInlineExtension,
Expand Down Expand Up @@ -43,13 +44,15 @@ import {
migrateLegacyStepCredential,
needsStepLoginBeforeInteractive,
normalizeStepSessionSelectorArgs,
PlanUsageController,
parseStepUpdateCommand,
prepareMain,
prepareStepInstallation,
readFeedbackUsername,
readGlobalStepConfig,
readOrCreateStepDeviceId,
readStoredCredential,
resolvePlanUsageAccount,
resolveStepAgentDir,
resolveStepConfigDir,
resolveStepConfigRoot,
Expand All @@ -67,6 +70,7 @@ import {
STEP_PROVIDER_ID,
STEPCODE_VERSION,
type StepSettingsManager,
scanUsageOverview,
setStderrDevLogStorageRootDirectory,
stopThemeWatcher,
syncStepLoginProfileEndpoint,
Expand All @@ -81,7 +85,15 @@ import { createStepExtensionFactories } from "#bootstrap/extensions";
import { captureRawStdout, sdkStdioRequested } from "#bootstrap/stdout-capture";
import { InteractiveMode, runPrintMode, runRpcMode } from "#modes/index";
import { createSdkStdioMode } from "#modes/sdk-stdio";
import { selectConfig, selectSession, showFirstTimeSetup, showStartupInput, showStartupSelector } from "#ui/index";
import {
selectConfig,
selectSession,
showFirstTimeSetup,
showStartupInput,
showStartupSelector,
showUsagePanel,
usagePresentationRows,
} from "#ui/index";
import { observability } from "./observability.ts";

/**
Expand Down Expand Up @@ -159,6 +171,14 @@ const telemetryIdentityReady = initializeTelemetryIdentity();
configureHttpDispatcher();

let currentStepSettingsManager: StepSettingsManager | undefined;
let usageRuntime: Parameters<typeof createStepCode>[0] | undefined;
const usageAccount = new PlanUsageController(() =>
resolvePlanUsageAccount(
getStepAuthPath(),
process.env,
process.argv.some((arg) => arg === "--api-key" || arg.startsWith("--api-key=")),
),
);
const hasExplicitStepCredential =
process.argv.slice(2).some((arg) => arg === "--api-key" || arg.startsWith("--api-key=")) ||
Boolean(process.env.STEP_API_KEY?.trim());
Expand Down Expand Up @@ -187,6 +207,34 @@ const stepMainOptions: MainOptions = {
extensionFactories: createStepExtensionFactories({
telemetry: telemetryReporter,
stepSettings: () => currentStepSettingsManager,
usageHost: {
account: usageAccount,
resources: () => {
const loader = usageRuntime?.session.resourceLoader;
return {
agents: loader?.getAgentsFiles().agentsFiles.map((file) => file.path) ?? [],
skills:
loader?.getSkills().skills.map((skill) => ({ name: skill.name, filePath: skill.filePath })) ?? [],
};
},
scan: (cwd, scope, signal) =>
scanUsageOverview(
[
join(resolveStepAgentDir(), "sessions"),
...(usageRuntime ? [usageRuntime.session.sessionManager.getSessionDir()] : []),
],
join(resolveStepStorageRoot(), "cache", "usage"),
cwd,
scope,
signal,
),
present: showUsagePanel,
output: (ctx, model) => {
const text = usagePresentationRows(model).join("\n");
if (ctx.mode === "rpc") ctx.ui.notify(text, "info");
else process.stderr.write(`${text}\n`);
},
},
feedbackIdentity: readCurrentFeedbackIdentity,
permission: {
approvalMode: stepPermissionArgs.approvalMode,
Expand All @@ -208,7 +256,10 @@ const stepMainOptions: MainOptions = {
defaultTheme: getStepDefaultTheme(),
defaultProvider: stepCodeConfig?.defaultProvider ?? stepPersistedDefaults.provider ?? STEP_DEFAULT_PROVIDER,
defaultModel: stepCodeConfig?.defaultModel ?? stepPersistedDefaults.model ?? STEP_DEFAULT_MODEL,
runtimeHostFactory: createStepCode,
runtimeHostFactory: (runtime) => {
usageRuntime = runtime;
return createStepCode(runtime);
},
stdioModeFactory: sdkStdio ? createSdkStdioMode({ writeFrame: rawStdoutWrite }) : undefined,
interactiveModeOptions: {
showChangelog: false,
Expand All @@ -218,6 +269,7 @@ const stepMainOptions: MainOptions = {
stepLogin: (host) =>
runStepLogin({
authPath: getStepAuthPath(),
planUsage: usageAccount,
createHost: () => host,
themeName: getStepDefaultTheme(),
}),
Expand Down Expand Up @@ -572,15 +624,17 @@ try {
} else {
const outcome = await runStepLogin({
authPath: getStepAuthPath(),
planUsage: usageAccount,
themeName: getStepDefaultTheme(),
noBrowser: loginArgs.includes("--no-browser"),
});
if (outcome.kind === "completed") {
syncStepLoginProfileEndpoint(getStepAuthPath());
process.stdout.write(`Signed in${outcome.profile ? ` with ${outcome.profile.title}` : ""}.\n`);
if (outcome.credentialsPath) process.stdout.write(`Credential written: ${outcome.credentialsPath}\n`);
if (outcome.warning) process.stderr.write(`${outcome.warning}\n`);
} else {
process.stderr.write("Sign-in cancelled. No credential was written.\n");
process.stderr.write("Sign-in cancelled.\n");
process.exitCode = 1;
}
}
Expand Down Expand Up @@ -643,21 +697,44 @@ try {
stdoutIsTTY: process.stdout.isTTY,
args: parsedInteractiveArgs,
});
const selectedProvider =
parsedInteractiveArgs.provider ??
(parsedInteractiveArgs.model?.includes("/")
? parsedInteractiveArgs.model.split("/", 1)[0]
: undefined) ??
process.env.STEP_PROVIDER ??
process.env.STEP_MODEL_PROVIDER ??
stepCodeConfig?.defaultProvider ??
stepPersistedDefaults.provider ??
STEP_DEFAULT_PROVIDER;
if (
!hasConfiguredStepCodeCredential(stepCodeConfig) &&
needsStepLoginBeforeInteractive({
authPath: getStepAuthPath(),
interactive: interactiveStartup,
})
interactiveStartup &&
selectedProvider === STEP_PROVIDER_ID &&
!hasExplicitStepCredential &&
!hasConfiguredStepCodeCredential(stepCodeConfig)
) {
const outcome = await runStepLogin({
const missingCredential = needsStepLoginBeforeInteractive({
authPath: getStepAuthPath(),
themeName: getStepDefaultTheme(),
interactive: interactiveStartup,
});
if (outcome.kind === "exit") {
shouldLaunchMain = false;
} else {
syncStepLoginProfileEndpoint(getStepAuthPath());
const check = missingCredential
? {}
: await checkStepPlanBeforeInteractive({
authPath: getStepAuthPath(),
interactive: interactiveStartup,
planUsage: usageAccount,
});
if (check.warning) process.stderr.write(`${check.warning}\n`);
if (missingCredential || check.requiredPlan) {
const outcome = await runStepLogin({
authPath: getStepAuthPath(),
planUsage: usageAccount,
requiredPlan: check.requiredPlan,
themeName: getStepDefaultTheme(),
});
if (outcome.warning) process.stderr.write(`${outcome.warning}\n`);
if (outcome.kind === "exit") shouldLaunchMain = false;
else syncStepLoginProfileEndpoint(getStepAuthPath());
}
}
if (shouldLaunchMain) {
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export {
showStartupSelector,
startStartupTui,
} from "./startup-ui.ts";

export { showUsagePanel, usagePresentationRows } from "./view/dialogs/usage-panel.ts";
16 changes: 13 additions & 3 deletions apps/cli/src/ui/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3307,11 +3307,12 @@ export class InteractiveMode {
return new Promise((resolve, reject) => {
let component: Component & { dispose?(): void };
let closed = false;
let ownOverlay: OverlayHandle | undefined;

const close = (result: T) => {
if (closed) return;
closed = true;
if (isOverlay) this.ui.hideOverlay();
if (isOverlay) ownOverlay?.hide();
else restoreEditor();
// Note: both branches above already call requestRender
resolve(result);
Expand All @@ -3324,7 +3325,14 @@ export class InteractiveMode {

Promise.resolve(factory(this.ui, theme, this.keybindings, close))
.then((c) => {
if (closed) return;
if (closed) {
try {
c.dispose?.();
} catch {
/* ignore dispose errors */
}
return;
}
component = c;
if (isOverlay) {
// Resolve overlay options - can be static or dynamic function
Expand All @@ -3341,6 +3349,7 @@ export class InteractiveMode {
return w ? { width: w } : undefined;
};
const handle = this.ui.showOverlay(component, resolveOptions());
ownOverlay = handle;
// Expose handle to caller for visibility control
options?.onHandle?.(handle);
} else {
Expand Down Expand Up @@ -5630,8 +5639,9 @@ export class InteractiveMode {
const outcome = await this.options.stepLogin(this.createStepLoginHost());
if (outcome.kind === "completed") {
await this.handleReloadCommand();
if (outcome.warning) this.showWarning(outcome.warning);
} else {
this.showStatus("Sign-in cancelled. No credential was written.");
this.showStatus("Sign-in cancelled.");
}
} catch (error: unknown) {
this.showError(`Failed to login to Step: ${error instanceof Error ? error.message : String(error)}`);
Expand Down
Loading
Loading