Skip to content

feat(desktop): restore observer-feed regressions from #1381 and classify 4 new session/update types#1412

Merged
wpfleger96 merged 16 commits into
mainfrom
duncan/observer-feed-1381-restore
Jul 1, 2026
Merged

feat(desktop): restore observer-feed regressions from #1381 and classify 4 new session/update types#1412
wpfleger96 merged 16 commits into
mainfrom
duncan/observer-feed-1381-restore

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

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:

  • System promptsession/new systemPrompt sections rendered as collapsible "System prompt" block
  • Prompt contextsession/prompt context sections rendered as collapsible "Prompt context" block
  • User messagesession/prompt user text rendered as a message bubble with channel link
  • Steering_goose/unstable/session/steer user text rendered as a steering message
  • Tool results — tool results correctly parsed and attached to their call items
  • Usage coalescingcurrent_usage_update accumulated across a turn into a single usage item

2. New lifecycle types (4 items)

Types present in the ACP stream since #1381 but not previously classified:

ACP type Displayed as
current_mode_update "Mode: <mode>" lifecycle row
usage_update Usage lifecycle row (coalesces within turn)
available_commands_update "Commands available" lifecycle row
config_option_update "Config: <key> = <value>" lifecycle row

3. System-prompt display ordering

Bug: The observer feed rendered Prompt context before System prompt on the first turn.

Root cause: pool.rs emits turn_started before session/new, so turn_started creates the turn bucket before the system-prompt item is produced. A simple turnId: null approach appends the system-prompt single to displayOrder after the turn block, placing it after Prompt context.

Fix: System-prompt items are tagged acpSource: "session/new". buildTranscriptDisplayBlocks holds them as a pendingSystemPrompt buffer and injects them into the first following turn's classifyTurnItems call as externalSystemPrompt. The prompt segment renders: user message → System prompt → Prompt context. If no session/prompt follows (incomplete stream or isolated test), the system-prompt falls back to a standalone single.

The PromptContextInline renderer uses context.title (not hard-coded "Prompt context") so both blocks display under their correct headings.

Display order per turn:

<user message bubble>
<System prompt sections>   ← from session/new, heading: "System prompt"
<Prompt context sections>  ← from session/prompt, heading: "Prompt context"

Tests

  • 11 E2E screenshot specs (shots 01–10: individual restored behaviors; shot 11: full first-turn ordering with the real pool.rs wire sequence)
  • 2 ordering regression tests using the full realistic wire sequence (turn_startedsession/newsession_resolvedsession/prompt); both fail on revert (1473 pass / 2 fail), pass on fix (1475/1475)
  • All existing grouping unit tests pass

@wpfleger96 wpfleger96 force-pushed the duncan/observer-feed-1381-restore branch from 787713f to ce9a03f Compare June 30, 2026 23:49
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 wpfleger96 changed the title fix(desktop): restore observer-feed regressions from #1381 feat(desktop): restore observer-feed regressions from #1381 and classify 4 new session/update types 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>
@wpfleger96 wpfleger96 force-pushed the duncan/observer-feed-1381-restore branch from 31579f2 to d791a25 Compare July 1, 2026 19:06
wpfleger96 pushed a commit that referenced this pull request Jul 1, 2026
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Shots 01–10: restored behaviors and new lifecycle types

Screenshots 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.

01-prompt-context-inline

02-system-prompt-title-restored

03-permission-approved

04-permission-cancelled

05-prompt-context-expanded

06-system-prompt-expanded

07-current-mode-update

08-usage-update-coalesced

09-available-commands-update

10-config-option-update

Shot 11: first-turn ordering fix (new)

Full pool.rs wire sequence (turn_started → session/new → session_resolved → session/prompt). Shows the corrected per-turn display order: user message bubble → System prompt (Base/System sections) → Prompt context (Buzz event/Thread context sections).

11-first-turn-ordering

@wpfleger96 wpfleger96 force-pushed the duncan/observer-feed-1381-restore branch from 7a3838d to 9fa77da Compare July 1, 2026 20:20
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>
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 15 commits July 1, 2026 16:30
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>
@wpfleger96 wpfleger96 force-pushed the duncan/observer-feed-1381-restore branch from 9fa77da to 0ffd40f Compare July 1, 2026 20:30
@wpfleger96 wpfleger96 merged commit fec7684 into main Jul 1, 2026
20 checks passed
@wpfleger96 wpfleger96 deleted the duncan/observer-feed-1381-restore branch July 1, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant