mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
feat: 正式启用 auto mode (#307)
* fix: 修复settings.json内存状态溢出的问题 * fix: 修复auto mode gate check未处理的promise rejection 在 bypassPermissionsKillswitch.ts 的 useKickOffCheckAndDisableAutoModeIfNeeded 中,void fire-and-forget 调用缺少 .catch() 处理,导致 verifyAutoModeGateAccess 失败时产生 unhandled promise rejection。同时移除 permissionSetup.ts 中冗余的 null check。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: 开放 auto mode 和 bypass mode 给所有用户 通过 Shift+Tab 统一循环:default → acceptEdits → plan → auto → bypassPermissions → default - 移除 USER_TYPE 分支判断,所有用户使用同一循环路径 - isBypassPermissionsModeAvailable 始终为 true - isAutoModeAvailable 初始化直接为 true - 移除 AutoModeOptInDialog 确认流程 - 简化 isAutoModeGateEnabled 仅保留快模式熔断器 - 简化 verifyAutoModeGateAccess 仅检查快模式 - 移除 GrowthBook/Statsig 远程门控 - bypass permissions killswitch 改为 no-op - 新增 24 个测试覆盖循环逻辑和门控不变量 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: 为sideQuery添加Langfuse追踪 sideQuery 绕过了 claude.ts 的主 API 路径,导致所有走 sideQuery 的调用 (auto mode classifier、permission explainer、session search 等)都没有 Langfuse 记录。现在为每次 sideQuery 调用创建独立 trace 并记录 LLM observation, 未配置 Langfuse 时全部 no-op。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: ACP availableModes 补齐 bypassPermissions 并修正测试 import 路径 - ACP agent availableModes 按条件包含 bypassPermissions(非 root/sandbox) - 顺序对齐 REPL 循环:default → acceptEdits → plan → auto → bypassPermissions - 新增 2 个测试验证 availableModes 包含 bypassPermissions 及模式切换 - 修正 getNextPermissionMode.test.ts 和 permissionSetup.test.ts 的 import 路径 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,16 +151,14 @@ import {
|
||||
isOpus1mMergeEnabled,
|
||||
modelDisplayString,
|
||||
} from '../../utils/model/model.js'
|
||||
import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
|
||||
import {
|
||||
cyclePermissionMode,
|
||||
getNextPermissionMode,
|
||||
} from '../../utils/permissions/getNextPermissionMode.js'
|
||||
import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
|
||||
import { editPromptInEditor } from '../../utils/promptEditor.js'
|
||||
import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
|
||||
// hasAutoModeOptIn removed — auto mode is available to all users
|
||||
import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
|
||||
import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
|
||||
import {
|
||||
@@ -187,7 +185,7 @@ import {
|
||||
findUltraplanTriggerPositions,
|
||||
findUltrareviewTriggerPositions,
|
||||
} from '../../utils/ultraplan/keyword.js'
|
||||
import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
|
||||
// AutoModeOptInDialog removed — auto mode is available to all users
|
||||
import { BridgeDialog } from '../BridgeDialog.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import {
|
||||
@@ -571,10 +569,6 @@ function PromptInput({
|
||||
const [showHistoryPicker, setShowHistoryPicker] = useState(false)
|
||||
const [showFastModePicker, setShowFastModePicker] = useState(false)
|
||||
const [showThinkingToggle, setShowThinkingToggle] = useState(false)
|
||||
const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false)
|
||||
const [previousModeBeforeAuto, setPreviousModeBeforeAuto] =
|
||||
useState<PermissionMode | null>(null)
|
||||
const autoModeOptInTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Check if cursor is on the first line of input
|
||||
const isCursorOnFirstLine = useMemo(() => {
|
||||
@@ -1883,86 +1877,11 @@ function PromptInput({
|
||||
|
||||
// Compute the next mode without triggering side effects first
|
||||
logForDebugging(
|
||||
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`,
|
||||
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode}`,
|
||||
)
|
||||
const nextMode = getNextPermissionMode(toolPermissionContext, teamContext)
|
||||
|
||||
// Check if user is entering auto mode for the first time. Gated on the
|
||||
// persistent settings flag (hasAutoModeOptIn) rather than the broader
|
||||
// hasAutoModeOptInAnySource so that --enable-auto-mode users still see
|
||||
// the warning dialog once — the CLI flag should grant carousel access,
|
||||
// not bypass the safety text.
|
||||
let isEnteringAutoModeFirstTime = false
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
isEnteringAutoModeFirstTime =
|
||||
nextMode === 'auto' &&
|
||||
toolPermissionContext.mode !== 'auto' &&
|
||||
!hasAutoModeOptIn() &&
|
||||
!viewingAgentTaskId // Only show for primary agent, not subagents
|
||||
}
|
||||
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
if (isEnteringAutoModeFirstTime) {
|
||||
// Store previous mode so we can revert if user declines
|
||||
setPreviousModeBeforeAuto(toolPermissionContext.mode)
|
||||
|
||||
// Only update the UI mode label — do NOT call transitionPermissionMode
|
||||
// or cyclePermissionMode yet; we haven't confirmed with the user.
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: {
|
||||
...prev.toolPermissionContext,
|
||||
mode: 'auto',
|
||||
},
|
||||
}))
|
||||
setToolPermissionContext({
|
||||
...toolPermissionContext,
|
||||
mode: 'auto',
|
||||
})
|
||||
|
||||
// Show opt-in dialog after 400ms debounce
|
||||
if (autoModeOptInTimeoutRef.current) {
|
||||
clearTimeout(autoModeOptInTimeoutRef.current)
|
||||
}
|
||||
autoModeOptInTimeoutRef.current = setTimeout(
|
||||
(setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
|
||||
setShowAutoModeOptIn(true)
|
||||
autoModeOptInTimeoutRef.current = null
|
||||
},
|
||||
400,
|
||||
setShowAutoModeOptIn,
|
||||
autoModeOptInTimeoutRef,
|
||||
)
|
||||
|
||||
if (helpOpen) {
|
||||
setHelpOpen(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss auto mode opt-in dialog if showing or pending (user is cycling away).
|
||||
// Do NOT revert to previousModeBeforeAuto here — shift+tab means "advance the
|
||||
// carousel", not "decline". Reverting causes a ping-pong loop: auto reverts to
|
||||
// the prior mode, whose next mode is auto again, forever.
|
||||
// The dialog's own decline button (handleAutoModeOptInDecline) handles revert.
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) {
|
||||
if (showAutoModeOptIn) {
|
||||
logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
|
||||
}
|
||||
setShowAutoModeOptIn(false)
|
||||
if (autoModeOptInTimeoutRef.current) {
|
||||
clearTimeout(autoModeOptInTimeoutRef.current)
|
||||
autoModeOptInTimeoutRef.current = null
|
||||
}
|
||||
setPreviousModeBeforeAuto(null)
|
||||
// Fall through — mode is 'auto', cyclePermissionMode below goes to 'default'.
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know this is NOT the first-time auto mode path,
|
||||
// call cyclePermissionMode to apply side effects (e.g. strip
|
||||
// Call cyclePermissionMode to apply side effects (e.g. strip
|
||||
// dangerous permissions, activate classifier)
|
||||
const { context: preparedContext } = cyclePermissionMode(
|
||||
toolPermissionContext,
|
||||
@@ -2007,91 +1926,10 @@ function PromptInput({
|
||||
}, [
|
||||
toolPermissionContext,
|
||||
teamContext,
|
||||
viewingAgentTaskId,
|
||||
viewedTeammate,
|
||||
setAppState,
|
||||
setToolPermissionContext,
|
||||
helpOpen,
|
||||
showAutoModeOptIn,
|
||||
])
|
||||
|
||||
// Handler for auto mode opt-in dialog acceptance
|
||||
const handleAutoModeOptInAccept = useCallback(() => {
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
setShowAutoModeOptIn(false)
|
||||
setPreviousModeBeforeAuto(null)
|
||||
|
||||
// Now that the user accepted, apply the full transition: activate the
|
||||
// auto mode backend (classifier, beta headers) and strip dangerous
|
||||
// permissions (e.g. Bash(*) always-allow rules).
|
||||
const strippedContext = transitionPermissionMode(
|
||||
previousModeBeforeAuto ?? toolPermissionContext.mode,
|
||||
'auto',
|
||||
toolPermissionContext,
|
||||
)
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: {
|
||||
...strippedContext,
|
||||
mode: 'auto',
|
||||
},
|
||||
}))
|
||||
setToolPermissionContext({
|
||||
...strippedContext,
|
||||
mode: 'auto',
|
||||
})
|
||||
|
||||
// Close help tips if they're open when auto mode is enabled
|
||||
if (helpOpen) {
|
||||
setHelpOpen(false)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
helpOpen,
|
||||
setHelpOpen,
|
||||
previousModeBeforeAuto,
|
||||
toolPermissionContext,
|
||||
setAppState,
|
||||
setToolPermissionContext,
|
||||
])
|
||||
|
||||
// Handler for auto mode opt-in dialog decline
|
||||
const handleAutoModeOptInDecline = useCallback(() => {
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
logForDebugging(
|
||||
`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`,
|
||||
)
|
||||
setShowAutoModeOptIn(false)
|
||||
if (autoModeOptInTimeoutRef.current) {
|
||||
clearTimeout(autoModeOptInTimeoutRef.current)
|
||||
autoModeOptInTimeoutRef.current = null
|
||||
}
|
||||
|
||||
// Revert to previous mode and remove auto from the carousel
|
||||
// for the rest of this session
|
||||
if (previousModeBeforeAuto) {
|
||||
setAutoModeActive(false)
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: {
|
||||
...prev.toolPermissionContext,
|
||||
mode: previousModeBeforeAuto,
|
||||
isAutoModeAvailable: false,
|
||||
},
|
||||
}))
|
||||
setToolPermissionContext({
|
||||
...toolPermissionContext,
|
||||
mode: previousModeBeforeAuto,
|
||||
isAutoModeAvailable: false,
|
||||
})
|
||||
setPreviousModeBeforeAuto(null)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
previousModeBeforeAuto,
|
||||
toolPermissionContext,
|
||||
setAppState,
|
||||
setToolPermissionContext,
|
||||
])
|
||||
|
||||
// Handler for chat:imagePaste - paste image from clipboard
|
||||
@@ -2758,20 +2596,7 @@ function PromptInput({
|
||||
// Portal dialog to DialogOverlay in fullscreen so it escapes the bottom
|
||||
// slot's overflowY:hidden clip (same pattern as SuggestionsOverlay).
|
||||
// Must be called before early returns below to satisfy rules-of-hooks.
|
||||
// Memoized so the portal useEffect doesn't churn on every PromptInput render.
|
||||
const autoModeOptInDialog = useMemo(
|
||||
() =>
|
||||
feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? (
|
||||
<AutoModeOptInDialog
|
||||
onAccept={handleAutoModeOptInAccept}
|
||||
onDecline={handleAutoModeOptInDecline}
|
||||
/>
|
||||
) : null,
|
||||
[showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline],
|
||||
)
|
||||
useSetPromptOverlayDialog(
|
||||
isFullscreenEnvEnabled() ? autoModeOptInDialog : null,
|
||||
)
|
||||
useSetPromptOverlayDialog(null)
|
||||
|
||||
if (showBashesDialog) {
|
||||
return (
|
||||
@@ -3077,7 +2902,6 @@ function PromptInput({
|
||||
isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined
|
||||
}
|
||||
/>
|
||||
{isFullscreenEnvEnabled() ? null : autoModeOptInDialog}
|
||||
{isFullscreenEnvEnabled() ? (
|
||||
// position=absolute takes zero layout height so the spinner
|
||||
// doesn't shift when a notification appears/disappears. Yoga
|
||||
@@ -3098,7 +2922,7 @@ function PromptInput({
|
||||
<Box
|
||||
position="absolute"
|
||||
marginTop={briefOwnsGap ? -2 : -1}
|
||||
height={suggestions.length === 0 && !showAutoModeOptIn ? 1 : 0}
|
||||
height={suggestions.length === 0 ? 1 : 0}
|
||||
width="100%"
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
|
||||
Reference in New Issue
Block a user