Skip to content

Fix/2249 - #375

Merged
conradmugabe merged 19 commits into
mainfrom
fix/2249
Jul 31, 2026
Merged

Fix/2249#375
conradmugabe merged 19 commits into
mainfrom
fix/2249

Conversation

@conradmugabe

@conradmugabe conradmugabe commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • Tests were added/updated according to the feature/bugfix/change made
  • Version was rolled according to semver requirements
  • API endpoints openapi schema was updated if applicable

Changes

Screenshots

Screencast.from.2026-07-29.19-44-34.mp4
Screencast.from.2026-07-29.19-44-08.mp4

conradmugabe and others added 12 commits July 26, 2026 23:30
Pre-existing formatting drift surfaced by the pre-commit hook, which runs
prettier across the whole codebase. Isolated here so it does not pollute
the functional commits that follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the message keys used by the voice call modal's status caption and
live transcript band: call status states, speaker labels, the empty state
and the jump-to-latest affordance.

Translated across en, es, fr and zh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Subscribes to RoomEvent.TranscriptionReceived and accumulates an ordered
transcript for the lifetime of a call.

Unlike the screen-sharing relay, which holds a single utterance and clears
it five seconds after it finalises, this keeps the full session so the user
can read back what was said.

- segment ids update their entry in place as speech grows; unknown ids append
- isFinal is sticky, so a late partial cannot reopen a finalised line
- timestamps are first-seen, so entries never reorder while updating
- speaker attribution compares against the local participant identity rather
  than falling back to the mentor name first, which would label every remote
  speaker as the mentor
- segment joining tolerates both cumulative and disjoint server behaviour
- every event is logged so an empty transcript can be diagnosed as agent
  silence rather than a UI fault

Not yet wired into the modal; the screen-sharing relay is left untouched and
marked as a convergence opportunity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nscript

A single isMuted flag drove both setMicrophoneEnabled and the muted prop on
RoomAudioRenderer, so muting your own microphone also silenced the mentor,
with nothing in the UI to say so.

Splits it into isMicMuted and isMentorAudioMuted with independent controls.
RoomAudioRenderer's muted prop is used alone rather than also calling
setVolume: it maps over subscribed remote tracks via useTracks, which is
reactive, so late joiners are covered without a re-apply.

The animated blob had the same conflation in visual form. It keyed off
!isMicMuted, so muting yourself made a live call look dead, and it never
reflected the mentor at all. It now animates whenever connected: the wave
bars track the user's voice, a violet ring tracks the mentor's, and both can
show at once.

Adds the live transcript band beneath the blob, backed by
useLiveKitTranscription. Newest line is prominent, older lines recede,
in-progress lines carry a caret until they finalise. Auto-scroll pins to the
latest line but yields when the user scrolls up to read back.

The two stacked status rows from the first pass are gone; they duplicated
both the blob and the button icons. One role=status caption replaces them,
and the transcript is a polite role=log so new lines are announced without
re-reading the history.

Regression tests assert each control leaves the other untouched, and that
the animation survives muting the microphone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The voice call close handler was identical to the screen sharing one except
that screen sharing calls refetchChats() on teardown and voice call did not.

Both modes pass the same session_id when minting call credentials — the same
id the chat thread renders — so the backend has what it needs to append the
conversation to that session. Screen sharing already refetches to pick it up.

Unverified: we have not confirmed the backend persists voice sessions. This
is symmetric with screen sharing and harmless if nothing new comes back, but
it needs checking against a live call before we rely on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the violet and indigo accents on the mentor-speaking ring, the
agent's transcript label, the in-progress caret and the live pill with
blues.

Speakers stay distinguishable by shade rather than hue: the mentor takes the
deeper blue that matches its ring, the user the lighter blue of the blob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The call interface had grown busy: the orb, a mentor ring, a status caption
and an always-on scrolling transcript all competing in one dialog.

ChatGPT Advanced Voice, Gemini Live and Google Meet converge on the same
shape — one animated indicator carrying call state, and captions opt-in
behind a CC button that is off by default. Gemini persists the choice across
calls and shows three fixed lines rather than a scrollback, on the basis
that the full record belongs to the transcript you read after hanging up.

This adopts that shape:

- adds a captions button as a fourth peer control, off by default, with the
  preference persisted in localStorage and guarded for SSR and for Safari
  private mode, where storage access throws
- when on, renders only the last three entries, newest at the bottom, at a
  fixed height with no scrolling
- removes the auto-scroll, the scroll-to-suspend behaviour, the
  jump-to-latest control and the live pill, along with the state, refs and
  translation keys that existed only to serve them
- moves the status caption to sr-only, preserving its precedence and live
  region so assistive tech still hears what changed, while sighted users
  read state off the orb

