diff --git a/src/utils/queryHelpers.test.ts b/src/utils/queryHelpers.test.ts new file mode 100644 index 00000000..fc5d96f1 --- /dev/null +++ b/src/utils/queryHelpers.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'bun:test' +import { + createUserInterruptionMessage, + INTERRUPT_MESSAGE_FOR_TOOL_USE, +} from './messages.js' +import { isResultSuccessful } from './queryHelpers.js' + +describe('isResultSuccessful', () => { + test('treats user interruption during tool use as a normal terminal result', () => { + const interruption = createUserInterruptionMessage({ toolUse: true }) + + expect(isResultSuccessful(interruption, 'tool_use')).toBe(true) + }) + + test('does not treat an ordinary user prompt as successful after tool_use', () => { + const ordinaryPrompt = createUserInterruptionMessage({ toolUse: true }) + ordinaryPrompt.message.content = [ + { + type: 'text', + text: `${INTERRUPT_MESSAGE_FOR_TOOL_USE} keep going`, + }, + ] + + expect(isResultSuccessful(ordinaryPrompt, 'tool_use')).toBe(false) + }) +}) diff --git a/src/utils/queryHelpers.ts b/src/utils/queryHelpers.ts index a3661baf..7379271a 100644 --- a/src/utils/queryHelpers.ts +++ b/src/utils/queryHelpers.ts @@ -27,7 +27,12 @@ import { createFileStateCacheWithSizeLimit, type FileStateCache, } from './fileStateCache.js' -import { isNotEmptyMessage, normalizeMessages } from './messages.js' +import { + INTERRUPT_MESSAGE, + INTERRUPT_MESSAGE_FOR_TOOL_USE, + isNotEmptyMessage, + normalizeMessages, +} from './messages.js' import { expandPath } from './path.js' import type { inputSchema as permissionToolInputSchema, @@ -50,6 +55,7 @@ const ASK_READ_FILE_STATE_CACHE_SIZE = 10 * Returns true if: * - Last message is assistant with text/thinking content * - Last message is user with only tool_result blocks + * - Last message is a synthetic user interruption * - Last message is the user prompt but the API completed with end_turn * (model chose to emit no content blocks) */ @@ -78,6 +84,16 @@ export function isResultSuccessful( ) { return true } + + const onlyBlock = Array.isArray(content) ? content[0] : undefined + if ( + content.length === 1 && + onlyBlock?.type === 'text' && + (onlyBlock.text === INTERRUPT_MESSAGE || + onlyBlock.text === INTERRUPT_MESSAGE_FOR_TOOL_USE) + ) { + return true + } } // Carve-out: API completed (message_delta set stop_reason) but yielded