mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)
纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,62 +6,91 @@
|
|||||||
* Part of the main.tsx React/JSX extraction effort. See sibling PRs
|
* Part of the main.tsx React/JSX extraction effort. See sibling PRs
|
||||||
* perf/extract-interactive-helpers and perf/launch-repl.
|
* perf/extract-interactive-helpers and perf/launch-repl.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import type { AssistantSession } from './assistant/sessionDiscovery.js';
|
import type { AssistantSession } from './assistant/sessionDiscovery.js'
|
||||||
import type { StatsStore } from './context/stats.js';
|
import type { StatsStore } from './context/stats.js'
|
||||||
import type { Root } from './ink.js';
|
import type { Root } from './ink.js'
|
||||||
import { renderAndRun, showSetupDialog } from './interactiveHelpers.js';
|
import { renderAndRun, showSetupDialog } from './interactiveHelpers.js'
|
||||||
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js';
|
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js'
|
||||||
import type { AppState } from './state/AppStateStore.js';
|
import type { AppState } from './state/AppStateStore.js'
|
||||||
import type { AgentMemoryScope } from './tools/AgentTool/agentMemory.js';
|
import type { AgentMemoryScope } from './tools/AgentTool/agentMemory.js'
|
||||||
import type { TeleportRemoteResponse } from './utils/conversationRecovery.js';
|
import type { TeleportRemoteResponse } from './utils/conversationRecovery.js'
|
||||||
import type { FpsMetrics } from './utils/fpsTracker.js';
|
import type { FpsMetrics } from './utils/fpsTracker.js'
|
||||||
import type { ValidationError } from './utils/settings/validation.js';
|
import type { ValidationError } from './utils/settings/validation.js'
|
||||||
|
|
||||||
// Type-only access to ResumeConversation's Props via the module type.
|
// Type-only access to ResumeConversation's Props via the module type.
|
||||||
// No runtime cost - erased at compile time.
|
// No runtime cost - erased at compile time.
|
||||||
type ResumeConversationProps = React.ComponentProps<typeof import('./screens/ResumeConversation.js').ResumeConversation>;
|
type ResumeConversationProps = React.ComponentProps<
|
||||||
|
typeof import('./screens/ResumeConversation.js').ResumeConversation
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site ~3173: SnapshotUpdateDialog (agent memory snapshot update prompt).
|
* Site ~3173: SnapshotUpdateDialog (agent memory snapshot update prompt).
|
||||||
* Original callback wiring: onComplete={done}, onCancel={() => done('keep')}.
|
* Original callback wiring: onComplete={done}, onCancel={() => done('keep')}.
|
||||||
*/
|
*/
|
||||||
export async function launchSnapshotUpdateDialog(root: Root, props: {
|
export async function launchSnapshotUpdateDialog(
|
||||||
agentType: string;
|
root: Root,
|
||||||
scope: AgentMemoryScope;
|
props: {
|
||||||
snapshotTimestamp: string;
|
agentType: string
|
||||||
}): Promise<'merge' | 'keep' | 'replace'> {
|
scope: AgentMemoryScope
|
||||||
const {
|
snapshotTimestamp: string
|
||||||
SnapshotUpdateDialog
|
},
|
||||||
} = await import('./components/agents/SnapshotUpdateDialog.js');
|
): Promise<'merge' | 'keep' | 'replace'> {
|
||||||
return showSetupDialog<'merge' | 'keep' | 'replace'>(root, done => <SnapshotUpdateDialog agentType={props.agentType} scope={props.scope} snapshotTimestamp={props.snapshotTimestamp} onComplete={done} onCancel={() => done('keep')} />);
|
const { SnapshotUpdateDialog } = await import(
|
||||||
|
'./components/agents/SnapshotUpdateDialog.js'
|
||||||
|
)
|
||||||
|
return showSetupDialog<'merge' | 'keep' | 'replace'>(root, done => (
|
||||||
|
<SnapshotUpdateDialog
|
||||||
|
agentType={props.agentType}
|
||||||
|
scope={props.scope}
|
||||||
|
snapshotTimestamp={props.snapshotTimestamp}
|
||||||
|
onComplete={done}
|
||||||
|
onCancel={() => done('keep')}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site ~3250: InvalidSettingsDialog (settings validation errors).
|
* Site ~3250: InvalidSettingsDialog (settings validation errors).
|
||||||
* Original callback wiring: onContinue={done}, onExit passed through from caller.
|
* Original callback wiring: onContinue={done}, onExit passed through from caller.
|
||||||
*/
|
*/
|
||||||
export async function launchInvalidSettingsDialog(root: Root, props: {
|
export async function launchInvalidSettingsDialog(
|
||||||
settingsErrors: ValidationError[];
|
root: Root,
|
||||||
onExit: () => void;
|
props: {
|
||||||
}): Promise<void> {
|
settingsErrors: ValidationError[]
|
||||||
const {
|
onExit: () => void
|
||||||
InvalidSettingsDialog
|
},
|
||||||
} = await import('./components/InvalidSettingsDialog.js');
|
): Promise<void> {
|
||||||
return showSetupDialog(root, done => <InvalidSettingsDialog settingsErrors={props.settingsErrors} onContinue={done} onExit={props.onExit} />);
|
const { InvalidSettingsDialog } = await import(
|
||||||
|
'./components/InvalidSettingsDialog.js'
|
||||||
|
)
|
||||||
|
return showSetupDialog(root, done => (
|
||||||
|
<InvalidSettingsDialog
|
||||||
|
settingsErrors={props.settingsErrors}
|
||||||
|
onContinue={done}
|
||||||
|
onExit={props.onExit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site ~4229: AssistantSessionChooser (pick a bridge session to attach to).
|
* Site ~4229: AssistantSessionChooser (pick a bridge session to attach to).
|
||||||
* Original callback wiring: onSelect={id => done(id)}, onCancel={() => done(null)}.
|
* Original callback wiring: onSelect={id => done(id)}, onCancel={() => done(null)}.
|
||||||
*/
|
*/
|
||||||
export async function launchAssistantSessionChooser(root: Root, props: {
|
export async function launchAssistantSessionChooser(
|
||||||
sessions: AssistantSession[];
|
root: Root,
|
||||||
}): Promise<string | null> {
|
props: { sessions: AssistantSession[] },
|
||||||
const {
|
): Promise<string | null> {
|
||||||
AssistantSessionChooser
|
const { AssistantSessionChooser } = await import(
|
||||||
} = await import('./assistant/AssistantSessionChooser.js');
|
'./assistant/AssistantSessionChooser.js'
|
||||||
return showSetupDialog<string | null>(root, done => <AssistantSessionChooser sessions={props.sessions} onSelect={id => done(id)} onCancel={() => done(null)} />);
|
)
|
||||||
|
return showSetupDialog<string | null>(root, done => (
|
||||||
|
<AssistantSessionChooser
|
||||||
|
sessions={props.sessions}
|
||||||
|
onSelect={id => done(id)}
|
||||||
|
onCancel={() => done(null)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,43 +99,71 @@ export async function launchAssistantSessionChooser(root: Root, props: {
|
|||||||
* success, null on cancel. Rejects on install failure so the caller can
|
* success, null on cancel. Rejects on install failure so the caller can
|
||||||
* distinguish errors from user cancellation.
|
* distinguish errors from user cancellation.
|
||||||
*/
|
*/
|
||||||
export async function launchAssistantInstallWizard(root: Root): Promise<string | null> {
|
export async function launchAssistantInstallWizard(
|
||||||
const {
|
root: Root,
|
||||||
NewInstallWizard,
|
): Promise<string | null> {
|
||||||
computeDefaultInstallDir
|
const { NewInstallWizard, computeDefaultInstallDir } = await import(
|
||||||
} = await import('./commands/assistant/assistant.js');
|
'./commands/assistant/assistant.js'
|
||||||
const defaultDir = await computeDefaultInstallDir();
|
)
|
||||||
let rejectWithError: (reason: Error) => void;
|
const defaultDir = await computeDefaultInstallDir()
|
||||||
|
let rejectWithError: (reason: Error) => void
|
||||||
const errorPromise = new Promise<never>((_, reject) => {
|
const errorPromise = new Promise<never>((_, reject) => {
|
||||||
rejectWithError = reject;
|
rejectWithError = reject
|
||||||
});
|
})
|
||||||
const resultPromise = showSetupDialog<string | null>(root, done => <NewInstallWizard defaultDir={defaultDir} onInstalled={dir => done(dir)} onCancel={() => done(null)} onError={message => rejectWithError(new Error(`Installation failed: ${message}`))} />);
|
const resultPromise = showSetupDialog<string | null>(root, done => (
|
||||||
return Promise.race([resultPromise, errorPromise]);
|
<NewInstallWizard
|
||||||
|
defaultDir={defaultDir}
|
||||||
|
onInstalled={dir => done(dir)}
|
||||||
|
onCancel={() => done(null)}
|
||||||
|
onError={message =>
|
||||||
|
rejectWithError(new Error(`Installation failed: ${message}`))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
return Promise.race([resultPromise, errorPromise])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site ~4549: TeleportResumeWrapper (interactive teleport session picker).
|
* Site ~4549: TeleportResumeWrapper (interactive teleport session picker).
|
||||||
* Original callback wiring: onComplete={done}, onCancel={() => done(null)}, source="cliArg".
|
* Original callback wiring: onComplete={done}, onCancel={() => done(null)}, source="cliArg".
|
||||||
*/
|
*/
|
||||||
export async function launchTeleportResumeWrapper(root: Root): Promise<TeleportRemoteResponse | null> {
|
export async function launchTeleportResumeWrapper(
|
||||||
const {
|
root: Root,
|
||||||
TeleportResumeWrapper
|
): Promise<TeleportRemoteResponse | null> {
|
||||||
} = await import('./components/TeleportResumeWrapper.js');
|
const { TeleportResumeWrapper } = await import(
|
||||||
return showSetupDialog<TeleportRemoteResponse | null>(root, done => <TeleportResumeWrapper onComplete={done} onCancel={() => done(null)} source="cliArg" />);
|
'./components/TeleportResumeWrapper.js'
|
||||||
|
)
|
||||||
|
return showSetupDialog<TeleportRemoteResponse | null>(root, done => (
|
||||||
|
<TeleportResumeWrapper
|
||||||
|
onComplete={done}
|
||||||
|
onCancel={() => done(null)}
|
||||||
|
source="cliArg"
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site ~4597: TeleportRepoMismatchDialog (pick a local checkout of the target repo).
|
* Site ~4597: TeleportRepoMismatchDialog (pick a local checkout of the target repo).
|
||||||
* Original callback wiring: onSelectPath={done}, onCancel={() => done(null)}.
|
* Original callback wiring: onSelectPath={done}, onCancel={() => done(null)}.
|
||||||
*/
|
*/
|
||||||
export async function launchTeleportRepoMismatchDialog(root: Root, props: {
|
export async function launchTeleportRepoMismatchDialog(
|
||||||
targetRepo: string;
|
root: Root,
|
||||||
initialPaths: string[];
|
props: {
|
||||||
}): Promise<string | null> {
|
targetRepo: string
|
||||||
const {
|
initialPaths: string[]
|
||||||
TeleportRepoMismatchDialog
|
},
|
||||||
} = await import('./components/TeleportRepoMismatchDialog.js');
|
): Promise<string | null> {
|
||||||
return showSetupDialog<string | null>(root, done => <TeleportRepoMismatchDialog targetRepo={props.targetRepo} initialPaths={props.initialPaths} onSelectPath={done} onCancel={() => done(null)} />);
|
const { TeleportRepoMismatchDialog } = await import(
|
||||||
|
'./components/TeleportRepoMismatchDialog.js'
|
||||||
|
)
|
||||||
|
return showSetupDialog<string | null>(root, done => (
|
||||||
|
<TeleportRepoMismatchDialog
|
||||||
|
targetRepo={props.targetRepo}
|
||||||
|
initialPaths={props.initialPaths}
|
||||||
|
onSelectPath={done}
|
||||||
|
onCancel={() => done(null)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,19 +171,31 @@ export async function launchTeleportRepoMismatchDialog(root: Root, props: {
|
|||||||
* Uses renderAndRun, NOT showSetupDialog. Wraps in <App><KeybindingSetup>.
|
* Uses renderAndRun, NOT showSetupDialog. Wraps in <App><KeybindingSetup>.
|
||||||
* Preserves original Promise.all parallelism between getWorktreePaths and imports.
|
* Preserves original Promise.all parallelism between getWorktreePaths and imports.
|
||||||
*/
|
*/
|
||||||
export async function launchResumeChooser(root: Root, appProps: {
|
export async function launchResumeChooser(
|
||||||
getFpsMetrics: () => FpsMetrics | undefined;
|
root: Root,
|
||||||
stats: StatsStore;
|
appProps: {
|
||||||
initialState: AppState;
|
getFpsMetrics: () => FpsMetrics | undefined
|
||||||
}, worktreePathsPromise: Promise<string[]>, resumeProps: Omit<ResumeConversationProps, 'worktreePaths'>): Promise<void> {
|
stats: StatsStore
|
||||||
const [worktreePaths, {
|
initialState: AppState
|
||||||
ResumeConversation
|
},
|
||||||
}, {
|
worktreePathsPromise: Promise<string[]>,
|
||||||
App
|
resumeProps: Omit<ResumeConversationProps, 'worktreePaths'>,
|
||||||
}] = await Promise.all([worktreePathsPromise, import('./screens/ResumeConversation.js'), import('./components/App.js')]);
|
): Promise<void> {
|
||||||
await renderAndRun(root, <App getFpsMetrics={appProps.getFpsMetrics} stats={appProps.stats} initialState={appProps.initialState}>
|
const [worktreePaths, { ResumeConversation }, { App }] = await Promise.all([
|
||||||
|
worktreePathsPromise,
|
||||||
|
import('./screens/ResumeConversation.js'),
|
||||||
|
import('./components/App.js'),
|
||||||
|
])
|
||||||
|
await renderAndRun(
|
||||||
|
root,
|
||||||
|
<App
|
||||||
|
getFpsMetrics={appProps.getFpsMetrics}
|
||||||
|
stats={appProps.stats}
|
||||||
|
initialState={appProps.initialState}
|
||||||
|
>
|
||||||
<KeybindingSetup>
|
<KeybindingSetup>
|
||||||
<ResumeConversation {...resumeProps} worktreePaths={worktreePaths} />
|
<ResumeConversation {...resumeProps} worktreePaths={worktreePaths} />
|
||||||
</KeybindingSetup>
|
</KeybindingSetup>
|
||||||
</App>);
|
</App>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { feature } from 'bun:bundle';
|
import { feature } from 'bun:bundle'
|
||||||
|
|
||||||
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
||||||
process.env.COREPACK_ENABLE_AUTO_PIN = '0';
|
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
|
||||||
|
|
||||||
// Set max heap size for child processes in CCR environments (containers have 16GB)
|
// Set max heap size for child processes in CCR environments (containers have 16GB)
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check
|
||||||
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
|
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
||||||
const existing = process.env.NODE_OPTIONS || '';
|
const existing = process.env.NODE_OPTIONS || ''
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
||||||
process.env.NODE_OPTIONS = existing ? `${existing} --max-old-space-size=8192` : '--max-old-space-size=8192';
|
process.env.NODE_OPTIONS = existing
|
||||||
|
? `${existing} --max-old-space-size=8192`
|
||||||
|
: '--max-old-space-size=8192'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Harness-science L0 ablation baseline. Inlined here (not init.ts) because
|
// Harness-science L0 ablation baseline. Inlined here (not init.ts) because
|
||||||
@@ -30,7 +32,7 @@ if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
|
|||||||
'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS',
|
'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS',
|
||||||
]) {
|
]) {
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
|
||||||
process.env[k] ??= '1';
|
process.env[k] ??= '1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,51 +42,64 @@ if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
|
|||||||
* Fast-path for --version has zero imports beyond this file.
|
* Fast-path for --version has zero imports beyond this file.
|
||||||
*/
|
*/
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
// Fast-path for --version/-v: zero module loading needed
|
// Fast-path for --version/-v: zero module loading needed
|
||||||
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
|
if (
|
||||||
|
args.length === 1 &&
|
||||||
|
(args[0] === '--version' || args[0] === '-v' || args[0] === '-V')
|
||||||
|
) {
|
||||||
// MACRO.VERSION is inlined at build time
|
// MACRO.VERSION is inlined at build time
|
||||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||||
console.log(`${MACRO.VERSION} (Claude Code)`);
|
console.log(`${MACRO.VERSION} (Claude Code)`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all other paths, load the startup profiler
|
// For all other paths, load the startup profiler
|
||||||
const { profileCheckpoint } = await import('../utils/startupProfiler.js');
|
const { profileCheckpoint } = await import('../utils/startupProfiler.js')
|
||||||
profileCheckpoint('cli_entry');
|
profileCheckpoint('cli_entry')
|
||||||
|
|
||||||
// Fast-path for --dump-system-prompt: output the rendered system prompt and exit.
|
// Fast-path for --dump-system-prompt: output the rendered system prompt and exit.
|
||||||
// Used by prompt sensitivity evals to extract the system prompt at a specific commit.
|
// Used by prompt sensitivity evals to extract the system prompt at a specific commit.
|
||||||
// Ant-only: eliminated from external builds via feature flag.
|
// Ant-only: eliminated from external builds via feature flag.
|
||||||
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
|
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
|
||||||
profileCheckpoint('cli_dump_system_prompt_path');
|
profileCheckpoint('cli_dump_system_prompt_path')
|
||||||
const { enableConfigs } = await import('../utils/config.js');
|
const { enableConfigs } = await import('../utils/config.js')
|
||||||
enableConfigs();
|
enableConfigs()
|
||||||
const { getMainLoopModel } = await import('../utils/model/model.js');
|
const { getMainLoopModel } = await import('../utils/model/model.js')
|
||||||
const modelIdx = args.indexOf('--model');
|
const modelIdx = args.indexOf('--model')
|
||||||
const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel();
|
const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel()
|
||||||
const { getSystemPrompt } = await import('../constants/prompts.js');
|
const { getSystemPrompt } = await import('../constants/prompts.js')
|
||||||
const prompt = await getSystemPrompt([], model);
|
const prompt = await getSystemPrompt([], model)
|
||||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||||
console.log(prompt.join('\n'));
|
console.log(prompt.join('\n'))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[2] === '--claude-in-chrome-mcp') {
|
if (process.argv[2] === '--claude-in-chrome-mcp') {
|
||||||
profileCheckpoint('cli_claude_in_chrome_mcp_path');
|
profileCheckpoint('cli_claude_in_chrome_mcp_path')
|
||||||
const { runClaudeInChromeMcpServer } = await import('../utils/claudeInChrome/mcpServer.js');
|
const { runClaudeInChromeMcpServer } = await import(
|
||||||
await runClaudeInChromeMcpServer();
|
'../utils/claudeInChrome/mcpServer.js'
|
||||||
return;
|
)
|
||||||
|
await runClaudeInChromeMcpServer()
|
||||||
|
return
|
||||||
} else if (process.argv[2] === '--chrome-native-host') {
|
} else if (process.argv[2] === '--chrome-native-host') {
|
||||||
profileCheckpoint('cli_chrome_native_host_path');
|
profileCheckpoint('cli_chrome_native_host_path')
|
||||||
const { runChromeNativeHost } = await import('../utils/claudeInChrome/chromeNativeHost.js');
|
const { runChromeNativeHost } = await import(
|
||||||
await runChromeNativeHost();
|
'../utils/claudeInChrome/chromeNativeHost.js'
|
||||||
return;
|
)
|
||||||
} else if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') {
|
await runChromeNativeHost()
|
||||||
profileCheckpoint('cli_computer_use_mcp_path');
|
return
|
||||||
const { runComputerUseMcpServer } = await import('../utils/computerUse/mcpServer.js');
|
} else if (
|
||||||
await runComputerUseMcpServer();
|
feature('CHICAGO_MCP') &&
|
||||||
return;
|
process.argv[2] === '--computer-use-mcp'
|
||||||
|
) {
|
||||||
|
profileCheckpoint('cli_computer_use_mcp_path')
|
||||||
|
const { runComputerUseMcpServer } = await import(
|
||||||
|
'../utils/computerUse/mcpServer.js'
|
||||||
|
)
|
||||||
|
await runComputerUseMcpServer()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `--daemon-worker=<kind>` (internal — supervisor spawns this).
|
// Fast-path for `--daemon-worker=<kind>` (internal — supervisor spawns this).
|
||||||
@@ -93,9 +108,9 @@ async function main(): Promise<void> {
|
|||||||
// workers are lean. If a worker kind needs configs/auth (assistant will),
|
// workers are lean. If a worker kind needs configs/auth (assistant will),
|
||||||
// it calls them inside its run() fn.
|
// it calls them inside its run() fn.
|
||||||
if (feature('DAEMON') && args[0] === '--daemon-worker') {
|
if (feature('DAEMON') && args[0] === '--daemon-worker') {
|
||||||
const { runDaemonWorker } = await import('../daemon/workerRegistry.js');
|
const { runDaemonWorker } = await import('../daemon/workerRegistry.js')
|
||||||
await runDaemonWorker(args[1]);
|
await runDaemonWorker(args[1])
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`):
|
// Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`):
|
||||||
@@ -110,51 +125,59 @@ async function main(): Promise<void> {
|
|||||||
args[0] === 'sync' ||
|
args[0] === 'sync' ||
|
||||||
args[0] === 'bridge')
|
args[0] === 'bridge')
|
||||||
) {
|
) {
|
||||||
profileCheckpoint('cli_bridge_path');
|
profileCheckpoint('cli_bridge_path')
|
||||||
const { enableConfigs } = await import('../utils/config.js');
|
const { enableConfigs } = await import('../utils/config.js')
|
||||||
enableConfigs();
|
enableConfigs()
|
||||||
const { getBridgeDisabledReason, checkBridgeMinVersion } = await import('../bridge/bridgeEnabled.js');
|
|
||||||
const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js');
|
const { getBridgeDisabledReason, checkBridgeMinVersion } = await import(
|
||||||
const { bridgeMain } = await import('../bridge/bridgeMain.js');
|
'../bridge/bridgeEnabled.js'
|
||||||
const { exitWithError } = await import('../utils/process.js');
|
)
|
||||||
|
const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js')
|
||||||
|
const { bridgeMain } = await import('../bridge/bridgeMain.js')
|
||||||
|
const { exitWithError } = await import('../utils/process.js')
|
||||||
|
|
||||||
// Auth check must come before the GrowthBook gate check — without auth,
|
// Auth check must come before the GrowthBook gate check — without auth,
|
||||||
// GrowthBook has no user context and would return a stale/default false.
|
// GrowthBook has no user context and would return a stale/default false.
|
||||||
// getBridgeDisabledReason awaits GB init, so the returned value is fresh
|
// getBridgeDisabledReason awaits GB init, so the returned value is fresh
|
||||||
// (not the stale disk cache), but init still needs auth headers to work.
|
// (not the stale disk cache), but init still needs auth headers to work.
|
||||||
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js');
|
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
||||||
if (!getClaudeAIOAuthTokens()?.accessToken) {
|
if (!getClaudeAIOAuthTokens()?.accessToken) {
|
||||||
exitWithError(BRIDGE_LOGIN_ERROR);
|
exitWithError(BRIDGE_LOGIN_ERROR)
|
||||||
}
|
}
|
||||||
const disabledReason = await getBridgeDisabledReason();
|
const disabledReason = await getBridgeDisabledReason()
|
||||||
if (disabledReason) {
|
if (disabledReason) {
|
||||||
exitWithError(`Error: ${disabledReason}`);
|
exitWithError(`Error: ${disabledReason}`)
|
||||||
}
|
}
|
||||||
const versionError = checkBridgeMinVersion();
|
const versionError = checkBridgeMinVersion()
|
||||||
if (versionError) {
|
if (versionError) {
|
||||||
exitWithError(versionError);
|
exitWithError(versionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridge is a remote control feature - check policy limits
|
// Bridge is a remote control feature - check policy limits
|
||||||
const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import('../services/policyLimits/index.js');
|
const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import(
|
||||||
await waitForPolicyLimitsToLoad();
|
'../services/policyLimits/index.js'
|
||||||
|
)
|
||||||
|
await waitForPolicyLimitsToLoad()
|
||||||
if (!isPolicyAllowed('allow_remote_control')) {
|
if (!isPolicyAllowed('allow_remote_control')) {
|
||||||
exitWithError("Error: Remote Control is disabled by your organization's policy.");
|
exitWithError(
|
||||||
|
"Error: Remote Control is disabled by your organization's policy.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
await bridgeMain(args.slice(1));
|
|
||||||
return;
|
await bridgeMain(args.slice(1))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `claude daemon [subcommand]`: long-running supervisor.
|
// Fast-path for `claude daemon [subcommand]`: long-running supervisor.
|
||||||
if (feature('DAEMON') && args[0] === 'daemon') {
|
if (feature('DAEMON') && args[0] === 'daemon') {
|
||||||
profileCheckpoint('cli_daemon_path');
|
profileCheckpoint('cli_daemon_path')
|
||||||
const { enableConfigs } = await import('../utils/config.js');
|
const { enableConfigs } = await import('../utils/config.js')
|
||||||
enableConfigs();
|
enableConfigs()
|
||||||
const { initSinks } = await import('../utils/sinks.js');
|
const { initSinks } = await import('../utils/sinks.js')
|
||||||
initSinks();
|
initSinks()
|
||||||
const { daemonMain } = await import('../daemon/main.js');
|
const { daemonMain } = await import('../daemon/main.js')
|
||||||
await daemonMain(args.slice(1));
|
await daemonMain(args.slice(1))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`.
|
// Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`.
|
||||||
@@ -169,103 +192,117 @@ async function main(): Promise<void> {
|
|||||||
args.includes('--bg') ||
|
args.includes('--bg') ||
|
||||||
args.includes('--background'))
|
args.includes('--background'))
|
||||||
) {
|
) {
|
||||||
profileCheckpoint('cli_bg_path');
|
profileCheckpoint('cli_bg_path')
|
||||||
const { enableConfigs } = await import('../utils/config.js');
|
const { enableConfigs } = await import('../utils/config.js')
|
||||||
enableConfigs();
|
enableConfigs()
|
||||||
const bg = await import('../cli/bg.js');
|
const bg = await import('../cli/bg.js')
|
||||||
switch (args[0]) {
|
switch (args[0]) {
|
||||||
case 'ps':
|
case 'ps':
|
||||||
await bg.psHandler(args.slice(1));
|
await bg.psHandler(args.slice(1))
|
||||||
break;
|
break
|
||||||
case 'logs':
|
case 'logs':
|
||||||
await bg.logsHandler(args[1]);
|
await bg.logsHandler(args[1])
|
||||||
break;
|
break
|
||||||
case 'attach':
|
case 'attach':
|
||||||
await bg.attachHandler(args[1]);
|
await bg.attachHandler(args[1])
|
||||||
break;
|
break
|
||||||
case 'kill':
|
case 'kill':
|
||||||
await bg.killHandler(args[1]);
|
await bg.killHandler(args[1])
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
await bg.handleBgFlag(args);
|
await bg.handleBgFlag(args)
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for template job commands.
|
// Fast-path for template job commands.
|
||||||
if (feature('TEMPLATES') && (args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')) {
|
if (
|
||||||
profileCheckpoint('cli_templates_path');
|
feature('TEMPLATES') &&
|
||||||
const { templatesMain } = await import('../cli/handlers/templateJobs.js');
|
(args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')
|
||||||
await templatesMain(args);
|
) {
|
||||||
|
profileCheckpoint('cli_templates_path')
|
||||||
|
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
|
||||||
|
await templatesMain(args)
|
||||||
// process.exit (not return) — mountFleetView's Ink TUI can leave event
|
// process.exit (not return) — mountFleetView's Ink TUI can leave event
|
||||||
// loop handles that prevent natural exit.
|
// loop handles that prevent natural exit.
|
||||||
// eslint-disable-next-line custom-rules/no-process-exit
|
// eslint-disable-next-line custom-rules/no-process-exit
|
||||||
process.exit(0);
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `claude environment-runner`: headless BYOC runner.
|
// Fast-path for `claude environment-runner`: headless BYOC runner.
|
||||||
// feature() must stay inline for build-time dead code elimination.
|
// feature() must stay inline for build-time dead code elimination.
|
||||||
if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') {
|
if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') {
|
||||||
profileCheckpoint('cli_environment_runner_path');
|
profileCheckpoint('cli_environment_runner_path')
|
||||||
const { environmentRunnerMain } = await import('../environment-runner/main.js');
|
const { environmentRunnerMain } = await import(
|
||||||
await environmentRunnerMain(args.slice(1));
|
'../environment-runner/main.js'
|
||||||
return;
|
)
|
||||||
|
await environmentRunnerMain(args.slice(1))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for `claude self-hosted-runner`: headless self-hosted-runner
|
// Fast-path for `claude self-hosted-runner`: headless self-hosted-runner
|
||||||
// targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS
|
// targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS
|
||||||
// heartbeat). feature() must stay inline for build-time dead code elimination.
|
// heartbeat). feature() must stay inline for build-time dead code elimination.
|
||||||
if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') {
|
if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') {
|
||||||
profileCheckpoint('cli_self_hosted_runner_path');
|
profileCheckpoint('cli_self_hosted_runner_path')
|
||||||
const { selfHostedRunnerMain } = await import('../self-hosted-runner/main.js');
|
const { selfHostedRunnerMain } = await import(
|
||||||
await selfHostedRunnerMain(args.slice(1));
|
'../self-hosted-runner/main.js'
|
||||||
return;
|
)
|
||||||
|
await selfHostedRunnerMain(args.slice(1))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast-path for --worktree --tmux: exec into tmux before loading full CLI
|
// Fast-path for --worktree --tmux: exec into tmux before loading full CLI
|
||||||
const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic');
|
const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic')
|
||||||
if (
|
if (
|
||||||
hasTmuxFlag &&
|
hasTmuxFlag &&
|
||||||
(args.includes('-w') || args.includes('--worktree') || args.some(a => a.startsWith('--worktree=')))
|
(args.includes('-w') ||
|
||||||
|
args.includes('--worktree') ||
|
||||||
|
args.some(a => a.startsWith('--worktree=')))
|
||||||
) {
|
) {
|
||||||
profileCheckpoint('cli_tmux_worktree_fast_path');
|
profileCheckpoint('cli_tmux_worktree_fast_path')
|
||||||
const { enableConfigs } = await import('../utils/config.js');
|
const { enableConfigs } = await import('../utils/config.js')
|
||||||
enableConfigs();
|
enableConfigs()
|
||||||
const { isWorktreeModeEnabled } = await import('../utils/worktreeModeEnabled.js');
|
const { isWorktreeModeEnabled } = await import(
|
||||||
|
'../utils/worktreeModeEnabled.js'
|
||||||
|
)
|
||||||
if (isWorktreeModeEnabled()) {
|
if (isWorktreeModeEnabled()) {
|
||||||
const { execIntoTmuxWorktree } = await import('../utils/worktree.js');
|
const { execIntoTmuxWorktree } = await import('../utils/worktree.js')
|
||||||
const result = await execIntoTmuxWorktree(args);
|
const result = await execIntoTmuxWorktree(args)
|
||||||
if (result.handled) {
|
if (result.handled) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
// If not handled (e.g., error), fall through to normal CLI
|
// If not handled (e.g., error), fall through to normal CLI
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
const { exitWithError } = await import('../utils/process.js');
|
const { exitWithError } = await import('../utils/process.js')
|
||||||
exitWithError(result.error);
|
exitWithError(result.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect common update flag mistakes to the update subcommand
|
// Redirect common update flag mistakes to the update subcommand
|
||||||
if (args.length === 1 && (args[0] === '--update' || args[0] === '--upgrade')) {
|
if (
|
||||||
process.argv = [process.argv[0]!, process.argv[1]!, 'update'];
|
args.length === 1 &&
|
||||||
|
(args[0] === '--update' || args[0] === '--upgrade')
|
||||||
|
) {
|
||||||
|
process.argv = [process.argv[0]!, process.argv[1]!, 'update']
|
||||||
}
|
}
|
||||||
|
|
||||||
// --bare: set SIMPLE early so gates fire during module eval / commander
|
// --bare: set SIMPLE early so gates fire during module eval / commander
|
||||||
// option building (not just inside the action handler).
|
// option building (not just inside the action handler).
|
||||||
if (args.includes('--bare')) {
|
if (args.includes('--bare')) {
|
||||||
process.env.CLAUDE_CODE_SIMPLE = '1';
|
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
// No special flags detected, load and run the full CLI
|
// No special flags detected, load and run the full CLI
|
||||||
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js');
|
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js')
|
||||||
startCapturingEarlyInput();
|
startCapturingEarlyInput()
|
||||||
profileCheckpoint('cli_before_main_import');
|
profileCheckpoint('cli_before_main_import')
|
||||||
const { main: cliMain } = await import('../main.jsx');
|
const { main: cliMain } = await import('../main.jsx')
|
||||||
profileCheckpoint('cli_after_main_import');
|
profileCheckpoint('cli_after_main_import')
|
||||||
await cliMain();
|
await cliMain()
|
||||||
profileCheckpoint('cli_after_main_complete');
|
profileCheckpoint('cli_after_main_complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
||||||
void main();
|
void main()
|
||||||
|
|||||||
@@ -1,46 +1,76 @@
|
|||||||
import { feature } from 'bun:bundle';
|
import { feature } from 'bun:bundle'
|
||||||
import { appendFileSync } from 'fs';
|
import { appendFileSync } from 'fs'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { logEvent } from 'src/services/analytics/index.js';
|
import { logEvent } from 'src/services/analytics/index.js'
|
||||||
import { gracefulShutdown, gracefulShutdownSync } from 'src/utils/gracefulShutdown.js';
|
import {
|
||||||
import { type ChannelEntry, getAllowedChannels, setAllowedChannels, setHasDevChannels, setSessionTrustAccepted, setStatsStore } from './bootstrap/state.js';
|
gracefulShutdown,
|
||||||
import type { Command } from './commands.js';
|
gracefulShutdownSync,
|
||||||
import { createStatsStore, type StatsStore } from './context/stats.js';
|
} from 'src/utils/gracefulShutdown.js'
|
||||||
import { getSystemContext } from './context.js';
|
import {
|
||||||
import { initializeTelemetryAfterTrust } from './entrypoints/init.js';
|
type ChannelEntry,
|
||||||
import { isSynchronizedOutputSupported } from './ink/terminal.js';
|
getAllowedChannels,
|
||||||
import type { RenderOptions, Root, TextProps } from './ink.js';
|
setAllowedChannels,
|
||||||
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js';
|
setHasDevChannels,
|
||||||
import { startDeferredPrefetches } from './main.js';
|
setSessionTrustAccepted,
|
||||||
import { checkGate_CACHED_OR_BLOCKING, initializeGrowthBook, resetGrowthBook } from './services/analytics/growthbook.js';
|
setStatsStore,
|
||||||
import { isQualifiedForGrove } from './services/api/grove.js';
|
} from './bootstrap/state.js'
|
||||||
import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js';
|
import type { Command } from './commands.js'
|
||||||
import { AppStateProvider } from './state/AppState.js';
|
import { createStatsStore, type StatsStore } from './context/stats.js'
|
||||||
import { onChangeAppState } from './state/onChangeAppState.js';
|
import { getSystemContext } from './context.js'
|
||||||
import { normalizeApiKeyForConfig } from './utils/authPortable.js';
|
import { initializeTelemetryAfterTrust } from './entrypoints/init.js'
|
||||||
import { getExternalClaudeMdIncludes, getMemoryFiles, shouldShowClaudeMdExternalIncludesWarning } from './utils/claudemd.js';
|
import { isSynchronizedOutputSupported } from './ink/terminal.js'
|
||||||
import { checkHasTrustDialogAccepted, getCustomApiKeyStatus, getGlobalConfig, saveGlobalConfig } from './utils/config.js';
|
import type { RenderOptions, Root, TextProps } from './ink.js'
|
||||||
import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js';
|
import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js'
|
||||||
import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js';
|
import { startDeferredPrefetches } from './main.js'
|
||||||
import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js';
|
import {
|
||||||
import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js';
|
checkGate_CACHED_OR_BLOCKING,
|
||||||
import { applyConfigEnvironmentVariables } from './utils/managedEnv.js';
|
initializeGrowthBook,
|
||||||
import type { PermissionMode } from './utils/permissions/PermissionMode.js';
|
resetGrowthBook,
|
||||||
import { getBaseRenderOptions } from './utils/renderOptions.js';
|
} from './services/analytics/growthbook.js'
|
||||||
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js';
|
import { isQualifiedForGrove } from './services/api/grove.js'
|
||||||
import { hasAutoModeOptIn, hasSkipDangerousModePermissionPrompt } from './utils/settings/settings.js';
|
import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js'
|
||||||
|
import { AppStateProvider } from './state/AppState.js'
|
||||||
|
import { onChangeAppState } from './state/onChangeAppState.js'
|
||||||
|
import { normalizeApiKeyForConfig } from './utils/authPortable.js'
|
||||||
|
import {
|
||||||
|
getExternalClaudeMdIncludes,
|
||||||
|
getMemoryFiles,
|
||||||
|
shouldShowClaudeMdExternalIncludesWarning,
|
||||||
|
} from './utils/claudemd.js'
|
||||||
|
import {
|
||||||
|
checkHasTrustDialogAccepted,
|
||||||
|
getCustomApiKeyStatus,
|
||||||
|
getGlobalConfig,
|
||||||
|
saveGlobalConfig,
|
||||||
|
} from './utils/config.js'
|
||||||
|
import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js'
|
||||||
|
import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js'
|
||||||
|
import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js'
|
||||||
|
import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js'
|
||||||
|
import { applyConfigEnvironmentVariables } from './utils/managedEnv.js'
|
||||||
|
import type { PermissionMode } from './utils/permissions/PermissionMode.js'
|
||||||
|
import { getBaseRenderOptions } from './utils/renderOptions.js'
|
||||||
|
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js'
|
||||||
|
import {
|
||||||
|
hasAutoModeOptIn,
|
||||||
|
hasSkipDangerousModePermissionPrompt,
|
||||||
|
} from './utils/settings/settings.js'
|
||||||
|
|
||||||
export function completeOnboarding(): void {
|
export function completeOnboarding(): void {
|
||||||
saveGlobalConfig(current => ({
|
saveGlobalConfig(current => ({
|
||||||
...current,
|
...current,
|
||||||
hasCompletedOnboarding: true,
|
hasCompletedOnboarding: true,
|
||||||
lastOnboardingVersion: MACRO.VERSION
|
lastOnboardingVersion: MACRO.VERSION,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
export function showDialog<T = void>(root: Root, renderer: (done: (result: T) => void) => React.ReactNode): Promise<T> {
|
export function showDialog<T = void>(
|
||||||
|
root: Root,
|
||||||
|
renderer: (done: (result: T) => void) => React.ReactNode,
|
||||||
|
): Promise<T> {
|
||||||
return new Promise<T>(resolve => {
|
return new Promise<T>(resolve => {
|
||||||
const done = (result: T): void => void resolve(result);
|
const done = (result: T): void => void resolve(result)
|
||||||
root.render(renderer(done));
|
root.render(renderer(done))
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,11 +79,12 @@ export function showDialog<T = void>(root: Root, renderer: (done: (result: T) =>
|
|||||||
* console.error is swallowed by Ink's patchConsole, so we render
|
* console.error is swallowed by Ink's patchConsole, so we render
|
||||||
* through the React tree instead.
|
* through the React tree instead.
|
||||||
*/
|
*/
|
||||||
export async function exitWithError(root: Root, message: string, beforeExit?: () => Promise<void>): Promise<never> {
|
export async function exitWithError(
|
||||||
return exitWithMessage(root, message, {
|
root: Root,
|
||||||
color: 'error',
|
message: string,
|
||||||
beforeExit
|
beforeExit?: () => Promise<void>,
|
||||||
});
|
): Promise<never> {
|
||||||
|
return exitWithMessage(root, message, { color: 'error', beforeExit })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,64 +93,93 @@ export async function exitWithError(root: Root, message: string, beforeExit?: ()
|
|||||||
* console output is swallowed by Ink's patchConsole, so we render
|
* console output is swallowed by Ink's patchConsole, so we render
|
||||||
* through the React tree instead.
|
* through the React tree instead.
|
||||||
*/
|
*/
|
||||||
export async function exitWithMessage(root: Root, message: string, options?: {
|
export async function exitWithMessage(
|
||||||
color?: TextProps['color'];
|
root: Root,
|
||||||
exitCode?: number;
|
message: string,
|
||||||
beforeExit?: () => Promise<void>;
|
options?: {
|
||||||
}): Promise<never> {
|
color?: TextProps['color']
|
||||||
const {
|
exitCode?: number
|
||||||
Text
|
beforeExit?: () => Promise<void>
|
||||||
} = await import('./ink.js');
|
},
|
||||||
const color = options?.color;
|
): Promise<never> {
|
||||||
const exitCode = options?.exitCode ?? 1;
|
const { Text } = await import('./ink.js')
|
||||||
root.render(color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>);
|
const color = options?.color
|
||||||
root.unmount();
|
const exitCode = options?.exitCode ?? 1
|
||||||
await options?.beforeExit?.();
|
root.render(
|
||||||
|
color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>,
|
||||||
|
)
|
||||||
|
root.unmount()
|
||||||
|
await options?.beforeExit?.()
|
||||||
// eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount
|
// eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount
|
||||||
process.exit(exitCode);
|
process.exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a setup dialog wrapped in AppStateProvider + KeybindingSetup.
|
* Show a setup dialog wrapped in AppStateProvider + KeybindingSetup.
|
||||||
* Reduces boilerplate in showSetupScreens() where every dialog needs these wrappers.
|
* Reduces boilerplate in showSetupScreens() where every dialog needs these wrappers.
|
||||||
*/
|
*/
|
||||||
export function showSetupDialog<T = void>(root: Root, renderer: (done: (result: T) => void) => React.ReactNode, options?: {
|
export function showSetupDialog<T = void>(
|
||||||
onChangeAppState?: typeof onChangeAppState;
|
root: Root,
|
||||||
}): Promise<T> {
|
renderer: (done: (result: T) => void) => React.ReactNode,
|
||||||
return showDialog<T>(root, done => <AppStateProvider onChangeAppState={options?.onChangeAppState}>
|
options?: { onChangeAppState?: typeof onChangeAppState },
|
||||||
|
): Promise<T> {
|
||||||
|
return showDialog<T>(root, done => (
|
||||||
|
<AppStateProvider onChangeAppState={options?.onChangeAppState}>
|
||||||
<KeybindingSetup>{renderer(done)}</KeybindingSetup>
|
<KeybindingSetup>{renderer(done)}</KeybindingSetup>
|
||||||
</AppStateProvider>);
|
</AppStateProvider>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the main UI into the root and wait for it to exit.
|
* Render the main UI into the root and wait for it to exit.
|
||||||
* Handles the common epilogue: start deferred prefetches, wait for exit, graceful shutdown.
|
* Handles the common epilogue: start deferred prefetches, wait for exit, graceful shutdown.
|
||||||
*/
|
*/
|
||||||
export async function renderAndRun(root: Root, element: React.ReactNode): Promise<void> {
|
export async function renderAndRun(
|
||||||
root.render(element);
|
root: Root,
|
||||||
startDeferredPrefetches();
|
element: React.ReactNode,
|
||||||
await root.waitUntilExit();
|
): Promise<void> {
|
||||||
await gracefulShutdown(0);
|
root.render(element)
|
||||||
|
startDeferredPrefetches()
|
||||||
|
await root.waitUntilExit()
|
||||||
|
await gracefulShutdown(0)
|
||||||
}
|
}
|
||||||
export async function showSetupScreens(root: Root, permissionMode: PermissionMode, allowDangerouslySkipPermissions: boolean, commands?: Command[], claudeInChrome?: boolean, devChannels?: ChannelEntry[]): Promise<boolean> {
|
|
||||||
if (("production" as string) === 'test' || isEnvTruthy(false) || process.env.IS_DEMO // Skip onboarding in demo mode
|
export async function showSetupScreens(
|
||||||
|
root: Root,
|
||||||
|
permissionMode: PermissionMode,
|
||||||
|
allowDangerouslySkipPermissions: boolean,
|
||||||
|
commands?: Command[],
|
||||||
|
claudeInChrome?: boolean,
|
||||||
|
devChannels?: ChannelEntry[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (
|
||||||
|
"production" === 'test' ||
|
||||||
|
isEnvTruthy(false) ||
|
||||||
|
process.env.IS_DEMO // Skip onboarding in demo mode
|
||||||
) {
|
) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
const config = getGlobalConfig();
|
|
||||||
let onboardingShown = false;
|
const config = getGlobalConfig()
|
||||||
if (!config.theme || !config.hasCompletedOnboarding // always show onboarding at least once
|
let onboardingShown = false
|
||||||
|
if (
|
||||||
|
!config.theme ||
|
||||||
|
!config.hasCompletedOnboarding // always show onboarding at least once
|
||||||
) {
|
) {
|
||||||
onboardingShown = true;
|
onboardingShown = true
|
||||||
const {
|
const { Onboarding } = await import('./components/Onboarding.js')
|
||||||
Onboarding
|
await showSetupDialog(
|
||||||
} = await import('./components/Onboarding.js');
|
root,
|
||||||
await showSetupDialog(root, done => <Onboarding onDone={() => {
|
done => (
|
||||||
completeOnboarding();
|
<Onboarding
|
||||||
void done();
|
onDone={() => {
|
||||||
}} />, {
|
completeOnboarding()
|
||||||
onChangeAppState
|
void done()
|
||||||
});
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{ onChangeAppState },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show the trust dialog in interactive sessions, regardless of permission mode.
|
// Always show the trust dialog in interactive sessions, regardless of permission mode.
|
||||||
@@ -133,70 +193,83 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|||||||
// If it returns true, the TrustDialog would auto-resolve regardless of
|
// If it returns true, the TrustDialog would auto-resolve regardless of
|
||||||
// security features, so we can skip the dynamic import and render cycle.
|
// security features, so we can skip the dynamic import and render cycle.
|
||||||
if (!checkHasTrustDialogAccepted()) {
|
if (!checkHasTrustDialogAccepted()) {
|
||||||
const {
|
const { TrustDialog } = await import(
|
||||||
TrustDialog
|
'./components/TrustDialog/TrustDialog.js'
|
||||||
} = await import('./components/TrustDialog/TrustDialog.js');
|
)
|
||||||
await showSetupDialog(root, done => <TrustDialog commands={commands} onDone={done} />);
|
await showSetupDialog(root, done => (
|
||||||
|
<TrustDialog commands={commands} onDone={done} />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal that trust has been verified for this session.
|
// Signal that trust has been verified for this session.
|
||||||
// GrowthBook checks this to decide whether to include auth headers.
|
// GrowthBook checks this to decide whether to include auth headers.
|
||||||
setSessionTrustAccepted(true);
|
setSessionTrustAccepted(true)
|
||||||
|
|
||||||
// Reset and reinitialize GrowthBook after trust is established.
|
// Reset and reinitialize GrowthBook after trust is established.
|
||||||
// Defense for login/logout: clears any prior client so the next init
|
// Defense for login/logout: clears any prior client so the next init
|
||||||
// picks up fresh auth headers.
|
// picks up fresh auth headers.
|
||||||
resetGrowthBook();
|
resetGrowthBook()
|
||||||
void initializeGrowthBook();
|
void initializeGrowthBook()
|
||||||
|
|
||||||
// Now that trust is established, prefetch system context if it wasn't already
|
// Now that trust is established, prefetch system context if it wasn't already
|
||||||
void getSystemContext();
|
void getSystemContext()
|
||||||
|
|
||||||
// If settings are valid, check for any mcp.json servers that need approval
|
// If settings are valid, check for any mcp.json servers that need approval
|
||||||
const {
|
const { errors: allErrors } = getSettingsWithAllErrors()
|
||||||
errors: allErrors
|
|
||||||
} = getSettingsWithAllErrors();
|
|
||||||
if (allErrors.length === 0) {
|
if (allErrors.length === 0) {
|
||||||
await handleMcpjsonServerApprovals(root);
|
await handleMcpjsonServerApprovals(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for claude.md includes that need approval
|
// Check for claude.md includes that need approval
|
||||||
if (await shouldShowClaudeMdExternalIncludesWarning()) {
|
if (await shouldShowClaudeMdExternalIncludesWarning()) {
|
||||||
const externalIncludes = getExternalClaudeMdIncludes(await getMemoryFiles(true));
|
const externalIncludes = getExternalClaudeMdIncludes(
|
||||||
const {
|
await getMemoryFiles(true),
|
||||||
ClaudeMdExternalIncludesDialog
|
)
|
||||||
} = await import('./components/ClaudeMdExternalIncludesDialog.js');
|
const { ClaudeMdExternalIncludesDialog } = await import(
|
||||||
await showSetupDialog(root, done => <ClaudeMdExternalIncludesDialog onDone={done} isStandaloneDialog externalIncludes={externalIncludes} />);
|
'./components/ClaudeMdExternalIncludesDialog.js'
|
||||||
|
)
|
||||||
|
await showSetupDialog(root, done => (
|
||||||
|
<ClaudeMdExternalIncludesDialog
|
||||||
|
onDone={done}
|
||||||
|
isStandaloneDialog
|
||||||
|
externalIncludes={externalIncludes}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track current repo path for teleport directory switching (fire-and-forget)
|
// Track current repo path for teleport directory switching (fire-and-forget)
|
||||||
// This must happen AFTER trust to prevent untrusted directories from poisoning the mapping
|
// This must happen AFTER trust to prevent untrusted directories from poisoning the mapping
|
||||||
void updateGithubRepoPathMapping();
|
void updateGithubRepoPathMapping()
|
||||||
if (feature('LODESTONE')) {
|
if (feature('LODESTONE')) {
|
||||||
updateDeepLinkTerminalPreference();
|
updateDeepLinkTerminalPreference()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply full environment variables after trust dialog is accepted OR in bypass mode
|
// Apply full environment variables after trust dialog is accepted OR in bypass mode
|
||||||
// In bypass mode (CI/CD, automation), we trust the environment so apply all variables
|
// In bypass mode (CI/CD, automation), we trust the environment so apply all variables
|
||||||
// In normal mode, this happens after the trust dialog is accepted
|
// In normal mode, this happens after the trust dialog is accepted
|
||||||
// This includes potentially dangerous environment variables from untrusted sources
|
// This includes potentially dangerous environment variables from untrusted sources
|
||||||
applyConfigEnvironmentVariables();
|
applyConfigEnvironmentVariables()
|
||||||
|
|
||||||
// Initialize telemetry after env vars are applied so OTEL endpoint env vars and
|
// Initialize telemetry after env vars are applied so OTEL endpoint env vars and
|
||||||
// otelHeadersHelper (which requires trust to execute) are available.
|
// otelHeadersHelper (which requires trust to execute) are available.
|
||||||
// Defer to next tick so the OTel dynamic import resolves after first render
|
// Defer to next tick so the OTel dynamic import resolves after first render
|
||||||
// instead of during the pre-render microtask queue.
|
// instead of during the pre-render microtask queue.
|
||||||
setImmediate(() => initializeTelemetryAfterTrust());
|
setImmediate(() => initializeTelemetryAfterTrust())
|
||||||
|
|
||||||
if (await isQualifiedForGrove()) {
|
if (await isQualifiedForGrove()) {
|
||||||
const {
|
const { GroveDialog } = await import('src/components/grove/Grove.js')
|
||||||
GroveDialog
|
const decision = await showSetupDialog<string>(root, done => (
|
||||||
} = await import('src/components/grove/Grove.js');
|
<GroveDialog
|
||||||
const decision = await showSetupDialog<string>(root, done => <GroveDialog showIfAlreadyViewed={false} location={onboardingShown ? 'onboarding' : 'policy_update_modal'} onDone={done} />);
|
showIfAlreadyViewed={false}
|
||||||
|
location={onboardingShown ? 'onboarding' : 'policy_update_modal'}
|
||||||
|
onDone={done}
|
||||||
|
/>
|
||||||
|
))
|
||||||
if (decision === 'escape') {
|
if (decision === 'escape') {
|
||||||
logEvent('tengu_grove_policy_exited', {});
|
logEvent('tengu_grove_policy_exited', {})
|
||||||
gracefulShutdownSync(0);
|
gracefulShutdownSync(0)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,33 +277,54 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|||||||
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
|
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
|
||||||
// processes but ignored by Claude Code itself (see auth.ts).
|
// processes but ignored by Claude Code itself (see auth.ts).
|
||||||
if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) {
|
if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) {
|
||||||
const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY);
|
const customApiKeyTruncated = normalizeApiKeyForConfig(
|
||||||
const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated);
|
process.env.ANTHROPIC_API_KEY,
|
||||||
|
)
|
||||||
|
const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated)
|
||||||
if (keyStatus === 'new') {
|
if (keyStatus === 'new') {
|
||||||
const {
|
const { ApproveApiKey } = await import('./components/ApproveApiKey.js')
|
||||||
ApproveApiKey
|
await showSetupDialog<boolean>(
|
||||||
} = await import('./components/ApproveApiKey.js');
|
root,
|
||||||
await showSetupDialog<boolean>(root, done => <ApproveApiKey customApiKeyTruncated={customApiKeyTruncated} onDone={done} />, {
|
done => (
|
||||||
onChangeAppState
|
<ApproveApiKey
|
||||||
});
|
customApiKeyTruncated={customApiKeyTruncated}
|
||||||
|
onDone={done}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{ onChangeAppState },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((permissionMode === 'bypassPermissions' || allowDangerouslySkipPermissions) && !hasSkipDangerousModePermissionPrompt()) {
|
|
||||||
const {
|
if (
|
||||||
BypassPermissionsModeDialog
|
(permissionMode === 'bypassPermissions' ||
|
||||||
} = await import('./components/BypassPermissionsModeDialog.js');
|
allowDangerouslySkipPermissions) &&
|
||||||
await showSetupDialog(root, done => <BypassPermissionsModeDialog onAccept={done} />);
|
!hasSkipDangerousModePermissionPrompt()
|
||||||
|
) {
|
||||||
|
const { BypassPermissionsModeDialog } = await import(
|
||||||
|
'./components/BypassPermissionsModeDialog.js'
|
||||||
|
)
|
||||||
|
await showSetupDialog(root, done => (
|
||||||
|
<BypassPermissionsModeDialog onAccept={done} />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||||
// Only show the opt-in dialog if auto mode actually resolved — if the
|
// Only show the opt-in dialog if auto mode actually resolved — if the
|
||||||
// gate denied it (org not allowlisted, settings disabled), showing
|
// gate denied it (org not allowlisted, settings disabled), showing
|
||||||
// consent for an unavailable feature is pointless. The
|
// consent for an unavailable feature is pointless. The
|
||||||
// verifyAutoModeGateAccess notification will explain why instead.
|
// verifyAutoModeGateAccess notification will explain why instead.
|
||||||
if (permissionMode === 'auto' && !hasAutoModeOptIn()) {
|
if (permissionMode === 'auto' && !hasAutoModeOptIn()) {
|
||||||
const {
|
const { AutoModeOptInDialog } = await import(
|
||||||
AutoModeOptInDialog
|
'./components/AutoModeOptInDialog.js'
|
||||||
} = await import('./components/AutoModeOptInDialog.js');
|
)
|
||||||
await showSetupDialog(root, done => <AutoModeOptInDialog onAccept={done} onDecline={() => gracefulShutdownSync(1)} declineExits />);
|
await showSetupDialog(root, done => (
|
||||||
|
<AutoModeOptInDialog
|
||||||
|
onAccept={done}
|
||||||
|
onDecline={() => gracefulShutdownSync(1)}
|
||||||
|
declineExits
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,14 +342,15 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|||||||
// initializeGrowthBook promise fired earlier). Also warms the
|
// initializeGrowthBook promise fired earlier). Also warms the
|
||||||
// isChannelsEnabled() check in the dev-channels dialog below.
|
// isChannelsEnabled() check in the dev-channels dialog below.
|
||||||
if (getAllowedChannels().length > 0 || (devChannels?.length ?? 0) > 0) {
|
if (getAllowedChannels().length > 0 || (devChannels?.length ?? 0) > 0) {
|
||||||
await checkGate_CACHED_OR_BLOCKING('tengu_harbor');
|
await checkGate_CACHED_OR_BLOCKING('tengu_harbor')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (devChannels && devChannels.length > 0) {
|
if (devChannels && devChannels.length > 0) {
|
||||||
const [{
|
const [{ isChannelsEnabled }, { getClaudeAIOAuthTokens }] =
|
||||||
isChannelsEnabled
|
await Promise.all([
|
||||||
}, {
|
import('./services/mcp/channelAllowlist.js'),
|
||||||
getClaudeAIOAuthTokens
|
import('./utils/auth.js'),
|
||||||
}] = await Promise.all([import('./services/mcp/channelAllowlist.js'), import('./utils/auth.js')]);
|
])
|
||||||
// Skip the dialog when channels are blocked (tengu_harbor off or no
|
// Skip the dialog when channels are blocked (tengu_harbor off or no
|
||||||
// OAuth) — accepting then immediately seeing "not available" in
|
// OAuth) — accepting then immediately seeing "not available" in
|
||||||
// ChannelsNotice is worse than no dialog. Append entries anyway so
|
// ChannelsNotice is worse than no dialog. Append entries anyway so
|
||||||
@@ -264,67 +359,80 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|||||||
// (hasNonDev check); the allowlist bypass it also grants is moot
|
// (hasNonDev check); the allowlist bypass it also grants is moot
|
||||||
// since the gate blocks upstream.
|
// since the gate blocks upstream.
|
||||||
if (!isChannelsEnabled() || !getClaudeAIOAuthTokens()?.accessToken) {
|
if (!isChannelsEnabled() || !getClaudeAIOAuthTokens()?.accessToken) {
|
||||||
setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({
|
setAllowedChannels([
|
||||||
...c,
|
...getAllowedChannels(),
|
||||||
dev: true
|
...devChannels.map(c => ({ ...c, dev: true })),
|
||||||
}))]);
|
])
|
||||||
setHasDevChannels(true);
|
setHasDevChannels(true)
|
||||||
} else {
|
} else {
|
||||||
const {
|
const { DevChannelsDialog } = await import(
|
||||||
DevChannelsDialog
|
'./components/DevChannelsDialog.js'
|
||||||
} = await import('./components/DevChannelsDialog.js');
|
)
|
||||||
await showSetupDialog(root, done => <DevChannelsDialog channels={devChannels} onAccept={() => {
|
await showSetupDialog(root, done => (
|
||||||
|
<DevChannelsDialog
|
||||||
|
channels={devChannels}
|
||||||
|
onAccept={() => {
|
||||||
// Mark dev entries per-entry so the allowlist bypass doesn't leak
|
// Mark dev entries per-entry so the allowlist bypass doesn't leak
|
||||||
// to --channels entries when both flags are passed.
|
// to --channels entries when both flags are passed.
|
||||||
setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({
|
setAllowedChannels([
|
||||||
...c,
|
...getAllowedChannels(),
|
||||||
dev: true
|
...devChannels.map(c => ({ ...c, dev: true })),
|
||||||
}))]);
|
])
|
||||||
setHasDevChannels(true);
|
setHasDevChannels(true)
|
||||||
void done();
|
void done()
|
||||||
}} />);
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show Chrome onboarding for first-time Claude in Chrome users
|
// Show Chrome onboarding for first-time Claude in Chrome users
|
||||||
if (claudeInChrome && !getGlobalConfig().hasCompletedClaudeInChromeOnboarding) {
|
if (
|
||||||
const {
|
claudeInChrome &&
|
||||||
ClaudeInChromeOnboarding
|
!getGlobalConfig().hasCompletedClaudeInChromeOnboarding
|
||||||
} = await import('./components/ClaudeInChromeOnboarding.js');
|
) {
|
||||||
await showSetupDialog(root, done => <ClaudeInChromeOnboarding onDone={done} />);
|
const { ClaudeInChromeOnboarding } = await import(
|
||||||
|
'./components/ClaudeInChromeOnboarding.js'
|
||||||
|
)
|
||||||
|
await showSetupDialog(root, done => (
|
||||||
|
<ClaudeInChromeOnboarding onDone={done} />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return onboardingShown;
|
|
||||||
|
return onboardingShown
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRenderContext(exitOnCtrlC: boolean): {
|
export function getRenderContext(exitOnCtrlC: boolean): {
|
||||||
renderOptions: RenderOptions;
|
renderOptions: RenderOptions
|
||||||
getFpsMetrics: () => FpsMetrics | undefined;
|
getFpsMetrics: () => FpsMetrics | undefined
|
||||||
stats: StatsStore;
|
stats: StatsStore
|
||||||
} {
|
} {
|
||||||
let lastFlickerTime = 0;
|
let lastFlickerTime = 0
|
||||||
const baseOptions = getBaseRenderOptions(exitOnCtrlC);
|
const baseOptions = getBaseRenderOptions(exitOnCtrlC)
|
||||||
|
|
||||||
// Log analytics event when stdin override is active
|
// Log analytics event when stdin override is active
|
||||||
if (baseOptions.stdin) {
|
if (baseOptions.stdin) {
|
||||||
logEvent('tengu_stdin_interactive', {});
|
logEvent('tengu_stdin_interactive', {})
|
||||||
}
|
}
|
||||||
const fpsTracker = new FpsTracker();
|
|
||||||
const stats = createStatsStore();
|
const fpsTracker = new FpsTracker()
|
||||||
setStatsStore(stats);
|
const stats = createStatsStore()
|
||||||
|
setStatsStore(stats)
|
||||||
|
|
||||||
// Bench mode: when set, append per-frame phase timings as JSONL for
|
// Bench mode: when set, append per-frame phase timings as JSONL for
|
||||||
// offline analysis by bench/repl-scroll.ts. Captures the full TUI
|
// offline analysis by bench/repl-scroll.ts. Captures the full TUI
|
||||||
// render pipeline (yoga → screen buffer → diff → optimize → stdout)
|
// render pipeline (yoga → screen buffer → diff → optimize → stdout)
|
||||||
// so perf work on any phase can be validated against real user flows.
|
// so perf work on any phase can be validated against real user flows.
|
||||||
const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG;
|
const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG
|
||||||
return {
|
return {
|
||||||
getFpsMetrics: () => fpsTracker.getMetrics(),
|
getFpsMetrics: () => fpsTracker.getMetrics(),
|
||||||
stats,
|
stats,
|
||||||
renderOptions: {
|
renderOptions: {
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
onFrame: event => {
|
onFrame: event => {
|
||||||
fpsTracker.record(event.durationMs);
|
fpsTracker.record(event.durationMs)
|
||||||
stats.observe('frame_duration_ms', event.durationMs);
|
stats.observe('frame_duration_ms', event.durationMs)
|
||||||
if (frameTimingLogPath && event.phases) {
|
if (frameTimingLogPath && event.phases) {
|
||||||
// Bench-only env-var-gated path: sync write so no frames dropped
|
// Bench-only env-var-gated path: sync write so no frames dropped
|
||||||
// on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are
|
// on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are
|
||||||
@@ -335,31 +443,31 @@ export function getRenderContext(exitOnCtrlC: boolean): {
|
|||||||
total: event.durationMs,
|
total: event.durationMs,
|
||||||
...event.phases,
|
...event.phases,
|
||||||
rss: process.memoryUsage.rss(),
|
rss: process.memoryUsage.rss(),
|
||||||
cpu: process.cpuUsage()
|
cpu: process.cpuUsage(),
|
||||||
}) + '\n';
|
}) + '\n'
|
||||||
// eslint-disable-next-line custom-rules/no-sync-fs -- bench-only, sync so no frames dropped on exit
|
// eslint-disable-next-line custom-rules/no-sync-fs -- bench-only, sync so no frames dropped on exit
|
||||||
appendFileSync(frameTimingLogPath, line);
|
appendFileSync(frameTimingLogPath, line)
|
||||||
}
|
}
|
||||||
// Skip flicker reporting for terminals with synchronized output —
|
// Skip flicker reporting for terminals with synchronized output —
|
||||||
// DEC 2026 buffers between BSU/ESU so clear+redraw is atomic.
|
// DEC 2026 buffers between BSU/ESU so clear+redraw is atomic.
|
||||||
if (isSynchronizedOutputSupported()) {
|
if (isSynchronizedOutputSupported()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
for (const flicker of event.flickers) {
|
for (const flicker of event.flickers) {
|
||||||
if (flicker.reason === 'resize') {
|
if (flicker.reason === 'resize') {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
if (now - lastFlickerTime < 1000) {
|
if (now - lastFlickerTime < 1000) {
|
||||||
logEvent('tengu_flicker', {
|
logEvent('tengu_flicker', {
|
||||||
desiredHeight: flicker.desiredHeight,
|
desiredHeight: flicker.desiredHeight,
|
||||||
actualHeight: flicker.availableHeight,
|
actualHeight: flicker.availableHeight,
|
||||||
reason: flicker.reason
|
reason: flicker.reason,
|
||||||
} as unknown as Record<string, boolean | number | undefined>);
|
} as unknown as Record<string, boolean | number | undefined>)
|
||||||
}
|
}
|
||||||
lastFlickerTime = now;
|
lastFlickerTime = now
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
6541
src/main.tsx
6541
src/main.tsx
File diff suppressed because it is too large
Load Diff
@@ -5,21 +5,22 @@
|
|||||||
// would resolve to scripts/external-stubs/src/types/ (doesn't exist).
|
// would resolve to scripts/external-stubs/src/types/ (doesn't exist).
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type M = any;
|
type M = any
|
||||||
|
|
||||||
export function useMoreRight(_args: {
|
export function useMoreRight(_args: {
|
||||||
enabled: boolean;
|
enabled: boolean
|
||||||
setMessages: (action: M[] | ((prev: M[]) => M[])) => void;
|
setMessages: (action: M[] | ((prev: M[]) => M[])) => void
|
||||||
inputValue: string;
|
inputValue: string
|
||||||
setInputValue: (s: string) => void;
|
setInputValue: (s: string) => void
|
||||||
setToolJSX: (args: M) => void;
|
setToolJSX: (args: M) => void
|
||||||
}): {
|
}): {
|
||||||
onBeforeQuery: (input: string, all: M[], n: number) => Promise<boolean>;
|
onBeforeQuery: (input: string, all: M[], n: number) => Promise<boolean>
|
||||||
onTurnComplete: (all: M[], aborted: boolean) => Promise<void>;
|
onTurnComplete: (all: M[], aborted: boolean) => Promise<void>
|
||||||
render: () => null;
|
render: () => null
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
onBeforeQuery: async () => true,
|
onBeforeQuery: async () => true,
|
||||||
onTurnComplete: async () => {},
|
onTurnComplete: async () => {},
|
||||||
render: () => null
|
render: () => null,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import type { StatsStore } from './context/stats.js';
|
import type { StatsStore } from './context/stats.js'
|
||||||
import type { Root } from './ink.js';
|
import type { Root } from './ink.js'
|
||||||
import type { Props as REPLProps } from './screens/REPL.js';
|
import type { Props as REPLProps } from './screens/REPL.js'
|
||||||
import type { AppState } from './state/AppStateStore.js';
|
import type { AppState } from './state/AppStateStore.js'
|
||||||
import type { FpsMetrics } from './utils/fpsTracker.js';
|
import type { FpsMetrics } from './utils/fpsTracker.js'
|
||||||
|
|
||||||
type AppWrapperProps = {
|
type AppWrapperProps = {
|
||||||
getFpsMetrics: () => FpsMetrics | undefined;
|
getFpsMetrics: () => FpsMetrics | undefined
|
||||||
stats?: StatsStore;
|
stats?: StatsStore
|
||||||
initialState: AppState;
|
initialState: AppState
|
||||||
};
|
}
|
||||||
export async function launchRepl(root: Root, appProps: AppWrapperProps, replProps: REPLProps, renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>): Promise<void> {
|
|
||||||
const {
|
export async function launchRepl(
|
||||||
App
|
root: Root,
|
||||||
} = await import('./components/App.js');
|
appProps: AppWrapperProps,
|
||||||
const {
|
replProps: REPLProps,
|
||||||
REPL
|
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
|
||||||
} = await import('./screens/REPL.js');
|
): Promise<void> {
|
||||||
await renderAndRun(root, <App {...appProps}>
|
const { App } = await import('./components/App.js')
|
||||||
<REPL {...replProps} />
|
const { REPL } = await import('./screens/REPL.js')
|
||||||
</App>);
|
await renderAndRun(
|
||||||
|
root,
|
||||||
|
<App {...appProps}>
|
||||||
|
<REPL {...replProps} />
|
||||||
|
</App>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,72 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useEffect, useRef } from 'react'
|
||||||
import { useEffect, useRef } from 'react';
|
import { KeyboardShortcutHint } from '../components/design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../components/design-system/KeyboardShortcutHint.js';
|
import { Box, Text } from '../ink.js'
|
||||||
import { Box, Text } from '../ink.js';
|
import { useKeybinding } from '../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onRun: () => void;
|
onRun: () => void
|
||||||
onCancel: () => void;
|
onCancel: () => void
|
||||||
reason: string;
|
reason: string
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that shows a notification about running /issue command
|
* Component that shows a notification about running /issue command
|
||||||
* with the ability to cancel via ESC key
|
* with the ability to cancel via ESC key
|
||||||
*/
|
*/
|
||||||
export function AutoRunIssueNotification(t0) {
|
export function AutoRunIssueNotification({
|
||||||
const $ = _c(8);
|
|
||||||
const {
|
|
||||||
onRun,
|
onRun,
|
||||||
onCancel,
|
onCancel,
|
||||||
reason
|
reason,
|
||||||
} = t0;
|
}: Props): React.ReactNode {
|
||||||
const hasRunRef = useRef(false);
|
const hasRunRef = useRef(false)
|
||||||
let t1;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
// Handle ESC key to cancel
|
||||||
t1 = {
|
useKeybinding('confirm:no', onCancel, { context: 'Confirmation' })
|
||||||
context: "Confirmation"
|
|
||||||
};
|
// Run /issue immediately on mount
|
||||||
$[0] = t1;
|
useEffect(() => {
|
||||||
} else {
|
|
||||||
t1 = $[0];
|
|
||||||
}
|
|
||||||
useKeybinding("confirm:no", onCancel, t1);
|
|
||||||
let t2;
|
|
||||||
let t3;
|
|
||||||
if ($[1] !== onRun) {
|
|
||||||
t2 = () => {
|
|
||||||
if (!hasRunRef.current) {
|
if (!hasRunRef.current) {
|
||||||
hasRunRef.current = true;
|
hasRunRef.current = true
|
||||||
onRun();
|
onRun()
|
||||||
}
|
}
|
||||||
};
|
}, [onRun])
|
||||||
t3 = [onRun];
|
|
||||||
$[1] = onRun;
|
return (
|
||||||
$[2] = t2;
|
<Box flexDirection="column" marginTop={1}>
|
||||||
$[3] = t3;
|
<Box>
|
||||||
} else {
|
<Text bold>Running feedback capture...</Text>
|
||||||
t2 = $[2];
|
</Box>
|
||||||
t3 = $[3];
|
<Box>
|
||||||
}
|
<Text dimColor>
|
||||||
useEffect(t2, t3);
|
Press <KeyboardShortcutHint shortcut="Esc" action="cancel" /> anytime
|
||||||
let t4;
|
</Text>
|
||||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
</Box>
|
||||||
t4 = <Box><Text bold={true}>Running feedback capture...</Text></Box>;
|
<Box>
|
||||||
$[4] = t4;
|
<Text dimColor>Reason: {reason}</Text>
|
||||||
} else {
|
</Box>
|
||||||
t4 = $[4];
|
</Box>
|
||||||
}
|
)
|
||||||
let t5;
|
|
||||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t5 = <Box><Text dimColor={true}>Press <KeyboardShortcutHint shortcut="Esc" action="cancel" /> anytime</Text></Box>;
|
|
||||||
$[5] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[5];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[6] !== reason) {
|
|
||||||
t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}<Box><Text dimColor={true}>Reason: {reason}</Text></Box></Box>;
|
|
||||||
$[6] = reason;
|
|
||||||
$[7] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[7];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
}
|
||||||
export type AutoRunIssueReason = 'feedback_survey_bad' | 'feedback_survey_good';
|
|
||||||
|
export type AutoRunIssueReason = 'feedback_survey_bad' | 'feedback_survey_good'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if /issue should auto-run for Ant users
|
* Determines if /issue should auto-run for Ant users
|
||||||
*/
|
*/
|
||||||
export function shouldAutoRunIssue(reason: AutoRunIssueReason): boolean {
|
export function shouldAutoRunIssue(reason: AutoRunIssueReason): boolean {
|
||||||
// Only for Ant users
|
// Only for Ant users
|
||||||
if ((process.env.USER_TYPE) !== 'ant') {
|
if (process.env.USER_TYPE !== 'ant') {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'feedback_survey_bad':
|
case 'feedback_survey_bad':
|
||||||
return false;
|
return false
|
||||||
case 'feedback_survey_good':
|
case 'feedback_survey_good':
|
||||||
return false;
|
return false
|
||||||
default:
|
default:
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +76,10 @@ export function shouldAutoRunIssue(reason: AutoRunIssueReason): boolean {
|
|||||||
*/
|
*/
|
||||||
export function getAutoRunCommand(reason: AutoRunIssueReason): string {
|
export function getAutoRunCommand(reason: AutoRunIssueReason): string {
|
||||||
// Only ant builds have the /good-claude command
|
// Only ant builds have the /good-claude command
|
||||||
if ((process.env.USER_TYPE) === 'ant' && reason === 'feedback_survey_good') {
|
if (process.env.USER_TYPE === 'ant' && reason === 'feedback_survey_good') {
|
||||||
return '/good-claude';
|
return '/good-claude'
|
||||||
}
|
}
|
||||||
return '/issue';
|
return '/issue'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,10 +88,10 @@ export function getAutoRunCommand(reason: AutoRunIssueReason): string {
|
|||||||
export function getAutoRunIssueReasonText(reason: AutoRunIssueReason): string {
|
export function getAutoRunIssueReasonText(reason: AutoRunIssueReason): string {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'feedback_survey_bad':
|
case 'feedback_survey_bad':
|
||||||
return 'You responded "Bad" to the feedback survey';
|
return 'You responded "Bad" to the feedback survey'
|
||||||
case 'feedback_survey_good':
|
case 'feedback_survey_good':
|
||||||
return 'You responded "Good" to the feedback survey';
|
return 'You responded "Good" to the feedback survey'
|
||||||
default:
|
default:
|
||||||
return 'Unknown reason';
|
return 'Unknown reason'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,146 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||||
import { Link, Text } from '../../ink.js';
|
import { Link, Text } from '../../ink.js'
|
||||||
import { renderToolResultMessage as renderDefaultMCPToolResultMessage } from '../../tools/MCPTool/UI.js';
|
import { renderToolResultMessage as renderDefaultMCPToolResultMessage } from '../../tools/MCPTool/UI.js'
|
||||||
import type { MCPToolResult } from '../../utils/mcpValidation.js';
|
import type { MCPToolResult } from '../../utils/mcpValidation.js'
|
||||||
import { truncateToWidth } from '../format.js';
|
import { truncateToWidth } from '../format.js'
|
||||||
import { trackClaudeInChromeTabId } from './common.js';
|
import { trackClaudeInChromeTabId } from './common.js'
|
||||||
export type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
||||||
|
export type { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All tool names from BROWSER_TOOLS in @ant/claude-for-chrome-mcp.
|
* All tool names from BROWSER_TOOLS in @ant/claude-for-chrome-mcp.
|
||||||
* Keep in sync with the package's BROWSER_TOOLS array.
|
* Keep in sync with the package's BROWSER_TOOLS array.
|
||||||
*/
|
*/
|
||||||
export type ChromeToolName = 'javascript_tool' | 'read_page' | 'find' | 'form_input' | 'computer' | 'navigate' | 'resize_window' | 'gif_creator' | 'upload_image' | 'get_page_text' | 'tabs_context_mcp' | 'tabs_create_mcp' | 'update_plan' | 'read_console_messages' | 'read_network_requests' | 'shortcuts_list' | 'shortcuts_execute';
|
export type ChromeToolName =
|
||||||
const CHROME_EXTENSION_FOCUS_TAB_URL_BASE = 'https://clau.de/chrome/tab/';
|
| 'javascript_tool'
|
||||||
function renderChromeToolUseMessage(input: Record<string, unknown>, toolName: ChromeToolName, verbose: boolean): React.ReactNode {
|
| 'read_page'
|
||||||
const tabId = input.tabId;
|
| 'find'
|
||||||
|
| 'form_input'
|
||||||
|
| 'computer'
|
||||||
|
| 'navigate'
|
||||||
|
| 'resize_window'
|
||||||
|
| 'gif_creator'
|
||||||
|
| 'upload_image'
|
||||||
|
| 'get_page_text'
|
||||||
|
| 'tabs_context_mcp'
|
||||||
|
| 'tabs_create_mcp'
|
||||||
|
| 'update_plan'
|
||||||
|
| 'read_console_messages'
|
||||||
|
| 'read_network_requests'
|
||||||
|
| 'shortcuts_list'
|
||||||
|
| 'shortcuts_execute'
|
||||||
|
|
||||||
|
const CHROME_EXTENSION_FOCUS_TAB_URL_BASE = 'https://clau.de/chrome/tab/'
|
||||||
|
|
||||||
|
function renderChromeToolUseMessage(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
toolName: ChromeToolName,
|
||||||
|
verbose: boolean,
|
||||||
|
): React.ReactNode {
|
||||||
|
const tabId = input.tabId
|
||||||
if (typeof tabId === 'number') {
|
if (typeof tabId === 'number') {
|
||||||
trackClaudeInChromeTabId(tabId);
|
trackClaudeInChromeTabId(tabId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build secondary info based on tool type and input
|
// Build secondary info based on tool type and input
|
||||||
const secondaryInfo: string[] = [];
|
const secondaryInfo: string[] = []
|
||||||
|
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'navigate':
|
case 'navigate':
|
||||||
if (typeof input.url === 'string') {
|
if (typeof input.url === 'string') {
|
||||||
try {
|
try {
|
||||||
const url = new URL(input.url);
|
const url = new URL(input.url)
|
||||||
secondaryInfo.push(url.hostname);
|
secondaryInfo.push(url.hostname)
|
||||||
} catch {
|
} catch {
|
||||||
secondaryInfo.push(truncateToWidth(input.url, 30));
|
secondaryInfo.push(truncateToWidth(input.url, 30))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'find':
|
case 'find':
|
||||||
if (typeof input.query === 'string') {
|
if (typeof input.query === 'string') {
|
||||||
secondaryInfo.push(`pattern: ${truncateToWidth(input.query, 30)}`);
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.query, 30)}`)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'computer':
|
case 'computer':
|
||||||
if (typeof input.action === 'string') {
|
if (typeof input.action === 'string') {
|
||||||
const action = input.action;
|
const action = input.action
|
||||||
if (action === 'left_click' || action === 'right_click' || action === 'double_click' || action === 'middle_click') {
|
if (
|
||||||
|
action === 'left_click' ||
|
||||||
|
action === 'right_click' ||
|
||||||
|
action === 'double_click' ||
|
||||||
|
action === 'middle_click'
|
||||||
|
) {
|
||||||
if (typeof input.ref === 'string') {
|
if (typeof input.ref === 'string') {
|
||||||
secondaryInfo.push(`${action} on ${input.ref}`);
|
secondaryInfo.push(`${action} on ${input.ref}`)
|
||||||
} else if (Array.isArray(input.coordinate)) {
|
} else if (Array.isArray(input.coordinate)) {
|
||||||
secondaryInfo.push(`${action} at (${input.coordinate.join(', ')})`);
|
secondaryInfo.push(`${action} at (${input.coordinate.join(', ')})`)
|
||||||
} else {
|
} else {
|
||||||
secondaryInfo.push(action);
|
secondaryInfo.push(action)
|
||||||
}
|
}
|
||||||
} else if (action === 'type' && typeof input.text === 'string') {
|
} else if (action === 'type' && typeof input.text === 'string') {
|
||||||
secondaryInfo.push(`type "${truncateToWidth(input.text, 15)}"`);
|
secondaryInfo.push(`type "${truncateToWidth(input.text, 15)}"`)
|
||||||
} else if (action === 'key' && typeof input.text === 'string') {
|
} else if (action === 'key' && typeof input.text === 'string') {
|
||||||
secondaryInfo.push(`key ${input.text}`);
|
secondaryInfo.push(`key ${input.text}`)
|
||||||
} else if (action === 'scroll' && typeof input.scroll_direction === 'string') {
|
} else if (
|
||||||
secondaryInfo.push(`scroll ${input.scroll_direction}`);
|
action === 'scroll' &&
|
||||||
|
typeof input.scroll_direction === 'string'
|
||||||
|
) {
|
||||||
|
secondaryInfo.push(`scroll ${input.scroll_direction}`)
|
||||||
} else if (action === 'wait' && typeof input.duration === 'number') {
|
} else if (action === 'wait' && typeof input.duration === 'number') {
|
||||||
secondaryInfo.push(`wait ${input.duration}s`);
|
secondaryInfo.push(`wait ${input.duration}s`)
|
||||||
} else if (action === 'left_click_drag') {
|
} else if (action === 'left_click_drag') {
|
||||||
secondaryInfo.push('drag');
|
secondaryInfo.push('drag')
|
||||||
} else {
|
} else {
|
||||||
secondaryInfo.push(action);
|
secondaryInfo.push(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'gif_creator':
|
case 'gif_creator':
|
||||||
if (typeof input.action === 'string') {
|
if (typeof input.action === 'string') {
|
||||||
secondaryInfo.push(`${input.action}`);
|
secondaryInfo.push(`${input.action}`)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'resize_window':
|
case 'resize_window':
|
||||||
if (typeof input.width === 'number' && typeof input.height === 'number') {
|
if (typeof input.width === 'number' && typeof input.height === 'number') {
|
||||||
secondaryInfo.push(`${input.width}x${input.height}`);
|
secondaryInfo.push(`${input.width}x${input.height}`)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'read_console_messages':
|
case 'read_console_messages':
|
||||||
if (typeof input.pattern === 'string') {
|
if (typeof input.pattern === 'string') {
|
||||||
secondaryInfo.push(`pattern: ${truncateToWidth(input.pattern, 20)}`);
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.pattern, 20)}`)
|
||||||
}
|
}
|
||||||
if (input.onlyErrors === true) {
|
if (input.onlyErrors === true) {
|
||||||
secondaryInfo.push('errors only');
|
secondaryInfo.push('errors only')
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'read_network_requests':
|
case 'read_network_requests':
|
||||||
if (typeof input.urlPattern === 'string') {
|
if (typeof input.urlPattern === 'string') {
|
||||||
secondaryInfo.push(`pattern: ${truncateToWidth(input.urlPattern, 20)}`);
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.urlPattern, 20)}`)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'shortcuts_execute':
|
case 'shortcuts_execute':
|
||||||
if (typeof input.shortcutId === 'string') {
|
if (typeof input.shortcutId === 'string') {
|
||||||
secondaryInfo.push(`shortcut_id: ${input.shortcutId}`);
|
secondaryInfo.push(`shortcut_id: ${input.shortcutId}`)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
case 'javascript_tool':
|
case 'javascript_tool':
|
||||||
// In verbose mode, show the full code
|
// In verbose mode, show the full code
|
||||||
if (verbose && typeof input.text === 'string') {
|
if (verbose && typeof input.text === 'string') {
|
||||||
return input.text;
|
return input.text
|
||||||
}
|
}
|
||||||
// In non-verbose mode, return empty string to preserve View Tab layout
|
// In non-verbose mode, return empty string to preserve View Tab layout
|
||||||
return '';
|
return ''
|
||||||
|
|
||||||
case 'tabs_create_mcp':
|
case 'tabs_create_mcp':
|
||||||
case 'tabs_context_mcp':
|
case 'tabs_context_mcp':
|
||||||
case 'form_input':
|
case 'form_input':
|
||||||
@@ -109,9 +151,10 @@ function renderChromeToolUseMessage(input: Record<string, unknown>, toolName: Ch
|
|||||||
case 'update_plan':
|
case 'update_plan':
|
||||||
// These tools don't have meaningful secondary info to show inline.
|
// These tools don't have meaningful secondary info to show inline.
|
||||||
// Return empty string (not null) to ensure tool header still renders.
|
// Return empty string (not null) to ensure tool header still renders.
|
||||||
return '';
|
return ''
|
||||||
}
|
}
|
||||||
return secondaryInfo.join(', ') || null;
|
|
||||||
|
return secondaryInfo.join(', ') || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,22 +166,29 @@ function renderChromeToolUseMessage(input: Record<string, unknown>, toolName: Ch
|
|||||||
*/
|
*/
|
||||||
function renderChromeViewTabLink(input: unknown): React.ReactNode {
|
function renderChromeViewTabLink(input: unknown): React.ReactNode {
|
||||||
if (!supportsHyperlinks()) {
|
if (!supportsHyperlinks()) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
if (typeof input !== 'object' || input === null || !('tabId' in input)) {
|
if (typeof input !== 'object' || input === null || !('tabId' in input)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const tabId = typeof input.tabId === 'number' ? input.tabId : typeof input.tabId === 'string' ? parseInt(input.tabId, 10) : NaN;
|
const tabId =
|
||||||
|
typeof input.tabId === 'number'
|
||||||
|
? input.tabId
|
||||||
|
: typeof input.tabId === 'string'
|
||||||
|
? parseInt(input.tabId, 10)
|
||||||
|
: NaN
|
||||||
if (isNaN(tabId)) {
|
if (isNaN(tabId)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const linkUrl = `${CHROME_EXTENSION_FOCUS_TAB_URL_BASE}${tabId}`;
|
const linkUrl = `${CHROME_EXTENSION_FOCUS_TAB_URL_BASE}${tabId}`
|
||||||
return <Text>
|
return (
|
||||||
|
<Text>
|
||||||
{' '}
|
{' '}
|
||||||
<Link url={linkUrl}>
|
<Link url={linkUrl}>
|
||||||
<Text color="subtle">[View Tab]</Text>
|
<Text color="subtle">[View Tab]</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</Text>;
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,72 +196,79 @@ function renderChromeViewTabLink(input: unknown): React.ReactNode {
|
|||||||
* Shows a brief summary for successful results. Errors are handled by
|
* Shows a brief summary for successful results. Errors are handled by
|
||||||
* the default renderToolUseErrorMessage when is_error is set.
|
* the default renderToolUseErrorMessage when is_error is set.
|
||||||
*/
|
*/
|
||||||
export function renderChromeToolResultMessage(output: MCPToolResult, toolName: ChromeToolName, verbose: boolean): React.ReactNode {
|
export function renderChromeToolResultMessage(
|
||||||
|
output: MCPToolResult,
|
||||||
|
toolName: ChromeToolName,
|
||||||
|
verbose: boolean,
|
||||||
|
): React.ReactNode {
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
return renderDefaultMCPToolResultMessage(output, [], {
|
return renderDefaultMCPToolResultMessage(output, [], { verbose })
|
||||||
verbose
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let summary: string | null = null;
|
|
||||||
|
let summary: string | null = null
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'navigate':
|
case 'navigate':
|
||||||
summary = 'Navigation completed';
|
summary = 'Navigation completed'
|
||||||
break;
|
break
|
||||||
case 'tabs_create_mcp':
|
case 'tabs_create_mcp':
|
||||||
summary = 'Tab created';
|
summary = 'Tab created'
|
||||||
break;
|
break
|
||||||
case 'tabs_context_mcp':
|
case 'tabs_context_mcp':
|
||||||
summary = 'Tabs read';
|
summary = 'Tabs read'
|
||||||
break;
|
break
|
||||||
case 'form_input':
|
case 'form_input':
|
||||||
summary = 'Input completed';
|
summary = 'Input completed'
|
||||||
break;
|
break
|
||||||
case 'computer':
|
case 'computer':
|
||||||
summary = 'Action completed';
|
summary = 'Action completed'
|
||||||
break;
|
break
|
||||||
case 'resize_window':
|
case 'resize_window':
|
||||||
summary = 'Window resized';
|
summary = 'Window resized'
|
||||||
break;
|
break
|
||||||
case 'find':
|
case 'find':
|
||||||
summary = 'Search completed';
|
summary = 'Search completed'
|
||||||
break;
|
break
|
||||||
case 'gif_creator':
|
case 'gif_creator':
|
||||||
summary = 'GIF action completed';
|
summary = 'GIF action completed'
|
||||||
break;
|
break
|
||||||
case 'read_console_messages':
|
case 'read_console_messages':
|
||||||
summary = 'Console messages retrieved';
|
summary = 'Console messages retrieved'
|
||||||
break;
|
break
|
||||||
case 'read_network_requests':
|
case 'read_network_requests':
|
||||||
summary = 'Network requests retrieved';
|
summary = 'Network requests retrieved'
|
||||||
break;
|
break
|
||||||
case 'shortcuts_list':
|
case 'shortcuts_list':
|
||||||
summary = 'Shortcuts retrieved';
|
summary = 'Shortcuts retrieved'
|
||||||
break;
|
break
|
||||||
case 'shortcuts_execute':
|
case 'shortcuts_execute':
|
||||||
summary = 'Shortcut executed';
|
summary = 'Shortcut executed'
|
||||||
break;
|
break
|
||||||
case 'javascript_tool':
|
case 'javascript_tool':
|
||||||
summary = 'Script executed';
|
summary = 'Script executed'
|
||||||
break;
|
break
|
||||||
case 'read_page':
|
case 'read_page':
|
||||||
summary = 'Page read';
|
summary = 'Page read'
|
||||||
break;
|
break
|
||||||
case 'upload_image':
|
case 'upload_image':
|
||||||
summary = 'Image uploaded';
|
summary = 'Image uploaded'
|
||||||
break;
|
break
|
||||||
case 'get_page_text':
|
case 'get_page_text':
|
||||||
summary = 'Page text retrieved';
|
summary = 'Page text retrieved'
|
||||||
break;
|
break
|
||||||
case 'update_plan':
|
case 'update_plan':
|
||||||
summary = 'Plan updated';
|
summary = 'Plan updated'
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summary) {
|
if (summary) {
|
||||||
return <MessageResponse height={1}>
|
return (
|
||||||
|
<MessageResponse height={1}>
|
||||||
<Text dimColor>{summary}</Text>
|
<Text dimColor>{summary}</Text>
|
||||||
</MessageResponse>;
|
</MessageResponse>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,43 +276,56 @@ export function renderChromeToolResultMessage(output: MCPToolResult, toolName: C
|
|||||||
* rendering for chrome tools in a single spread operation.
|
* rendering for chrome tools in a single spread operation.
|
||||||
*/
|
*/
|
||||||
export function getClaudeInChromeMCPToolOverrides(toolName: string): {
|
export function getClaudeInChromeMCPToolOverrides(toolName: string): {
|
||||||
userFacingName: (input?: Record<string, unknown>) => string;
|
userFacingName: (input?: Record<string, unknown>) => string
|
||||||
renderToolUseMessage: (input: Record<string, unknown>, options: {
|
renderToolUseMessage: (
|
||||||
verbose: boolean;
|
input: Record<string, unknown>,
|
||||||
}) => React.ReactNode;
|
options: { verbose: boolean },
|
||||||
renderToolUseTag: (input: Partial<Record<string, unknown>>) => React.ReactNode;
|
) => React.ReactNode
|
||||||
renderToolResultMessage: (output: string | MCPToolResult, progressMessagesForMessage: unknown[], options: {
|
renderToolUseTag: (input: Partial<Record<string, unknown>>) => React.ReactNode
|
||||||
verbose: boolean;
|
renderToolResultMessage: (
|
||||||
}) => React.ReactNode;
|
output: string | MCPToolResult,
|
||||||
|
progressMessagesForMessage: unknown[],
|
||||||
|
options: { verbose: boolean },
|
||||||
|
) => React.ReactNode
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
userFacingName(_input?: Record<string, unknown>) {
|
userFacingName(_input?: Record<string, unknown>) {
|
||||||
// Trim the _mcp postfix that show up in some of the tool names
|
// Trim the _mcp postfix that show up in some of the tool names
|
||||||
const displayName = toolName.replace(/_mcp$/, '');
|
const displayName = toolName.replace(/_mcp$/, '')
|
||||||
return `Claude in Chrome[${displayName}]`;
|
return `Claude in Chrome[${displayName}]`
|
||||||
},
|
},
|
||||||
renderToolUseMessage(input: Record<string, unknown>, {
|
renderToolUseMessage(
|
||||||
verbose
|
input: Record<string, unknown>,
|
||||||
}: {
|
{ verbose }: { verbose: boolean },
|
||||||
verbose: boolean;
|
): React.ReactNode {
|
||||||
}): React.ReactNode {
|
return renderChromeToolUseMessage(
|
||||||
return renderChromeToolUseMessage(input, toolName as ChromeToolName, verbose);
|
input,
|
||||||
|
toolName as ChromeToolName,
|
||||||
|
verbose,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
renderToolUseTag(input: Partial<Record<string, unknown>>): React.ReactNode {
|
renderToolUseTag(input: Partial<Record<string, unknown>>): React.ReactNode {
|
||||||
return renderChromeViewTabLink(input);
|
return renderChromeViewTabLink(input)
|
||||||
},
|
},
|
||||||
renderToolResultMessage(output: string | MCPToolResult, _progressMessagesForMessage: unknown[], {
|
renderToolResultMessage(
|
||||||
verbose
|
output: string | MCPToolResult,
|
||||||
}: {
|
_progressMessagesForMessage: unknown[],
|
||||||
verbose: boolean;
|
{ verbose }: { verbose: boolean },
|
||||||
}): React.ReactNode {
|
): React.ReactNode {
|
||||||
if (!isMCPToolResult(output)) {
|
if (!isMCPToolResult(output)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
return renderChromeToolResultMessage(output, toolName as ChromeToolName, verbose);
|
return renderChromeToolResultMessage(
|
||||||
|
output,
|
||||||
|
toolName as ChromeToolName,
|
||||||
|
verbose,
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
function isMCPToolResult(output: string | MCPToolResult): output is MCPToolResult {
|
|
||||||
return typeof output === 'object' && output !== null;
|
function isMCPToolResult(
|
||||||
|
output: string | MCPToolResult,
|
||||||
|
): output is MCPToolResult {
|
||||||
|
return typeof output === 'object' && output !== null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||||
import { Text } from '../../ink.js';
|
import { Text } from '../../ink.js'
|
||||||
import { truncateToWidth } from '../format.js';
|
import { truncateToWidth } from '../format.js'
|
||||||
import type { MCPToolResult } from '../mcpValidation.js';
|
import type { MCPToolResult } from '../mcpValidation.js'
|
||||||
|
|
||||||
type CuToolInput = Record<string, unknown> & {
|
type CuToolInput = Record<string, unknown> & {
|
||||||
coordinate?: [number, number];
|
coordinate?: [number, number]
|
||||||
start_coordinate?: [number, number];
|
start_coordinate?: [number, number]
|
||||||
text?: string;
|
text?: string
|
||||||
apps?: Array<{
|
apps?: Array<{ displayName?: string }>
|
||||||
displayName?: string;
|
region?: [number, number, number, number]
|
||||||
}>;
|
direction?: string
|
||||||
region?: [number, number, number, number];
|
amount?: number
|
||||||
direction?: string;
|
duration?: number
|
||||||
amount?: number;
|
|
||||||
duration?: number;
|
|
||||||
};
|
|
||||||
function fmtCoord(c: [number, number] | undefined): string {
|
|
||||||
return c ? `(${c[0]}, ${c[1]})` : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtCoord(c: [number, number] | undefined): string {
|
||||||
|
return c ? `(${c[0]}, ${c[1]})` : ''
|
||||||
|
}
|
||||||
|
|
||||||
const RESULT_SUMMARY: Readonly<Partial<Record<string, string>>> = {
|
const RESULT_SUMMARY: Readonly<Partial<Record<string, string>>> = {
|
||||||
screenshot: 'Captured',
|
screenshot: 'Captured',
|
||||||
zoom: 'Captured',
|
zoom: 'Captured',
|
||||||
@@ -32,8 +33,8 @@ const RESULT_SUMMARY: Readonly<Partial<Record<string, string>>> = {
|
|||||||
hold_key: 'Pressed',
|
hold_key: 'Pressed',
|
||||||
scroll: 'Scrolled',
|
scroll: 'Scrolled',
|
||||||
left_click_drag: 'Dragged',
|
left_click_drag: 'Dragged',
|
||||||
open_application: 'Opened'
|
open_application: 'Opened',
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendering overrides for `mcp__computer-use__*` tools. Spread into the MCP
|
* Rendering overrides for `mcp__computer-use__*` tools. Spread into the MCP
|
||||||
@@ -41,18 +42,22 @@ const RESULT_SUMMARY: Readonly<Partial<Record<string, string>>> = {
|
|||||||
* Mirror of `getClaudeInChromeMCPToolOverrides`.
|
* Mirror of `getClaudeInChromeMCPToolOverrides`.
|
||||||
*/
|
*/
|
||||||
export function getComputerUseMCPRenderingOverrides(toolName: string): {
|
export function getComputerUseMCPRenderingOverrides(toolName: string): {
|
||||||
userFacingName: () => string;
|
userFacingName: () => string
|
||||||
renderToolUseMessage: (input: Record<string, unknown>, options: {
|
renderToolUseMessage: (
|
||||||
verbose: boolean;
|
input: Record<string, unknown>,
|
||||||
}) => React.ReactNode;
|
options: { verbose: boolean },
|
||||||
renderToolResultMessage: (output: MCPToolResult, progressMessages: unknown[], options: {
|
) => React.ReactNode
|
||||||
verbose: boolean;
|
renderToolResultMessage: (
|
||||||
}) => React.ReactNode;
|
output: MCPToolResult,
|
||||||
|
progressMessages: unknown[],
|
||||||
|
options: { verbose: boolean },
|
||||||
|
) => React.ReactNode
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
userFacingName() {
|
userFacingName() {
|
||||||
return `Computer Use[${toolName}]`;
|
return `Computer Use[${toolName}]`
|
||||||
},
|
},
|
||||||
|
|
||||||
// AssistantToolUseMessage.tsx contract: null hides the ENTIRE row, '' shows
|
// AssistantToolUseMessage.tsx contract: null hides the ENTIRE row, '' shows
|
||||||
// the tool name without "(args)". Every path below returns '' when there's
|
// the tool name without "(args)". Every path below returns '' when there's
|
||||||
// nothing to show — never null.
|
// nothing to show — never null.
|
||||||
@@ -64,61 +69,89 @@ export function getComputerUseMCPRenderingOverrides(toolName: string): {
|
|||||||
case 'cursor_position':
|
case 'cursor_position':
|
||||||
case 'list_granted_applications':
|
case 'list_granted_applications':
|
||||||
case 'read_clipboard':
|
case 'read_clipboard':
|
||||||
return '';
|
return ''
|
||||||
|
|
||||||
case 'left_click':
|
case 'left_click':
|
||||||
case 'right_click':
|
case 'right_click':
|
||||||
case 'middle_click':
|
case 'middle_click':
|
||||||
case 'double_click':
|
case 'double_click':
|
||||||
case 'triple_click':
|
case 'triple_click':
|
||||||
case 'mouse_move':
|
case 'mouse_move':
|
||||||
return fmtCoord(input.coordinate);
|
return fmtCoord(input.coordinate)
|
||||||
|
|
||||||
case 'left_click_drag':
|
case 'left_click_drag':
|
||||||
return input.start_coordinate ? `${fmtCoord(input.start_coordinate)} → ${fmtCoord(input.coordinate)}` : `to ${fmtCoord(input.coordinate)}`;
|
return input.start_coordinate
|
||||||
|
? `${fmtCoord(input.start_coordinate)} → ${fmtCoord(input.coordinate)}`
|
||||||
|
: `to ${fmtCoord(input.coordinate)}`
|
||||||
|
|
||||||
case 'type':
|
case 'type':
|
||||||
return typeof input.text === 'string' ? `"${truncateToWidth(input.text, 40)}"` : '';
|
return typeof input.text === 'string'
|
||||||
|
? `"${truncateToWidth(input.text, 40)}"`
|
||||||
|
: ''
|
||||||
|
|
||||||
case 'key':
|
case 'key':
|
||||||
case 'hold_key':
|
case 'hold_key':
|
||||||
return typeof input.text === 'string' ? input.text : '';
|
return typeof input.text === 'string' ? input.text : ''
|
||||||
|
|
||||||
case 'scroll':
|
case 'scroll':
|
||||||
return [input.direction, input.amount && `×${input.amount}`, input.coordinate && `at ${fmtCoord(input.coordinate)}`].filter(Boolean).join(' ');
|
return [
|
||||||
case 'zoom':
|
input.direction,
|
||||||
{
|
input.amount && `×${input.amount}`,
|
||||||
const r = input.region;
|
input.coordinate && `at ${fmtCoord(input.coordinate)}`,
|
||||||
return Array.isArray(r) && r.length === 4 ? `[${r[0]}, ${r[1]}, ${r[2]}, ${r[3]}]` : '';
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
case 'zoom': {
|
||||||
|
const r = input.region
|
||||||
|
return Array.isArray(r) && r.length === 4
|
||||||
|
? `[${r[0]}, ${r[1]}, ${r[2]}, ${r[3]}]`
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'wait':
|
case 'wait':
|
||||||
return typeof input.duration === 'number' ? `${input.duration}s` : '';
|
return typeof input.duration === 'number' ? `${input.duration}s` : ''
|
||||||
|
|
||||||
case 'write_clipboard':
|
case 'write_clipboard':
|
||||||
return typeof input.text === 'string' ? `"${truncateToWidth(input.text, 40)}"` : '';
|
return typeof input.text === 'string'
|
||||||
|
? `"${truncateToWidth(input.text, 40)}"`
|
||||||
|
: ''
|
||||||
|
|
||||||
case 'open_application':
|
case 'open_application':
|
||||||
return typeof input.bundle_id === 'string' ? String(input.bundle_id) : '';
|
return typeof input.bundle_id === 'string'
|
||||||
case 'request_access':
|
? String(input.bundle_id)
|
||||||
{
|
: ''
|
||||||
const apps = input.apps;
|
|
||||||
if (!Array.isArray(apps)) return '';
|
case 'request_access': {
|
||||||
const names = apps.map(a => typeof a?.displayName === 'string' ? a.displayName : '').filter(Boolean);
|
const apps = input.apps
|
||||||
return names.join(', ');
|
if (!Array.isArray(apps)) return ''
|
||||||
|
const names = apps
|
||||||
|
.map(a => (typeof a?.displayName === 'string' ? a.displayName : ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
return names.join(', ')
|
||||||
}
|
}
|
||||||
case 'computer_batch':
|
|
||||||
{
|
case 'computer_batch': {
|
||||||
const actions = input.actions;
|
const actions = input.actions
|
||||||
return Array.isArray(actions) ? `${actions.length} actions` : '';
|
return Array.isArray(actions) ? `${actions.length} actions` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderToolResultMessage(output, _progress, {
|
|
||||||
verbose
|
renderToolResultMessage(output, _progress, { verbose }) {
|
||||||
}) {
|
if (verbose || typeof output !== 'object' || output === null) return null
|
||||||
if (verbose || typeof output !== 'object' || output === null) return null;
|
|
||||||
|
|
||||||
// Non-verbose: one-line dim summary, like Chrome's pattern.
|
// Non-verbose: one-line dim summary, like Chrome's pattern.
|
||||||
const summary = RESULT_SUMMARY[toolName];
|
const summary = RESULT_SUMMARY[toolName]
|
||||||
if (!summary) return null;
|
if (!summary) return null
|
||||||
return <MessageResponse height={1}>
|
return (
|
||||||
|
<MessageResponse height={1}>
|
||||||
<Text dimColor>{summary}</Text>
|
<Text dimColor>{summary}</Text>
|
||||||
</MessageResponse>;
|
</MessageResponse>
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,22 +16,35 @@
|
|||||||
* GrowthBook gate `tengu_malort_pedway` (see gates.ts).
|
* GrowthBook gate `tengu_malort_pedway` (see gates.ts).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { bindSessionContext, type ComputerUseSessionContext, type CuCallToolResult, type CuPermissionRequest, type CuPermissionResponse, DEFAULT_GRANT_FLAGS, type ScreenshotDims } from '@ant/computer-use-mcp';
|
import {
|
||||||
import * as React from 'react';
|
bindSessionContext,
|
||||||
import { getSessionId } from '../../bootstrap/state.js';
|
type ComputerUseSessionContext,
|
||||||
import { ComputerUseApproval } from '../../components/permissions/ComputerUseApproval/ComputerUseApproval.js';
|
type CuCallToolResult,
|
||||||
import type { Tool, ToolUseContext } from '../../Tool.js';
|
type CuPermissionRequest,
|
||||||
import { logForDebugging } from '../debug.js';
|
type CuPermissionResponse,
|
||||||
import { checkComputerUseLock, tryAcquireComputerUseLock } from './computerUseLock.js';
|
DEFAULT_GRANT_FLAGS,
|
||||||
import { registerEscHotkey } from './escHotkey.js';
|
type ScreenshotDims,
|
||||||
import { getChicagoCoordinateMode } from './gates.js';
|
} from '@ant/computer-use-mcp'
|
||||||
import { getComputerUseHostAdapter } from './hostAdapter.js';
|
import * as React from 'react'
|
||||||
import { getComputerUseMCPRenderingOverrides } from './toolRendering.js';
|
import { getSessionId } from '../../bootstrap/state.js'
|
||||||
type CallOverride = Pick<Tool, 'call'>['call'];
|
import { ComputerUseApproval } from '../../components/permissions/ComputerUseApproval/ComputerUseApproval.js'
|
||||||
|
import type { Tool, ToolUseContext } from '../../Tool.js'
|
||||||
|
import { logForDebugging } from '../debug.js'
|
||||||
|
import {
|
||||||
|
checkComputerUseLock,
|
||||||
|
tryAcquireComputerUseLock,
|
||||||
|
} from './computerUseLock.js'
|
||||||
|
import { registerEscHotkey } from './escHotkey.js'
|
||||||
|
import { getChicagoCoordinateMode } from './gates.js'
|
||||||
|
import { getComputerUseHostAdapter } from './hostAdapter.js'
|
||||||
|
import { getComputerUseMCPRenderingOverrides } from './toolRendering.js'
|
||||||
|
|
||||||
|
type CallOverride = Pick<Tool, 'call'>['call']
|
||||||
|
|
||||||
type Binding = {
|
type Binding = {
|
||||||
ctx: ComputerUseSessionContext;
|
ctx: ComputerUseSessionContext
|
||||||
dispatch: (name: string, args: unknown) => Promise<CuCallToolResult>;
|
dispatch: (name: string, args: unknown) => Promise<CuCallToolResult>
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached binding — built on first `.call()`, reused for process lifetime.
|
* Cached binding — built on first `.call()`, reused for process lifetime.
|
||||||
@@ -46,35 +59,47 @@ type Binding = {
|
|||||||
* its internal screenshot blob survives, but `ToolUseContext` is per-call.
|
* its internal screenshot blob survives, but `ToolUseContext` is per-call.
|
||||||
* Tests will need to either inject the cache or run serially.
|
* Tests will need to either inject the cache or run serially.
|
||||||
*/
|
*/
|
||||||
let binding: Binding | undefined;
|
let binding: Binding | undefined
|
||||||
let currentToolUseContext: ToolUseContext | undefined;
|
let currentToolUseContext: ToolUseContext | undefined
|
||||||
|
|
||||||
function tuc(): ToolUseContext {
|
function tuc(): ToolUseContext {
|
||||||
// Safe: `binding` is only populated when `currentToolUseContext` is set.
|
// Safe: `binding` is only populated when `currentToolUseContext` is set.
|
||||||
// Called only from within `ctx` callbacks, which only fire during dispatch.
|
// Called only from within `ctx` callbacks, which only fire during dispatch.
|
||||||
return currentToolUseContext!;
|
return currentToolUseContext!
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLockHeld(holder: string): string {
|
function formatLockHeld(holder: string): string {
|
||||||
return `Computer use is in use by another Claude session (${holder.slice(0, 8)}…). Wait for that session to finish or run /exit there.`;
|
return `Computer use is in use by another Claude session (${holder.slice(0, 8)}…). Wait for that session to finish or run /exit there.`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSessionContext(): ComputerUseSessionContext {
|
export function buildSessionContext(): ComputerUseSessionContext {
|
||||||
return {
|
return {
|
||||||
// ── Read state fresh via the per-call ref ─────────────────────────────
|
// ── Read state fresh via the per-call ref ─────────────────────────────
|
||||||
getAllowedApps: () => tuc().getAppState().computerUseMcpState?.allowedApps ?? [],
|
getAllowedApps: () =>
|
||||||
getGrantFlags: () => tuc().getAppState().computerUseMcpState?.grantFlags ?? DEFAULT_GRANT_FLAGS,
|
tuc().getAppState().computerUseMcpState?.allowedApps ?? [],
|
||||||
|
getGrantFlags: () =>
|
||||||
|
tuc().getAppState().computerUseMcpState?.grantFlags ??
|
||||||
|
DEFAULT_GRANT_FLAGS,
|
||||||
// cc-2 has no Settings page for user-denied apps yet.
|
// cc-2 has no Settings page for user-denied apps yet.
|
||||||
getUserDeniedBundleIds: () => [],
|
getUserDeniedBundleIds: () => [],
|
||||||
getSelectedDisplayId: () => tuc().getAppState().computerUseMcpState?.selectedDisplayId,
|
getSelectedDisplayId: () =>
|
||||||
getDisplayPinnedByModel: () => tuc().getAppState().computerUseMcpState?.displayPinnedByModel ?? false,
|
tuc().getAppState().computerUseMcpState?.selectedDisplayId,
|
||||||
getDisplayResolvedForApps: () => tuc().getAppState().computerUseMcpState?.displayResolvedForApps,
|
getDisplayPinnedByModel: () =>
|
||||||
|
tuc().getAppState().computerUseMcpState?.displayPinnedByModel ?? false,
|
||||||
|
getDisplayResolvedForApps: () =>
|
||||||
|
tuc().getAppState().computerUseMcpState?.displayResolvedForApps,
|
||||||
getLastScreenshotDims: (): ScreenshotDims | undefined => {
|
getLastScreenshotDims: (): ScreenshotDims | undefined => {
|
||||||
const d = tuc().getAppState().computerUseMcpState?.lastScreenshotDims;
|
const d = tuc().getAppState().computerUseMcpState?.lastScreenshotDims
|
||||||
return d ? {
|
return d
|
||||||
|
? {
|
||||||
...d,
|
...d,
|
||||||
displayId: d.displayId ?? 0,
|
displayId: d.displayId ?? 0,
|
||||||
originX: d.originX ?? 0,
|
originX: d.originX ?? 0,
|
||||||
originY: d.originY ?? 0
|
originY: d.originY ?? 0,
|
||||||
} : undefined;
|
}
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Write-backs ────────────────────────────────────────────────────────
|
// ── Write-backs ────────────────────────────────────────────────────────
|
||||||
// `setToolJSX` is guaranteed present — the gate in `main.tsx` excludes
|
// `setToolJSX` is guaranteed present — the gate in `main.tsx` excludes
|
||||||
// non-interactive sessions. The package's `_dialogSignal` (tool-finished
|
// non-interactive sessions. The package's `_dialogSignal` (tool-finished
|
||||||
@@ -82,45 +107,61 @@ export function buildSessionContext(): ComputerUseSessionContext {
|
|||||||
// the dialog can't outlive it. Ctrl+C is what matters, and
|
// the dialog can't outlive it. Ctrl+C is what matters, and
|
||||||
// `runPermissionDialog` wires that from the per-call ref's abortController.
|
// `runPermissionDialog` wires that from the per-call ref's abortController.
|
||||||
onPermissionRequest: (req, _dialogSignal) => runPermissionDialog(req),
|
onPermissionRequest: (req, _dialogSignal) => runPermissionDialog(req),
|
||||||
|
|
||||||
// Package does the merge (dedupe + truthy-only flags). We just persist.
|
// Package does the merge (dedupe + truthy-only flags). We just persist.
|
||||||
onAllowedAppsChanged: (apps, flags) => tuc().setAppState(prev => {
|
onAllowedAppsChanged: (apps, flags) =>
|
||||||
const cu = prev.computerUseMcpState;
|
tuc().setAppState(prev => {
|
||||||
const prevApps = cu?.allowedApps;
|
const cu = prev.computerUseMcpState
|
||||||
const prevFlags = cu?.grantFlags;
|
const prevApps = cu?.allowedApps
|
||||||
const sameApps = prevApps?.length === apps.length && apps.every((a, i) => prevApps[i]?.bundleId === a.bundleId);
|
const prevFlags = cu?.grantFlags
|
||||||
const sameFlags = prevFlags?.clipboardRead === flags.clipboardRead && prevFlags?.clipboardWrite === flags.clipboardWrite && prevFlags?.systemKeyCombos === flags.systemKeyCombos;
|
const sameApps =
|
||||||
return sameApps && sameFlags ? prev : {
|
prevApps?.length === apps.length &&
|
||||||
|
apps.every((a, i) => prevApps[i]?.bundleId === a.bundleId)
|
||||||
|
const sameFlags =
|
||||||
|
prevFlags?.clipboardRead === flags.clipboardRead &&
|
||||||
|
prevFlags?.clipboardWrite === flags.clipboardWrite &&
|
||||||
|
prevFlags?.systemKeyCombos === flags.systemKeyCombos
|
||||||
|
return sameApps && sameFlags
|
||||||
|
? prev
|
||||||
|
: {
|
||||||
...prev,
|
...prev,
|
||||||
computerUseMcpState: {
|
computerUseMcpState: {
|
||||||
...cu,
|
...cu,
|
||||||
allowedApps: [...apps],
|
allowedApps: [...apps],
|
||||||
grantFlags: flags
|
grantFlags: flags,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
onAppsHidden: ids => {
|
onAppsHidden: ids => {
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return
|
||||||
tuc().setAppState(prev => {
|
tuc().setAppState(prev => {
|
||||||
const cu = prev.computerUseMcpState;
|
const cu = prev.computerUseMcpState
|
||||||
const existing = cu?.hiddenDuringTurn;
|
const existing = cu?.hiddenDuringTurn
|
||||||
if (existing && ids.every(id => existing.has(id))) return prev;
|
if (existing && ids.every(id => existing.has(id))) return prev
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
computerUseMcpState: {
|
computerUseMcpState: {
|
||||||
...cu,
|
...cu,
|
||||||
hiddenDuringTurn: new Set([...(existing ?? []), ...ids])
|
hiddenDuringTurn: new Set([...(existing ?? []), ...ids]),
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// Resolver writeback only fires under a pin when Swift fell back to main
|
// Resolver writeback only fires under a pin when Swift fell back to main
|
||||||
// (pinned display unplugged) — the pin is semantically dead, so clear it
|
// (pinned display unplugged) — the pin is semantically dead, so clear it
|
||||||
// and the app-set key so the chase chain runs next time. When autoResolve
|
// and the app-set key so the chase chain runs next time. When autoResolve
|
||||||
// was true, onDisplayResolvedForApps re-sets the key in the same tick.
|
// was true, onDisplayResolvedForApps re-sets the key in the same tick.
|
||||||
onResolvedDisplayUpdated: id => tuc().setAppState(prev => {
|
onResolvedDisplayUpdated: id =>
|
||||||
const cu = prev.computerUseMcpState;
|
tuc().setAppState(prev => {
|
||||||
if (cu?.selectedDisplayId === id && !cu.displayPinnedByModel && cu.displayResolvedForApps === undefined) {
|
const cu = prev.computerUseMcpState
|
||||||
return prev;
|
if (
|
||||||
|
cu?.selectedDisplayId === id &&
|
||||||
|
!cu.displayPinnedByModel &&
|
||||||
|
cu.displayResolvedForApps === undefined
|
||||||
|
) {
|
||||||
|
return prev
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -128,18 +169,24 @@ export function buildSessionContext(): ComputerUseSessionContext {
|
|||||||
...cu,
|
...cu,
|
||||||
selectedDisplayId: id,
|
selectedDisplayId: id,
|
||||||
displayPinnedByModel: false,
|
displayPinnedByModel: false,
|
||||||
displayResolvedForApps: undefined
|
displayResolvedForApps: undefined,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// switch_display(name) pins; switch_display("auto") unpins and clears the
|
// switch_display(name) pins; switch_display("auto") unpins and clears the
|
||||||
// app-set key so the next screenshot auto-resolves fresh.
|
// app-set key so the next screenshot auto-resolves fresh.
|
||||||
onDisplayPinned: id => tuc().setAppState(prev => {
|
onDisplayPinned: id =>
|
||||||
const cu = prev.computerUseMcpState;
|
tuc().setAppState(prev => {
|
||||||
const pinned = id !== undefined;
|
const cu = prev.computerUseMcpState
|
||||||
const nextResolvedFor = pinned ? cu?.displayResolvedForApps : undefined;
|
const pinned = id !== undefined
|
||||||
if (cu?.selectedDisplayId === id && cu?.displayPinnedByModel === pinned && cu?.displayResolvedForApps === nextResolvedFor) {
|
const nextResolvedFor = pinned ? cu?.displayResolvedForApps : undefined
|
||||||
return prev;
|
if (
|
||||||
|
cu?.selectedDisplayId === id &&
|
||||||
|
cu?.displayPinnedByModel === pinned &&
|
||||||
|
cu?.displayResolvedForApps === nextResolvedFor
|
||||||
|
) {
|
||||||
|
return prev
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -147,57 +194,56 @@ export function buildSessionContext(): ComputerUseSessionContext {
|
|||||||
...cu,
|
...cu,
|
||||||
selectedDisplayId: id,
|
selectedDisplayId: id,
|
||||||
displayPinnedByModel: pinned,
|
displayPinnedByModel: pinned,
|
||||||
displayResolvedForApps: nextResolvedFor
|
displayResolvedForApps: nextResolvedFor,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
onDisplayResolvedForApps: key => tuc().setAppState(prev => {
|
|
||||||
const cu = prev.computerUseMcpState;
|
onDisplayResolvedForApps: key =>
|
||||||
if (cu?.displayResolvedForApps === key) return prev;
|
tuc().setAppState(prev => {
|
||||||
|
const cu = prev.computerUseMcpState
|
||||||
|
if (cu?.displayResolvedForApps === key) return prev
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
computerUseMcpState: {
|
computerUseMcpState: { ...cu, displayResolvedForApps: key },
|
||||||
...cu,
|
|
||||||
displayResolvedForApps: key
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
onScreenshotCaptured: dims => tuc().setAppState(prev => {
|
|
||||||
const cu = prev.computerUseMcpState;
|
onScreenshotCaptured: dims =>
|
||||||
const p = cu?.lastScreenshotDims;
|
tuc().setAppState(prev => {
|
||||||
return p?.width === dims.width && p?.height === dims.height && p?.displayWidth === dims.displayWidth && p?.displayHeight === dims.displayHeight && p?.displayId === dims.displayId && p?.originX === dims.originX && p?.originY === dims.originY ? prev : {
|
const cu = prev.computerUseMcpState
|
||||||
|
const p = cu?.lastScreenshotDims
|
||||||
|
return p?.width === dims.width &&
|
||||||
|
p?.height === dims.height &&
|
||||||
|
p?.displayWidth === dims.displayWidth &&
|
||||||
|
p?.displayHeight === dims.displayHeight &&
|
||||||
|
p?.displayId === dims.displayId &&
|
||||||
|
p?.originX === dims.originX &&
|
||||||
|
p?.originY === dims.originY
|
||||||
|
? prev
|
||||||
|
: {
|
||||||
...prev,
|
...prev,
|
||||||
computerUseMcpState: {
|
computerUseMcpState: { ...cu, lastScreenshotDims: dims },
|
||||||
...cu,
|
|
||||||
lastScreenshotDims: dims
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ── Lock — async, direct file-lock calls ───────────────────────────────
|
// ── Lock — async, direct file-lock calls ───────────────────────────────
|
||||||
// No `lockHolderForGate` dance: the package's gate is async now. It
|
// No `lockHolderForGate` dance: the package's gate is async now. It
|
||||||
// awaits `checkCuLock`, and on `holder: undefined` + non-deferring tool
|
// awaits `checkCuLock`, and on `holder: undefined` + non-deferring tool
|
||||||
// awaits `acquireCuLock`. `defersLockAcquire` is the PACKAGE's set —
|
// awaits `acquireCuLock`. `defersLockAcquire` is the PACKAGE's set —
|
||||||
// the local copy is gone.
|
// the local copy is gone.
|
||||||
checkCuLock: async () => {
|
checkCuLock: async () => {
|
||||||
const c = await checkComputerUseLock();
|
const c = await checkComputerUseLock()
|
||||||
switch (c.kind) {
|
switch (c.kind) {
|
||||||
case 'free':
|
case 'free':
|
||||||
return {
|
return { holder: undefined, isSelf: false }
|
||||||
holder: undefined,
|
|
||||||
isSelf: false
|
|
||||||
};
|
|
||||||
case 'held_by_self':
|
case 'held_by_self':
|
||||||
return {
|
return { holder: getSessionId(), isSelf: true }
|
||||||
holder: getSessionId(),
|
|
||||||
isSelf: true
|
|
||||||
};
|
|
||||||
case 'blocked':
|
case 'blocked':
|
||||||
return {
|
return { holder: c.by, isSelf: false }
|
||||||
holder: c.by,
|
|
||||||
isSelf: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Called only when checkCuLock returned `holder: undefined`. The O_EXCL
|
// Called only when checkCuLock returned `holder: undefined`. The O_EXCL
|
||||||
// acquire is atomic — if another process grabbed it in the gap (rare),
|
// acquire is atomic — if another process grabbed it in the gap (rare),
|
||||||
// throw so the tool fails instead of proceeding without the lock.
|
// throw so the tool fails instead of proceeding without the lock.
|
||||||
@@ -205,9 +251,9 @@ export function buildSessionContext(): ComputerUseSessionContext {
|
|||||||
// but is possible under parallel tool-use interleaving — don't spam the
|
// but is possible under parallel tool-use interleaving — don't spam the
|
||||||
// notification in that case.
|
// notification in that case.
|
||||||
acquireCuLock: async () => {
|
acquireCuLock: async () => {
|
||||||
const r = await tryAcquireComputerUseLock();
|
const r = await tryAcquireComputerUseLock()
|
||||||
if (r.kind === 'blocked') {
|
if (r.kind === 'blocked') {
|
||||||
throw new Error(formatLockHeld(r.by));
|
throw new Error(formatLockHeld(r.by))
|
||||||
}
|
}
|
||||||
if (r.fresh) {
|
if (r.fresh) {
|
||||||
// Global Escape → abort. Consumes the event (PI defense — prompt
|
// Global Escape → abort. Consumes the event (PI defense — prompt
|
||||||
@@ -215,26 +261,34 @@ export function buildSessionContext(): ComputerUseSessionContext {
|
|||||||
// CFRunLoopSource is processed by the drainRunLoop pump, so this
|
// CFRunLoopSource is processed by the drainRunLoop pump, so this
|
||||||
// holds a pump retain until unregisterEscHotkey() in cleanup.ts.
|
// holds a pump retain until unregisterEscHotkey() in cleanup.ts.
|
||||||
const escRegistered = registerEscHotkey(() => {
|
const escRegistered = registerEscHotkey(() => {
|
||||||
logForDebugging('[cu-esc] user escape, aborting turn');
|
logForDebugging('[cu-esc] user escape, aborting turn')
|
||||||
tuc().abortController.abort();
|
tuc().abortController.abort()
|
||||||
});
|
})
|
||||||
tuc().sendOSNotification?.({
|
tuc().sendOSNotification?.({
|
||||||
message: escRegistered ? 'Claude is using your computer · press Esc to stop' : 'Claude is using your computer · press Ctrl+C to stop',
|
message: escRegistered
|
||||||
notificationType: 'computer_use_enter'
|
? 'Claude is using your computer · press Esc to stop'
|
||||||
});
|
: 'Claude is using your computer · press Ctrl+C to stop',
|
||||||
|
notificationType: 'computer_use_enter',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatLockHeldMessage: formatLockHeld
|
|
||||||
};
|
formatLockHeldMessage: formatLockHeld,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrBind(): Binding {
|
function getOrBind(): Binding {
|
||||||
if (binding) return binding;
|
if (binding) return binding
|
||||||
const ctx = buildSessionContext();
|
const ctx = buildSessionContext()
|
||||||
binding = {
|
binding = {
|
||||||
ctx,
|
ctx,
|
||||||
dispatch: bindSessionContext(getComputerUseHostAdapter(), getChicagoCoordinateMode(), ctx)
|
dispatch: bindSessionContext(
|
||||||
};
|
getComputerUseHostAdapter(),
|
||||||
return binding;
|
getChicagoCoordinateMode(),
|
||||||
|
ctx,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return binding
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -242,21 +296,25 @@ function getOrBind(): Binding {
|
|||||||
* tool: rendering overrides from `toolRendering.tsx` plus a `.call()` that
|
* tool: rendering overrides from `toolRendering.tsx` plus a `.call()` that
|
||||||
* dispatches through the cached binder.
|
* dispatches through the cached binder.
|
||||||
*/
|
*/
|
||||||
type ComputerUseMCPToolOverrides = ReturnType<typeof getComputerUseMCPRenderingOverrides> & {
|
type ComputerUseMCPToolOverrides = ReturnType<
|
||||||
call: CallOverride;
|
typeof getComputerUseMCPRenderingOverrides
|
||||||
};
|
> & {
|
||||||
export function getComputerUseMCPToolOverrides(toolName: string): ComputerUseMCPToolOverrides {
|
call: CallOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComputerUseMCPToolOverrides(
|
||||||
|
toolName: string,
|
||||||
|
): ComputerUseMCPToolOverrides {
|
||||||
const call: CallOverride = async (args, context: ToolUseContext) => {
|
const call: CallOverride = async (args, context: ToolUseContext) => {
|
||||||
currentToolUseContext = context;
|
currentToolUseContext = context
|
||||||
const {
|
const { dispatch } = getOrBind()
|
||||||
dispatch
|
|
||||||
} = getOrBind();
|
const { telemetry, ...result } = await dispatch(toolName, args)
|
||||||
const {
|
|
||||||
telemetry,
|
|
||||||
...result
|
|
||||||
} = await dispatch(toolName, args);
|
|
||||||
if (telemetry?.error_kind) {
|
if (telemetry?.error_kind) {
|
||||||
logForDebugging(`[Computer Use MCP] ${toolName} error_kind=${telemetry.error_kind}`);
|
logForDebugging(
|
||||||
|
`[Computer Use MCP] ${toolName} error_kind=${telemetry.error_kind}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCP content blocks → Anthropic API blocks. CU only produces text and
|
// MCP content blocks → Anthropic API blocks. CU only produces text and
|
||||||
@@ -265,25 +323,30 @@ export function getComputerUseMCPToolOverrides(toolName: string): ComputerUseMCP
|
|||||||
// shape just maps to the API's base64-source shape. The package's result
|
// shape just maps to the API's base64-source shape. The package's result
|
||||||
// type admits audio/resource too, but CU's handleToolCall never emits
|
// type admits audio/resource too, but CU's handleToolCall never emits
|
||||||
// those; the fallthrough coerces them to empty text.
|
// those; the fallthrough coerces them to empty text.
|
||||||
const data = Array.isArray(result.content) ? result.content.map(item => item.type === 'image' ? {
|
const data = Array.isArray(result.content)
|
||||||
|
? result.content.map(item =>
|
||||||
|
item.type === 'image'
|
||||||
|
? {
|
||||||
type: 'image' as const,
|
type: 'image' as const,
|
||||||
source: {
|
source: {
|
||||||
type: 'base64' as const,
|
type: 'base64' as const,
|
||||||
media_type: item.mimeType ?? 'image/jpeg',
|
media_type: item.mimeType ?? 'image/jpeg',
|
||||||
data: item.data
|
data: item.data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} : {
|
: {
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
text: item.type === 'text' ? item.text : ''
|
text: item.type === 'text' ? item.text : '',
|
||||||
}) : result.content;
|
},
|
||||||
return {
|
)
|
||||||
data
|
: result.content
|
||||||
};
|
return { data }
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getComputerUseMCPRenderingOverrides(toolName),
|
...getComputerUseMCPRenderingOverrides(toolName),
|
||||||
call
|
call,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,43 +356,43 @@ export function getComputerUseMCPToolOverrides(toolName: string): ComputerUseMCP
|
|||||||
* The merge-into-AppState that used to live here (dedupe + truthy-only flags)
|
* The merge-into-AppState that used to live here (dedupe + truthy-only flags)
|
||||||
* is now in the package's `bindSessionContext` → `onAllowedAppsChanged`.
|
* is now in the package's `bindSessionContext` → `onAllowedAppsChanged`.
|
||||||
*/
|
*/
|
||||||
async function runPermissionDialog(req: CuPermissionRequest): Promise<CuPermissionResponse> {
|
async function runPermissionDialog(
|
||||||
const context = tuc();
|
req: CuPermissionRequest,
|
||||||
const setToolJSX = context.setToolJSX;
|
): Promise<CuPermissionResponse> {
|
||||||
|
const context = tuc()
|
||||||
|
const setToolJSX = context.setToolJSX
|
||||||
if (!setToolJSX) {
|
if (!setToolJSX) {
|
||||||
// Shouldn't happen — main.tsx gate excludes non-interactive. Fail safe.
|
// Shouldn't happen — main.tsx gate excludes non-interactive. Fail safe.
|
||||||
return {
|
return { granted: [], denied: [], flags: DEFAULT_GRANT_FLAGS }
|
||||||
granted: [],
|
|
||||||
denied: [],
|
|
||||||
flags: DEFAULT_GRANT_FLAGS
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await new Promise<CuPermissionResponse>((resolve, reject) => {
|
return await new Promise<CuPermissionResponse>((resolve, reject) => {
|
||||||
const signal = context.abortController.signal;
|
const signal = context.abortController.signal
|
||||||
// If already aborted, addEventListener won't fire — reject now so the
|
// If already aborted, addEventListener won't fire — reject now so the
|
||||||
// promise doesn't hang waiting for a user who Ctrl+C'd.
|
// promise doesn't hang waiting for a user who Ctrl+C'd.
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
reject(new Error('Computer Use permission dialog aborted'));
|
reject(new Error('Computer Use permission dialog aborted'))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const onAbort = (): void => {
|
const onAbort = (): void => {
|
||||||
signal.removeEventListener('abort', onAbort);
|
signal.removeEventListener('abort', onAbort)
|
||||||
reject(new Error('Computer Use permission dialog aborted'));
|
reject(new Error('Computer Use permission dialog aborted'))
|
||||||
};
|
}
|
||||||
signal.addEventListener('abort', onAbort);
|
signal.addEventListener('abort', onAbort)
|
||||||
|
|
||||||
setToolJSX({
|
setToolJSX({
|
||||||
jsx: React.createElement(ComputerUseApproval, {
|
jsx: React.createElement(ComputerUseApproval, {
|
||||||
request: req,
|
request: req,
|
||||||
onDone: (resp: CuPermissionResponse) => {
|
onDone: (resp: CuPermissionResponse) => {
|
||||||
signal.removeEventListener('abort', onAbort);
|
signal.removeEventListener('abort', onAbort)
|
||||||
resolve(resp);
|
resolve(resp)
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
shouldHidePromptInput: true
|
shouldHidePromptInput: true,
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setToolJSX(null);
|
setToolJSX(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react'
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi'
|
||||||
import { Messages } from '../components/Messages.js';
|
import { Messages } from '../components/Messages.js'
|
||||||
import { KeybindingProvider } from '../keybindings/KeybindingContext.js';
|
import { KeybindingProvider } from '../keybindings/KeybindingContext.js'
|
||||||
import { loadKeybindingsSyncWithWarnings } from '../keybindings/loadUserBindings.js';
|
import { loadKeybindingsSyncWithWarnings } from '../keybindings/loadUserBindings.js'
|
||||||
import type { KeybindingContextName } from '../keybindings/types.js';
|
import type { KeybindingContextName } from '../keybindings/types.js'
|
||||||
import { AppStateProvider } from '../state/AppState.js';
|
import { AppStateProvider } from '../state/AppState.js'
|
||||||
import type { Tools } from '../Tool.js';
|
import type { Tools } from '../Tool.js'
|
||||||
import type { Message } from '../types/message.js';
|
import type { Message } from '../types/message.js'
|
||||||
import { renderToAnsiString } from './staticRender.js';
|
import { renderToAnsiString } from './staticRender.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal keybinding provider for static/headless renders.
|
* Minimal keybinding provider for static/headless renders.
|
||||||
@@ -15,19 +15,29 @@ import { renderToAnsiString } from './staticRender.js';
|
|||||||
* and would hang in headless renders with no stdin).
|
* and would hang in headless renders with no stdin).
|
||||||
*/
|
*/
|
||||||
function StaticKeybindingProvider({
|
function StaticKeybindingProvider({
|
||||||
children
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const {
|
const { bindings } = loadKeybindingsSyncWithWarnings()
|
||||||
bindings
|
const pendingChordRef = useRef(null)
|
||||||
} = loadKeybindingsSyncWithWarnings();
|
const handlerRegistryRef = useRef(new Map())
|
||||||
const pendingChordRef = useRef(null);
|
const activeContexts = useRef(new Set<KeybindingContextName>()).current
|
||||||
const handlerRegistryRef = useRef(new Map());
|
|
||||||
const activeContexts = useRef(new Set<KeybindingContextName>()).current;
|
return (
|
||||||
return <KeybindingProvider bindings={bindings} pendingChordRef={pendingChordRef} pendingChord={null} setPendingChord={() => {}} activeContexts={activeContexts} registerActiveContext={() => {}} unregisterActiveContext={() => {}} handlerRegistryRef={handlerRegistryRef}>
|
<KeybindingProvider
|
||||||
|
bindings={bindings}
|
||||||
|
pendingChordRef={pendingChordRef}
|
||||||
|
pendingChord={null}
|
||||||
|
setPendingChord={() => {}}
|
||||||
|
activeContexts={activeContexts}
|
||||||
|
registerActiveContext={() => {}}
|
||||||
|
unregisterActiveContext={() => {}}
|
||||||
|
handlerRegistryRef={handlerRegistryRef}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</KeybindingProvider>;
|
</KeybindingProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upper-bound how many NormalizedMessages a Message can produce.
|
// Upper-bound how many NormalizedMessages a Message can produce.
|
||||||
@@ -35,9 +45,9 @@ function StaticKeybindingProvider({
|
|||||||
// NormalizedMessages — 1:1 with block count. String content = 1 block.
|
// NormalizedMessages — 1:1 with block count. String content = 1 block.
|
||||||
// AttachmentMessage etc. have no .message and normalize to ≤1.
|
// AttachmentMessage etc. have no .message and normalize to ≤1.
|
||||||
function normalizedUpperBound(m: Message): number {
|
function normalizedUpperBound(m: Message): number {
|
||||||
if (!('message' in m)) return 1;
|
if (!('message' in m)) return 1
|
||||||
const c = m.message.content;
|
const c = m.message.content
|
||||||
return Array.isArray(c) ? c.length : 1;
|
return Array.isArray(c) ? c.length : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,35 +62,59 @@ function normalizedUpperBound(m: Message): number {
|
|||||||
* the full normalized array so tool_use↔tool_result resolves regardless of
|
* the full normalized array so tool_use↔tool_result resolves regardless of
|
||||||
* which chunk each landed in.
|
* which chunk each landed in.
|
||||||
*/
|
*/
|
||||||
export async function streamRenderedMessages(messages: Message[], tools: Tools, sink: (ansiChunk: string) => void | Promise<void>, {
|
export async function streamRenderedMessages(
|
||||||
|
messages: Message[],
|
||||||
|
tools: Tools,
|
||||||
|
sink: (ansiChunk: string) => void | Promise<void>,
|
||||||
|
{
|
||||||
columns,
|
columns,
|
||||||
verbose = false,
|
verbose = false,
|
||||||
chunkSize = 40,
|
chunkSize = 40,
|
||||||
onProgress
|
onProgress,
|
||||||
}: {
|
}: {
|
||||||
columns?: number;
|
columns?: number
|
||||||
verbose?: boolean;
|
verbose?: boolean
|
||||||
chunkSize?: number;
|
chunkSize?: number
|
||||||
onProgress?: (rendered: number) => void;
|
onProgress?: (rendered: number) => void
|
||||||
} = {}): Promise<void> {
|
} = {},
|
||||||
const renderChunk = (range: readonly [number, number]) => renderToAnsiString(<AppStateProvider>
|
): Promise<void> {
|
||||||
|
const renderChunk = (range: readonly [number, number]) =>
|
||||||
|
renderToAnsiString(
|
||||||
|
<AppStateProvider>
|
||||||
<StaticKeybindingProvider>
|
<StaticKeybindingProvider>
|
||||||
<Messages messages={messages} tools={tools} commands={[]} verbose={verbose} toolJSX={null} toolUseConfirmQueue={[]} inProgressToolUseIDs={new Set()} isMessageSelectorVisible={false} conversationId="export" screen="prompt" streamingToolUses={[]} showAllInTranscript={true} isLoading={false} renderRange={range} />
|
<Messages
|
||||||
|
messages={messages}
|
||||||
|
tools={tools}
|
||||||
|
commands={[]}
|
||||||
|
verbose={verbose}
|
||||||
|
toolJSX={null}
|
||||||
|
toolUseConfirmQueue={[]}
|
||||||
|
inProgressToolUseIDs={new Set()}
|
||||||
|
isMessageSelectorVisible={false}
|
||||||
|
conversationId="export"
|
||||||
|
screen="prompt"
|
||||||
|
streamingToolUses={[]}
|
||||||
|
showAllInTranscript={true}
|
||||||
|
isLoading={false}
|
||||||
|
renderRange={range}
|
||||||
|
/>
|
||||||
</StaticKeybindingProvider>
|
</StaticKeybindingProvider>
|
||||||
</AppStateProvider>, columns);
|
</AppStateProvider>,
|
||||||
|
columns,
|
||||||
|
)
|
||||||
|
|
||||||
// renderRange indexes into the post-collapse array whose length we can't
|
// renderRange indexes into the post-collapse array whose length we can't
|
||||||
// see from here — normalize splits each Message into one NormalizedMessage
|
// see from here — normalize splits each Message into one NormalizedMessage
|
||||||
// per content block (unbounded per message), collapse merges some back.
|
// per content block (unbounded per message), collapse merges some back.
|
||||||
// Ceiling is the exact normalize output count + chunkSize so the loop
|
// Ceiling is the exact normalize output count + chunkSize so the loop
|
||||||
// always reaches the empty slice where break fires (collapse only shrinks).
|
// always reaches the empty slice where break fires (collapse only shrinks).
|
||||||
let ceiling = chunkSize;
|
let ceiling = chunkSize
|
||||||
for (const m of messages) ceiling += normalizedUpperBound(m);
|
for (const m of messages) ceiling += normalizedUpperBound(m)
|
||||||
for (let offset = 0; offset < ceiling; offset += chunkSize) {
|
for (let offset = 0; offset < ceiling; offset += chunkSize) {
|
||||||
const ansi = await renderChunk([offset, offset + chunkSize]);
|
const ansi = await renderChunk([offset, offset + chunkSize])
|
||||||
if (stripAnsi(ansi).trim() === '') break;
|
if (stripAnsi(ansi).trim() === '') break
|
||||||
await sink(ansi);
|
await sink(ansi)
|
||||||
onProgress?.(offset + chunkSize);
|
onProgress?.(offset + chunkSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +122,17 @@ export async function streamRenderedMessages(messages: Message[], tools: Tools,
|
|||||||
* Renders messages to a plain text string suitable for export.
|
* Renders messages to a plain text string suitable for export.
|
||||||
* Uses the same React rendering logic as the interactive UI.
|
* Uses the same React rendering logic as the interactive UI.
|
||||||
*/
|
*/
|
||||||
export async function renderMessagesToPlainText(messages: Message[], tools: Tools = [], columns?: number): Promise<string> {
|
export async function renderMessagesToPlainText(
|
||||||
const parts: string[] = [];
|
messages: Message[],
|
||||||
await streamRenderedMessages(messages, tools, chunk => void parts.push(stripAnsi(chunk)), {
|
tools: Tools = [],
|
||||||
columns
|
columns?: number,
|
||||||
});
|
): Promise<string> {
|
||||||
return parts.join('');
|
const parts: string[] = []
|
||||||
|
await streamRenderedMessages(
|
||||||
|
messages,
|
||||||
|
tools,
|
||||||
|
chunk => void parts.push(stripAnsi(chunk)),
|
||||||
|
{ columns },
|
||||||
|
)
|
||||||
|
return parts.join('')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { Text } from '../ink.js';
|
import { Text } from '../ink.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse-highlight every occurrence of `query` in `text` (case-insensitive).
|
* Inverse-highlight every occurrence of `query` in `text` (case-insensitive).
|
||||||
@@ -7,21 +7,23 @@ import { Text } from '../ink.js';
|
|||||||
* and preview panes.
|
* and preview panes.
|
||||||
*/
|
*/
|
||||||
export function highlightMatch(text: string, query: string): React.ReactNode {
|
export function highlightMatch(text: string, query: string): React.ReactNode {
|
||||||
if (!query) return text;
|
if (!query) return text
|
||||||
const queryLower = query.toLowerCase();
|
const queryLower = query.toLowerCase()
|
||||||
const textLower = text.toLowerCase();
|
const textLower = text.toLowerCase()
|
||||||
const parts: React.ReactNode[] = [];
|
const parts: React.ReactNode[] = []
|
||||||
let offset = 0;
|
let offset = 0
|
||||||
let idx = textLower.indexOf(queryLower, offset);
|
let idx = textLower.indexOf(queryLower, offset)
|
||||||
if (idx === -1) return text;
|
if (idx === -1) return text
|
||||||
while (idx !== -1) {
|
while (idx !== -1) {
|
||||||
if (idx > offset) parts.push(text.slice(offset, idx));
|
if (idx > offset) parts.push(text.slice(offset, idx))
|
||||||
parts.push(<Text key={idx} inverse>
|
parts.push(
|
||||||
|
<Text key={idx} inverse>
|
||||||
{text.slice(idx, idx + query.length)}
|
{text.slice(idx, idx + query.length)}
|
||||||
</Text>);
|
</Text>,
|
||||||
offset = idx + query.length;
|
)
|
||||||
idx = textLower.indexOf(queryLower, offset);
|
offset = idx + query.length
|
||||||
|
idx = textLower.indexOf(queryLower, offset)
|
||||||
}
|
}
|
||||||
if (offset < text.length) parts.push(text.slice(offset));
|
if (offset < text.length) parts.push(text.slice(offset))
|
||||||
return <>{parts}</>;
|
return <>{parts}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { performBackgroundPluginInstallations } from '../../services/plugins/PluginInstallationManager.js';
|
import { performBackgroundPluginInstallations } from '../../services/plugins/PluginInstallationManager.js'
|
||||||
import type { AppState } from '../../state/AppState.js';
|
import type { AppState } from '../../state/AppState.js'
|
||||||
import { checkHasTrustDialogAccepted } from '../config.js';
|
import { checkHasTrustDialogAccepted } from '../config.js'
|
||||||
import { logForDebugging } from '../debug.js';
|
import { logForDebugging } from '../debug.js'
|
||||||
import { clearMarketplacesCache, registerSeedMarketplaces } from './marketplaceManager.js';
|
import {
|
||||||
import { clearPluginCache } from './pluginLoader.js';
|
clearMarketplacesCache,
|
||||||
type SetAppState = (f: (prevState: AppState) => AppState) => void;
|
registerSeedMarketplaces,
|
||||||
|
} from './marketplaceManager.js'
|
||||||
|
import { clearPluginCache } from './pluginLoader.js'
|
||||||
|
|
||||||
|
type SetAppState = (f: (prevState: AppState) => AppState) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform plugin startup checks and initiate background installations
|
* Perform plugin startup checks and initiate background installations
|
||||||
@@ -21,16 +25,21 @@ type SetAppState = (f: (prevState: AppState) => AppState) => void;
|
|||||||
*
|
*
|
||||||
* @param setAppState Function to update app state with installation progress
|
* @param setAppState Function to update app state with installation progress
|
||||||
*/
|
*/
|
||||||
export async function performStartupChecks(setAppState: SetAppState): Promise<void> {
|
export async function performStartupChecks(
|
||||||
logForDebugging('performStartupChecks called');
|
setAppState: SetAppState,
|
||||||
|
): Promise<void> {
|
||||||
|
logForDebugging('performStartupChecks called')
|
||||||
|
|
||||||
// Check if the current directory has been trusted
|
// Check if the current directory has been trusted
|
||||||
if (!checkHasTrustDialogAccepted()) {
|
if (!checkHasTrustDialogAccepted()) {
|
||||||
logForDebugging('Trust not accepted for current directory - skipping plugin installations');
|
logForDebugging(
|
||||||
return;
|
'Trust not accepted for current directory - skipping plugin installations',
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logForDebugging('Starting background plugin installations');
|
logForDebugging('Starting background plugin installations')
|
||||||
|
|
||||||
// Register seed marketplaces (CLAUDE_CODE_PLUGIN_SEED_DIR) before diffing.
|
// Register seed marketplaces (CLAUDE_CODE_PLUGIN_SEED_DIR) before diffing.
|
||||||
// Idempotent; no-op if seed not configured. Without this, background install
|
// Idempotent; no-op if seed not configured. Without this, background install
|
||||||
@@ -39,31 +48,30 @@ export async function performStartupChecks(setAppState: SetAppState): Promise<vo
|
|||||||
// If registration changed state, clear caches so earlier plugin-load passes
|
// If registration changed state, clear caches so earlier plugin-load passes
|
||||||
// (e.g. getAllMcpConfigs during REPL init) don't keep stale "marketplace
|
// (e.g. getAllMcpConfigs during REPL init) don't keep stale "marketplace
|
||||||
// not found" results.
|
// not found" results.
|
||||||
const seedChanged = await registerSeedMarketplaces();
|
const seedChanged = await registerSeedMarketplaces()
|
||||||
if (seedChanged) {
|
if (seedChanged) {
|
||||||
clearMarketplacesCache();
|
clearMarketplacesCache()
|
||||||
clearPluginCache('performStartupChecks: seed marketplaces changed');
|
clearPluginCache('performStartupChecks: seed marketplaces changed')
|
||||||
// Set needsRefresh so useManagePlugins notifies the user to run
|
// Set needsRefresh so useManagePlugins notifies the user to run
|
||||||
// /reload-plugins. Without this signal, the initial plugin-load
|
// /reload-plugins. Without this signal, the initial plugin-load
|
||||||
// (which raced and cached "marketplace not found") would persist
|
// (which raced and cached "marketplace not found") would persist
|
||||||
// until the user manually reloads.
|
// until the user manually reloads.
|
||||||
setAppState(prev => {
|
setAppState(prev => {
|
||||||
if (prev.plugins.needsRefresh) return prev;
|
if (prev.plugins.needsRefresh) return prev
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
plugins: {
|
plugins: { ...prev.plugins, needsRefresh: true },
|
||||||
...prev.plugins,
|
|
||||||
needsRefresh: true
|
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start background installations without waiting
|
// Start background installations without waiting
|
||||||
// This will update AppState as installations progress
|
// This will update AppState as installations progress
|
||||||
await performBackgroundPluginInstallations(setAppState);
|
await performBackgroundPluginInstallations(setAppState)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Even if something fails here, don't block startup
|
// Even if something fails here, don't block startup
|
||||||
logForDebugging(`Error initiating background plugin installations: ${error}`);
|
logForDebugging(
|
||||||
|
`Error initiating background plugin installations: ${error}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +1,152 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios'
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
import { logEvent } from 'src/services/analytics/index.js';
|
import { logEvent } from 'src/services/analytics/index.js'
|
||||||
import { Spinner } from '../components/Spinner.js';
|
import { Spinner } from '../components/Spinner.js'
|
||||||
import { getOauthConfig } from '../constants/oauth.js';
|
import { getOauthConfig } from '../constants/oauth.js'
|
||||||
import { useTimeout } from '../hooks/useTimeout.js';
|
import { useTimeout } from '../hooks/useTimeout.js'
|
||||||
import { Box, Text } from '../ink.js';
|
import { Box, Text } from '../ink.js'
|
||||||
import { getSSLErrorHint } from '../services/api/errorUtils.js';
|
import { getSSLErrorHint } from '../services/api/errorUtils.js'
|
||||||
import { getUserAgent } from './http.js';
|
import { getUserAgent } from './http.js'
|
||||||
import { logError } from './log.js';
|
import { logError } from './log.js'
|
||||||
|
|
||||||
export interface PreflightCheckResult {
|
export interface PreflightCheckResult {
|
||||||
success: boolean;
|
success: boolean
|
||||||
error?: string;
|
error?: string
|
||||||
sslHint?: string;
|
sslHint?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkEndpoints(): Promise<PreflightCheckResult> {
|
async function checkEndpoints(): Promise<PreflightCheckResult> {
|
||||||
try {
|
try {
|
||||||
const oauthConfig = getOauthConfig();
|
const oauthConfig = getOauthConfig()
|
||||||
const tokenUrl = new URL(oauthConfig.TOKEN_URL);
|
const tokenUrl = new URL(oauthConfig.TOKEN_URL)
|
||||||
const endpoints = [`${oauthConfig.BASE_API_URL}/api/hello`, `${tokenUrl.origin}/v1/oauth/hello`];
|
const endpoints = [
|
||||||
const checkEndpoint = async (url: string): Promise<PreflightCheckResult> => {
|
`${oauthConfig.BASE_API_URL}/api/hello`,
|
||||||
|
`${tokenUrl.origin}/v1/oauth/hello`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const checkEndpoint = async (
|
||||||
|
url: string,
|
||||||
|
): Promise<PreflightCheckResult> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: {
|
headers: { 'User-Agent': getUserAgent() },
|
||||||
'User-Agent': getUserAgent()
|
})
|
||||||
}
|
|
||||||
});
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
const hostname = new URL(url).hostname;
|
const hostname = new URL(url).hostname
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to connect to ${hostname}: Status ${response.status}`
|
error: `Failed to connect to ${hostname}: Status ${response.status}`,
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {
|
}
|
||||||
success: true
|
return { success: true }
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const hostname = new URL(url).hostname;
|
const hostname = new URL(url).hostname
|
||||||
const sslHint = getSSLErrorHint(error);
|
const sslHint = getSSLErrorHint(error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to connect to ${hostname}: ${error instanceof Error ? (error as ErrnoException).code || error.message : String(error)}`,
|
error: `Failed to connect to ${hostname}: ${error instanceof Error ? (error as ErrnoException).code || error.message : String(error)}`,
|
||||||
sslHint: sslHint ?? undefined
|
sslHint: sslHint ?? undefined,
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const results = await Promise.all(endpoints.map(checkEndpoint));
|
}
|
||||||
const failedResult = results.find(result => !result.success);
|
|
||||||
|
const results = await Promise.all(endpoints.map(checkEndpoint))
|
||||||
|
const failedResult = results.find(result => !result.success)
|
||||||
|
|
||||||
if (failedResult) {
|
if (failedResult) {
|
||||||
// Log failure to Statsig
|
// Log failure to Statsig
|
||||||
logEvent('tengu_preflight_check_failed', {
|
logEvent('tengu_preflight_check_failed', {
|
||||||
isConnectivityError: false,
|
isConnectivityError: false,
|
||||||
hasErrorMessage: !!failedResult.error,
|
hasErrorMessage: !!failedResult.error,
|
||||||
isSSLError: !!failedResult.sslHint
|
isSSLError: !!failedResult.sslHint,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return failedResult || {
|
|
||||||
success: true
|
return failedResult || { success: true }
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error as Error);
|
logError(error as Error)
|
||||||
|
|
||||||
// Log to Statsig
|
// Log to Statsig
|
||||||
logEvent('tengu_preflight_check_failed', {
|
logEvent('tengu_preflight_check_failed', {
|
||||||
isConnectivityError: true
|
isConnectivityError: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Connectivity check error: ${error instanceof Error ? (error as ErrnoException).code || error.message : String(error)}`
|
error: `Connectivity check error: ${error instanceof Error ? (error as ErrnoException).code || error.message : String(error)}`,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreflightStepProps {
|
interface PreflightStepProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
export function PreflightStep({ onSuccess }: PreflightStepProps) {
|
|
||||||
const [result, setResult] = useState<PreflightCheckResult | null>(null);
|
export function PreflightStep({
|
||||||
const [isChecking, setIsChecking] = useState(true);
|
onSuccess,
|
||||||
const showSpinner = useTimeout(1000) && isChecking;
|
}: PreflightStepProps): React.ReactNode {
|
||||||
|
const [result, setResult] = useState<PreflightCheckResult | null>(null)
|
||||||
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
|
|
||||||
|
// delay showing the check since it's so fast that we normally
|
||||||
|
// want to just immediately show the next step without a flash
|
||||||
|
const showSpinner = useTimeout(1000) && isChecking
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkEndpoints().then((checkResult) => {
|
async function run() {
|
||||||
setResult(checkResult);
|
const checkResult = await checkEndpoints()
|
||||||
setIsChecking(false);
|
setResult(checkResult)
|
||||||
});
|
setIsChecking(false)
|
||||||
}, []);
|
}
|
||||||
|
void run()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
onSuccess();
|
onSuccess()
|
||||||
} else if (result && !result.success) {
|
} else if (result && !result.success) {
|
||||||
logError(result.error ?? 'Preflight connectivity check failed');
|
const timer = setTimeout(() => process.exit(1), 100)
|
||||||
onSuccess();
|
return () => clearTimeout(timer)
|
||||||
}
|
}
|
||||||
}, [result, onSuccess]);
|
}, [result, onSuccess])
|
||||||
|
|
||||||
let content = null;
|
return (
|
||||||
if (isChecking && showSpinner) {
|
<Box flexDirection="column" gap={1} paddingLeft={1}>
|
||||||
content = (
|
{isChecking && showSpinner ? (
|
||||||
<Box paddingLeft={1}>
|
<Box paddingLeft={1}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<Text>Checking connectivity...</Text>
|
<Text>Checking connectivity...</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
) : (
|
||||||
} else if (!result?.success && !isChecking) {
|
!result?.success &&
|
||||||
content = (
|
!isChecking && (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text color="error">Unable to connect to Anthropic services</Text>
|
<Text color="error">Unable to connect to Anthropic services</Text>
|
||||||
<Text color="error">{result?.error}</Text>
|
<Text color="error">{result?.error}</Text>
|
||||||
{result?.sslHint ? (
|
{result?.sslHint ? (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text>{result.sslHint}</Text>
|
<Text>{result.sslHint}</Text>
|
||||||
<Text color="suggestion">See https://code.claude.com/docs/en/network-config</Text>
|
<Text color="suggestion">
|
||||||
|
See https://code.claude.com/docs/en/network-config
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text>Please check your internet connection and network settings.</Text>
|
|
||||||
<Text>
|
<Text>
|
||||||
Note: Claude Code might not be available in your country. Check supported countries at{" "}
|
Please check your internet connection and network settings.
|
||||||
<Text color="suggestion">https://anthropic.com/supported-countries</Text>
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Note: Claude Code might not be available in your country.
|
||||||
|
Check supported countries at{' '}
|
||||||
|
<Text color="suggestion">
|
||||||
|
https://anthropic.com/supported-countries
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
)}
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection="column" gap={1} paddingLeft={1}>
|
|
||||||
{content}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,97 @@
|
|||||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources';
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources'
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { BashModeProgress } from 'src/components/BashModeProgress.js';
|
import { BashModeProgress } from 'src/components/BashModeProgress.js'
|
||||||
import type { SetToolJSXFn } from 'src/Tool.js';
|
import type { SetToolJSXFn } from 'src/Tool.js'
|
||||||
import { BashTool } from 'src/tools/BashTool/BashTool.js';
|
import { BashTool } from 'src/tools/BashTool/BashTool.js'
|
||||||
import type { AttachmentMessage, SystemMessage, UserMessage } from 'src/types/message.js';
|
import type {
|
||||||
import type { ShellProgress } from 'src/types/tools.js';
|
AttachmentMessage,
|
||||||
import { logEvent } from '../../services/analytics/index.js';
|
SystemMessage,
|
||||||
import { errorMessage, ShellError } from '../errors.js';
|
UserMessage,
|
||||||
import { createSyntheticUserCaveatMessage, createUserInterruptionMessage, createUserMessage, prepareUserContent } from '../messages.js';
|
} from 'src/types/message.js'
|
||||||
import { resolveDefaultShell } from '../shell/resolveDefaultShell.js';
|
import type { ShellProgress } from 'src/types/tools.js'
|
||||||
import { isPowerShellToolEnabled } from '../shell/shellToolUtils.js';
|
import { logEvent } from '../../services/analytics/index.js'
|
||||||
import { processToolResultBlock } from '../toolResultStorage.js';
|
import { errorMessage, ShellError } from '../errors.js'
|
||||||
import { escapeXml } from '../xml.js';
|
import {
|
||||||
import type { ProcessUserInputContext } from './processUserInput.js';
|
createSyntheticUserCaveatMessage,
|
||||||
export async function processBashCommand(inputString: string, precedingInputBlocks: ContentBlockParam[], attachmentMessages: AttachmentMessage[], context: ProcessUserInputContext, setToolJSX: SetToolJSXFn): Promise<{
|
createUserInterruptionMessage,
|
||||||
messages: (UserMessage | AttachmentMessage | SystemMessage)[];
|
createUserMessage,
|
||||||
shouldQuery: boolean;
|
prepareUserContent,
|
||||||
|
} from '../messages.js'
|
||||||
|
import { resolveDefaultShell } from '../shell/resolveDefaultShell.js'
|
||||||
|
import { isPowerShellToolEnabled } from '../shell/shellToolUtils.js'
|
||||||
|
import { processToolResultBlock } from '../toolResultStorage.js'
|
||||||
|
import { escapeXml } from '../xml.js'
|
||||||
|
import type { ProcessUserInputContext } from './processUserInput.js'
|
||||||
|
|
||||||
|
export async function processBashCommand(
|
||||||
|
inputString: string,
|
||||||
|
precedingInputBlocks: ContentBlockParam[],
|
||||||
|
attachmentMessages: AttachmentMessage[],
|
||||||
|
context: ProcessUserInputContext,
|
||||||
|
setToolJSX: SetToolJSXFn,
|
||||||
|
): Promise<{
|
||||||
|
messages: (UserMessage | AttachmentMessage | SystemMessage)[]
|
||||||
|
shouldQuery: boolean
|
||||||
}> {
|
}> {
|
||||||
// Shell routing (docs/design/ps-shell-selection.md §5.2): consult
|
// Shell routing (docs/design/ps-shell-selection.md §5.2): consult
|
||||||
// defaultShell, fall back to bash. isPowerShellToolEnabled() applies the
|
// defaultShell, fall back to bash. isPowerShellToolEnabled() applies the
|
||||||
// same platform + env-var gate as tools.ts so input-box routing matches
|
// same platform + env-var gate as tools.ts so input-box routing matches
|
||||||
// tool-list visibility. Computed up front so telemetry records the
|
// tool-list visibility. Computed up front so telemetry records the
|
||||||
// actual shell, not the raw setting.
|
// actual shell, not the raw setting.
|
||||||
const usePowerShell = isPowerShellToolEnabled() && resolveDefaultShell() === 'powershell';
|
const usePowerShell =
|
||||||
logEvent('tengu_input_bash', {
|
isPowerShellToolEnabled() && resolveDefaultShell() === 'powershell'
|
||||||
powershell: usePowerShell
|
|
||||||
});
|
logEvent('tengu_input_bash', { powershell: usePowerShell })
|
||||||
|
|
||||||
const userMessage = createUserMessage({
|
const userMessage = createUserMessage({
|
||||||
content: prepareUserContent({
|
content: prepareUserContent({
|
||||||
inputString: `<bash-input>${inputString}</bash-input>`,
|
inputString: `<bash-input>${inputString}</bash-input>`,
|
||||||
precedingInputBlocks
|
precedingInputBlocks,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
});
|
|
||||||
|
|
||||||
// ctrl+b to background indicator
|
// ctrl+b to background indicator
|
||||||
let jsx: React.ReactNode;
|
let jsx: React.ReactNode
|
||||||
|
|
||||||
// Just show initial UI
|
// Just show initial UI
|
||||||
setToolJSX({
|
setToolJSX({
|
||||||
jsx: <BashModeProgress input={inputString} progress={null} verbose={context.options.verbose} />,
|
jsx: (
|
||||||
shouldHidePromptInput: false
|
<BashModeProgress
|
||||||
});
|
input={inputString}
|
||||||
|
progress={null}
|
||||||
|
verbose={context.options.verbose}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
shouldHidePromptInput: false,
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bashModeContext: ProcessUserInputContext = {
|
const bashModeContext: ProcessUserInputContext = {
|
||||||
...context,
|
...context,
|
||||||
// TODO: Clean up this hack
|
// TODO: Clean up this hack
|
||||||
setToolJSX: _ => {
|
setToolJSX: _ => {
|
||||||
jsx = _?.jsx;
|
jsx = _?.jsx
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Progress UI — shared across both shell backends (both emit ShellProgress)
|
// Progress UI — shared across both shell backends (both emit ShellProgress)
|
||||||
const onProgress = (progress: {
|
const onProgress = (progress: { data: ShellProgress }) => {
|
||||||
data: ShellProgress;
|
|
||||||
}) => {
|
|
||||||
setToolJSX({
|
setToolJSX({
|
||||||
jsx: <>
|
jsx: (
|
||||||
<BashModeProgress input={inputString!} progress={progress.data} verbose={context.options.verbose} />
|
<>
|
||||||
|
<BashModeProgress
|
||||||
|
input={inputString!}
|
||||||
|
progress={progress.data}
|
||||||
|
verbose={context.options.verbose}
|
||||||
|
/>
|
||||||
{jsx}
|
{jsx}
|
||||||
</>,
|
</>
|
||||||
|
),
|
||||||
shouldHidePromptInput: false,
|
shouldHidePromptInput: false,
|
||||||
showSpinner: false
|
showSpinner: false,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// User-initiated `!` commands run outside sandbox. Both shell tools honor
|
// User-initiated `!` commands run outside sandbox. Both shell tools honor
|
||||||
// dangerouslyDisableSandbox (checked against areUnsandboxedCommandsAllowed()
|
// dangerouslyDisableSandbox (checked against areUnsandboxedCommandsAllowed()
|
||||||
@@ -71,69 +99,107 @@ export async function processBashCommand(inputString: string, precedingInputBloc
|
|||||||
// native, shouldUseSandbox() returns false regardless (unsupported platform).
|
// native, shouldUseSandbox() returns false regardless (unsupported platform).
|
||||||
// Lazy-require PowerShellTool so its ~300KB chunk only loads when the
|
// Lazy-require PowerShellTool so its ~300KB chunk only loads when the
|
||||||
// user has actually selected the powershell default shell.
|
// user has actually selected the powershell default shell.
|
||||||
type PSMod = typeof import('src/tools/PowerShellTool/PowerShellTool.js');
|
type PSMod = typeof import('src/tools/PowerShellTool/PowerShellTool.js')
|
||||||
let PowerShellTool: PSMod['PowerShellTool'] | null = null;
|
let PowerShellTool: PSMod['PowerShellTool'] | null = null
|
||||||
if (usePowerShell) {
|
if (usePowerShell) {
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
PowerShellTool = (require('src/tools/PowerShellTool/PowerShellTool.js') as PSMod).PowerShellTool;
|
PowerShellTool = (
|
||||||
|
require('src/tools/PowerShellTool/PowerShellTool.js') as PSMod
|
||||||
|
).PowerShellTool
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
}
|
}
|
||||||
const shellTool = PowerShellTool ?? BashTool;
|
const shellTool = PowerShellTool ?? BashTool
|
||||||
const response = PowerShellTool ? await PowerShellTool.call({
|
|
||||||
|
const response = PowerShellTool
|
||||||
|
? await PowerShellTool.call(
|
||||||
|
{ command: inputString, dangerouslyDisableSandbox: true },
|
||||||
|
bashModeContext,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
onProgress,
|
||||||
|
)
|
||||||
|
: await BashTool.call(
|
||||||
|
{
|
||||||
command: inputString,
|
command: inputString,
|
||||||
dangerouslyDisableSandbox: true
|
dangerouslyDisableSandbox: true,
|
||||||
}, bashModeContext, undefined, undefined, onProgress) : await BashTool.call({
|
},
|
||||||
command: inputString,
|
bashModeContext,
|
||||||
dangerouslyDisableSandbox: true
|
undefined,
|
||||||
}, bashModeContext, undefined, undefined, onProgress);
|
undefined,
|
||||||
const data = response.data;
|
onProgress,
|
||||||
|
)
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('No result received from shell command');
|
throw new Error('No result received from shell command')
|
||||||
}
|
}
|
||||||
const stderr = data.stderr;
|
|
||||||
|
const stderr = data.stderr
|
||||||
// Reuse the same formatting pipeline as inline !`cmd` bash (promptShellExecution)
|
// Reuse the same formatting pipeline as inline !`cmd` bash (promptShellExecution)
|
||||||
// and model-initiated Bash. When BashTool.call() persists large output to disk,
|
// and model-initiated Bash. When BashTool.call() persists large output to disk,
|
||||||
// data.persistedOutputPath is set and the formatter wraps in <persisted-output>.
|
// data.persistedOutputPath is set and the formatter wraps in <persisted-output>.
|
||||||
// Pass stderr:'' to keep it separate for the <bash-stderr> UI tag.
|
// Pass stderr:'' to keep it separate for the <bash-stderr> UI tag.
|
||||||
const mapped = await processToolResultBlock(shellTool, {
|
const mapped = await processToolResultBlock(
|
||||||
...data,
|
shellTool,
|
||||||
stderr: ''
|
{ ...data, stderr: '' },
|
||||||
}, randomUUID());
|
randomUUID(),
|
||||||
|
)
|
||||||
// mapped.content may contain our own <persisted-output> wrapper (trusted
|
// mapped.content may contain our own <persisted-output> wrapper (trusted
|
||||||
// XML from buildLargeToolResultMessage). Escaping it would turn structural
|
// XML from buildLargeToolResultMessage). Escaping it would turn structural
|
||||||
// tags into <persisted-output>, breaking the model's parse and
|
// tags into <persisted-output>, breaking the model's parse and
|
||||||
// UserBashOutputMessage's extractTag. Escape the raw fallback only.
|
// UserBashOutputMessage's extractTag. Escape the raw fallback only.
|
||||||
const stdout = typeof mapped.content === 'string' ? mapped.content : escapeXml(data.stdout);
|
const stdout =
|
||||||
|
typeof mapped.content === 'string'
|
||||||
|
? mapped.content
|
||||||
|
: escapeXml(data.stdout)
|
||||||
return {
|
return {
|
||||||
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
|
messages: [
|
||||||
content: `<bash-stdout>${stdout}</bash-stdout><bash-stderr>${escapeXml(stderr)}</bash-stderr>`
|
createSyntheticUserCaveatMessage(),
|
||||||
})],
|
userMessage,
|
||||||
shouldQuery: false
|
...attachmentMessages,
|
||||||
};
|
createUserMessage({
|
||||||
|
content: `<bash-stdout>${stdout}</bash-stdout><bash-stderr>${escapeXml(stderr)}</bash-stderr>`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
shouldQuery: false,
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ShellError) {
|
if (e instanceof ShellError) {
|
||||||
if (e.interrupted) {
|
if (e.interrupted) {
|
||||||
return {
|
return {
|
||||||
messages: [createSyntheticUserCaveatMessage(), userMessage, createUserInterruptionMessage({
|
messages: [
|
||||||
toolUse: false
|
createSyntheticUserCaveatMessage(),
|
||||||
}), ...attachmentMessages],
|
userMessage,
|
||||||
shouldQuery: false
|
createUserInterruptionMessage({ toolUse: false }),
|
||||||
};
|
...attachmentMessages,
|
||||||
|
],
|
||||||
|
shouldQuery: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
|
messages: [
|
||||||
content: `<bash-stdout>${escapeXml(e.stdout)}</bash-stdout><bash-stderr>${escapeXml(e.stderr)}</bash-stderr>`
|
createSyntheticUserCaveatMessage(),
|
||||||
})],
|
userMessage,
|
||||||
shouldQuery: false
|
...attachmentMessages,
|
||||||
};
|
createUserMessage({
|
||||||
|
content: `<bash-stdout>${escapeXml(e.stdout)}</bash-stdout><bash-stderr>${escapeXml(e.stderr)}</bash-stderr>`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
shouldQuery: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
|
messages: [
|
||||||
content: `<bash-stderr>Command failed: ${escapeXml(errorMessage(e))}</bash-stderr>`
|
createSyntheticUserCaveatMessage(),
|
||||||
})],
|
userMessage,
|
||||||
shouldQuery: false
|
...attachmentMessages,
|
||||||
};
|
createUserMessage({
|
||||||
|
content: `<bash-stderr>Command failed: ${escapeXml(errorMessage(e))}</bash-stderr>`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
shouldQuery: false,
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setToolJSX(null);
|
setToolJSX(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useLayoutEffect } from 'react'
|
||||||
import { useLayoutEffect } from 'react';
|
import { PassThrough } from 'stream'
|
||||||
import { PassThrough } from 'stream';
|
import stripAnsi from 'strip-ansi'
|
||||||
import stripAnsi from 'strip-ansi';
|
import { render, useApp } from '../ink.js'
|
||||||
import { render, useApp } from '../ink.js';
|
|
||||||
|
|
||||||
// This is a workaround for the fact that Ink doesn't support multiple <Static>
|
// This is a workaround for the fact that Ink doesn't support multiple <Static>
|
||||||
// components in the same render tree. Instead of using a <Static> we just render
|
// components in the same render tree. Instead of using a <Static> we just render
|
||||||
@@ -15,44 +14,26 @@ import { render, useApp } from '../ink.js';
|
|||||||
* before exiting. This is more robust than process.nextTick() for React 19's
|
* before exiting. This is more robust than process.nextTick() for React 19's
|
||||||
* async render cycle.
|
* async render cycle.
|
||||||
*/
|
*/
|
||||||
function RenderOnceAndExit(t0) {
|
function RenderOnceAndExit({
|
||||||
const $ = _c(5);
|
children,
|
||||||
const {
|
}: {
|
||||||
children
|
children: React.ReactNode
|
||||||
} = t0;
|
}): React.ReactNode {
|
||||||
const {
|
const { exit } = useApp()
|
||||||
exit
|
|
||||||
} = useApp();
|
// useLayoutEffect runs synchronously after React commits DOM mutations.
|
||||||
let t1;
|
// setTimeout(0) defers exit to allow Ink to flush output to the stream.
|
||||||
let t2;
|
useLayoutEffect(() => {
|
||||||
if ($[0] !== exit) {
|
const timer = setTimeout(exit, 0)
|
||||||
t1 = () => {
|
return () => clearTimeout(timer)
|
||||||
const timer = setTimeout(exit, 0);
|
}, [exit])
|
||||||
return () => clearTimeout(timer);
|
|
||||||
};
|
return <>{children}</>
|
||||||
t2 = [exit];
|
|
||||||
$[0] = exit;
|
|
||||||
$[1] = t1;
|
|
||||||
$[2] = t2;
|
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
t2 = $[2];
|
|
||||||
}
|
|
||||||
useLayoutEffect(t1, t2);
|
|
||||||
let t3;
|
|
||||||
if ($[3] !== children) {
|
|
||||||
t3 = <>{children}</>;
|
|
||||||
$[3] = children;
|
|
||||||
$[4] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[4];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEC synchronized update markers used by terminals
|
// DEC synchronized update markers used by terminals
|
||||||
const SYNC_START = '\x1B[?2026h';
|
const SYNC_START = '\x1B[?2026h'
|
||||||
const SYNC_END = '\x1B[?2026l';
|
const SYNC_END = '\x1B[?2026l'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts content from the first complete frame in Ink's output.
|
* Extracts content from the first complete frame in Ink's output.
|
||||||
@@ -60,56 +41,64 @@ const SYNC_END = '\x1B[?2026l';
|
|||||||
* update sequences ([?2026h ... [?2026l). We only want the first frame's content.
|
* update sequences ([?2026h ... [?2026l). We only want the first frame's content.
|
||||||
*/
|
*/
|
||||||
function extractFirstFrame(output: string): string {
|
function extractFirstFrame(output: string): string {
|
||||||
const startIndex = output.indexOf(SYNC_START);
|
const startIndex = output.indexOf(SYNC_START)
|
||||||
if (startIndex === -1) return output;
|
if (startIndex === -1) return output
|
||||||
const contentStart = startIndex + SYNC_START.length;
|
|
||||||
const endIndex = output.indexOf(SYNC_END, contentStart);
|
const contentStart = startIndex + SYNC_START.length
|
||||||
if (endIndex === -1) return output;
|
const endIndex = output.indexOf(SYNC_END, contentStart)
|
||||||
return output.slice(contentStart, endIndex);
|
if (endIndex === -1) return output
|
||||||
|
|
||||||
|
return output.slice(contentStart, endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a React node to a string with ANSI escape codes (for terminal output).
|
* Renders a React node to a string with ANSI escape codes (for terminal output).
|
||||||
*/
|
*/
|
||||||
export function renderToAnsiString(node: React.ReactNode, columns?: number): Promise<string> {
|
export function renderToAnsiString(
|
||||||
|
node: React.ReactNode,
|
||||||
|
columns?: number,
|
||||||
|
): Promise<string> {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
let output = '';
|
let output = ''
|
||||||
|
|
||||||
// Capture all writes. Set .columns so Ink (ink.tsx:~165) picks up a
|
// Capture all writes. Set .columns so Ink (ink.tsx:~165) picks up a
|
||||||
// chosen width instead of PassThrough's undefined → 80 fallback —
|
// chosen width instead of PassThrough's undefined → 80 fallback —
|
||||||
// useful for rendering at terminal width for file dumps that should
|
// useful for rendering at terminal width for file dumps that should
|
||||||
// match what the user sees on screen.
|
// match what the user sees on screen.
|
||||||
const stream = new PassThrough();
|
const stream = new PassThrough()
|
||||||
if (columns !== undefined) {
|
if (columns !== undefined) {
|
||||||
;
|
;(stream as unknown as { columns: number }).columns = columns
|
||||||
(stream as unknown as {
|
|
||||||
columns: number;
|
|
||||||
}).columns = columns;
|
|
||||||
}
|
}
|
||||||
stream.on('data', chunk => {
|
stream.on('data', chunk => {
|
||||||
output += chunk.toString();
|
output += chunk.toString()
|
||||||
});
|
})
|
||||||
|
|
||||||
// Render the component wrapped in RenderOnceAndExit
|
// Render the component wrapped in RenderOnceAndExit
|
||||||
// Non-TTY stdout (PassThrough) gives full-frame output instead of diffs
|
// Non-TTY stdout (PassThrough) gives full-frame output instead of diffs
|
||||||
const instance = await render(<RenderOnceAndExit>{node}</RenderOnceAndExit>, {
|
const instance = await render(
|
||||||
|
<RenderOnceAndExit>{node}</RenderOnceAndExit>,
|
||||||
|
{
|
||||||
stdout: stream as unknown as NodeJS.WriteStream,
|
stdout: stream as unknown as NodeJS.WriteStream,
|
||||||
patchConsole: false
|
patchConsole: false,
|
||||||
});
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Wait for the component to exit naturally
|
// Wait for the component to exit naturally
|
||||||
await instance.waitUntilExit();
|
await instance.waitUntilExit()
|
||||||
|
|
||||||
// Extract only the first frame's content to avoid duplication
|
// Extract only the first frame's content to avoid duplication
|
||||||
// (Ink outputs multiple frames in non-TTY mode)
|
// (Ink outputs multiple frames in non-TTY mode)
|
||||||
await resolve(extractFirstFrame(output));
|
await resolve(extractFirstFrame(output))
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a React node to a plain text string (ANSI codes stripped).
|
* Renders a React node to a plain text string (ANSI codes stripped).
|
||||||
*/
|
*/
|
||||||
export async function renderToString(node: React.ReactNode, columns?: number): Promise<string> {
|
export async function renderToString(
|
||||||
const output = await renderToAnsiString(node, columns);
|
node: React.ReactNode,
|
||||||
return stripAnsi(output);
|
columns?: number,
|
||||||
|
): Promise<string> {
|
||||||
|
const output = await renderToAnsiString(node, columns)
|
||||||
|
return stripAnsi(output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,361 +1,472 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk'
|
||||||
import figures from 'figures';
|
import figures from 'figures'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { color, Text } from '../ink.js';
|
import { color, Text } from '../ink.js'
|
||||||
import type { MCPServerConnection } from '../services/mcp/types.js';
|
import type { MCPServerConnection } from '../services/mcp/types.js'
|
||||||
import { getAccountInformation, isClaudeAISubscriber } from './auth.js';
|
import { getAccountInformation, isClaudeAISubscriber } from './auth.js'
|
||||||
import { getLargeMemoryFiles, getMemoryFiles, MAX_MEMORY_CHARACTER_COUNT } from './claudemd.js';
|
import {
|
||||||
import { getDoctorDiagnostic } from './doctorDiagnostic.js';
|
getLargeMemoryFiles,
|
||||||
import { getAWSRegion, getDefaultVertexRegion, isEnvTruthy } from './envUtils.js';
|
getMemoryFiles,
|
||||||
import { getDisplayPath } from './file.js';
|
MAX_MEMORY_CHARACTER_COUNT,
|
||||||
import { formatNumber } from './format.js';
|
} from './claudemd.js'
|
||||||
import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js';
|
import { getDoctorDiagnostic } from './doctorDiagnostic.js'
|
||||||
import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js';
|
import {
|
||||||
import { getAPIProvider } from './model/providers.js';
|
getAWSRegion,
|
||||||
import { getMTLSConfig } from './mtls.js';
|
getDefaultVertexRegion,
|
||||||
import { checkInstall } from './nativeInstaller/index.js';
|
isEnvTruthy,
|
||||||
import { getProxyUrl } from './proxy.js';
|
} from './envUtils.js'
|
||||||
import { SandboxManager } from './sandbox/sandbox-adapter.js';
|
import { getDisplayPath } from './file.js'
|
||||||
import { getSettingsWithAllErrors } from './settings/allErrors.js';
|
import { formatNumber } from './format.js'
|
||||||
import { getEnabledSettingSources, getSettingSourceDisplayNameCapitalized } from './settings/constants.js';
|
import {
|
||||||
import { getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource } from './settings/settings.js';
|
getIdeClientName,
|
||||||
import type { ThemeName } from './theme.js';
|
type IDEExtensionInstallationStatus,
|
||||||
|
isJetBrainsIde,
|
||||||
|
toIDEDisplayName,
|
||||||
|
} from './ide.js'
|
||||||
|
import {
|
||||||
|
getClaudeAiUserDefaultModelDescription,
|
||||||
|
modelDisplayString,
|
||||||
|
} from './model/model.js'
|
||||||
|
import { getAPIProvider } from './model/providers.js'
|
||||||
|
import { getMTLSConfig } from './mtls.js'
|
||||||
|
import { checkInstall } from './nativeInstaller/index.js'
|
||||||
|
import { getProxyUrl } from './proxy.js'
|
||||||
|
import { SandboxManager } from './sandbox/sandbox-adapter.js'
|
||||||
|
import { getSettingsWithAllErrors } from './settings/allErrors.js'
|
||||||
|
import {
|
||||||
|
getEnabledSettingSources,
|
||||||
|
getSettingSourceDisplayNameCapitalized,
|
||||||
|
} from './settings/constants.js'
|
||||||
|
import {
|
||||||
|
getManagedFileSettingsPresence,
|
||||||
|
getPolicySettingsOrigin,
|
||||||
|
getSettingsForSource,
|
||||||
|
} from './settings/settings.js'
|
||||||
|
import type { ThemeName } from './theme.js'
|
||||||
|
|
||||||
export type Property = {
|
export type Property = {
|
||||||
label?: string;
|
label?: string
|
||||||
value: React.ReactNode | Array<string>;
|
value: React.ReactNode | Array<string>
|
||||||
};
|
|
||||||
export type Diagnostic = React.ReactNode;
|
|
||||||
export function buildSandboxProperties(): Property[] {
|
|
||||||
if ((process.env.USER_TYPE) !== 'ant') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const isSandboxed = SandboxManager.isSandboxingEnabled();
|
|
||||||
return [{
|
|
||||||
label: 'Bash Sandbox',
|
|
||||||
value: isSandboxed ? 'Enabled' : 'Disabled'
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
export function buildIDEProperties(mcpClients: MCPServerConnection[], ideInstallationStatus: IDEExtensionInstallationStatus | null = null, theme: ThemeName): Property[] {
|
|
||||||
const ideClient = mcpClients?.find(client => client.name === 'ide');
|
export type Diagnostic = React.ReactNode
|
||||||
|
|
||||||
|
export function buildSandboxProperties(): Property[] {
|
||||||
|
if (process.env.USER_TYPE !== 'ant') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSandboxed = SandboxManager.isSandboxingEnabled()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Bash Sandbox',
|
||||||
|
value: isSandboxed ? 'Enabled' : 'Disabled',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildIDEProperties(
|
||||||
|
mcpClients: MCPServerConnection[],
|
||||||
|
ideInstallationStatus: IDEExtensionInstallationStatus | null = null,
|
||||||
|
theme: ThemeName,
|
||||||
|
): Property[] {
|
||||||
|
const ideClient = mcpClients?.find(client => client.name === 'ide')
|
||||||
|
|
||||||
if (ideInstallationStatus) {
|
if (ideInstallationStatus) {
|
||||||
const ideName = toIDEDisplayName(ideInstallationStatus.ideType);
|
const ideName = toIDEDisplayName(ideInstallationStatus.ideType)
|
||||||
const pluginOrExtension = isJetBrainsIde(ideInstallationStatus.ideType) ? 'plugin' : 'extension';
|
const pluginOrExtension = isJetBrainsIde(ideInstallationStatus.ideType)
|
||||||
|
? 'plugin'
|
||||||
|
: 'extension'
|
||||||
|
|
||||||
if (ideInstallationStatus.error) {
|
if (ideInstallationStatus.error) {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: <Text>
|
value: (
|
||||||
|
<Text>
|
||||||
{color('error', theme)(figures.cross)} Error installing {ideName}{' '}
|
{color('error', theme)(figures.cross)} Error installing {ideName}{' '}
|
||||||
{pluginOrExtension}: {ideInstallationStatus.error}
|
{pluginOrExtension}: {ideInstallationStatus.error}
|
||||||
{'\n'}Please restart your IDE and try again.
|
{'\n'}Please restart your IDE and try again.
|
||||||
</Text>
|
</Text>
|
||||||
}];
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ideInstallationStatus.installed) {
|
if (ideInstallationStatus.installed) {
|
||||||
if (ideClient && ideClient.type === 'connected') {
|
if (ideClient && ideClient.type === 'connected') {
|
||||||
if (ideInstallationStatus.installedVersion !== ideClient.serverInfo?.version) {
|
if (
|
||||||
return [{
|
ideInstallationStatus.installedVersion !==
|
||||||
|
ideClient.serverInfo?.version
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion} (server version: ${ideClient.serverInfo?.version})`
|
value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion} (server version: ${ideClient.serverInfo?.version})`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion}`
|
value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion}`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: `Installed ${ideName} ${pluginOrExtension}`
|
value: `Installed ${ideName} ${pluginOrExtension}`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (ideClient) {
|
} else if (ideClient) {
|
||||||
const ideName = getIdeClientName(ideClient) ?? 'IDE';
|
const ideName = getIdeClientName(ideClient) ?? 'IDE'
|
||||||
if (ideClient.type === 'connected') {
|
if (ideClient.type === 'connected') {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: `Connected to ${ideName} extension`
|
value: `Connected to ${ideName} extension`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
label: 'IDE',
|
label: 'IDE',
|
||||||
value: `${color('error', theme)(figures.cross)} Not connected to ${ideName}`
|
value: `${color('error', theme)(figures.cross)} Not connected to ${ideName}`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
export function buildMcpProperties(clients: MCPServerConnection[] = [], theme: ThemeName): Property[] {
|
|
||||||
const servers = clients.filter(client => client.name !== 'ide');
|
export function buildMcpProperties(
|
||||||
|
clients: MCPServerConnection[] = [],
|
||||||
|
theme: ThemeName,
|
||||||
|
): Property[] {
|
||||||
|
const servers = clients.filter(client => client.name !== 'ide')
|
||||||
if (!servers.length) {
|
if (!servers.length) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary instead of a full server list — 20+ servers wrapped onto many
|
// Summary instead of a full server list — 20+ servers wrapped onto many
|
||||||
// rows, dominating the Status pane. Show counts by state + /mcp hint.
|
// rows, dominating the Status pane. Show counts by state + /mcp hint.
|
||||||
const byState = {
|
const byState = { connected: 0, pending: 0, needsAuth: 0, failed: 0 }
|
||||||
connected: 0,
|
|
||||||
pending: 0,
|
|
||||||
needsAuth: 0,
|
|
||||||
failed: 0
|
|
||||||
};
|
|
||||||
for (const s of servers) {
|
for (const s of servers) {
|
||||||
if (s.type === 'connected') byState.connected++;else if (s.type === 'pending') byState.pending++;else if (s.type === 'needs-auth') byState.needsAuth++;else byState.failed++;
|
if (s.type === 'connected') byState.connected++
|
||||||
|
else if (s.type === 'pending') byState.pending++
|
||||||
|
else if (s.type === 'needs-auth') byState.needsAuth++
|
||||||
|
else byState.failed++
|
||||||
}
|
}
|
||||||
const parts: string[] = [];
|
const parts: string[] = []
|
||||||
if (byState.connected) parts.push(color('success', theme)(`${byState.connected} connected`));
|
if (byState.connected)
|
||||||
if (byState.needsAuth) parts.push(color('warning', theme)(`${byState.needsAuth} need auth`));
|
parts.push(color('success', theme)(`${byState.connected} connected`))
|
||||||
if (byState.pending) parts.push(color('inactive', theme)(`${byState.pending} pending`));
|
if (byState.needsAuth)
|
||||||
if (byState.failed) parts.push(color('error', theme)(`${byState.failed} failed`));
|
parts.push(color('warning', theme)(`${byState.needsAuth} need auth`))
|
||||||
return [{
|
if (byState.pending)
|
||||||
|
parts.push(color('inactive', theme)(`${byState.pending} pending`))
|
||||||
|
if (byState.failed)
|
||||||
|
parts.push(color('error', theme)(`${byState.failed} failed`))
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
label: 'MCP servers',
|
label: 'MCP servers',
|
||||||
value: `${parts.join(', ')} ${color('inactive', theme)('· /mcp')}`
|
value: `${parts.join(', ')} ${color('inactive', theme)('· /mcp')}`,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildMemoryDiagnostics(): Promise<Diagnostic[]> {
|
export async function buildMemoryDiagnostics(): Promise<Diagnostic[]> {
|
||||||
const files = await getMemoryFiles();
|
const files = await getMemoryFiles()
|
||||||
const largeFiles = getLargeMemoryFiles(files);
|
const largeFiles = getLargeMemoryFiles(files)
|
||||||
const diagnostics: Diagnostic[] = [];
|
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
largeFiles.forEach(file => {
|
largeFiles.forEach(file => {
|
||||||
const displayPath = getDisplayPath(file.path);
|
const displayPath = getDisplayPath(file.path)
|
||||||
diagnostics.push(`Large ${displayPath} will impact performance (${formatNumber(file.content.length)} chars > ${formatNumber(MAX_MEMORY_CHARACTER_COUNT)})`);
|
diagnostics.push(
|
||||||
});
|
`Large ${displayPath} will impact performance (${formatNumber(file.content.length)} chars > ${formatNumber(MAX_MEMORY_CHARACTER_COUNT)})`,
|
||||||
return diagnostics;
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSettingSourcesProperties(): Property[] {
|
export function buildSettingSourcesProperties(): Property[] {
|
||||||
const enabledSources = getEnabledSettingSources();
|
const enabledSources = getEnabledSettingSources()
|
||||||
|
|
||||||
// Filter to only sources that actually have settings loaded
|
// Filter to only sources that actually have settings loaded
|
||||||
const sourcesWithSettings = enabledSources.filter(source => {
|
const sourcesWithSettings = enabledSources.filter(source => {
|
||||||
const settings = getSettingsForSource(source);
|
const settings = getSettingsForSource(source)
|
||||||
return settings !== null && Object.keys(settings).length > 0;
|
return settings !== null && Object.keys(settings).length > 0
|
||||||
});
|
})
|
||||||
|
|
||||||
// Map internal names to user-friendly names
|
// Map internal names to user-friendly names
|
||||||
// For policySettings, distinguish between remote and local (or skip if neither exists)
|
// For policySettings, distinguish between remote and local (or skip if neither exists)
|
||||||
const sourceNames = sourcesWithSettings.map(source => {
|
const sourceNames = sourcesWithSettings
|
||||||
|
.map(source => {
|
||||||
if (source === 'policySettings') {
|
if (source === 'policySettings') {
|
||||||
const origin = getPolicySettingsOrigin();
|
const origin = getPolicySettingsOrigin()
|
||||||
if (origin === null) {
|
if (origin === null) {
|
||||||
return null; // Skip - no policy settings exist
|
return null // Skip - no policy settings exist
|
||||||
}
|
}
|
||||||
switch (origin) {
|
switch (origin) {
|
||||||
case 'remote':
|
case 'remote':
|
||||||
return 'Enterprise managed settings (remote)';
|
return 'Enterprise managed settings (remote)'
|
||||||
case 'plist':
|
case 'plist':
|
||||||
return 'Enterprise managed settings (plist)';
|
return 'Enterprise managed settings (plist)'
|
||||||
case 'hklm':
|
case 'hklm':
|
||||||
return 'Enterprise managed settings (HKLM)';
|
return 'Enterprise managed settings (HKLM)'
|
||||||
case 'file':
|
case 'file': {
|
||||||
{
|
const { hasBase, hasDropIns } = getManagedFileSettingsPresence()
|
||||||
const {
|
|
||||||
hasBase,
|
|
||||||
hasDropIns
|
|
||||||
} = getManagedFileSettingsPresence();
|
|
||||||
if (hasBase && hasDropIns) {
|
if (hasBase && hasDropIns) {
|
||||||
return 'Enterprise managed settings (file + drop-ins)';
|
return 'Enterprise managed settings (file + drop-ins)'
|
||||||
}
|
}
|
||||||
if (hasDropIns) {
|
if (hasDropIns) {
|
||||||
return 'Enterprise managed settings (drop-ins)';
|
return 'Enterprise managed settings (drop-ins)'
|
||||||
}
|
}
|
||||||
return 'Enterprise managed settings (file)';
|
return 'Enterprise managed settings (file)'
|
||||||
}
|
}
|
||||||
case 'hkcu':
|
case 'hkcu':
|
||||||
return 'Enterprise managed settings (HKCU)';
|
return 'Enterprise managed settings (HKCU)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getSettingSourceDisplayNameCapitalized(source);
|
return getSettingSourceDisplayNameCapitalized(source)
|
||||||
}).filter((name): name is string => name !== null);
|
})
|
||||||
return [{
|
.filter((name): name is string => name !== null)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
label: 'Setting sources',
|
label: 'Setting sources',
|
||||||
value: sourceNames
|
value: sourceNames,
|
||||||
}];
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildInstallationDiagnostics(): Promise<Diagnostic[]> {
|
export async function buildInstallationDiagnostics(): Promise<Diagnostic[]> {
|
||||||
const installWarnings = await checkInstall();
|
const installWarnings = await checkInstall()
|
||||||
return installWarnings.map(warning => warning.message);
|
return installWarnings.map(warning => warning.message)
|
||||||
}
|
}
|
||||||
export async function buildInstallationHealthDiagnostics(): Promise<Diagnostic[]> {
|
|
||||||
const diagnostic = await getDoctorDiagnostic();
|
export async function buildInstallationHealthDiagnostics(): Promise<
|
||||||
const items: Diagnostic[] = [];
|
Diagnostic[]
|
||||||
const {
|
> {
|
||||||
errors: validationErrors
|
const diagnostic = await getDoctorDiagnostic()
|
||||||
} = getSettingsWithAllErrors();
|
const items: Diagnostic[] = []
|
||||||
|
|
||||||
|
const { errors: validationErrors } = getSettingsWithAllErrors()
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
const invalidFiles = Array.from(new Set(validationErrors.map(error => error.file)));
|
const invalidFiles = Array.from(
|
||||||
const fileList = invalidFiles.join(', ');
|
new Set(validationErrors.map(error => error.file)),
|
||||||
items.push(`Found invalid settings files: ${fileList}. They will be ignored.`);
|
)
|
||||||
|
const fileList = invalidFiles.join(', ')
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
`Found invalid settings files: ${fileList}. They will be ignored.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add warnings from doctor diagnostic (includes leftover installations, config mismatches, etc.)
|
// Add warnings from doctor diagnostic (includes leftover installations, config mismatches, etc.)
|
||||||
diagnostic.warnings.forEach(warning => {
|
diagnostic.warnings.forEach(warning => {
|
||||||
items.push(warning.issue);
|
items.push(warning.issue)
|
||||||
});
|
})
|
||||||
|
|
||||||
if (diagnostic.hasUpdatePermissions === false) {
|
if (diagnostic.hasUpdatePermissions === false) {
|
||||||
items.push('No write permissions for auto-updates (requires sudo)');
|
items.push('No write permissions for auto-updates (requires sudo)')
|
||||||
}
|
}
|
||||||
return items;
|
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAccountProperties(): Property[] {
|
export function buildAccountProperties(): Property[] {
|
||||||
const accountInfo = getAccountInformation();
|
const accountInfo = getAccountInformation()
|
||||||
if (!accountInfo) {
|
if (!accountInfo) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
const properties: Property[] = [];
|
|
||||||
|
const properties: Property[] = []
|
||||||
|
|
||||||
if (accountInfo.subscription) {
|
if (accountInfo.subscription) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Login method',
|
label: 'Login method',
|
||||||
value: `${accountInfo.subscription} Account`
|
value: `${accountInfo.subscription} Account`,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountInfo.tokenSource) {
|
if (accountInfo.tokenSource) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Auth token',
|
label: 'Auth token',
|
||||||
value: accountInfo.tokenSource
|
value: accountInfo.tokenSource,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountInfo.apiKeySource) {
|
if (accountInfo.apiKeySource) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'API key',
|
label: 'API key',
|
||||||
value: accountInfo.apiKeySource
|
value: accountInfo.apiKeySource,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide sensitive account info in demo mode
|
// Hide sensitive account info in demo mode
|
||||||
if (accountInfo.organization && !process.env.IS_DEMO) {
|
if (accountInfo.organization && !process.env.IS_DEMO) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Organization',
|
label: 'Organization',
|
||||||
value: accountInfo.organization
|
value: accountInfo.organization,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if (accountInfo.email && !process.env.IS_DEMO) {
|
if (accountInfo.email && !process.env.IS_DEMO) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
value: accountInfo.email
|
value: accountInfo.email,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return properties;
|
|
||||||
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAPIProviderProperties(): Property[] {
|
export function buildAPIProviderProperties(): Property[] {
|
||||||
const apiProvider = getAPIProvider();
|
const apiProvider = getAPIProvider()
|
||||||
const properties: Property[] = [];
|
|
||||||
|
const properties: Property[] = []
|
||||||
|
|
||||||
if (apiProvider !== 'firstParty') {
|
if (apiProvider !== 'firstParty') {
|
||||||
const providerLabel = {
|
const providerLabel = {
|
||||||
bedrock: 'AWS Bedrock',
|
bedrock: 'AWS Bedrock',
|
||||||
vertex: 'Google Vertex AI',
|
vertex: 'Google Vertex AI',
|
||||||
foundry: 'Microsoft Foundry'
|
foundry: 'Microsoft Foundry',
|
||||||
}[apiProvider];
|
}[apiProvider]
|
||||||
|
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'API provider',
|
label: 'API provider',
|
||||||
value: providerLabel
|
value: providerLabel,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiProvider === 'firstParty') {
|
if (apiProvider === 'firstParty') {
|
||||||
const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL;
|
const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL
|
||||||
if (anthropicBaseUrl) {
|
if (anthropicBaseUrl) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Anthropic base URL',
|
label: 'Anthropic base URL',
|
||||||
value: anthropicBaseUrl
|
value: anthropicBaseUrl,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} else if (apiProvider === 'bedrock') {
|
} else if (apiProvider === 'bedrock') {
|
||||||
const bedrockBaseUrl = process.env.BEDROCK_BASE_URL;
|
const bedrockBaseUrl = process.env.BEDROCK_BASE_URL
|
||||||
if (bedrockBaseUrl) {
|
if (bedrockBaseUrl) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Bedrock base URL',
|
label: 'Bedrock base URL',
|
||||||
value: bedrockBaseUrl
|
value: bedrockBaseUrl,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'AWS region',
|
label: 'AWS region',
|
||||||
value: getAWSRegion()
|
value: getAWSRegion(),
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) {
|
||||||
properties.push({
|
properties.push({
|
||||||
value: 'AWS auth skipped'
|
value: 'AWS auth skipped',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} else if (apiProvider === 'vertex') {
|
} else if (apiProvider === 'vertex') {
|
||||||
const vertexBaseUrl = process.env.VERTEX_BASE_URL;
|
const vertexBaseUrl = process.env.VERTEX_BASE_URL
|
||||||
if (vertexBaseUrl) {
|
if (vertexBaseUrl) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Vertex base URL',
|
label: 'Vertex base URL',
|
||||||
value: vertexBaseUrl
|
value: vertexBaseUrl,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
const gcpProject = process.env.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
||||||
|
const gcpProject = process.env.ANTHROPIC_VERTEX_PROJECT_ID
|
||||||
if (gcpProject) {
|
if (gcpProject) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'GCP project',
|
label: 'GCP project',
|
||||||
value: gcpProject
|
value: gcpProject,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Default region',
|
label: 'Default region',
|
||||||
value: getDefaultVertexRegion()
|
value: getDefaultVertexRegion(),
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) {
|
||||||
properties.push({
|
properties.push({
|
||||||
value: 'GCP auth skipped'
|
value: 'GCP auth skipped',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} else if (apiProvider === 'foundry') {
|
} else if (apiProvider === 'foundry') {
|
||||||
const foundryBaseUrl = process.env.ANTHROPIC_FOUNDRY_BASE_URL;
|
const foundryBaseUrl = process.env.ANTHROPIC_FOUNDRY_BASE_URL
|
||||||
if (foundryBaseUrl) {
|
if (foundryBaseUrl) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Microsoft Foundry base URL',
|
label: 'Microsoft Foundry base URL',
|
||||||
value: foundryBaseUrl
|
value: foundryBaseUrl,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
const foundryResource = process.env.ANTHROPIC_FOUNDRY_RESOURCE;
|
|
||||||
|
const foundryResource = process.env.ANTHROPIC_FOUNDRY_RESOURCE
|
||||||
if (foundryResource) {
|
if (foundryResource) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Microsoft Foundry resource',
|
label: 'Microsoft Foundry resource',
|
||||||
value: foundryResource
|
value: foundryResource,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FOUNDRY_AUTH)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FOUNDRY_AUTH)) {
|
||||||
properties.push({
|
properties.push({
|
||||||
value: 'Microsoft Foundry auth skipped'
|
value: 'Microsoft Foundry auth skipped',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const proxyUrl = getProxyUrl();
|
|
||||||
|
const proxyUrl = getProxyUrl()
|
||||||
if (proxyUrl) {
|
if (proxyUrl) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Proxy',
|
label: 'Proxy',
|
||||||
value: proxyUrl
|
value: proxyUrl,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
const mtlsConfig = getMTLSConfig();
|
|
||||||
|
const mtlsConfig = getMTLSConfig()
|
||||||
if (process.env.NODE_EXTRA_CA_CERTS) {
|
if (process.env.NODE_EXTRA_CA_CERTS) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'Additional CA cert(s)',
|
label: 'Additional CA cert(s)',
|
||||||
value: process.env.NODE_EXTRA_CA_CERTS
|
value: process.env.NODE_EXTRA_CA_CERTS,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if (mtlsConfig) {
|
if (mtlsConfig) {
|
||||||
if (mtlsConfig.cert && process.env.CLAUDE_CODE_CLIENT_CERT) {
|
if (mtlsConfig.cert && process.env.CLAUDE_CODE_CLIENT_CERT) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'mTLS client cert',
|
label: 'mTLS client cert',
|
||||||
value: process.env.CLAUDE_CODE_CLIENT_CERT
|
value: process.env.CLAUDE_CODE_CLIENT_CERT,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mtlsConfig.key && process.env.CLAUDE_CODE_CLIENT_KEY) {
|
if (mtlsConfig.key && process.env.CLAUDE_CODE_CLIENT_KEY) {
|
||||||
properties.push({
|
properties.push({
|
||||||
label: 'mTLS client key',
|
label: 'mTLS client key',
|
||||||
value: process.env.CLAUDE_CODE_CLIENT_KEY
|
value: process.env.CLAUDE_CODE_CLIENT_KEY,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return properties;
|
|
||||||
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModelDisplayLabel(mainLoopModel: string | null): string {
|
export function getModelDisplayLabel(mainLoopModel: string | null): string {
|
||||||
let modelLabel = modelDisplayString(mainLoopModel);
|
let modelLabel = modelDisplayString(mainLoopModel)
|
||||||
|
|
||||||
if (mainLoopModel === null && isClaudeAISubscriber()) {
|
if (mainLoopModel === null && isClaudeAISubscriber()) {
|
||||||
const description = getClaudeAiUserDefaultModelDescription();
|
const description = getClaudeAiUserDefaultModelDescription()
|
||||||
modelLabel = `${chalk.bold('Default')} ${description}`;
|
|
||||||
|
modelLabel = `${chalk.bold('Default')} ${description}`
|
||||||
}
|
}
|
||||||
return modelLabel;
|
|
||||||
|
return modelLabel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||||
import { Box, Text } from '../ink.js';
|
import { Box, Text } from '../ink.js'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { getLargeMemoryFiles, MAX_MEMORY_CHARACTER_COUNT, type MemoryFileInfo } from './claudemd.js';
|
import {
|
||||||
import figures from 'figures';
|
getLargeMemoryFiles,
|
||||||
import { getCwd } from './cwd.js';
|
MAX_MEMORY_CHARACTER_COUNT,
|
||||||
import { relative } from 'path';
|
type MemoryFileInfo,
|
||||||
import { formatNumber } from './format.js';
|
} from './claudemd.js'
|
||||||
import type { getGlobalConfig } from './config.js';
|
import figures from 'figures'
|
||||||
import { getAnthropicApiKeyWithSource, getApiKeyFromConfigOrMacOSKeychain, getAuthTokenSource, isClaudeAISubscriber } from './auth.js';
|
import { getCwd } from './cwd.js'
|
||||||
import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js';
|
import { relative } from 'path'
|
||||||
import { getAgentDescriptionsTotalTokens, AGENT_DESCRIPTIONS_THRESHOLD } from './statusNoticeHelpers.js';
|
import { formatNumber } from './format.js'
|
||||||
import { isSupportedJetBrainsTerminal, toIDEDisplayName, getTerminalIdeType } from './ide.js';
|
import type { getGlobalConfig } from './config.js'
|
||||||
import { isJetBrainsPluginInstalledCachedSync } from './jetbrains.js';
|
import {
|
||||||
|
getAnthropicApiKeyWithSource,
|
||||||
|
getApiKeyFromConfigOrMacOSKeychain,
|
||||||
|
getAuthTokenSource,
|
||||||
|
isClaudeAISubscriber,
|
||||||
|
} from './auth.js'
|
||||||
|
import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js'
|
||||||
|
import {
|
||||||
|
getAgentDescriptionsTotalTokens,
|
||||||
|
AGENT_DESCRIPTIONS_THRESHOLD,
|
||||||
|
} from './statusNoticeHelpers.js'
|
||||||
|
import {
|
||||||
|
isSupportedJetBrainsTerminal,
|
||||||
|
toIDEDisplayName,
|
||||||
|
getTerminalIdeType,
|
||||||
|
} from './ide.js'
|
||||||
|
import { isJetBrainsPluginInstalledCachedSync } from './jetbrains.js'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type StatusNoticeType = 'warning' | 'info';
|
export type StatusNoticeType = 'warning' | 'info'
|
||||||
|
|
||||||
export type StatusNoticeContext = {
|
export type StatusNoticeContext = {
|
||||||
config: ReturnType<typeof getGlobalConfig>;
|
config: ReturnType<typeof getGlobalConfig>
|
||||||
agentDefinitions?: AgentDefinitionsResult;
|
agentDefinitions?: AgentDefinitionsResult
|
||||||
memoryFiles: MemoryFileInfo[];
|
memoryFiles: MemoryFileInfo[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type StatusNoticeDefinition = {
|
export type StatusNoticeDefinition = {
|
||||||
id: string;
|
id: string
|
||||||
type: StatusNoticeType;
|
type: StatusNoticeType
|
||||||
isActive: (context: StatusNoticeContext) => boolean;
|
isActive: (context: StatusNoticeContext) => boolean
|
||||||
render: (context: StatusNoticeContext) => React.ReactNode;
|
render: (context: StatusNoticeContext) => React.ReactNode
|
||||||
};
|
}
|
||||||
|
|
||||||
// Individual notice definitions
|
// Individual notice definitions
|
||||||
const largeMemoryFilesNotice: StatusNoticeDefinition = {
|
const largeMemoryFilesNotice: StatusNoticeDefinition = {
|
||||||
@@ -33,11 +51,16 @@ const largeMemoryFilesNotice: StatusNoticeDefinition = {
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
isActive: ctx => getLargeMemoryFiles(ctx.memoryFiles).length > 0,
|
isActive: ctx => getLargeMemoryFiles(ctx.memoryFiles).length > 0,
|
||||||
render: ctx => {
|
render: ctx => {
|
||||||
const largeMemoryFiles = getLargeMemoryFiles(ctx.memoryFiles);
|
const largeMemoryFiles = getLargeMemoryFiles(ctx.memoryFiles)
|
||||||
return <>
|
return (
|
||||||
|
<>
|
||||||
{largeMemoryFiles.map(file => {
|
{largeMemoryFiles.map(file => {
|
||||||
const displayPath = file.path.startsWith(getCwd()) ? relative(getCwd(), file.path) : file.path;
|
const displayPath = file.path.startsWith(getCwd())
|
||||||
return <Box key={file.path} flexDirection="row">
|
? relative(getCwd(), file.path)
|
||||||
|
: file.path
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={file.path} flexDirection="row">
|
||||||
<Text color="warning">{figures.warning}</Text>
|
<Text color="warning">{figures.warning}</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
Large <Text bold>{displayPath}</Text> will impact performance (
|
Large <Text bold>{displayPath}</Text> will impact performance (
|
||||||
@@ -45,76 +68,92 @@ const largeMemoryFilesNotice: StatusNoticeDefinition = {
|
|||||||
{formatNumber(MAX_MEMORY_CHARACTER_COUNT)})
|
{formatNumber(MAX_MEMORY_CHARACTER_COUNT)})
|
||||||
<Text dimColor> · /memory to edit</Text>
|
<Text dimColor> · /memory to edit</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</>;
|
</>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const claudeAiSubscriberExternalTokenNotice: StatusNoticeDefinition = {
|
const claudeAiSubscriberExternalTokenNotice: StatusNoticeDefinition = {
|
||||||
id: 'claude-ai-external-token',
|
id: 'claude-ai-external-token',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
isActive: () => {
|
isActive: () => {
|
||||||
const authTokenInfo = getAuthTokenSource();
|
const authTokenInfo = getAuthTokenSource()
|
||||||
return isClaudeAISubscriber() && (authTokenInfo.source === 'ANTHROPIC_AUTH_TOKEN' || authTokenInfo.source === 'apiKeyHelper');
|
return (
|
||||||
|
isClaudeAISubscriber() &&
|
||||||
|
(authTokenInfo.source === 'ANTHROPIC_AUTH_TOKEN' ||
|
||||||
|
authTokenInfo.source === 'apiKeyHelper')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
const authTokenInfo = getAuthTokenSource();
|
const authTokenInfo = getAuthTokenSource()
|
||||||
return <Box flexDirection="row" marginTop={1}>
|
return (
|
||||||
|
<Box flexDirection="row" marginTop={1}>
|
||||||
<Text color="warning">{figures.warning}</Text>
|
<Text color="warning">{figures.warning}</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
Auth conflict: Using {authTokenInfo.source} instead of Claude account
|
Auth conflict: Using {authTokenInfo.source} instead of Claude account
|
||||||
subscription token. Either unset {authTokenInfo.source}, or run
|
subscription token. Either unset {authTokenInfo.source}, or run
|
||||||
`claude /logout`.
|
`claude /logout`.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const apiKeyConflictNotice: StatusNoticeDefinition = {
|
const apiKeyConflictNotice: StatusNoticeDefinition = {
|
||||||
id: 'api-key-conflict',
|
id: 'api-key-conflict',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
isActive: () => {
|
isActive: () => {
|
||||||
const {
|
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
|
||||||
source: apiKeySource
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
} = getAnthropicApiKeyWithSource({
|
})
|
||||||
skipRetrievingKeyFromApiKeyHelper: true
|
return (
|
||||||
});
|
!!getApiKeyFromConfigOrMacOSKeychain() &&
|
||||||
return !!getApiKeyFromConfigOrMacOSKeychain() && (apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper');
|
(apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
const {
|
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
|
||||||
source: apiKeySource
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
} = getAnthropicApiKeyWithSource({
|
})
|
||||||
skipRetrievingKeyFromApiKeyHelper: true
|
return (
|
||||||
});
|
<Box flexDirection="row" marginTop={1}>
|
||||||
return <Box flexDirection="row" marginTop={1}>
|
|
||||||
<Text color="warning">{figures.warning}</Text>
|
<Text color="warning">{figures.warning}</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
Auth conflict: Using {apiKeySource} instead of Anthropic Console key.
|
Auth conflict: Using {apiKeySource} instead of Anthropic Console key.
|
||||||
Either unset {apiKeySource}, or run `claude /logout`.
|
Either unset {apiKeySource}, or run `claude /logout`.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const bothAuthMethodsNotice: StatusNoticeDefinition = {
|
const bothAuthMethodsNotice: StatusNoticeDefinition = {
|
||||||
id: 'both-auth-methods',
|
id: 'both-auth-methods',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
isActive: () => {
|
isActive: () => {
|
||||||
const {
|
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
|
||||||
source: apiKeySource
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
} = getAnthropicApiKeyWithSource({
|
})
|
||||||
skipRetrievingKeyFromApiKeyHelper: true
|
const authTokenInfo = getAuthTokenSource()
|
||||||
});
|
return (
|
||||||
const authTokenInfo = getAuthTokenSource();
|
apiKeySource !== 'none' &&
|
||||||
return apiKeySource !== 'none' && authTokenInfo.source !== 'none' && !(apiKeySource === 'apiKeyHelper' && authTokenInfo.source === 'apiKeyHelper');
|
authTokenInfo.source !== 'none' &&
|
||||||
|
!(
|
||||||
|
apiKeySource === 'apiKeyHelper' &&
|
||||||
|
authTokenInfo.source === 'apiKeyHelper'
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
const {
|
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
|
||||||
source: apiKeySource
|
skipRetrievingKeyFromApiKeyHelper: true,
|
||||||
} = getAnthropicApiKeyWithSource({
|
})
|
||||||
skipRetrievingKeyFromApiKeyHelper: true
|
const authTokenInfo = getAuthTokenSource()
|
||||||
});
|
return (
|
||||||
const authTokenInfo = getAuthTokenSource();
|
<Box flexDirection="column" marginTop={1}>
|
||||||
return <Box flexDirection="column" marginTop={1}>
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Text color="warning">{figures.warning}</Text>
|
<Text color="warning">{figures.warning}</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
@@ -125,28 +164,43 @@ const bothAuthMethodsNotice: StatusNoticeDefinition = {
|
|||||||
<Box flexDirection="column" marginLeft={3}>
|
<Box flexDirection="column" marginLeft={3}>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
· Trying to use{' '}
|
· Trying to use{' '}
|
||||||
{authTokenInfo.source === 'claude.ai' ? 'claude.ai' : authTokenInfo.source}
|
{authTokenInfo.source === 'claude.ai'
|
||||||
|
? 'claude.ai'
|
||||||
|
: authTokenInfo.source}
|
||||||
?{' '}
|
?{' '}
|
||||||
{apiKeySource === 'ANTHROPIC_API_KEY' ? 'Unset the ANTHROPIC_API_KEY environment variable, or claude /logout then say "No" to the API key approval before login.' : apiKeySource === 'apiKeyHelper' ? 'Unset the apiKeyHelper setting.' : 'claude /logout'}
|
{apiKeySource === 'ANTHROPIC_API_KEY'
|
||||||
|
? 'Unset the ANTHROPIC_API_KEY environment variable, or claude /logout then say "No" to the API key approval before login.'
|
||||||
|
: apiKeySource === 'apiKeyHelper'
|
||||||
|
? 'Unset the apiKeyHelper setting.'
|
||||||
|
: 'claude /logout'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
· Trying to use {apiKeySource}?{' '}
|
· Trying to use {apiKeySource}?{' '}
|
||||||
{authTokenInfo.source === 'claude.ai' ? 'claude /logout to sign out of claude.ai.' : `Unset the ${authTokenInfo.source} environment variable.`}
|
{authTokenInfo.source === 'claude.ai'
|
||||||
|
? 'claude /logout to sign out of claude.ai.'
|
||||||
|
: `Unset the ${authTokenInfo.source} environment variable.`}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const largeAgentDescriptionsNotice: StatusNoticeDefinition = {
|
const largeAgentDescriptionsNotice: StatusNoticeDefinition = {
|
||||||
id: 'large-agent-descriptions',
|
id: 'large-agent-descriptions',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
isActive: context => {
|
isActive: context => {
|
||||||
const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions);
|
const totalTokens = getAgentDescriptionsTotalTokens(
|
||||||
return totalTokens > AGENT_DESCRIPTIONS_THRESHOLD;
|
context.agentDefinitions,
|
||||||
|
)
|
||||||
|
return totalTokens > AGENT_DESCRIPTIONS_THRESHOLD
|
||||||
},
|
},
|
||||||
render: context => {
|
render: context => {
|
||||||
const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions);
|
const totalTokens = getAgentDescriptionsTotalTokens(
|
||||||
return <Box flexDirection="row">
|
context.agentDefinitions,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row">
|
||||||
<Text color="warning">{figures.warning}</Text>
|
<Text color="warning">{figures.warning}</Text>
|
||||||
<Text color="warning">
|
<Text color="warning">
|
||||||
Large cumulative agent descriptions will impact performance (~
|
Large cumulative agent descriptions will impact performance (~
|
||||||
@@ -154,44 +208,58 @@ const largeAgentDescriptionsNotice: StatusNoticeDefinition = {
|
|||||||
{formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)})
|
{formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)})
|
||||||
<Text dimColor> · /agents to manage</Text>
|
<Text dimColor> · /agents to manage</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const jetbrainsPluginNotice: StatusNoticeDefinition = {
|
const jetbrainsPluginNotice: StatusNoticeDefinition = {
|
||||||
id: 'jetbrains-plugin-install',
|
id: 'jetbrains-plugin-install',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
isActive: context => {
|
isActive: context => {
|
||||||
// Only show if running in JetBrains built-in terminal
|
// Only show if running in JetBrains built-in terminal
|
||||||
if (!isSupportedJetBrainsTerminal()) {
|
if (!isSupportedJetBrainsTerminal()) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
// Don't show if auto-install is disabled
|
// Don't show if auto-install is disabled
|
||||||
const shouldAutoInstall = context.config.autoInstallIdeExtension ?? true;
|
const shouldAutoInstall = context.config.autoInstallIdeExtension ?? true
|
||||||
if (!shouldAutoInstall) {
|
if (!shouldAutoInstall) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
// Check if plugin is already installed (cached to avoid repeated filesystem checks)
|
// Check if plugin is already installed (cached to avoid repeated filesystem checks)
|
||||||
const ideType = getTerminalIdeType();
|
const ideType = getTerminalIdeType()
|
||||||
return ideType !== null && !isJetBrainsPluginInstalledCachedSync(ideType);
|
return ideType !== null && !isJetBrainsPluginInstalledCachedSync(ideType)
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
const ideType = getTerminalIdeType();
|
const ideType = getTerminalIdeType()
|
||||||
const ideName = toIDEDisplayName(ideType);
|
const ideName = toIDEDisplayName(ideType)
|
||||||
return <Box flexDirection="row" gap={1} marginLeft={1}>
|
return (
|
||||||
|
<Box flexDirection="row" gap={1} marginLeft={1}>
|
||||||
<Text color="ide">{figures.arrowUp}</Text>
|
<Text color="ide">{figures.arrowUp}</Text>
|
||||||
<Text>
|
<Text>
|
||||||
Install the <Text color="ide">{ideName}</Text> plugin from the
|
Install the <Text color="ide">{ideName}</Text> plugin from the
|
||||||
JetBrains Marketplace:{' '}
|
JetBrains Marketplace:{' '}
|
||||||
<Text bold>https://docs.claude.com/s/claude-code-jetbrains</Text>
|
<Text bold>https://docs.claude.com/s/claude-code-jetbrains</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// All notice definitions
|
// All notice definitions
|
||||||
export const statusNoticeDefinitions: StatusNoticeDefinition[] = [largeMemoryFilesNotice, largeAgentDescriptionsNotice, claudeAiSubscriberExternalTokenNotice, apiKeyConflictNotice, bothAuthMethodsNotice, jetbrainsPluginNotice];
|
export const statusNoticeDefinitions: StatusNoticeDefinition[] = [
|
||||||
|
largeMemoryFilesNotice,
|
||||||
|
largeAgentDescriptionsNotice,
|
||||||
|
claudeAiSubscriberExternalTokenNotice,
|
||||||
|
apiKeyConflictNotice,
|
||||||
|
bothAuthMethodsNotice,
|
||||||
|
jetbrainsPluginNotice,
|
||||||
|
]
|
||||||
|
|
||||||
// Helper functions for external use
|
// Helper functions for external use
|
||||||
export function getActiveNotices(context: StatusNoticeContext): StatusNoticeDefinition[] {
|
export function getActiveNotices(
|
||||||
return statusNoticeDefinitions.filter(notice => notice.isActive(context));
|
context: StatusNoticeContext,
|
||||||
|
): StatusNoticeDefinition[] {
|
||||||
|
return statusNoticeDefinitions.filter(notice => notice.isActive(context))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,379 +1,377 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
import { type OptionWithDescription, Select } from '../../components/CustomSelect/index.js';
|
type OptionWithDescription,
|
||||||
import { Pane } from '../../components/design-system/Pane.js';
|
Select,
|
||||||
import { Spinner } from '../../components/Spinner.js';
|
} from '../../components/CustomSelect/index.js'
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
import { Pane } from '../../components/design-system/Pane.js'
|
||||||
|
import { Spinner } from '../../components/Spinner.js'
|
||||||
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to proceed through setup steps
|
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to proceed through setup steps
|
||||||
import { Box, Text, useInput } from '../../ink.js';
|
import { Box, Text, useInput } from '../../ink.js'
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import { detectPythonPackageManager, getPythonApiInstructions, installIt2, markIt2SetupComplete, type PythonPackageManager, setPreferTmuxOverIterm2, verifyIt2Setup } from './backends/it2Setup.js';
|
import {
|
||||||
type SetupStep = 'initial' | 'installing' | 'install-failed' | 'verify-api' | 'api-instructions' | 'verifying' | 'success' | 'failed';
|
detectPythonPackageManager,
|
||||||
|
getPythonApiInstructions,
|
||||||
|
installIt2,
|
||||||
|
markIt2SetupComplete,
|
||||||
|
type PythonPackageManager,
|
||||||
|
setPreferTmuxOverIterm2,
|
||||||
|
verifyIt2Setup,
|
||||||
|
} from './backends/it2Setup.js'
|
||||||
|
|
||||||
|
type SetupStep =
|
||||||
|
| 'initial'
|
||||||
|
| 'installing'
|
||||||
|
| 'install-failed'
|
||||||
|
| 'verify-api'
|
||||||
|
| 'api-instructions'
|
||||||
|
| 'verifying'
|
||||||
|
| 'success'
|
||||||
|
| 'failed'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDone: (result: 'installed' | 'use-tmux' | 'cancelled') => void;
|
onDone: (result: 'installed' | 'use-tmux' | 'cancelled') => void
|
||||||
tmuxAvailable: boolean;
|
tmuxAvailable: boolean
|
||||||
};
|
}
|
||||||
export function It2SetupPrompt(t0) {
|
|
||||||
const $ = _c(44);
|
export function It2SetupPrompt({
|
||||||
const {
|
|
||||||
onDone,
|
onDone,
|
||||||
tmuxAvailable
|
tmuxAvailable,
|
||||||
} = t0;
|
}: Props): React.ReactNode {
|
||||||
const [step, setStep] = useState("initial");
|
const [step, setStep] = useState<SetupStep>('initial')
|
||||||
const [packageManager, setPackageManager] = useState(null);
|
const [packageManager, setPackageManager] =
|
||||||
const [error, setError] = useState(null);
|
useState<PythonPackageManager | null>(null)
|
||||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
const [error, setError] = useState<string | null>(null)
|
||||||
let t1;
|
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||||
let t2;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
// Detect package manager on mount
|
||||||
t1 = () => {
|
useEffect(() => {
|
||||||
detectPythonPackageManager().then(pm => {
|
void detectPythonPackageManager().then(pm => {
|
||||||
setPackageManager(pm);
|
setPackageManager(pm)
|
||||||
});
|
})
|
||||||
};
|
}, [])
|
||||||
t2 = [];
|
|
||||||
$[0] = t1;
|
const handleCancel = useCallback(() => {
|
||||||
$[1] = t2;
|
onDone('cancelled')
|
||||||
} else {
|
}, [onDone])
|
||||||
t1 = $[0];
|
|
||||||
t2 = $[1];
|
useKeybinding('confirm:no', handleCancel, {
|
||||||
}
|
context: 'Confirmation',
|
||||||
useEffect(t1, t2);
|
isActive: step !== 'installing' && step !== 'verifying',
|
||||||
let t3;
|
})
|
||||||
if ($[2] !== onDone) {
|
|
||||||
t3 = () => {
|
// Handle keyboard input for verification step
|
||||||
onDone("cancelled");
|
useInput((_input, key) => {
|
||||||
};
|
if (step === 'api-instructions' && key.return) {
|
||||||
$[2] = onDone;
|
setStep('verifying')
|
||||||
$[3] = t3;
|
void verifyIt2Setup().then(result => {
|
||||||
} else {
|
|
||||||
t3 = $[3];
|
|
||||||
}
|
|
||||||
const handleCancel = t3;
|
|
||||||
const t4 = step !== "installing" && step !== "verifying";
|
|
||||||
let t5;
|
|
||||||
if ($[4] !== t4) {
|
|
||||||
t5 = {
|
|
||||||
context: "Confirmation",
|
|
||||||
isActive: t4
|
|
||||||
};
|
|
||||||
$[4] = t4;
|
|
||||||
$[5] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[5];
|
|
||||||
}
|
|
||||||
useKeybinding("confirm:no", handleCancel, t5);
|
|
||||||
let t6;
|
|
||||||
if ($[6] !== onDone || $[7] !== step) {
|
|
||||||
t6 = (_input, key) => {
|
|
||||||
if (step === "api-instructions" && key.return) {
|
|
||||||
setStep("verifying");
|
|
||||||
verifyIt2Setup().then(result => {
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
markIt2SetupComplete();
|
markIt2SetupComplete()
|
||||||
setStep("success");
|
setStep('success')
|
||||||
setTimeout(onDone, 1500, "installed" as const);
|
setTimeout(onDone, 1500, 'installed' as const)
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || "Verification failed");
|
setError(result.error || 'Verification failed')
|
||||||
setStep("failed");
|
setStep('failed')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
$[6] = onDone;
|
|
||||||
$[7] = step;
|
// Handle installation
|
||||||
$[8] = t6;
|
async function handleInstall(): Promise<void> {
|
||||||
} else {
|
|
||||||
t6 = $[8];
|
|
||||||
}
|
|
||||||
useInput(t6);
|
|
||||||
let t7;
|
|
||||||
if ($[9] !== packageManager) {
|
|
||||||
t7 = async function handleInstall() {
|
|
||||||
if (!packageManager) {
|
if (!packageManager) {
|
||||||
setError("No Python package manager found (uvx, pipx, or pip)");
|
setError('No Python package manager found (uvx, pipx, or pip)')
|
||||||
setStep("failed");
|
setStep('failed')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setStep("installing");
|
|
||||||
const result_0 = await installIt2(packageManager);
|
setStep('installing')
|
||||||
if (result_0.success) {
|
const result = await installIt2(packageManager)
|
||||||
setStep("api-instructions");
|
|
||||||
|
if (result.success) {
|
||||||
|
// Show Python API instructions
|
||||||
|
setStep('api-instructions')
|
||||||
} else {
|
} else {
|
||||||
setError(result_0.error || "Installation failed");
|
setError(result.error || 'Installation failed')
|
||||||
setStep("install-failed");
|
setStep('install-failed')
|
||||||
}
|
}
|
||||||
};
|
|
||||||
$[9] = packageManager;
|
|
||||||
$[10] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[10];
|
|
||||||
}
|
}
|
||||||
const handleInstall = t7;
|
|
||||||
let t8;
|
// Handle using tmux instead
|
||||||
if ($[11] !== onDone) {
|
function handleUseTmux(): void {
|
||||||
t8 = function handleUseTmux() {
|
setPreferTmuxOverIterm2(true)
|
||||||
setPreferTmuxOverIterm2(true);
|
onDone('use-tmux')
|
||||||
onDone("use-tmux");
|
|
||||||
};
|
|
||||||
$[11] = onDone;
|
|
||||||
$[12] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[12];
|
|
||||||
}
|
}
|
||||||
const handleUseTmux = t8;
|
|
||||||
let T0;
|
// Render based on current step
|
||||||
let T1;
|
const renderContent = (): React.ReactNode => {
|
||||||
let t10;
|
|
||||||
let t11;
|
|
||||||
let t12;
|
|
||||||
let t13;
|
|
||||||
let t14;
|
|
||||||
let t9;
|
|
||||||
if ($[13] !== error || $[14] !== handleInstall || $[15] !== handleUseTmux || $[16] !== onDone || $[17] !== packageManager || $[18] !== step || $[19] !== tmuxAvailable) {
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case "initial":
|
case 'initial':
|
||||||
{
|
return renderInitialPrompt()
|
||||||
return renderInitialPrompt();
|
case 'installing':
|
||||||
}
|
return renderInstalling()
|
||||||
case "installing":
|
case 'install-failed':
|
||||||
{
|
return renderInstallFailed()
|
||||||
return renderInstalling();
|
case 'api-instructions':
|
||||||
}
|
return renderApiInstructions()
|
||||||
case "install-failed":
|
case 'verifying':
|
||||||
{
|
return renderVerifying()
|
||||||
return renderInstallFailed();
|
case 'success':
|
||||||
}
|
return renderSuccess()
|
||||||
case "api-instructions":
|
case 'failed':
|
||||||
{
|
return renderFailed()
|
||||||
return renderApiInstructions();
|
|
||||||
}
|
|
||||||
case "verifying":
|
|
||||||
{
|
|
||||||
return renderVerifying();
|
|
||||||
}
|
|
||||||
case "success":
|
|
||||||
{
|
|
||||||
return renderSuccess();
|
|
||||||
}
|
|
||||||
case "failed":
|
|
||||||
{
|
|
||||||
return renderFailed();
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInitialPrompt(): React.ReactNode {
|
||||||
|
const options: OptionWithDescription<string>[] = [
|
||||||
{
|
{
|
||||||
return null;
|
label: 'Install it2 now',
|
||||||
}
|
value: 'install',
|
||||||
}
|
description: packageManager
|
||||||
};
|
? `Uses ${packageManager} to install the it2 CLI tool`
|
||||||
function renderInitialPrompt() {
|
: 'Requires Python (uvx, pipx, or pip)',
|
||||||
const options = [{
|
},
|
||||||
label: "Install it2 now",
|
]
|
||||||
value: "install",
|
|
||||||
description: packageManager ? `Uses ${packageManager} to install the it2 CLI tool` : "Requires Python (uvx, pipx, or pip)"
|
|
||||||
}];
|
|
||||||
if (tmuxAvailable) {
|
if (tmuxAvailable) {
|
||||||
options.push({
|
options.push({
|
||||||
label: "Use tmux instead",
|
label: 'Use tmux instead',
|
||||||
value: "tmux",
|
value: 'tmux',
|
||||||
description: "Opens teammates in a separate tmux session"
|
description: 'Opens teammates in a separate tmux session',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
label: "Cancel",
|
label: 'Cancel',
|
||||||
value: "cancel",
|
value: 'cancel',
|
||||||
description: "Skip teammate spawning for now"
|
description: 'Skip teammate spawning for now',
|
||||||
});
|
})
|
||||||
return <Box flexDirection="column" gap={1}><Text>To use native iTerm2 split panes for teammates, you need the{" "}<Text bold={true}>it2</Text> CLI tool.</Text><Text dimColor={true}>This enables teammates to appear as split panes within your current window.</Text><Box marginTop={1}><Select options={options} onChange={value => {
|
|
||||||
bb61: switch (value) {
|
return (
|
||||||
case "install":
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text>
|
||||||
|
To use native iTerm2 split panes for teammates, you need the{' '}
|
||||||
|
<Text bold>it2</Text> CLI tool.
|
||||||
|
</Text>
|
||||||
|
<Text dimColor>
|
||||||
|
This enables teammates to appear as split panes within your current
|
||||||
|
window.
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
onChange={value => {
|
||||||
|
switch (value) {
|
||||||
|
case 'install':
|
||||||
|
void handleInstall()
|
||||||
|
break
|
||||||
|
case 'tmux':
|
||||||
|
handleUseTmux()
|
||||||
|
break
|
||||||
|
case 'cancel':
|
||||||
|
onDone('cancelled')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => onDone('cancelled')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstalling(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Box>
|
||||||
|
<Spinner />
|
||||||
|
<Text> Installing it2 using {packageManager}…</Text>
|
||||||
|
</Box>
|
||||||
|
<Text dimColor>This may take a moment.</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstallFailed(): React.ReactNode {
|
||||||
|
const options: OptionWithDescription<string>[] = [
|
||||||
{
|
{
|
||||||
handleInstall();
|
label: 'Try again',
|
||||||
break bb61;
|
value: 'retry',
|
||||||
}
|
description: 'Retry the installation',
|
||||||
case "tmux":
|
},
|
||||||
{
|
]
|
||||||
handleUseTmux();
|
|
||||||
break bb61;
|
|
||||||
}
|
|
||||||
case "cancel":
|
|
||||||
{
|
|
||||||
onDone("cancelled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}} onCancel={() => onDone("cancelled")} /></Box></Box>;
|
|
||||||
}
|
|
||||||
function renderInstalling() {
|
|
||||||
return <Box flexDirection="column" gap={1}><Box><Spinner /><Text> Installing it2 using {packageManager}…</Text></Box><Text dimColor={true}>This may take a moment.</Text></Box>;
|
|
||||||
}
|
|
||||||
function renderInstallFailed() {
|
|
||||||
const options_0 = [{
|
|
||||||
label: "Try again",
|
|
||||||
value: "retry",
|
|
||||||
description: "Retry the installation"
|
|
||||||
}];
|
|
||||||
if (tmuxAvailable) {
|
if (tmuxAvailable) {
|
||||||
options_0.push({
|
options.push({
|
||||||
label: "Use tmux instead",
|
label: 'Use tmux instead',
|
||||||
value: "tmux",
|
value: 'tmux',
|
||||||
description: "Falls back to tmux for teammate panes"
|
description: 'Falls back to tmux for teammate panes',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
options_0.push({
|
|
||||||
label: "Cancel",
|
options.push({
|
||||||
value: "cancel",
|
label: 'Cancel',
|
||||||
description: "Skip teammate spawning for now"
|
value: 'cancel',
|
||||||
});
|
description: 'Skip teammate spawning for now',
|
||||||
return <Box flexDirection="column" gap={1}><Text color="error">Installation failed</Text>{error && <Text dimColor={true}>{error}</Text>}<Text dimColor={true}>You can try installing manually:{" "}{packageManager === "uvx" ? "uv tool install it2" : packageManager === "pipx" ? "pipx install it2" : "pip install --user it2"}</Text><Box marginTop={1}><Select options={options_0} onChange={value_0 => {
|
})
|
||||||
bb89: switch (value_0) {
|
|
||||||
case "retry":
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="error">Installation failed</Text>
|
||||||
|
{error && <Text dimColor>{error}</Text>}
|
||||||
|
<Text dimColor>
|
||||||
|
You can try installing manually:{' '}
|
||||||
|
{packageManager === 'uvx'
|
||||||
|
? 'uv tool install it2'
|
||||||
|
: packageManager === 'pipx'
|
||||||
|
? 'pipx install it2'
|
||||||
|
: 'pip install --user it2'}
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
onChange={value => {
|
||||||
|
switch (value) {
|
||||||
|
case 'retry':
|
||||||
|
void handleInstall()
|
||||||
|
break
|
||||||
|
case 'tmux':
|
||||||
|
handleUseTmux()
|
||||||
|
break
|
||||||
|
case 'cancel':
|
||||||
|
onDone('cancelled')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => onDone('cancelled')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderApiInstructions(): React.ReactNode {
|
||||||
|
const instructions = getPythonApiInstructions()
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="success">✓ it2 installed successfully</Text>
|
||||||
|
<Box flexDirection="column" marginTop={1}>
|
||||||
|
{instructions.map((line, i) => (
|
||||||
|
<Text key={i}>{line}</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text dimColor>Press Enter when ready to verify…</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVerifying(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Spinner />
|
||||||
|
<Text> Verifying it2 can communicate with iTerm2…</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuccess(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="success">✓ iTerm2 split pane support is ready</Text>
|
||||||
|
<Text dimColor>Teammates will now appear as split panes.</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFailed(): React.ReactNode {
|
||||||
|
const options: OptionWithDescription<string>[] = [
|
||||||
{
|
{
|
||||||
handleInstall();
|
label: 'Try again',
|
||||||
break bb89;
|
value: 'retry',
|
||||||
}
|
description: 'Verify the connection again',
|
||||||
case "tmux":
|
},
|
||||||
{
|
]
|
||||||
handleUseTmux();
|
|
||||||
break bb89;
|
|
||||||
}
|
|
||||||
case "cancel":
|
|
||||||
{
|
|
||||||
onDone("cancelled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}} onCancel={() => onDone("cancelled")} /></Box></Box>;
|
|
||||||
}
|
|
||||||
function renderApiInstructions() {
|
|
||||||
const instructions = getPythonApiInstructions();
|
|
||||||
return <Box flexDirection="column" gap={1}><Text color="success">✓ it2 installed successfully</Text><Box flexDirection="column" marginTop={1}>{instructions.map(_temp)}</Box><Box marginTop={1}><Text dimColor={true}>Press Enter when ready to verify…</Text></Box></Box>;
|
|
||||||
}
|
|
||||||
function renderVerifying() {
|
|
||||||
return <Box><Spinner /><Text> Verifying it2 can communicate with iTerm2…</Text></Box>;
|
|
||||||
}
|
|
||||||
function renderSuccess() {
|
|
||||||
return <Box flexDirection="column"><Text color="success">✓ iTerm2 split pane support is ready</Text><Text dimColor={true}>Teammates will now appear as split panes.</Text></Box>;
|
|
||||||
}
|
|
||||||
function renderFailed() {
|
|
||||||
const options_1 = [{
|
|
||||||
label: "Try again",
|
|
||||||
value: "retry",
|
|
||||||
description: "Verify the connection again"
|
|
||||||
}];
|
|
||||||
if (tmuxAvailable) {
|
if (tmuxAvailable) {
|
||||||
options_1.push({
|
options.push({
|
||||||
label: "Use tmux instead",
|
label: 'Use tmux instead',
|
||||||
value: "tmux",
|
value: 'tmux',
|
||||||
description: "Falls back to tmux for teammate panes"
|
description: 'Falls back to tmux for teammate panes',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
options_1.push({
|
|
||||||
label: "Cancel",
|
options.push({
|
||||||
value: "cancel",
|
label: 'Cancel',
|
||||||
description: "Skip teammate spawning for now"
|
value: 'cancel',
|
||||||
});
|
description: 'Skip teammate spawning for now',
|
||||||
return <Box flexDirection="column" gap={1}><Text color="error">Verification failed</Text>{error && <Text dimColor={true}>{error}</Text>}<Text>Make sure:</Text><Box flexDirection="column" paddingLeft={2}><Text>· Python API is enabled in iTerm2 preferences</Text><Text>· You may need to restart iTerm2 after enabling</Text></Box><Box marginTop={1}><Select options={options_1} onChange={value_1 => {
|
})
|
||||||
bb115: switch (value_1) {
|
|
||||||
case "retry":
|
return (
|
||||||
{
|
<Box flexDirection="column" gap={1}>
|
||||||
setStep("verifying");
|
<Text color="error">Verification failed</Text>
|
||||||
verifyIt2Setup().then(result_1 => {
|
{error && <Text dimColor>{error}</Text>}
|
||||||
if (result_1.success) {
|
<Text>Make sure:</Text>
|
||||||
markIt2SetupComplete();
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
setStep("success");
|
<Text>· Python API is enabled in iTerm2 preferences</Text>
|
||||||
setTimeout(onDone, 1500, "installed" as const);
|
<Text>· You may need to restart iTerm2 after enabling</Text>
|
||||||
|
</Box>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
onChange={value => {
|
||||||
|
switch (value) {
|
||||||
|
case 'retry':
|
||||||
|
setStep('verifying')
|
||||||
|
void verifyIt2Setup().then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
markIt2SetupComplete()
|
||||||
|
setStep('success')
|
||||||
|
setTimeout(onDone, 1500, 'installed' as const)
|
||||||
} else {
|
} else {
|
||||||
setError(result_1.error || "Verification failed");
|
setError(result.error || 'Verification failed')
|
||||||
setStep("failed");
|
setStep('failed')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
break bb115;
|
break
|
||||||
|
case 'tmux':
|
||||||
|
handleUseTmux()
|
||||||
|
break
|
||||||
|
case 'cancel':
|
||||||
|
onDone('cancelled')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "tmux":
|
}}
|
||||||
{
|
onCancel={() => onDone('cancelled')}
|
||||||
handleUseTmux();
|
/>
|
||||||
break bb115;
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case "cancel":
|
|
||||||
{
|
return (
|
||||||
onDone("cancelled");
|
<Pane color="permission">
|
||||||
}
|
<Box flexDirection="column" gap={1} paddingBottom={1}>
|
||||||
}
|
<Text bold color="permission">
|
||||||
}} onCancel={() => onDone("cancelled")} /></Box></Box>;
|
iTerm2 Split Pane Setup
|
||||||
}
|
</Text>
|
||||||
T1 = Pane;
|
{renderContent()}
|
||||||
t14 = "permission";
|
{step !== 'installing' &&
|
||||||
T0 = Box;
|
step !== 'verifying' &&
|
||||||
t9 = "column";
|
step !== 'success' && (
|
||||||
t10 = 1;
|
<Text dimColor italic>
|
||||||
t11 = 1;
|
{exitState.pending ? (
|
||||||
if ($[28] === Symbol.for("react.memo_cache_sentinel")) {
|
<>Press {exitState.keyName} again to exit</>
|
||||||
t12 = <Text bold={true} color="permission">iTerm2 Split Pane Setup</Text>;
|
) : (
|
||||||
$[28] = t12;
|
<>Esc to cancel</>
|
||||||
} else {
|
)}
|
||||||
t12 = $[28];
|
</Text>
|
||||||
}
|
)}
|
||||||
t13 = renderContent();
|
</Box>
|
||||||
$[13] = error;
|
</Pane>
|
||||||
$[14] = handleInstall;
|
)
|
||||||
$[15] = handleUseTmux;
|
|
||||||
$[16] = onDone;
|
|
||||||
$[17] = packageManager;
|
|
||||||
$[18] = step;
|
|
||||||
$[19] = tmuxAvailable;
|
|
||||||
$[20] = T0;
|
|
||||||
$[21] = T1;
|
|
||||||
$[22] = t10;
|
|
||||||
$[23] = t11;
|
|
||||||
$[24] = t12;
|
|
||||||
$[25] = t13;
|
|
||||||
$[26] = t14;
|
|
||||||
$[27] = t9;
|
|
||||||
} else {
|
|
||||||
T0 = $[20];
|
|
||||||
T1 = $[21];
|
|
||||||
t10 = $[22];
|
|
||||||
t11 = $[23];
|
|
||||||
t12 = $[24];
|
|
||||||
t13 = $[25];
|
|
||||||
t14 = $[26];
|
|
||||||
t9 = $[27];
|
|
||||||
}
|
|
||||||
let t15;
|
|
||||||
if ($[29] !== exitState || $[30] !== step) {
|
|
||||||
t15 = step !== "installing" && step !== "verifying" && step !== "success" && <Text dimColor={true} italic={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Esc to cancel</>}</Text>;
|
|
||||||
$[29] = exitState;
|
|
||||||
$[30] = step;
|
|
||||||
$[31] = t15;
|
|
||||||
} else {
|
|
||||||
t15 = $[31];
|
|
||||||
}
|
|
||||||
let t16;
|
|
||||||
if ($[32] !== T0 || $[33] !== t10 || $[34] !== t11 || $[35] !== t12 || $[36] !== t13 || $[37] !== t15 || $[38] !== t9) {
|
|
||||||
t16 = <T0 flexDirection={t9} gap={t10} paddingBottom={t11}>{t12}{t13}{t15}</T0>;
|
|
||||||
$[32] = T0;
|
|
||||||
$[33] = t10;
|
|
||||||
$[34] = t11;
|
|
||||||
$[35] = t12;
|
|
||||||
$[36] = t13;
|
|
||||||
$[37] = t15;
|
|
||||||
$[38] = t9;
|
|
||||||
$[39] = t16;
|
|
||||||
} else {
|
|
||||||
t16 = $[39];
|
|
||||||
}
|
|
||||||
let t17;
|
|
||||||
if ($[40] !== T1 || $[41] !== t14 || $[42] !== t16) {
|
|
||||||
t17 = <T1 color={t14}>{t16}</T1>;
|
|
||||||
$[40] = T1;
|
|
||||||
$[41] = t14;
|
|
||||||
$[42] = t16;
|
|
||||||
$[43] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[43];
|
|
||||||
}
|
|
||||||
return t17;
|
|
||||||
}
|
|
||||||
function _temp(line, i) {
|
|
||||||
return <Text key={i}>{line}</Text>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user