fix(chat): preserve prompt when session creation fails

Keep the new-session draft open until the backend confirms that the session was created successfully. This prevents a failed request from closing the draft and discarding the user's creation context.

Restore the exact submitted composer text after a new-session send failure and persist it as the draft. Only restore when the composer is still empty or unchanged, so text entered while the request is pending is not overwritten.

Add regression coverage that simulates an offline session creation request and verifies that the draft remains open with its title intact.
This commit is contained in:
Bohdan Triapitsyn
2026-07-17 10:31:56 +03:00
parent bd68e303d4
commit 95e3e1cf2e
3 changed files with 37 additions and 1 deletions
@@ -2355,6 +2355,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
console.error('Message send failed:', rawMessage || error);
const currentInput = textareaRef.current?.value ?? messageRef.current;
if (newSessionDraftOpen && inputSnapshot.message && (!currentInput || currentInput === inputSnapshot.message)) {
setMessage(inputSnapshot.message);
saveStoredDraft(null, inputSnapshot.message);
}
const isSoftNetworkError =
normalized.includes('timeout') ||
normalized.includes('timed out') ||
@@ -283,6 +283,35 @@ describe('openNewSessionDraft project binding', () => {
});
});
describe('createSession draft lifecycle', () => {
let originalCreateSession;
beforeEach(() => {
originalCreateSession = opencodeClient.createSession;
useSessionUIStore.setState({
currentSessionId: null,
currentSessionDirectory: null,
newSessionDraft: { open: true, directoryOverride: '/projects/alpha', parentID: null, title: 'Draft title' },
});
});
afterEach(() => {
opencodeClient.createSession = originalCreateSession;
});
test('keeps the draft open when session creation fails', async () => {
opencodeClient.createSession = async () => {
throw new Error('offline');
};
const session = await useSessionUIStore.getState().createSession('Draft title', '/projects/alpha');
expect(session).toBeNull();
expect(useSessionUIStore.getState().newSessionDraft.open).toBe(true);
expect(useSessionUIStore.getState().newSessionDraft.title).toBe('Draft title');
});
});
describe('routeMessage skill invocation', () => {
// OpenCode registers every skill as a command (source: "skill"), so a skill
// selected from the slash menu must be dispatched via session.command so its
+2 -1
View File
@@ -1196,13 +1196,14 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
createSession: async (title, directoryOverride, parentID, metadata) => {
const draft = get().newSessionDraft
const targetFolderId = draft.targetFolderId
get().closeNewSessionDraft()
try {
const dir = directoryOverride ?? opencodeClient.getDirectory()
const session = await createSessionAction(title, dir, parentID ?? null, metadata)
if (!session) return null
get().closeNewSessionDraft()
if (targetFolderId) {
const scopeKey = directoryOverride || get().lastLoadedDirectory || session.directory
if (scopeKey) {