Show messages between agents as cards with the other agent's status (#34824)

* Snapshot agent message relations and verify task display prefixes

* Show native chat agent messages in navigable cards

* Keep agent message cards derived from canonical input and sidebar state

* test: correct agent card contracts and cover raw queue editing

* test: repair agent card runtime and operation contracts

* fix: preserve queued image previews and typed test measurements

* fix: use checked worker fixture and owned sender button typography

* fix: complete agent card fixtures and native presentation

* test: preserve queued edit exports and current authorship

* test: use namespace type imports in queued edit fixture

* Preserve sender progress and consolidate card contracts

* Simplify sender and notice contract instrumentation

* Remove duplicate sender and notice measurement mirrors

* Use a template literal for the dispatch boundary fixture

* Correct sender and notice verification phase accounting

* Consolidate agent message tests around user contracts

* Restore folded agent message preview on focus exit

* Consolidate agent message regression coverage

* Consolidate agent message fixture data

* Fix display text test lint diagnostics

* Use conversation view type for mail test records

* Reuse active copy control in card workflow test
This commit is contained in:
Brennan Benson
2026-10-29 13:09:50 -07:00
committed by GitHub
parent cb21824bd9
commit 7321b066e8
33 changed files with 2677 additions and 153 deletions
@@ -20,7 +20,13 @@ describe('who another agent’s message is from, on mobile', () => {
})
it('names each sender, an unnamed one as an agent, and counts the rest', () => {
expect(agentMessageAttribution('Message from', from(['Coder']))).toBe('Message from Coder')
const attributed = from(['Coder'])
expect(
agentMessageAttribution('Message from', {
...attributed,
senders: attributed.senders.map((sender) => ({ ...sender, relation: 'worker' }))
})
).toBe('Message from Coder')
expect(agentMessageAttribution('From', from([]))).toBe('From an agent')
expect(agentMessageAttribution('From', from(['A', 'B', 'C', 'D']))).toBe('From A, B, C +1')
})
@@ -70,7 +70,10 @@ export function mobileQueuedMessageCards(
...card,
line: queuedMessageCardLine({
...card,
text: stripSentImagePromptMarkers(card.text, card.images?.length ?? 0)
displayText: stripSentImagePromptMarkers(
card.displayText ?? card.text,
card.images?.length ?? 0
)
}),
needsAttention: queuedMessageCardNeedsAttention(card),
caption: queuedMessageCardCaption(card, facts.agentName, facts.statedFailures ?? []),
@@ -58,6 +58,74 @@ describe('mobile in-place queued Edit', () => {
expect(state.hook!.queued.edit).toEqual(expect.any(Function))
})
it('opens full canonical task input and publishes a changed Save as user-authored raw input', async () => {
const prefix = 'You are working inside Orca, a multi-agent IDE.\n=== TASK ===\n'
const message = queuedDraft({
messageId: 'task',
body: {
kind: 'message',
role: 'user',
blocks: [
{ type: 'text', text: `${prefix} Task 🐋 [Image #1] ` },
{ type: 'image-ref', path: '/host/shot.png' }
],
from: {
kind: 'agent',
senders: [
{
party: { address: 'term_coord', terminalHandle: 'term_coord', orcaSessionId: null },
name: 'Coordinator',
relation: 'coordinator'
}
],
orchestration: { message: 'task', runId: 'r', taskId: 't', dispatchId: 'd' }
}
}
})
await mount(message)
expect(state.hook!.queued.cards[0]).toMatchObject({
line: ' Task 🐋',
attribution: 'From Coordinator'
})
expect(state.hook!.queued.cards[0].images).toHaveLength(1)
expect(state.hook!.queued.editor!.text).toBe(`${prefix} Task 🐋 [Image #1] `)
await act(async () => {
expect(await state.hook!.queued.editor!.save()).toBe(true)
})
expect(
sendRequest.mock.calls.some(([method]) => method === 'agentSession.queuedMessageUpdate')
).toBe(false)
expect(state.hook!.queued.cards[0]).toMatchObject({
line: ' Task 🐋',
attribution: 'From Coordinator'
})
await act(async () => {
await state.hook!.queued.edit!('task')
})
act(() => state.hook!.queued.editor!.change(`${prefix}Edited`))
await act(async () => {
expect(await state.hook!.queued.editor!.save()).toBe(true)
})
const update = sendRequest.mock.calls.find(
([method]) => method === 'agentSession.queuedMessageUpdate'
)
expect(fieldsOf(update?.[1]).text).toBe(`${prefix}Edited`)
// The current host commits changed content without the original agent attribution.
const { from: _sender, ...userBody } = withText(message.body, `${prefix}Edited`)
const saved: typeof message = { ...message, body: userBody }
act(() => state.listener?.(batchEvent([saved])))
const [savedCard] = state.hook!.queued.cards
expect(savedCard).toMatchObject({
line: `${prefix}Edited`,
attribution: null
})
await act(async () => {
await state.hook!.queued.edit!('task')
})
expect(state.hook!.queued.editor!.text).toBe(`${prefix}Edited`)
await act(async () => state.hook!.queued.editor!.cancel())
})
it.each([
[false, false],
[true, false],
@@ -19,7 +19,7 @@ import {
} from './structured-agent-session-host'
import {
HOST_TEST_NOW as NOW,
HOST_TEST_SESSION as SESSION,
HOST_TEST_SESSION as DEFAULT_SESSION,
HOST_TEST_THREAD as THREAD,
hostTestAttachParams,
hostTestMessage,
@@ -52,6 +52,7 @@ export type QueuedMessageTestRig = Awaited<ReturnType<typeof createQueuedMessage
export async function createQueuedMessageTestRig(
options: QueuedRigProviderOptions & {
sessionId?: string
idleSweep?: { idleMs: number; intervalMs: number }
now?: () => number
/** Teardown records its restart offers, which `restartOffers` lists. */
@@ -61,6 +62,7 @@ export async function createQueuedMessageTestRig(
probeOwner?: StructuredAgentSessionHostDeps['probeOwner']
} = {}
) {
const SESSION = options.sessionId ?? DEFAULT_SESSION
const root = await mkdtemp(join(tmpdir(), 'orca-queued-messages-'))
resetHostTestOperationIds()
const store = await openTestAgentSessionRecordStore(root)
@@ -83,6 +85,12 @@ export async function createQueuedMessageTestRig(
let host = makeHost()
const firstAttach = hostTestAttachParams(null, {
...(options.location ? { location: options.location } : {}),
envelope: {
sessionId: SESSION,
clientOperationId: hostTestOperationId(),
expectedRuntimeFence: null,
payloadFingerprint: ''
},
...(options.agent === 'claude'
? ({
provider: 'claude',
@@ -0,0 +1,106 @@
import type { AgentMessageSender } from '../../../shared/agent-session/agent-session-message-source'
import { readWorkerTerminalHostScope } from '../../../shared/worker-terminal-host-scope'
import type { OrchestrationDb, DispatchContextRow } from './db'
import { canonicalOrcaSessionId } from './canonical-orca-session-id'
import { resolveOrchestrationParty } from './orchestration-party'
import { runCoordinatorKey } from './orchestration-caller-identity'
type Party = Pick<AgentMessageSender['party'], 'terminalHandle' | 'orcaSessionId'>
function sameParty(a: Party, b: Party): boolean {
if (a.orcaSessionId && b.orcaSessionId) {
return canonicalOrcaSessionId(a.orcaSessionId) === canonicalOrcaSessionId(b.orcaSessionId)
}
return Boolean(a.terminalHandle && a.terminalHandle === b.terminalHandle)
}
function localDispatch(dispatch: DispatchContextRow): boolean {
const scope = readWorkerTerminalHostScope(dispatch.host_scope)
return scope.kind === 'absent' || (scope.kind === 'local' && scope.id === 'local')
}
/** Delivery-time evidence only; missing, ambiguous or foreign lineage is unknown. */
export function agentMessageRelation(args: {
db: OrchestrationDb | null
sender: AgentMessageSender['party']
recipient: string
runId: string
reportedDispatchId?: string
recipientDispatchId: string | null
}): AgentMessageSender['relation'] {
try {
const { db, sender, runId } = args
if (!db || sender.address.startsWith('dispatch:') || args.recipient.startsWith('dispatch:')) {
return undefined
}
const run = db.getRun(runId)
if (!run) {
return undefined
}
const recipient =
args.recipient === `run:${runId}`
? runCoordinatorKey(run)
: resolveOrchestrationParty(args.recipient, db)
const coordinator = runCoordinatorKey(run)
const dispatchFor = (party: Party, id?: string | null) => {
const candidates = id
? [db.getDispatchContextById(id)]
: db.getActiveDispatchMailboxOwners(
party.terminalHandle ??
(party.orcaSessionId ? `orca_session_id:${party.orcaSessionId}` : '')
)
const matches = candidates.filter((row): row is DispatchContextRow =>
Boolean(
row &&
row.run_id === runId &&
localDispatch(row) &&
sameParty(party, {
terminalHandle: row.assignee_handle,
orcaSessionId: row.assignee_orca_session_id
})
)
)
return matches.length === 1 ? matches[0] : undefined
}
const parentOf = (dispatch: DispatchContextRow | undefined): Party | undefined => {
if (!dispatch) {
return undefined
}
if (dispatch.creator_handle || dispatch.creator_orca_session_id) {
return {
terminalHandle: dispatch.creator_handle,
orcaSessionId: dispatch.creator_orca_session_id
}
}
if (dispatch.creator_dispatch_id) {
const creator = db.getDispatchContextById(dispatch.creator_dispatch_id)
return creator && creator.run_id === runId && localDispatch(creator)
? {
terminalHandle: creator.assignee_handle,
orcaSessionId: creator.assignee_orca_session_id
}
: undefined
}
return undefined
}
const senderDispatch = dispatchFor(sender, args.reportedDispatchId)
const recipientDispatch = dispatchFor(recipient, args.recipientDispatchId)
const senderParent = parentOf(senderDispatch)
const recipientParent = parentOf(recipientDispatch)
if (!sameParty(sender, recipient)) {
if (senderParent && sameParty(senderParent, recipient)) {
return 'worker'
}
if (recipientParent && sameParty(recipientParent, sender)) {
return 'coordinator'
}
}
return (senderParent || sameParty(sender, coordinator)) &&
(recipientParent || sameParty(recipient, coordinator))
? 'none'
: undefined
} catch {
// Lineage is informational; an unavailable database must never block delivery.
return undefined
}
}
@@ -14,7 +14,9 @@ export function dispatchTaskSource(args: {
const { dispatch } = args
return {
kind: 'agent',
senders: args.from ? [agentMessageSender(args.from, args.db, args.senderName)] : [],
senders: args.from
? [{ ...agentMessageSender(args.from, args.db, args.senderName), relation: 'coordinator' }]
: [],
orchestration: {
message: 'task',
runId: dispatch.run_id,
@@ -1,5 +1,20 @@
import { describe, expect, it } from 'vitest'
import { structuredMailSource } from './structured-mail-source'
import { OrchestrationDb } from './db'
import { describe, expect, it, vi } from 'vitest'
import { readAgentMessageSource } from '../../../shared/agent-session/agent-session-message-source'
import { agentMessageHeading } from '../../../shared/agent-session/agent-message-presentation'
import {
agentSessionConversationView,
type AgentSessionConversationView
} from '../../../shared/agent-session-conversation-view'
import {
agentSessionLeaseFixture,
agentSessionRecordFixture
} from '../../../shared/agent-session-record.test-fixture'
import { testOrcaSessionId } from '../../../shared/orca-session-address-test-fixture'
import { orchestrationSenderName } from './orchestration-sender-name'
import * as relations from './agent-message-relation'
import * as lineage from './structured-session-lineage'
import { structuredMailSource, type MailSourceMessage } from './structured-mail-source'
const SESSION = '4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37'
@@ -70,3 +85,218 @@ describe('who delivered mail is from', () => {
).toBeNull()
})
})
it('snapshots immediate relations across completion/reload and keeps unknown evidence non-gating', () => {
const db = new OrchestrationDb(':memory:')
try {
const run = db.createRun({
objective: 'cards',
coordinatorHandle: 'term_coord',
coordinatorPaneKey: null
})
const assign = (handle: string, creator: string) =>
db.createDispatchContext({
taskId: db.createTask({ runId: run.id, spec: handle }).id,
assigneeHandle: handle,
creator: { kind: 'terminal', handle: creator },
maxDepth: 8
})
const worker = assign('term_worker', 'term_coord')
assign('term_peer', 'term_coord')
const child = assign('term_child', 'term_worker')
const mail = (
recipient: string,
addresses: string[],
dispatchId: string | null = null,
report?: string
) =>
structuredMailSource({
db,
mailboxHandle: recipient,
dispatchId,
senderName: () => 'Sender',
batch: addresses.map<MailSourceMessage>((address, i) => ({
id: `m${i}`,
from_handle: address,
run_id: run.id,
type: report ? 'worker_done' : 'status',
payload: report ? JSON.stringify({ dispatchId: report }) : null
}))
})
expect(mail('term_coord', ['term_worker']).senders[0].relation).toBe('worker')
expect(agentMessageHeading(mail('term_coord', ['term_worker']))).toBe('worker')
expect(mail('term_worker', ['term_coord'], worker.id).senders[0].relation).toBe('coordinator')
expect(agentMessageHeading(mail('term_worker', ['term_coord'], worker.id))).toBe('coordinator')
expect(mail('term_coord', ['term_child']).senders[0].relation).toBe('none')
const delivered = mail('term_worker', ['term_child', 'term_peer'], worker.id)
db.db.prepare("UPDATE dispatch_contexts SET status = 'completed' WHERE id = ?").run(child.id)
const reload = readAgentMessageSource(JSON.parse(JSON.stringify(delivered)))!
expect(reload.senders.map((sender) => sender.relation)).toEqual(['worker', 'none'])
expect(agentMessageHeading(reload)).toBe('generic')
expect(mail('term_worker', ['term_child'], worker.id).senders[0].relation).toBeUndefined()
expect(mail('term_worker', ['term_child'], worker.id, child.id).senders[0].relation).toBe(
'worker'
)
expect(mail('term_coord', ['unknown']).senders[0].relation).toBeUndefined()
db.db
.prepare('UPDATE dispatch_contexts SET creator_handle = NULL, depth = 1 WHERE id = ?')
.run(worker.id)
expect(mail('term_coord', ['term_worker']).senders[0].relation).toBeUndefined()
db.db
.prepare('UPDATE dispatch_contexts SET creator_handle = ? WHERE id = ?')
.run('term_coord', worker.id)
const good: MailSourceMessage = {
id: 'good',
from_handle: 'term_worker',
run_id: run.id,
type: 'worker_done',
payload: JSON.stringify({ dispatchId: worker.id })
}
for (const bad of [
{ ...good, id: 'foreign', run_id: 'missing' },
{ ...good, id: 'wrong-report', payload: JSON.stringify({ dispatchId: child.id }) },
{ ...good, id: 'invalid', payload: '{' }
]) {
const source = structuredMailSource({
db,
mailboxHandle: 'term_coord',
dispatchId: null,
batch: [good, bad],
senderName: () => 'Sender'
})
expect(source.senders[0].relation).toBeUndefined()
expect(source.orchestration).toMatchObject({
messages: [{ messageId: 'good' }, { messageId: bad.id }]
})
}
db.db
.prepare('UPDATE dispatch_contexts SET host_scope = ? WHERE id = ?')
.run(JSON.stringify({ kind: 'ssh', targetId: 'elsewhere' }), worker.id)
expect(mail('term_coord', ['term_worker']).senders[0].relation).toBeUndefined()
} finally {
vi.restoreAllMocks()
db.close()
}
})
it.each([100, 1000])(
'bounds real canonical record work for repeated/distinct/unknown notices with %i workers',
(n) => {
const db = new OrchestrationDb(':memory:')
const coordinator = testOrcaSessionId('coordinator')
const sessions = Array.from({ length: n }, (_, i) => testOrcaSessionId(`worker-${i}`))
const base = agentSessionConversationView(
agentSessionRecordFixture(agentSessionLeaseFixture({ sessionId: coordinator }))
)
const successor = testOrcaSessionId('cleared-worker')
const records: AgentSessionConversationView[] = sessions.map((sessionId, i) => ({
...base,
sessionId,
conversationName: `Worker ${i}`
}))
records[0] = {
...records[0],
conversationCommand: {
command: 'clear',
state: 'completed',
phase: 'committed',
operationId: 'clear',
callerKey: 'caller',
replacementSessionId: successor
}
}
records.push(
{ ...base, conversationName: 'Coordinator' },
{ ...base, sessionId: successor, conversationName: 'Worker 0' }
)
const bySession = new Map(records.map((record) => [record.sessionId, record]))
let visits = 0
records.forEach((record, i) =>
Object.defineProperty(records, i, {
get() {
visits++
return record
}
})
)
const reader = {
listRecords: () => records,
getRecord: (id: string) => bySession.get(id) ?? null
}
vi.spyOn(lineage, 'readAgentSessionRecordStore').mockReturnValue(reader)
try {
const run = db.createRun({
objective: 'cost',
coordinatorHandle: null,
coordinatorPaneKey: null,
coordinatorOrcaSessionId: coordinator
})
const dispatches = sessions.slice(0, 10).map((session) =>
db.createDispatchContext({
taskId: db.createTask({ runId: run.id, spec: 'work' }).id,
assigneeHandle: `orca_session_id:${session}`,
creator: { kind: 'session', orcaSessionId: coordinator },
maxDepth: 8
})
)
for (const evidence of ['repeated', 'distinct', 'unknown'] as const) {
const distinct = evidence === 'distinct'
const relation = vi.spyOn(relations, 'agentMessageRelation')
relation.mockClear()
const batch: MailSourceMessage[] = Array.from({ length: n }, (_, i) => ({
id: `m${i}`,
from_handle: `orca_session_id:${!distinct || i % 10 === 0 ? successor : sessions[i % 10]}`,
run_id: evidence === 'unknown' ? 'missing' : run.id,
type: 'worker_done',
payload: JSON.stringify({ dispatchId: dispatches[distinct ? i % 10 : 0].id })
}))
let batchReads = 0
batch.forEach((message, i) =>
Object.defineProperty(batch, i, {
get() {
batchReads++
return message
}
})
)
visits = 0
const source = structuredMailSource({
db,
mailboxHandle: `orca_session_id:${coordinator}`,
dispatchId: null,
batch,
senderName: (party, report) =>
orchestrationSenderName(
party,
{
db,
records: reader,
chatTab: () => null,
terminal: () => null,
generatedTitlesEnabled: true
},
report
)
})
const groups = distinct ? 10 : 1
// Known groups walk canonical records eight times; a missing Run ends after sender canonicalization.
expect(visits).toBe((evidence === 'unknown' ? 1 : 8 * groups) * records.length)
expect(relation).toHaveBeenCalledTimes(groups)
expect(batchReads).toBe(2 * n)
expect(source.senders[0].party.orcaSessionId).toBe(sessions[0])
expect(source.senders.map((sender) => [sender.name, sender.relation])).toEqual(
Array.from({ length: groups }, (_, i) => [
`Worker ${i}`,
evidence === 'unknown' ? undefined : 'worker'
])
)
expect(
source.orchestration?.message === 'mail-notice' && source.orchestration.messages
).toHaveLength(n)
}
} finally {
vi.restoreAllMocks()
db.close()
}
}
)
@@ -6,6 +6,7 @@ import type {
} from '../../../shared/agent-session/agent-session-message-source'
import type { AgentJournalMessageItem } from '../../../shared/agent-session-journal-types'
import type { MessageRow, OrchestrationDb } from './db'
import { agentMessageRelation } from './agent-message-relation'
import { agentMessageSender, type SenderNameResolver } from './agent-message-sender'
import type { OrchestrationCliCommand } from './cli-command'
import { formatMessagePointer } from './formatter'
@@ -23,23 +24,54 @@ export function structuredMailSource(input: {
batch: readonly MailSourceMessage[]
senderName: SenderNameResolver
}): AgentMessageSource {
const senders = new Map<string, AgentMessageSender>()
for (const { from_handle: address } of input.batch) {
if (!senders.has(address)) {
senders.set(
address,
agentMessageSender(
address,
input.db,
input.senderName,
reportedDispatchId(input.batch, address)
)
)
const groups = new Map<string, { message: MailSourceMessage; report: ReportFact }[]>()
for (const message of input.batch) {
let group = groups.get(message.from_handle)
if (!group) {
group = []
groups.set(message.from_handle, group)
}
group.push({ message, report: reportFact(message) })
}
const senders: AgentMessageSender[] = []
for (const [address, group] of groups) {
const reported = group.find(({ report }) => report.kind === 'valid')?.report
const sender = agentMessageSender(
address,
input.db,
input.senderName,
reported?.kind === 'valid' ? reported.dispatchId : undefined
)
const relations = new Map<string, AgentMessageSender['relation']>()
const values = group.map(({ message, report }) => {
// Sender and DB are fixed by this group/invocation; every varying evidence field is keyed.
const key = JSON.stringify([input.mailboxHandle, input.dispatchId, message.run_id, report])
if (!relations.has(key)) {
relations.set(
key,
report.kind === 'invalid'
? undefined
: agentMessageRelation({
db: input.db,
sender: sender.party,
recipient: input.mailboxHandle,
runId: message.run_id,
reportedDispatchId: report.kind === 'valid' ? report.dispatchId : undefined,
recipientDispatchId: input.dispatchId
})
)
}
return relations.get(key)
})
const relation = values[0]
senders.push({
...sender,
...(relation && values.every((value) => value === relation) ? { relation } : {})
})
}
return {
kind: 'agent',
senders: [...senders.values()],
senders,
orchestration: {
message: 'mail-notice',
mailbox: input.mailboxHandle,
@@ -77,28 +109,27 @@ export function structuredMailNotice(input: {
}
}
/** The dispatch a sender's own `worker_done` here reports: the task it just finished, whose
* dispatch that report already settled. */
function reportedDispatchId(
batch: readonly MailSourceMessage[],
address: string
): string | undefined {
for (const message of batch) {
if (message.from_handle !== address || message.type !== 'worker_done' || !message.payload) {
continue
}
type ReportFact = { kind: 'mail' } | { kind: 'invalid' } | { kind: 'valid'; dispatchId: string }
function reportFact(message: MailSourceMessage): ReportFact {
if (message.type !== 'worker_done') {
return { kind: 'mail' }
}
if (message.payload) {
try {
const payload: unknown = JSON.parse(message.payload)
const dispatchId =
typeof payload === 'object' && payload !== null && 'dispatchId' in payload
? payload.dispatchId
: undefined
if (typeof dispatchId === 'string' && dispatchId.length > 0) {
return dispatchId
if (
typeof payload === 'object' &&
payload !== null &&
'dispatchId' in payload &&
typeof payload.dispatchId === 'string' &&
payload.dispatchId.length > 0
) {
return { kind: 'valid', dispatchId: payload.dispatchId }
}
} catch {
// A payload that does not parse names no dispatch.
// Invalid evidence stays unknown; delivery and the other messages remain intact.
}
}
return undefined
return { kind: 'invalid' }
}
@@ -23,6 +23,7 @@ const IDENTITY: StructuredWorkerIdentity = {
function harness(options: {
/** False: the session cannot be read (not attached). */
relationReadFails?: boolean
attached?: boolean
dispatchState?: 'accepted' | 'rejected' | 'unknown'
/** The chat was busy: its queue holds the pointer as a card. */
@@ -62,7 +63,14 @@ function harness(options: {
() => undefined
)
const stored = new Map<string, StructuredPointerOperationRow>()
const relationRead = vi.fn(() => {
if (options.relationReadFails) {
throw new Error('lineage unavailable')
}
return undefined
})
const db = {
getRun: relationRead,
getDispatchContextById: () => ({ run_id: 'run_1' }),
getOutstandingMailboxDelivery: (handle: string) =>
((options.outstandingRunDelivery ?? false) && handle.startsWith('run:')) ||
@@ -104,6 +112,7 @@ function harness(options: {
})
return {
delivery,
relationRead,
markAsDelivered,
send: sendMock,
readContext,
@@ -232,19 +241,30 @@ describe('structured mailbox pointer delivery', () => {
expect(markAsDelivered).toHaveBeenCalledWith(['m1'], 'dispatch:d1')
})
it('rechecks a held notice when the answer edge lands during its host call', async () => {
const { delivery, send, markAsDelivered, setAwaitingHuman } = harness({})
setAwaitingHuman(true)
send.mockImplementationOnce(async () => {
setAwaitingHuman(false)
delivery.onJournalActivity('session-1')
return { kind: 'held' }
})
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(send).toHaveBeenCalledTimes(2)
expect(markAsDelivered).toHaveBeenCalledWith(['m1'], 'dispatch:d1')
})
it.each([false, true])(
'rechecks the original dispatch and direct failed-relation held notice (failure=%s)',
async (relationReadFails) => {
const { delivery, send, markAsDelivered, setAwaitingHuman, relationRead } = harness({
relationReadFails,
...(relationReadFails ? { mailbox: 'term_worker' } : {})
})
setAwaitingHuman(true)
send.mockImplementationOnce(async () => {
setAwaitingHuman(false)
delivery.onJournalActivity('session-1')
return { kind: 'held' }
})
delivery.deliverForHandle(relationReadFails ? 'term_worker' : 'dispatch:d1')
await flush()
expect(send).toHaveBeenCalledTimes(2)
expect(relationRead).toHaveBeenCalledTimes(relationReadFails ? 2 : 0)
expect(send.mock.calls[1][0].body.from?.senders[0].relation).toBeUndefined()
expect(markAsDelivered).toHaveBeenCalledWith(
['m1'],
relationReadFails ? 'term_worker' : 'dispatch:d1'
)
}
)
it('sends the pointer through the chat, with who it is from, and counts it pointed once queued', async () => {
const { delivery, send, markAsDelivered, stored } = harness({ queued: true })
@@ -0,0 +1,278 @@
import { afterEach, expect, it, vi } from 'vitest'
import { createOrchestrationRpcHarness } from './rpc-test-harness'
import { resolveOrchestrationSessionCaller } from '../../orchestration-session-caller'
import { setStructuredAgentSessionHost } from '../../../../native-chat/agent-session-wire/structured-agent-session-registry'
import {
createQueuedMessageTestRig,
eventually,
QUEUED_RIG_CALLER,
type QueuedMessageTestRig
} from '../../../../native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture'
import {
hostTestAttachParams,
hostTestOperationId
} from '../../../../native-chat/agent-session-wire/structured-agent-session-host-test-data'
import { testOrcaSessionId } from '../../../../../shared/orca-session-address-test-fixture'
import { agentMessageDisplayText } from '../../../../../shared/agent-session/agent-message-display-text'
import { readAgentMessageSource } from '../../../../../shared/agent-session/agent-session-message-source'
import { nativeChatMessageCopyText } from '../../../../../shared/native-chat-message-copy-text'
import {
projectQueuedMessageCards,
queuedMessageCardLine
} from '../../../../../shared/structured-agent-session-queued-cards'
import { projectStructuredAgentSessionStatusSummary } from '../../../../../shared/structured-agent-session-projection'
import { latestStructuredAgentSessionPrompt } from '../../../../../shared/structured-agent-session-latest-request'
import { isOrcaDispatchPrompt } from '../../../../../shared/agent-row-primary-text'
import * as placement from './worker/worker-start-agent-placement'
import * as topology from './worker/worker-topology'
const h = createOrchestrationRpcHarness()
let rig: QueuedMessageTestRig | undefined
const COORD = testOrcaSessionId('command-coordinator')
const WORKER = testOrcaSessionId('command-worker')
afterEach(async () => {
setStructuredAgentSessionHost(null)
await rig?.dispose()
rig = undefined
h.cleanup()
vi.unstubAllEnvs()
vi.restoreAllMocks()
vi.doUnmock('../../../../../cli/runtime-client')
vi.doUnmock('../../../../../cli/format')
vi.resetModules()
process.exitCode = 0
})
it.each(['worker-start', 'dispatch'] as const)(
'real %s argv constructs, persists and delivers canonical input with a derived task display',
async (command) => {
rig = await createQueuedMessageTestRig({ sessionId: WORKER })
const current = rig
setStructuredAgentSessionHost(current.host)
const attach = hostTestAttachParams(null, {
envelope: {
sessionId: COORD,
clientOperationId: hostTestOperationId(),
expectedRuntimeFence: null,
payloadFingerprint: ''
}
})
expect(await current.host.attach(QUEUED_RIG_CALLER, attach)).toMatchObject({ ok: true })
const { db, runtime } = h.setup(false)
vi.spyOn(runtime, 'ensureStructuredAgentSessionHost').mockResolvedValue()
vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {})
type WorkspaceLookup = {
showManagedTerminalWorkspace: (
...args: Parameters<typeof runtime.showManagedTerminalWorkspace>
) => Promise<Awaited<ReturnType<typeof placement.placeWorkerAgent>>['worktree']>
}
vi.spyOn<WorkspaceLookup, 'showManagedTerminalWorkspace'>(
runtime,
'showManagedTerminalWorkspace'
).mockResolvedValue({
id: 'workspace-1',
repoId: 'repo'
})
vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport').mockResolvedValue({
supported: true
})
vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockReturnValue({
runtimeId: runtime.getRuntimeId(),
terminalHandle: 'term_worker',
ptyId: 'pty',
worktreeId: 'workspace-1',
paneKey: 'tab:11111111-1111-4111-8111-111111111111',
processIncarnation: 'fixture:1',
launchTokenHash: null,
hostScope: { kind: 'local', hostId: 'local' }
})
// Placement/provider are external fixtures; admission, constructors, host, journal, queue and settlement run in production.
vi.spyOn(placement, 'placeWorkerAgent').mockImplementation(async (args) => ({
mode: args.mode,
worktree: { id: 'workspace-1', repoId: 'repo' },
terminalHandle: 'term_worker',
structuredSession: {
host: current.host,
identity: {
sessionId: WORKER,
handle: 'term_worker',
agent: args.agent ?? null,
paneKey: 'tab:11111111-1111-4111-8111-111111111111',
processIncarnation: 'fixture:1',
worktreeId: 'workspace-1',
hostScope: { kind: 'local', hostId: 'local' }
},
fence: current.store.requireWritableRecord(WORKER).lease.runtimeFence
},
setupReceipt: placement.EXISTING_WORKTREE_SETUP
}))
vi.spyOn(topology, 'monitorWorkerSetup').mockImplementation(() => {})
const run = db.createRun({
objective: 'cards',
coordinatorHandle: null,
coordinatorPaneKey: null,
coordinatorOrcaSessionId: COORD
})
vi.stubEnv('ORCA_AGENT_SESSION_ID', COORD)
vi.stubEnv('ORCA_TERMINAL_HANDLE', '')
vi.stubEnv('ORCA_PAIRING_CODE', '')
vi.stubEnv('ORCA_ENVIRONMENT', '')
const requests: string[] = []
vi.doMock('../../../../../cli/runtime-client', async (original) => ({
...(await original<Record<string, unknown>>()),
RuntimeClient: function () {
return {
isRemote: false,
async call(method: string, params: Record<string, unknown> = {}) {
const admitted = await resolveOrchestrationSessionCaller(
runtime,
{
id: 'cli',
authToken: 'test-token',
method,
params,
orchestrationCompatibilityEvidence: { agentSessionId: COORD }
},
undefined
)
requests.push(method)
const rpc = h.findMethod(method)
const parsed = rpc.params?.parse(admitted.request.params)
return {
result: await rpc.handler(parsed, {
runtime,
orchestrationCaller: admitted.caller,
orchestrationCompatibilityEvidence:
admitted.request.orchestrationCompatibilityEvidence
})
}
}
}
}
}))
const output = vi.fn()
vi.doMock('../../../../../cli/format', () => ({ printResult: output }))
const cliPath = '../../../../../cli/index'
const { main }: { main(argv: string[]): Promise<void> } = await import(cliPath)
const task = ' Current task 🐋\r\n\r\n=== TASK ===\nDo not clip the task. '
let argv: string[]
let openingSend: string | undefined
if (command === 'dispatch') {
openingSend = await current.workingSend()
const created = db.createTask({ runId: run.id, spec: task })
argv = [
'orchestration',
'dispatch',
'--task',
created.id,
'--to',
`orca_session_id:${WORKER}`,
'--inject',
'--return-preamble',
'--json'
]
} else {
argv = [
'orchestration',
'worker-start',
'--spec',
task,
'--agent',
'codex',
'--timeout-ms',
'1000',
'--json'
]
}
const running = main(argv)
if (command === 'worker-start') {
await eventually(() => expect(current.dispatch).toHaveBeenCalled())
const id = current.dispatch.mock.calls[0][0].clientMessageId
await current.settleAccepted(id, 'task-turn')
}
await running
expect(requests).toContain(
command === 'dispatch' ? 'orchestration.dispatch' : 'orchestration.workerStart'
)
const snapshot = await current.host.journalSnapshot(WORKER)
const history = await current.host.history({ sessionId: WORKER, direction: 'tail' })
if (!history.ok) {
throw new Error('cannot reload real history')
}
const queued = history.page.queuedMessages?.[0]
if (command === 'dispatch') {
expect(queued, 'busy --inject must enter the queue').toBeDefined()
if (!queued) {
throw new Error('busy dispatch bypassed queue')
}
} else {
expect(queued, 'worker-start sends immediately').toBeUndefined()
}
const body =
queued?.body ??
snapshot.items.find((item) => item.body.kind === 'message' && item.body.from)?.body
if (!body || body.kind !== 'message') {
throw new Error('production host did not persist task')
}
const from = readAgentMessageSource(JSON.parse(JSON.stringify(body.from)))
expect(from?.senders[0].party.orcaSessionId).toBe(COORD)
expect(from?.senders[0].relation).toBe('coordinator')
expect(agentMessageDisplayText(body.blocks, from)).toBe(task)
const raw = nativeChatMessageCopyText(body)
expect(raw).toContain('You are working inside Orca')
expect(raw.endsWith(task)).toBe(true)
const items = [{ itemId: 'task', sequence: 1, revision: 1, observedAt: 1, body }]
expect(latestStructuredAgentSessionPrompt(items)).toBe(raw)
const prompt = projectStructuredAgentSessionStatusSummary(items).latestPrompt
expect(isOrcaDispatchPrompt(prompt)).toBe(true)
expect(prompt).toContain('=== TASK === Current task')
if (from?.orchestration?.message !== 'task') {
throw new Error('task identity missing')
}
expect(prompt).toContain(`Your task ID is: ${from.orchestration.taskId}`)
if (queued) {
const [card] = projectQueuedMessageCards([queued], [], {})
expect(queuedMessageCardLine(card)).toBe(task)
expect(card.text).toBe(raw) // Exactly what the editor opens.
const journal = current.host.collaboratorsForTests().sessions.get(WORKER)!.journal
const edited = `${raw.slice(0, raw.length - task.length)}Edited task`
const fields = {
messageId: queued.messageId,
expectedBodyFingerprint: journal.queuedMessages.get(queued.messageId)!.fingerprint,
text: edited
}
expect(
await current.host.queuedMessageUpdate(QUEUED_RIG_CALLER, {
envelope: current.envelope(
fields,
'agentSession.queuedMessageUpdate',
hostTestOperationId()
),
...fields
})
).toMatchObject({ ok: true })
const page = await current.host.history({ sessionId: WORKER, direction: 'tail' })
if (!page.ok) {
throw new Error('cannot reopen queue')
}
const reopened = page.page.queuedMessages!.find((row) => row.messageId === queued.messageId)!
expect(nativeChatMessageCopyText(reopened.body)).toBe(edited)
expect(reopened.body).not.toHaveProperty('from')
if (!openingSend) {
throw new Error('busy dispatch has no opening send')
}
expect(await current.sendNow(queued.messageId)).toMatchObject({ ok: true })
await current.settleAccepted(openingSend, 'opening-task-turn')
await eventually(async () => {
const handoff = await current.handoffId(queued.messageId)
const delivered = current.dispatch.mock.calls.find(
([args]) => args.clientMessageId === handoff
)
expect(delivered).toBeDefined()
expect(nativeChatMessageCopyText(delivered![0].body)).toBe(edited)
})
} else {
expect(nativeChatMessageCopyText(current.dispatch.mock.calls[0][0].body)).toBe(raw)
}
}
)
@@ -0,0 +1,138 @@
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import { defineFixture } from '../../component-fixtures/component-fixture'
import { makeTab } from '@/store/slices/store-test-helpers'
import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source'
import { NativeChatAgentMessageCard } from './NativeChatAgentMessageCard'
import { NativeChatUserMessageRow } from './NativeChatUserMessageRow'
const workspace = 'agent-message-fixture'
const image =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAAA8CAIAAADXHaAKAAAAvklEQVR4nO3SUQkAIBTAwBfUAAawP5YQhHFwAfaxWftAxnwvgIcMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSbm/mJH95bvMnAAAAABJRU5ErkJggg=='
const prose =
'First paragraph with **real Markdown**, a [web link](https://example.com) and inline `code`.\n\nSecond paragraph keeps its own spacing. This longer line wraps differently in the narrow frame, so Show more is based on rendered lines.\n\n![Example image](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAAA8CAIAAADXHaAKAAAAvklEQVR4nO3SUQkAIBTAwBfUAAawP5YQhHFwAfaxWftAxnwvgIcMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSTE0KYYmxdCkGJoUQ5NiaFIMTYqhSbm/mJH95bvMnAAAAABJRU5ErkJggg==)\n\nLast paragraph after the image.'
function cards() {
const human: NativeChatMessage = {
id: 'human',
role: 'user',
source: 'transcript',
blocks: [{ type: 'text', text: 'An ordinary human message.' }],
timestamp: 1_800_000_000_000
}
return (
<div className="space-y-4 p-4">
{(['task', 'coordinator', 'worker', 'generic'] as const).map((kind, i) => {
const from: AgentMessageSource = {
kind: 'agent',
senders: (kind === 'generic'
? ['A sender on another host with a long name', null, 'Parser worker', 'Fourth sender']
: ['Parser worker']
).map((name, index) => {
const address =
kind === 'generic' && index === 0
? 'dispatch:missing'
: kind === 'task'
? 'term_progress'
: `term_sender${index || ''}`
return {
party: {
address,
terminalHandle: kind === 'generic' && index === 0 ? null : address,
orcaSessionId: null
},
name,
...(kind === 'worker' || kind === 'coordinator' ? { relation: kind } : {})
}
}),
orchestration:
kind === 'task'
? { message: 'task', runId: 'r', taskId: 't', dispatchId: 'd' }
: {
message: 'mail-notice',
mailbox: 'term_recipient',
dispatchId: null,
messages: []
}
}
return (
<NativeChatAgentMessageCard
key={kind}
message={{
id: kind,
role: 'user',
source: 'transcript',
blocks: [
{ type: 'text', text: i === 0 ? 'A short task fits without a disclosure.' : prose },
...(kind === 'generic'
? [{ type: 'image-ref' as const, url: image, alt: 'Attached diagram' }]
: [])
],
timestamp: 1_800_000_000_000,
from
}}
from={from}
runtimeContext={{
worktreeId: workspace,
worktreePath: null,
target: { kind: 'local' }
}}
allowFileUriLinks={false}
onRefolded={() => {}}
/>
)
})}
<NativeChatUserMessageRow
message={human}
markdown="An ordinary human message."
prose={human.blocks}
rowRef={{ current: null }}
allowFileUriLinks={false}
onRefolded={() => {}}
/>
</div>
)
}
const agentStatusByPaneKey: Record<string, AgentStatusEntry> = {}
for (const [id, agentType, state] of [
['sender', 'codex', 'done'],
['progress', 'claude', 'working']
] as const) {
const paneKey = `${id}:11111111-1111-4111-8111-111111111111`
agentStatusByPaneKey[paneKey] = {
paneKey,
worktreeId: workspace,
terminalHandle: `term_${id}`,
agentType,
state,
prompt: 'Parser worker',
updatedAt: 1_800_000_000_000,
stateStartedAt: 1_800_000_000_000,
stateHistory: [],
...(state === 'working'
? { programStatus: { state, progress: 40, observedAt: 1_800_000_000_000 } }
: {})
}
}
const store = {
activeWorktreeId: workspace,
activeWorkspaceExecutionHostId: 'local' as const,
tabsByWorktree: {
[workspace]: ['sender', 'progress'].map((id) => makeTab({ id, worktreeId: workspace }))
},
agentStatusByPaneKey
}
export const wide = defineFixture({ width: 720, store, render: cards })
export const narrow = defineFixture({ width: 340, store, render: cards })
export const expanded = defineFixture({
width: 720,
store,
render: cards,
interact: (frame) => {
for (const button of frame.querySelectorAll('button')) {
if (button.textContent === 'Show more') {
button.click()
}
}
}
})
@@ -0,0 +1,614 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { act, cleanup, fireEvent, renderHook, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { render } from './native-chat-tooltip-test-harness'
import { useAppStore } from '@/store'
import { makeTab, makeWorktree, TEST_REPO } from '@/store/slices/store-test-helpers'
import { readStoreListenerCount } from '@/store/store-listener-census'
import { getKnownExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { resolveStructuredAgentSessionOwner } from '@/runtime/structured-agent-session-owner'
import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types'
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
import type { AgentMessageSenderView } from '../../../../shared/agent-session/agent-message-presentation'
import { runtimeEnvironmentStatusFromSnapshot } from '../../../../shared/runtime-host-status'
import * as presentation from '@/lib/program-status/agent-row-presentation'
import * as cache from '@/components/dashboard/worktree-agent-rows-cache'
import * as selectors from '@/components/sidebar/worktree-agent-row-selectors'
import { useAgentMessageSenderView } from './native-chat-agent-message-sender-view'
import { useAgentMessageSenderSource } from './native-chat-agent-message-sender-source'
import { AgentStateDot } from '@/components/AgentStateDot'
import { AGENT_SESSION_WRITE_NOTICE_COPY } from '../../../../shared/agent-session-write-notice-copy'
import * as markdown from './NativeChatMarkdown'
import * as display from '../../../../shared/agent-session/agent-message-display-text'
import type * as SonnerModule from 'sonner'
import type * as RuntimeRpc from '@/runtime/runtime-rpc-client'
import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source'
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import { buildNativeChatTranscriptSlots } from './native-chat-transcript-slots'
import { nativeChatSlotCopyText } from './native-chat-message-copy-text'
import { NativeChatAgentMessageCard } from './NativeChatAgentMessageCard'
const navigation = vi.hoisted(() => ({ rpc: vi.fn(), toast: vi.fn() }))
vi.mock('@/runtime/runtime-rpc-client', async (original) => ({
...(await original<typeof RuntimeRpc>()),
callRuntimeRpc: navigation.rpc
}))
vi.mock('sonner', async (importOriginal) => {
const original = await importOriginal<typeof SonnerModule>()
return {
...original,
toast: Object.assign(
function (this: unknown, ...args: Parameters<typeof original.toast>) {
return original.toast.apply(this, args)
},
original.toast,
{ error: navigation.toast }
)
}
})
const initialStore = useAppStore.getState()
const LEAF = '11111111-1111-4111-8111-111111111111'
const renders = new Map<number, number>()
const views = new Map<number, AgentMessageSenderView>()
function status(i: number, worktreeId = 'workspace'): AgentStatusEntry {
return {
paneKey: `tab-${i}:${LEAF}`,
worktreeId,
terminalHandle: `term_${i}`,
agentType: 'claude',
state: 'working',
prompt: 'work',
updatedAt: 2_000_000,
stateStartedAt: 2_000_000,
stateHistory: []
}
}
function Probe({
id,
workspace = 'workspace',
remote = false
}: {
id: number
workspace?: string
remote?: boolean
}) {
const value = useAgentMessageSenderView(
{
party: { address: `term_${id}`, terminalHandle: `term_${id}`, orcaSessionId: null },
name: 'Sender'
},
workspace,
remote ? { kind: 'environment', environmentId: 'remote' } : { kind: 'local' }
)
views.set(id, value)
renders.set(id, (renders.get(id) ?? 0) + 1)
return null
}
function write(i: number, update: Partial<AgentStatusEntry>) {
const state = useAppStore.getState()
const key = status(i).paneKey
act(() =>
useAppStore.setState({
agentStatusByPaneKey: {
...state.agentStatusByPaneKey,
[key]: { ...state.agentStatusByPaneKey[key], ...update }
},
agentStatusEpoch: state.agentStatusEpoch + 1
})
)
}
let height = 48
let resize: () => void
let disconnects = 0
beforeEach(() => {
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(2_000_000)
renders.clear()
views.clear()
useAppStore.setState(
{
...initialStore,
activeWorktreeId: 'workspace',
activeWorkspaceExecutionHostId: 'local',
tabsByWorktree: { workspace: [makeTab({ id: 'tab-0', worktreeId: 'workspace' })] },
unifiedTabsByWorktree: {},
agentStatusByPaneKey: { [status(0).paneKey]: status(0) },
retainedAgentsByPaneKey: {},
migrationUnsupportedByPtyId: {},
runtimeAgentOrchestrationByPaneKey: {},
runtimePaneTitlesByTabId: {},
terminalLayoutsByTabId: {},
ptyIdsByTabId: {},
paneForegroundAgentByPaneKey: {},
agentStatusEpoch: 1
},
true
)
navigation.rpc.mockReset()
navigation.toast.mockReset()
height = 48
disconnects = 0
vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => {
return height
})
const computedStyle = getComputedStyle
vi.stubGlobal('getComputedStyle', (element: Element) => {
const style = computedStyle(element)
return new Proxy(style, {
get(target, key) {
if (key === 'lineHeight') {
return '24px'
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the style facade delegates native properties to the real style receiver, including binding its methods below.
const value = target[key as keyof CSSStyleDeclaration]
return typeof value === 'function' ? value.bind(target) : value
}
})
})
vi.stubGlobal(
'ResizeObserver',
class {
constructor(callback: () => void) {
resize = callback
}
observe() {}
disconnect() {
disconnects++
}
}
)
})
afterEach(() => {
cleanup()
useAppStore.setState(initialStore, true)
vi.restoreAllMocks()
vi.unstubAllGlobals()
vi.useRealTimers()
})
const from: AgentMessageSource = {
kind: 'agent',
orchestration: null,
senders: ['Build cards', 'Other sender'].map((name, i) => ({
name,
party: { address: `term_${i}`, terminalHandle: `term_${i}`, orcaSessionId: null }
}))
}
const context = { worktreeId: 'workspace', worktreePath: null, target: { kind: 'local' as const } }
function card(message: NativeChatMessage) {
return (
<NativeChatAgentMessageCard
message={message}
from={message.from!}
runtimeContext={context}
allowFileUriLinks={false}
onRefolded={() => {}}
/>
)
}
function slots(message: NativeChatMessage) {
return buildNativeChatTranscriptSlots({
messages: [message],
turnKeys: [undefined],
liveTurnKey: undefined,
receipts: new Map(),
turnStatuses: { active: null, completedByTurn: {} },
turnDiffs: new Map(),
expandedTurnKeys: new Set(),
isWorking: false,
lifecycleWorking: false
})
}
it.each(['task', 'mail'] as const)(
'keeps native %s progress independent from Markdown and reparses own edits',
async (kind) => {
const paneKey = status(0).paneKey
const entry = {
...status(0),
programStatus: { state: 'working' as const, progress: 40, observedAt: 2_000_000 }
}
useAppStore.setState({ agentStatusByPaneKey: { [paneKey]: entry } })
const prefix =
kind === 'task' ? 'You are working inside Orca, a multi-agent IDE.\n=== TASK ===\n' : ''
const message: NativeChatMessage = {
id: `progress-${kind}`,
role: 'user',
source: 'transcript',
timestamp: 1,
from: {
...from,
orchestration:
kind === 'task'
? { message: 'task', runId: 'r', taskId: 't', dispatchId: 'd' }
: { message: 'mail-notice', mailbox: 'term_0', dispatchId: null, messages: [] }
},
blocks: [
{
type: 'text',
text: `${prefix}Current **task** with [link](https://example.com) and ![image](https://example.com/image.png).\n\nSecond paragraph with [another link](https://example.com/second).`
}
]
}
const convert = vi.spyOn(display, 'agentMessageTranscriptText')
const renderMarkdown = vi.spyOn(markdown, 'NativeChatMarkdown')
let mounted = render(card(message))
expect(
screen.getByText(kind === 'task' ? 'Task from your coordinator' : 'Message from')
).toBeInTheDocument()
expect(screen.getByText('task', { selector: 'strong' })).toBeInTheDocument()
expect(convert).toHaveBeenCalled()
expect(renderMarkdown).toHaveBeenCalled()
expect(mounted.container.querySelectorAll('p')).toHaveLength(2)
expect(screen.getByRole('link', { name: 'link' })).toHaveAttribute(
'href',
'https://example.com'
)
expect(screen.getByRole('img', { name: 'image' })).toHaveAttribute(
'src',
'https://example.com/image.png'
)
const text =
'Current **task** with [link](https://example.com) and ![image](https://example.com/image.png).\n\nSecond paragraph with [another link](https://example.com/second).'
const writeClipboardText = vi.fn(async () => {})
vi.stubGlobal('api', { ui: { writeClipboardText } })
navigation.rpc.mockRejectedValue(new Error('unreachable'))
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Build cards' })))
expect(navigation.rpc).toHaveBeenCalledWith({ kind: 'local' }, 'orchestration.partyLocation', {
address: 'term_0'
})
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Other sender' })))
expect(navigation.rpc).toHaveBeenLastCalledWith(
{ kind: 'local' },
'orchestration.partyLocation',
{ address: 'term_1' }
)
expect(navigation.toast).toHaveBeenCalledWith(
`${AGENT_SESSION_WRITE_NOTICE_COPY.unreachable} ${AGENT_SESSION_WRITE_NOTICE_COPY.tryAgain}`
)
expect(screen.queryByRole('button', { name: 'Show more' })).toBeNull()
act(() => {
height = 120
resize()
})
const more = screen.getByRole('button', { name: 'Show more' })
expect(more).toHaveAttribute('aria-expanded', 'false')
const clip = document.getElementById(more.getAttribute('aria-controls')!)!
const first = screen.getByRole('link', { name: 'link' })
const second = screen.getByRole('link', { name: 'another link' })
const documentFocus = vi.spyOn(document, 'hasFocus').mockReturnValue(true)
first.focus()
clip.scrollTop = 74
second.focus()
expect(second).toHaveFocus()
expect(clip.scrollTop).toBe(74)
documentFocus.mockReturnValue(false)
second.blur()
expect(clip.scrollTop).toBe(74)
documentFocus.mockReturnValue(true)
second.focus()
more.focus()
expect(more).toHaveFocus()
expect(clip.scrollTop).toBe(0)
expect(more).toHaveAttribute('aria-expanded', 'false')
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Copy message' })))
expect(writeClipboardText).toHaveBeenCalledWith(text)
expect(nativeChatSlotCopyText(slots(message), message.id)).toBe(text)
fireEvent.click(screen.getByRole('button', { name: 'Show more' }))
expect(screen.getByRole('button', { name: 'Show less' })).toHaveAttribute(
'aria-expanded',
'true'
)
fireEvent.click(screen.getByRole('button', { name: 'Show less' }))
act(() => {
height = 48
resize()
})
expect(screen.queryByRole('button', { name: 'Show more' })).toBeNull()
for (const remount of [false, true]) {
if (remount) {
mounted.unmount()
mounted = render(card(message))
}
const ring = screen.getByRole('progressbar', { name: 'Working · 40%' })
expect(ring).toHaveAttribute('aria-valuenow', '40')
expect(ring.querySelector('circle[pathLength="100"]')).toHaveAttribute(
'stroke-dasharray',
'40 100'
)
}
for (const spy of [convert, renderMarkdown]) {
spy.mockClear()
}
for (const progress of [60, null, 60, 0, 100]) {
act(() =>
useAppStore
.getState()
.setAgentProgramStatus(
paneKey,
progress === null ? null : { state: 'working', progress, observedAt: 2_000_000 }
)
)
if (progress === null) {
expect(screen.queryByRole('progressbar')).toBeNull()
expect(screen.getByLabelText('Working')).toBeInTheDocument()
} else {
const ring = screen.getByRole('progressbar', { name: `Working · ${progress}%` })
expect(ring).toHaveAttribute('aria-valuenow', String(progress))
expect(ring.querySelector('circle[pathLength="100"]')).toHaveAttribute(
'stroke-dasharray',
`${progress} 100`
)
expect(screen.queryByLabelText('Done')).toBeNull()
}
for (const spy of [convert, renderMarkdown]) {
expect(spy).not.toHaveBeenCalled()
}
}
act(() =>
useAppStore.setState({
agentStatusByPaneKey: { [paneKey]: { ...entry, state: 'done' } },
agentStatusEpoch: 2
})
)
expect(screen.getByLabelText('Done')).toBeInTheDocument()
expect(screen.queryByRole('progressbar')).toBeNull()
for (const spy of [convert, renderMarkdown]) {
expect(spy).not.toHaveBeenCalled()
}
const edited = { ...message, blocks: [{ type: 'text' as const, text: `${prefix}Edited task` }] }
mounted.rerender(card(edited))
expect(screen.getByText('Edited task')).toBeInTheDocument()
expect(convert).toHaveBeenCalled()
expect(renderMarkdown).toHaveBeenCalled()
const copyButton = screen.getByRole('button', { name: 'Copy message' })
await act(async () => fireEvent.click(copyButton))
expect(copyButton).toHaveAccessibleName('Copied')
expect(writeClipboardText).toHaveBeenCalledWith('Edited task')
expect(nativeChatSlotCopyText(slots(edited), edited.id)).toBe('Edited task')
const multiple: NativeChatMessage = {
...edited,
blocks: [...edited.blocks, { type: 'text', text: 'Another paragraph' }]
}
mounted.rerender(card(multiple))
await act(async () => fireEvent.click(copyButton))
expect(writeClipboardText).toHaveBeenCalledWith(`${prefix}Edited task\n\nAnother paragraph`)
expect(nativeChatSlotCopyText(slots(multiple), multiple.id)).toBe(
`${prefix}Edited task\n\nAnother paragraph`
)
mounted.unmount()
expect(disconnects).toBeGreaterThan(0)
}
)
it.each([100, 1000])(
'shares actual row acquisition and isolates selected progress across %i cards',
(n) => {
let visits = 0
const live = new Proxy(
Object.fromEntries(
Array.from({ length: n }, (_, i) => {
const entry = status(i)
return [entry.paneKey, entry]
})
),
{
ownKeys(target) {
const keys = Object.getOwnPropertyNames(target)
visits += keys.length
return keys
}
}
)
useAppStore.setState({
agentStatusByPaneKey: live,
tabsByWorktree: {
workspace: Array.from({ length: n }, (_, i) =>
makeTab({ id: `tab-${i}`, worktreeId: 'workspace' })
)
},
terminalLayoutsByTabId: Object.fromEntries(
Array.from({ length: n }, (_, i): [string, TerminalLayoutSnapshot] => [
`tab-${i}`,
{ root: { type: 'leaf', leafId: LEAF }, activeLeafId: LEAF, expandedLeafId: null }
])
)
})
const normalize = vi.spyOn(presentation, 'resolveRowProgramStatus')
const mounted = render(
<>
{Array.from({ length: n }, (_, id) => (
<Probe key={id} id={id} />
))}
</>
)
// Handle lookup, visible-authority acquisition and sidebar indexing each enumerate the N live entries.
expect(visits).toBe(n * 3)
expect(normalize).toHaveBeenCalledTimes(n)
for (let i = 0; i < n; i++) {
expect(views.get(i)?.dot).toBe('working')
}
const before = new Map(renders)
visits = 0
normalize.mockClear()
act(() => useAppStore.setState({ sidebarBody: 'agents' }))
expect(visits).toBe(0)
expect(normalize).not.toHaveBeenCalled()
expect(renders).toEqual(before)
for (const progress of [40, 60]) {
normalize.mockClear()
const epoch = useAppStore.getState().agentStatusEpoch
act(() =>
useAppStore.getState().setAgentProgramStatus(status(0).paneKey, {
state: 'working',
progress,
observedAt: 2_000_000
})
)
expect(useAppStore.getState().agentStatusEpoch).toBe(epoch)
expect(normalize).toHaveBeenCalledTimes(n)
expect(views.get(0)?.progress).toBe(progress)
expect(renders.get(0)).toBe(before.get(0)! + (progress === 40 ? 1 : 2))
for (let i = 1; i < n; i++) {
expect(renders.get(i)).toBe(before.get(i))
}
}
mounted.unmount()
}
)
it('keeps other hosts isolated, retains completed senders, and releases disconnected subscriptions', () => {
const create = vi.spyOn(cache, 'createWorktreeAgentRowsCache')
const baseline = readStoreListenerCount()
expect(baseline).not.toBeNull()
const other = status(9, 'remote')
useAppStore.setState({
repos: [TEST_REPO],
worktreesByRepo: {
[TEST_REPO.id]: [
makeWorktree({ id: 'remote', repoId: TEST_REPO.id, hostId: 'runtime:remote' })
]
},
tabsByWorktree: {
...useAppStore.getState().tabsByWorktree,
remote: [makeTab({ id: 'tab-9', worktreeId: 'remote' })]
},
agentStatusByPaneKey: { ...useAppStore.getState().agentStatusByPaneKey, [other.paneKey]: other }
})
const mounted = render(
<>
<Probe id={0} />
<Probe id={9} workspace="remote" remote />
</>
)
expect(readStoreListenerCount()).toBeGreaterThan(baseline!)
const leased = create.mock.results.flatMap((result) =>
result.type === 'return' && result.value.byWorktree.size ? [result.value] : []
)
expect(leased.flatMap((value) => [...value.byWorktree.keys()]).sort()).toEqual([
'remote',
'workspace'
])
const before = new Map(renders)
write(9, { state: 'waiting' })
expect(views.get(9)?.dot).toBe('waiting')
expect(renders.get(9)).toBe(before.get(9)! + 1)
expect(renders.get(0)).toBe(before.get(0))
write(0, {
agentType: 'codex',
programStatus: { state: 'blocked', progress: 60, observedAt: 2_000_000 }
})
expect(views.get(0)).toEqual({ agentType: 'codex', dot: 'permission', progress: 60 })
expect(renders.get(9)).toBe(before.get(9)! + 1)
for (const value of leased) {
expect(value.lastComputedWorktreeIds.length).toBeLessThanOrEqual(1)
}
const entry = { ...status(0), state: 'done' as const }
act(() =>
useAppStore.setState({
agentStatusByPaneKey: { [other.paneKey]: other },
retainedAgentsByPaneKey: {
[entry.paneKey]: {
entry,
tab: makeTab({ id: 'tab-0', worktreeId: 'workspace' }),
worktreeId: 'workspace',
agentType: 'claude',
startedAt: 1
}
},
runtimeStatusByEnvironmentId: new Map([
[
'remote',
runtimeEnvironmentStatusFromSnapshot({
environmentId: 'remote',
pairingRevision: 1,
sequence: 1,
checkedAt: 1,
verification: 'blocked',
transport: 'disconnected',
retired: true,
status: null
})
]
])
})
)
expect(views.get(0)?.dot).toBe('done')
act(() => useAppStore.setState({ activeWorkspaceExecutionHostId: 'runtime:remote' }))
expect(views.get(0)).toEqual({})
act(() => useAppStore.setState({ activeWorkspaceExecutionHostId: 'local' }))
expect(views.get(0)?.dot).toBe('done')
const disconnected = new Map(renders)
const constructions = create.mock.calls.length
act(() => useAppStore.setState({ sidebarBody: 'agents' }))
expect(renders).toEqual(disconnected)
expect(create).toHaveBeenCalledTimes(constructions)
mounted.unmount()
for (const value of leased) {
expect(value.byWorktree.size).toBe(0)
}
expect(readStoreListenerCount()).toBe(baseline)
render(<Probe id={0} />)
expect(views.get(0)?.dot).toBe('done')
})
it('uses the child row own glyph even when its parent program needs permission', () => {
write(0, {
programStatus: { state: 'blocked', progress: 60, observedAt: 2_000_000 },
subagents: [{ id: 'child', state: 'working', startedAt: 2_000_000, description: 'Child' }]
})
const state = useAppStore.getState()
const projected = cache.selectWorktreeAgentRowsCached({
state,
worktreeId: 'workspace',
now: 2_000_000,
generation: '',
orchestration: selectors.selectRuntimeAgentOrchestrationForWorktree(state, 'workspace')
})
const child = projected.find((row) => row.childRow)!
expect(child).toBeDefined()
const locate = () => ({ host: 'local', worktreeId: 'workspace', paneKey: child.paneKey })
let value: AgentMessageSenderView | undefined
function Child() {
value = useAgentMessageSenderSource(locate)
return value.dot ? (
<AgentStateDot state={value.dot} progress={value.progress} size="md" title={null} />
) : null
}
render(<Child />)
expect(value?.dot).toBe('working')
expect(value?.progress).toBeUndefined()
expect(screen.queryByRole('progressbar')).toBeNull()
expect(screen.getByLabelText('Working')).toBeDefined()
})
it('refuses a target-less ambiguous workspace twin while an explicit local target resolves', () => {
const workspace = `${TEST_REPO.id}::/w`
const entry = status(0, workspace)
useAppStore.setState({
repos: [TEST_REPO],
activeWorktreeId: 'different',
activeWorkspaceExecutionHostId: 'local',
detectedWorktreesByRepo: {},
worktreesByRepo: {
[TEST_REPO.id]: [
makeWorktree({ id: workspace, repoId: TEST_REPO.id, hostId: 'local' }),
makeWorktree({ id: workspace, repoId: TEST_REPO.id, hostId: 'runtime:e' })
]
},
tabsByWorktree: { [workspace]: [makeTab({ id: 'tab-0', worktreeId: workspace })] },
agentStatusByPaneKey: { [entry.paneKey]: entry }
})
const state = useAppStore.getState()
expect(getKnownExecutionHostIdForWorktree(state, workspace)).toBe('local')
expect(resolveStructuredAgentSessionOwner(state, workspace)).toBeNull()
const view = renderHook(() => useAgentMessageSenderView(from.senders[0], workspace))
expect(view.result.current).toEqual({})
view.unmount()
const explicit = renderHook(() =>
useAgentMessageSenderView(from.senders[0], workspace, { kind: 'local' })
)
expect(explicit.result.current.dot).toBe('working')
})
@@ -0,0 +1,189 @@
import { useId, useLayoutEffect, useRef, useState } from 'react'
import { AgentStateDot } from '@/components/AgentStateDot'
import { AgentTypeIcon } from '@/lib/agent-type-icon'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source'
import {
agentMessageHeading,
agentMessagePrimarySender,
agentMessageOverflows,
agentMessageFoldLabel,
agentMessageCanCopy,
AGENT_MESSAGE_FOLDED_LINES
} from '../../../../shared/agent-session/agent-message-presentation'
import { agentMessageTranscriptText } from '../../../../shared/agent-session/agent-message-display-text'
import { NativeChatAgentMessageSenders } from './NativeChatAgentMessageSenders'
import { useAgentMessageSenderView } from './native-chat-agent-message-sender-view'
import { useNativeChatVisualOwner } from './native-chat-visual-owner'
import { useNativeChatDisclosure } from './native-chat-disclosure-store'
import { NativeChatMarkdown } from './NativeChatMarkdown'
import { NativeChatCodeBlock } from './NativeChatCodeBlock'
import { NativeChatCopyButton } from './NativeChatCopyButton'
import { NativeChatMessageTimestamp } from './NativeChatMessageTimestamp'
import { NativeChatImageAttachments } from './NativeChatTranscriptChrome'
function heading(from: AgentMessageSource): string {
switch (agentMessageHeading(from)) {
case 'task':
return translate(
'components.native-chat.agentMessage.taskFromCoordinator',
'Task from your coordinator'
)
case 'coordinator':
return translate(
'components.native-chat.agentMessage.messageFromCoordinator',
'Message from your coordinator'
)
case 'worker':
return translate(
'components.native-chat.agentMessage.updateFromWorker',
'Update from your worker'
)
case 'generic':
return translate('components.native-chat.agentMessage.messageFrom', 'Message from')
}
}
export function NativeChatAgentMessageCard({
message,
from,
runtimeContext,
onLinkClick,
allowFileUriLinks,
onRefolded
}: {
message: NativeChatMessage
from: AgentMessageSource
runtimeContext?: RuntimeFileOperationArgs | null
onLinkClick?: CommentMarkdownLinkClickHandler
allowFileUriLinks: boolean
onRefolded: () => void
}): React.JSX.Element {
const owner = useNativeChatVisualOwner()
const worktreeId = owner?.worktreeId ?? runtimeContext?.worktreeId ?? null
const target = owner?.target ?? runtimeContext?.target
const text = agentMessageTranscriptText(message.blocks, from)
const disclosure = useNativeChatDisclosure(`agent-message:${message.id}`, false)
const clip = useRef<HTMLDivElement>(null)
const clipId = useId()
const [overflows, setOverflows] = useState(false)
useLayoutEffect(() => {
const element = clip.current
if (!element) {
return
}
const measure = () => {
const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight)
setOverflows(agentMessageOverflows(element.scrollHeight, lineHeight))
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(element)
return () => observer.disconnect()
}, [text])
return (
<div className="group flex w-full gap-3 rounded-xl border border-border bg-card p-3 text-sm text-card-foreground">
<AgentMessageAvatar
sender={agentMessagePrimarySender(from)}
worktreeId={worktreeId}
target={target}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{heading(from)}</span>
<NativeChatMessageTimestamp
timestamp={message.timestamp}
className="text-muted-foreground"
/>
</div>
<NativeChatAgentMessageSenders from={from} chatWorktreeId={worktreeId} target={target} />
<NativeChatImageAttachments
blocks={message.blocks}
runtimeContext={runtimeContext}
enablePreview={runtimeContext !== undefined}
/>
<div
ref={clip}
id={clipId}
className={cn(
'overflow-hidden leading-6',
overflows && !disclosure.open && 'mask-b-from-[calc(100%-1.75rem)]'
)}
style={disclosure.open ? undefined : { maxHeight: `${AGENT_MESSAGE_FOLDED_LINES}lh` }}
onBlur={(event) => {
if (document.hasFocus() && !event.currentTarget.contains(event.relatedTarget)) {
event.currentTarget.scrollTop = 0
}
}}
>
<NativeChatMarkdown
rowKey={message.id}
content={text}
variant="document"
className="text-sm"
renderCodeBlock={NativeChatCodeBlock}
onLinkClick={onLinkClick}
allowFileUriLinks={allowFileUriLinks}
/>
</div>
<div className="flex items-center justify-between">
{overflows ? (
<Button
type="button"
variant="ghost"
size="xs"
aria-expanded={disclosure.open}
aria-controls={clipId}
onClick={() => {
disclosure.setOpen(!disclosure.open)
if (disclosure.open) {
requestAnimationFrame(onRefolded)
}
}}
>
{agentMessageFoldLabel(disclosure.open) === 'expanded'
? translate('components.native-chat.userMessage.showLess', 'Show less')
: translate('components.native-chat.agentMessage.showMore', 'Show more')}
</Button>
) : (
<span />
)}
{agentMessageCanCopy(text) ? (
<NativeChatCopyButton
text={text}
className="transition-opacity can-hover:pointer-events-none can-hover:opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 [.group:has(:focus-visible)_&]:pointer-events-auto [.group:has(:focus-visible)_&]:opacity-100"
/>
) : null}
</div>
</div>
</div>
)
}
function AgentMessageAvatar({
sender,
worktreeId,
target
}: {
sender: AgentMessageSource['senders'][number] | undefined
worktreeId: string | null
target?: RuntimeClientTarget
}): React.JSX.Element {
const view = useAgentMessageSenderView(sender, worktreeId, target)
return (
<div className="relative mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-muted">
<AgentTypeIcon agentType={view.agentType} size={16} />
{view.dot ? (
<span className="absolute -right-0.5 -bottom-0.5 rounded-full bg-card ring-2 ring-card">
<AgentStateDot state={view.dot} size="md" progress={view.progress} />
</span>
) : null}
</div>
)
}
@@ -67,7 +67,9 @@ describe("another agent's message in the transcript", () => {
const source = from([{ address: CODER.address, name: 'Coder' }])
renderUserMessage(source)
fireEvent.click(screen.getByRole('button', { name: 'Coder' }))
expect(openAgentMessageSender).toHaveBeenCalledWith(source, source.senders[0], 'wt-chat')
expect(openAgentMessageSender).toHaveBeenCalledWith(source, source.senders[0], 'wt-chat', {
kind: 'local'
})
})
it('separates the names as the queued card does, and keeps two same-named senders two', () => {
@@ -77,8 +79,9 @@ describe("another agent's message in the transcript", () => {
{ address: 'term_b', name: 'Codex' }
])
)
expect(screen.getByText('Message from')).toBeInTheDocument()
expect(screen.getAllByRole('button', { name: 'Codex' })).toHaveLength(2)
expect(container.textContent).toContain('Message fromCodex,Codex')
expect(container.textContent).toContain('Codex,Codex')
})
it('names each sender, an unnamed one as an agent, and counts the rest', () => {
@@ -115,7 +115,7 @@ describe('the name a sender is shown under', () => {
agentStatusByPaneKey: { [`${TAB}:${LEAF}`]: agentRow(HANDLE) }
})
renderSenders(source({}, 'Recorded name'))
expect(screen.getByRole('button', { name: 'Recorded name' })).toBeInTheDocument()
expect(screen.getByText('Recorded name')).toBeInTheDocument()
expect(screen.queryByText('Parser worker')).not.toBeInTheDocument()
})
@@ -130,7 +130,7 @@ describe('the name a sender is shown under', () => {
tabsByWorktree: { [WORKTREE]: [terminalTab({ customTitle: 'New name' })] }
})
})
expect(screen.getByRole('button', { name: 'Recorded name' })).toBeInTheDocument()
expect(screen.getByText('Recorded name')).toBeInTheDocument()
expect(screen.queryByText('New name')).not.toBeInTheDocument()
})
@@ -144,7 +144,7 @@ describe('the name a sender is shown under', () => {
orcaSessionId: SESSION
})
)
expect(screen.getByRole('button', { name: 'Port the parser' })).toBeInTheDocument()
expect(screen.getByText('Port the parser')).toBeInTheDocument()
})
it('keeps the recorded name for a chat sender whose tab has no name of its own', () => {
@@ -155,7 +155,7 @@ describe('the name a sender is shown under', () => {
'Port the parser task'
)
)
expect(screen.getByRole('button', { name: 'Port the parser task' })).toBeInTheDocument()
expect(screen.getByText('Port the parser task')).toBeInTheDocument()
})
it('keeps the recorded name when its agent row has no name of its own', () => {
@@ -164,12 +164,12 @@ describe('the name a sender is shown under', () => {
agentStatusByPaneKey: { [`${TAB}:${LEAF}`]: agentRow(HANDLE) }
})
renderSenders(source({}))
expect(screen.getByRole('button', { name: 'Recorded name' })).toBeInTheDocument()
expect(screen.getByText('Recorded name')).toBeInTheDocument()
})
it('keeps the recorded name once the sender is gone from this window', () => {
renderSenders(source({}))
expect(screen.getByRole('button', { name: 'Recorded name' })).toBeInTheDocument()
expect(screen.getByText('Recorded name')).toBeInTheDocument()
})
it('keeps the recorded name for a sender on another host, whatever runs here', () => {
@@ -1,8 +1,10 @@
import { Fragment } from 'react'
import { ArrowUpRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { openAgentMessageSender } from '@/lib/open-agent-message-sender'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import { agentMessageSenderCanOpen } from '../../../../shared/agent-session/agent-message-presentation'
import { useNativeChatVisualOwner } from './native-chat-visual-owner'
import {
agentMessageSendersShown,
@@ -18,34 +20,39 @@ import {
export function NativeChatAgentMessageSenders({
from,
chatWorktreeId,
target,
queued = false
}: {
from: AgentMessageSource
chatWorktreeId: string | null
target?: RuntimeClientTarget
queued?: boolean
}): React.JSX.Element {
const { shown, more } = agentMessageSendersShown(from)
const owner = useNativeChatVisualOwner()
const worktreeId = owner?.worktreeId ?? chatWorktreeId
return (
<div className="flex min-w-0 max-w-full flex-wrap items-center text-xs text-chat-foreground-faint">
<div
className={`flex min-w-0 max-w-full flex-wrap items-center ${queued ? 'text-xs text-chat-foreground-faint' : '-ml-2.5 text-sm text-foreground'}`}
>
<span>
{queued
? translate('components.native-chat.queuedMessages.fromSender', 'From')
: translate('components.native-chat.agentMessage.messageFrom', 'Message from')}
{queued ? translate('components.native-chat.queuedMessages.fromSender', 'From') : null}
</span>
{shown.length === 0 ? <span className="px-2">{unnamedSenderLabel()}</span> : null}
{shown.length === 0 ? (
<span className={queued ? 'px-2' : 'px-2.5'}>{unnamedSenderLabel()}</span>
) : null}
{shown.map((sender, index) => (
<Fragment key={sender.party.address}>
<SenderName
from={from}
sender={sender}
chatWorktreeId={worktreeId}
target={owner?.target}
target={owner?.target ?? target}
queued={queued}
/>
{/* Pulled back over the name's padding, so it reads "A, B". */}
{index < shown.length - 1 ? (
<span aria-hidden className="-ml-2">
<span aria-hidden className={queued ? '-ml-2' : '-ml-2.5'}>
,
</span>
) : null}
@@ -60,17 +67,19 @@ function SenderName({
from,
sender,
chatWorktreeId,
target
target,
queued
}: {
from: AgentMessageSource
sender: AgentMessageSender
chatWorktreeId: string | null
target?: RuntimeClientTarget
queued: boolean
}): React.JSX.Element {
const label = useAgentMessageSenderLabel(sender, chatWorktreeId, target)
if (!chatWorktreeId || !opensFromHere(sender)) {
if (!agentMessageSenderCanOpen(sender, Boolean(chatWorktreeId)) || !chatWorktreeId) {
return (
<span className="max-w-48 truncate px-2" title={label}>
<span className={`max-w-48 truncate ${queued ? 'px-2' : 'px-2.5'}`} title={label}>
{label}
</span>
)
@@ -78,9 +87,11 @@ function SenderName({
return (
<Button
type="button"
variant="link"
size="xs"
title={label}
variant={queued ? 'link' : 'ghost'}
size={queued ? 'xs' : 'sm'}
title={
queued ? label : translate('components.native-chat.agentMessage.goToAgent', 'Go to agent')
}
onClick={() =>
void (target
? openAgentMessageSender(from, sender, chatWorktreeId, target)
@@ -88,11 +99,7 @@ function SenderName({
}
>
<span className="max-w-48 truncate">{label}</span>
{!queued ? <ArrowUpRight className="size-3" aria-hidden /> : null}
</Button>
)
}
/** A federated sender (`dispatch:<id>`) runs on another host, which this chat's host cannot open. */
function opensFromHere(sender: AgentMessageSender): boolean {
return !sender.party.address.startsWith('dispatch:')
}
@@ -0,0 +1,170 @@
// @vitest-environment happy-dom
import {
done,
script,
SESSION,
SCOPE,
write,
writeSpy
} from './structured-queued-edit.test-fixture'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import type { AgentSessionQueuedMessage } from '../../../../shared/agent-session-wire'
import { ORCA_DISPATCH_STATUS_PREAMBLE_PREFIX } from '../../../../shared/orca-dispatch-status-prompt'
import { agentSessionSendBodyFingerprint } from '../../../../shared/structured-agent-session-send-mutation'
import { projectQueuedMessageCards } from '../../../../shared/structured-agent-session-queued-cards'
import { NativeChatQueuedMessageCard } from './NativeChatQueuedMessageCard'
import type { NativeChatStructuredComposerTransport } from './native-chat-composer-types'
import { changePrompt, promptValue } from './native-chat-prompt-editor.test-support'
import { render } from './native-chat-tooltip-test-harness'
import { useStructuredAgentSessionQueuedEdit } from './use-structured-agent-session-queued-edit'
vi.mock('../../store', async () => {
const { create } = await import('zustand')
return {
useAppStore: create(() => ({
dictationState: 'idle',
settings: { voice: { enabled: false }, nativeChatSessionOptions: {} },
agentStatusByPaneKey: {},
tabsByWorktree: {},
unifiedTabsByWorktree: {},
worktreesByRepo: {},
detectedWorktreesByRepo: {},
folderWorkspaces: [],
updateSettings: vi.fn()
}))
}
})
// Keep host discovery and OS file intake outside this text-only queued consumer contract.
vi.mock('./use-native-chat-session-options', () => ({
useNativeChatSessionOptions: () => ({ surface: null, snapshot: [] })
}))
vi.mock('./use-native-chat-mention-files', () => ({
useNativeChatMentionFiles: () => ({ files: [], loading: false, failed: false })
}))
vi.mock('./use-native-chat-skills', () => ({
useNativeChatSkills: () => ({ status: 'ready', skills: [], error: null, retry: vi.fn() })
}))
vi.mock('./use-native-chat-file-drops', () => ({
useNativeChatFileDrops: () => ({
pickAttachments: vi.fn(),
attachExternalPaths: vi.fn(),
captureExternalPaths: vi.fn(),
captureCopiedFiles: vi.fn(),
resolveAttachmentOwner: () => ({ kind: 'local' })
})
}))
const transport: NativeChatStructuredComposerTransport = {
capturePresentation: () => () => true,
send: vi.fn(() => true),
dispatchCommand: vi.fn(async () => ({ handled: false, accepted: false, error: null })),
optionsSurface: {
getSnapshot: () => [],
setOption: vi.fn(),
invokeAction: vi.fn(),
subscribe: () => () => {}
},
optionSnapshot: [],
onError: vi.fn(),
runtime: 'local',
sessionId: SESSION,
runtimeEnvironmentId: null
}
function QueuedTask({ message }: { message: AgentSessionQueuedMessage }) {
const messages = [message]
const cards = projectQueuedMessageCards(messages, [], {})
const { editor, begin } = useStructuredAgentSessionQueuedEdit({
transport: { target: { kind: 'local' }, sessionId: SESSION, capable: true, write },
messages,
cards,
submissions: [],
composerScopeKey: SCOPE,
promptOpen: false
})
return (
<ul>
<NativeChatQueuedMessageCard
card={cards[0]!}
chatWorktreeId={null}
showsSteerShortcut={false}
onSteer={() => {}}
onDelete={() => {}}
onEdit={() => void begin(message.messageId)}
editor={editor ?? undefined}
editAttachTo={{ terminalTabId: 'tab', transport }}
/>
</ul>
)
}
afterEach(() => vi.restoreAllMocks())
it('shows the queued task and edits, saves and reopens its full raw input in the chat composer', async () => {
const prefix = `${ORCA_DISPATCH_STATUS_PREAMBLE_PREFIX} You are a dispatched worker.\n=== TASK ===\n`
const task = 'Inspect the queued task'
const changedTask = 'Inspect the edited task'
const message: AgentSessionQueuedMessage = {
messageId: 'card',
position: 1,
state: 'waiting',
body: {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: prefix + task }],
from: {
kind: 'agent',
senders: [],
orchestration: { message: 'task', runId: 'r', taskId: 't', dispatchId: 'd' }
}
}
}
// The host's queuedMessageUpdateDecision removes attribution on a content-changing Save.
const { from: _sender, ...userBody } = message.body
const saved: AgentSessionQueuedMessage = {
...message,
body: { ...userBody, blocks: [{ type: 'text', text: prefix + changedTask }] }
}
script.answer = done({
status: 'updated',
messageId: message.messageId,
fingerprint: agentSessionSendBodyFingerprint(SESSION, saved.body)
})
const view = render(<QueuedTask message={message} />)
expect(screen.getByTitle(task).textContent).toBe(task)
expect(screen.getByText('From')).toBeDefined()
expect(screen.getByText('an agent')).toBeDefined()
expect(screen.queryByText(/You are working inside Orca/)).toBeNull()
fireEvent.pointerDown(screen.getByRole('button', { name: 'More actions' }))
fireEvent.click(await screen.findByRole('menuitem', { name: 'Edit message' }))
const field = await screen.findByRole('textbox', { name: 'Edit message' })
await waitFor(() => expect(field.getAttribute('contenteditable')).toBe('true'))
expect(promptValue(field)).toBe(prefix + task)
changePrompt(field, prefix + changedTask)
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(writeSpy).toHaveBeenCalledOnce())
expect(writeSpy.mock.calls[0]?.[2]).toMatchObject({
messageId: 'card',
text: prefix + changedTask
})
await waitFor(() => expect(screen.queryByRole('textbox')).toBeNull())
view.rerender(<QueuedTask message={saved} />)
expect(screen.getByTitle(prefix + changedTask, { normalizer: (text) => text }).textContent).toBe(
prefix + changedTask
)
expect(screen.queryByText('From')).toBeNull()
expect(screen.queryByText('an agent')).toBeNull()
fireEvent.pointerDown(screen.getByRole('button', { name: 'More actions' }))
fireEvent.click(await screen.findByRole('menuitem', { name: 'Edit message' }))
const reopened = await screen.findByRole('textbox', { name: 'Edit message' })
await waitFor(() => expect(reopened.getAttribute('contenteditable')).toBe('true'))
expect(promptValue(reopened)).toBe(prefix + changedTask)
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByRole('textbox')).toBeNull())
expect(writeSpy).toHaveBeenCalledOnce()
expect(transport.send).not.toHaveBeenCalled()
})
@@ -13,7 +13,7 @@ import { NativeChatUserMessageFold } from './NativeChatUserMessageFold'
import { NativeChatCodeBlock } from './NativeChatCodeBlock'
import { NativeChatCopyButton } from './NativeChatCopyButton'
import { NativeChatMessageTimestamp } from './NativeChatMessageTimestamp'
import { NativeChatAgentMessageSenders } from './NativeChatAgentMessageSenders'
import { NativeChatAgentMessageCard } from './NativeChatAgentMessageCard'
import {
NativeChatDetailsDisclosure,
NativeChatImageAttachments
@@ -96,62 +96,62 @@ export function NativeChatUserMessageRow({
className={cn('group relative flex flex-col gap-0.5', from ? 'items-start' : 'items-end')}
>
{from ? (
<NativeChatAgentMessageSenders
<NativeChatAgentMessageCard
message={message}
from={from}
chatWorktreeId={runtimeContext?.worktreeId ?? null}
runtimeContext={runtimeContext}
onLinkClick={onLinkClick}
allowFileUriLinks={allowFileUriLinks}
onRefolded={onRefolded}
/>
) : null}
<div
className={cn(
'max-w-[80%] text-sm',
from
? 'select-text border-l-2 border-border/60 pl-3 text-chat-foreground'
: 'rounded-xl border border-chat-user-border bg-chat-user-surface px-3.5 py-2.5 text-chat-foreground-strong'
)}
>
{markdown ? (
<>
) : (
<div className="max-w-[80%] rounded-xl border border-chat-user-border bg-chat-user-surface px-3.5 py-2.5 text-sm text-chat-foreground-strong">
{markdown ? (
<>
<NativeChatImageAttachments
blocks={prose}
runtimeContext={runtimeContext}
enablePreview={runtimeContext !== undefined}
/>
<NativeChatUserMessageFold
messageId={message.id}
markdown={markdown}
onRefolded={onRefolded}
>
<NativeChatMarkdown
rowKey={message.id}
content={markdown}
variant="document"
className="text-sm"
renderCodeBlock={NativeChatCodeBlock}
onLinkClick={onLinkClick}
allowFileUriLinks={allowFileUriLinks}
/>
</NativeChatUserMessageFold>
</>
) : (
<NativeChatImageAttachments
blocks={prose}
runtimeContext={runtimeContext}
enablePreview={runtimeContext !== undefined}
/>
<NativeChatUserMessageFold
messageId={message.id}
markdown={markdown}
onRefolded={onRefolded}
>
<NativeChatMarkdown
rowKey={message.id}
content={markdown}
variant="document"
className="text-sm"
renderCodeBlock={NativeChatCodeBlock}
onLinkClick={onLinkClick}
allowFileUriLinks={allowFileUriLinks}
/>
</NativeChatUserMessageFold>
</>
) : (
<NativeChatImageAttachments
blocks={prose}
runtimeContext={runtimeContext}
enablePreview={runtimeContext !== undefined}
/>
)}
</div>
)}
</div>
)}
{message.sentAs === 'goal' ? (
<div className="flex items-center gap-1 text-xs text-chat-foreground-faint">
<Goal className="size-3" aria-hidden />
<span>{translate('components.native-chat.goal.sentAsGoal', 'Sent as goal')}</span>
</div>
) : null}
<UserMessageMeta
markdown={markdown}
timestamp={message.timestamp}
sending={sending}
{...(rewind ? { rewind: { itemId: message.id, surface: rewind } } : {})}
/>
{!from ? (
<UserMessageMeta
markdown={markdown}
timestamp={message.timestamp}
sending={sending}
{...(rewind ? { rewind: { itemId: message.id, surface: rewind } } : {})}
/>
) : null}
{deliveryNotice?.text !== undefined ? (
<div className="flex max-w-[85%] items-center gap-2 text-2xs text-destructive/80">
<span className="min-w-0 break-words">{deliveryNotice.text}</span>
@@ -174,7 +174,7 @@ it('uses the recipient host when identical session text is published by two host
}
})
render(<NativeChatAgentMessageSenders from={source()} chatWorktreeId={RECIPIENT} />)
expect(screen.getByRole('button', { name: 'Remote sender' })).toBeInTheDocument()
expect(screen.getByText('Remote sender')).toBeInTheDocument()
})
it('shows a chat sender by its current rename', () => {
@@ -183,11 +183,11 @@ it('shows a chat sender by its current rename', () => {
const status = feed.snapshot
expect(feed.names.has(ROOT)).toBe(false)
render(<NativeChatAgentMessageSenders from={source()} chatWorktreeId={RECIPIENT} />)
expect(screen.getByRole('button', { name: 'Original sender' })).toBeInTheDocument()
expect(screen.getByText('Original sender')).toBeInTheDocument()
act(() => publish('local', undefined, [{ sessionId: ROOT, conversationName: null }]))
expect(screen.getByRole('button', { name: 'Original sender' })).toBeInTheDocument()
expect(screen.getByText('Original sender')).toBeInTheDocument()
act(() => publish('local', undefined, [{ sessionId: ROOT, conversationName: 'Renamed sender' }]))
expect(screen.getByRole('button', { name: 'Renamed sender' })).toBeInTheDocument()
expect(screen.getByText('Renamed sender')).toBeInTheDocument()
expect(feed.snapshot).toBe(status)
expect(status.get(ROOT)?.conversationName).toBeUndefined()
})
@@ -211,14 +211,14 @@ it.each([false, true])(
})
const from = source(true)
render(<NativeChatAgentMessageSenders from={from} chatWorktreeId={RECIPIENT} queued={queued} />)
expect(screen.getByRole('button', { name: 'Original sender' })).toBeInTheDocument()
expect(screen.getByText('Original sender')).toBeInTheDocument()
act(() => {
useAppStore.setState({
agentStatusByPaneKey: { [PANE]: row({ key: 'session_id', id: 'provider-b' }) },
tabsByWorktree: { [SENDER]: [{ ...terminal, customTitle: 'Renamed B' }] }
})
})
const link = screen.getByRole('button', { name: 'Original sender' })
const link = screen.getByText('Original sender')
expect(link).toBeInTheDocument()
fireEvent.click(link)
expect(openAgentMessageSender).toHaveBeenCalledWith(from, from.senders[0], RECIPIENT)
@@ -0,0 +1,62 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { makeUnifiedTab } from '@/store/slices/store-test-helpers'
import { findAgentMessageSenderChatTab } from './native-chat-agent-message-sender-label'
const initial = useAppStore.getState()
beforeEach(() => {
useAppStore.setState({
activeWorktreeId: 'workspace',
activeWorkspaceExecutionHostId: 'local'
})
})
afterEach(() => {
useAppStore.setState(initial, true)
vi.restoreAllMocks()
})
it.each([100, 1000])(
'shares host-qualified tab collision scans for %i distinct senders and repeated cards',
(n) => {
const tab = (id: number, host: 'local' | 'runtime:remote') =>
makeUnifiedTab({
id: `${host}-${id}`,
entityId: `session-${id}`,
worktreeId: 'workspace',
groupId: 'g',
contentType: 'agent-session',
executionHostId: host
})
let tabVisits = 0
const bucket = Array.from({ length: n }, (_, i) => [
tab(i, 'local'),
tab(i, 'runtime:remote')
]).flat()
bucket.forEach((value, i) =>
Object.defineProperty(bucket, i, {
get() {
tabVisits++
return value
}
})
)
useAppStore.setState({ unifiedTabsByWorktree: { workspace: bucket } })
const state = useAppStore.getState()
for (let pass = 0; pass < 2; pass++) {
for (let i = 0; i < n; i++) {
expect(findAgentMessageSenderChatTab(state, `session-${i}`, 'local')?.id).toBe(`local-${i}`)
}
}
// Workspace/session index, per-bucket session index, and qualified collision index each walk 2N tabs once.
expect(tabVisits).toBe(n * 6)
tabVisits = 0
useAppStore.setState({ activeWorkspaceExecutionHostId: 'runtime:remote' })
for (let i = 0; i < n; i++) {
expect(
findAgentMessageSenderChatTab(useAppStore.getState(), `session-${i}`, 'local')?.id
).toBe(`local-${i}`)
}
expect(tabVisits).toBe(n * 2) // Only the owner-qualified index invalidates.
}
)
@@ -1,3 +1,4 @@
import { agentMessageSenderLabel } from '../../../../shared/agent-session/agent-message-presentation'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
@@ -8,6 +9,7 @@ import {
import { useStructuredChatTabConversationName } from '@/runtime/structured-conversation-name'
import {
structuredAgentSessionOwnerForTab,
structuredAgentSessionOwnerInputs,
resolveStructuredAgentSessionOwner,
executionHostIdForStructuredTarget
} from '@/runtime/structured-agent-session-owner'
@@ -20,7 +22,46 @@ export function unnamedSenderLabel(): string {
return translate('components.native-chat.agentMessage.unnamedSender', 'an agent')
}
function findChatTab(
const qualifiedTabs = new WeakMap<
AppState['unifiedTabsByWorktree'],
{
inputs: unknown[]
tabs: Map<string, Map<string | null, Tab>>
}
>()
function qualifiedSenderTab(
state: AppState,
sessionId: string,
host: string | null
): Tab | undefined {
const source = state.unifiedTabsByWorktree
const inputs = structuredAgentSessionOwnerInputs(state)
let cached = qualifiedTabs.get(source)
if (!cached || inputs.some((value, index) => value !== cached?.inputs[index])) {
const tabs = new Map<string, Map<string | null, Tab>>()
for (const bucket of Object.values(source)) {
for (const tab of bucket) {
if (tab.contentType !== 'agent-session') {
continue
}
const owner = structuredAgentSessionOwnerForTab(state, tab)
let byHost = tabs.get(tab.entityId)
if (!byHost) {
byHost = new Map()
tabs.set(tab.entityId, byHost)
}
if (!byHost.has(owner)) {
byHost.set(owner, tab)
}
}
}
cached = { inputs, tabs }
qualifiedTabs.set(source, cached)
}
return cached.tabs.get(sessionId)?.get(host)
}
export function findAgentMessageSenderChatTab(
state: AppState,
sessionId: string,
executionHostId: string | null
@@ -32,14 +73,7 @@ function findChatTab(
return tab
}
// A workspace bucket can mirror identical session ids from two hosts.
const qualified =
tab &&
tabsByWorktree[worktreeId]?.find(
(candidate) =>
candidate.contentType === 'agent-session' &&
candidate.entityId === sessionId &&
structuredAgentSessionOwnerForTab(state, candidate) === executionHostId
)
const qualified = tab && qualifiedSenderTab(state, sessionId, executionHostId)
if (qualified) {
return qualified
}
@@ -47,6 +81,18 @@ function findChatTab(
return undefined
}
export function agentMessageSenderOwner(
state: AppState,
chatWorktreeId: string | null,
target?: RuntimeClientTarget
): string | null {
return target
? executionHostIdForStructuredTarget(target)
: chatWorktreeId && getKnownExecutionHostIdForWorktree(state, chatWorktreeId)
? resolveStructuredAgentSessionOwner(state, chatWorktreeId)
: null
}
/** Live chat names follow the sender's chat on the receiving chat's host; CLI names stay as recorded. */
export function useAgentMessageSenderLabel(
sender: AgentMessageSender,
@@ -55,17 +101,19 @@ export function useAgentMessageSenderLabel(
): string {
const { address, orcaSessionId } = sender.party
const local = !address.startsWith('dispatch:')
const owner = useAppStore((state) =>
target
? executionHostIdForStructuredTarget(target)
: chatWorktreeId && getKnownExecutionHostIdForWorktree(state, chatWorktreeId)
? resolveStructuredAgentSessionOwner(state, chatWorktreeId)
: null
)
const owner = useAppStore((state) => agentMessageSenderOwner(state, chatWorktreeId, target))
const chatTab = useAppStore((state) =>
local && orcaSessionId ? findChatTab(state, orcaSessionId, owner) : undefined
local && owner && orcaSessionId
? findAgentMessageSenderChatTab(state, orcaSessionId, owner)
: undefined
)
const conversationName = useStructuredChatTabConversationName(chatTab)
const ownName = chatTab ? chatTab.customLabel?.trim() || conversationName : null
return ownName || sender.name || chatTab?.label.trim() || unnamedSenderLabel()
return agentMessageSenderLabel({
localChat: Boolean(local && chatTab),
customLabel: chatTab?.customLabel,
conversationName,
snapshotName: sender.name,
tabLabel: chatTab?.label,
fallback: unnamedSenderLabel()
})
}
@@ -0,0 +1,169 @@
import { useCallback, useMemo, useSyncExternalStore } from 'react'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import {
createWorktreeAgentRowsCache,
startWorktreeAgentRowsCachePass,
selectWorktreeAgentRowsCached
} from '@/components/dashboard/worktree-agent-rows-cache'
import { createWorktreeAgentFreshnessSelector } from '@/components/sidebar/worktree-agent-freshness-selector'
import { selectRuntimeAgentOrchestrationForWorktree } from '@/components/sidebar/worktree-agent-row-selectors'
import { agentRowDisplayDotState } from '@/lib/agent-row-dot-state'
import { resolveRowProgramStatus } from '@/lib/program-status/agent-row-presentation'
import {
agentMessageSenderView,
type AgentMessageSenderView
} from '../../../../shared/agent-session/agent-message-presentation'
const EMPTY_VIEW: AgentMessageSenderView = Object.freeze({})
export type SenderLocation = {
host: string
worktreeId: string
paneKey: string
agentType?: AgentMessageSenderView['agentType']
}
type LocateSender = (state: AppState) => SenderLocation | undefined
function equalView(a: AgentMessageSenderView, b: AgentMessageSenderView): boolean {
return a.agentType === b.agentType && a.dot === b.dot && a.progress === b.progress
}
function createSource(worktreeId: string) {
const cache = createWorktreeAgentRowsCache()
const freshness = createWorktreeAgentFreshnessSelector(worktreeId)
let snapshot: AppState | undefined
let rows: ReturnType<typeof selectWorktreeAgentRowsCached> | undefined
let views = new Map<string, AgentMessageSenderView>()
return {
leases: 0,
dispose() {
snapshot = undefined
rows = undefined
views.clear()
cache.byWorktree.clear()
startWorktreeAgentRowsCachePass(cache)
},
read(state: AppState, paneKey: string): AgentMessageSenderView | undefined {
if (snapshot !== state) {
snapshot = state
startWorktreeAgentRowsCachePass(cache)
const next = selectWorktreeAgentRowsCached({
state,
worktreeId,
cache,
now: Date.now(),
generation: freshness(state),
orchestration: selectRuntimeAgentOrchestrationForWorktree(state, worktreeId)
})
if (rows !== next) {
rows = next
const indexed = new Map<string, AgentMessageSenderView>()
for (const row of next) {
if (indexed.has(row.paneKey)) {
continue
}
const { presentation, dotStateOverride } = resolveRowProgramStatus(row)
const value = {
agentType: row.agentType,
dot: dotStateOverride ?? agentRowDisplayDotState(row),
progress: presentation.progress
}
const previous = views.get(row.paneKey)
indexed.set(row.paneKey, previous && equalView(previous, value) ? previous : value)
}
views = indexed
}
}
return views.get(paneKey)
}
}
}
type Source = ReturnType<typeof createSource>
const subscribedSources = new Map<string, Source>()
// Uncommitted renders have no registry lease; their snapshot and derived rows can be collected.
const transientSources = new WeakMap<AppState, Map<string, Source>>()
function sourceKey(location: SenderLocation): string {
return JSON.stringify([location.host, location.worktreeId])
}
function readSource(state: AppState, location: SenderLocation): Source {
const key = sourceKey(location)
const subscribed = subscribedSources.get(key)
if (subscribed) {
return subscribed
}
let sources = transientSources.get(state)
if (!sources) {
sources = new Map()
transientSources.set(state, sources)
}
let source = sources.get(key)
if (!source) {
source = createSource(location.worktreeId)
sources.set(key, source)
}
return source
}
/** Share the sidebar projection before notifying cards; only subscription leases retain a source. */
export function useAgentMessageSenderSource(locate: LocateSender): AgentMessageSenderView {
const selected = useMemo(() => {
let previous = EMPTY_VIEW
return (state: AppState) => {
const location = locate(state)
const next = location
? agentMessageSenderView(
readSource(state, location).read(state, location.paneKey),
location.agentType
)
: EMPTY_VIEW
if (!equalView(previous, next)) {
previous = next
}
return previous
}
}, [locate])
const subscribe = useCallback(
(notify: () => void) => {
let lease: { key: string; source: Source } | undefined
const release = () => {
if (lease && --lease.source.leases === 0) {
subscribedSources.delete(lease.key)
lease.source.dispose()
}
lease = undefined
}
const update = () => {
const state = useAppStore.getState()
const location = locate(state)
const key = location ? sourceKey(location) : undefined
if (lease?.key !== key) {
release()
if (location && key) {
const source = readSource(state, location)
transientSources.get(state)?.delete(key)
source.leases++
subscribedSources.set(key, source)
lease = { key, source }
}
}
return selected(state)
}
let value = update()
const unsubscribe = useAppStore.subscribe(() => {
const next = update()
if (next !== value) {
value = next
notify()
}
})
return () => {
unsubscribe()
release()
}
},
[locate, selected]
)
const getSnapshot = useCallback(() => selected(useAppStore.getState()), [selected])
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
}
@@ -0,0 +1,107 @@
import { useCallback, useMemo } from 'react'
import {
useAgentMessageSenderSource,
type SenderLocation
} from './native-chat-agent-message-sender-source'
import type { AppState } from '@/store/types'
import { getKnownExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { structuredAgentSessionOwnerInputs } from '@/runtime/structured-agent-session-owner'
import { structuredAgentSessionPaneKey } from '../../../../shared/structured-agent-session-projection'
import type { AgentMessageSender } from '../../../../shared/agent-session/agent-session-message-source'
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import {
agentMessageSenderOwner,
findAgentMessageSenderChatTab
} from './native-chat-agent-message-sender-label'
const handleIndexes = new WeakMap<
object,
{ inputs: unknown[]; index: Map<string, Map<string | null, AgentStatusEntry[]>> }
>()
function byHandle(
state: AppState,
source: AppState['agentStatusByPaneKey'] | AppState['retainedAgentsByPaneKey']
) {
const inputs = structuredAgentSessionOwnerInputs(state)
let cached = handleIndexes.get(source)
if (!cached || inputs.some((value, index) => value !== cached?.inputs[index])) {
const index = new Map<string, Map<string | null, AgentStatusEntry[]>>()
const owners = new Map<string, string | null>()
for (const value of Object.values(source)) {
const entry = 'entry' in value ? value.entry : value
if (!entry.terminalHandle || !entry.worktreeId) {
continue
}
if (!owners.has(entry.worktreeId)) {
owners.set(entry.worktreeId, getKnownExecutionHostIdForWorktree(state, entry.worktreeId))
}
const owner = owners.get(entry.worktreeId) ?? null
let byHost = index.get(entry.terminalHandle)
if (!byHost) {
byHost = new Map()
index.set(entry.terminalHandle, byHost)
}
const entries = byHost.get(owner) ?? []
entries.push(entry)
byHost.set(owner, entries)
}
cached = { inputs, index }
handleIndexes.set(source, cached)
}
return cached.index
}
export function useAgentMessageSenderView(
sender: AgentMessageSender | undefined,
chatWorktreeId: string | null,
target?: RuntimeClientTarget
) {
const address = sender?.party.address
const sessionId = sender?.party.orcaSessionId
const handle = sender?.party.terminalHandle
const targetKind = target?.kind
const environmentId = target?.kind === 'environment' ? target.environmentId : undefined
const stableTarget = useMemo<RuntimeClientTarget | undefined>(
() =>
targetKind === 'local'
? { kind: 'local' }
: environmentId !== undefined
? { kind: 'environment', environmentId }
: undefined,
[targetKind, environmentId]
)
const locate = useCallback(
(state: AppState): SenderLocation | undefined => {
if (!address || address.startsWith('dispatch:')) {
return undefined
}
const owner = agentMessageSenderOwner(state, chatWorktreeId, stableTarget)
if (!owner) {
return undefined
}
const chatTab = sessionId ? findAgentMessageSenderChatTab(state, sessionId, owner) : undefined
let paneKey = chatTab && structuredAgentSessionPaneKey(chatTab.id, chatTab.entityId)
if (!paneKey && handle) {
for (const source of [state.agentStatusByPaneKey, state.retainedAgentsByPaneKey]) {
const matches = byHandle(state, source).get(handle)?.get(owner) ?? []
if (matches.length) {
paneKey = matches.length === 1 ? matches[0].paneKey : undefined
break
}
}
}
const worktreeId =
chatTab?.worktreeId ??
(paneKey
? (state.agentStatusByPaneKey[paneKey]?.worktreeId ??
state.retainedAgentsByPaneKey[paneKey]?.worktreeId)
: undefined)
return worktreeId && paneKey
? { host: owner, worktreeId, paneKey, agentType: chatTab?.agentSessionAgent }
: undefined
},
[address, sessionId, handle, chatWorktreeId, stableTarget]
)
return useAgentMessageSenderSource(locate)
}
@@ -1,3 +1,4 @@
import { agentMessageTranscriptText } from '../../../../shared/agent-session/agent-message-display-text'
import { nativeChatMessageCopyText } from '../../../../shared/native-chat-message-copy-text'
import { nativeChatStoppedBeforeStartLabel } from './native-chat-stopped-before-start-row'
import type { NativeChatTranscriptSlot } from './native-chat-transcript-slots'
@@ -9,6 +10,8 @@ export function nativeChatSlotCopyText(
): string | null {
const slot = slots.find((entry) => entry.kind === 'message' && entry.message.id === messageId)
return slot?.kind === 'message'
? nativeChatMessageCopyText(slot.message, nativeChatStoppedBeforeStartLabel())
? slot.message.from && slot.message.role === 'user'
? agentMessageTranscriptText(slot.message.blocks, slot.message.from)
: nativeChatMessageCopyText(slot.message, nativeChatStoppedBeforeStartLabel())
: null
}
@@ -7,6 +7,9 @@ import { queuedMessageWithEdit } from '../../../../shared/queued-message-edit'
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, vi } from 'vitest'
import { toast } from 'sonner'
import type * as SonnerModule from 'sonner'
import type * as StructuredSessionClient from '@/runtime/structured-agent-session-client'
import type * as RendererI18n from '@/i18n/i18n'
import type { AgentJournalSubmission } from '../../../../shared/agent-session-journal-types'
import type {
AgentSessionQueuedMessage,
@@ -44,11 +47,28 @@ const hoistedRpc = vi.hoisted(() =>
)
)
export const rpc = hoistedRpc
vi.mock('@/runtime/structured-agent-session-client', () => ({
const toastError = vi.hoisted(() => vi.fn<typeof SonnerModule.toast.error>())
vi.mock('@/runtime/structured-agent-session-client', async (importOriginal) => ({
...(await importOriginal<typeof StructuredSessionClient>()),
callStructuredAgentSession: hoistedRpc
}))
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
vi.mock('sonner', async (importOriginal) => {
const original = await importOriginal<typeof SonnerModule>()
return {
...original,
toast: Object.assign(
function (this: unknown, ...args: Parameters<typeof original.toast>) {
return original.toast.apply(this, args)
},
original.toast,
{ error: toastError }
)
}
})
vi.mock('@/i18n/i18n', async (importOriginal) => ({
...(await importOriginal<typeof RendererI18n>()),
translate: (_key: string, fallback: string) => fallback
}))
export const SESSION = 'folder-session'
export const SCOPE = 'pane-draft'
+6 -1
View File
@@ -19730,7 +19730,12 @@
},
"agentMessage": {
"messageFrom": "Message from",
"unnamedSender": "an agent"
"unnamedSender": "an agent",
"goToAgent": "Go to agent",
"messageFromCoordinator": "Message from your coordinator",
"showMore": "Show more",
"taskFromCoordinator": "Task from your coordinator",
"updateFromWorker": "Update from your worker"
},
"goal": {
"placeholder": "Describe your goal, define measurable outcomes for best results",
@@ -0,0 +1,70 @@
import { expect, it, vi } from 'vitest'
import { agentMessageDisplayText, agentMessageTranscriptText } from './agent-message-display-text'
import type { AgentMessageSource } from './agent-session-message-source'
import type { NativeChatBlock } from '../native-chat-types'
import * as grammar from '../orca-dispatch-status-prompt'
const task: AgentMessageSource = {
kind: 'agent',
senders: [],
orchestration: { message: 'task', runId: 'r', taskId: 't', dispatchId: 'd' }
}
const prefix = `${grammar.ORCA_DISPATCH_STATUS_PREAMBLE_PREFIX}\r\nInstructions\r\n=== TASK ===\r\n`
it('keeps exact task suffixes and canonical blocks, with paragraph fallback for ineligible inputs', () => {
for (const suffix of ['', ' Task 🐋\r\n\r\n=== TASK ===\ntrailing ']) {
const blocks: NativeChatBlock[] = [
{ type: 'text', text: prefix + suffix },
{ type: 'image-ref', path: '/image.png' }
]
const original = structuredClone(blocks)
expect(agentMessageDisplayText(blocks, task)).toBe(suffix)
expect(agentMessageTranscriptText(blocks, task)).toBe(suffix)
expect(agentMessageDisplayText(blocks)).toBeUndefined()
expect(blocks).toEqual(original)
}
for (const blocks of [
[{ type: 'text', text: 'ordinary prose' }],
[
{
type: 'text',
text: `${grammar.ORCA_DISPATCH_STATUS_PREAMBLE_PREFIX}\n${'x'.repeat(24_576)}\n=== TASK ===\nlate`
}
]
] satisfies NativeChatBlock[][]) {
expect(agentMessageDisplayText(blocks, task)).toBeUndefined()
expect(agentMessageTranscriptText(blocks, task)).toBe(
blocks.map((block) => (block.type === 'text' ? block.text : '')).join('\n\n')
)
}
})
it.each([100, 1000])(
'parses %i real task inputs once and invalidates immutable edited blocks',
(n) => {
const parse = vi.spyOn(grammar, 'orcaDispatchTaskBodyOffset')
const suffix = 'task '.repeat(6000)
const inputs = Array.from({ length: n }, (): NativeChatBlock[] => [
{ type: 'text', text: prefix + suffix }
])
try {
for (const blocks of inputs) {
expect(agentMessageDisplayText(blocks, task)).toBe(suffix)
}
expect(parse).toHaveBeenCalledTimes(n)
parse.mockClear()
for (const blocks of inputs) {
expect(agentMessageDisplayText(blocks, task)).toBe(suffix)
}
expect(parse).not.toHaveBeenCalled()
for (const _blocks of inputs) {
expect(
agentMessageDisplayText([{ type: 'text', text: `${prefix}${suffix}edited` }], task)
).toBe(`${suffix}edited`)
}
expect(parse).toHaveBeenCalledTimes(n)
} finally {
parse.mockRestore()
}
}
)
@@ -0,0 +1,36 @@
import type { AgentMessageSource } from './agent-session-message-source'
import type { NativeChatBlock } from '../native-chat-types'
import { nativeChatProseToMarkdown } from '../native-chat/transcript/native-chat-prose'
import { orcaDispatchTaskBodyOffset } from '../orca-dispatch-status-prompt'
const taskBodies = new WeakMap<readonly NativeChatBlock[], string | undefined>()
/** Display only: providers, queue editors and context readers retain the original input. */
export function agentMessageDisplayText(
blocks: readonly NativeChatBlock[],
from?: AgentMessageSource
): string | undefined {
if (from?.orchestration?.message !== 'task') {
return undefined
}
if (taskBodies.has(blocks)) {
return taskBodies.get(blocks)
}
if (blocks.some((block) => block.type !== 'text' && block.type !== 'image-ref')) {
taskBodies.set(blocks, undefined)
return undefined
}
const texts = blocks.filter((block) => block.type === 'text')
const text = texts.length === 1 ? texts[0].text : undefined
const offset = text === undefined ? undefined : orcaDispatchTaskBodyOffset(text)
const body = offset === undefined ? undefined : text?.slice(offset)
taskBodies.set(blocks, body)
return body
}
export function agentMessageTranscriptText(
blocks: NativeChatBlock[],
from?: AgentMessageSource
): string {
return agentMessageDisplayText(blocks, from) ?? nativeChatProseToMarkdown(blocks)
}
@@ -0,0 +1,84 @@
import type { AgentMessageSender, AgentMessageSource } from './agent-session-message-source'
import type { AgentStatusEntry } from '../agent-status-types'
export const AGENT_MESSAGE_FOLDED_LINES = 3
export type AgentMessageHeading = 'task' | 'coordinator' | 'worker' | 'generic'
export type AgentMessageSenderView = Readonly<{
agentType?: AgentStatusEntry['agentType']
progress?: number
dot?:
| 'working'
| 'monitoring'
| 'blocked'
| 'waiting'
| 'interrupted'
| 'failed'
| 'done'
| 'idle'
| 'unverifiable'
| 'unconfirmed'
| 'permission'
}>
export function agentMessageHeading(from: AgentMessageSource): AgentMessageHeading {
if (from.orchestration?.message === 'task') {
return 'task'
}
if (from.orchestration?.message === 'mail-notice' && from.senders.length > 0) {
if (from.senders.every((sender) => sender.relation === 'coordinator')) {
return 'coordinator'
}
if (from.senders.every((sender) => sender.relation === 'worker')) {
return 'worker'
}
}
return 'generic'
}
export function agentMessageSenderLabel(facts: {
localChat: boolean
customLabel?: string | null
conversationName?: string | null
snapshotName?: string | null
tabLabel?: string | null
fallback: string
}): string {
return (
(facts.localChat && (facts.customLabel?.trim() || facts.conversationName)) ||
facts.snapshotName ||
facts.tabLabel?.trim() ||
facts.fallback
)
}
export function agentMessageSenderCanOpen(
sender: AgentMessageSender,
hasWorkspace: boolean
): boolean {
return hasWorkspace && !sender.party.address.startsWith('dispatch:')
}
export function agentMessagePrimarySender(
from: AgentMessageSource
): AgentMessageSender | undefined {
return from.senders[0]
}
export function agentMessageSenderView(
row: AgentMessageSenderView | undefined,
chatType: AgentMessageSenderView['agentType']
): AgentMessageSenderView {
return row ?? { agentType: chatType }
}
export function agentMessageOverflows(height: number, lineHeight: number): boolean {
return height > lineHeight * AGENT_MESSAGE_FOLDED_LINES + 1
}
export function agentMessageFoldLabel(open: boolean): 'expanded' | 'collapsed' {
return open ? 'expanded' : 'collapsed'
}
export function agentMessageCanCopy(text: string): boolean {
return text.length > 0
}
@@ -44,6 +44,11 @@ describe("a message's sender, read through its one reader", () => {
orchestration: { message: 'task', runId: 'r1', taskId: 't1', dispatchId: 'ctx_1' }
}
expect(readAgentMessageSource(structuredClone(task))).toEqual(task)
const parsed = readAgentMessageSource({
...task,
senders: FROM.senders.map((sender) => ({ ...sender, relation: 'future' }))
})
expect(parsed).toEqual(task)
})
it('keeps an agent an agent when its orchestration kind is newer than this build', () => {
@@ -11,6 +11,7 @@ export type AgentMessageSender = Readonly<{
party: Omit<OrchestrationPartyIdentity, 'paneKey'>
/** What Orca called the sender when it wrote, kept for when it is gone; null when it had none. */
name: string | null
relation?: 'worker' | 'coordinator' | 'none'
}>
/** One orchestration message a notice points at: its record, and its sender's `senders` address. */
@@ -80,6 +81,7 @@ const senderSchema = z.object({
terminalHandle: z.string().nullable(),
orcaSessionId: orcaSessionIdSchema.nullable()
}),
relation: salvagedOptional('relation', z.enum(['worker', 'coordinator', 'none'])),
name: z.unknown().transform(agentMessageSenderName)
})
+30
View File
@@ -119,6 +119,36 @@ export function findOrcaDispatchTaskMarkerIndex(value: string): number {
: value.indexOf(ORCA_DISPATCH_STATUS_TASK_MARKER)
}
/** Offset into the original task input; detection is bounded, the authored suffix is not. */
export function orcaDispatchTaskBodyOffset(value: string): number | undefined {
const start = findOrcaDispatchPreambleStart(value)
if (start === -1) {
return undefined
}
const scanEnd = Math.min(value.length, ORCA_DISPATCH_STATUS_SOURCE_SCAN_LIMIT)
const scan = value.slice(start, scanEnd)
const marker = findOrcaDispatchTaskMarkerIndex(scan)
if (marker === -1) {
return undefined
}
let offset = start + marker + ORCA_DISPATCH_STATUS_TASK_MARKER.length
// A marker at the scan bound must still end its original delimiter line.
if (
offset < value.length &&
!isLineBreak(value.charCodeAt(offset)) &&
(scan.includes('\n') || scan.includes('\r'))
) {
return undefined
}
if (value.charCodeAt(offset) === 13) {
offset++
}
if (value.charCodeAt(offset) === 10) {
offset++
}
return offset
}
function isLineBreak(code: number): boolean {
return code === 10 || code === 13
}
@@ -1,5 +1,6 @@
// Derive hold labels on the client; the wire carries queue state, not presentation wording.
import { agentMessageDisplayText } from './agent-session/agent-message-display-text'
import {
readWholeAgentSessionFailureFact,
type AgentSessionFailureFact,
@@ -44,6 +45,7 @@ export type QueuedMessageCard = {
messageId: string
position: number
text: string
displayText?: string
/** The draft's images, which an edit of its text keeps. */
images?: readonly NativeChatImageRefBlock[]
state: 'waiting' | 'returned'
@@ -100,6 +102,13 @@ export function projectQueuedMessageCards(
messageId: message.messageId,
position: message.position,
text: queuedMessageCardText(message.body),
...(from
? {
displayText:
agentMessageDisplayText(message.body.blocks, from) ??
queuedMessageCardText(message.body)
}
: {}),
...queuedMessageCardImages(message.body),
state: message.state,
hold,
@@ -171,12 +180,13 @@ export function newestSteerableQueuedMessageCard(
/** The card's line: its text, or for a card of images alone, the image's name or their count. */
export function queuedMessageCardLine(
card: Pick<QueuedMessageCard, 'text' | 'images'>,
card: Pick<QueuedMessageCard, 'text' | 'images' | 'displayText'>,
copy: NativeChatImageLabelCopy = NATIVE_CHAT_IMAGE_LABEL_ENGLISH
): string {
const images = card.images ?? []
if (card.text.trim() || images.length === 0) {
return card.text
const text = card.displayText ?? card.text
if (text.trim() || images.length === 0) {
return text
}
return images.length === 1
? nativeChatImageLabel(images[0], copy)