Skip to content

Commit 8fcc0e4

Browse files
authored
fix(chat): 低于门槛的单步思考不再消失;卡片展开收起加动画 (#20)
Two defects from the user's live test (screenshots 63329 vs 63330): 1. Lone thinking dropped. The visibility gate was ROUND-level (`!roundFolded || block.id in keepVisibleIds` with an always-empty keepVisibleIds), so once ANY run in the round carded, every block not owned by a card was hidden — including the lone thinking of a thinking→reply-text run (1 step, below the 2-step card threshold, nothing else to render it). The user saw the thinking bubble simply disappear when folding was on. The gate is block-level now (`block.id !in foldedBlockIds`): a block no card owns renders inline. Same fix in shouldShowProcessToolRow — its roundFolded branch hid below-threshold tool blocks the same way. The card threshold itself is unchanged: a run needs ≥ 2 steps ("超过1步则进入折叠"), one step stays inline — now visibly so. 2. Screen flash on expand/collapse. The row list appeared in a single frame and the whole list jumped. The card container now animates its size change (gentle spring), so it grows/shrinks in place while the list reflows around it. Also deletes ROUND_MIN_FOLDABLE_STEPS — a dead constant (zero references) left over from the pre-card round-fold model that only misleads readers about the threshold. Regression test: a carded run followed by a lone thinking + reply text — the card forms, its steps stay inside it, and the lone thinking renders inline. Co-authored-by: tall-1997 <278631348+tall-1997@users.noreply.github.com>
1 parent 842a4c9 commit 8fcc0e4

3 files changed

Lines changed: 71 additions & 13 deletions

File tree

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ package com.openminis.app.ui.chat
77
// Imports trimmed after ChatScreen.kt split; externally-called ones internal.
88

99
import androidx.compose.animation.AnimatedVisibility
10+
import androidx.compose.animation.animateContentSize
1011
import androidx.compose.animation.core.LinearEasing
12+
import androidx.compose.animation.core.Spring
1113
import androidx.compose.animation.core.animateFloat
1214
import androidx.compose.animation.core.infiniteRepeatable
1315
import androidx.compose.animation.core.rememberInfiniteTransition
16+
import androidx.compose.animation.core.spring
1417
import androidx.compose.animation.core.tween
1518
import androidx.compose.material.icons.filled.KeyboardArrowDown
1619
import androidx.compose.material.icons.filled.KeyboardArrowRight
@@ -1056,6 +1059,17 @@ internal fun ProcessSummaryBar(
10561059
.border(0.5.dp, thinkingBlue.copy(alpha = 0.15f), shape)
10571060
.clip(shape)
10581061
.clickable(onClick = onToggle)
1062+
// [P-process-card] Animate the expand/collapse height change:
1063+
// without this the row list appears in one frame and the whole
1064+
// screen visibly jumps (user: "让用户感觉整个屏幕闪一下"). A
1065+
// gentle spring keeps the card growing/shrinking in place while
1066+
// the list reflows around it.
1067+
.animateContentSize(
1068+
animationSpec = spring(
1069+
dampingRatio = Spring.DampingRatioLowBouncy,
1070+
stiffness = Spring.StiffnessMediumLow,
1071+
),
1072+
)
10591073
.padding(horizontal = 12.dp, vertical = 8.dp),
10601074
) {
10611075
Row(verticalAlignment = Alignment.CenterVertically) {

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatFlatItems.kt‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,6 @@ internal data class ProcessStepRef(
449449
val block: AssistantBlock,
450450
)
451451

452-
/** Rounds with fewer foldable steps than this never fold (rule 1). */
453-
private const val ROUND_MIN_FOLDABLE_STEPS = 3
454-
455452
/** A process run shorter than this stays inline (a card for one row is noise). */
456453
private const val SEGMENT_MIN_STEPS = 2
457454

@@ -813,6 +810,14 @@ internal fun buildFlatChatItems(
813810
// list for good — expanding the card renders them INSIDE the card.
814811
// A card the user has not opened folds its steps; a card they opened
815812
// (its own key, not the round's) keeps them in the card either way.
813+
//
814+
// [P-process-card-single-step] foldedBlockIds is BLOCK-level: only
815+
// the blocks of runs that actually became cards. A run below the
816+
// 2-step threshold (a lone thinking before reply text) never cards,
817+
// so its blocks stay OUT of foldedBlockIds and must keep rendering
818+
// inline — the old `!roundFolded || in keepVisibleIds` gate hid
819+
// them whenever ANY card existed in the round, which silently
820+
// dropped the lone thinking (user: "只有一步思考的气泡被丢弃了").
816821
val foldedBlockIds: Set<String> = if (round == null || isSystem) {
817822
emptySet()
818823
} else {
@@ -831,8 +836,7 @@ internal fun buildFlatChatItems(
831836
when (block.kind) {
832837
"text" -> block.content.isNotEmpty()
833838
"info" -> true
834-
"thinking" -> block.id !in foldedBlockIds &&
835-
(!roundFolded || block.id in keepVisibleIds)
839+
"thinking" -> block.id !in foldedBlockIds
836840
"tool_use" -> block.id !in foldedBlockIds && shouldShowProcessToolRow(
837841
block,
838842
showCompletedToolCards = showCompletedToolCards,
@@ -950,7 +954,10 @@ internal fun buildFlatChatItems(
950954
}
951955
}
952956
"thinking" -> {
953-
if (block.id !in foldedBlockIds && (!roundFolded || block.id in keepVisibleIds)) {
957+
// [P-process-card-single-step] Block-level gate: a
958+
// thinking block not owned by any card renders inline,
959+
// whether or not sibling runs in the round carded.
960+
if (block.id !in foldedBlockIds) {
954961
out.add(dedupe(FlatChatItem.AssistantThinking(
955962
messageId = message.id,
956963
block = block,
@@ -1090,15 +1097,18 @@ internal fun shouldShowProcessToolRow(
10901097
block: AssistantBlock,
10911098
showCompletedToolCards: Boolean,
10921099
foldAiProcess: Boolean,
1093-
roundFolded: Boolean,
1094-
keepVisibleIds: Set<String>,
1100+
@Suppress("UNUSED_PARAMETER") roundFolded: Boolean,
1101+
@Suppress("UNUSED_PARAMETER") keepVisibleIds: Set<String>,
10951102
): Boolean {
10961103
if (isAlwaysVisibleProcessTool(block)) return true
1097-
// A tool the process card already owns never renders a second copy in
1098-
// the list, running or not — the card header names the live step.
1099-
// isInFlightProcessTool only decides the fold-OFF list, where there is
1100-
// no card to hold it.
1101-
if (roundFolded) return block.id in keepVisibleIds
1104+
// [P-process-card-single-step] No round-level gate here: every caller
1105+
// has already filtered `block.id !in foldedBlockIds`, so a block that
1106+
// reaches this function belongs to a run that did NOT card (below the
1107+
// 2-step threshold) and must render inline. The old
1108+
// `if (roundFolded) return block.id in keepVisibleIds` hid those
1109+
// blocks whenever any sibling run in the round carded — dropping a
1110+
// lone tool the same way the lone thinking was dropped. Card-owned
1111+
// blocks never reach here; the block-level check upstream is the gate.
11021112
if (isInFlightProcessTool(block)) return true
11031113
// Fold mode owns visibility: a running or user-expanded round shows every
11041114
// step in-thread (the fold bar no longer carries tool chips, so hiding

‎src/android/app/src/test/java/com/openminis/app/ui/chat/ChatFlatItemsProcessFoldTest.kt‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,40 @@ class ChatFlatItemsProcessFoldTest {
116116
assertEquals(2, summary.toolCount)
117117
}
118118

119+
@Test
120+
fun `a lone thinking after a carded run stays inline`() {
121+
// [P-process-card-single-step] Regression: the visibility gate used
122+
// to be ROUND-level (`!roundFolded || in keepVisibleIds`), so once
123+
// any run in the round carded, a LONE thinking block from a
124+
// below-threshold run (thinking → reply text, one step) was hidden
125+
// with nowhere to render — the user lost the thinking entirely
126+
// ("只有一步思考的气泡被丢弃了"). The gate is block-level now: a
127+
// block no card owns renders inline.
128+
val items = buildFlatChatItems(
129+
listOf(
130+
assistant(
131+
blocks = listOf(
132+
thinking(id = "th1"),
133+
tool(id = "t1"),
134+
tool(id = "t2"),
135+
text(id = "tx1", content = "reply"),
136+
thinking(id = "th2"),
137+
text(id = "tx2", content = "tail"),
138+
),
139+
),
140+
),
141+
showCompletedToolCards = false,
142+
foldAiProcess = true,
143+
)
144+
val k = kinds(items)
145+
// The carded run (3 steps) is a summary and its steps are inside it.
146+
assertTrue(k.contains("summary"))
147+
assertFalse(k.contains("tool:bash"))
148+
// The lone thinking after the reply text stays inline — it belongs
149+
// to no card.
150+
assertTrue(k.contains("thinking"))
151+
}
152+
119153
@Test
120154
fun `a two step run becomes a card and a single step stays inline`() {
121155
// [P-process-card] The card threshold is the RUN, not the round:

0 commit comments

Comments
 (0)