fix(sessions): show fork icon + order number on nested forked sessions (#934)

* fix(sessions): fork icon + depth number, no indentation

Multiply-nested forked sessions rendered with deep indentation that crushed
the card off-screen and made titles unreadable, and the per-sibling order
number was uninformative (mostly "1").

- SessionTreeItem: replace forkIndex (sibling order) with forkDepth (nesting
  level: 1 = child of a root, 2 = fork of a fork, ...); flattenSessionTree
  populates it.
- SessionCard: render CallSplit fork icon + the depth number (primary tint).
  Removed the depth-based start padding so names stay full-width readable —
  the icon+number now carries the structure instead of indentation.
- strings: sessions_fork_indicator (en/zh/ko).
- SessionTreeTest: assert isFork/forkDepth across nested fixtures.

No backend change — backend already returns parent_session_id per SessionInfo.

* test(ws): make testAutoReconnect tolerate transient RECONNECTING state

The test gated on connectionStatus.first { it == RECONNECTING } with a
15s timeout. With the test backoff forced to 0ms the RECONNECTING state is
emitted + consumed before the collector attaches, so the wait missed it and
timed out (kotlinx.coroutines.TimeoutCancellationException) — the flake CI
saw under parallel load.

- Replace the brittle RECONNECTING .first{} wait with a tolerant check that
  accepts RECONNECTING-or-later (the reconnect clearly fired).
- Keep connect2Latch (second socket opening) as the definitive proof + add a
  final CONNECTED assertion.
- Reset the test backoff in a finally so later tests never inherit 0ms.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-18 17:06:51 +03:00
1 parent 53c86d5871
commit 82e2d787af
7 files changed
+103 -26

No files matched your search

@@ -32,6 +32,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.CallSplit
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
@@ -791,8 +792,9 @@ fun SessionsScreen(
SessionCard(
session = session,
displayTitle = item.displayTitle,
depth = item.depth,
branchStem = item.branchStem,
isFork = item.isFork,
forkDepth = item.forkDepth,
query = state.searchQuery,
isSelecting = state.isSelecting,
isSelected = session.id in state.selectedIds,
@@ -958,8 +960,9 @@ fun SessionsScreen(
private fun SessionCard(
session: com.m57.hermescontrol.data.model.SessionInfo,
displayTitle: String,
depth: Int,
branchStem: String?,
isFork: Boolean = false,
forkDepth: Int = 0,
query: String,
isSelecting: Boolean,
isSelected: Boolean,
@@ -984,7 +987,6 @@ private fun SessionCard(
modifier =
Modifier
.fillMaxWidth()
.padding(start = (depth * 16).dp)
.testTag("session_card_${session.id}")
.combinedClickable(
onClick = onCardClick,
@@ -1022,13 +1024,25 @@ private fun SessionCard(
Spacer(modifier = Modifier.width(spacing.sm))
}
// Branch tree stem
if (branchStem != null && !isSelecting) {
Text(
text = branchStem,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.outline,
// Fork marker: a fork icon + its nesting depth, so a chain reads
// 🍴1 → 🍴2 → 🍴3 and a branch-off pops out. No indentation — the
// number carries the structure so names never get squished.
if (isFork && !isSelecting) {
Icon(
imageVector = Icons.Filled.CallSplit,
contentDescription = stringResource(R.string.sessions_fork_indicator),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp),
)
if (forkDepth > 0) {
Spacer(modifier = Modifier.width(spacing.xs))
Text(
text = forkDepth.toString(),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
}
Spacer(modifier = Modifier.width(spacing.sm))
}