diff --git a/apps/doc-approval/web-react/src/App.tsx b/apps/doc-approval/web-react/src/App.tsx index 0b80ca1..e263452 100644 --- a/apps/doc-approval/web-react/src/App.tsx +++ b/apps/doc-approval/web-react/src/App.tsx @@ -9,11 +9,19 @@ import { submitDocument, type HostRunResult, type RuntimeStatus, + type SessionPresentation, type TraverseEmbedderApi, } from './host/embeddedHost' const DOCUMENT_MAX_LENGTH = 10000 +const IDLE_PRESENTATION: SessionPresentation = { + presentationState: 'idle', + presentationError: null, + capabilityProgress: [], + activeCapabilityId: null, +} + export interface AppProps { embedder?: TraverseEmbedderApi | null } @@ -25,6 +33,7 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const [prodEmbedder, setProdEmbedder] = useState(null) const [submitting, setSubmitting] = useState(false) const [result, setResult] = useState(null) + const [livePresentation, setLivePresentation] = useState(null) const embedder = injected ? injectedEmbedder : prodEmbedder const runtimeStatus: RuntimeStatus = injected @@ -55,8 +64,13 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { if (!canSubmit || !embedder) return setSubmitting(true) setResult(null) + setLivePresentation(IDLE_PRESENTATION) try { - setResult(submitDocument(embedder, document)) + setResult( + submitDocument(embedder, document, (presentation) => { + setLivePresentation(presentation) + }), + ) } finally { setSubmitting(false) } @@ -66,9 +80,17 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const handleReset = useCallback(() => { setResult(null) + setLivePresentation(null) setDocument('') }, []) + const presentationState = + livePresentation?.presentationState ?? result?.presentationState ?? null + const activeCapabilityId = + livePresentation?.activeCapabilityId ?? result?.activeCapabilityId ?? null + const capabilityProgress = + livePresentation?.capabilityProgress ?? result?.capabilityProgress ?? [] + return (
@@ -95,9 +117,9 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { workspace={DEFAULT_WORKSPACE} workflowId={DEFAULT_WORKFLOW_ID} status={runtimeStatus} - presentationState={result?.presentationState ?? null} - activeCapabilityId={result?.activeCapabilityId ?? null} - capabilityProgress={result?.capabilityProgress ?? []} + presentationState={presentationState} + activeCapabilityId={activeCapabilityId} + capabilityProgress={capabilityProgress} /> { expect(result.capabilityProgress.length).toBeGreaterThan(0) }) + it('submitDocument notifies presentation after each subscribed event', () => { + const embedder = createTestEmbedder(sampleOutput) + const states: string[] = [] + const result = submitDocument(embedder, 'Invoice for Acme Corp', (presentation) => { + states.push(presentation.presentationState) + }) + expect(states).toContain('loading') + expect(states.at(-1)).toBe('loaded') + expect(result.presentationState).toBe('loaded') + }) + it('submitDocument surfaces scripted execution errors', () => { const embedder = new EmbedderTestDouble({ appId: 'doc-approval', diff --git a/apps/doc-approval/web-react/src/host/embeddedHost.ts b/apps/doc-approval/web-react/src/host/embeddedHost.ts index 14be111..e1c9970 100644 --- a/apps/doc-approval/web-react/src/host/embeddedHost.ts +++ b/apps/doc-approval/web-react/src/host/embeddedHost.ts @@ -1,12 +1,7 @@ -import type { - CapabilityProgressStep, - EmbedderEventLike, - PresentationState, -} from 'event-ui-conformance' +import type { EmbedderEventLike, PresentationState, SessionPresentation } from 'event-ui-conformance' import { - activeCapabilityId, - mapCapabilityProgress, - mapPresentationState, + mapSessionPresentation, + observeSessionPresentation as observeSessionPresentationFromPackage, } from 'event-ui-conformance' import type { EmbedderEvent, @@ -41,12 +36,13 @@ export interface HostRunResult { /** Spec 001 error text from event payloads (never invented). */ presentationError: string | null /** Spec 002 ordered capability invoke/result progress. */ - capabilityProgress: CapabilityProgressStep[] + capabilityProgress: SessionPresentation['capabilityProgress'] /** Spec 002 active capability id when an invoke is still open. */ activeCapabilityId: string | null } -export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, CapabilityProgressStep } +export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, SessionPresentation } +export type { CapabilityProgressStep } from 'event-ui-conformance' export function createTestEmbedder(output: DocApprovalOutput): TraverseEmbedderApi { return new EmbedderTestDouble({ @@ -84,13 +80,45 @@ function errorMessageFromData(data: JsonValue): string | null { return null } -function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { - return events.map((event) => ({ +function toEventLike(event: EmbedderEvent): EmbedderEventLike { + return { event_type: event.event_type, sequence: event.sequence, session_id: event.session_id, data: event.data, - })) + } +} + +function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { + return events.map(toEventLike) +} + +/** Map public embedder events to Spec 001/002 UI fields (shared package). */ +export function mapEmbedderSessionPresentation( + events: readonly EmbedderEvent[], + options?: { fallbackError?: string | null }, +): SessionPresentation { + return mapSessionPresentation(toEventLikes(events), options) +} + +/** + * Subscribe via the public embedder API and map each event with the shared + * Spec 001/002 helpers. Prefer a fresh subscribe per run. + */ +export function observeSessionPresentation( + host: TraverseEmbedderApi, + onChange: (presentation: SessionPresentation) => void, +): void { + observeSessionPresentationFromPackage( + { + subscribe(listener) { + host.subscribe((event) => { + listener(toEventLike(event)) + }) + }, + }, + onChange, + ) } function withPresentation( @@ -100,29 +128,27 @@ function withPresentation( >, collected: readonly EmbedderEvent[], ): HostRunResult { - const likes = toEventLikes(collected) - const snap = mapPresentationState(likes) - const presentationState: PresentationState = - base.error && snap.state === 'idle' ? 'error' : snap.state return { ...base, - presentationState, - presentationError: - snap.errorMessage ?? (base.error && snap.state === 'idle' ? base.error : null), - capabilityProgress: mapCapabilityProgress(likes), - activeCapabilityId: activeCapabilityId(likes), + ...mapEmbedderSessionPresentation(collected, { fallbackError: base.error }), } } -export function submitDocument(embedder: TraverseEmbedderApi, document: string): HostRunResult { +/** Submit `{ document }` to `doc-approval.pipeline` and collect terminal output. */ +export function submitDocument( + embedder: TraverseEmbedderApi, + document: string, + onPresentation?: (presentation: SessionPresentation) => void, +): HostRunResult { const collected: EmbedderEvent[] = [] embedder.subscribe((event) => { collected.push(event) + onPresentation?.(mapEmbedderSessionPresentation(collected)) }) const outcome = embedder.submit(DEFAULT_WORKFLOW_ID, { document }) if (outcome.status === 'rejected') { - return withPresentation( + const rejected = withPresentation( { sessionId: outcome.sessionId ?? 'sess-unknown', output: null, @@ -134,6 +160,13 @@ export function submitDocument(embedder: TraverseEmbedderApi, document: string): }, [], ) + onPresentation?.({ + presentationState: rejected.presentationState, + presentationError: rejected.presentationError, + capabilityProgress: rejected.capabilityProgress, + activeCapabilityId: rejected.activeCapabilityId, + }) + return rejected } const sessionId = outcome.sessionId ?? 'sess-unknown' diff --git a/apps/loop/web-react/src/App.tsx b/apps/loop/web-react/src/App.tsx index 570d16a..9f72396 100644 --- a/apps/loop/web-react/src/App.tsx +++ b/apps/loop/web-react/src/App.tsx @@ -10,11 +10,19 @@ import { submitTranscript, type HostRunResult, type RuntimeStatus, + type SessionPresentation, type TraverseEmbedderApi, } from './host/embeddedHost' const TRANSCRIPT_MAX_LENGTH = 5000 +const IDLE_PRESENTATION: SessionPresentation = { + presentationState: 'idle', + presentationError: null, + capabilityProgress: [], + activeCapabilityId: null, +} + export interface AppProps { embedder?: TraverseEmbedderApi | null } @@ -26,6 +34,7 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const [prodEmbedder, setProdEmbedder] = useState(null) const [submitting, setSubmitting] = useState(false) const [result, setResult] = useState(null) + const [livePresentation, setLivePresentation] = useState(null) const embedder = injected ? injectedEmbedder : prodEmbedder const runtimeStatus: RuntimeStatus = injected @@ -56,8 +65,13 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { if (!canSubmit || !embedder) return setSubmitting(true) setResult(null) + setLivePresentation(IDLE_PRESENTATION) try { - setResult(submitTranscript(embedder, transcript)) + setResult( + submitTranscript(embedder, transcript, (presentation) => { + setLivePresentation(presentation) + }), + ) } finally { setSubmitting(false) } @@ -67,9 +81,17 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const handleReset = useCallback(() => { setResult(null) + setLivePresentation(null) setTranscript('') }, []) + const presentationState = + livePresentation?.presentationState ?? result?.presentationState ?? null + const activeCapabilityId = + livePresentation?.activeCapabilityId ?? result?.activeCapabilityId ?? null + const capabilityProgress = + livePresentation?.capabilityProgress ?? result?.capabilityProgress ?? [] + return (
@@ -96,9 +118,9 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { workspace={DEFAULT_WORKSPACE} workflowId={DEFAULT_WORKFLOW_ID} status={runtimeStatus} - presentationState={result?.presentationState ?? null} - activeCapabilityId={result?.activeCapabilityId ?? null} - capabilityProgress={result?.capabilityProgress ?? []} + presentationState={presentationState} + activeCapabilityId={activeCapabilityId} + capabilityProgress={capabilityProgress} /> { // Accept resumes via a follow-up submit of the runtime candidate payload. if (!embedder || !result?.rawOutput) return + setLivePresentation(IDLE_PRESENTATION) setResult( submitTranscript( embedder, typeof result.rawOutput === 'object' ? JSON.stringify({ decision: 'accept', candidates: result.rawOutput }) : String(result.rawOutput), + (presentation) => { + setLivePresentation(presentation) + }, ), ) }} diff --git a/apps/loop/web-react/src/host/embeddedHost.test.ts b/apps/loop/web-react/src/host/embeddedHost.test.ts index 466347b..9d4e266 100644 --- a/apps/loop/web-react/src/host/embeddedHost.test.ts +++ b/apps/loop/web-react/src/host/embeddedHost.test.ts @@ -49,6 +49,21 @@ describe('embeddedHost', () => { expect(result.capabilityProgress.length).toBeGreaterThan(0) }) + it('submitTranscript notifies presentation after each subscribed event', () => { + const embedder = createTestEmbedder(sampleOutput) + const states: string[] = [] + const result = submitTranscript( + embedder, + 'Alex will send the follow-up email.', + (presentation) => { + states.push(presentation.presentationState) + }, + ) + expect(states).toContain('loading') + expect(states.at(-1)).toBe('loaded') + expect(result.presentationState).toBe('loaded') + }) + it('submitTranscript surfaces scripted execution errors', () => { const embedder = new EmbedderTestDouble({ appId: 'loop', diff --git a/apps/loop/web-react/src/host/embeddedHost.ts b/apps/loop/web-react/src/host/embeddedHost.ts index 611dc57..56d03e5 100644 --- a/apps/loop/web-react/src/host/embeddedHost.ts +++ b/apps/loop/web-react/src/host/embeddedHost.ts @@ -1,12 +1,7 @@ -import type { - CapabilityProgressStep, - EmbedderEventLike, - PresentationState, -} from 'event-ui-conformance' +import type { EmbedderEventLike, PresentationState, SessionPresentation } from 'event-ui-conformance' import { - activeCapabilityId, - mapCapabilityProgress, - mapPresentationState, + mapSessionPresentation, + observeSessionPresentation as observeSessionPresentationFromPackage, } from 'event-ui-conformance' import type { EmbedderEvent, @@ -41,12 +36,13 @@ export interface HostRunResult { /** Spec 001 error text from event payloads (never invented). */ presentationError: string | null /** Spec 002 ordered capability invoke/result progress. */ - capabilityProgress: CapabilityProgressStep[] + capabilityProgress: SessionPresentation['capabilityProgress'] /** Spec 002 active capability id when an invoke is still open. */ activeCapabilityId: string | null } -export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, CapabilityProgressStep } +export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, SessionPresentation } +export type { CapabilityProgressStep } from 'event-ui-conformance' export function createTestEmbedder(output: LoopOutput): TraverseEmbedderApi { return new EmbedderTestDouble({ @@ -84,13 +80,45 @@ function errorMessageFromData(data: JsonValue): string | null { return null } -function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { - return events.map((event) => ({ +function toEventLike(event: EmbedderEvent): EmbedderEventLike { + return { event_type: event.event_type, sequence: event.sequence, session_id: event.session_id, data: event.data, - })) + } +} + +function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { + return events.map(toEventLike) +} + +/** Map public embedder events to Spec 001/002 UI fields (shared package). */ +export function mapEmbedderSessionPresentation( + events: readonly EmbedderEvent[], + options?: { fallbackError?: string | null }, +): SessionPresentation { + return mapSessionPresentation(toEventLikes(events), options) +} + +/** + * Subscribe via the public embedder API and map each event with the shared + * Spec 001/002 helpers. Prefer a fresh subscribe per run. + */ +export function observeSessionPresentation( + host: TraverseEmbedderApi, + onChange: (presentation: SessionPresentation) => void, +): void { + observeSessionPresentationFromPackage( + { + subscribe(listener) { + host.subscribe((event) => { + listener(toEventLike(event)) + }) + }, + }, + onChange, + ) } function withPresentation( @@ -100,29 +128,27 @@ function withPresentation( >, collected: readonly EmbedderEvent[], ): HostRunResult { - const likes = toEventLikes(collected) - const snap = mapPresentationState(likes) - const presentationState: PresentationState = - base.error && snap.state === 'idle' ? 'error' : snap.state return { ...base, - presentationState, - presentationError: - snap.errorMessage ?? (base.error && snap.state === 'idle' ? base.error : null), - capabilityProgress: mapCapabilityProgress(likes), - activeCapabilityId: activeCapabilityId(likes), + ...mapEmbedderSessionPresentation(collected, { fallbackError: base.error }), } } -export function submitTranscript(embedder: TraverseEmbedderApi, transcript: string): HostRunResult { +/** Submit `{ transcript }` to `loop.wf1` and collect terminal output. */ +export function submitTranscript( + embedder: TraverseEmbedderApi, + transcript: string, + onPresentation?: (presentation: SessionPresentation) => void, +): HostRunResult { const collected: EmbedderEvent[] = [] embedder.subscribe((event) => { collected.push(event) + onPresentation?.(mapEmbedderSessionPresentation(collected)) }) const outcome = embedder.submit(DEFAULT_WORKFLOW_ID, { transcript }) if (outcome.status === 'rejected') { - return withPresentation( + const rejected = withPresentation( { sessionId: outcome.sessionId ?? 'sess-unknown', output: null, @@ -134,6 +160,13 @@ export function submitTranscript(embedder: TraverseEmbedderApi, transcript: stri }, [], ) + onPresentation?.({ + presentationState: rejected.presentationState, + presentationError: rejected.presentationError, + capabilityProgress: rejected.capabilityProgress, + activeCapabilityId: rejected.activeCapabilityId, + }) + return rejected } const sessionId = outcome.sessionId ?? 'sess-unknown' diff --git a/apps/meeting-notes/web-react/src/App.tsx b/apps/meeting-notes/web-react/src/App.tsx index 5b6ea57..4115b2b 100644 --- a/apps/meeting-notes/web-react/src/App.tsx +++ b/apps/meeting-notes/web-react/src/App.tsx @@ -9,11 +9,19 @@ import { submitTranscript, type HostRunResult, type RuntimeStatus, + type SessionPresentation, type TraverseEmbedderApi, } from './host/embeddedHost' const TRANSCRIPT_MAX_LENGTH = 5000 +const IDLE_PRESENTATION: SessionPresentation = { + presentationState: 'idle', + presentationError: null, + capabilityProgress: [], + activeCapabilityId: null, +} + export interface AppProps { embedder?: TraverseEmbedderApi | null } @@ -25,6 +33,7 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const [prodEmbedder, setProdEmbedder] = useState(null) const [submitting, setSubmitting] = useState(false) const [result, setResult] = useState(null) + const [livePresentation, setLivePresentation] = useState(null) const embedder = injected ? injectedEmbedder : prodEmbedder const runtimeStatus: RuntimeStatus = injected @@ -55,8 +64,13 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { if (!canSubmit || !embedder) return setSubmitting(true) setResult(null) + setLivePresentation(IDLE_PRESENTATION) try { - setResult(submitTranscript(embedder, transcript)) + setResult( + submitTranscript(embedder, transcript, (presentation) => { + setLivePresentation(presentation) + }), + ) } finally { setSubmitting(false) } @@ -66,9 +80,17 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { const handleReset = useCallback(() => { setResult(null) + setLivePresentation(null) setTranscript('') }, []) + const presentationState = + livePresentation?.presentationState ?? result?.presentationState ?? null + const activeCapabilityId = + livePresentation?.activeCapabilityId ?? result?.activeCapabilityId ?? null + const capabilityProgress = + livePresentation?.capabilityProgress ?? result?.capabilityProgress ?? [] + return (
@@ -95,9 +117,9 @@ function App({ embedder: injectedEmbedder }: AppProps = {}) { workspace={DEFAULT_WORKSPACE} workflowId={DEFAULT_WORKFLOW_ID} status={runtimeStatus} - presentationState={result?.presentationState ?? null} - activeCapabilityId={result?.activeCapabilityId ?? null} - capabilityProgress={result?.capabilityProgress ?? []} + presentationState={presentationState} + activeCapabilityId={activeCapabilityId} + capabilityProgress={capabilityProgress} /> { expect(result.capabilityProgress.length).toBeGreaterThan(0) }) + it('submitTranscript notifies presentation after each subscribed event', () => { + const embedder = createTestEmbedder(sampleOutput) + const states: string[] = [] + const result = submitTranscript( + embedder, + 'Alex will send the follow-up email.', + (presentation) => { + states.push(presentation.presentationState) + }, + ) + expect(states).toContain('loading') + expect(states.at(-1)).toBe('loaded') + expect(result.presentationState).toBe('loaded') + }) + it('submitTranscript surfaces scripted execution errors', () => { const embedder = new EmbedderTestDouble({ appId: 'meeting-notes', diff --git a/apps/meeting-notes/web-react/src/host/embeddedHost.ts b/apps/meeting-notes/web-react/src/host/embeddedHost.ts index 23d7739..25e5e5e 100644 --- a/apps/meeting-notes/web-react/src/host/embeddedHost.ts +++ b/apps/meeting-notes/web-react/src/host/embeddedHost.ts @@ -1,12 +1,7 @@ -import type { - CapabilityProgressStep, - EmbedderEventLike, - PresentationState, -} from 'event-ui-conformance' +import type { EmbedderEventLike, PresentationState, SessionPresentation } from 'event-ui-conformance' import { - activeCapabilityId, - mapCapabilityProgress, - mapPresentationState, + mapSessionPresentation, + observeSessionPresentation as observeSessionPresentationFromPackage, } from 'event-ui-conformance' import type { EmbedderEvent, @@ -41,12 +36,13 @@ export interface HostRunResult { /** Spec 001 error text from event payloads (never invented). */ presentationError: string | null /** Spec 002 ordered capability invoke/result progress. */ - capabilityProgress: CapabilityProgressStep[] + capabilityProgress: SessionPresentation['capabilityProgress'] /** Spec 002 active capability id when an invoke is still open. */ activeCapabilityId: string | null } -export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, CapabilityProgressStep } +export type { TraverseEmbedderApi, EmbedderEvent, PresentationState, SessionPresentation } +export type { CapabilityProgressStep } from 'event-ui-conformance' export function createTestEmbedder(output: MeetingNotesOutput): TraverseEmbedderApi { return new EmbedderTestDouble({ @@ -84,13 +80,45 @@ function errorMessageFromData(data: JsonValue): string | null { return null } -function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { - return events.map((event) => ({ +function toEventLike(event: EmbedderEvent): EmbedderEventLike { + return { event_type: event.event_type, sequence: event.sequence, session_id: event.session_id, data: event.data, - })) + } +} + +function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { + return events.map(toEventLike) +} + +/** Map public embedder events to Spec 001/002 UI fields (shared package). */ +export function mapEmbedderSessionPresentation( + events: readonly EmbedderEvent[], + options?: { fallbackError?: string | null }, +): SessionPresentation { + return mapSessionPresentation(toEventLikes(events), options) +} + +/** + * Subscribe via the public embedder API and map each event with the shared + * Spec 001/002 helpers. Prefer a fresh subscribe per run. + */ +export function observeSessionPresentation( + host: TraverseEmbedderApi, + onChange: (presentation: SessionPresentation) => void, +): void { + observeSessionPresentationFromPackage( + { + subscribe(listener) { + host.subscribe((event) => { + listener(toEventLike(event)) + }) + }, + }, + onChange, + ) } function withPresentation( @@ -100,29 +128,27 @@ function withPresentation( >, collected: readonly EmbedderEvent[], ): HostRunResult { - const likes = toEventLikes(collected) - const snap = mapPresentationState(likes) - const presentationState: PresentationState = - base.error && snap.state === 'idle' ? 'error' : snap.state return { ...base, - presentationState, - presentationError: - snap.errorMessage ?? (base.error && snap.state === 'idle' ? base.error : null), - capabilityProgress: mapCapabilityProgress(likes), - activeCapabilityId: activeCapabilityId(likes), + ...mapEmbedderSessionPresentation(collected, { fallbackError: base.error }), } } -export function submitTranscript(embedder: TraverseEmbedderApi, transcript: string): HostRunResult { +/** Submit `{ transcript }` to `meeting-notes.process` and collect terminal output. */ +export function submitTranscript( + embedder: TraverseEmbedderApi, + transcript: string, + onPresentation?: (presentation: SessionPresentation) => void, +): HostRunResult { const collected: EmbedderEvent[] = [] embedder.subscribe((event) => { collected.push(event) + onPresentation?.(mapEmbedderSessionPresentation(collected)) }) const outcome = embedder.submit(DEFAULT_WORKFLOW_ID, { transcript }) if (outcome.status === 'rejected') { - return withPresentation( + const rejected = withPresentation( { sessionId: outcome.sessionId ?? 'sess-unknown', output: null, @@ -134,6 +160,13 @@ export function submitTranscript(embedder: TraverseEmbedderApi, transcript: stri }, [], ) + onPresentation?.({ + presentationState: rejected.presentationState, + presentationError: rejected.presentationError, + capabilityProgress: rejected.capabilityProgress, + activeCapabilityId: rejected.activeCapabilityId, + }) + return rejected } const sessionId = outcome.sessionId ?? 'sess-unknown' diff --git a/apps/trace-explorer/web-react/src/host/sessionPresentation.test.ts b/apps/trace-explorer/web-react/src/host/sessionPresentation.test.ts index 6084184..be946d7 100644 --- a/apps/trace-explorer/web-react/src/host/sessionPresentation.test.ts +++ b/apps/trace-explorer/web-react/src/host/sessionPresentation.test.ts @@ -42,6 +42,7 @@ describe('observeSessionPresentation', () => { }) expect(states.at(-1)).toBe('idle') host.submit('fixture.process', { note: 'n' }) + expect(states).toContain('loading') expect(states.at(-1)).toBe('loaded') }) }) diff --git a/apps/trace-explorer/web-react/src/host/sessionPresentation.ts b/apps/trace-explorer/web-react/src/host/sessionPresentation.ts index b6ed606..fcb0bed 100644 --- a/apps/trace-explorer/web-react/src/host/sessionPresentation.ts +++ b/apps/trace-explorer/web-react/src/host/sessionPresentation.ts @@ -1,43 +1,30 @@ -import type { - CapabilityProgressStep, - EmbedderEventLike, - PresentationState, -} from 'event-ui-conformance' +import type { EmbedderEventLike, SessionPresentation } from 'event-ui-conformance' import { - activeCapabilityId, - mapCapabilityProgress, - mapPresentationState, + mapSessionPresentation as mapSessionPresentationFromPackage, + observeSessionPresentation as observeSessionPresentationFromPackage, } from 'event-ui-conformance' import type { EmbedderEvent, TraverseEmbedderApi } from 'traverse-embedder-web' -export type SessionPresentation = { - presentationState: PresentationState - presentationError: string | null - capabilityProgress: CapabilityProgressStep[] - activeCapabilityId: string | null -} +export type { SessionPresentation } -function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { - return events.map((event) => ({ +function toEventLike(event: EmbedderEvent): EmbedderEventLike { + return { event_type: event.event_type, sequence: event.sequence, session_id: event.session_id, data: event.data, - })) + } +} + +function toEventLikes(events: readonly EmbedderEvent[]): EmbedderEventLike[] { + return events.map(toEventLike) } /** Map an ordered public embedder event stream to Spec 001/002 UI fields. */ export function mapSessionPresentation( events: readonly EmbedderEvent[], ): SessionPresentation { - const likes = toEventLikes(events) - const snap = mapPresentationState(likes) - return { - presentationState: snap.state, - presentationError: snap.errorMessage, - capabilityProgress: mapCapabilityProgress(likes), - activeCapabilityId: activeCapabilityId(likes), - } + return mapSessionPresentationFromPackage(toEventLikes(events)) } /** @@ -49,10 +36,14 @@ export function observeSessionPresentation( host: TraverseEmbedderApi, onChange: (presentation: SessionPresentation) => void, ): void { - const collected: EmbedderEvent[] = [] - host.subscribe((event) => { - collected.push(event) - onChange(mapSessionPresentation(collected)) - }) - onChange(mapSessionPresentation(collected)) + observeSessionPresentationFromPackage( + { + subscribe(listener) { + host.subscribe((event) => { + listener(toEventLike(event)) + }) + }, + }, + onChange, + ) } diff --git a/docs/kit-runner-persona.md b/docs/kit-runner-persona.md index c153c87..b3f2c3d 100644 --- a/docs/kit-runner-persona.md +++ b/docs/kit-runner-persona.md @@ -71,7 +71,7 @@ Local npm gates always. Manifest / `registry_ref` / runbook probes always. `TRAV | Registry MCP is not an OS-shell path | `llm-mcp-mode-a-spec119-scaffold` Done (fail-closed); live kit execute `llm-mcp-traverse-starter-catalog` Blocked | | Creating a **new** app id from CLI + this kit | [`new-app-author.md`](new-app-author.md) (`new-app-author-e2e`) | | `onboarding_check.sh` is not a merge-blocking CI gate | By design (slow `npm install`); `embedded_smoke` is the PR gate | -| Live Spec 001 subscribe on non-starter primary webs | `web-session-presentation-port-primary` (Blocked on shared helper) | +| Live Spec 001 subscribe on non-starter primary webs | Done via `web-session-presentation-port-primary` | ## File bugs