Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/bestie-voice.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# Bestie realtime voice

Open Bestie from its companion launcher anywhere the app offers the panel, then
click the headphones button. Speak naturally and interrupt a reply by speaking.
The panel shows transcripts, microphone mute, thinking level and tool approval.
click Call at the bottom of the panel. Speak naturally and interrupt a reply by speaking.
The ellipsis menu beside Close contains thinking level and tool approval settings.
During a call, the bottom controls show mute, live microphone activity and hang-up;
Bestie gently nods and tilts in response to its audio playback, changing its lean
after speech pauses and settling when quiet. The microphone
waveform uses monochrome bars and a mirrored layer at 20% opacity. Call controls
fade into a stable footer when starting, with immediate keyboard transitions. Reduced motion keeps
both indicators still while their opacity responds to audio. Transcripts are hidden
by default. Toggle **Show transcript** in the ellipsis menu at any time to reveal
or hide the current conversation using the Messages app’s message rows. When shown,
the space above the call controls splits evenly between Bestie and a separate
transcript scroll area. Messages soften into a subtle blur and fade at its top edge. Hiding
the transcript does not erase it or restart the call. The choice lasts while
Bestie is enabled in this app session. Tool approval requests remain visible.
Closing the panel, changing community/account, disabling Bestie or disconnecting
the relay ends the call. Immediate panel relocation retains the same call.

Expand Down
47 changes: 47 additions & 0 deletions docs/review/bestie-voice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Bestie voice review screenshots

Captured from the real Bestie components in Chromium using isolated identities, synthetic audio, and fixture transcript text. These are UI states, not recordings of a live model conversation.

## idle

![idle](idle.png)

## settings

![settings](settings.png)

## connecting

![connecting](connecting.png)

## listening

![listening](listening.png)

## speaking

![speaking](speaking.png)

## muted

![muted](muted.png)

## transcript

![transcript](transcript.png)

## transcript-dark

![transcript-dark](transcript-dark.png)

## transcript-compact

![transcript-compact](transcript-compact.png)

Reproduce with:

```sh
BESTIE_REVIEW_SCREENSHOTS=1 bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs bestie.spec.mjs --project chromium --project webkit --no-deps
```

Captures are written under `test-results/browser/`.
Binary file added docs/review/bestie-voice/connecting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/idle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/listening.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/muted.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/speaking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/transcript-compact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/transcript-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/bestie-voice/transcript.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/pages.integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
renderToStaticMarkup(
createElement(firstBestie.component, { target: "", close() {} }),
),
/Voice is available/,
/Voice isn’t available in this app\./,
);
await services.plugins.change("disable", "buzz.bestie");
assert.equal(
Expand Down
255 changes: 255 additions & 0 deletions src/bundled/bestie/Bestie.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
.panel {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
color: var(--text-primary);
}
.body {
display: grid;
flex: 1;
grid-template-rows: minmax(0, 1fr);
min-height: 0;
overflow: hidden;
padding: 0 24px;
}
.body[data-transcript="true"] {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
.identity {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
min-height: 0;
padding: 24px 0 16px;
}
.avatar {
position: relative;
display: grid;
place-items: center;
width: 176px;
height: 176px;
flex-shrink: 1;
min-height: 0;
}
.avatar img {
position: absolute;
inset: 0;
margin: auto;
max-width: 100%;
width: 160px;
height: 160px;
max-height: 100%;
object-fit: contain;
transform-origin: 50% 75%;
}
.status {
margin: 0;
text-align: center;
color: var(--text-secondary);
}
.transcriptViewport {
position: relative;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.transcript {
height: 100%;
min-width: 0;
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-width: thin;
padding-top: 32px;
scroll-padding-top: 32px;
mask-image: linear-gradient(to bottom, transparent, black 32px);
}
.transcriptViewport::after {
content: "";
position: absolute;
inset: 0 0 auto;
height: 32px;
pointer-events: none;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
mask-image: linear-gradient(to bottom, black, transparent);
}

.permission {
flex-shrink: 1;
min-height: 0;
max-height: 40%;
overflow: auto;
margin: 12px 24px 0;
padding: 16px;
border-radius: var(--radius-control);
background: var(--neutral-2);
}
.permission pre {
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
margin-block: 12px;
}
.decisions {
display: flex;
gap: 8px;
}
.controls {
--call-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
display: grid;
flex-shrink: 0;
align-items: center;
padding: 16px 24px 24px;
}
.callStart,
.callControls {
grid-area: 1 / 1;
min-width: 0;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
transition:
opacity var(--duration-state) var(--call-ease-out),
transform var(--duration-settle) var(--call-ease-out),
visibility 0s;
}
.callControls {
gap: 20px;
opacity: 0;
visibility: hidden;
transform: translateY(8px) scale(0.97);
transition-delay: 0s, 0s, var(--duration-state);
}
.callStart {
opacity: 1;
transform: translateY(0) scale(1);
}
.controls[data-active="true"] .callStart {
opacity: 0;
visibility: hidden;
transform: translateY(-4px) scale(0.96);
transition-delay: 0s, 0s, var(--duration-state);
}
.controls[data-active="true"] .callControls {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
transition-delay: 0s;
}
.muteControl,
.hangup {
display: flex;
flex-shrink: 0;
}
.controls button:active:not([data-disabled]) {
transform: scale(0.97);
}
.hangup button {
background: var(--red-9);
color: var(--text-on-accent);
}
.hangup button:hover:not([data-disabled]) {
background: var(--red-10);
}
.wave {
position: relative;
height: 48px;
min-width: 0;
flex: 1;
color: var(--text-secondary);
}
.waveFront,
.waveTrail {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.waveTrail {
opacity: 0.2;
transform: scaleX(-1);
}
.wave span {
display: block;
flex: 0 0 2px;
height: 40px;
background: currentColor;
border-radius: var(--radius-pill);
clip-path: inset(19px 0 round 1px);
}
.controls .wave {
opacity: 0;
transition: opacity var(--duration-state) var(--call-ease-out);
}
.controls[data-active="true"] .wave {
opacity: 1;
transition-delay: 40ms;
}
.controls[data-instant="true"] .callStart,
.controls[data-instant="true"] .callControls,
.controls[data-instant="true"] .wave {
transition: none;
}
.menuPositioner {
z-index: 1000;
}
.menu {
min-width: 220px;
max-height: var(--available-height);
overflow: auto;
padding: 6px;
border-radius: var(--radius-control);
border: 1px solid var(--border-primary);
background: var(--bg-float);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
.menuLabel {
padding: 8px 10px 4px;
color: var(--text-secondary);
}
.menuItem {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 7px 10px;
border-radius: var(--radius-row);
cursor: default;
}
.menuItem[data-highlighted] {
outline: none;
background: var(--neutral-4);
}
.menuItem[data-disabled] {
color: var(--text-disabled);
}
.separator {
height: 1px;
background: var(--border-primary);
margin: 6px;
}
.menuHint {
color: var(--text-secondary);
padding: 8px 10px;
margin: 0;
}

@media (prefers-reduced-motion: reduce) {
.callStart,
.callControls,
.controls[data-active="true"] .callStart,
.controls[data-active="true"] .callControls {
transform: none;
}
.controls button:active:not([data-disabled]) {
transform: none;
}
}
Loading
Loading