fix(chat): limit draft transition animation
This commit is contained in:
@@ -566,6 +566,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
|||||||
// Session UI state
|
// Session UI state
|
||||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||||
const currentSessionDirectory = useSessionUIStore((s) => s.currentSessionDirectory);
|
const currentSessionDirectory = useSessionUIStore((s) => s.currentSessionDirectory);
|
||||||
|
const materializedDraftSessionId = useSessionUIStore((s) => s.materializedDraftSessionId);
|
||||||
|
const clearMaterializedDraftSession = useSessionUIStore((s) => s.clearMaterializedDraftSession);
|
||||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||||
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
|
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
|
||||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||||
@@ -1107,8 +1109,11 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
|||||||
const previousDraftOpenRef = React.useRef(draftOpen);
|
const previousDraftOpenRef = React.useRef(draftOpen);
|
||||||
const previousDraftLayoutVisibleRef = React.useRef(draftOpen);
|
const previousDraftLayoutVisibleRef = React.useRef(draftOpen);
|
||||||
const [draftExitAnimating, setDraftExitAnimating] = React.useState(false);
|
const [draftExitAnimating, setDraftExitAnimating] = React.useState(false);
|
||||||
|
const shouldAnimateDraftTransition = Boolean(
|
||||||
|
currentSessionId && materializedDraftSessionId === currentSessionId,
|
||||||
|
);
|
||||||
const draftPresentationExiting = draftExitAnimating
|
const draftPresentationExiting = draftExitAnimating
|
||||||
|| (previousDraftOpenRef.current && !draftOpen && Boolean(currentSessionId));
|
|| (previousDraftOpenRef.current && !draftOpen && shouldAnimateDraftTransition);
|
||||||
const draftLayoutVisible = draftOpen || draftPresentationExiting;
|
const draftLayoutVisible = draftOpen || draftPresentationExiting;
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
@@ -1116,12 +1121,12 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
|||||||
setDraftExitAnimating(false);
|
setDraftExitAnimating(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!previousDraftOpenRef.current || !currentSessionId) return;
|
if (!previousDraftOpenRef.current || !shouldAnimateDraftTransition) return;
|
||||||
|
|
||||||
setDraftExitAnimating(true);
|
setDraftExitAnimating(true);
|
||||||
const timeoutId = window.setTimeout(() => setDraftExitAnimating(false), DRAFT_EXIT_DURATION_MS);
|
const timeoutId = window.setTimeout(() => setDraftExitAnimating(false), DRAFT_EXIT_DURATION_MS);
|
||||||
return () => window.clearTimeout(timeoutId);
|
return () => window.clearTimeout(timeoutId);
|
||||||
}, [currentSessionId, draftOpen]);
|
}, [draftOpen, shouldAnimateDraftTransition]);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
previousDraftOpenRef.current = draftOpen;
|
previousDraftOpenRef.current = draftOpen;
|
||||||
@@ -1139,7 +1144,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
|||||||
&& Boolean(currentSessionId);
|
&& Boolean(currentSessionId);
|
||||||
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
||||||
|
|
||||||
if (leftDraftLayout && previousRect && !reduceMotion && !useCompactDraftLayout && !isDesktopExpandedInput) {
|
const shouldMoveComposer = leftDraftLayout && shouldAnimateDraftTransition;
|
||||||
|
if (shouldMoveComposer && previousRect && !reduceMotion && !useCompactDraftLayout && !isDesktopExpandedInput) {
|
||||||
const deltaX = previousRect.left - currentRect.left;
|
const deltaX = previousRect.left - currentRect.left;
|
||||||
const deltaY = previousRect.top - currentRect.top;
|
const deltaY = previousRect.top - currentRect.top;
|
||||||
composerSlot.animate(
|
composerSlot.animate(
|
||||||
@@ -1150,10 +1156,19 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
|||||||
{ duration: COMPOSER_MOVE_DURATION_MS, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' },
|
{ duration: COMPOSER_MOVE_DURATION_MS, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousComposerRectRef.current = currentRect;
|
previousComposerRectRef.current = currentRect;
|
||||||
previousDraftLayoutVisibleRef.current = draftLayoutVisible;
|
previousDraftLayoutVisibleRef.current = draftLayoutVisible;
|
||||||
}, [currentSessionId, draftLayoutVisible, isDesktopExpandedInput, useCompactDraftLayout]);
|
if (leftDraftLayout && currentSessionId) {
|
||||||
|
clearMaterializedDraftSession(currentSessionId);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
clearMaterializedDraftSession,
|
||||||
|
currentSessionId,
|
||||||
|
draftLayoutVisible,
|
||||||
|
isDesktopExpandedInput,
|
||||||
|
shouldAnimateDraftTransition,
|
||||||
|
useCompactDraftLayout,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!currentSessionId && !draftOpen) {
|
if (!currentSessionId && !draftOpen) {
|
||||||
// The auto-open effect runs on the next tick. Use a neutral background
|
// The auto-open effect runs on the next tick. Use a neutral background
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ belongs to one of them.
|
|||||||
becomes its first session. Draft-only UI first fades for 120ms while the editor
|
becomes its first session. Draft-only UI first fades for 120ms while the editor
|
||||||
stays in place. The parent then moves the editor to its final session position
|
stays in place. The parent then moves the editor to its final session position
|
||||||
with a 180ms transform-only FLIP animation. Reduced-motion mode skips these
|
with a 180ms transform-only FLIP animation. Reduced-motion mode skips these
|
||||||
transitions. Do not restore separate draft and session composer branches:
|
transitions. `session-ui-store.ts` marks sessions materialized from a submitted
|
||||||
|
draft, so selecting an existing session while a draft is open switches without
|
||||||
|
animation. Do not restore separate draft and session composer branches:
|
||||||
remounting the editor loses focus and interrupts the transition. Keep the
|
remounting the editor loses focus and interrupts the transition. Keep the
|
||||||
existing mobile fixed-position rules unchanged.
|
existing mobile fixed-position rules unchanged.
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ So:
|
|||||||
| `global-session-status.ts` | Incremental non-idle session status index reconciled from events and authoritative directory snapshots | All known directories in the active runtime |
|
| `global-session-status.ts` | Incremental non-idle session status index reconciled from events and authoritative directory snapshots | All known directories in the active runtime |
|
||||||
| `session-ordering.ts` | Ephemeral lifecycle rank used by every user-visible session list | All known sessions in the active runtime |
|
| `session-ordering.ts` | Ephemeral lifecycle rank used by every user-visible session list | All known sessions in the active runtime |
|
||||||
| `session-activity-timing.ts` | Elapsed time of the running turn and of the turn that just finished, plus the persisted starts that survive a reload | All known sessions in the active runtime |
|
| `session-activity-timing.ts` | Elapsed time of the running turn and of the turn that just finished, plus the persisted starts that survive a reload | All known sessions in the active runtime |
|
||||||
| `session-ui-store.ts` | Session selection, draft lifecycle, abort prompts, worktree metadata, SDK-facing action entrypoints | App UI state |
|
| `session-ui-store.ts` | Session selection, draft lifecycle, one-shot draft-materialization transition identity, abort prompts, worktree metadata, SDK-facing action entrypoints | App UI state |
|
||||||
| `useGlobalSessionsStore.ts` | Global active sessions, global archived sessions, `sessionsByDirectory` | All opened project/worktree session lists |
|
| `useGlobalSessionsStore.ts` | Global active sessions, global archived sessions, `sessionsByDirectory` | All opened project/worktree session lists |
|
||||||
| `viewport-store.ts` | Scroll anchors, session memory, loading indicators | App UI state |
|
| `viewport-store.ts` | Scroll anchors, session memory, loading indicators | App UI state |
|
||||||
| `attachment-files.ts` | Attachment picker allowlists, MIME/content validation, structured-text sanitization, and HEIC conversion | Local chat attachments across shared UI runtimes |
|
| `attachment-files.ts` | Attachment picker allowlists, MIME/content validation, structured-text sanitization, and HEIC conversion | Local chat attachments across shared UI runtimes |
|
||||||
|
|||||||
@@ -727,6 +727,7 @@ export async function createSession(
|
|||||||
directoryOverride?: string | null,
|
directoryOverride?: string | null,
|
||||||
parentID?: string | null,
|
parentID?: string | null,
|
||||||
metadata?: Record<string, unknown>,
|
metadata?: Record<string, unknown>,
|
||||||
|
selectionTransition?: "submitted-draft",
|
||||||
): Promise<Session | null> {
|
): Promise<Session | null> {
|
||||||
try {
|
try {
|
||||||
// Capture the effective directory used for session creation so we can fall
|
// Capture the effective directory used for session creation so we can fall
|
||||||
@@ -747,7 +748,7 @@ export async function createSession(
|
|||||||
if (sessionDirectory) {
|
if (sessionDirectory) {
|
||||||
registerSessionDirectory(session.id, sessionDirectory)
|
registerSessionDirectory(session.id, sessionDirectory)
|
||||||
}
|
}
|
||||||
useSessionUIStore.getState().setCurrentSession(session.id, sessionDirectory)
|
useSessionUIStore.getState().setCurrentSession(session.id, sessionDirectory, selectionTransition)
|
||||||
useSessionUIStore.getState().markSessionAsOpenChamberCreated(session.id)
|
useSessionUIStore.getState().markSessionAsOpenChamberCreated(session.id)
|
||||||
useGlobalSessionsStore.getState().upsertSession(session)
|
useGlobalSessionsStore.getState().upsertSession(session)
|
||||||
return session
|
return session
|
||||||
|
|||||||
@@ -200,6 +200,47 @@ describe('session-worktree-store worktree routing', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('draft materialization transition identity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useSessionUIStore.setState({
|
||||||
|
currentSessionId: null,
|
||||||
|
currentSessionDirectory: null,
|
||||||
|
materializedDraftSessionId: null,
|
||||||
|
newSessionDraft: { open: true, target: 'project', directoryOverride: '/projects/alpha' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('marks and consumes only the submitted draft session', () => {
|
||||||
|
useSessionUIStore.getState().setCurrentSession(
|
||||||
|
'session-created',
|
||||||
|
'/projects/alpha',
|
||||||
|
'submitted-draft',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(useSessionUIStore.getState().materializedDraftSessionId).toBe('session-created');
|
||||||
|
|
||||||
|
useSessionUIStore.getState().clearMaterializedDraftSession('another-session');
|
||||||
|
expect(useSessionUIStore.getState().materializedDraftSessionId).toBe('session-created');
|
||||||
|
|
||||||
|
useSessionUIStore.getState().clearMaterializedDraftSession('session-created');
|
||||||
|
expect(useSessionUIStore.getState().materializedDraftSessionId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears the marker when navigating from a draft to an existing session', () => {
|
||||||
|
useSessionUIStore.getState().setCurrentSession(
|
||||||
|
'session-created',
|
||||||
|
'/projects/alpha',
|
||||||
|
'submitted-draft',
|
||||||
|
);
|
||||||
|
useSessionUIStore.setState({
|
||||||
|
newSessionDraft: { open: true, target: 'project', directoryOverride: '/projects/alpha' },
|
||||||
|
});
|
||||||
|
useSessionUIStore.getState().setCurrentSession('session-existing', '/projects/alpha');
|
||||||
|
|
||||||
|
expect(useSessionUIStore.getState().materializedDraftSessionId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('routeMessage directory scoping', () => {
|
describe('routeMessage directory scoping', () => {
|
||||||
test('runs sends in the provided session directory', async () => {
|
test('runs sends in the provided session directory', async () => {
|
||||||
// The session directory travels as an explicit request param (not via
|
// The session directory travels as an explicit request param (not via
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ export type SessionHistoryMeta = {
|
|||||||
export type SessionUIState = {
|
export type SessionUIState = {
|
||||||
currentSessionId: string | null
|
currentSessionId: string | null
|
||||||
currentSessionDirectory: string | null
|
currentSessionDirectory: string | null
|
||||||
|
materializedDraftSessionId: string | null
|
||||||
newSessionDraft: NewSessionDraftState
|
newSessionDraft: NewSessionDraftState
|
||||||
abortPromptSessionId: string | null
|
abortPromptSessionId: string | null
|
||||||
abortPromptExpiresAt: number | null
|
abortPromptExpiresAt: number | null
|
||||||
@@ -318,7 +319,12 @@ export type SessionUIState = {
|
|||||||
dismissPendingChangesBar: (sessionId: string, signature: string | null) => void
|
dismissPendingChangesBar: (sessionId: string, signature: string | null) => void
|
||||||
|
|
||||||
// Actions — UI state management
|
// Actions — UI state management
|
||||||
setCurrentSession: (id: string | null, directoryHint?: string | null) => void
|
setCurrentSession: (
|
||||||
|
id: string | null,
|
||||||
|
directoryHint?: string | null,
|
||||||
|
transition?: "submitted-draft",
|
||||||
|
) => void
|
||||||
|
clearMaterializedDraftSession: (sessionId: string) => void
|
||||||
prepareForRuntimeSwitch: (apiBaseUrl?: string | null) => void
|
prepareForRuntimeSwitch: (apiBaseUrl?: string | null) => void
|
||||||
restoreForRuntimeSwitch: (apiBaseUrl?: string | null) => void
|
restoreForRuntimeSwitch: (apiBaseUrl?: string | null) => void
|
||||||
openNewSessionDraft: (options?: Partial<NewSessionDraftState> & { automatic?: boolean }) => void
|
openNewSessionDraft: (options?: Partial<NewSessionDraftState> & { automatic?: boolean }) => void
|
||||||
@@ -357,7 +363,12 @@ export type SessionUIState = {
|
|||||||
options?: SendMessageOptions,
|
options?: SendMessageOptions,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
|
|
||||||
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null, metadata?: Record<string, unknown>) => Promise<Session | null>
|
createSession: (
|
||||||
|
title?: string,
|
||||||
|
directoryOverride?: string | null,
|
||||||
|
parentID?: string | null,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
) => Promise<Session | null>
|
||||||
deleteSession: (id: string, options?: DeleteSessionOptions) => Promise<boolean>
|
deleteSession: (id: string, options?: DeleteSessionOptions) => Promise<boolean>
|
||||||
deleteSessions: (ids: string[], options?: DeleteSessionsOptions) => Promise<{ deletedIds: string[]; failedIds: string[] }>
|
deleteSessions: (ids: string[], options?: DeleteSessionsOptions) => Promise<{ deletedIds: string[]; failedIds: string[] }>
|
||||||
archiveSession: (id: string) => Promise<boolean>
|
archiveSession: (id: string) => Promise<boolean>
|
||||||
@@ -715,6 +726,48 @@ const recoverStaleDraftDirectory = async (openedDraft: NewSessionDraftState): Pr
|
|||||||
void activateConfigForDirectory(recovered)
|
void activateConfigForDirectory(recovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createSessionWithDraftLifecycle = async (
|
||||||
|
title?: string,
|
||||||
|
directoryOverride?: string | null,
|
||||||
|
parentID?: string | null,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
selectionTransition?: "submitted-draft",
|
||||||
|
): Promise<Session | null> => {
|
||||||
|
const store = useSessionUIStore.getState()
|
||||||
|
const draft = store.newSessionDraft
|
||||||
|
const targetFolderId = draft.targetFolderId
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resolved = await resolveCreatableDraftDirectory(draft, directoryOverride)
|
||||||
|
if (resolved.status === "aborted") return null
|
||||||
|
const directory = resolved.directory
|
||||||
|
const session = await createSessionAction(
|
||||||
|
title,
|
||||||
|
directory,
|
||||||
|
parentID ?? null,
|
||||||
|
metadata,
|
||||||
|
selectionTransition,
|
||||||
|
)
|
||||||
|
if (!session) return null
|
||||||
|
|
||||||
|
useSessionUIStore.getState().closeNewSessionDraft()
|
||||||
|
|
||||||
|
if (targetFolderId) {
|
||||||
|
const currentStore = useSessionUIStore.getState()
|
||||||
|
const scopeDirectory = directory || currentStore.lastLoadedDirectory || session.directory
|
||||||
|
const scopeKey = getChatsRootFromDirectory(scopeDirectory) ?? scopeDirectory
|
||||||
|
if (scopeKey) {
|
||||||
|
useSessionFoldersStore.getState().addSessionToFolder(scopeKey, targetFolderId, session.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[session-ui-store] createSession failed", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function materializeOpenDraftSession(selection: {
|
export async function materializeOpenDraftSession(selection: {
|
||||||
providerID: string
|
providerID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
@@ -752,13 +805,14 @@ export async function materializeOpenDraftSession(selection: {
|
|||||||
await waitForWorktreeBootstrapIfConfigured(draftDirectoryOverride, draftProjectId)
|
await waitForWorktreeBootstrapIfConfigured(draftDirectoryOverride, draftProjectId)
|
||||||
|
|
||||||
const draftPins = draft.projectContextPins ?? { notes: [], plans: [] }
|
const draftPins = draft.projectContextPins ?? { notes: [], plans: [] }
|
||||||
const created = await store.createSession(
|
const created = await createSessionWithDraftLifecycle(
|
||||||
draft.title,
|
draft.title,
|
||||||
draftDirectoryOverride,
|
draftDirectoryOverride,
|
||||||
draft.parentID ?? null,
|
draft.parentID ?? null,
|
||||||
draftPins.notes.length > 0 || draftPins.plans.length > 0
|
draftPins.notes.length > 0 || draftPins.plans.length > 0
|
||||||
? { openchamber: { project_context_pins: draftPins } }
|
? { openchamber: { project_context_pins: draftPins } }
|
||||||
: undefined,
|
: undefined,
|
||||||
|
"submitted-draft",
|
||||||
)
|
)
|
||||||
if (!created?.id) {
|
if (!created?.id) {
|
||||||
if (isChatDraft && draftDirectoryOverride) {
|
if (isChatDraft && draftDirectoryOverride) {
|
||||||
@@ -796,8 +850,6 @@ export async function materializeOpenDraftSession(selection: {
|
|||||||
|
|
||||||
store.initializeNewOpenChamberSession(created.id, configState.agents ?? [])
|
store.initializeNewOpenChamberSession(created.id, configState.agents ?? [])
|
||||||
|
|
||||||
store.setCurrentSession(created.id, createdDirectory)
|
|
||||||
|
|
||||||
if (draftPermissionAutoAcceptEnabled) {
|
if (draftPermissionAutoAcceptEnabled) {
|
||||||
void import("@/stores/permissionStore")
|
void import("@/stores/permissionStore")
|
||||||
.then(({ usePermissionStore }) => usePermissionStore.getState().setSessionAutoAccept(created.id, true))
|
.then(({ usePermissionStore }) => usePermissionStore.getState().setSessionAutoAccept(created.id, true))
|
||||||
@@ -838,6 +890,7 @@ const PERSISTED_WORKTREE_MAP = readPersistedWorktreeTopology(runtimeMemoryKey())
|
|||||||
export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||||
currentSessionId: null,
|
currentSessionId: null,
|
||||||
currentSessionDirectory: null,
|
currentSessionDirectory: null,
|
||||||
|
materializedDraftSessionId: null,
|
||||||
newSessionDraft: { ...DEFAULT_DRAFT },
|
newSessionDraft: { ...DEFAULT_DRAFT },
|
||||||
abortPromptSessionId: null,
|
abortPromptSessionId: null,
|
||||||
abortPromptExpiresAt: null,
|
abortPromptExpiresAt: null,
|
||||||
@@ -856,7 +909,14 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// setCurrentSession
|
// setCurrentSession
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
setCurrentSession: (id, directoryHint?: string | null) => {
|
setCurrentSession: (id, directoryHint?: string | null, transition?: "submitted-draft") => {
|
||||||
|
const materializedDraftSessionId = id && transition === "submitted-draft" ? id : null
|
||||||
|
// Publish the transition identity before closing the draft. Those are two
|
||||||
|
// separate store updates, and ChatContainer must never observe a closed
|
||||||
|
// draft with the previous transition identity.
|
||||||
|
if (get().materializedDraftSessionId !== materializedDraftSessionId) {
|
||||||
|
set({ materializedDraftSessionId })
|
||||||
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
get().closeNewSessionDraft()
|
get().closeNewSessionDraft()
|
||||||
}
|
}
|
||||||
@@ -890,7 +950,10 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
|
|
||||||
// Set the directory together with the session id so chat hooks read the
|
// Set the directory together with the session id so chat hooks read the
|
||||||
// same child store that send/SSE events will update during startup races.
|
// same child store that send/SSE events will update during startup races.
|
||||||
set({ currentSessionId: id, currentSessionDirectory: id ? resolvedDir ?? null : null })
|
set({
|
||||||
|
currentSessionId: id,
|
||||||
|
currentSessionDirectory: id ? resolvedDir ?? null : null,
|
||||||
|
})
|
||||||
guessedSelectionSessionId = isGuessedDir && id ? id : null
|
guessedSelectionSessionId = isGuessedDir && id ? id : null
|
||||||
const rememberedDir = isGuessedDir ? null : resolvedDir ?? null
|
const rememberedDir = isGuessedDir ? null : resolvedDir ?? null
|
||||||
writeRuntimeSessionMemory(key, { sessionId: id, directory: rememberedDir })
|
writeRuntimeSessionMemory(key, { sessionId: id, directory: rememberedDir })
|
||||||
@@ -945,6 +1008,11 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearMaterializedDraftSession: (sessionId) => {
|
||||||
|
if (get().materializedDraftSessionId !== sessionId) return
|
||||||
|
set({ materializedDraftSessionId: null })
|
||||||
|
},
|
||||||
|
|
||||||
prepareForRuntimeSwitch: (apiBaseUrl?: string | null) => {
|
prepareForRuntimeSwitch: (apiBaseUrl?: string | null) => {
|
||||||
const key = runtimeMemoryKey(apiBaseUrl)
|
const key = runtimeMemoryKey(apiBaseUrl)
|
||||||
const directory = useDirectoryStore.getState().currentDirectory || null
|
const directory = useDirectoryStore.getState().currentDirectory || null
|
||||||
@@ -1663,33 +1731,8 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// createSession
|
// createSession
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
createSession: async (title, directoryOverride, parentID, metadata) => {
|
createSession: (title, directoryOverride, parentID, metadata) =>
|
||||||
const draft = get().newSessionDraft
|
createSessionWithDraftLifecycle(title, directoryOverride, parentID, metadata),
|
||||||
const targetFolderId = draft.targetFolderId
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resolved = await resolveCreatableDraftDirectory(draft, directoryOverride)
|
|
||||||
if (resolved.status === "aborted") return null
|
|
||||||
const dir = resolved.directory
|
|
||||||
const session = await createSessionAction(title, dir, parentID ?? null, metadata)
|
|
||||||
if (!session) return null
|
|
||||||
|
|
||||||
get().closeNewSessionDraft()
|
|
||||||
|
|
||||||
if (targetFolderId) {
|
|
||||||
const scopeDirectory = dir || get().lastLoadedDirectory || session.directory
|
|
||||||
const scopeKey = getChatsRootFromDirectory(scopeDirectory) ?? scopeDirectory
|
|
||||||
if (scopeKey) {
|
|
||||||
useSessionFoldersStore.getState().addSessionToFolder(scopeKey, targetFolderId, session.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return session
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[session-ui-store] createSession failed", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// deleteSession — calls SDK, SSE event updates child store
|
// deleteSession — calls SDK, SSE event updates child store
|
||||||
|
|||||||
Reference in New Issue
Block a user