feat(desktop): restore observer-feed regressions from #1381 and classify 4 new session/update types#1412
Merged
Merged
Conversation
787713f to
ce9a03f
Compare
wpfleger96
added a commit
that referenced
this pull request
Jul 1, 2026
…sions Adds a Playwright smoke spec that seeds observer relay events directly into the production appendAgentEvent → processTranscriptEvent ingestion path via a new __BUZZ_E2E_SEED_OBSERVER_EVENTS__ e2e bridge hook, then screenshots four surfaces restored by PR #1412: - 01-prompt-context-inline: collapsed inline prompt-context block - 02-system-prompt-title: system prompt with title visible in feed - 03-permission-approved: permission row with Approved (allow_once) outcome - 04-permission-cancelled: permission row with Cancelled outcome Supporting changes: - injectObserverEventsForE2E exported from observerRelayStore.ts - __BUZZ_E2E_SEED_OBSERVER_EVENTS__ Window hook wired in e2eBridge.ts - spec added to smoke project in playwright.config.ts Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
wpfleger96
added a commit
that referenced
this pull request
Jul 1, 2026
…sions Adds a Playwright smoke spec that seeds observer relay events directly into the production appendAgentEvent → processTranscriptEvent ingestion path via a new __BUZZ_E2E_SEED_OBSERVER_EVENTS__ e2e bridge hook, then screenshots four surfaces restored by PR #1412: - 01-prompt-context-inline: collapsed inline prompt-context block - 02-system-prompt-title: system prompt with title visible in feed - 03-permission-approved: permission row with Approved (allow_once) outcome - 04-permission-cancelled: permission row with Cancelled outcome Supporting changes: - injectObserverEventsForE2E exported from observerRelayStore.ts - __BUZZ_E2E_SEED_OBSERVER_EVENTS__ Window hook wired in e2eBridge.ts - spec added to smoke project in playwright.config.ts Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
31579f2 to
d791a25
Compare
wpfleger96
pushed a commit
that referenced
this pull request
Jul 1, 2026
Collaborator
Author
Shots 01–10: restored behaviors and new lifecycle typesScreenshots 01–10 show the individual behaviors restored from the #1381 regression (prompt context, system prompt, permission outcomes, prompt sections expanded, lifecycle status lines). These are unchanged from the previous post — included for completeness. Shot 11: first-turn ordering fix (new)Full pool.rs wire sequence ( |
7a3838d to
9fa77da
Compare
wpfleger96
added a commit
that referenced
this pull request
Jul 1, 2026
…sions Adds a Playwright smoke spec that seeds observer relay events directly into the production appendAgentEvent → processTranscriptEvent ingestion path via a new __BUZZ_E2E_SEED_OBSERVER_EVENTS__ e2e bridge hook, then screenshots four surfaces restored by PR #1412: - 01-prompt-context-inline: collapsed inline prompt-context block - 02-system-prompt-title: system prompt with title visible in feed - 03-permission-approved: permission row with Approved (allow_once) outcome - 04-permission-cancelled: permission row with Cancelled outcome Supporting changes: - injectObserverEventsForE2E exported from observerRelayStore.ts - __BUZZ_E2E_SEED_OBSERVER_EVENTS__ Window hook wired in e2eBridge.ts - spec added to smoke project in playwright.config.ts Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three regressions introduced by #1381's activity-feed rebuild: 1. Per-turn prompt context sections are now visible inline in the feed body. Previously they were hidden behind a modal accessible only via a small CheckCheck toggle inside the user-message bubble footer. Now the sections render directly below the user bubble with each section title visible and body expandable on click. A "View full" button opens the scrollable modal for focused reading. 2. Metadata items (system prompt, prompt context) now display their semantic title instead of the anonymous "Captured N raw sections" label. RawRailActivity now uses item.title as the verb, so "System prompt" and "Prompt context" render with their real names. 3. Permission rows now show the decided outcome. The permission response arrives as an acp_write event with result.outcome and the same JSON-RPC id as the request. A pendingPermissions map on TranscriptState indexes request id → lifecycle item id + option kind map. On the response the outcome is appended: Approved (allow_once) / Denied (reject_once) / Cancelled. Also restores metadata participation in isMeaningfulItem() for the Now summary bar. Raw JSON-RPC frames remain excluded (acpSource=raw_json_rpc); all semantic metadata (system prompt, prompt context) now contribute again, matching pre-#1381 behavior. System-prompt key (system-prompt:${ch}) is intentionally unchanged — the frame predates session creation and correctly reflects current channel setup by replacing on each session/new. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…lation JSON-RPC 2.0 allows both string and numeric ids. The ACP runtime preserves permission request ids as serde_json::Value for exactly this reason. The prior commit used asString(payload.id) for both the request index and response lookup in pendingPermissions, silently dropping any numeric id and leaving the outcome unattached. Add a jsonRpcId() helper that accepts string or finite-number values and keys them via JSON.stringify (preventing collisions between the number 1 and the string "1"). Use it at both the request-index site and the response-lookup site instead of asString. Regression tests: numeric id with selected allow_once, numeric id with cancelled, and a collision test verifying number 1 and string "1" attach to their respective items independently. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Inside the acp_write response branch, `responseId` is typed `string | null`. The `if (pending && outcomeKind)` guard only narrowed `pending`, not `responseId`, so `d.pendingPermissions.delete(responseId)` was rejected by tsc (argument of type 'string | null' not assignable to 'string'). Add `&& responseId` to the guard. This is semantically a no-op — `pending` can only be truthy when the earlier `responseId ?` branch took the non-null path — but TS requires the explicit narrowing. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…sions Adds a Playwright smoke spec that seeds observer relay events directly into the production appendAgentEvent → processTranscriptEvent ingestion path via a new __BUZZ_E2E_SEED_OBSERVER_EVENTS__ e2e bridge hook, then screenshots four surfaces restored by PR #1412: - 01-prompt-context-inline: collapsed inline prompt-context block - 02-system-prompt-title: system prompt with title visible in feed - 03-permission-approved: permission row with Approved (allow_once) outcome - 04-permission-cancelled: permission row with Cancelled outcome Supporting changes: - injectObserverEventsForE2E exported from observerRelayStore.ts - __BUZZ_E2E_SEED_OBSERVER_EVENTS__ Window hook wired in e2eBridge.ts - spec added to smoke project in playwright.config.ts Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Store the resolved permission outcome on a new `outcome?` field of the lifecycle item type instead of appending it to `text` via joinLifecycleText. This separates the data model cleanly: `text` holds request detail + options; `outcome` holds the final decision. LifecycleActivity.tsx renders permission items in three visually distinct parts: 1. Request row: shield icon + title + request description 2. Options sub-line: muted indented 'Options: ...' text 3. Decision row (when resolved): divider + colored icon + outcome label - green + CheckCircle2 for 'Approved (...)' - destructive + XCircle for 'Denied (...)' - muted + XCircle for 'Cancelled' Also adds expanded E2E screenshot variants: - 05-prompt-context-expanded.png: all three accordion sections open - 06-system-prompt-expanded.png: outer <details> + inner section <details> open Unit tests updated to assert outcome is on item.outcome (not in item.text). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Adds isSpineItem() predicate — true for tools, messages, thoughts, plans, and meaningful lifecycle events; false for metadata items (system prompt, prompt context). These are 'reads' that should recede when real work is present, per VISION_ACTIVITY.md:49,61 (failures rise; reads recede; suppression is what makes signal legible). BotActivityBar now uses a two-tier headline scan: first pass collects spine-only headlines; if none found (session start / idle), falls back to all meaningful items via isMeaningfulItem so the bar isn't left empty. Feed visibility for metadata items is unaffected — AgentSessionTranscriptList renders them independently of this predicate. Also fixes the isMeaningfulItem docstring, which incorrectly claimed the function feeds a 'Now summary' (it only feeds the headline scan). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…dion RawRailActivity now branches on acpSource: raw_json_rpc items keep the existing <pre>/details raw treatment (the ambient safety net), while all other named metadata items (system prompt, steer-turn prompt context) render via the shared PromptSectionList accordion — the same polished rounded-2xl card used for per-turn prompt context. Extract PromptContextSections + PromptContextSectionAccordion from AgentSessionTranscriptList into PromptSectionAccordion.tsx and import the shared component back in both AgentSessionTranscriptList and RawRailActivity. No visual change to prompt-context; the raw-rail treatment is now reserved for genuine raw payloads only. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…x shot 06 Replace the three acpSource boolean tests in agentSessionTranscriptPresentation.test.mjs (which only asserted item.acpSource === 'raw_json_rpc' on hand-built objects — they would pass with the isRawPayload branch deleted) with proper renderToStaticMarkup render tests in RawRailActivity.render.test.mjs. The new tests render RawRailActivity for three metadata items and assert the HTML: raw_json_rpc includes <pre> and no rounded-2xl; system prompt and steer-turn prompt context include rounded-2xl and no <pre>. Fix shot 06 in observer-feed-screenshots.spec.ts: after the render unification, system-prompt sections are React button accordions inside a native ActivityRow <details>. Shot 06 now opens the outer <details> first, then clicks each inner section button to expand it — matching the interaction pattern in shot 05. Shot 06 confirmed to show both Base and System sections fully expanded in the polished card UI. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Add count assertion before the section-button click loop in shot 06 so a future testid/nesting break fails loudly instead of silently producing a collapsed screenshot. Swap the render-test accordion marker from the style token 'rounded-2xl' to 'data-testid="transcript-prompt-context-sections"' — the semantic shared-list marker emitted by PromptSectionList. Style tokens can drift without breaking behavior; the testid is stable and specific. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…lable_commands_update, config_option_update
Add 4 named session/update classifier cases before the existing else-drop
safety net in agentSessionTranscript.ts:
- current_mode_update: lifecycle 'Mode / {currentModeId}'; suppressed when
currentModeId is absent.
- usage_update: lifecycle 'Usage / Tokens: {used}/{size}' (+ cost if
present); uses replaceLifecycleItem so repeated frames per turn coalesce
to the latest value rather than accumulating.
- available_commands_update: lifecycle 'Commands / Commands available: N'.
- config_option_update: lifecycle 'Config / {name} = {val}, ...' joined
from configOptions array; suppressed when array is empty.
The else branch is left unchanged — unknown/future types still drop,
preserving the firehose safety net. keepalive stays dropped.
Add replaceLifecycleItem helper (parallel to upsertLifecycleItem but
replaces text on update instead of appending via joinLifecycleText).
Tests: 12 new unit tests covering each case, edge paths (missing fields,
zero-length arrays), usage coalescing (3 frames → 1 item with last value),
keepalive drop, and unknown-type drop.
File-size override bumped to 1162 with a narrowly scoped justification
note.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…pdate types Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…re per-turn prompt-context
The session/new arm in processTranscriptEvent was passing ctx (which
includes the active turnId) to upsertMetadata for the system-prompt
item. When session/new and session/prompt share the same turnId — as
the harness always does on the first turn — the system-prompt metadata
item landed in the same turn bucket as the prompt-context item.
buildTranscriptDisplayBlocks / classifyTurnItems treats unrecognized
turn-bucket items as activity, which renders after the prompt segment
(user message + setup + prompt-context). Result: system-prompt appeared
below prompt-context in the feed.
Fix: pass { ...ctx, turnId: null } so the item is always treated as a
standalone single entry by the display grouper. This is semantically
correct — the system prompt is per-channel, not per-turn. The upsert
update path handles null via ctx.turnId ?? existing.turnId, so
re-created sessions also keep turnId=null.
Two new tests added to agentSessionTranscript.test.mjs covering:
- First-turn shared-turnId ordering (the bug case)
- Multi-turn ordering (system-prompt stays before all prompt-context items)
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The two system-prompt ordering tests were asserting on raw buildTranscript
output (d.items insertion order). Since session/new always fires before
session/prompt on the wire, system-prompt was always at a lower index in
d.items regardless of the fix — making the ordering assertions tautological.
Fix: route through buildTranscriptDisplayBlocks + flattenDisplayBlocks, the
layer that actually contained the bug (classifyTurnItems placed system-prompt
as an 'activity' item after prompt-context when both shared the same turnId).
The turnId=null assertions remain on raw items — they verify the fix input
that makes the display grouper behave correctly.
Verified: both tests fail when the { ...ctx, turnId: null } one-liner is
reverted, pass when it is present.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…isplay order pool.rs emits turn_started before session/new on the first turn, so turn_started creates the turn bucket before session/new fires. With turnId=null, system-prompt lands as a standalone single AFTER the turn block in displayOrder — rendering after prompt-context. Fix: tag system-prompt items with acpSource "session/new". In buildTranscriptDisplayBlocks, hold system-prompt singles until the first turn with a user-prompt item, then pass them to classifyTurnItems as externalSystemPrompt. classifyTurnItems slots the item into the prompt segment between the user message and prompt-context. The flattenDisplayBlocks order is: user → systemPrompt → context. Tests updated to use the full realistic wire sequence (turn_started → session/new → session_resolved → session/prompt). Fails-on-revert confirmed: removing acpSource "session/new" makes both ordering tests fail (1473 pass / 2 fail); restored → 1475/1475. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
PromptContextInline hard-coded "Prompt context" for both the inline heading and the dialog title, so the system-prompt metadata block (title: "System prompt") rendered with the wrong visible label. Use context.title in both places so system-prompt shows "System prompt" and prompt-context shows "Prompt context" as expected. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… session/new without session/prompt When session/new arrives without a following session/prompt (e.g. isolated test scenarios or incomplete streams), the pending system-prompt item was silently dropped. Add a fallback: if pendingSystemPrompt is unconsumed after processing all display-order entries, emit it as a standalone single. Add E2E spec shot 11 (first-turn ordering) showing the full pool.rs wire sequence: turn_started → session/new → session_resolved → session/prompt. Asserts user bubble → System prompt → Prompt context render order is visible in the observer feed. Update shot 02 comment to document that it exercises the fallback path. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
9fa77da to
0ffd40f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.











