oficina / meet Public

Native gate checks the app's own glow; docs for glass and the glow

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev db3a52dd53eec2d8b6bb401fa4125cb1ba2fe5f5 parent a73fa04
4 files changed, +43 −3
  • native/DESIGN.md +32 −0
  • native/README.md +6 −3
  • native/apple/Sources/CallsKit/Call.swift +3 −0
  • test/browser/native.mjs +2 −0
diff --git a/native/DESIGN.md b/native/DESIGN.md
index 1dab73a..4fa47b8 100644
--- a/native/DESIGN.md
+++ b/native/DESIGN.md
@@ -156,3 +156,35 @@ Snapshots: `CallsInterop --call-snapshots <dir>` (also part of
iPhone's come from `CallsPhoneUITests.testFullScreenCallSnapshots` against the
debug build's `-CallsCallFixture` calls (`CallFixture`), written to
`CALLS_SNAPSHOT_DIR` or `/tmp/immersive-call`.
+
+## Glass and the speaking glow · 2026-09-25 (0.3.7)
+
+Supersedes the hairline edges and button tiles of 0.3.5's **Over the picture**.
+
+- **One glass language, no hairlines.** Every surface over the picture — the
+ bar, the title pill, notices, name labels, the iPhone's round buttons and the
+ mini window's tab — is borderless glass: Liquid Glass on macOS 26 and iOS 26,
+ the ultra-thin material before, with black at 62 % on it
+ (`CallOverlay.scrim`). Rendered over white the glass measures #5B5B5B, so
+ white on it is 6.8:1 (6.2:1 from the black alone). The environment value
+ `callOverlayGlass` turns the glass off to show the fallback.
+- **Buttons are glyphs on the pane**: white, a light wash under the pointer,
+ and a capsule only for a state — near-white for on (sharing, a panel open),
+ amber with the slashed symbol for microphone or camera off, red for Leave.
+- **Devices** over the picture is opaque and dark (`CallOverlay.panelFill`)
+ with the dark appearance's labels: 16.7:1 primary and 5.1:1 secondary as
+ rendered. It holds the controls up while open and never fades with them.
+- **The speaking glow** replaces the white ring round a tile and the meter
+ beside the name, on Mac, iPhone, web and the mini window's tab: the accent's
+ hue lit up (`CallsAppearance.glow`) as a halo behind the name label, bright
+ as the voice (`SpeakingGlow`, and `judgeGlow`/`glowStyle` in room.js). The
+ halo redraws on a timeline only until it arrives; on the web it is a
+ registered `--glow` transitioned by CSS. "No sound from …" and the microphone
+ warnings stay as words by the name; the Devices panel keeps a compact meter.
+
+Snapshots: `CallsInterop --glass-snapshots <dir>` renders real windows below
+the desktop and captures them with `screencapture -l` (an off-screen bitmap
+draws no Liquid Glass); the iPhone's are
+`CallsPhoneUITests.testGlowAndGlassSnapshots` (`-CallsFixtureSpeaking`,
+`-CallsFixtureNoGlass`).
+
diff --git a/native/README.md b/native/README.md
index 7d8233e..805f593 100644
--- a/native/README.md
+++ b/native/README.md
@@ -107,11 +107,14 @@ while sharing; the panel itself shows one warning line only when something is
wrong (a stretched or broken-up signal, echo cancellation off, noise reduction
bypassed, processing refused — `AudioWarning`).
-**Sound is measured, not inferred** (spec §15.20). On 2026-09-25 Tony, in a
+**Sound is measured, not inferred** (spec §15.20). Since 0.3.7 a tile shows it
+as the speaking glow behind the name (`SpeakingGlow`: -42 to -12 dBFS over a
+0.3 floor, lit once speech has kept up 0.15 s, 50 ms attack, 300 ms release,
+a steady 0.6 under Reduce Motion) in place of the white ring and the meter. On 2026-09-25 Tony, in a
browser, could not hear Paul on the Mac app, and Paul's panel looked fine
because nothing on it measured sound. Now:
-- **Your tile** and the panel's **Microphone level** meter what is sent — the
+- **Your tile's glow** and the panel's **Microphone level** meter what is sent — the
capture hook's level after noise reduction (`CaptureLevel`, read at 10 Hz).
Unmuted and capturing, with nothing reaching the hook above -90 dBFS
*before* the suppressor for five seconds, the tile says "Your microphone
@@ -120,7 +123,7 @@ because nothing on it measured sound. Now:
- **Everyone else's tile** meters what arrives from them: a renderer on their
remote audio track (`RemoteAudioTap`), which sees each decoded frame before
the output volume, read at 5 Hz. Muted, the tile shows the Muted badge and no
- meter. Reporting themselves unmuted, connected, with audio bytes still rising
+ glow. Reporting themselves unmuted, connected, with audio bytes still rising
(inbound-rtp, one `statistics(for:)` per receiver a second) and the decoded
peak under -85 dBFS for five seconds, it says "No sound from Bea" instead,
and the app reports those people as `noSoundFrom` through the bridge. A thin
diff --git a/native/apple/Sources/CallsKit/Call.swift b/native/apple/Sources/CallsKit/Call.swift
index c02bd4b..9f959a9 100644
--- a/native/apple/Sources/CallsKit/Call.swift
+++ b/native/apple/Sources/CallsKit/Call.swift
@@ -769,6 +769,9 @@ private final class Events: NSObject, BridgeListenerProtocol {
micPeakSinceSample = 0
values["noSoundFrom"] = UInt64(levels.noSoundFrom.count)
values["micSilent"] = levels.mic.silent ? 1 : 0
+ // The speaking glow behind your own name (0.3.7): lit while what is
+ // sent is speech that has kept up.
+ values["micGlow"] = levels.mic.glow.glow.lit ? 1 : 0
return values
}
}
diff --git a/test/browser/native.mjs b/test/browser/native.mjs
index abd8d74..d98bd65 100644
--- a/test/browser/native.mjs
+++ b/test/browser/native.mjs
@@ -143,6 +143,8 @@ try {
await wait(() => sample?.stats.micPeakPermille > 10, "the app's own meter sees what it sends");
assert.equal(sample.stats.micSilent, 0, "a live synthetic microphone is not reported as picking nothing up");
console.log(`PASS the app's microphone meter reads what it sends, with no silence warning (noSoundFrom ${sample.stats.noSoundFrom})`);
+ await wait(() => sample?.stats.micGlow === 1, "the app's own name glows while its synthetic voice speaks");
+ console.log('PASS the speaking glow behind the app\'s own name lights for its voice');
}
// Choose the system default on the audio device module during the live
// call, then refuse an unavailable microphone. Both must leave media and