fix(native-chat): a chat whose draft holds text is never reused or claimed as empty

The empty-chat check read the pane's old draft key, while the composer and
every hand-back write the conversation's. A chat with typed or given-back
text read as empty, so a bare new chat reused it and a text request claimed
it. It reads the conversation's draft now.
This commit is contained in:
Brennan Benson
2026-10-04 17:47:36 -07:00
parent a0f0b765f9
commit d4e469b697
3 changed files with 27 additions and 26 deletions
@@ -113,10 +113,8 @@ import {
clearNativeChatDraftCacheForTests,
writeNativeChatDraftCache
} from '@/components/native-chat/native-chat-draft-cache'
import {
structuredAgentSessionPaneKey,
structuredAgentSessionTabId
} from '../../../shared/structured-agent-session-projection'
import { structuredAgentSessionTabId } from '../../../shared/structured-agent-session-projection'
import { structuredAgentSessionDraftScopeKey } from '@/components/native-chat/native-chat-composer-draft-store'
import { adoptAgentSessionLaunchVerdict } from './agent-session-launch-plan'
import {
beginStructuredAgentSessionProvisionalLaunch,
@@ -279,8 +277,8 @@ describe('a second "new chat" with no text', () => {
it('opens a new chat when the idle chat has a typed draft', async () => {
pick('plus-pick-1')
await publishIdle(first.sessionId)
const tabId = structuredAgentSessionTabId(first.sessionId)
writeNativeChatDraftCache(structuredAgentSessionPaneKey(tabId, first.sessionId), 'half a q')
// Under the conversation's key, which the composer and every hand-back write.
writeNativeChatDraftCache(structuredAgentSessionDraftScopeKey(first.sessionId), 'half a q')
expect(pick('plus-pick-2').sessionId).toBe(second.sessionId)
})
@@ -288,8 +286,7 @@ describe('a second "new chat" with no text', () => {
it('opens a new chat when the idle chat has an image in its composer', async () => {
pick('plus-pick-1')
await publishIdle(first.sessionId)
const tabId = structuredAgentSessionTabId(first.sessionId)
appendNativeChatAttachmentCache(structuredAgentSessionPaneKey(tabId, first.sessionId), [
appendNativeChatAttachmentCache(structuredAgentSessionDraftScopeKey(first.sessionId), [
{ id: 'shot', path: '/tmp/shot.png' }
])
@@ -484,6 +481,15 @@ describe('a "new chat" with text', () => {
)
})
it('leaves a starting chat whose draft holds text, given back or typed, to its user', () => {
mocks.launch.mockImplementation(() => new Promise(() => undefined))
const blank = pick('plus-pick-1')
writeNativeChatDraftCache(structuredAgentSessionDraftScopeKey(blank.sessionId), 'my words')
expect(pick('notes-send', { prompt: 'review notes' }).sessionId).toBe(second.sessionId)
expect(readOutbox(blank.sessionId)).toEqual([])
})
it('takes an empty chat still starting, as before', () => {
mocks.launch.mockImplementation(() => new Promise(() => undefined))
const blank = pick('plus-pick-1')
@@ -67,10 +67,7 @@ import {
readNativeChatDraftCache,
writeNativeChatDraftCache
} from '@/components/native-chat/native-chat-draft-cache'
import {
structuredAgentSessionPaneKey,
structuredAgentSessionTabId
} from '../../../shared/structured-agent-session-projection'
import { structuredAgentSessionDraftScopeKey } from '@/components/native-chat/native-chat-composer-draft-store'
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-agent-session'
import {
getStructuredAgentSessionLaunchLifecycle,
@@ -520,10 +517,8 @@ describe('an empty chat still starting', () => {
it('leaves a chat its user is typing into to them, and opens a new chat for the notes', async () => {
const blank = startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'plus-pick' })
const paneKey = structuredAgentSessionPaneKey(
structuredAgentSessionTabId(blank.sessionId),
blank.sessionId
)
// The conversation's draft, which its composer writes.
const paneKey = structuredAgentSessionDraftScopeKey(blank.sessionId)
writeNativeChatDraftCache(paneKey, 'half a question')
const notes = startStructuredAgentLaunch(WORKTREE_ID, 'codex', notesSend)
@@ -1,19 +1,19 @@
import {
structuredAgentSessionPaneKey,
structuredAgentSessionTabId
} from '../../../shared/structured-agent-session-projection'
import { getStructuredAgentSessionOutbox } from '@/components/native-chat/structured-agent-session-outbox-storage'
import { readNativeChatDraftCache } from '@/components/native-chat/native-chat-draft-cache'
// The draft store, not the composer hook: the launch must not load the composer (a store cycle).
import { readNativeChatComposerDraft } from '@/components/native-chat/native-chat-composer-draft-store'
import {
readNativeChatComposerDraft,
structuredAgentSessionDraftScopeKey
} from '@/components/native-chat/native-chat-composer-draft-store'
/** A starting chat is empty until its user sends into it or puts text or images in its composer;
* after that it is theirs, and another request never goes into it. */
/** A starting chat is empty until its user sends into it or puts text or images in its draft (typed,
* or given back); after that it is theirs, and another request never goes into it. Read by the
* conversation's key, which the composer and every hand-back write. */
export function isStructuredLaunchChatEmpty(sessionId: string): boolean {
const paneKey = structuredAgentSessionPaneKey(structuredAgentSessionTabId(sessionId), sessionId)
const scopeKey = structuredAgentSessionDraftScopeKey(sessionId)
return (
getStructuredAgentSessionOutbox(sessionId).length === 0 &&
readNativeChatDraftCache(paneKey).trim() === '' &&
readNativeChatComposerDraft(paneKey).images.length === 0
readNativeChatDraftCache(scopeKey).trim() === '' &&
readNativeChatComposerDraft(scopeKey).images.length === 0
)
}