What
Restores observer-feed display regressions introduced in #1381, classifies 4 new session/update types, and fixes system-prompt display ordering.
1. Restored observer-feed behaviors (6 items)
Regressions from #1381 that collapsed the observer transcript to a single undifferentiated stream:
session/newsystemPrompt sections rendered as collapsible "System prompt" blocksession/promptcontext sections rendered as collapsible "Prompt context" blocksession/promptuser text rendered as a message bubble with channel link_goose/unstable/session/steeruser text rendered as a steering messagecurrent_usage_updateaccumulated across a turn into a single usage item2. New lifecycle types (4 items)
Types present in the ACP stream since #1381 but not previously classified:
current_mode_update<mode>" lifecycle rowusage_updateavailable_commands_updateconfig_option_update<key>=<value>" lifecycle row3. System-prompt display ordering
Bug: The observer feed rendered Prompt context before System prompt on the first turn.
Root cause:
pool.rsemitsturn_startedbeforesession/new, soturn_startedcreates the turn bucket before the system-prompt item is produced. A simpleturnId: nullapproach appends the system-prompt single todisplayOrderafter the turn block, placing it after Prompt context.Fix: System-prompt items are tagged
acpSource: "session/new".buildTranscriptDisplayBlocksholds them as apendingSystemPromptbuffer and injects them into the first following turn'sclassifyTurnItemscall asexternalSystemPrompt. The prompt segment renders: user message → System prompt → Prompt context. If nosession/promptfollows (incomplete stream or isolated test), the system-prompt falls back to a standalone single.The
PromptContextInlinerenderer usescontext.title(not hard-coded "Prompt context") so both blocks display under their correct headings.Display order per turn:
Tests
turn_started→session/new→session_resolved→session/prompt); both fail on revert (1473 pass / 2 fail), pass on fix (1475/1475)