fix: coalesce streamed agent plan messages (#3764)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# ADR-2026-09-17-coalesce-streamed-agent-plans: Coalesce streamed agent plans
|
||||
|
||||
**Status:** accepted
|
||||
**Date:** 2026-09-17
|
||||
**Area:** protocol
|
||||
|
||||
## Context
|
||||
|
||||
Some ACP agents stream a plan by repeatedly updating the `plan` input of one
|
||||
tool call. Kandev currently converts every snapshot into an `agent_plan` event
|
||||
without the source tool-call identity, then persists every event as a new
|
||||
conversation message. A single plan can therefore appear as many cumulative
|
||||
cards in live and replayed transcripts.
|
||||
|
||||
The plan remains useful before the tool call completes, distinct plan
|
||||
invocations must remain distinct, and existing stored transcripts cannot be
|
||||
assumed to contain correlation metadata.
|
||||
|
||||
## Decision
|
||||
|
||||
Every agent-plan event produced from a tool update carries that source
|
||||
`ToolCallID`. The durable conversation boundary identifies one plan stream by
|
||||
session, turn, and tool call, and upserts its latest accepted snapshot into one
|
||||
`agent_plan` message.
|
||||
|
||||
The web transcript projection uses the durable correlation identity when it is
|
||||
present. For historical uncorrelated rows, it collapses only contiguous
|
||||
same-turn `agent_plan` messages whose content forms a strict cumulative prefix
|
||||
chain. It does not rewrite stored historical rows.
|
||||
|
||||
## Consequences
|
||||
|
||||
Live updates and replay use the same durable message identity, so reloading does
|
||||
not reveal intermediate snapshots. Separate tool calls retain separate cards.
|
||||
The ACP-to-conversation path must preserve tool-call identity and test
|
||||
idempotent updates.
|
||||
|
||||
Legacy cleanup is deliberately conservative. Historical snapshots that are not
|
||||
contiguous prefixes remain visible rather than risking accidental data loss.
|
||||
The compatibility projection can be removed only after the retained history no
|
||||
longer contains uncorrelated snapshots.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- Persist only the final tool input. This removes duplication but withholds a
|
||||
useful plan while a long-running tool call is still in progress.
|
||||
- Deduplicate only in the frontend. This hides the symptom in one projection
|
||||
while retaining duplicate durable messages for reload, pagination, APIs, and
|
||||
future clients.
|
||||
- Coalesce by content similarity or turn alone. This can merge independent plan
|
||||
invocations and makes identity depend on mutable text.
|
||||
- Rewrite all historical rows. This adds destructive migration risk for a
|
||||
rendering compatibility issue that can be handled conservatively at read
|
||||
time.
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
status: done
|
||||
requirements:
|
||||
- REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001
|
||||
system_design:
|
||||
- ../../specs/agents/system-design/agent-plan-stream-coalescing.md
|
||||
---
|
||||
|
||||
# Agent plan stream coalescing
|
||||
|
||||
## Overview
|
||||
|
||||
Turn repeated ACP plan-input snapshots from one tool call into one durable,
|
||||
updating conversation message. Retain a conservative frontend projection for
|
||||
historical duplicate rows that lack correlation metadata.
|
||||
|
||||
## Delivery order
|
||||
|
||||
1. Add a failing ACP adapter test proving an emitted plan event carries the
|
||||
source tool-call identity.
|
||||
2. Add failing orchestrator and task-service tests proving repeated snapshots
|
||||
upsert one `agent_plan` message while distinct tool calls remain separate.
|
||||
3. Implement adapter correlation and durable message upsert.
|
||||
4. Add failing frontend tests for correlated rows and contiguous legacy prefix
|
||||
chains, then implement the compatibility projection.
|
||||
5. Prove replay behavior through a focused Playwright transcript scenario and
|
||||
run the targeted backend, frontend, and specification checks.
|
||||
|
||||
The work is one vertical slice because adapter correlation, persistence, and
|
||||
projection are all required for one stable live-and-replay outcome.
|
||||
|
||||
## Technical approach
|
||||
|
||||
Use the existing `AgentEvent.ToolCallID` field for
|
||||
`EventTypeAgentPlan`. The orchestrator shall pass the task, session, active
|
||||
turn, source tool call, and snapshot to a dedicated task-service upsert path.
|
||||
The task service derives the deterministic message ID and namespaced
|
||||
correlation metadata from the session, turn, and source tool call. The
|
||||
repository serializes the read/create/update decision for that identity in one
|
||||
transaction; the first event creates one `agent_plan` row and each later
|
||||
accepted event updates that row's content.
|
||||
|
||||
Keep uncorrelated adapter events append-compatible. In
|
||||
`use-processed-messages`, normalize visible plans before activity grouping:
|
||||
retain only the newest row for a repeated durable correlation key, and collapse
|
||||
uncorrelated rows only when they are adjacent, share a turn, and form a strict
|
||||
content-prefix chain.
|
||||
|
||||
Do not add a database migration or modify `AgentPlanMessage`.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
`UI-01: Streamed agent plan transcript`, entry point: desktop or phone task
|
||||
conversation after one plan-producing tool call.
|
||||
|
||||
Current observed behavior:
|
||||
|
||||
```text
|
||||
Conversation (single scroll owner)
|
||||
+ Agent plan: Batch ES reads [collapsed]
|
||||
+ Agent plan: Batch ES reads [collapsed]
|
||||
+ Agent plan: Batch ES reads [collapsed]
|
||||
+ ...one card per partial snapshot
|
||||
```
|
||||
|
||||
Proposed shared desktop and phone behavior:
|
||||
|
||||
```text
|
||||
Conversation (single scroll owner)
|
||||
+ Agent plan: Batch ES reads [collapsed]
|
||||
latest complete snapshot
|
||||
```
|
||||
|
||||
The one-card cardinality is required by
|
||||
`AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.1` and `.5`. Spacing, card
|
||||
geometry, collapse controls, scroll behavior, and touch targets are unchanged.
|
||||
|
||||
## Verification strategy
|
||||
|
||||
- ACP adapter unit tests cover `ToolCallID` propagation and empty-plan guards.
|
||||
- Task-service and orchestrator tests cover create, update, identical no-op,
|
||||
retry, distinct tool calls, serialized concurrent updates, and uncorrelated
|
||||
compatibility. A PostgreSQL multi-connection regression proves the identity
|
||||
lock spans the read/update transaction.
|
||||
- Frontend unit tests cover correlated duplicates, legacy prefix chains, turn
|
||||
and adjacency boundaries, and non-prefix plans.
|
||||
- One Chromium Playwright scenario loads persisted duplicate snapshots and
|
||||
observes one latest plan card. The shared normalization hook makes a second
|
||||
mobile scenario redundant because no responsive presentation changes.
|
||||
- Specification validation and diff checks close the package.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Coalesce streamed agent plans](task-01-coalesce-streamed-agent-plans.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
- ACP adapter, task-service, and orchestrator plan regressions passed.
|
||||
- Agent-plan repository regressions passed, including a real PostgreSQL
|
||||
multi-connection lock-order case and the identical-snapshot no-op assertion.
|
||||
- Task-service and orchestrator plan regressions passed under the Go race
|
||||
detector.
|
||||
- Backend application and integration message adapters compiled.
|
||||
- 87 focused frontend tests, TypeScript typecheck, and changed-file ESLint
|
||||
passed.
|
||||
- The fresh production-build Chromium replay scenario passed before and after
|
||||
reload.
|
||||
|
||||
## Risks and exclusions
|
||||
|
||||
- A correlation key that is not namespaced can collide with the visible tool
|
||||
call message.
|
||||
- Content-length heuristics can merge unrelated plans; the legacy path must use
|
||||
strict adjacency, same-turn, and prefix checks.
|
||||
- Retried or concurrent delivery can race with message creation and update;
|
||||
the persistence boundary must serialize the full mutation by plan identity.
|
||||
- Do not rewrite historical rows, change task-plan documents, change plan-card
|
||||
UI, or broaden transcript deduplication to other message types.
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
id: "01-coalesce-streamed-agent-plans"
|
||||
title: "Coalesce streamed agent plans"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001
|
||||
acceptance_criteria:
|
||||
- AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.1
|
||||
- AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.2
|
||||
- AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.3
|
||||
- AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.4
|
||||
- AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.5
|
||||
system_design:
|
||||
- ../../specs/agents/system-design/agent-plan-stream-coalescing.md
|
||||
---
|
||||
|
||||
# Task 01: Coalesce streamed agent plans
|
||||
|
||||
## Summary
|
||||
|
||||
Correlate ACP plan snapshots to their source tool call, persist one updating
|
||||
`agent_plan` message per invocation, and collapse conservative legacy snapshot
|
||||
chains during transcript projection.
|
||||
|
||||
## In scope
|
||||
|
||||
- Propagate source `ToolCallID` on ACP agent-plan events.
|
||||
- Add a task-service agent-plan upsert keyed by session, turn, and source tool
|
||||
call, and route correlated orchestrator events through it.
|
||||
- Preserve append behavior for uncorrelated plan events.
|
||||
- Normalize correlated and qualifying legacy snapshots before message grouping.
|
||||
- Add red-first backend and frontend tests and one replay-focused Chromium
|
||||
Playwright regression.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Database migrations or historical-row rewrites.
|
||||
- Task-plan document history or safe-edit behavior.
|
||||
- Plan-card markup, localization, layout, responsive branches, controls,
|
||||
scrolling, or touch behavior.
|
||||
- Deduplicating other message types.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Repeated snapshots from one source tool call create one durable plan message
|
||||
whose content is the latest accepted snapshot; retries remain idempotent and
|
||||
separate tool calls remain separate.
|
||||
- Live, replayed, and paginated transcripts project correlated plans once and
|
||||
fold only adjacent same-turn strict-prefix legacy chains.
|
||||
- Focused browser evidence displays one latest plan card from persisted
|
||||
duplicates without changing desktop or phone composition.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
`UI-01: Streamed agent plan transcript` from the
|
||||
[combined plan preview](plan.md#ascii-ui-preview):
|
||||
|
||||
```text
|
||||
Desktop and phone task conversation
|
||||
+ Agent plan: Batch ES reads [collapsed]
|
||||
latest complete snapshot
|
||||
```
|
||||
|
||||
Card controls and the existing single transcript scroll owner are unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend
|
||||
go test ./internal/agentctl/server/adapter/transport/acp -run 'AgentPlan'
|
||||
go test ./internal/task/service ./internal/orchestrator -run 'AgentPlan'
|
||||
go test ./internal/task/repository/sqlite -run 'AgentPlan'
|
||||
cd ../
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --filter @kandev/web test -- hooks/processed-message-filtering.test.ts hooks/use-processed-messages.test.ts
|
||||
pnpm --filter @kandev/web run typecheck
|
||||
cd web
|
||||
pnpm exec eslint hooks/processed-message-filtering.ts hooks/processed-message-filtering.test.ts hooks/use-processed-messages.ts hooks/use-processed-messages.test.ts e2e/tests/chat/agent-plan-coalescing.spec.ts
|
||||
pnpm e2e:run --host --project chromium tests/chat/agent-plan-coalescing.spec.ts -- --retries=0
|
||||
cd ../..
|
||||
python3 scripts/list-docs.py decisions --format paths
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
python3 scripts/list-docs.py validate
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_tools.go`
|
||||
- `apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_tools_test.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_streaming.go`
|
||||
- `apps/backend/internal/orchestrator/event_handlers_streaming_test.go`
|
||||
- `apps/backend/internal/task/service/service_messages.go`
|
||||
- `apps/backend/internal/task/service/service_messages_test.go`
|
||||
- `apps/web/hooks/processed-message-filtering.ts`
|
||||
- `apps/web/hooks/processed-message-filtering.test.ts`
|
||||
- `apps/web/hooks/use-processed-messages.ts`
|
||||
- `apps/web/hooks/use-processed-messages.test.ts`
|
||||
- `apps/web/e2e/tests/chat/agent-plan-coalescing.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- Tool-call IDs are not globally unique; session and turn scope must remain part
|
||||
of the durable identity.
|
||||
- The first snapshot and a retry can arrive close together; creation and update
|
||||
cannot be separate non-idempotent operations.
|
||||
- The legacy compatibility rule must fail closed at any ambiguity.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001` and all acceptance criteria.
|
||||
- The agent-plan stream system design and
|
||||
`ADR-2026-09-17-coalesce-streamed-agent-plans`.
|
||||
- Existing tool-call message update patterns and transcript visibility filters.
|
||||
|
||||
## Results
|
||||
|
||||
- RED: the ACP adapter emitted plan snapshots with an empty tool-call identity,
|
||||
the orchestrator appended a new session message, and transcript filtering
|
||||
retained both correlated and legacy cumulative snapshots.
|
||||
- GREEN: ACP plan events now carry the emitted source tool-call ID. The
|
||||
orchestrator routes correlated plans to a deterministic task-service upsert,
|
||||
while uncorrelated adapter events retain append compatibility.
|
||||
- GREEN: the repository serializes each correlated plan's read/create/update
|
||||
and conversation receipt in one identity-locked transaction. Identical
|
||||
snapshots skip both the write and update event.
|
||||
- GREEN: transcript filtering keeps the final delivery for each durable
|
||||
correlation and folds only adjacent same-turn strict-prefix legacy chains.
|
||||
- Focused backend tests passed in the ACP adapter, task service, and
|
||||
orchestrator; the task-service and orchestrator cases also passed with
|
||||
`-race`. Backend application and integration message adapters compiled.
|
||||
- Focused frontend tests passed: 87 tests across the filtering and processed
|
||||
message suites. TypeScript typecheck and changed-file ESLint passed.
|
||||
- The production-build Chromium replay scenario passed: 1 test confirmed one
|
||||
latest plan card before and after page reload.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
status: active
|
||||
system: agents
|
||||
created: 2026-09-17
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
|
||||
# Agent Plan Stream Coalescing Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
Agent providers can stream a growing plan through repeated updates for one tool
|
||||
invocation. The agent system owns the provider-facing event contract and must
|
||||
present that logical plan once, without exposing transport snapshots as
|
||||
separate conversation entries.
|
||||
|
||||
## Terminology
|
||||
|
||||
- **Plan snapshot:** One complete plan value observed on an agent tool update.
|
||||
- **Plan stream identity:** The session, turn, and source tool-call identity
|
||||
that together identify one logical plan-producing invocation.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001: One transcript item per plan stream
|
||||
|
||||
**Intent:** Users can read and revisit an agent-authored plan without repeated
|
||||
cards created by incremental provider transport updates.
|
||||
|
||||
**User story:** As a task user, I want one current plan card for each plan
|
||||
invocation, so that streamed drafting does not overwhelm the conversation.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.1:** When one plan-producing
|
||||
tool invocation emits multiple snapshots, the conversation shall contain one
|
||||
plan item for that invocation and shall show its latest accepted snapshot.
|
||||
- **AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.2:** When separate plan-producing
|
||||
tool invocations occur in the same session or turn, the conversation shall
|
||||
keep them as separate plan items.
|
||||
- **AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.3:** After live delivery, page
|
||||
reload, session resume, or history pagination, the conversation shall show
|
||||
the same one-item-per-invocation result.
|
||||
- **AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.4:** When historical messages
|
||||
lack a plan stream identity, a contiguous same-turn sequence whose content
|
||||
grows strictly by prefix shall render only its final snapshot; non-prefix
|
||||
plans and plans separated by another conversation item shall remain distinct.
|
||||
- **AC-AGENTS-AGENT-PLAN-STREAM-COALESCING-001.5:** Desktop and phone task
|
||||
conversations shall apply the same plan-item cardinality without changing
|
||||
the existing plan card controls, scrolling, or touch behavior.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Deleting or rewriting historical conversation rows.
|
||||
- Changing task-plan document storage, revision history, or agent plan-write
|
||||
safety.
|
||||
- Changing plan-card layout, controls, copy, or responsive composition.
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
status: current
|
||||
system: agents
|
||||
requirements:
|
||||
- REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001
|
||||
---
|
||||
|
||||
# Agent Plan Stream Coalescing System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The Agents system owns conversion of provider updates into durable conversation
|
||||
messages. This design covers ACP plan correlation, transcript persistence, and
|
||||
the compatibility projection used by the web conversation. It does not change
|
||||
task-plan document storage or the plan-card component.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| `REQ-AGENTS-AGENT-PLAN-STREAM-COALESCING-001` | [Data and contracts](#data-and-contracts), [Control flow](#control-flow), [Persistence and compatibility](#persistence-and-compatibility) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- `internal/agentctl/server/adapter/transport/acp` extracts `rawInput.plan` from
|
||||
ACP tool updates and attaches the source `ToolCallID` to
|
||||
`streams.EventTypeAgentPlan`.
|
||||
- `internal/orchestrator` resolves the active session and turn, then passes
|
||||
those IDs with the task, source tool call, and snapshot to the task service.
|
||||
- `internal/task/service` derives the deterministic message ID and namespaced
|
||||
correlation metadata. Its agent-plan upsert uses the same durable mutation
|
||||
path for first delivery, retries, and later snapshots.
|
||||
- `internal/task/repository/sqlite` owns the atomic read/create/update decision
|
||||
and conversation receipt. PostgreSQL takes a transaction-scoped advisory
|
||||
lock for the deterministic plan-message identity before its first read;
|
||||
SQLite takes its writer lock before reading.
|
||||
- `apps/web/hooks/use-processed-messages.ts` projects correlated rows and
|
||||
conservatively folds legacy cumulative snapshots before render grouping.
|
||||
- `AgentPlanMessage` remains the presentation component for the resulting
|
||||
message and does not perform deduplication.
|
||||
|
||||
## Data and contracts
|
||||
|
||||
`streams.AgentEvent` already carries `ToolCallID`. An
|
||||
`EventTypeAgentPlan` emitted from an ACP tool update shall set:
|
||||
|
||||
- `SessionID` to the agent session;
|
||||
- `ToolCallID` to the source ACP tool call;
|
||||
- `PlanContent` to the complete plan snapshot from that update.
|
||||
|
||||
The task service derives a namespaced plan-message correlation key from
|
||||
`ToolCallID`. The namespace prevents collision with the visible tool-call
|
||||
message while preserving the original source identity in message metadata. The
|
||||
durable message identity additionally includes the session and active turn, so
|
||||
providers that reuse tool-call identifiers cannot merge unrelated plans.
|
||||
|
||||
An uncorrelated `EventTypeAgentPlan` remains supported for adapters that do not
|
||||
yet provide a tool-call identity. It follows the existing append behavior and
|
||||
is eligible only for the conservative web compatibility projection.
|
||||
|
||||
## Control flow
|
||||
|
||||
1. The ACP adapter receives a tool-call update containing a non-empty
|
||||
`rawInput.plan`.
|
||||
2. It emits an agent-plan event with the tool-call identity on every accepted
|
||||
snapshot.
|
||||
3. The orchestrator resolves the active turn and asks the task service to
|
||||
upsert the correlated `agent_plan` message.
|
||||
4. The task service derives the durable message identity and asks the
|
||||
repository to apply the snapshot. The repository holds one identity-scoped
|
||||
transaction lock across the first read, create or update, and conversation
|
||||
receipt. An identical snapshot is a no-op. Concurrent calls therefore read
|
||||
the prior committed snapshot and cannot overwrite in reverse after reading
|
||||
the same stale value.
|
||||
5. WebSocket delivery and conversation replay both expose that same message.
|
||||
6. Before grouping render items, the web projection retains the latest message
|
||||
for a repeated durable correlation key and applies the legacy prefix rule
|
||||
only to uncorrelated rows.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
Missing session IDs, empty plan content, and unavailable message persistence
|
||||
remain non-actionable events under the existing handler guard. A correlated
|
||||
retry is idempotent and cannot create another plan row. If a provider omits
|
||||
`ToolCallID`, Kandev preserves the plan as a separate message rather than
|
||||
discarding it.
|
||||
|
||||
Legacy folding requires all messages to be `agent_plan` entries in the same
|
||||
turn, adjacent after normal visibility filtering, and each previous content to
|
||||
be a strict prefix of the next. Any failed condition ends the chain. This
|
||||
failure-closed rule may leave ambiguous duplicates visible but cannot merge
|
||||
unrelated plans.
|
||||
|
||||
## Persistence and compatibility
|
||||
|
||||
No schema migration is required. The existing message content and metadata
|
||||
fields store the latest snapshot and its correlation identity. Upsert identity
|
||||
is deterministic across retries and process restarts. The repository's
|
||||
transaction-scoped identity lock also spans multiple backend instances that
|
||||
share PostgreSQL.
|
||||
|
||||
Existing duplicate rows remain unchanged. The web compatibility projection
|
||||
selects the final row from qualifying legacy chains, which preserves immutable
|
||||
history while repairing conversation readability on live and paginated reads.
|
||||
|
||||
## Observability
|
||||
|
||||
Existing agent event and message update logs expose the event type, task,
|
||||
session, and tool-call update outcome. Failures use the current
|
||||
`failed to create agent plan message` error path, renamed to describe the
|
||||
upsert operation when correlation is present.
|
||||
|
||||
## Responsive presentation
|
||||
|
||||
This change only normalizes transcript data before the existing plan card is
|
||||
rendered. Desktop and phone use the same message hook and therefore receive the
|
||||
same one-card projection. The plan-card composition, scroll owner, controls,
|
||||
touch targets, safe-area behavior, and navigation do not change. Targeted hook
|
||||
tests satisfy mobile parity because no viewport-dependent UI behavior changes.
|
||||
|
||||
## Related decisions
|
||||
|
||||
- [ADR-2026-09-17-coalesce-streamed-agent-plans](../../../decisions/2026-09-17-coalesce-streamed-agent-plans.md)
|
||||
Reference in New Issue
Block a user