Merge pull request #2928 from openchamber/feat/stale-last-directory-fallback-1be0
fix(sessions): recover new chats from a deleted lastDirectory
This commit is contained in:
@@ -9,6 +9,7 @@ let configCalls = 0;
|
||||
let runtimeKey = 'test-runtime';
|
||||
const promptAsyncCalls: unknown[][] = [];
|
||||
const promptAsyncResults: Array<unknown> = [];
|
||||
const pathGetResults: Array<unknown> = [];
|
||||
|
||||
const promptAsyncMock = mock(async (...args: unknown[]) => {
|
||||
promptAsyncCalls.push(args);
|
||||
@@ -17,6 +18,12 @@ const promptAsyncMock = mock(async (...args: unknown[]) => {
|
||||
return next ?? { response: new Response(null, { status: 200 }) };
|
||||
});
|
||||
|
||||
const pathGetMock = mock(async () => {
|
||||
const next = pathGetResults.shift();
|
||||
if (next instanceof Error) throw next;
|
||||
return next ?? { data: { directory: '/workspace/project' } };
|
||||
});
|
||||
|
||||
mock.module('@opencode-ai/sdk/v2', () => ({
|
||||
createOpencodeClient: mock(() => ({
|
||||
config: {
|
||||
@@ -30,6 +37,9 @@ mock.module('@opencode-ai/sdk/v2', () => ({
|
||||
session: {
|
||||
promptAsync: promptAsyncMock,
|
||||
},
|
||||
path: {
|
||||
get: pathGetMock,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -64,6 +74,17 @@ beforeEach(() => {
|
||||
runtimeKey = 'test-runtime';
|
||||
promptAsyncCalls.length = 0;
|
||||
promptAsyncResults.length = 0;
|
||||
pathGetResults.length = 0;
|
||||
});
|
||||
|
||||
describe('opencodeClient directory availability', () => {
|
||||
test('distinguishes a missing directory from an unavailable path probe', async () => {
|
||||
pathGetResults.push({ error: { code: 'ENOENT', message: 'no such file or directory' } });
|
||||
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('missing');
|
||||
|
||||
pathGetResults.push(new Error('offline'));
|
||||
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('opencodeClient getConfig cache', () => {
|
||||
|
||||
@@ -68,6 +68,21 @@ type SdkResult<T> = {
|
||||
response?: { status?: number };
|
||||
};
|
||||
|
||||
type DirectoryAvailability = "available" | "missing" | "unknown";
|
||||
|
||||
const isMissingDirectoryError = (error: unknown): boolean => {
|
||||
if (error instanceof FilesystemError) {
|
||||
return error.reason === "not-found" || error.reason === "not-directory";
|
||||
}
|
||||
if (error && typeof error === "object") {
|
||||
const code = (error as { code?: unknown }).code;
|
||||
if (code === "ENOENT" || code === "ENOTDIR") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return /\bENOENT\b|\bENOTDIR\b|no such file or directory/i.test(formatSdkError(error));
|
||||
};
|
||||
|
||||
function unwrapSdkData<T>(result: SdkResult<T>, operation: string): T {
|
||||
if (result.error) {
|
||||
const status = result.response?.status;
|
||||
@@ -506,17 +521,28 @@ class OpencodeService {
|
||||
* This is intentionally NOT the same as local filesystem access in the UI runtime.
|
||||
*/
|
||||
async probeDirectory(directory: string): Promise<boolean> {
|
||||
return (await this.getDirectoryAvailability(directory)) === "available";
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinguishes a confirmed-missing directory from an unavailable probe.
|
||||
* Offline, permission, and other transport failures stay `unknown` so callers
|
||||
* do not treat a temporary outage as proof the path was deleted.
|
||||
*/
|
||||
async getDirectoryAvailability(directory: string): Promise<DirectoryAvailability> {
|
||||
const normalized = this.normalizeCandidatePath(directory);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
return "unknown";
|
||||
}
|
||||
try {
|
||||
const response = await this.client.path.get({ directory: normalized });
|
||||
const info = response.data as { directory?: unknown } | undefined;
|
||||
const returned = typeof info?.directory === 'string' ? info.directory : null;
|
||||
return Boolean(returned && returned.trim().length > 0);
|
||||
} catch {
|
||||
return false;
|
||||
const response = await this.client.path.get({ directory: normalized }) as SdkResult<{ directory?: unknown }>;
|
||||
if (response.error) {
|
||||
return isMissingDirectoryError(response.error) ? "missing" : "unknown";
|
||||
}
|
||||
const returned = typeof response.data?.directory === "string" ? response.data.directory.trim() : "";
|
||||
return returned ? "available" : "unknown";
|
||||
} catch (error) {
|
||||
return isMissingDirectoryError(error) ? "missing" : "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -251,7 +251,8 @@ Rules:
|
||||
4. Sending after a revert commits the new branch optimistically: remove the reverted tail and marker before inserting the new message, and restore both if the send is rejected.
|
||||
5. Composer and queued sends carry their captured runtime, directory, and session through asynchronous preparation. A runtime change cancels the send instead of re-resolving it against the new runtime.
|
||||
6. After session creation, the directory returned by the server is authoritative over the requested draft directory. The server may canonicalize a worktree path, and the first prompt must use the same directory identity as the created session.
|
||||
7. A prompt send that fails **after** the request left the client is ambiguous, never a definite failure: the server may already be answering it. Transports tag those errors (`markAmbiguousTransportFailure` in `@/lib/relay/transport-error`; the relay tunnel tags every stream that dies with a request in flight), and `isAmbiguousSendFailure` reads the tag before falling back to status/text heuristics. An ambiguous failure waits for the connection to return, refetches recent messages, and confirms the optimistic message in place instead of rolling it back — rolling it back lets the message queue re-send a prompt the engine is already running, producing two independent AI responses for one user message.
|
||||
7. Regular new-chat drafts that inherit the persisted current/last directory must not create a session against a confirmed-missing path. Fall back to the active project only when OpenCode reports the directory missing; keep explicit worktree targets, in-flight worktree creation, and unknown/offline probes unchanged, and do not persist the fallback until session creation succeeds. A concurrent draft rewrite to that same active-project fallback must not abort session creation.
|
||||
8. A prompt send that fails **after** the request left the client is ambiguous, never a definite failure: the server may already be answering it. Transports tag those errors (`markAmbiguousTransportFailure` in `@/lib/relay/transport-error`; the relay tunnel tags every stream that dies with a request in flight), and `isAmbiguousSendFailure` reads the tag before falling back to status/text heuristics. An ambiguous failure waits for the connection to return, refetches recent messages, and confirms the optimistic message in place instead of rolling it back — rolling it back lets the message queue re-send a prompt the engine is already running, producing two independent AI responses for one user message.
|
||||
|
||||
Examples of global-store updates performed in `session-actions.ts`:
|
||||
|
||||
|
||||
@@ -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,155 @@ describe('createSession draft lifecycle', () => {
|
||||
expect(useSessionUIStore.getState().newSessionDraft.open).toBe(true);
|
||||
expect(useSessionUIStore.getState().newSessionDraft.title).toBe('Draft title');
|
||||
});
|
||||
|
||||
test('rewrites an implicit new-chat draft to the active project before the session is created', async () => {
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
|
||||
opencodeClient.getDirectoryAvailability = async () => 'missing';
|
||||
|
||||
useSessionUIStore.getState().openNewSessionDraft();
|
||||
await Bun.sleep(0);
|
||||
|
||||
expect(useSessionUIStore.getState().newSessionDraft.directoryOverride).toBe('/projects/main');
|
||||
expect(useSessionUIStore.getState().newSessionDraft.selectedProjectId).toBe('project-main');
|
||||
expect(getDeferredSafeStorage().getItem('lastDirectory')).toBe('/private/deleted-worktree');
|
||||
});
|
||||
|
||||
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('still creates against the active project when the draft is rewritten during the create probe', async () => {
|
||||
const createSessionCalls = [];
|
||||
const availabilityResolvers = [];
|
||||
useProjectsStore.setState({
|
||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||
activeProjectId: 'project-main',
|
||||
});
|
||||
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
|
||||
opencodeClient.getDirectoryAvailability = () => new Promise((resolve) => {
|
||||
availabilityResolvers.push(resolve);
|
||||
});
|
||||
opencodeClient.createSession = async (_params, directory) => {
|
||||
createSessionCalls.push(directory);
|
||||
return { id: 'session-race', directory };
|
||||
};
|
||||
|
||||
useSessionUIStore.getState().openNewSessionDraft();
|
||||
const createPromise = useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
|
||||
expect(availabilityResolvers.length).toBe(2);
|
||||
|
||||
availabilityResolvers[0]('missing');
|
||||
await Bun.sleep(0);
|
||||
expect(useSessionUIStore.getState().newSessionDraft.directoryOverride).toBe('/projects/main');
|
||||
|
||||
availabilityResolvers[1]('missing');
|
||||
const session = await createPromise;
|
||||
|
||||
expect(session).not.toBeNull();
|
||||
expect(createSessionCalls).toEqual(['/projects/main']);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -603,6 +603,103 @@ const waitForWorktreeBootstrapIfConfigured = async (directory: string | null, pr
|
||||
}
|
||||
}
|
||||
|
||||
const resolveActiveProjectDirectory = (draft: NewSessionDraftState): string | null => {
|
||||
const projectsState = useProjectsStore.getState()
|
||||
return normalizePath(
|
||||
projectsState.getActiveProject()?.path
|
||||
?? (draft.selectedProjectId
|
||||
? projectsState.projects.find((project) => project.id === draft.selectedProjectId)?.path
|
||||
: null)
|
||||
?? null,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular new-chat drafts inherit the persisted current/last directory. If that
|
||||
* path is confirmed missing (deleted worktree), fall back to the active project.
|
||||
* Explicit worktree targets, in-flight worktree creation, and unknown/offline
|
||||
* probes stay unchanged so a temporary outage cannot rewrite the destination.
|
||||
* A concurrent rewrite of the same implicit draft to that fallback is accepted
|
||||
* instead of aborting create.
|
||||
*/
|
||||
const resolveCreatableDraftDirectory = async (
|
||||
draft: NewSessionDraftState,
|
||||
requestedDirectory: string | null | undefined,
|
||||
): Promise<{ status: "ok"; directory: string | null | undefined } | { status: "aborted" }> => {
|
||||
const directory = requestedDirectory ?? opencodeClient.getDirectory() ?? null
|
||||
const isRecoverableDraftDirectory =
|
||||
draft.open
|
||||
&& draft.preserveDirectoryOverride !== true
|
||||
&& !draft.pendingWorktreeRequestId
|
||||
&& !draft.bootstrapPendingDirectory
|
||||
&& normalizePath(draft.directoryOverride) === normalizePath(directory)
|
||||
|
||||
if (!isRecoverableDraftDirectory || !directory) {
|
||||
return { status: "ok", directory }
|
||||
}
|
||||
|
||||
const activeProjectDirectory = resolveActiveProjectDirectory(draft)
|
||||
if (!activeProjectDirectory || normalizePath(directory) === activeProjectDirectory) {
|
||||
return { status: "ok", directory }
|
||||
}
|
||||
|
||||
const runtimeKey = getRuntimeKey()
|
||||
const draftDirectory = draft.directoryOverride
|
||||
const availability = await opencodeClient.getDirectoryAvailability(directory)
|
||||
const currentDraft = useSessionUIStore.getState().newSessionDraft
|
||||
const currentDirectory = normalizePath(currentDraft.directoryOverride)
|
||||
const capturedDirectory = normalizePath(draftDirectory)
|
||||
// openNewSessionDraft may rewrite the same implicit draft to this fallback
|
||||
// while createSession's probe is still in flight. That is the intended
|
||||
// destination, not a user change, so do not abort the create.
|
||||
const recoveredToActiveProject = currentDirectory === activeProjectDirectory
|
||||
&& capturedDirectory !== activeProjectDirectory
|
||||
const draftChanged = !currentDraft.open
|
||||
|| currentDraft.preserveDirectoryOverride !== draft.preserveDirectoryOverride
|
||||
|| currentDraft.pendingWorktreeRequestId !== draft.pendingWorktreeRequestId
|
||||
|| (currentDirectory !== capturedDirectory && !recoveredToActiveProject)
|
||||
|
||||
if (getRuntimeKey() !== runtimeKey || draftChanged) {
|
||||
return { status: "aborted" }
|
||||
}
|
||||
|
||||
if (recoveredToActiveProject) {
|
||||
return { status: "ok", directory: activeProjectDirectory }
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
directory: availability === "missing" ? activeProjectDirectory : directory,
|
||||
}
|
||||
}
|
||||
|
||||
const recoverStaleDraftDirectory = async (openedDraft: NewSessionDraftState): Promise<void> => {
|
||||
const resolved = await resolveCreatableDraftDirectory(openedDraft, openedDraft.directoryOverride)
|
||||
if (resolved.status !== "ok") return
|
||||
const recovered = normalizePath(resolved.directory ?? null)
|
||||
const original = normalizePath(openedDraft.directoryOverride)
|
||||
if (!recovered || recovered === original) return
|
||||
|
||||
const currentDraft = useSessionUIStore.getState().newSessionDraft
|
||||
if (!currentDraft.open) return
|
||||
if (currentDraft.preserveDirectoryOverride === true) return
|
||||
if (currentDraft.pendingWorktreeRequestId) return
|
||||
if (normalizePath(currentDraft.directoryOverride) !== original) return
|
||||
|
||||
const recoveredProject = useProjectsStore.getState().projects.find((project) => (
|
||||
normalizePath(project.path) === recovered
|
||||
))
|
||||
const nextDraft: NewSessionDraftState = {
|
||||
...currentDraft,
|
||||
selectedProjectId: recoveredProject?.id ?? currentDraft.selectedProjectId,
|
||||
directoryOverride: recovered,
|
||||
}
|
||||
useSessionUIStore.setState({ newSessionDraft: nextDraft })
|
||||
writeRuntimeSessionMemory(runtimeMemoryKey(), { draft: nextDraft })
|
||||
persistDraftTarget({ projectId: nextDraft.selectedProjectId ?? null, directory: recovered })
|
||||
void activateConfigForDirectory(recovered)
|
||||
}
|
||||
|
||||
export async function materializeOpenDraftSession(selection: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
@@ -964,6 +1061,8 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
if (directory && directory !== useDirectoryStore.getState().currentDirectory) {
|
||||
useDirectoryStore.getState().setDirectory(directory)
|
||||
}
|
||||
|
||||
void recoverStaleDraftDirectory(nextDraft)
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1416,14 +1515,16 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
const targetFolderId = draft.targetFolderId
|
||||
|
||||
try {
|
||||
const dir = directoryOverride ?? opencodeClient.getDirectory()
|
||||
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 scopeKey = directoryOverride || get().lastLoadedDirectory || session.directory
|
||||
const scopeKey = dir || get().lastLoadedDirectory || session.directory
|
||||
if (scopeKey) {
|
||||
useSessionFoldersStore.getState().addSessionToFolder(scopeKey, targetFolderId, session.id)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
## [Unreleased]
|
||||
|
||||
- Chat: new chats no longer start against a deleted last worktree directory; they fall back to the active project instead of saving the first message and never starting.
|
||||
- Git: the pull request panel now follows the current open PR for the branch instead of keeping a merged or closed one after reload or a later open PR (thanks to @makeittech).
|
||||
|
||||
## [1.18.4] - 2026-08-14
|
||||
|
||||
Reference in New Issue
Block a user