fix(sessions): accept in-flight draft rewrite to the fallback directory

Create no longer aborts when recoverStaleDraftDirectory rewrites the
implicit new-chat draft to the active project during the create probe.
Also rank the Unreleased Chat bullet below the changelog highlights.

Co-authored-by: serkraser <serkraser@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-15 06:33:28 +00:00
co-authored by serkraser
parent a500a5d4e9
commit d817c44c46
4 changed files with 47 additions and 3 deletions
+1 -1
View File
@@ -4,10 +4,10 @@ All notable changes to this project will be documented in this file.
## [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).
- **Usage/Claude:** Claude plan limits now work when you are signed in through Claude Code, without also signing into Anthropic in OpenCode; the account is read from Claude Code's own login on macOS, Linux, and WSL. The page shows your session and weekly limits again, adds per-model weekly limits and extra usage spending, and names your plan. Limits are kept on screen instead of disappearing when Anthropic temporarily blocks refreshes.
- **Settings/Integrations:** a new Integrations settings page lists Claude Code, Command Code, and Cursor plugins with install, update, setup, and remove actions, plus Discord and Telegram Coming soon placeholders.
- 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.
- Chat: opening a busy subagent in the context panel now shows its history instead of only the working-status line (thanks to @makeittech).
## [1.18.4] - 2026-08-14
+1 -1
View File
@@ -251,7 +251,7 @@ 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. 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.
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`:
@@ -555,6 +555,37 @@ describe('createSession draft lifecycle', () => {
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' }],
+14 -1
View File
@@ -619,6 +619,8 @@ const resolveActiveProjectDirectory = (draft: NewSessionDraftState): string | nu
* 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,
@@ -645,15 +647,26 @@ const resolveCreatableDraftDirectory = async (
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
|| normalizePath(currentDraft.directoryOverride) !== normalizePath(draftDirectory)
|| (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,