fix: restore interactive OpenAI REPL startup
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -25,6 +25,7 @@
|
|||||||
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||||
"ajv": "^8.17.0",
|
"ajv": "^8.17.0",
|
||||||
|
"auto-bind": "^5.0.1",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"bidi-js": "^1.0.3",
|
"bidi-js": "^1.0.3",
|
||||||
"chalk": "^5.4.0",
|
"chalk": "^5.4.0",
|
||||||
@@ -350,6 +351,8 @@
|
|||||||
|
|
||||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||||
|
|
||||||
"axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
|
"axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||||
"ajv": "^8.17.0",
|
"ajv": "^8.17.0",
|
||||||
|
"auto-bind": "^5.0.1",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"bidi-js": "^1.0.3",
|
"bidi-js": "^1.0.3",
|
||||||
"chalk": "^5.4.0",
|
"chalk": "^5.4.0",
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ const result = await Bun.build({
|
|||||||
'plist',
|
'plist',
|
||||||
'cacache',
|
'cacache',
|
||||||
'fuse',
|
'fuse',
|
||||||
'auto-bind',
|
|
||||||
'code-excerpt',
|
'code-excerpt',
|
||||||
'stack-utils',
|
'stack-utils',
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { PureComponent, type ReactNode } from 'react';
|
import React, { PureComponent, type ReactNode } from 'react';
|
||||||
import { updateLastInteractionTime } from '../../bootstrap/state.js';
|
import { updateLastInteractionTime } from '../../bootstrap/state.js';
|
||||||
import { logForDebugging } from '../../utils/debug.js';
|
|
||||||
import { stopCapturingEarlyInput } from '../../utils/earlyInput.js';
|
import { stopCapturingEarlyInput } from '../../utils/earlyInput.js';
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||||
import { isMouseClicksDisabled } from '../../utils/fullscreen.js';
|
import { isMouseClicksDisabled } from '../../utils/fullscreen.js';
|
||||||
@@ -204,6 +203,7 @@ export default class App extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override componentDidCatch(error: Error) {
|
override componentDidCatch(error: Error) {
|
||||||
|
logError(error);
|
||||||
this.handleExit(error);
|
this.handleExit(error);
|
||||||
}
|
}
|
||||||
handleSetRawMode = (isEnabled: boolean): void => {
|
handleSetRawMode = (isEnabled: boolean): void => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import noop from 'lodash-es/noop.js';
|
|||||||
import throttle from 'lodash-es/throttle.js';
|
import throttle from 'lodash-es/throttle.js';
|
||||||
import React, { type ReactNode } from 'react';
|
import React, { type ReactNode } from 'react';
|
||||||
import type { FiberRoot } from 'react-reconciler';
|
import type { FiberRoot } from 'react-reconciler';
|
||||||
import { ConcurrentRoot } from 'react-reconciler/constants.js';
|
import { LegacyRoot } from 'react-reconciler/constants.js';
|
||||||
import { onExit } from 'signal-exit';
|
import { onExit } from 'signal-exit';
|
||||||
import { flushInteractionTime } from 'src/bootstrap/state.js';
|
import { flushInteractionTime } from 'src/bootstrap/state.js';
|
||||||
import { getYogaCounters } from 'src/native-ts/yoga-layout/index.js';
|
import { getYogaCounters } from 'src/native-ts/yoga-layout/index.js';
|
||||||
@@ -177,6 +177,19 @@ export default class Ink {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
private reportRenderError = (label: string, error: unknown): void => {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||||
|
logForDebugging(`[Ink:${label}] ${message}`, {
|
||||||
|
level: 'error',
|
||||||
|
});
|
||||||
|
logError(error);
|
||||||
|
try {
|
||||||
|
this.options.stderr.write(`[Ink:${label}] ${message}\n`);
|
||||||
|
} catch {
|
||||||
|
// Best-effort fallback only.
|
||||||
|
}
|
||||||
|
};
|
||||||
constructor(private readonly options: Options) {
|
constructor(private readonly options: Options) {
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
if (this.options.patchConsole) {
|
if (this.options.patchConsole) {
|
||||||
@@ -259,13 +272,26 @@ export default class Ink {
|
|||||||
|
|
||||||
// @ts-expect-error @types/react-reconciler@0.32.3 declares 11 args with transitionCallbacks,
|
// @ts-expect-error @types/react-reconciler@0.32.3 declares 11 args with transitionCallbacks,
|
||||||
// but react-reconciler 0.33.0 source only accepts 10 args (no transitionCallbacks)
|
// but react-reconciler 0.33.0 source only accepts 10 args (no transitionCallbacks)
|
||||||
this.container = reconciler.createContainer(this.rootNode, ConcurrentRoot, null, false, null, 'id', noop,
|
this.container = reconciler.createContainer(
|
||||||
// onUncaughtError
|
this.rootNode,
|
||||||
noop,
|
LegacyRoot,
|
||||||
// onCaughtError
|
null,
|
||||||
noop,
|
false,
|
||||||
// onRecoverableError
|
null,
|
||||||
noop // onDefaultTransitionIndicator
|
'id',
|
||||||
|
noop,
|
||||||
|
// onUncaughtError
|
||||||
|
error => {
|
||||||
|
this.reportRenderError('uncaught', error);
|
||||||
|
},
|
||||||
|
// onCaughtError
|
||||||
|
error => {
|
||||||
|
this.reportRenderError('caught', error);
|
||||||
|
},
|
||||||
|
// onRecoverableError
|
||||||
|
error => {
|
||||||
|
this.reportRenderError('recoverable', error);
|
||||||
|
}, // onDefaultTransitionIndicator
|
||||||
);
|
);
|
||||||
if ("production" === 'development') {
|
if ("production" === 'development') {
|
||||||
reconciler.injectIntoDevTools({
|
reconciler.injectIntoDevTools({
|
||||||
@@ -1440,6 +1466,7 @@ export default class Ink {
|
|||||||
this.cursorDeclaration = decl;
|
this.cursorDeclaration = decl;
|
||||||
};
|
};
|
||||||
render(node: ReactNode): void {
|
render(node: ReactNode): void {
|
||||||
|
logForDebugging('[Ink:render] start');
|
||||||
this.currentNode = node;
|
this.currentNode = node;
|
||||||
const tree = <App stdin={this.options.stdin} stdout={this.options.stdout} stderr={this.options.stderr} exitOnCtrlC={this.options.exitOnCtrlC} onExit={this.unmount} terminalColumns={this.terminalColumns} terminalRows={this.terminalRows} selection={this.selection} onSelectionChange={this.notifySelectionChange} onClickAt={this.dispatchClick} onHoverAt={this.dispatchHover} getHyperlinkAt={this.getHyperlinkAt} onOpenHyperlink={this.openHyperlink} onMultiClick={this.handleMultiClick} onSelectionDrag={this.handleSelectionDrag} onStdinResume={this.reassertTerminalModes} onCursorDeclaration={this.setCursorDeclaration} dispatchKeyboardEvent={this.dispatchKeyboardEvent}>
|
const tree = <App stdin={this.options.stdin} stdout={this.options.stdout} stderr={this.options.stderr} exitOnCtrlC={this.options.exitOnCtrlC} onExit={this.unmount} terminalColumns={this.terminalColumns} terminalRows={this.terminalRows} selection={this.selection} onSelectionChange={this.notifySelectionChange} onClickAt={this.dispatchClick} onHoverAt={this.dispatchHover} getHyperlinkAt={this.getHyperlinkAt} onOpenHyperlink={this.openHyperlink} onMultiClick={this.handleMultiClick} onSelectionDrag={this.handleSelectionDrag} onStdinResume={this.reassertTerminalModes} onCursorDeclaration={this.setCursorDeclaration} dispatchKeyboardEvent={this.dispatchKeyboardEvent}>
|
||||||
<TerminalWriteProvider value={this.writeRaw}>
|
<TerminalWriteProvider value={this.writeRaw}>
|
||||||
@@ -1447,10 +1474,8 @@ export default class Ink {
|
|||||||
</TerminalWriteProvider>
|
</TerminalWriteProvider>
|
||||||
</App>;
|
</App>;
|
||||||
|
|
||||||
// @ts-expect-error updateContainerSync exists in react-reconciler but not in @types/react-reconciler
|
reconciler.updateContainer(tree, this.container, null, noop);
|
||||||
reconciler.updateContainerSync(tree, this.container, null, noop);
|
logForDebugging('[Ink:render] updateContainer complete');
|
||||||
// @ts-expect-error flushSyncWork exists in react-reconciler but not in @types/react-reconciler
|
|
||||||
reconciler.flushSyncWork();
|
|
||||||
}
|
}
|
||||||
unmount(error?: Error | number | null): void {
|
unmount(error?: Error | number | null): void {
|
||||||
if (this.isUnmounted) {
|
if (this.isUnmounted) {
|
||||||
|
|||||||
29
src/main.tsx
29
src/main.tsx
@@ -905,14 +905,7 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
// Use preAction hook to run initialization only when executing a command,
|
// Use preAction hook to run initialization only when executing a command,
|
||||||
// not when displaying help. This avoids the need for env variable signaling.
|
// not when displaying help. This avoids the need for env variable signaling.
|
||||||
program.hook('preAction', async thisCommand => {
|
program.hook('preAction', async thisCommand => {
|
||||||
profileCheckpoint('preAction_start');
|
|
||||||
// Await async subprocess loads started at module evaluation (lines 12-20).
|
|
||||||
// Nearly free — subprocesses complete during the ~135ms of imports above.
|
|
||||||
// Must resolve before init() which triggers the first settings read
|
|
||||||
// (applySafeConfigEnvironmentVariables → getSettingsForSource('policySettings')
|
|
||||||
// → isRemoteManagedSettingsEligible → sync keychain reads otherwise ~65ms).
|
|
||||||
await Promise.all([ensureMdmSettingsLoaded(), ensureKeychainPrefetchCompleted()]);
|
await Promise.all([ensureMdmSettingsLoaded(), ensureKeychainPrefetchCompleted()]);
|
||||||
profileCheckpoint('preAction_after_mdm');
|
|
||||||
await init();
|
await init();
|
||||||
profileCheckpoint('preAction_after_init');
|
profileCheckpoint('preAction_after_init');
|
||||||
|
|
||||||
@@ -2236,10 +2229,8 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
event: 'startup' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
event: 'startup' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
durationMs: Math.round(process.uptime() * 1000)
|
durationMs: Math.round(process.uptime() * 1000)
|
||||||
});
|
});
|
||||||
logForDebugging('[STARTUP] Running showSetupScreens()...');
|
|
||||||
const setupScreensStart = Date.now();
|
const setupScreensStart = Date.now();
|
||||||
const onboardingShown = await showSetupScreens(root, permissionMode, allowDangerouslySkipPermissions, commands, enableClaudeInChrome, devChannels);
|
const onboardingShown = await showSetupScreens(root, permissionMode, allowDangerouslySkipPermissions, commands, enableClaudeInChrome, devChannels);
|
||||||
logForDebugging(`[STARTUP] showSetupScreens() completed in ${Date.now() - setupScreensStart}ms`);
|
|
||||||
|
|
||||||
// Now that trust is established and GrowthBook has auth headers,
|
// Now that trust is established and GrowthBook has auth headers,
|
||||||
// resolve the --remote-control / --rc entitlement gate.
|
// resolve the --remote-control / --rc entitlement gate.
|
||||||
@@ -2296,9 +2287,6 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the active token's org matches forceLoginOrgUUID (if set
|
|
||||||
// in managed settings). Runs after onboarding so managed settings and
|
|
||||||
// login state are fully loaded.
|
|
||||||
const orgValidation = await validateForceLoginOrg();
|
const orgValidation = await validateForceLoginOrg();
|
||||||
if (!orgValidation.valid) {
|
if (!orgValidation.valid) {
|
||||||
await exitWithError(root, orgValidation.message);
|
await exitWithError(root, orgValidation.message);
|
||||||
@@ -2320,14 +2308,12 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
// Must be after inline plugins are set (if any) so --plugin-dir LSP servers are included.
|
// Must be after inline plugins are set (if any) so --plugin-dir LSP servers are included.
|
||||||
initializeLspServerManager();
|
initializeLspServerManager();
|
||||||
|
|
||||||
// Show settings validation errors after trust is established
|
|
||||||
// MCP config errors don't block settings from loading, so exclude them
|
|
||||||
if (!isNonInteractiveSession) {
|
if (!isNonInteractiveSession) {
|
||||||
const {
|
const {
|
||||||
errors
|
errors
|
||||||
} = getSettingsWithErrors();
|
} = getSettingsWithErrors();
|
||||||
const nonMcpErrors = errors.filter(e => !e.mcpErrorMetadata);
|
const nonMcpErrors = errors.filter(e => !e.mcpErrorMetadata);
|
||||||
if (nonMcpErrors.length > 0) {
|
if (nonMcpErrors.length > 0 && !isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) {
|
||||||
await launchInvalidSettingsDialog(root, {
|
await launchInvalidSettingsDialog(root, {
|
||||||
settingsErrors: nonMcpErrors,
|
settingsErrors: nonMcpErrors,
|
||||||
onExit: () => gracefulShutdownSync(1)
|
onExit: () => gracefulShutdownSync(1)
|
||||||
@@ -2336,8 +2322,6 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check quota status, fast mode, passes eligibility, and bootstrap data
|
// Check quota status, fast mode, passes eligibility, and bootstrap data
|
||||||
// after trust is established. These make API calls which could trigger
|
|
||||||
// apiKeyHelper execution.
|
|
||||||
// --bare / SIMPLE: skip — these are cache-warms for the REPL's
|
// --bare / SIMPLE: skip — these are cache-warms for the REPL's
|
||||||
// first-turn responsiveness (quota, passes, fastMode, bootstrap data). Fast
|
// first-turn responsiveness (quota, passes, fastMode, bootstrap data). Fast
|
||||||
// mode doesn't apply to the Agent SDK anyway (see getFastModeUnavailableReason).
|
// mode doesn't apply to the Agent SDK anyway (see getFastModeUnavailableReason).
|
||||||
@@ -2377,11 +2361,9 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
void refreshExampleCommands(); // Pre-fetch example commands (runs git log, no API call)
|
void refreshExampleCommands(); // Pre-fetch example commands (runs git log, no API call)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve MCP configs (started early, overlaps with setup/trust dialog work)
|
|
||||||
const {
|
const {
|
||||||
servers: existingMcpConfigs
|
servers: existingMcpConfigs
|
||||||
} = await mcpConfigPromise;
|
} = await mcpConfigPromise;
|
||||||
logForDebugging(`[STARTUP] MCP configs resolved in ${mcpConfigResolvedMs}ms (awaited at +${Date.now() - mcpConfigStart}ms)`);
|
|
||||||
// CLI flag (--mcp-config) should override file-based configs, matching settings precedence
|
// CLI flag (--mcp-config) should override file-based configs, matching settings precedence
|
||||||
const allMcpConfigs = {
|
const allMcpConfigs = {
|
||||||
...existingMcpConfigs,
|
...existingMcpConfigs,
|
||||||
@@ -3041,9 +3023,8 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
}
|
}
|
||||||
const initialTools = mcpTools;
|
const initialTools = mcpTools;
|
||||||
|
|
||||||
// Increment numStartups synchronously — first-render readers like
|
// Keep startup bookkeeping best-effort so restricted environments can
|
||||||
// shouldShowEffortCallout (via useState initializer) need the updated
|
// still reach the REPL even when the global config is not writable.
|
||||||
// value before setImmediate fires. Defer only telemetry.
|
|
||||||
saveGlobalConfig(current => ({
|
saveGlobalConfig(current => ({
|
||||||
...current,
|
...current,
|
||||||
numStartups: (current.numStartups ?? 0) + 1
|
numStartups: (current.numStartups ?? 0) + 1
|
||||||
@@ -3758,10 +3739,6 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pass unresolved hooks promise to REPL so it can render immediately
|
|
||||||
// instead of blocking ~500ms waiting for SessionStart hooks to finish.
|
|
||||||
// REPL will inject hook messages when they resolve and await them before
|
|
||||||
// the first API call so the model always sees hook context.
|
|
||||||
const pendingHookMessages = hooksPromise && hookMessages.length === 0 ? hooksPromise : undefined;
|
const pendingHookMessages = hooksPromise && hookMessages.length === 0 ? hooksPromise : undefined;
|
||||||
profileCheckpoint('action_after_hooks');
|
profileCheckpoint('action_after_hooks');
|
||||||
maybeActivateProactive(options);
|
maybeActivateProactive(options);
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ export function REPL({
|
|||||||
sshSession,
|
sshSession,
|
||||||
thinkingConfig
|
thinkingConfig
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
|
logForDebugging('[REPL:render] function entry');
|
||||||
const isRemoteSession = !!remoteSessionConfig;
|
const isRemoteSession = !!remoteSessionConfig;
|
||||||
|
|
||||||
// Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
|
// Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
|
||||||
|
|||||||
@@ -832,6 +832,14 @@ export function saveGlobalConfig(
|
|||||||
writeThroughGlobalConfigCache(written)
|
writeThroughGlobalConfigCache(written)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const code = getErrnoCode(error)
|
||||||
|
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') {
|
||||||
|
logForDebugging(
|
||||||
|
`Skipping global config write due to permission error: ${error}`,
|
||||||
|
{ level: 'error' },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
logForDebugging(`Failed to save config with lock: ${error}`, {
|
logForDebugging(`Failed to save config with lock: ${error}`, {
|
||||||
level: 'error',
|
level: 'error',
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user