Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 7 additions & 90 deletions surface/mobile/src/lib/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
isNetworkFailure,
looksLikeSummonSigil,
parseSummonSigil,
pendingMessageFromSendPayload,
pendingSpawnFromPayload,
PENDING_SESSION_PREFIX,
randomId,
reconcileDraftSnapshot,
Expand Down Expand Up @@ -732,105 +734,20 @@ 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') {
const payload = op.payload as MobileMsgSendPayload;
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,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VoiceMeta, 'fileId' | 'durationMs' | 'waveform'>;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions surface/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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' };

Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion surface/web/src/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
12 changes: 6 additions & 6 deletions surface/web/src/useChatMessageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 2 additions & 1 deletion surface/web/src/useSessionActions.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Loading