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]
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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' }],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user