mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
fix(mobile): preserve prompt refusal during draft clear
This commit is contained in:
@@ -340,7 +340,7 @@ describe('clearMobileNativeChatInput', () => {
|
||||
const clearInput = buildAgentTuiClearInputForText('Linked Linear issue: ABC-123\nhttps://x')
|
||||
await expect(
|
||||
clearMobileNativeChatInput({ client, terminal: 'term', clearInput })
|
||||
).resolves.toBe(true)
|
||||
).resolves.toBe('accepted')
|
||||
expect(params(client)).toMatchObject({ text: clearInput, enter: false })
|
||||
})
|
||||
|
||||
@@ -353,7 +353,26 @@ describe('clearMobileNativeChatInput', () => {
|
||||
})
|
||||
await expect(
|
||||
clearMobileNativeChatInput({ client, terminal: 'term', clearInput: '\x15' })
|
||||
).resolves.toBe(false)
|
||||
).resolves.toBe('rejected')
|
||||
})
|
||||
|
||||
it('preserves an interactive prompt rejection from a guarded clear', async () => {
|
||||
const client = clientWithResponse({
|
||||
id: 'request',
|
||||
ok: true,
|
||||
result: { send: { accepted: false, refusedReason: 'interactive-prompt' } },
|
||||
_meta: { runtimeId: 'runtime' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
clearMobileNativeChatInput({
|
||||
client,
|
||||
terminal: 'term',
|
||||
clearInput: '\x15',
|
||||
guardInteractivePrompt: true
|
||||
})
|
||||
).resolves.toBe('interactive-prompt')
|
||||
expect(params(client)).toMatchObject({ nativeChat: true })
|
||||
})
|
||||
|
||||
it('refuses to start an underfunded clear rather than half-clearing', async () => {
|
||||
@@ -365,7 +384,7 @@ describe('clearMobileNativeChatInput', () => {
|
||||
clearInput: '\x15',
|
||||
deadline: Date.now() + 10
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
).resolves.toBe('rejected')
|
||||
expect(client.sendRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,6 +40,7 @@ type MobileNativeChatSendArgs = {
|
||||
* desktop (ack loss after a write, or a cutover that cannot tell whether the
|
||||
* frame was written) — callers must not present it as a definite send failure. */
|
||||
export type MobileNativeChatSendOutcome = 'accepted' | 'rejected' | 'unknown' | 'interactive-prompt'
|
||||
export type MobileNativeChatClearOutcome = Exclude<MobileNativeChatSendOutcome, 'unknown'>
|
||||
|
||||
/** Without an explicit timeout `sendRequest` waits for reconnect indefinitely, and
|
||||
* the composer holds `sending` (send arrow dimmed, no error) for as long as it
|
||||
@@ -119,11 +120,11 @@ export async function clearMobileNativeChatInput(args: {
|
||||
guardInteractivePrompt?: boolean
|
||||
mobileClient?: MobileTerminalClient
|
||||
deadline?: number
|
||||
}): Promise<boolean> {
|
||||
}): Promise<MobileNativeChatClearOutcome> {
|
||||
const timeoutMs =
|
||||
args.deadline === undefined ? MOBILE_NATIVE_CHAT_SEND_TIMEOUT_MS : args.deadline - Date.now()
|
||||
if (timeoutMs < MOBILE_NATIVE_CHAT_MIN_WRITE_TIMEOUT_MS) {
|
||||
return false
|
||||
return 'rejected'
|
||||
}
|
||||
try {
|
||||
const response = await args.client.sendRequest(
|
||||
@@ -137,9 +138,14 @@ export async function clearMobileNativeChatInput(args: {
|
||||
},
|
||||
{ timeoutMs, budgetSpansConnect: true }
|
||||
)
|
||||
return isTerminalSendRpcAccepted(response)
|
||||
if (isTerminalSendRpcAccepted(response)) {
|
||||
return 'accepted'
|
||||
}
|
||||
return getTerminalSendRpcRefusedReason(response) === 'interactive-prompt'
|
||||
? 'interactive-prompt'
|
||||
: 'rejected'
|
||||
} catch {
|
||||
// A failed clear must not send the body on top of an uncleared line.
|
||||
return false
|
||||
return 'rejected'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('useMobileNativeChatMessageSend', () => {
|
||||
sendWithOutcome.mockReset()
|
||||
sendWithOutcome.mockResolvedValue('accepted')
|
||||
clearInputWrite.mockReset()
|
||||
clearInputWrite.mockResolvedValue(true)
|
||||
clearInputWrite.mockResolvedValue('accepted')
|
||||
acceptSend.mockReset()
|
||||
holdUnconfirmedSend.mockReset()
|
||||
restoreRejectedDraft.mockReset()
|
||||
@@ -131,7 +131,7 @@ describe('useMobileNativeChatMessageSend', () => {
|
||||
|
||||
it('aborts without sending the body when the clear is rejected', async () => {
|
||||
// Sending on top of an uncleared line is exactly the concatenation bug.
|
||||
clearInputWrite.mockResolvedValue(false)
|
||||
clearInputWrite.mockResolvedValue('rejected')
|
||||
mount(() => ({ text: DRAFT, createdAt: 1 }))
|
||||
let result: boolean | undefined
|
||||
await act(async () => {
|
||||
@@ -141,6 +141,22 @@ describe('useMobileNativeChatMessageSend', () => {
|
||||
expect(sendWithOutcome).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports an interactive prompt rejected while clearing a seeded launch draft', async () => {
|
||||
clearInputWrite.mockResolvedValue('interactive-prompt')
|
||||
mount(() => ({ text: DRAFT, createdAt: 1 }))
|
||||
|
||||
let result: boolean | undefined
|
||||
await act(async () => {
|
||||
result = await api!.send('hello')
|
||||
})
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(restoreRejectedDraft).toHaveBeenCalledTimes(1)
|
||||
expect(sendWithOutcome).not.toHaveBeenCalled()
|
||||
expect(acceptSend).not.toHaveBeenCalled()
|
||||
expect(onSendError).toHaveBeenCalledWith('Finish the terminal prompt before sending')
|
||||
})
|
||||
|
||||
it('drops the body write\u2019s own Ctrl+U prefix once the dedicated clear ran', async () => {
|
||||
// A Ctrl+U written immediately before body text in the SAME write arrives as
|
||||
// a literal control character, so it would head the received message.
|
||||
|
||||
@@ -135,7 +135,7 @@ export function useMobileNativeChatMessageSend(args: {
|
||||
// than pasting on top of an uncleared line.
|
||||
const seededLaunchDraft = readSeededLaunchDraftSeed()
|
||||
if (seededLaunchDraft && !images?.length) {
|
||||
const cleared = await clearMobileNativeChatInput({
|
||||
const clearOutcome = await clearMobileNativeChatInput({
|
||||
client,
|
||||
terminal: handle,
|
||||
clearInput: buildAgentTuiClearInputForText(seededLaunchDraft.text),
|
||||
@@ -145,11 +145,15 @@ export function useMobileNativeChatMessageSend(args: {
|
||||
? { mobileClient: { id: deviceTokenRef.current, type: 'mobile' } }
|
||||
: {})
|
||||
})
|
||||
if (!cleared) {
|
||||
if (clearOutcome !== 'accepted') {
|
||||
if (syncComposer) {
|
||||
restoreRejectedDraft(origin, text)
|
||||
}
|
||||
onSendError('Message not sent')
|
||||
onSendError(
|
||||
clearOutcome === 'interactive-prompt'
|
||||
? 'Finish the terminal prompt before sending'
|
||||
: 'Message not sent'
|
||||
)
|
||||
return 'rejected'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user