fix(chat): enable draft auto-accept before first message (#2045)
* fix(chat): enable draft auto-accept before first message * fix(test): use supported bun assertions * fix(chat): apply draft auto-accept before session switch --------- Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
bashrusakh
parent
0242765bc8
commit
9bfc5bf0be
@@ -75,6 +75,7 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|||||||
import { createWorktreeDraft } from '@/lib/worktreeSessionCreator';
|
import { createWorktreeDraft } from '@/lib/worktreeSessionCreator';
|
||||||
import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
|
import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
|
||||||
import { usePermissionStore } from '@/stores/permissionStore';
|
import { usePermissionStore } from '@/stores/permissionStore';
|
||||||
|
import { togglePermissionAutoAccept } from './permissionAutoAccept';
|
||||||
import { extractGitChangedFiles } from './changedFiles';
|
import { extractGitChangedFiles } from './changedFiles';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
@@ -648,7 +649,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
|
|||||||
type PermissionAutoAcceptButtonProps = {
|
type PermissionAutoAcceptButtonProps = {
|
||||||
footerIconButtonClass: string;
|
footerIconButtonClass: string;
|
||||||
iconSizeClass: string;
|
iconSizeClass: string;
|
||||||
permissionScopeSessionId: string | null;
|
isInteractive: boolean;
|
||||||
permissionAutoAcceptEnabled: boolean;
|
permissionAutoAcceptEnabled: boolean;
|
||||||
handlePermissionAutoAcceptToggle: () => void;
|
handlePermissionAutoAcceptToggle: () => void;
|
||||||
withTooltip?: boolean;
|
withTooltip?: boolean;
|
||||||
@@ -659,7 +660,7 @@ const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButto
|
|||||||
const {
|
const {
|
||||||
footerIconButtonClass,
|
footerIconButtonClass,
|
||||||
iconSizeClass,
|
iconSizeClass,
|
||||||
permissionScopeSessionId,
|
isInteractive,
|
||||||
permissionAutoAcceptEnabled,
|
permissionAutoAcceptEnabled,
|
||||||
handlePermissionAutoAcceptToggle,
|
handlePermissionAutoAcceptToggle,
|
||||||
withTooltip = false,
|
withTooltip = false,
|
||||||
@@ -679,7 +680,7 @@ const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButto
|
|||||||
className={cn(
|
className={cn(
|
||||||
footerIconButtonClass,
|
footerIconButtonClass,
|
||||||
'rounded-md hover:bg-transparent',
|
'rounded-md hover:bg-transparent',
|
||||||
!permissionScopeSessionId && 'opacity-30',
|
!isInteractive && 'opacity-30',
|
||||||
)}
|
)}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -1083,7 +1084,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
);
|
);
|
||||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||||
const newSessionDraftOpen = Boolean(newSessionDraft?.open);
|
const newSessionDraftOpen = Boolean(newSessionDraft?.open);
|
||||||
|
const draftPermissionAutoAcceptEnabled = useSessionUIStore((s) => (
|
||||||
|
s.newSessionDraft?.open ? s.newSessionDraft.permissionAutoAcceptEnabled === true : false
|
||||||
|
));
|
||||||
const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget);
|
const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget);
|
||||||
|
const setDraftPermissionAutoAcceptEnabled = useSessionUIStore((s) => s.setDraftPermissionAutoAcceptEnabled);
|
||||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||||
const availableWorktreesByProject = useSessionUIStore((s) => s.availableWorktreesByProject);
|
const availableWorktreesByProject = useSessionUIStore((s) => s.availableWorktreesByProject);
|
||||||
const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId);
|
const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId);
|
||||||
@@ -4523,22 +4528,32 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
const permissionScopeSessionId = currentSessionId ?? currentManagementSessionId;
|
const permissionScopeSessionId = currentSessionId ?? currentManagementSessionId;
|
||||||
const permissionAutoAcceptEnabled = usePermissionStore((state) => {
|
const permissionAutoAcceptEnabled = usePermissionStore((state) => {
|
||||||
if (!permissionScopeSessionId) {
|
if (!permissionScopeSessionId) {
|
||||||
return false;
|
return draftPermissionAutoAcceptEnabled;
|
||||||
}
|
}
|
||||||
return state.isSessionAutoAccepting(permissionScopeSessionId);
|
return state.isSessionAutoAccepting(permissionScopeSessionId);
|
||||||
});
|
});
|
||||||
|
const isPermissionAutoAcceptInteractive = Boolean(permissionScopeSessionId || newSessionDraftOpen);
|
||||||
|
|
||||||
const handlePermissionAutoAcceptToggle = React.useCallback(() => {
|
const handlePermissionAutoAcceptToggle = React.useCallback(() => {
|
||||||
if (!permissionScopeSessionId) {
|
togglePermissionAutoAccept({
|
||||||
toast.error(t('chat.chatInput.toast.openSessionFirst'));
|
permissionScopeSessionId,
|
||||||
return;
|
newSessionDraftOpen,
|
||||||
}
|
draftPermissionAutoAcceptEnabled,
|
||||||
|
permissionAutoAcceptEnabled,
|
||||||
const nextEnabled = !permissionAutoAcceptEnabled;
|
setDraftPermissionAutoAcceptEnabled,
|
||||||
setSessionAutoAccept(permissionScopeSessionId, nextEnabled).catch(() => {
|
setSessionAutoAccept,
|
||||||
toast.error(t('chat.chatInput.toast.togglePermissionAutoAcceptFailed'));
|
onOpenSessionFirst: () => toast.error(t('chat.chatInput.toast.openSessionFirst')),
|
||||||
|
onToggleFailed: () => toast.error(t('chat.chatInput.toast.togglePermissionAutoAcceptFailed')),
|
||||||
});
|
});
|
||||||
}, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept, t]);
|
}, [
|
||||||
|
draftPermissionAutoAcceptEnabled,
|
||||||
|
newSessionDraftOpen,
|
||||||
|
permissionAutoAcceptEnabled,
|
||||||
|
permissionScopeSessionId,
|
||||||
|
setDraftPermissionAutoAcceptEnabled,
|
||||||
|
setSessionAutoAccept,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
|
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
|
||||||
@@ -5296,7 +5311,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
<PermissionAutoAcceptButton
|
<PermissionAutoAcceptButton
|
||||||
footerIconButtonClass={footerIconButtonClass}
|
footerIconButtonClass={footerIconButtonClass}
|
||||||
iconSizeClass={iconSizeClass}
|
iconSizeClass={iconSizeClass}
|
||||||
permissionScopeSessionId={permissionScopeSessionId}
|
isInteractive={isPermissionAutoAcceptInteractive}
|
||||||
permissionAutoAcceptEnabled={permissionAutoAcceptEnabled}
|
permissionAutoAcceptEnabled={permissionAutoAcceptEnabled}
|
||||||
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
||||||
/>
|
/>
|
||||||
@@ -5364,7 +5379,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
<PermissionAutoAcceptButton
|
<PermissionAutoAcceptButton
|
||||||
footerIconButtonClass={footerIconButtonClass}
|
footerIconButtonClass={footerIconButtonClass}
|
||||||
iconSizeClass={iconSizeClass}
|
iconSizeClass={iconSizeClass}
|
||||||
permissionScopeSessionId={permissionScopeSessionId}
|
isInteractive={isPermissionAutoAcceptInteractive}
|
||||||
permissionAutoAcceptEnabled={permissionAutoAcceptEnabled}
|
permissionAutoAcceptEnabled={permissionAutoAcceptEnabled}
|
||||||
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
||||||
withTooltip
|
withTooltip
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export type PermissionAutoAcceptToggleArgs = {
|
||||||
|
permissionScopeSessionId: string | null;
|
||||||
|
newSessionDraftOpen: boolean;
|
||||||
|
draftPermissionAutoAcceptEnabled: boolean;
|
||||||
|
permissionAutoAcceptEnabled: boolean;
|
||||||
|
setDraftPermissionAutoAcceptEnabled: (enabled: boolean) => void;
|
||||||
|
setSessionAutoAccept: (sessionId: string, enabled: boolean) => Promise<void>;
|
||||||
|
onOpenSessionFirst: () => void;
|
||||||
|
onToggleFailed: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const togglePermissionAutoAccept = (args: PermissionAutoAcceptToggleArgs): void => {
|
||||||
|
const {
|
||||||
|
permissionScopeSessionId,
|
||||||
|
newSessionDraftOpen,
|
||||||
|
draftPermissionAutoAcceptEnabled,
|
||||||
|
permissionAutoAcceptEnabled,
|
||||||
|
setDraftPermissionAutoAcceptEnabled,
|
||||||
|
setSessionAutoAccept,
|
||||||
|
onOpenSessionFirst,
|
||||||
|
onToggleFailed,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
if (!permissionScopeSessionId) {
|
||||||
|
if (!newSessionDraftOpen) {
|
||||||
|
onOpenSessionFirst();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraftPermissionAutoAcceptEnabled(!draftPermissionAutoAcceptEnabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEnabled = !permissionAutoAcceptEnabled;
|
||||||
|
void setSessionAutoAccept(permissionScopeSessionId, nextEnabled).catch(onToggleFailed);
|
||||||
|
};
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import { beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
|
import { togglePermissionAutoAccept } from "../../components/chat/permissionAutoAccept"
|
||||||
|
|
||||||
|
const storage = new Map<string, string>()
|
||||||
|
const createSessionCalls: Array<{ title?: string; directory: string | null; parentID: string | null; metadata?: unknown }> = []
|
||||||
|
const permissionAutoAcceptCalls: Array<[string, boolean]> = []
|
||||||
|
|
||||||
|
const getMockCalls = (fn: unknown): unknown[][] => ((fn as { mock?: { calls: unknown[][] } }).mock?.calls ?? [])
|
||||||
|
|
||||||
|
mock.module("zustand", () => ({
|
||||||
|
create: () => (initializer: (set: (patch: unknown | ((state: unknown) => unknown)) => void, get: () => unknown) => Record<string, unknown>) => {
|
||||||
|
let state: Record<string, unknown>
|
||||||
|
const get = () => state
|
||||||
|
const set = (patch: unknown | ((current: Record<string, unknown>) => unknown)) => {
|
||||||
|
const next = typeof patch === "function" ? patch(state) : patch
|
||||||
|
state = next && typeof next === "object" ? { ...state, ...(next as Record<string, unknown>) } : state
|
||||||
|
}
|
||||||
|
|
||||||
|
state = initializer(set, get)
|
||||||
|
|
||||||
|
const store = ((selector?: (current: Record<string, unknown>) => unknown) => (
|
||||||
|
typeof selector === "function" ? selector(state) : state
|
||||||
|
)) as unknown as {
|
||||||
|
getState: () => Record<string, unknown>
|
||||||
|
setState: (patch: unknown | ((current: Record<string, unknown>) => unknown)) => void
|
||||||
|
subscribe: () => () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
store.getState = () => state
|
||||||
|
store.setState = (patch) => set(patch)
|
||||||
|
store.subscribe = () => () => undefined
|
||||||
|
|
||||||
|
return store
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const deferredStorage: Storage = {
|
||||||
|
getItem: (key: string) => storage.get(key) ?? null,
|
||||||
|
setItem: (key: string, value: string) => {
|
||||||
|
storage.set(key, value)
|
||||||
|
},
|
||||||
|
removeItem: (key: string) => {
|
||||||
|
storage.delete(key)
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
storage.clear()
|
||||||
|
},
|
||||||
|
key: (index: number) => Array.from(storage.keys())[index] ?? null,
|
||||||
|
get length() {
|
||||||
|
return storage.size
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.module("@/stores/utils/safeStorage", () => ({
|
||||||
|
getDeferredSafeStorage: () => deferredStorage,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/lib/opencode/client", () => ({
|
||||||
|
opencodeClient: {
|
||||||
|
getDirectory: () => null,
|
||||||
|
setDirectory: mock(() => undefined),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/permissionStore", () => ({
|
||||||
|
usePermissionStore: {
|
||||||
|
getState: () => ({
|
||||||
|
setSessionAutoAccept: mock(async (sessionId: string, enabled: boolean) => {
|
||||||
|
permissionAutoAcceptCalls.push([sessionId, enabled])
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useConfigStore", () => ({
|
||||||
|
useConfigStore: {
|
||||||
|
getState: () => ({
|
||||||
|
currentAgentName: "agent-default",
|
||||||
|
agents: [],
|
||||||
|
activateDirectory: mock(async () => undefined),
|
||||||
|
applyDefaultModelAgentSelection: mock(() => undefined),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useProjectsStore", () => ({
|
||||||
|
useProjectsStore: {
|
||||||
|
getState: () => ({
|
||||||
|
projects: [],
|
||||||
|
activeProjectId: null,
|
||||||
|
getActiveProject: () => null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useDirectoryStore", () => ({
|
||||||
|
useDirectoryStore: {
|
||||||
|
getState: () => ({
|
||||||
|
currentDirectory: null,
|
||||||
|
setDirectory: mock(() => undefined),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useGlobalSessionsStore", () => ({
|
||||||
|
useGlobalSessionsStore: {
|
||||||
|
getState: () => ({
|
||||||
|
activeSessions: [],
|
||||||
|
archivedSessions: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolveGlobalSessionDirectory: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useSessionFoldersStore", () => ({
|
||||||
|
useSessionFoldersStore: {
|
||||||
|
getState: () => ({
|
||||||
|
addSessionToFolder: mock(() => undefined),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useCommandsStore", () => ({
|
||||||
|
useCommandsStore: {
|
||||||
|
getState: () => ({
|
||||||
|
commands: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/stores/useSkillsStore", () => ({
|
||||||
|
useSkillsStore: {
|
||||||
|
getState: () => ({
|
||||||
|
skills: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/components/ui", () => ({
|
||||||
|
toast: {
|
||||||
|
error: () => undefined,
|
||||||
|
info: () => undefined,
|
||||||
|
success: () => undefined,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../selection-store", () => ({
|
||||||
|
useSelectionStore: {
|
||||||
|
getState: () => ({
|
||||||
|
saveSessionModelSelection: () => undefined,
|
||||||
|
saveSessionAgentSelection: () => undefined,
|
||||||
|
saveAgentModelForSession: () => undefined,
|
||||||
|
saveAgentModelVariantForSession: () => undefined,
|
||||||
|
getSessionAgentSelection: () => null,
|
||||||
|
getSessionModelSelection: () => null,
|
||||||
|
getAgentModelForSession: () => null,
|
||||||
|
getAgentModelVariantForSession: () => undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/lib/runtime-switch", () => ({
|
||||||
|
getRuntimeApiBaseUrl: () => "",
|
||||||
|
getRuntimeKey: () => "test-runtime",
|
||||||
|
initializeRuntimeEndpoint: () => undefined,
|
||||||
|
subscribeRuntimeEndpointChanged: () => () => undefined,
|
||||||
|
switchRuntimeEndpoint: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/lib/userSendAnimation", () => ({
|
||||||
|
markPendingUserSendAnimation: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../sync-context", () => ({
|
||||||
|
setActiveSession: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../notification-store", () => ({
|
||||||
|
markSessionViewed: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../session-navigation", () => ({
|
||||||
|
setSessionOpener: () => undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../session-worktree-contract", () => ({
|
||||||
|
getAttachedSessionDirectory: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../session-worktree-store", () => ({
|
||||||
|
useSessionWorktreeStore: {
|
||||||
|
getState: () => ({
|
||||||
|
getAttachment: () => undefined,
|
||||||
|
setAttachment: () => undefined,
|
||||||
|
clearAttachment: () => undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../viewport-store", () => ({
|
||||||
|
getViewportSessionMemory: () => null,
|
||||||
|
viewportSessionKey: (sessionId: string) => sessionId,
|
||||||
|
useViewportStore: {
|
||||||
|
getState: () => ({
|
||||||
|
updateViewportAnchor: mock(() => undefined),
|
||||||
|
}),
|
||||||
|
setState: () => undefined,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../input-store", () => ({
|
||||||
|
useInputStore: {
|
||||||
|
getState: () => ({
|
||||||
|
clearAttachedFiles: () => undefined,
|
||||||
|
setPendingInputText: () => undefined,
|
||||||
|
addRestoredAttachment: () => undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../sync-refs", () => ({
|
||||||
|
getDirectoryState: () => null,
|
||||||
|
getSyncSessions: () => [],
|
||||||
|
getSyncMessages: () => [],
|
||||||
|
getSyncParts: () => [],
|
||||||
|
getAllSyncSessions: () => [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../session-actions", () => ({
|
||||||
|
createSession: mock(async (title: string | undefined, directory: string | null, parentID: string | null, metadata?: unknown) => {
|
||||||
|
createSessionCalls.push({ title, directory, parentID, metadata })
|
||||||
|
return { id: "ses_issue_2039", directory }
|
||||||
|
}),
|
||||||
|
deleteSession: mock(async () => true),
|
||||||
|
archiveSession: mock(async () => true),
|
||||||
|
updateSessionTitle: mock(async () => undefined),
|
||||||
|
shareSession: mock(async () => undefined),
|
||||||
|
unshareSession: mock(async () => undefined),
|
||||||
|
optimisticSend: mock(async () => undefined),
|
||||||
|
refetchSessionMessages: mock(async () => undefined),
|
||||||
|
revertToMessage: mock(async () => undefined),
|
||||||
|
unrevertSession: mock(async () => undefined),
|
||||||
|
forkFromMessage: mock(async () => undefined),
|
||||||
|
fetchMessagesForSession: mock(async () => undefined),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { materializeOpenDraftSession, useSessionUIStore } = await import("../session-ui-store")
|
||||||
|
|
||||||
|
describe("issue 2039 draft auto-accept", () => {
|
||||||
|
test("toggles draft state before a session exists", () => {
|
||||||
|
const setDraftPermissionAutoAcceptEnabled = mock(() => undefined)
|
||||||
|
const setSessionAutoAccept = mock(async () => undefined)
|
||||||
|
const onOpenSessionFirst = mock(() => undefined)
|
||||||
|
const onToggleFailed = mock(() => undefined)
|
||||||
|
|
||||||
|
togglePermissionAutoAccept({
|
||||||
|
permissionScopeSessionId: null,
|
||||||
|
newSessionDraftOpen: true,
|
||||||
|
draftPermissionAutoAcceptEnabled: false,
|
||||||
|
permissionAutoAcceptEnabled: false,
|
||||||
|
setDraftPermissionAutoAcceptEnabled,
|
||||||
|
setSessionAutoAccept,
|
||||||
|
onOpenSessionFirst,
|
||||||
|
onToggleFailed,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMockCalls(setDraftPermissionAutoAcceptEnabled).length).toBe(1)
|
||||||
|
expect(getMockCalls(setDraftPermissionAutoAcceptEnabled)[0]).toEqual([true])
|
||||||
|
expect(getMockCalls(setSessionAutoAccept).length).toBe(0)
|
||||||
|
expect(getMockCalls(onOpenSessionFirst).length).toBe(0)
|
||||||
|
expect(getMockCalls(onToggleFailed).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("guards the toggle when no draft is open", () => {
|
||||||
|
const setDraftPermissionAutoAcceptEnabled = mock(() => undefined)
|
||||||
|
const setSessionAutoAccept = mock(async () => undefined)
|
||||||
|
const onOpenSessionFirst = mock(() => undefined)
|
||||||
|
const onToggleFailed = mock(() => undefined)
|
||||||
|
|
||||||
|
togglePermissionAutoAccept({
|
||||||
|
permissionScopeSessionId: null,
|
||||||
|
newSessionDraftOpen: false,
|
||||||
|
draftPermissionAutoAcceptEnabled: false,
|
||||||
|
permissionAutoAcceptEnabled: false,
|
||||||
|
setDraftPermissionAutoAcceptEnabled,
|
||||||
|
setSessionAutoAccept,
|
||||||
|
onOpenSessionFirst,
|
||||||
|
onToggleFailed,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMockCalls(setDraftPermissionAutoAcceptEnabled).length).toBe(0)
|
||||||
|
expect(getMockCalls(setSessionAutoAccept).length).toBe(0)
|
||||||
|
expect(getMockCalls(onOpenSessionFirst).length).toBe(1)
|
||||||
|
expect(getMockCalls(onToggleFailed).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
storage.clear()
|
||||||
|
createSessionCalls.length = 0
|
||||||
|
permissionAutoAcceptCalls.length = 0
|
||||||
|
|
||||||
|
useSessionUIStore.setState({
|
||||||
|
currentSessionId: null,
|
||||||
|
currentSessionDirectory: null,
|
||||||
|
newSessionDraft: {
|
||||||
|
open: false,
|
||||||
|
directoryOverride: null,
|
||||||
|
parentID: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("stores auto-accept in the draft and applies it when the session materializes", async () => {
|
||||||
|
useSessionUIStore.getState().openNewSessionDraft()
|
||||||
|
|
||||||
|
expect(useSessionUIStore.getState().newSessionDraft.permissionAutoAcceptEnabled).toBe(false)
|
||||||
|
|
||||||
|
useSessionUIStore.getState().setDraftPermissionAutoAcceptEnabled(true)
|
||||||
|
|
||||||
|
expect(useSessionUIStore.getState().newSessionDraft.permissionAutoAcceptEnabled).toBe(true)
|
||||||
|
|
||||||
|
const result = await materializeOpenDraftSession({
|
||||||
|
providerID: "provider",
|
||||||
|
modelID: "model",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result?.sessionId).toBe("ses_issue_2039")
|
||||||
|
expect(createSessionCalls).toHaveLength(1)
|
||||||
|
expect(permissionAutoAcceptCalls).toEqual([["ses_issue_2039", true]])
|
||||||
|
expect(useSessionUIStore.getState().currentSessionId).toBe("ses_issue_2039")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not apply draft auto-accept after the draft is closed", async () => {
|
||||||
|
useSessionUIStore.getState().openNewSessionDraft()
|
||||||
|
useSessionUIStore.getState().setDraftPermissionAutoAcceptEnabled(true)
|
||||||
|
useSessionUIStore.getState().closeNewSessionDraft()
|
||||||
|
|
||||||
|
expect(useSessionUIStore.getState().newSessionDraft.open).toBe(false)
|
||||||
|
expect(useSessionUIStore.getState().newSessionDraft.permissionAutoAcceptEnabled === undefined).toBe(true)
|
||||||
|
|
||||||
|
const result = await materializeOpenDraftSession({
|
||||||
|
providerID: "provider",
|
||||||
|
modelID: "model",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(createSessionCalls).toHaveLength(0)
|
||||||
|
expect(permissionAutoAcceptCalls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -195,6 +195,7 @@ export type NewSessionDraftState = {
|
|||||||
open: boolean
|
open: boolean
|
||||||
selectedProjectId?: string | null
|
selectedProjectId?: string | null
|
||||||
directoryOverride: string | null
|
directoryOverride: string | null
|
||||||
|
permissionAutoAcceptEnabled?: boolean
|
||||||
pendingWorktreeRequestId?: string | null
|
pendingWorktreeRequestId?: string | null
|
||||||
bootstrapPendingDirectory?: string | null
|
bootstrapPendingDirectory?: string | null
|
||||||
preserveDirectoryOverride?: boolean
|
preserveDirectoryOverride?: boolean
|
||||||
@@ -251,6 +252,7 @@ export type SessionUIState = {
|
|||||||
closeNewSessionDraft: () => void
|
closeNewSessionDraft: () => void
|
||||||
setNewSessionDraftTarget: (target: { projectId?: string | null; selectedProjectId?: string | null; directoryOverride?: string | null }, options?: { force?: boolean }) => void
|
setNewSessionDraftTarget: (target: { projectId?: string | null; selectedProjectId?: string | null; directoryOverride?: string | null }, options?: { force?: boolean }) => void
|
||||||
setDraftPreserveDirectoryOverride: (value: boolean) => void
|
setDraftPreserveDirectoryOverride: (value: boolean) => void
|
||||||
|
setDraftPermissionAutoAcceptEnabled: (enabled: boolean) => void
|
||||||
acknowledgeSessionAbort: (sessionId: string) => void
|
acknowledgeSessionAbort: (sessionId: string) => void
|
||||||
clearAbortPrompt: () => void
|
clearAbortPrompt: () => void
|
||||||
armAbortPrompt: (durationMs?: number) => number | null
|
armAbortPrompt: (durationMs?: number) => number | null
|
||||||
@@ -446,6 +448,7 @@ export async function materializeOpenDraftSession(selection: {
|
|||||||
const store = useSessionUIStore.getState()
|
const store = useSessionUIStore.getState()
|
||||||
const draft = store.newSessionDraft
|
const draft = store.newSessionDraft
|
||||||
if (!draft?.open) return null
|
if (!draft?.open) return null
|
||||||
|
const draftPermissionAutoAcceptEnabled = draft.permissionAutoAcceptEnabled === true
|
||||||
|
|
||||||
const trimmedAgent = typeof selection.agent === "string" && selection.agent.trim().length > 0
|
const trimmedAgent = typeof selection.agent === "string" && selection.agent.trim().length > 0
|
||||||
? selection.agent.trim()
|
? selection.agent.trim()
|
||||||
@@ -485,6 +488,15 @@ export async function materializeOpenDraftSession(selection: {
|
|||||||
useSelectionStore.getState().saveAgentModelVariantForSession(created.id, effectiveDraftAgent, selection.providerID, selection.modelID, selection.variant)
|
useSelectionStore.getState().saveAgentModelVariantForSession(created.id, effectiveDraftAgent, selection.providerID, selection.modelID, selection.variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (draftPermissionAutoAcceptEnabled) {
|
||||||
|
try {
|
||||||
|
const { usePermissionStore } = await import("@/stores/permissionStore")
|
||||||
|
await usePermissionStore.getState().setSessionAutoAccept(created.id, true)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to apply draft permission auto-accept to new session:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
store.initializeNewOpenChamberSession(created.id, configState.agents ?? [])
|
store.initializeNewOpenChamberSession(created.id, configState.agents ?? [])
|
||||||
|
|
||||||
store.setCurrentSession(created.id, createdDirectory)
|
store.setCurrentSession(created.id, createdDirectory)
|
||||||
@@ -732,6 +744,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
open: true,
|
open: true,
|
||||||
selectedProjectId: selectedProject?.id ?? null,
|
selectedProjectId: selectedProject?.id ?? null,
|
||||||
directoryOverride: directory,
|
directoryOverride: directory,
|
||||||
|
permissionAutoAcceptEnabled: options?.permissionAutoAcceptEnabled === true,
|
||||||
pendingWorktreeRequestId: options?.pendingWorktreeRequestId ?? null,
|
pendingWorktreeRequestId: options?.pendingWorktreeRequestId ?? null,
|
||||||
bootstrapPendingDirectory: normalizePath(options?.bootstrapPendingDirectory ?? null),
|
bootstrapPendingDirectory: normalizePath(options?.bootstrapPendingDirectory ?? null),
|
||||||
preserveDirectoryOverride: options?.preserveDirectoryOverride === true,
|
preserveDirectoryOverride: options?.preserveDirectoryOverride === true,
|
||||||
@@ -827,6 +840,12 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
return { newSessionDraft: { ...s.newSessionDraft, preserveDirectoryOverride: value } }
|
return { newSessionDraft: { ...s.newSessionDraft, preserveDirectoryOverride: value } }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setDraftPermissionAutoAcceptEnabled: (enabled) =>
|
||||||
|
set((s) => {
|
||||||
|
if (!s.newSessionDraft?.open) return s
|
||||||
|
return { newSessionDraft: { ...s.newSessionDraft, permissionAutoAcceptEnabled: enabled } }
|
||||||
|
}),
|
||||||
|
|
||||||
acknowledgeSessionAbort: (sessionId) =>
|
acknowledgeSessionAbort: (sessionId) =>
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const flags = new Map(s.sessionAbortFlags)
|
const flags = new Map(s.sessionAbortFlags)
|
||||||
|
|||||||
Reference in New Issue
Block a user