(
rawType: string,
payload: unknown,
sessionId: string,
meta?: ProjectMeta,
)
| 615 | } |
| 616 | |
| 617 | function _project( |
| 618 | rawType: string, |
| 619 | payload: unknown, |
| 620 | sessionId: string, |
| 621 | meta?: ProjectMeta, |
| 622 | ): AppEvent[] { |
| 623 | const s = getOrCreate(sessionId); |
| 624 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 625 | const p = payload as any; |
| 626 | const out: AppEvent[] = []; |
| 627 | |
| 628 | // Drop subagent-scoped transcript frames (see MAIN_AGENT_TRANSCRIPT_FRAMES). |
| 629 | // A subagent carries its own agentId; only the main agent's stream builds the |
| 630 | // visible transcript. Lifecycle frames (subagent.*, goal.*, background.*) are |
| 631 | // intentionally NOT in the set — they describe the subagent for the task view |
| 632 | // and must always be projected. |
| 633 | const frameAgentId: unknown = p?.agentId; |
| 634 | if (typeof frameAgentId === 'string' && frameAgentId !== MAIN_AGENT_ID) { |
| 635 | const isSideChannel = sideChannelAgents.has(frameAgentId); |
| 636 | // Side-channel agents (e.g. BTW side chat) stream text/thinking deltas and |
| 637 | // a turn boundary over the parent session channel. Route them to the web |
| 638 | // layer as agent-scoped events instead of dropping them or folding them |
| 639 | // into the parent transcript. |
| 640 | if (isSideChannel && (rawType === 'thinking.delta' || rawType === 'assistant.delta')) { |
| 641 | const deltaText: string = p?.delta ?? ''; |
| 642 | if (!deltaText) return []; |
| 643 | return [ |
| 644 | { |
| 645 | type: 'agentDelta' as const, |
| 646 | sessionId, |
| 647 | agentId: frameAgentId, |
| 648 | delta: { [rawType === 'thinking.delta' ? ('thinking' as const) : ('text' as const)]: deltaText }, |
| 649 | }, |
| 650 | ]; |
| 651 | } |
| 652 | if (isSideChannel && rawType === 'turn.ended') { |
| 653 | return [ |
| 654 | { type: 'agentTurnEnded' as const, sessionId, agentId: frameAgentId, reason: p?.reason }, |
| 655 | ]; |
| 656 | } |
| 657 | if (MAIN_AGENT_TRANSCRIPT_FRAMES.has(rawType)) { |
| 658 | return projectSubagentProgress(s, sessionId, frameAgentId, rawType, p ?? {}, sideChannelAgents); |
| 659 | } |
| 660 | } |
| 661 | |
| 662 | switch (rawType) { |
| 663 | // ----------------------------------------------------------------------- |
| 664 | case 'session.meta.updated': { |
| 665 | // The daemon auto-generates a title from the first prompt (and other |
| 666 | // clients can rename a session); it also reports the latest user prompt |
| 667 | // via patch.lastPrompt. It announces all of these via this event. We |
| 668 | // don't have the full AppSession here, so emit a lightweight |
| 669 | // sessionMetaUpdated that patches only the changed meta fields. |
| 670 | const title: string | undefined = p?.patch?.title ?? p?.title; |
| 671 | const lastPrompt: string | undefined = p?.patch?.lastPrompt; |
| 672 | const patch: { title?: string; lastPrompt?: string } = {}; |
| 673 | if (typeof title === 'string' && title.length > 0) patch.title = title; |
| 674 | if (typeof lastPrompt === 'string') patch.lastPrompt = lastPrompt; |
no test coverage detected