key conversation list items so card state survives result reshapes (T-285)
The conversation ListView.builder built its items (_ConversationTurn, _ActivityCard) with no keys, so Flutter matched the stateful subtrees inside them (ConversationCard collapse/hover/focus; ClideHolderCard expand state) to widgets by POSITION. The visible list reshapes exactly when a tool result lands — T-262 folds a success result into its call card and suppresses the standalone result, errors append a sticky card, clusters re-fold — so after a read/write completed, a card's collapse/hover state (or a cluster's identity) could reattach to the wrong card. Give each list item a stable ValueKey from its identity: sticky item by item.uuid, folded cluster by its first item's uuid (namespaced turn./cluster./ run./step. so the four call sites can't collide), plus super.key on the _ConversationTurn/_ActivityCard constructors. Tests: unfolded cards carry per-item keys; a folded cluster carries its first-item key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -260,8 +260,12 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
itemCount: groups.length,
|
||||
itemBuilder: (context, i) {
|
||||
final g = groups[i];
|
||||
// Stable keys pin each card's State (collapse/hover, cluster expand)
|
||||
// to its logical item, so streaming result updates that reshape the
|
||||
// visible list don't reattach State to the wrong card (T-285).
|
||||
return switch (g) {
|
||||
StickyItem(:final item) => _ConversationTurn(
|
||||
key: ValueKey('turn.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
@@ -271,6 +275,7 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
runByToolUseId: fold.runByToolUseId,
|
||||
),
|
||||
FoldedCluster(:final items) => _ActivityCard(
|
||||
key: ValueKey('cluster.${items.first.uuid}'),
|
||||
items: items,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
@@ -310,6 +315,7 @@ void _openRecord(BuildContext context, String id) {
|
||||
/// One conversation item, rendered by kind.
|
||||
class _ConversationTurn extends StatelessWidget {
|
||||
const _ConversationTurn({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.tokens,
|
||||
this.toolUseOutcomes = const <String, bool>{},
|
||||
@@ -543,6 +549,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
children: [
|
||||
for (final r in runItems)
|
||||
_ConversationTurn(
|
||||
key: ValueKey('run.${r.uuid}'),
|
||||
item: r,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: toolUseOutcomes,
|
||||
@@ -650,6 +657,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
/// background toggles collapse. Stateless — the holder owns the expand state.
|
||||
class _ActivityCard extends StatelessWidget {
|
||||
const _ActivityCard({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.tokens,
|
||||
required this.toolUseOutcomes,
|
||||
@@ -676,6 +684,7 @@ class _ActivityCard extends StatelessWidget {
|
||||
children: [
|
||||
for (final item in items)
|
||||
_ConversationTurn(
|
||||
key: ValueKey('step.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
toolUseOutcomes: toolUseOutcomes,
|
||||
|
||||
Reference in New Issue
Block a user