Skip to content

Commit 92f8a92

Browse files
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>
1 parent a67e2d0 commit 92f8a92

18 files changed

Lines changed: 510 additions & 605 deletions

File tree

src/Tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const getEmptyToolPermissionContext: () => ToolPermissionContext =
146146
alwaysAllowRules: {},
147147
alwaysDenyRules: {},
148148
alwaysAskRules: {},
149-
isBypassPermissionsModeAvailable: false,
149+
isBypassPermissionsModeAvailable: true,
150150
})
151151

152152
export type CompactProgressEvent =

src/__tests__/Tool.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ describe('getEmptyToolPermissionContext', () => {
166166
expect(ctx.alwaysAskRules).toEqual({})
167167
})
168168

169-
test('returns isBypassPermissionsModeAvailable as false', () => {
169+
test('returns isBypassPermissionsModeAvailable as true', () => {
170170
const ctx = getEmptyToolPermissionContext()
171-
expect(ctx.isBypassPermissionsModeAvailable).toBe(false)
171+
expect(ctx.isBypassPermissionsModeAvailable).toBe(true)
172172
})
173173
})
174174

src/commands/login/login.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ import type { LocalJSXCommandOnDone } from '../../types/command.js'
1818
import { stripSignatureBlocks } from '../../utils/messages.js'
1919
import {
2020
checkAndDisableAutoModeIfNeeded,
21-
checkAndDisableBypassPermissionsIfNeeded,
2221
resetAutoModeGateCheck,
23-
resetBypassPermissionsCheck,
2422
} from '../../utils/permissions/bypassPermissionsKillswitch.js'
2523
import { resetUserCache } from '../../utils/user.js'
2624

