fix(sessions): fall back when lastDirectory is a deleted worktree
New chats inherited a persisted lastDirectory even after that worktree was removed, so the first message saved but the prompt never started. Validate the implicit draft directory, fall back to the active project only when OpenCode confirms the path is missing, and leave explicit worktree targets and unknown probes unchanged. Co-authored-by: serkraser <serkraser@gmail.com>
This commit is contained in:
co-authored by
serkraser
parent
6b1e677aaf
commit
90e79b04a4
@@ -9,6 +9,7 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useCommandsStore } from '@/stores/useCommandsStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
import { getDeferredSafeStorage } from '@/stores/utils/safeStorage';
|
||||
|
||||
/**
|
||||
* Unit tests for session worktree routing through the authoritative store.
|
||||
@@ -408,9 +409,21 @@ describe('openNewSessionDraft project binding', () => {
|
||||
|
||||
describe('createSession draft lifecycle', () => {
|
||||
let originalCreateSession;
|
||||
let originalGetDirectoryAvailability;
|
||||
let originalProjects;
|
||||
let originalActiveProjectId;
|
||||
let originalDirectoryState;
|
||||
let originalClientDirectory;
|
||||
let originalLastDirectory;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCreateSession = opencodeClient.createSession;
|
||||
originalGetDirectoryAvailability = opencodeClient.getDirectoryAvailability;
|
||||
originalProjects = useProjectsStore.getState().projects;
|
||||
originalActiveProjectId = useProjectsStore.getState().activeProjectId;
|
||||
originalDirectoryState = useDirectoryStore.getState();
|
||||
originalClientDirectory = opencodeClient.getDirectory();
|
||||
originalLastDirectory = getDeferredSafeStorage().getItem('lastDirectory');
|
||||
useSessionUIStore.setState({
|
||||
currentSessionId: null,
|
||||
currentSessionDirectory: null,
|
||||
@@ -420,6 +433,15 @@ describe('createSession draft lifecycle', () => {
|
||||
|
||||
afterEach(() => {
|
||||
opencodeClient.createSession = originalCreateSession;
|
||||
opencodeClient.getDirectoryAvailability = originalGetDirectoryAvailability;
|
||||
useProjectsStore.setState({ projects: originalProjects, activeProjectId: originalActiveProjectId });
|
||||
useDirectoryStore.setState(originalDirectoryState, true);
|
||||
opencodeClient.setDirectory(originalClientDirectory ?? undefined);
|
||||
if (originalLastDirectory === null) {
|
||||
getDeferredSafeStorage().removeItem('lastDirectory');
|
||||
} else {
|
||||
getDeferredSafeStorage().setItem('lastDirectory', originalLastDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the draft open when session creation fails', async () => {
|
||||
@@ -433,6 +455,108 @@ describe('createSession draft lifecycle', () => {
|
||||
expect(useSessionUIStore.getState().newSessionDraft.open).toBe(true);
|
||||
expect(useSessionUIStore.getState().newSessionDraft.title).toBe('Draft title');
|
||||
});
|
||||
|
||||
test('falls back to the current active project when a regular new-chat directory is missing', async () => {
|
||||
const createSessionCalls = [];
|
||||
useProjectsStore.setState({
|
||||
projects: [
|
||||
{ id: 'project-draft', path: '/projects/draft', label: 'Draft' },
|
||||
{ id: 'project-active', path: '/projects/active', label: 'Active' },
|
||||
],
|
||||
activeProjectId: 'project-active',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
|
||||
useSessionUIStore.getState().openNewSessionDraft();
|
||||
opencodeClient.getDirectoryAvailability = async () => 'missing';
|
||||
opencodeClient.createSession = async (_params, directory) => {
|
||||
createSessionCalls.push(directory);
|
||||
return { id: 'session-fallback', directory };
|
||||
};
|
||||
|
||||
await useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
|
||||
|
||||
expect(createSessionCalls).toEqual(['/projects/active']);
|
||||
expect(useDirectoryStore.getState().currentDirectory).toBe('/projects/active');
|
||||
expect(getDeferredSafeStorage().getItem('lastDirectory')).toBe('/projects/active');
|
||||
});
|
||||
|
||||
test('keeps an explicitly pinned worktree directory unchanged', async () => {
|
||||
const createSessionCalls = [];
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/deleted-worktree', preserveDirectoryOverride: true });
|
||||
expect(useSessionUIStore.getState().newSessionDraft.preserveDirectoryOverride).toBe(true);
|
||||
opencodeClient.getDirectoryAvailability = async () => 'missing';
|
||||
opencodeClient.createSession = async (_params, directory) => {
|
||||
createSessionCalls.push(directory);
|
||||
return { id: 'session-pinned', directory };
|
||||
};
|
||||
|
||||
await useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
|
||||
|
||||
expect(createSessionCalls).toEqual(['/private/deleted-worktree']);
|
||||
});
|
||||
|
||||
test('keeps a ChatInput-style current-directory draft recoverable when that path is missing', async () => {
|
||||
const createSessionCalls = [];
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
|
||||
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/deleted-worktree' });
|
||||
expect(useSessionUIStore.getState().newSessionDraft.preserveDirectoryOverride).not.toBe(true);
|
||||
opencodeClient.getDirectoryAvailability = async () => 'missing';
|
||||
opencodeClient.createSession = async (_params, directory) => {
|
||||
createSessionCalls.push(directory);
|
||||
return { id: 'session-chat-input', directory };
|
||||
};
|
||||
|
||||
await useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
|
||||
|
||||
expect(createSessionCalls).toEqual(['/projects/main']);
|
||||
});
|
||||
|
||||
test('keeps the stale directory when its availability cannot be confirmed', async () => {
|
||||
const createSessionCalls = [];
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/unavailable-worktree', { showOverlay: false });
|
||||
useSessionUIStore.getState().openNewSessionDraft();
|
||||
opencodeClient.getDirectoryAvailability = async () => 'unknown';
|
||||
opencodeClient.createSession = async (_params, directory) => {
|
||||
createSessionCalls.push(directory);
|
||||
return { id: 'session-unavailable', directory };
|
||||
};
|
||||
|
||||
await useSessionUIStore.getState().createSession('Draft title', '/private/unavailable-worktree');
|
||||
|
||||
expect(createSessionCalls).toEqual(['/private/unavailable-worktree']);
|
||||
expect(useDirectoryStore.getState().currentDirectory).toBe('/private/unavailable-worktree');
|
||||
});
|
||||
|
||||
test('does not persist a fallback when session creation fails', async () => {
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
|
||||
useSessionUIStore.getState().openNewSessionDraft();
|
||||
opencodeClient.getDirectoryAvailability = async () => 'missing';
|
||||
opencodeClient.createSession = async () => {
|
||||
throw new Error('offline');
|
||||
};
|
||||
|
||||
const session = await useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
|
||||
|
||||
expect(session).toBeNull();
|
||||
expect(useDirectoryStore.getState().currentDirectory).toBe('/private/deleted-worktree');
|
||||
expect(getDeferredSafeStorage().getItem('lastDirectory')).toBe('/private/deleted-worktree');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user