The transcription hook is unchanged and still accumulates the whole call; it
is only the rendering that is bounded.

Known gap: an absent ring now reads the same whether the mentor is silent or
muted. The desaturated orb and the agent button's own state are the only
remaining cues for sighted users.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Captions intermittently pushed the modal wider than its own max width. The
cause was `truncate` on the older caption lines: it expands to
white-space: nowrap, and a nowrap box's intrinsic minimum width is its
entire unwrapped line. DialogContent is a CSS grid whose item had no
min-w-0, and grid items default to min-width: auto, so that intrinsic width
propagated up and sized the dialog past max-w-lg.

That explains why it only sometimes happened: a short line stayed under the
limit, a five-hundred-character agent turn did not. The band's own
overflow-hidden clipped the text visually while doing nothing to the
min-width chain, which is why it looked contained until it wasn't.

Replaces truncate with line-clamp, which wraps rather than forcing one line,
and adds min-w-0 to every box between the dialog's grid item and the caption
lines, plus break-words so an unbroken URL cannot reproduce it by the other
route. The band was also capped at max-w-xl, wider than the dialog's own
max-w-lg and therefore unreachable; it is now max-w-md.

Reshapes the captions at the same time. Real agent turns are paragraphs, not
one-liners, so three lines competing for one box left the oldest as a
truncated sliver. Captions now show the exchange: the newest line, plus the
most recent line from the other speaker, selected by speaker rather than
position so consecutive turns from one voice cannot hide the reply. Each
gets its own slot — the answered turn clamped to two rows, the live line in
its own viewport clipping against the bottom — so a long reply can no longer
crowd out its own question.

Shortens the captions tooltip to one word; the aria-label still describes
the action. Adds collision padding to the control tooltips, though they
portal to body and so resolve against the viewport rather than the dialog.

Regression tests use a four-hundred-character multi-paragraph turn and an
unbreakable URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The live caption sat about fifty pixels below the line above it. The live
window was flex-1, so it claimed all the band's leftover height and its own
justify-end then dropped a short reply to the bottom of that claim. The dead
air was invisible only when the agent's turn happened to be long enough to
fill the box.

Replaces the grow with a ceiling of 3.6875rem — the band's 6rem less the
two-row worst case of the answered turn, so the live box can never starve
the line above it. That height also holds exactly three text-sm rows, so the
cap falls on a whole line rather than clipping a fourth in half.

Both blocks now size to their content and stack flush at the bottom. Short
replies sit directly beneath the question; long ones grow to the cap and
clip inside their own box, which keeps the guarantee that a paragraph cannot
push the exchange off the top.

Drops the bottom margin from the newest line, which was floating the pair
off the band's lower edge and, with it, pushing three live rows past the cap.

The band stays 6rem. With growth gone the slack collects above the pair,
where there is no background or border to reveal it, and a fixed height
keeps the orb from shifting as captions change length.

Tests pin the intent — no grow class, the cap present, margins on the right
line — but jsdom has no layout engine, so the arithmetic above is the actual
guarantee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The call was a full-viewport dialog holding three small things, so most of
it was white space and the captions band inside it kept looking crushed
however it was sized. It is a card now, sized to what it holds, and the
mentor is the subject of it.

Presence: the mentor's own avatar, with a halo while the line is open and a
ring while they speak, replaces the abstract orb. That retires ten drifting
particles, five sound-wave bars and two competing pulses — nine animations
that decorated rather than informed. The caller's own voice moved off the
mentor's face and onto the microphone button they control.

Status: one pill (shared Badge) with a state dot and a running call clock.
"Listening…" / "Agent speaking" said what the ring, the dot and the controls
already show, and changed every few seconds doing it, so the words are now
sr-only — there is no ring to look at with a screen reader. The connection
message stays visible while connecting, since nothing else conveys it.

Transcript: the whole conversation rather than the last exchange, rendered
as the same bubbles as the chat thread the call belongs to, minus its action
toolbar — those actions need a saved message and a caption is not one until
the call ends. Consecutive segments from one speaker are folded into a turn:
LiveKit flushes an utterance in pieces, and a bubble per piece splits
sentences wherever the recogniser paused. The panel follows the live turn and
releases the moment the reader scrolls back.

Captions are on by default now; only an explicit stored "false" turns them
off, so a first call is captioned and storage failures fall back to on.

Throughout: shared Avatar/Badge/Button/Tooltip and the theme's own tokens
instead of hardcoded white, grey and red. The dialog's own close button is
gone — hanging up is the way out of a call, and two controls that both end it
is one too many; Escape and the overlay still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaiooFFF4z3yMLHqsdKMBk
…e row

