You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f4c68e3
Browse filesBrowse the repository at this point in the historyBrowse files
feat(webui): restore the turn chevron, phrase rotation and the uncapped group body (#120)
webui-parity 76, gaps G4 / G5 / G7 of ticket 61. Three changes that put the
session surface back on the desktop's shape; none of them is a new feature.
G4 — the settled turn bar carries the desktop's `>` chevron again. The gate
is `hasExpandableTurnContent(stats)`, the reference's own
`hasExpandableContent` question restated over the two counts the composite
summary already prints, so a plain question turn keeps a bare summary line.
The chevron coordinates state instead of nesting DOM: the turn's activity
groups are laid out flat here, so the arrow drives them and they keep their
own folding semantics. The coordination key is the turn ordinal, not the unit
index, because activity runs are re-cut every streaming frame.
G5 — the streaming indicator rotates through the desktop's phrase table:
2000–3000 ms before the first swap, 3500 ms after, a weighted bucket draw
(basic 0.75 / specific 0.15 / motion 0.1) with the previous phrase filtered
out. The phase label the engine reported stays on screen until the first
swap. Under prefers-reduced-motion the rotation continues: a text swap is
not motion, the desktop makes the same trade, and stopping would reinstall
the frozen-label defect. The animated half still switches off through the
existing globals.css rules.
G7 — the activity group body loses its 230px cap and its nested scrollbar,
and takes the reference's zero gap plus a 28px row floor. The transcript
column already scrolls, and a second scroller cut a long tool run's own
steps off mid-list. Removing the cap costs no render work: a collapsed
<details> keeps its body in the DOM either way. The tool detail's `pre` drops
from 320px to the reference's 180px, the one scroll container the desktop
keeps.
Tests: three new suites (57 cases) plus one updated call-site assertion, and
nine mutations verified red-then-restored byte-for-byte.
Co-authored-by: s39-dev <s39-dev@local>
Copy file name to clipboardExpand all lines: docs/webui.md
+103-8Lines changed: 103 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -477,6 +477,8 @@ below cites the component file and one `data-testid` per surface.
477
477
| Composer + drop overlay |`components/composer.tsx`|`composer-drop-overlay`, `composer-send-button`|
478
478
| Chat (virtual list ≥ 200 messages) |`components/chat.tsx` + `chat-virtual-list.tsx`|`chat-virtual-top-spacer`|
479
479
| Turn process bar (composite summary + output rate since ticket 46 PR3) |`components/activity-group.tsx#TurnProcessDisclosure`|`turn-process-disclosure`|
@@ -1304,6 +1306,20 @@ reads like "Thought 1 time, ran 1 command"):
1304
1306
AND tools) opens expanded; a pure-tool run starts collapsed; thinking
1305
1307
rows nested in a mixed run start collapsed while a thoughts-only run
1306
1308
starts with its thinking row expanded.
1309
+
-**The group body is uncapped and has no scrollbar of its own (webui-parity
1310
+
61, restoring the desktop shape)**: the body used to carry
1311
+
`max-h-[230px] overflow-y-auto`, so a long tool run nested a second
1312
+
scrollbar inside a page that already scrolls and cut its own steps off
1313
+
mid-list. The reference stylesheet gives `.activity-group-items`
1314
+
`gap: 0` plus a 28px minimum row height and no `max-height`; this
1315
+
repository now matches — zero gap as the component's `gap-0`, and the
1316
+
28px floor in `app/globals.css` as `.activity-group-items > *` (outside
1317
+
every `@layer`, so Tailwind's purge cannot reach a hand-written rule).
1318
+
Removing the cap costs no render work either way: a collapsed
1319
+
`<details>` keeps its body in the DOM, so the row count was never the
1320
+
reason for the cap. The one scroll container the desktop keeps inside a
1321
+
group is the tool detail's `pre`, capped at 180px (the reference
1322
+
`.webui-tool-detail-section pre`); this repository shipped 320px.
1307
1323
1308
1324
**Thinking block** (one thought):
1309
1325
@@ -1464,12 +1480,57 @@ While a turn is in flight, a process bar renders at the transcript tail
1464
1480
four server files; fixing it (persisting the marker, or moving it to
1465
1481
a structured field) needs its own ticket — until then the settled bar
1466
1482
is transient by design of the transport.
1467
-
- A 0.5px separator closes the bar from below. The retired
1468
-
expand-to-repeat-the-same-sentence interaction is gone (there is no
1469
-
content to expand — the reference renders a plain summary row when
1470
-
the turn has no expandable content of its own); the
1483
+
- A 0.5px separator closes the bar from below; the
1471
1484
`turn-process-disclosure` testid is kept.
1472
1485
1486
+
#### The turn bar's expand chevron (webui-parity 61, restoring the desktop shape)
1487
+
1488
+
A settled turn bar carries the desktop's `>` chevron
1489
+
(`turn-process-chevron`). Ticket 46 removed it on the grounds that there
1490
+
was "nothing to expand" — this repository lays the thinking and tool
1491
+
steps out flat as activity groups instead of nesting them inside the
1492
+
turn bar's collapsible region, so the chevron had nothing to point at.
1493
+
Webui-parity 61 changes that premise: the chevron is **state
1494
+
coordination** that drives the turn's activity groups, without
1495
+
re-arranging the DOM.
1496
+
1497
+
-**When it appears**: the turn has thoughts or tool calls. The predicate
1498
+
is the exported pure function `hasExpandableTurnContent(stats)` — the
1499
+
same two counts the composite summary already prints, and the same
1500
+
question the reference asks through `WebuiTurnProcess`'s
1501
+
`hasExpandableContent` (which `AssistantBody.tsx` computes from that
1502
+
turn's thinking text, tools and activity segments). A plain question
1503
+
turn (no thought, no tool) has nothing to disclose and renders the bare
1504
+
summary line with no chevron; that is the desktop's own rule, and the
1505
+
reason the turn in desktop screenshot 02 does show one is that the turn
1506
+
itself had process content.
1507
+
-**A live turn never shows it.** The reference reaches the same state
1508
+
through its `forceExpanded` / `disabled` props, whose docblock says
1509
+
those modes "suppress the toggle and keep available details open". Here
1510
+
there is a harder reason as well: an activity group holding a running
1511
+
tool is force-open under `data-active` and cannot be collapsed, so a
1512
+
live chevron would be a control that cannot act.
1513
+
-**What expanding shows**: the turn's thinking and tool steps — the
1514
+
activity groups. The turn's answer text is not part of it: the
1515
+
reference keeps the answer outside the collapse through
1516
+
`collapsedContent`, and here the answer is never inside a group.
1517
+
-**The coordination key is the turn ordinal, not the unit index.**
1518
+
Activity runs are re-cut on every streaming frame, so a unit index
1519
+
drifts while its turn ordinal only advances when a new user prompt
1520
+
arrives; keying on units would detach the chevron from the groups it
1521
+
drives the moment a tool header lands. `computeTurnLayout` in
1522
+
`webapp/lib/turn-stats.ts` is the pure function that produces the
1523
+
ordinals, and the intent lives in one
1524
+
`Map<turn ordinal, expanded>` in `Chat`.
1525
+
-**Until the user clicks, every group keeps its own default** (mixed run
1526
+
open, pure-tool run collapsed). The chevron's `aria-expanded` reads
1527
+
`computeTurnLayout#defaultExpandedByTurn` (true when any group of the
1528
+
turn opens by default) and inverts it on the first click, so the first
1529
+
click always changes something visible. After that the turn's groups
1530
+
move as one block.
1531
+
- Neither the `data-active` forced-open rule nor a group's own folding
1532
+
semantics change: a collapse intent never overrides forced open.
1533
+
1473
1534
## Loading states: transcript skeleton and streaming indicator (ticket U8)
1474
1535
1475
1536
The two waiting windows on the conversation surface have distinct treatments,
@@ -1478,7 +1539,7 @@ and neither is a bare spinner:
1478
1539
| Waiting for | What renders | Where it lives |
1479
1540
| --- | --- | --- |
1480
1541
| The first state snapshot (page load, engine boot) |`TranscriptSkeleton` — a shimmer placeholder shaped like the real transcript: right-aligned user bubbles, full-width assistant text lines, a tool-summary row with indented output lines. The connection copy (connecting / disconnected) stays underneath | The page-level `!state` branch in `app/page.tsx`; the component itself is `webapp/components/loading-states.tsx`|
1481
-
| The current turn's output (`running.active`) |`ActivityPulse` at the transcript tail — the desktop's three-dot loader plus a shimmer bar sitting where the next line of output will land, next to the phase label (thinking / working / …) |`components/chat.tsx#ThinkingIndicator`, gated by the exported pure function `isSessionActivityActive`|
1542
+
| The current turn's output (`running.active`) |`ActivityPulse` at the transcript tail — the desktop's three-dot loader plus a shimmer bar sitting where the next line of output will land. The label shows the phase the engine reported (thinking / working / …) first, then rotates through the desktop's weighted phrase table|`components/chat.tsx#ThinkingIndicator`, gated by the exported pure function `isSessionActivityActive`|
1482
1543
1483
1544
Invariants worth keeping when touching either branch:
1484
1545
@@ -1501,13 +1562,47 @@ Invariants worth keeping when touching either branch:
1501
1562
- Rendering tests for both components and the reduced-motion tripwire live in
1502
1563
`webapp/test/loading-skeleton.test.ts` (SSR through
1503
1564
`renderToStaticMarkup`; the suite has no DOM harness).
1565
+
-**Streaming-label phrase rotation (webui-parity 61, restoring the desktop
1566
+
shape)**: the desktop does not park one static label on screen for the
1567
+
length of a turn. The schedule and the draw are transcribed from the
1568
+
reference `ActivityIndicator.tsx`: 2000–3000 ms (drawn) before the first
1569
+
swap, 3500 ms between swaps after that, a weighted bucket draw — basic
1570
+
0.75, specific 0.15, motion 0.1 — uniform inside the drawn bucket, with
1571
+
the previous phrase filtered out so nothing repeats back to back. The
1572
+
tables are in `webapp/lib/thinking-phrases.ts`, indexed by `Locale` with
1573
+
the same structure in both languages; a separate module rather than the
1574
+
flat `lib/i18n.ts` dictionary, for the reason `lib/i18n-agent-team.ts`
1575
+
gives. The draw and the schedule are the exported pure functions
1576
+
`pickWeightedPhrase` / `computeThinkingPhraseStartDelay`, and the timer
1577
+
lives in `ActivityPulse` alone.
1578
+
-**Under `prefers-reduced-motion` the rotation continues, deliberately.** A
1579
+
phrase swap is a discrete text replacement, not motion: there is no
1580
+
translation, no scaling and no continuous movement for a vestibular
1581
+
trigger to react to, and the desktop makes the same trade — its
1582
+
reduced-motion branch halts the lottie and leaves the label ticking.
1583
+
Stopping the rotation would re-install the frozen-label defect G5 exists
1584
+
to remove. The animated half of the indicator (three dots, shimmer) is
1585
+
still switched off by the explicit `globals.css` rules above, so a
1586
+
reduce-motion user sees a still indicator with a still-ticking label.
1587
+
-**No side effects on streaming**: the rotation's state lives in
1588
+
`ActivityPulse` itself, so a swap re-renders that one `<span>`
1589
+
(`activity-indicator-label`) and nothing above it — the transcript
1590
+
bodies, the markdown and the streaming cursor are not in the update path.
1591
+
The effect depends on the phrase table alone (a module-level constant
1592
+
looked up by locale, stable identity), never on a per-render closure and
1593
+
never on `label` — the phase copy changes with the engine, and depending
1594
+
on it would reset the schedule mid-turn. The timer is a chained
1595
+
`setTimeout` rather than a `setInterval` and is cleared on cleanup; the
1596
+
component unmounts when the turn settles, leaving no pending timer.
1597
+
Effects do not run on the server, so the SSR and hydration first frames
1598
+
are both the phase copy — no first-paint swap.
1504
1599
- Boundary with ticket 46: while a thought streams, the 「推理中...」+
1505
1600
ticking-seconds readout lives on the thinking block's summary row inside
1506
1601
the tail activity group, and the live 「已执行 N 秒」 turn bar sits at
1507
1602
the transcript tail (see the "Session rendering" section above); the
1508
-
`ActivityPulse` in this table (three dots + shimmer + phase label)
1509
-
also appears only at the transcript tail, below the turn bar.
1510
-
Different positions, different jobs; none replaces another.
1603
+
`ActivityPulse` in this table (three dots + shimmer + phase label +
1604
+
rotating phrases) also appears only at the transcript tail, below the
1605
+
turn bar. Different positions, different jobs; none replaces another.
0 commit comments