mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* fix(mobile): show skill commands you ran in the chat Claude records a skill run (/simplify, /review-pr 123) as a command envelope, which the noise filter hides. Desktop surfaces it as the typed `/name args` before folding; the phone only normalised image markers, so the turn vanished and the reply answered nothing. Both apps now prepare a terminal agent's transcript through one shared prepareNativeChatMessages: skill envelopes, reporting-command output and image marker turns. * fix(native-chat): keep a streamed sentence from coming back after a tool or thought row Both chats hide the live preview once the transcript shows it, but each only compared the preview with the last row. When a tool call or thought landed below the sentence, the last row had no words and the earlier sentence came back at the bottom as a live message. latestNativeChatReplyText reads back past those rows to the newest assistant words in the turn; desktop's preview check and the phone's streaming gate both use it. * fix(mobile): draw errors, warnings and status notices as notices, not agent replies The phone drew every system row through the agent's markdown, so an API retry warning, a sign-in failure, "Context compacted" or "Stopped manually" read as the agent's own words. Only the clear boundary and two host notices were special-cased. Desktop's notice rules move to shared native-chat-notice: which block of a system row is its notice, and the look it gets (divider, command output, host status, plan, aside, or a toned line). Desktop's notice row switches on it with no change in what it draws; the phone's new notice row draws the same looks, with words in faint type when a row has no notice. * fix(native-chat): show a new reply that follows a tool call on desktop The earlier fix read back past a tool call or thought to the newest assistant words, then kept desktop's length rule. Commentary written before the tool is often longer than the reply after it, so a new, shorter reply stayed hidden until the transcript caught up. The last-row rule is back as it was. Reading back now only hides the preview when the earlier words already contain it, which is the sentence still on screen while the tool runs. The phone's gate reads only rows that landed after the current reply began, so it was not affected. * fix(mobile): open the actions sheet from a long press on a plan's link A plan card's Markdown was drawn without the row's long-press handler, so on Android holding a link or file path inside a plan no longer opened Copy message / Select text, as it did before plans were drawn as notices. The notice row now passes the handler to the plan's Markdown.
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
deriveNativeChatStreamingText,
|
|
nativeChatStreamingMessage,
|
|
NATIVE_CHAT_STREAMING_ID
|
|
} from './native-chat-streaming'
|
|
import type { NativeChatMessage } from './native-chat-types'
|
|
|
|
const assistant = (text: string): NativeChatMessage => ({
|
|
id: `a-${text.length}`,
|
|
role: 'assistant',
|
|
blocks: [{ type: 'text', text }],
|
|
timestamp: null,
|
|
source: 'transcript'
|
|
})
|
|
const user = (text: string): NativeChatMessage => ({
|
|
id: `u-${text.length}`,
|
|
role: 'user',
|
|
blocks: [{ type: 'text', text }],
|
|
timestamp: null,
|
|
source: 'transcript'
|
|
})
|
|
|
|
describe('deriveNativeChatStreamingText', () => {
|
|
it('returns null when not working (stale preview never shows)', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({ messages: [], previewText: 'Hello there', working: false })
|
|
).toBeNull()
|
|
})
|
|
|
|
it('returns null for empty / whitespace preview', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({ messages: [], previewText: '', working: true })
|
|
).toBeNull()
|
|
expect(
|
|
deriveNativeChatStreamingText({ messages: [], previewText: ' ', working: true })
|
|
).toBeNull()
|
|
})
|
|
|
|
it('shows the preview while it leads an empty/user-tailed transcript', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [user('do the thing')],
|
|
previewText: 'Working on it',
|
|
working: true
|
|
})
|
|
).toBe('Working on it')
|
|
})
|
|
|
|
it('treats an optimistic user echo as the active streaming-turn boundary', () => {
|
|
const optimistic = {
|
|
...user('new prompt'),
|
|
id: 'pending:send-1',
|
|
timestamp: 20,
|
|
source: 'scrape' as const
|
|
}
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('A much longer answer from the completed prior turn'), optimistic],
|
|
previewText: 'New reply',
|
|
working: true
|
|
})
|
|
).toBe('New reply')
|
|
})
|
|
|
|
it('drops the preview once the real assistant turn contains it (no duplicate)', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('Working on it, here is the full answer.')],
|
|
previewText: 'Working on it',
|
|
working: true
|
|
})
|
|
).toBeNull()
|
|
})
|
|
|
|
it('drops the preview when it is not longer than the last assistant turn (no flicker)', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('Same length text')],
|
|
previewText: 'Same length text',
|
|
working: true
|
|
})
|
|
).toBeNull()
|
|
})
|
|
|
|
it('drops a preview flagged as tool output even when it leads the transcript', () => {
|
|
// Regression: providers publish a tool's stdout as `lastAssistantMessage` for status
|
|
// cards. It leads every transcript assistant turn and never appears in one, so without
|
|
// this gate it rendered as the reply and no catch-up rule could ever retire it.
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('Partial')],
|
|
previewText: 'Exit code 1\nimport { Foo } from "./foo"\nexport function bar() {}',
|
|
working: true,
|
|
previewIsToolOutput: true
|
|
})
|
|
).toBeNull()
|
|
})
|
|
|
|
it('still shows a leading preview when it is not tool output', () => {
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('Partial')],
|
|
previewText: 'Partial answer that is now much longer than before',
|
|
working: true,
|
|
previewIsToolOutput: false
|
|
})
|
|
).toBe('Partial answer that is now much longer than before')
|
|
})
|
|
|
|
it('keeps showing while the preview still leads (grows past the last turn)', () => {
|
|
// The transcript hasn't flushed the new content yet; preview is longer.
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages: [assistant('Partial')],
|
|
previewText: 'Partial answer that is now much longer than before',
|
|
working: true
|
|
})
|
|
).toBe('Partial answer that is now much longer than before')
|
|
})
|
|
it('hides the sentence it streamed while a tool runs, but not a new reply after the tool', () => {
|
|
// A Claude transcript writes the sentence and the tool call it led into as separate rows.
|
|
const sentence = 'Checking the notes file now, then I will compare it with the settings.'
|
|
const toolCall: NativeChatMessage = {
|
|
id: 'f4470ba3',
|
|
role: 'assistant',
|
|
blocks: [{ type: 'tool-call', name: 'Bash', input: { command: 'sleep 12' } }],
|
|
timestamp: null,
|
|
source: 'transcript'
|
|
}
|
|
const messages = [user('Say it, then run sleep 12.'), assistant(sentence), toolCall]
|
|
expect(
|
|
deriveNativeChatStreamingText({ messages, previewText: sentence, working: true })
|
|
).toBeNull()
|
|
expect(
|
|
deriveNativeChatStreamingText({
|
|
messages,
|
|
previewText: 'The settings look correct.',
|
|
working: true
|
|
})
|
|
).toBe('The settings look correct.')
|
|
})
|
|
})
|
|
|
|
describe('nativeChatStreamingMessage', () => {
|
|
it('builds a stable-id assistant hook message', () => {
|
|
const m = nativeChatStreamingMessage('hi')
|
|
expect(m.id).toBe(NATIVE_CHAT_STREAMING_ID)
|
|
expect(m.role).toBe('assistant')
|
|
expect(m.source).toBe('hook')
|
|
expect(m.blocks).toEqual([{ type: 'text', text: 'hi' }])
|
|
})
|
|
})
|