diff --git a/surface/mobile/src/lib/chat.tsx b/surface/mobile/src/lib/chat.tsx index 0630b2e03..da472802c 100644 --- a/surface/mobile/src/lib/chat.tsx +++ b/surface/mobile/src/lib/chat.tsx @@ -21,6 +21,8 @@ import { isNetworkFailure, looksLikeSummonSigil, parseSummonSigil, + pendingMessageFromSendPayload, + pendingSpawnFromPayload, PENDING_SESSION_PREFIX, randomId, reconcileDraftSnapshot, @@ -732,91 +734,6 @@ export function ChatProvider({ session, children }: { session: Session; children dispatch({ type: 'init-me', handle: me.handle, id: me.id }); }, [me.handle, me.id]); - const pendingMessageFromSendPayload = useCallback( - (msg: MobileMsgSendPayload): ChatMessage => { - const voiceFileId = msg.voice ? msg.attachments?.[0]?.id : null; - return { - id: null, - clientMsgId: msg.clientMsgId, - channelId: msg.channelId, - threadRootEventId: msg.threadRootEventId ?? null, - ...(msg.broadcast === true ? { broadcast: true } : {}), - text: msg.text, - edited: false, - author: me, - createdAt: msg.createdAt ?? new Date().toISOString(), - replyCount: 0, - lastReplyId: 0, - status: 'pending', - ...(msg.attachments && msg.attachments.length > 0 ? { attachments: msg.attachments } : {}), - ...(msg.voice && voiceFileId - ? { - voice: { - fileId: voiceFileId, - durationMs: msg.voice.durationMs, - waveform: msg.voice.waveform, - transcript: { status: 'pending' }, - }, - } - : {}), - }; - }, - [me], - ); - - const pendingSpawnFromPayload = useCallback( - (payload: SessionSpawnPayload): { message: ChatMessage; session: AgentSession } => { - const createdAt = payload.createdAt ?? new Date().toISOString(); - return { - session: { - id: payload.clientSpawnId, - workspaceId: '', - channelId: payload.channelId, - threadRootEventId: payload.threadRootEventId ?? null, - title: payload.task.slice(0, 80), - status: 'spawning', - harness: payload.harness ?? 'codex', - repo: payload.repo ?? payload.repos?.[0]?.repo ?? null, - branch: payload.branch ?? payload.repos?.[0]?.ref ?? null, - repos: payload.repos ?? null, - githubIdentityMode: payload.githubIdentityMode ?? null, - agentProfileVersionId: payload.agentProfileVersionId ?? null, - spawnedBy: me.id, - spawnerName: me.displayName, - driverId: null, - archivedAt: null, - pinned: false, - pendingSeatRequests: [], - suggestions: [], - answerProposals: [], - seatEvents: [], - costUsd: 0, - resultText: null, - createdAt, - completedAt: null, - lastEventId: 0, - permalink: '', - }, - message: { - id: null, - clientMsgId: payload.clientSpawnId, - channelId: payload.channelId, - threadRootEventId: payload.threadRootEventId ?? null, - ...(payload.broadcastCard === true ? { broadcast: true } : {}), - text: payload.task, - edited: false, - author: me, - createdAt, - replyCount: 0, - lastReplyId: 0, - status: 'pending', - sessionId: payload.clientSpawnId, - }, - }; - }, - [me], - ); - const applyQueuedOp = useCallback( (op: QueuedOp) => { if (op.opType === 'msg.send') { @@ -824,13 +741,13 @@ export function ChatProvider({ session, children }: { session: Session; children dispatch({ type: 'send-pending', channelId: payload.channelId, - message: pendingMessageFromSendPayload(payload), + message: pendingMessageFromSendPayload(payload, me), }); return; } if (op.opType === 'session.spawn') { const payload = op.payload as SessionSpawnPayload; - const pending = pendingSpawnFromPayload(payload); + const pending = pendingSpawnFromPayload(payload, me); dispatch({ type: 'session-spawn-pending', channelId: payload.channelId, @@ -892,7 +809,7 @@ export function ChatProvider({ session, children }: { session: Session; children cacheReadCursorAdvance(payload.channelId, payload.lastReadEventId, 'queued read.mark'); } }, - [cacheReadCursorAdvance, pendingMessageFromSendPayload, pendingSpawnFromPayload], + [cacheReadCursorAdvance, me], ); useEffect(() => { @@ -1338,7 +1255,7 @@ export function ChatProvider({ session, children }: { session: Session; children ...(opts?.attachmentRefs?.length ? { attachmentRefs: opts.attachmentRefs } : {}), createdAt: now, }; - const pending = pendingSpawnFromPayload(payload); + const pending = pendingSpawnFromPayload(payload, me); void enqueueOp( { opId: randomId(), @@ -1359,7 +1276,7 @@ export function ChatProvider({ session, children }: { session: Session; children dispatch({ type: 'session-spawn-failed', channelId, tempId }); }); }, - [enqueueOp, onApiError, pendingSpawnFromPayload], + [enqueueOp, me, onApiError], ); // Zero-setup demo: spawns the scripted `demo` harness (streams a short diff --git a/surface/web/src/chatQueuedOverlays.ts b/surface/shared/src/chatQueuedOverlays.ts similarity index 91% rename from surface/web/src/chatQueuedOverlays.ts rename to surface/shared/src/chatQueuedOverlays.ts index 41e7ce782..5b43a0f56 100644 --- a/surface/web/src/chatQueuedOverlays.ts +++ b/surface/shared/src/chatQueuedOverlays.ts @@ -1,15 +1,7 @@ -import type { - AppAction, - AttachmentMeta, - ChatMessage, - MsgSendPayload, - OpType, - ReactionSetPayload, - SessionSpawnPayload, - UserRef, - VoiceMeta, -} from '@atrium/surface-client'; -import type { Session } from './sessions/types'; +import type { AppAction } from './appState'; +import type { MsgSendPayload, OpType, ReactionSetPayload, SessionSpawnPayload } from './opQueue'; +import type { Session } from './sessions'; +import type { AttachmentMeta, ChatMessage, UserRef, VoiceMeta } from './timeline'; export type VoiceMsgSendPayload = MsgSendPayload & { voice?: Pick; @@ -81,8 +73,11 @@ export function pendingSpawnFromPayload( title: payload.task.slice(0, 80), status: 'spawning', harness: payload.harness ?? 'codex', - repo: payload.repo ?? null, - branch: payload.branch ?? null, + repo: payload.repo ?? payload.repos?.[0]?.repo ?? null, + branch: payload.branch ?? payload.repos?.[0]?.ref ?? null, + repos: payload.repos ?? null, + githubIdentityMode: payload.githubIdentityMode ?? null, + agentProfileVersionId: payload.agentProfileVersionId ?? null, spawnedBy: me.id, spawnerName: me.displayName, driverId: null, diff --git a/surface/shared/src/index.ts b/surface/shared/src/index.ts index a8d1d2668..9f058fce3 100644 --- a/surface/shared/src/index.ts +++ b/surface/shared/src/index.ts @@ -14,6 +14,7 @@ export * from './activity'; export * from './calls'; export * from './sync'; export * from './opQueue'; +export * from './chatQueuedOverlays'; export { isNetworkFailure } from './api'; export * from './queueStatus'; export * from './cache'; diff --git a/surface/web/test/chatQueuedOverlays.test.ts b/surface/shared/test/chatQueuedOverlays.test.ts similarity index 82% rename from surface/web/test/chatQueuedOverlays.test.ts rename to surface/shared/test/chatQueuedOverlays.test.ts index d71418c10..ac921a78b 100644 --- a/surface/web/test/chatQueuedOverlays.test.ts +++ b/surface/shared/test/chatQueuedOverlays.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from 'vitest'; -import type { MsgSendPayload, OpType, ReactionSetPayload, SessionSpawnPayload, UserRef } from '@atrium/surface-client'; -import { pendingMessageFromSendPayload, pendingSpawnFromPayload, queuedOverlayAction } from '../src/chatQueuedOverlays'; +import { + pendingMessageFromSendPayload, + pendingSpawnFromPayload, + queuedOverlayAction, + type MsgSendPayload, + type OpType, + type ReactionSetPayload, + type SessionSpawnPayload, + type UserRef, +} from '../src/index'; const me: UserRef = { id: 'user-1', handle: 'me', displayName: 'Me User' }; @@ -97,6 +105,10 @@ describe('chatQueuedOverlays', () => { harness: 'codex', repo: 'useatrium/atrium', branch: 'feature/reporting', + repos: null, + githubIdentityMode: null, + agentProfileVersionId: null, + providerAuthRequired: null, spawnedBy: 'user-1', spawnerName: 'Me User', costUsd: 0, @@ -112,10 +124,33 @@ describe('chatQueuedOverlays', () => { author: me, status: 'pending', sessionId: 'pending-session-1', + sessionTask: 'Run the quarterly report and summarize anomalies', createdAt: '2026-06-28T14:00:00.000Z', }); }); + it('carries multi-repo spawn metadata and falls back to the first repo/ref', () => { + const payload: SessionSpawnPayload = { + channelId: 'ch-1', + task: 'Ship the migration', + clientSpawnId: 'pending-session-2', + repos: [{ repo: 'useatrium/atrium', ref: 'feature/multi' }, { repo: 'useatrium/centaur' }], + githubIdentityMode: 'app_installation', + agentProfileVersionId: 'apv-42', + createdAt: '2026-06-28T14:05:00.000Z', + }; + + const pending = pendingSpawnFromPayload(payload, me); + + expect(pending.session).toMatchObject({ + repo: 'useatrium/atrium', + branch: 'feature/multi', + repos: [{ repo: 'useatrium/atrium', ref: 'feature/multi' }, { repo: 'useatrium/centaur' }], + githubIdentityMode: 'app_installation', + agentProfileVersionId: 'apv-42', + }); + }); + it('maps queued overlay ops to reducer actions', () => { expect( queuedOverlayAction( diff --git a/surface/web/src/Chat.tsx b/surface/web/src/Chat.tsx index 18456b192..826ff6e58 100644 --- a/surface/web/src/Chat.tsx +++ b/surface/web/src/Chat.tsx @@ -83,7 +83,7 @@ import { createQueueLockProvider, queuedFailureMessage, } from './chatQueue'; -import { queuedOverlayAction } from './chatQueuedOverlays'; +import { queuedOverlayAction } from '@atrium/surface-client'; import { useChannelActions } from './useChannelActions'; import { useChatMessageActions } from './useChatMessageActions'; import { useDraftState } from './useDraftState'; diff --git a/surface/web/src/useChatMessageActions.ts b/surface/web/src/useChatMessageActions.ts index 4662e3b76..dcfcc8785 100644 --- a/surface/web/src/useChatMessageActions.ts +++ b/surface/web/src/useChatMessageActions.ts @@ -9,20 +9,20 @@ import type { EnqueueOpInput, MsgSendPayload, OpType, + QueuedThreadSteerPayload, ReactionSetPayload, SessionSpawnPayload, UserRef, + VoiceMsgSendPayload, } from '@atrium/surface-client'; -import { randomId } from '@atrium/surface-client'; -import { PENDING_SESSION_PREFIX } from './sessions/types'; -import type { SpawnConfig } from './sessions/SpawnDialog'; import { pendingMessageFromSendPayload, pendingMessageFromThreadSteerPayload, pendingSpawnFromPayload, - type QueuedThreadSteerPayload, - type VoiceMsgSendPayload, -} from './chatQueuedOverlays'; + randomId, +} from '@atrium/surface-client'; +import { PENDING_SESSION_PREFIX } from './sessions/types'; +import type { SpawnConfig } from './sessions/SpawnDialog'; import { showErrorToast } from './components/Toasts'; import type { AttachmentMeta } from '@atrium/surface-client'; diff --git a/surface/web/src/useSessionActions.ts b/surface/web/src/useSessionActions.ts index 201d8201d..13f4682a1 100644 --- a/surface/web/src/useSessionActions.ts +++ b/surface/web/src/useSessionActions.ts @@ -1,14 +1,15 @@ import { useCallback } from 'react'; import { + pendingMessageFromThreadSteerPayload, randomId, type AppAction, type AttachmentMeta, type AttachmentRef, type EnqueueOpInput, + type QueuedThreadSteerPayload, type SessionQuestionAnswers, type UserRef, } from '@atrium/surface-client'; -import { pendingMessageFromThreadSteerPayload, type QueuedThreadSteerPayload } from './chatQueuedOverlays'; type SessionActionType = | 'session.answer'