mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
Support native-prefill prompt delivery and fix chat message sorting
* Treat native draft pre-fills as successful deliveries instead of marking the seeded launch prompts as failed. * Group native chat messages into sorting tiers (real content, streaming preview, optimistic echoes) so optimistic bubbles don't sort past the streaming preview due to finite timestamps.
This commit is contained in:
@@ -33,13 +33,13 @@ describe('orderNativeChatMessages', () => {
|
||||
expect(ordered.map((m) => m.id)).toEqual(['a', 'z'])
|
||||
})
|
||||
|
||||
it('sorts the synthetic streaming message after timestamped content', () => {
|
||||
it('sorts the streaming preview after real content but before optimistic echoes', () => {
|
||||
const ordered = orderNativeChatMessages([
|
||||
msg({ id: 'pending:abc', role: 'user', timestamp: 20, source: 'scrape' }),
|
||||
msg({ id: NATIVE_CHAT_STREAMING_ID, timestamp: null }),
|
||||
msg({ id: 'real-user', role: 'user', timestamp: 10 }),
|
||||
msg({ id: 'pending-user', role: 'user', timestamp: 20, source: 'scrape' })
|
||||
msg({ id: 'real-user', role: 'user', timestamp: 10 })
|
||||
])
|
||||
expect(ordered.map((m) => m.id)).toEqual(['real-user', 'pending-user', 'streaming'])
|
||||
expect(ordered.map((m) => m.id)).toEqual(['real-user', 'streaming', 'pending:abc'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ export function isPendingMessageId(id: string): boolean {
|
||||
return id.startsWith('pending:')
|
||||
}
|
||||
|
||||
// Why: the seeded prompt has a synthetic id that never matches the real turn's,
|
||||
// so dedup/prune match on normalized user-message text instead — this hides the
|
||||
// optimistic bubble once the transcript's own copy of the turn catches up.
|
||||
export function launchPromptAsMessage(
|
||||
entry: NativeChatLaunchPrompt | null,
|
||||
existingMessages: NativeChatMessage[] = []
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../../../shared/native-chat-types'
|
||||
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat-streaming'
|
||||
import { normalizeImageTranscriptMessages } from './native-chat-image-transcript-markers'
|
||||
import { isLaunchPromptMessageId, isPendingMessageId } from './native-chat-pending'
|
||||
|
||||
/** Messages grouped by source. Higher-priority sources (transcript > hook >
|
||||
* scrape) supersede lower ones when they describe the same turn. */
|
||||
@@ -85,20 +86,31 @@ function supersedes(candidate: NativeChatMessage, existing: NativeChatMessage):
|
||||
return candidateRank > existingRank
|
||||
}
|
||||
|
||||
function messageSortTimestamp(message: NativeChatMessage): number {
|
||||
// Why: the tail bubbles form fixed tiers that timestamps alone can't express.
|
||||
// The streaming preview (null timestamp) must follow real content but sit ahead
|
||||
// of the optimistic composer echoes, which carry finite `sentAt` timestamps that
|
||||
// would otherwise sort past it. Rank first, then timestamp within a tier.
|
||||
function messageSortRank(message: NativeChatMessage): number {
|
||||
if (message.id === NATIVE_CHAT_STREAMING_ID) {
|
||||
return Number.POSITIVE_INFINITY
|
||||
return 1
|
||||
}
|
||||
return message.timestamp ?? Number.NEGATIVE_INFINITY
|
||||
if (isPendingMessageId(message.id) || isLaunchPromptMessageId(message.id)) {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Why: null timestamps (sources that can't supply one, e.g. scrape segments)
|
||||
// sort before any real timestamp so they don't jump to the end. The synthetic
|
||||
// streaming preview is the one exception: it is a tail bubble and sorts last.
|
||||
// Ties break on id for a stable, deterministic order.
|
||||
// sort before any real timestamp within their tier so they don't jump to the
|
||||
// end. Ties break on id for a stable, deterministic order.
|
||||
export function compareMessages(a: NativeChatMessage, b: NativeChatMessage): number {
|
||||
const at = messageSortTimestamp(a)
|
||||
const bt = messageSortTimestamp(b)
|
||||
const ar = messageSortRank(a)
|
||||
const br = messageSortRank(b)
|
||||
if (ar !== br) {
|
||||
return ar - br
|
||||
}
|
||||
const at = a.timestamp ?? Number.NEGATIVE_INFINITY
|
||||
const bt = b.timestamp ?? Number.NEGATIVE_INFINITY
|
||||
if (at !== bt) {
|
||||
return at - bt
|
||||
}
|
||||
|
||||
@@ -97,6 +97,25 @@ describe('deliverLaunchPromptToAgentTab', () => {
|
||||
expect(mocks.markNativeChatLaunchPromptFailed).toHaveBeenCalledWith('tab-1')
|
||||
})
|
||||
|
||||
it('treats native-prefill delivery as success without flagging the seeded prompt', async () => {
|
||||
// claude delivers via `--prefill` at launch, so paste no-ops (returns false)
|
||||
// when forcePaste is false — that is a native delivery, not a failure.
|
||||
mocks.pasteDraftWhenAgentReady.mockResolvedValue(false)
|
||||
|
||||
await expect(
|
||||
deliverLaunchPromptToAgentTab({
|
||||
tabId: 'tab-1',
|
||||
agent: 'claude',
|
||||
content: 'Large generated prompt',
|
||||
submit: true,
|
||||
forcePaste: false
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(mocks.seedNativeChatLaunchPrompt).toHaveBeenCalled()
|
||||
expect(mocks.markNativeChatLaunchPromptFailed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not mark unseeded launches failed', async () => {
|
||||
mocks.pasteDraftWhenAgentReady.mockResolvedValue(false)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { agentDeliversDraftViaNativePrefill } from '@/lib/agent-native-draft-prefill'
|
||||
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
|
||||
import { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent'
|
||||
import { useAppStore } from '@/store'
|
||||
@@ -25,6 +26,11 @@ export function deliverLaunchPromptToAgentTab(args: {
|
||||
})
|
||||
}
|
||||
|
||||
// Why: native-prefill agents (claude/openclaude etc.) get the prompt at launch,
|
||||
// so pasteDraftWhenAgentReady returns false without pasting. That is a successful
|
||||
// native delivery, not a failure — don't flag the seeded bubble in that case.
|
||||
const deliversViaNativePrefill = agentDeliversDraftViaNativePrefill(agent, forcePaste)
|
||||
|
||||
return pasteDraftWhenAgentReady({
|
||||
tabId,
|
||||
content,
|
||||
@@ -34,9 +40,9 @@ export function deliverLaunchPromptToAgentTab(args: {
|
||||
timeoutMs,
|
||||
onTimeout
|
||||
}).then((delivered) => {
|
||||
if (shouldSeed && !delivered) {
|
||||
if (shouldSeed && !delivered && !deliversViaNativePrefill) {
|
||||
useAppStore.getState().markNativeChatLaunchPromptFailed(tabId)
|
||||
}
|
||||
return delivered
|
||||
return delivered || deliversViaNativePrefill
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { TuiAgent } from '../../../shared/types'
|
||||
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
|
||||
|
||||
// Why: agents with a native draft-prefill flag/env launch with the prompt
|
||||
// already in their input box, so the paste helpers intentionally no-op (return
|
||||
// false) unless `forcePaste` overrides. Callers use this to tell "delivered
|
||||
// natively" apart from a real paste failure.
|
||||
export function agentDeliversDraftViaNativePrefill(
|
||||
agent: TuiAgent | undefined,
|
||||
forcePaste: boolean | undefined
|
||||
): boolean {
|
||||
if (forcePaste) {
|
||||
return false
|
||||
}
|
||||
const agentConfig = agent ? TUI_AGENT_CONFIG[agent] : null
|
||||
return Boolean(agentConfig?.draftPromptFlag || agentConfig?.draftPromptEnvVar)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { waitForAgentReady } from './agent-ready-wait'
|
||||
import { getSettingsForWorktreeRuntimeOwner } from './worktree-runtime-owner'
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
import { sendAgentDraftPasteContent } from './agent-draft-paste-content'
|
||||
import { agentDeliversDraftViaNativePrefill } from './agent-native-draft-prefill'
|
||||
import { waitForAgentDraftInputReady } from './agent-draft-readiness'
|
||||
import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition'
|
||||
export {
|
||||
@@ -91,7 +92,7 @@ export async function pasteDraftWhenAgentReady(args: {
|
||||
// duplicate it. Callers should not invoke this helper for those agents;
|
||||
// the early return guards against accidental double-injection if a stale
|
||||
// call slips through.
|
||||
if (!forcePaste && (agentConfig?.draftPromptFlag || agentConfig?.draftPromptEnvVar)) {
|
||||
if (agentDeliversDraftViaNativePrefill(agent, forcePaste)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ export async function pasteDraftToAgentPtyWhenReady(args: {
|
||||
const { tabId, ptyId, content, agent, submit, forcePaste, timeoutMs, onTimeout } = args
|
||||
const agentConfig = agent ? TUI_AGENT_CONFIG[agent] : null
|
||||
|
||||
if (!forcePaste && (agentConfig?.draftPromptFlag || agentConfig?.draftPromptEnvVar)) {
|
||||
if (agentDeliversDraftViaNativePrefill(agent, forcePaste)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user