fix: 退出启动对话框时终端残留一行内容
gracefulShutdownSync 启动异步 shutdown 后同步返回,React 立即 重新渲染组件,与 cleanupTerminalModes() 中的 Ink unmount 产生 竞态条件,导致退出后终端残留对话框内容。 修复方案:引入 pendingExitCode state,退出路径先清空画面 (渲染 null),在 useEffect 中延迟到下一个 tick 再调用 gracefulShutdownSync,确保 Ink 在终端清理前已完成空帧刷新。 影响三个启动对话框:TrustDialog、BypassPermissionsModeDialog、 DevChannelsDialog。 Co-Authored-By: glm-5.1 <zai-org@claude-code-best.win>
This commit is contained in:
parent
75c6a5e22e
commit
2d4b94ab8e
|
|
@ -1,61 +1,70 @@
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react';
|
||||||
import { logEvent } from 'src/services/analytics/index.js'
|
import { logEvent } from 'src/services/analytics/index.js';
|
||||||
import { Box, Link, Newline, Text } from '@anthropic/ink'
|
import { Box, Link, Newline, Text } from '@anthropic/ink';
|
||||||
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
|
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
|
||||||
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
import { updateSettingsForSource } from '../utils/settings/settings.js';
|
||||||
import { Select } from './CustomSelect/index.js'
|
import { Select } from './CustomSelect/index.js';
|
||||||
import { Dialog } from '@anthropic/ink'
|
import { Dialog } from '@anthropic/ink';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onAccept(): void
|
onAccept(): void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function BypassPermissionsModeDialog({
|
export function BypassPermissionsModeDialog({ onAccept }: Props): React.ReactNode {
|
||||||
onAccept,
|
const [pendingExitCode, setPendingExitCode] = React.useState<number | null>(null);
|
||||||
}: Props): React.ReactNode {
|
|
||||||
|
// Clear screen before shutdown so residual dialog content doesn't leak
|
||||||
|
// to the terminal. Deferred to next tick so Ink flushes the null render.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
logEvent('tengu_bypass_permissions_mode_dialog_shown', {})
|
if (pendingExitCode !== null) {
|
||||||
}, [])
|
const code = pendingExitCode;
|
||||||
|
const timer = setTimeout(() => gracefulShutdownSync(code));
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [pendingExitCode]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
logEvent('tengu_bypass_permissions_mode_dialog_shown', {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
function onChange(value: 'accept' | 'decline') {
|
function onChange(value: 'accept' | 'decline') {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'accept': {
|
case 'accept': {
|
||||||
logEvent('tengu_bypass_permissions_mode_dialog_accept', {})
|
logEvent('tengu_bypass_permissions_mode_dialog_accept', {});
|
||||||
|
|
||||||
updateSettingsForSource('userSettings', {
|
updateSettingsForSource('userSettings', {
|
||||||
skipDangerousModePermissionPrompt: true,
|
skipDangerousModePermissionPrompt: true,
|
||||||
})
|
});
|
||||||
onAccept()
|
onAccept();
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
case 'decline': {
|
case 'decline': {
|
||||||
gracefulShutdownSync(1)
|
setPendingExitCode(1);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
gracefulShutdownSync(0)
|
setPendingExitCode(0);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
if (pendingExitCode !== null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog title="WARNING: Claude Code running in Bypass Permissions mode" color="error" onCancel={handleEscape}>
|
||||||
title="WARNING: Claude Code running in Bypass Permissions mode"
|
|
||||||
color="error"
|
|
||||||
onCancel={handleEscape}
|
|
||||||
>
|
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text>
|
<Text>
|
||||||
In Bypass Permissions mode, Claude Code will not ask for your approval
|
In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous
|
||||||
before running potentially dangerous commands.
|
commands.
|
||||||
<Newline />
|
<Newline />
|
||||||
This mode should only be used in a sandboxed container/VM that has
|
This mode should only be used in a sandboxed container/VM that has restricted internet access and can easily
|
||||||
restricted internet access and can easily be restored if damaged.
|
be restored if damaged.
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
By proceeding, you accept all responsibility for actions taken while
|
By proceeding, you accept all responsibility for actions taken while running in Bypass Permissions mode.
|
||||||
running in Bypass Permissions mode.
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Link url="https://code.claude.com/docs/en/security" />
|
<Link url="https://code.claude.com/docs/en/security" />
|
||||||
|
|
@ -69,5 +78,5 @@ export function BypassPermissionsModeDialog({
|
||||||
onChange={value => onChange(value as 'accept' | 'decline')}
|
onChange={value => onChange(value as 'accept' | 'decline')}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,58 @@
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react';
|
||||||
import type { ChannelEntry } from '../bootstrap/state.js'
|
import type { ChannelEntry } from '../bootstrap/state.js';
|
||||||
import { Box, Text, Dialog } from '@anthropic/ink'
|
import { Box, Text, Dialog } from '@anthropic/ink';
|
||||||
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
|
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
|
||||||
import { Select } from './CustomSelect/index.js'
|
import { Select } from './CustomSelect/index.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channels: ChannelEntry[]
|
channels: ChannelEntry[];
|
||||||
onAccept(): void
|
onAccept(): void;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export function DevChannelsDialog({ channels, onAccept }: Props): React.ReactNode {
|
||||||
|
const [pendingExitCode, setPendingExitCode] = React.useState<number | null>(null);
|
||||||
|
|
||||||
|
// Clear screen before shutdown so residual dialog content doesn't leak
|
||||||
|
// to the terminal. Deferred to next tick so Ink flushes the null render.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (pendingExitCode !== null) {
|
||||||
|
const code = pendingExitCode;
|
||||||
|
const timer = setTimeout(() => gracefulShutdownSync(code));
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [pendingExitCode]);
|
||||||
|
|
||||||
export function DevChannelsDialog({
|
|
||||||
channels,
|
|
||||||
onAccept,
|
|
||||||
}: Props): React.ReactNode {
|
|
||||||
function onChange(value: 'accept' | 'exit') {
|
function onChange(value: 'accept' | 'exit') {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'accept':
|
case 'accept':
|
||||||
onAccept()
|
onAccept();
|
||||||
break
|
break;
|
||||||
case 'exit':
|
case 'exit':
|
||||||
gracefulShutdownSync(1)
|
setPendingExitCode(1);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
gracefulShutdownSync(0)
|
setPendingExitCode(0);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
if (pendingExitCode !== null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog title="WARNING: Loading development channels" color="error" onCancel={handleEscape}>
|
||||||
title="WARNING: Loading development channels"
|
|
||||||
color="error"
|
|
||||||
onCancel={handleEscape}
|
|
||||||
>
|
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text>
|
<Text>
|
||||||
--dangerously-load-development-channels is for local channel
|
--dangerously-load-development-channels is for local channel development only. Do not use this option to run
|
||||||
development only. Do not use this option to run channels you have
|
channels you have downloaded off the internet.
|
||||||
downloaded off the internet.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text>Please use --channels to run a list of approved channels.</Text>
|
<Text>Please use --channels to run a list of approved channels.</Text>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
Channels:{' '}
|
Channels:{' '}
|
||||||
{channels
|
{channels
|
||||||
.map(c =>
|
.map(c => (c.kind === 'plugin' ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`))
|
||||||
c.kind === 'plugin'
|
|
||||||
? `plugin:${c.name}@${c.marketplace}`
|
|
||||||
: `server:${c.name}`,
|
|
||||||
)
|
|
||||||
.join(', ')}
|
.join(', ')}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -61,5 +65,5 @@ export function DevChannelsDialog({
|
||||||
onChange={value => onChange(value as 'accept' | 'exit')}
|
onChange={value => onChange(value as 'accept' | 'exit')}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os';
|
||||||
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 { setSessionTrustAccepted } from '../../bootstrap/state.js'
|
import { setSessionTrustAccepted } from '../../bootstrap/state.js';
|
||||||
import type { Command } from '../../commands.js'
|
import type { Command } from '../../commands.js';
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||||
import { Box, Link, Text } from '@anthropic/ink'
|
import { Box, Link, Text } from '@anthropic/ink';
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||||
import { getMcpConfigsByScope } from '../../services/mcp/config.js'
|
import { getMcpConfigsByScope } from '../../services/mcp/config.js';
|
||||||
import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'
|
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js';
|
||||||
import {
|
import { checkHasTrustDialogAccepted, saveCurrentProjectConfig } from '../../utils/config.js';
|
||||||
checkHasTrustDialogAccepted,
|
import { getCwd } from '../../utils/cwd.js';
|
||||||
saveCurrentProjectConfig,
|
import { getFsImplementation } from '../../utils/fsOperations.js';
|
||||||
} from '../../utils/config.js'
|
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js';
|
||||||
import { getCwd } from '../../utils/cwd.js'
|
import { Select } from '../CustomSelect/index.js';
|
||||||
import { getFsImplementation } from '../../utils/fsOperations.js'
|
import { PermissionDialog } from '../permissions/PermissionDialog.js';
|
||||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'
|
|
||||||
import { Select } from '../CustomSelect/index.js'
|
|
||||||
import { PermissionDialog } from '../permissions/PermissionDialog.js'
|
|
||||||
import {
|
import {
|
||||||
getApiKeyHelperSources,
|
getApiKeyHelperSources,
|
||||||
getAwsCommandsSources,
|
getAwsCommandsSources,
|
||||||
|
|
@ -25,77 +22,82 @@ import {
|
||||||
getGcpCommandsSources,
|
getGcpCommandsSources,
|
||||||
getHooksSources,
|
getHooksSources,
|
||||||
getOtelHeadersHelperSources,
|
getOtelHeadersHelperSources,
|
||||||
} from './utils.js'
|
} from './utils.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDone(): void
|
onDone(): void;
|
||||||
commands?: Command[]
|
commands?: Command[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
const { servers: projectServers } = getMcpConfigsByScope('project')
|
const { servers: projectServers } = getMcpConfigsByScope('project');
|
||||||
|
|
||||||
// In all cases, we generally check only the project-level and
|
// In all cases, we generally check only the project-level and
|
||||||
// project-local-level settings, which we assume that users do not configure
|
// project-local-level settings, which we assume that users do not configure
|
||||||
// directly compared to user-level settings.
|
// directly compared to user-level settings.
|
||||||
|
|
||||||
// Check for MCPs
|
// Check for MCPs
|
||||||
const hasMcpServers = Object.keys(projectServers).length > 0
|
const hasMcpServers = Object.keys(projectServers).length > 0;
|
||||||
// Check for hooks
|
// Check for hooks
|
||||||
const hooksSettingSources = getHooksSources()
|
const hooksSettingSources = getHooksSources();
|
||||||
const hasHooks = hooksSettingSources.length > 0
|
const hasHooks = hooksSettingSources.length > 0;
|
||||||
// Check whether code execution is allowed in permissions and slash commands
|
// Check whether code execution is allowed in permissions and slash commands
|
||||||
const bashSettingSources = getBashPermissionSources()
|
const bashSettingSources = getBashPermissionSources();
|
||||||
// Check for apiKeyHelper which executes arbitrary commands
|
// Check for apiKeyHelper which executes arbitrary commands
|
||||||
const apiKeyHelperSources = getApiKeyHelperSources()
|
const apiKeyHelperSources = getApiKeyHelperSources();
|
||||||
const hasApiKeyHelper = apiKeyHelperSources.length > 0
|
const hasApiKeyHelper = apiKeyHelperSources.length > 0;
|
||||||
// Check for AWS commands which execute arbitrary commands
|
// Check for AWS commands which execute arbitrary commands
|
||||||
const awsCommandsSources = getAwsCommandsSources()
|
const awsCommandsSources = getAwsCommandsSources();
|
||||||
const hasAwsCommands = awsCommandsSources.length > 0
|
const hasAwsCommands = awsCommandsSources.length > 0;
|
||||||
// Check for GCP commands which execute arbitrary commands
|
// Check for GCP commands which execute arbitrary commands
|
||||||
const gcpCommandsSources = getGcpCommandsSources()
|
const gcpCommandsSources = getGcpCommandsSources();
|
||||||
const hasGcpCommands = gcpCommandsSources.length > 0
|
const hasGcpCommands = gcpCommandsSources.length > 0;
|
||||||
// Check for otelHeadersHelper which executes arbitrary commands
|
// Check for otelHeadersHelper which executes arbitrary commands
|
||||||
const otelHeadersHelperSources = getOtelHeadersHelperSources()
|
const otelHeadersHelperSources = getOtelHeadersHelperSources();
|
||||||
const hasOtelHeadersHelper = otelHeadersHelperSources.length > 0
|
const hasOtelHeadersHelper = otelHeadersHelperSources.length > 0;
|
||||||
// Check for dangerous environment variables (not in SAFE_ENV_VARS)
|
// Check for dangerous environment variables (not in SAFE_ENV_VARS)
|
||||||
const dangerousEnvVarsSources = getDangerousEnvVarsSources()
|
const dangerousEnvVarsSources = getDangerousEnvVarsSources();
|
||||||
const hasDangerousEnvVars = dangerousEnvVarsSources.length > 0
|
const hasDangerousEnvVars = dangerousEnvVarsSources.length > 0;
|
||||||
|
|
||||||
const hasSlashCommandBash =
|
const hasSlashCommandBash =
|
||||||
commands?.some(
|
commands?.some(
|
||||||
command =>
|
command =>
|
||||||
command.type === 'prompt' &&
|
command.type === 'prompt' &&
|
||||||
command.loadedFrom === 'commands_DEPRECATED' &&
|
command.loadedFrom === 'commands_DEPRECATED' &&
|
||||||
(command.source === 'projectSettings' ||
|
(command.source === 'projectSettings' || command.source === 'localSettings') &&
|
||||||
command.source === 'localSettings') &&
|
command.allowedTools?.some((tool: string) => tool === BASH_TOOL_NAME || tool.startsWith(BASH_TOOL_NAME + '(')),
|
||||||
command.allowedTools?.some(
|
) ?? false;
|
||||||
(tool: string) =>
|
|
||||||
tool === BASH_TOOL_NAME || tool.startsWith(BASH_TOOL_NAME + '('),
|
|
||||||
),
|
|
||||||
) ?? false
|
|
||||||
|
|
||||||
const hasSkillsBash =
|
const hasSkillsBash =
|
||||||
commands?.some(
|
commands?.some(
|
||||||
command =>
|
command =>
|
||||||
command.type === 'prompt' &&
|
command.type === 'prompt' &&
|
||||||
(command.loadedFrom === 'skills' || command.loadedFrom === 'plugin') &&
|
(command.loadedFrom === 'skills' || command.loadedFrom === 'plugin') &&
|
||||||
(command.source === 'projectSettings' ||
|
(command.source === 'projectSettings' || command.source === 'localSettings' || command.source === 'plugin') &&
|
||||||
command.source === 'localSettings' ||
|
command.allowedTools?.some((tool: string) => tool === BASH_TOOL_NAME || tool.startsWith(BASH_TOOL_NAME + '(')),
|
||||||
command.source === 'plugin') &&
|
) ?? false;
|
||||||
command.allowedTools?.some(
|
|
||||||
(tool: string) =>
|
|
||||||
tool === BASH_TOOL_NAME || tool.startsWith(BASH_TOOL_NAME + '('),
|
|
||||||
),
|
|
||||||
) ?? false
|
|
||||||
|
|
||||||
const hasAnyBashExecution =
|
const hasAnyBashExecution = bashSettingSources.length > 0 || hasSlashCommandBash || hasSkillsBash;
|
||||||
bashSettingSources.length > 0 || hasSlashCommandBash || hasSkillsBash
|
|
||||||
|
|
||||||
const hasTrustDialogAccepted = checkHasTrustDialogAccepted()
|
const hasTrustDialogAccepted = checkHasTrustDialogAccepted();
|
||||||
|
const [pendingExitCode, setPendingExitCode] = React.useState<number | null>(null);
|
||||||
|
|
||||||
|
// When a non-null exit code is set, render null (clear screen) first,
|
||||||
|
// then trigger shutdown in the next tick so Ink has time to flush
|
||||||
|
// the empty frame before cleanupTerminalModes() unmounts and exits
|
||||||
|
// the alt screen. Without this deferral, gracefulShutdownSync starts
|
||||||
|
// async cleanup immediately after React commit, racing the reconciler
|
||||||
|
// and leaving residual TrustDialog output on the terminal.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (pendingExitCode !== null) {
|
||||||
|
const code = pendingExitCode;
|
||||||
|
const timer = setTimeout(() => gracefulShutdownSync(code));
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [pendingExitCode]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const isHomeDir = homedir() === getCwd()
|
const isHomeDir = homedir() === getCwd();
|
||||||
logEvent('tengu_trust_dialog_shown', {
|
logEvent('tengu_trust_dialog_shown', {
|
||||||
isHomeDir,
|
isHomeDir,
|
||||||
hasMcpServers,
|
hasMcpServers,
|
||||||
|
|
@ -106,7 +108,7 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
hasGcpCommands,
|
hasGcpCommands,
|
||||||
hasOtelHeadersHelper,
|
hasOtelHeadersHelper,
|
||||||
hasDangerousEnvVars,
|
hasDangerousEnvVars,
|
||||||
})
|
});
|
||||||
}, [
|
}, [
|
||||||
hasMcpServers,
|
hasMcpServers,
|
||||||
hasHooks,
|
hasHooks,
|
||||||
|
|
@ -116,15 +118,20 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
hasGcpCommands,
|
hasGcpCommands,
|
||||||
hasOtelHeadersHelper,
|
hasOtelHeadersHelper,
|
||||||
hasDangerousEnvVars,
|
hasDangerousEnvVars,
|
||||||
])
|
]);
|
||||||
|
|
||||||
function onChange(value: 'enable_all' | 'exit') {
|
function onChange(value: 'enable_all' | 'exit') {
|
||||||
if (value === 'exit') {
|
if (value === 'exit') {
|
||||||
gracefulShutdownSync(1)
|
// Set pendingExitCode to clear the screen before triggering shutdown.
|
||||||
return
|
// The useEffect above defers gracefulShutdownSync to the next tick
|
||||||
|
// so Ink can flush the empty frame first — otherwise
|
||||||
|
// cleanupTerminalModes races React's re-render and leaves
|
||||||
|
// residual TrustDialog content on the terminal.
|
||||||
|
setPendingExitCode(1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHomeDir = homedir() === getCwd()
|
const isHomeDir = homedir() === getCwd();
|
||||||
|
|
||||||
logEvent('tengu_trust_dialog_accept', {
|
logEvent('tengu_trust_dialog_accept', {
|
||||||
isHomeDir,
|
isHomeDir,
|
||||||
|
|
@ -136,18 +143,18 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
hasGcpCommands,
|
hasGcpCommands,
|
||||||
hasOtelHeadersHelper,
|
hasOtelHeadersHelper,
|
||||||
hasDangerousEnvVars,
|
hasDangerousEnvVars,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (isHomeDir) {
|
if (isHomeDir) {
|
||||||
// For home directory, store trust in session memory only (not persisted to disk)
|
// For home directory, store trust in session memory only (not persisted to disk)
|
||||||
// This allows hooks and other trust-requiring features to work during this session
|
// This allows hooks and other trust-requiring features to work during this session
|
||||||
// while preserving the security intent of not permanently trusting home dir
|
// while preserving the security intent of not permanently trusting home dir
|
||||||
setSessionTrustAccepted(true)
|
setSessionTrustAccepted(true);
|
||||||
} else {
|
} else {
|
||||||
saveCurrentProjectConfig(current => ({
|
saveCurrentProjectConfig(current => ({
|
||||||
...current,
|
...current,
|
||||||
hasTrustDialogAccepted: true,
|
hasTrustDialogAccepted: true,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do NOT write MCP server settings here. handleMcpjsonServerApprovals in
|
// Do NOT write MCP server settings here. handleMcpjsonServerApprovals in
|
||||||
|
|
@ -155,7 +162,7 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
// UI. Writing enabledMcpjsonServers/enableAllProjectMcpServers here would
|
// UI. Writing enabledMcpjsonServers/enableAllProjectMcpServers here would
|
||||||
// mark every server 'approved' and silently skip that dialog. See #15558.
|
// mark every server 'approved' and silently skip that dialog. See #15558.
|
||||||
|
|
||||||
onDone()
|
onDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default onExit is useApp().exit() → Ink.unmount(), which tears down the
|
// Default onExit is useApp().exit() → Ink.unmount(), which tears down the
|
||||||
|
|
@ -164,48 +171,41 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
// so the default would hang the await forever. With keybinding
|
// so the default would hang the await forever. With keybinding
|
||||||
// customization enabled, the chokidar watcher (persistent: true) keeps the
|
// customization enabled, the chokidar watcher (persistent: true) keeps the
|
||||||
// event loop alive and the process freezes. Explicitly exit 1 like "No".
|
// event loop alive and the process freezes. Explicitly exit 1 like "No".
|
||||||
const exitState = useExitOnCtrlCDWithKeybindings(() =>
|
const exitState = useExitOnCtrlCDWithKeybindings(() => setPendingExitCode(1));
|
||||||
gracefulShutdownSync(1),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use configurable keybinding for ESC to cancel/exit
|
// Use configurable keybinding for ESC to cancel/exit
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'confirm:no',
|
'confirm:no',
|
||||||
() => {
|
() => {
|
||||||
gracefulShutdownSync(0)
|
setPendingExitCode(0);
|
||||||
},
|
},
|
||||||
{ context: 'Confirmation' },
|
{ context: 'Confirmation' },
|
||||||
)
|
);
|
||||||
|
|
||||||
|
// When pendingExitCode is set, render nothing so the screen is cleared
|
||||||
|
// before shutdown cleans up the alt screen. See the useEffect above.
|
||||||
|
if (pendingExitCode !== null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Automatically resolve the trust dialog if there is nothing to be shown.
|
// Automatically resolve the trust dialog if there is nothing to be shown.
|
||||||
if (hasTrustDialogAccepted) {
|
if (hasTrustDialogAccepted) {
|
||||||
setTimeout(onDone)
|
setTimeout(onDone);
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PermissionDialog
|
<PermissionDialog color="warning" titleColor="warning" title="Accessing workspace:">
|
||||||
color="warning"
|
|
||||||
titleColor="warning"
|
|
||||||
title="Accessing workspace:"
|
|
||||||
>
|
|
||||||
<Box flexDirection="column" gap={1} paddingTop={1}>
|
<Box flexDirection="column" gap={1} paddingTop={1}>
|
||||||
<Text bold>{getFsImplementation().cwd()}</Text>
|
<Text bold>{getFsImplementation().cwd()}</Text>
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
Quick safety check: Is this a project you created or one you trust?
|
Is this a project you trust? (Your own code, a well-known open source project, or work from your team).
|
||||||
(Like your own code, a well-known open source project, or work from
|
|
||||||
your team). If not, take a moment to review what{"'"}s in this folder
|
|
||||||
first.
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
Claude Code{"'"}ll be able to read, edit, and execute files here.
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text>Once trusted, Claude Code can read, edit, and run commands in this folder.</Text>
|
||||||
|
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
<Link url="https://code.claude.com/docs/en/security">
|
<Link url="https://code.claude.com/docs/en/security">Security guide</Link>
|
||||||
Security guide
|
|
||||||
</Link>
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -218,13 +218,9 @@ export function TrustDialog({ onDone, commands }: Props): React.ReactNode {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
{exitState.pending ? (
|
{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to confirm · Esc to cancel</>}
|
||||||
<>Press {exitState.keyName} again to exit</>
|
|
||||||
) : (
|
|
||||||
<>Enter to confirm · Esc to cancel</>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</PermissionDialog>
|
</PermissionDialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user