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:
co-authored by
serkraser
parent
a500a5d4e9
commit
d817c44c46
+1
-1
@@ -4,10 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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).
|
- 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.
|
- **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.
|
- **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).
|
- 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.18.4] - 2026-08-14
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
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`:
|
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');
|
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 () => {
|
test('does not persist a fallback when session creation fails', async () => {
|
||||||
useProjectsStore.setState({
|
useProjectsStore.setState({
|
||||||
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
projects: [{ id: 'project-main', path: '/projects/main', label: 'Main' }],
|
||||||
|
|||||||
@@ -619,6 +619,8 @@ const resolveActiveProjectDirectory = (draft: NewSessionDraftState): string | nu
|
|||||||
* path is confirmed missing (deleted worktree), fall back to the active project.
|
* path is confirmed missing (deleted worktree), fall back to the active project.
|
||||||
* Explicit worktree targets, in-flight worktree creation, and unknown/offline
|
* Explicit worktree targets, in-flight worktree creation, and unknown/offline
|
||||||
* probes stay unchanged so a temporary outage cannot rewrite the destination.
|
* 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 (
|
const resolveCreatableDraftDirectory = async (
|
||||||
draft: NewSessionDraftState,
|
draft: NewSessionDraftState,
|
||||||
@@ -645,15 +647,26 @@ const resolveCreatableDraftDirectory = async (
|
|||||||
const draftDirectory = draft.directoryOverride
|
const draftDirectory = draft.directoryOverride
|
||||||
const availability = await opencodeClient.getDirectoryAvailability(directory)
|
const availability = await opencodeClient.getDirectoryAvailability(directory)
|
||||||
const currentDraft = useSessionUIStore.getState().newSessionDraft
|
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
|
const draftChanged = !currentDraft.open
|
||||||
|| currentDraft.preserveDirectoryOverride !== draft.preserveDirectoryOverride
|
|| currentDraft.preserveDirectoryOverride !== draft.preserveDirectoryOverride
|
||||||
|| currentDraft.pendingWorktreeRequestId !== draft.pendingWorktreeRequestId
|
|| currentDraft.pendingWorktreeRequestId !== draft.pendingWorktreeRequestId
|
||||||
|| normalizePath(currentDraft.directoryOverride) !== normalizePath(draftDirectory)
|
|| (currentDirectory !== capturedDirectory && !recoveredToActiveProject)
|
||||||
|
|
||||||
if (getRuntimeKey() !== runtimeKey || draftChanged) {
|
if (getRuntimeKey() !== runtimeKey || draftChanged) {
|
||||||
return { status: "aborted" }
|
return { status: "aborted" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recoveredToActiveProject) {
|
||||||
|
return { status: "ok", directory: activeProjectDirectory }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
directory: availability === "missing" ? activeProjectDirectory : directory,
|
directory: availability === "missing" ? activeProjectDirectory : directory,
|
||||||
|
|||||||
Reference in New Issue
Block a user