Search now sits directly above the chats it searches, below the divider
rather than up with New Chat, and takes the same row treatment as its new
neighbours: same colour, radius, focus ring and label truncation. It had
been styled after the New Chat pill it used to sit under, which is no
longer where it lives.

"Search chats" is now "Search" and "Chats" is now "Recents", in all four
locales. The dialog's placeholder keeps the longer wording, which is what a
placeholder is for.

Neither group inside Recents is headed any more. "Recent" was a caps label
repeating the panel's own name back at the reader, and with it gone "Pinned"
had nothing to distinguish itself from — so pinned rows say so on the row:
the pin sits in the three-dot slot and steps aside on hover for the menu
that can unpin it. An unpinned row's slot stays empty until you reach for
it. The pin also gives way while the row menu is open or an action is in
flight, neither of which hover can see. Screen readers get the word, since
they cannot see the pin or where the row sits.

Fixes a pre-existing hover bug in passing: the row menus used the unnamed
`group-hover` variant, and the shared Sidebar wrapper is itself a `.group`,
so a pointer anywhere in the sidebar revealed every row's menu at once. They
are scoped to `group/chat-row` now. The same mistake is still live on the
projects rows (index.tsx), left alone here.

E2E page objects follow the labels, and the recent/pinned lists carry
testids because the headings they used to be located by are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaiooFFF4z3yMLHqsdKMBk
The slot sat at `right-1.5`, so its 24px box centred 6px inboard of the
chevrons on the Agents/Recents/Projects triggers above it — two icon columns
down the same edge, neither aligned to the other. Flush with the row's own
edge, the slot centres on the same axis: measured 231 for every chevron, the
pin and the three-dot icon alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaiooFFF4z3yMLHqsdKMBk
@conradmugabe conradmugabe self-assigned this Jul 28, 2026
@conradmugabe
conradmugabe requested a review from mamigot as a code owner July 28, 2026 13:35
@conradmugabe conradmugabe added the run-tests Trigger PR validation pipeline label Jul 28, 2026
@mamigot

mamigot commented Jul 28, 2026

Copy link
Copy Markdown
Member

Claude encountered an error —— View job


I'll analyze this and get back to you.

@conradmugabe
conradmugabe requested a review from sonegillis1 July 29, 2026 16:39
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR E2E - OK

Full report, traces and screenshots

result OK
suite mentor - mode full - env stg1
tested against prod release 1.119.0 (env already on it)
failed 0
new failures 0

sonegillis1 and others added 3 commits July 30, 2026 04:30
pr-e2e-tests.yml:
- Fail the summary gate when build-app-image fails/cancels. It only
  checked e2e + unit-tests; a failed image build makes e2e resolve to
  `skipped` (not `failure`), so a broken build went green with no e2e
  run at all. build-app-image is already in the job's `needs`.
- Offline-detection guard: count only in_progress/completed jobs as
  "started" (not transient waiting/pending), and widen the busy-probe
  window to the 20 most recent runs (matching the env picker) so a run
  queued behind a long suite isn't misdiagnosed as an offline runner.

pnpm-workspace.yaml:
- Correct the brace-expansion override comment: minimatch@3 is pulled by
  eslint@9 + plugins (not just rollup-plugin-copy), so pinning
  brace-expansion to the non-callable 5.0.8 is a latent `pnpm lint` crash
  if a `{}` brace glob is ever added to the eslint config. Document the
  hazard and the (untested) minimatch@3 -> ^10 mitigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/pr-e2e-tests.yml
conradmugabe and others added 2 commits July 31, 2026 22:00
The dedup test (TC2) failed twice in run 30658029299 because the mentor it
was driving got deleted mid-test.

beforeEach read the mentor id straight out of the landing redirect via
getPlatformContext(). That redirect follows recently-accessed-mentors on
the shared test account, so under parallel workers it hands back whichever
disposable mentor another spec created most recently. Both attempts adopted
one: "E2E Mentor 1785527199152", then journey 63's "E2E Eval Mentor-
1785527203140". Each spec's cleanup then deleted its own mentor on schedule,
settings/ and public-settings/ started 404ing, the chat never mounted and
.chat-user-message-query timed out at 90s.

TC2 is the only test in the file long enough to overlap a delete (3.0m and
2.5m, against 17-30s for the rest), which is why it alone failed.

Create and own a mentor per test instead, reaped per suite via MentorTracker
so the delete cannot race a retry reusing the worker. Timeout goes to 300s to
cover creation on top of the existing two page loads and two AI round trips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQ7pVFebzkWMaey1z3tMKK
@bnsoni
bnsoni self-requested a review July 31, 2026 22:55
@conradmugabe
conradmugabe merged commit 29b163b into main Jul 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-tests Trigger PR validation pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants