mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-22 05:15:13 +00:00
* feat(agent): /home composer parity with image attachments
The /home composer used the same ConversationInput component as the
chat screen but passed attachmentsEnabled={false}, and the home →
chat handoff was a URL search param `?q=<text>` that physically
can't carry binary attachments. Pasting a screenshot at /home did
nothing.
Add a small in-memory registry (pending-initial-message.ts) as the
rich-data side channel for the same navigation: the home composer
writes { agentId, text, attachments } there before navigating; the
chat screen consumes it on mount and replays through the existing
harness send() path that already supports attachments. URL `?q=`
stays for shareable text-only prompts; the registry wins when both
are present. Module-scope, 10s TTL, destructive consume.
Net: home is now flagged attachmentsEnabled={true}; users can paste,
drag, or pick image files at /home and they survive the navigation
into the chat screen with previews intact.
* docs(agent): clarify why initial-message ref reset is safe post-registry-fire
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, it } from 'bun:test'
|
|
import type { StagedAttachment } from '@/lib/attachments'
|
|
import {
|
|
consumePendingInitialMessage,
|
|
peekPendingInitialMessage,
|
|
setPendingInitialMessage,
|
|
} from './pending-initial-message'
|
|
|
|
function makeAttachment(id: string): StagedAttachment {
|
|
return {
|
|
id,
|
|
kind: 'image',
|
|
mediaType: 'image/png',
|
|
name: `${id}.png`,
|
|
dataUrl: `data:image/png;base64,${id}`,
|
|
payload: {
|
|
kind: 'image',
|
|
mediaType: 'image/png',
|
|
name: `${id}.png`,
|
|
dataUrl: `data:image/png;base64,${id}`,
|
|
},
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
// Drain any leftover pending entry so tests don't leak into each
|
|
// other (the module-scope state survives across `it` blocks).
|
|
consumePendingInitialMessage('drain')
|
|
// If still set, clear by consuming with the matching id.
|
|
const leftover = peekPendingInitialMessage()
|
|
if (leftover) consumePendingInitialMessage(leftover.agentId)
|
|
})
|
|
|
|
describe('pending-initial-message', () => {
|
|
it('consume returns the payload set for the same agentId', () => {
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-a',
|
|
text: 'hello',
|
|
attachments: [makeAttachment('one')],
|
|
createdAt: Date.now(),
|
|
})
|
|
const result = consumePendingInitialMessage('agent-a')
|
|
expect(result?.text).toBe('hello')
|
|
expect(result?.attachments).toHaveLength(1)
|
|
expect(result?.attachments[0]?.id).toBe('one')
|
|
})
|
|
|
|
it('consume is destructive — second call returns null', () => {
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-a',
|
|
text: 'hello',
|
|
attachments: [],
|
|
createdAt: Date.now(),
|
|
})
|
|
expect(consumePendingInitialMessage('agent-a')).not.toBeNull()
|
|
expect(consumePendingInitialMessage('agent-a')).toBeNull()
|
|
})
|
|
|
|
it('consume returns null and preserves entry when agentId differs', () => {
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-a',
|
|
text: 'hello',
|
|
attachments: [],
|
|
createdAt: Date.now(),
|
|
})
|
|
expect(consumePendingInitialMessage('agent-b')).toBeNull()
|
|
expect(peekPendingInitialMessage()?.agentId).toBe('agent-a')
|
|
expect(consumePendingInitialMessage('agent-a')).not.toBeNull()
|
|
})
|
|
|
|
it('returns null for entries older than the TTL', () => {
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-a',
|
|
text: 'old',
|
|
attachments: [],
|
|
createdAt: Date.now() - 11_000, // older than 10 s TTL
|
|
})
|
|
expect(consumePendingInitialMessage('agent-a')).toBeNull()
|
|
})
|
|
|
|
it('replaces a previous pending entry when set is called again', () => {
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-a',
|
|
text: 'first',
|
|
attachments: [],
|
|
createdAt: Date.now(),
|
|
})
|
|
setPendingInitialMessage({
|
|
agentId: 'agent-b',
|
|
text: 'second',
|
|
attachments: [makeAttachment('two')],
|
|
createdAt: Date.now(),
|
|
})
|
|
expect(consumePendingInitialMessage('agent-a')).toBeNull()
|
|
const result = consumePendingInitialMessage('agent-b')
|
|
expect(result?.text).toBe('second')
|
|
expect(result?.attachments[0]?.id).toBe('two')
|
|
})
|
|
|
|
it('no-ops when set is called with empty agentId', () => {
|
|
setPendingInitialMessage({
|
|
agentId: '',
|
|
text: 'oops',
|
|
attachments: [],
|
|
createdAt: Date.now(),
|
|
})
|
|
expect(peekPendingInitialMessage()).toBeNull()
|
|
})
|
|
})
|