@@ -54,20 +52,13 @@ export async function call(
5452
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
5553
void enrollTrustedDevice()
5654
// Reset killswitch gate checks and re-run with new org
57-
resetBypassPermissionsCheck()
55+
resetAutoModeGateCheck()
5856
const appState = context.getAppState()
59-
void checkAndDisableBypassPermissionsIfNeeded(
57+
void checkAndDisableAutoModeIfNeeded(
6058
appState.toolPermissionContext,
6159
context.setAppState,
60+
appState.fastMode,
6261
)
63-
if (feature('TRANSCRIPT_CLASSIFIER')) {
64-
resetAutoModeGateCheck()
65-
void checkAndDisableAutoModeIfNeeded(
66-
appState.toolPermissionContext,
67-
context.setAppState,
68-
appState.fastMode,
69-
)
70-
}
7162
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
7263
context.setAppState(prev => ({
7364
...prev,

src/components/PromptInput/PromptInput.tsx

Lines changed: 6 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,14 @@ import {
151151
isOpus1mMergeEnabled,
152152
modelDisplayString,
153153
} from '../../utils/model/model.js'
154-
import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
155154
import {
156155
cyclePermissionMode,
157156
getNextPermissionMode,
158157
} from '../../utils/permissions/getNextPermissionMode.js'
159-
import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
160158
import { getPlatform } from '../../utils/platform.js'
161159
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
162160
import { editPromptInEditor } from '../../utils/promptEditor.js'
163-
import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
161+
// hasAutoModeOptIn removed — auto mode is available to all users
164162
import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
165163
import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
166164
import {
@@ -187,7 +185,7 @@ import {
187185
findUltraplanTriggerPositions,
188186
findUltrareviewTriggerPositions,
189187
} from '../../utils/ultraplan/keyword.js'
190-
import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
188+
// AutoModeOptInDialog removed — auto mode is available to all users
191189
import { BridgeDialog } from '../BridgeDialog.js'
192190
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
193191
import {
@@ -571,10 +569,6 @@ function PromptInput({
571569
const [showHistoryPicker, setShowHistoryPicker] = useState(false)
572570
const [showFastModePicker, setShowFastModePicker] = useState(false)
573571
const [showThinkingToggle, setShowThinkingToggle] = useState(false)
574-
const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false)
575-
const [previousModeBeforeAuto, setPreviousModeBeforeAuto] =
576-
useState<PermissionMode | null>(null)
577-
const autoModeOptInTimeoutRef = useRef<NodeJS.Timeout | null>(null)
578572

579573
// Check if cursor is on the first line of input
580574
const isCursorOnFirstLine = useMemo(() => {
@@ -1883,86 +1877,11 @@ function PromptInput({
18831877

18841878
// Compute the next mode without triggering side effects first
18851879
logForDebugging(
1886-
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`,
1880+
`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode}`,
18871881
)
18881882
const nextMode = getNextPermissionMode(toolPermissionContext, teamContext)
18891883

1890-
// Check if user is entering auto mode for the first time. Gated on the
1891-
// persistent settings flag (hasAutoModeOptIn) rather than the broader
1892-
// hasAutoModeOptInAnySource so that --enable-auto-mode users still see
1893-
// the warning dialog once — the CLI flag should grant carousel access,
1894-
// not bypass the safety text.
1895-
let isEnteringAutoModeFirstTime = false
1896-
if (feature('TRANSCRIPT_CLASSIFIER')) {
1897-
isEnteringAutoModeFirstTime =
1898-
nextMode === 'auto' &&
1899-
toolPermissionContext.mode !== 'auto' &&
1900-
!hasAutoModeOptIn() &&
1901-
!viewingAgentTaskId // Only show for primary agent, not subagents
1902-
}
1903-
1904-
if (feature('TRANSCRIPT_CLASSIFIER')) {
1905-
if (isEnteringAutoModeFirstTime) {
1906-
// Store previous mode so we can revert if user declines
1907-
setPreviousModeBeforeAuto(toolPermissionContext.mode)
1908-
1909-
// Only update the UI mode label — do NOT call transitionPermissionMode
1910-
// or cyclePermissionMode yet; we haven't confirmed with the user.
1911-
setAppState(prev => ({
1912-
...prev,
1913-
toolPermissionContext: {
1914-
...prev.toolPermissionContext,
1915-
mode: 'auto',
1916-
},
1917-
}))
1918-
setToolPermissionContext({
1919-
...toolPermissionContext,
1920-
mode: 'auto',
1921-
})
1922-
1923-
// Show opt-in dialog after 400ms debounce
1924-
if (autoModeOptInTimeoutRef.current) {
1925-
clearTimeout(autoModeOptInTimeoutRef.current)
1926-
}
1927-
autoModeOptInTimeoutRef.current = setTimeout(
1928-
(setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
1929-
setShowAutoModeOptIn(true)
1930-
autoModeOptInTimeoutRef.current = null
1931-
},
1932-
400,
1933-
setShowAutoModeOptIn,
1934-
autoModeOptInTimeoutRef,
1935-
)
1936-
1937-
if (helpOpen) {
1938-
setHelpOpen(false)
1939-
}
1940-
return
1941-
}
1942-
}
1943-
1944-
// Dismiss auto mode opt-in dialog if showing or pending (user is cycling away).
1945-
// Do NOT revert to previousModeBeforeAuto here — shift+tab means "advance the
1946-
// carousel", not "decline". Reverting causes a ping-pong loop: auto reverts to
1947-
// the prior mode, whose next mode is auto again, forever.
1948-
// The dialog's own decline button (handleAutoModeOptInDecline) handles revert.
1949-
if (feature('TRANSCRIPT_CLASSIFIER')) {
1950-
if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) {
1951-
if (showAutoModeOptIn) {
1952-
logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
1953-
}
1954-
setShowAutoModeOptIn(false)
1955-
if (autoModeOptInTimeoutRef.current) {
1956-
clearTimeout(autoModeOptInTimeoutRef.current)
1957-
autoModeOptInTimeoutRef.current = null
1958-
}
1959-
setPreviousModeBeforeAuto(null)
1960-
// Fall through — mode is 'auto', cyclePermissionMode below goes to 'default'.
1961-
}
1962-
}
1963-
1964-
// Now that we know this is NOT the first-time auto mode path,
1965-
// call cyclePermissionMode to apply side effects (e.g. strip
1884+
// Call cyclePermissionMode to apply side effects (e.g. strip
19661885
// dangerous permissions, activate classifier)
19671886
const { context: preparedContext } = cyclePermissionMode(
19681887
toolPermissionContext,
@@ -2007,91 +1926,10 @@ function PromptInput({
20071926
}, [
20081927
toolPermissionContext,
20091928
teamContext,
2010-
viewingAgentTaskId,
20111929
viewedTeammate,
20121930
setAppState,
20131931
setToolPermissionContext,
20141932
helpOpen,
2015-
showAutoModeOptIn,
2016-
])
2017-
2018-
// Handler for auto mode opt-in dialog acceptance
2019-
const handleAutoModeOptInAccept = useCallback(() => {
2020-
if (feature('TRANSCRIPT_CLASSIFIER')) {
2021-
setShowAutoModeOptIn(false)
2022-
setPreviousModeBeforeAuto(null)
2023-
2024-
// Now that the user accepted, apply the full transition: activate the
2025-
// auto mode backend (classifier, beta headers) and strip dangerous
2026-
// permissions (e.g. Bash(*) always-allow rules).
2027-
const strippedContext = transitionPermissionMode(
2028-
previousModeBeforeAuto ?? toolPermissionContext.mode,
2029-
'auto',
2030-
toolPermissionContext,
2031-
)
2032-
setAppState(prev => ({
2033-
...prev,
2034-
toolPermissionContext: {
2035-
...strippedContext,
2036-
mode: 'auto',
2037-
},
2038-
}))
2039-
setToolPermissionContext({
2040-
...strippedContext,
2041-
mode: 'auto',
2042-
})
2043-
2044-
// Close help tips if they're open when auto mode is enabled
2045-
if (helpOpen) {
2046-
setHelpOpen(false)
2047-
}
2048-
}
2049-
}, [
2050-
helpOpen,
2051-
setHelpOpen,
2052-
previousModeBeforeAuto,
2053-
toolPermissionContext,
2054-
setAppState,
2055-
setToolPermissionContext,
2056-
])
2057-
2058-
// Handler for auto mode opt-in dialog decline
2059-
const handleAutoModeOptInDecline = useCallback(() => {
2060-
if (feature('TRANSCRIPT_CLASSIFIER')) {
2061-
logForDebugging(
2062-
`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`,
2063-
)
2064-
setShowAutoModeOptIn(false)
2065-
if (autoModeOptInTimeoutRef.current) {
2066-
clearTimeout(autoModeOptInTimeoutRef.current)
2067-
autoModeOptInTimeoutRef.current = null
2068-
}
2069-
2070-
// Revert to previous mode and remove auto from the carousel
2071-
// for the rest of this session
2072-
if (previousModeBeforeAuto) {
2073-
setAutoModeActive(false)
2074-
setAppState(prev => ({
2075-
...prev,
2076-
toolPermissionContext: {
2077-
...prev.toolPermissionContext,
2078-
mode: previousModeBeforeAuto,
2079-
isAutoModeAvailable: false,
2080-
},
2081-
}))
2082-
setToolPermissionContext({
2083-
...toolPermissionContext,
2084-
mode: previousModeBeforeAuto,
2085-
isAutoModeAvailable: false,
2086-
})
2087-
setPreviousModeBeforeAuto(null)
2088-
}
2089-
}
2090-
}, [
2091-
previousModeBeforeAuto,
2092-
toolPermissionContext,
2093-
setAppState,
2094-
setToolPermissionContext,
20951933
])
20961934

20971935
// Handler for chat:imagePaste - paste image from clipboard
@@ -2758,20 +2596,7 @@ function PromptInput({
27582596
// Portal dialog to DialogOverlay in fullscreen so it escapes the bottom
27592597
// slot's overflowY:hidden clip (same pattern as SuggestionsOverlay).
27602598
// Must be called before early returns below to satisfy rules-of-hooks.
2761-
// Memoized so the portal useEffect doesn't churn on every PromptInput render.
2762-
const autoModeOptInDialog = useMemo(
2763-
() =>
2764-
feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? (
2765-
<AutoModeOptInDialog
2766-
onAccept={handleAutoModeOptInAccept}
2767-
onDecline={handleAutoModeOptInDecline}
2768-
/>
2769-
) : null,
2770-
[showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline],
2771-
)
2772-
useSetPromptOverlayDialog(
2773-
isFullscreenEnvEnabled() ? autoModeOptInDialog : null,
2774-
)
2599+
useSetPromptOverlayDialog(null)
27752600

27762601
if (showBashesDialog) {
27772602
return (
@@ -3077,7 +2902,6 @@ function PromptInput({
30772902
isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined
30782903
}
30792904
/>
3080-
{isFullscreenEnvEnabled() ? null : autoModeOptInDialog}
30812905
{isFullscreenEnvEnabled() ? (
30822906
// position=absolute takes zero layout height so the spinner
30832907
// doesn't shift when a notification appears/disappears. Yoga
@@ -3098,7 +2922,7 @@ function PromptInput({
30982922
<Box
30992923
position="absolute"
31002924
marginTop={briefOwnsGap ? -2 : -1}
3101-
height={suggestions.length === 0 && !showAutoModeOptIn ? 1 : 0}
2925+
height={suggestions.length === 0 ? 1 : 0}
31022926
width="100%"
31032927
paddingLeft={2}
31042928
paddingRight={1}

src/interactiveHelpers.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import type { PermissionMode } from './utils/permissions/PermissionMode.js'
5252
import { getBaseRenderOptions } from './utils/renderOptions.js'
5353
import { getSettingsWithAllErrors } from './utils/settings/allErrors.js'
5454
import {
55-
hasAutoModeOptIn,
5655
hasSkipDangerousModePermissionPrompt,
5756
} from './utils/settings/settings.js'
5857

@@ -309,25 +308,6 @@ export async function showSetupScreens(
309308
))
310309
}
311310

312-
if (feature('TRANSCRIPT_CLASSIFIER')) {
313-
// Only show the opt-in dialog if auto mode actually resolved — if the
314-
// gate denied it (org not allowlisted, settings disabled), showing
315-
// consent for an unavailable feature is pointless. The
316-
// verifyAutoModeGateAccess notification will explain why instead.
317-
if (permissionMode === 'auto' && !hasAutoModeOptIn()) {
318-
const { AutoModeOptInDialog } = await import(
319-
'./components/AutoModeOptInDialog.js'
320-
)
321-
await showSetupDialog(root, done => (
322-
<AutoModeOptInDialog
323-
onAccept={done}
324-
onDecline={() => gracefulShutdownSync(1)}
325-
declineExits
326-
/>
327-
))
328-
}
329-
}
330-
331311
// --dangerously-load-development-channels confirmation. On accept, append
332312
// dev channels to any --channels list already set in main.tsx. Org policy
333313
// is NOT bypassed — gateChannelServer() still runs; this flag only exists

src/main.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ import {
242242
import { ensureModelStringsInitialized } from "./utils/model/modelStrings.js";
243243
import { PERMISSION_MODES } from "./utils/permissions/PermissionMode.js";
244244
import {
245-
checkAndDisableBypassPermissions,
246245
getAutoModeEnabledStateIfCached,
247246
initializeToolPermissionContext,
248247
initialPermissionModeFromCLI,
@@ -3910,19 +3909,7 @@ async function run(): Promise<CommanderCommand> {
39103909
onChangeAppState,
39113910
);
39123911

3913-
// Check if bypassPermissions should be disabled based on Statsig gate
3914-
// This runs in parallel to the code below, to avoid blocking the main loop.
3915-
if (
3916-
toolPermissionContext.mode === "bypassPermissions" ||
3917-
allowDangerouslySkipPermissions
3918-
) {
3919-
void checkAndDisableBypassPermissions(
3920-
toolPermissionContext,
3921-
);
3922-
}
3923-
39243912
// Async check of auto mode gate — corrects state and disables auto if needed.
3925-
// Gated on TRANSCRIPT_CLASSIFIER (not USER_TYPE) so GrowthBook kill switch runs for external builds too.
39263913
if (feature("TRANSCRIPT_CLASSIFIER")) {
39273914
void verifyAutoModeGateAccess(
39283915
toolPermissionContext,

0 commit comments

Comments
 (0)