From c939202672f21719949cebd53a63f7cd84e079e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 16:28:17 +0000 Subject: [PATCH 1/4] Give the voice-agent transcript distinct you/agent colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live transcript tinted only the "you:"/"agent:" prefix, and the two colors were near-identical cobolt purples (Cobolt 400 vs 300) that downsampled to the *same* 16-color ANSI slot — so on a basic terminal the whole conversation read as one color and you couldn't tell your turns from the agent's at a glance. Tint the whole transcript line (label + body) in the speaker's hue, and give the agent a contrasting teal (downsamples to ANSI cyan) while "you" keeps the reserved brand purple. The two now stay clearly distinct on any terminal, truecolor or not. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AFrwHX8EdECmkNsU8LqJNf --- aai_cli/agent/render.py | 9 +++++++-- aai_cli/ui/theme.py | 22 ++++++++++++++++------ tests/test_agent_render.py | 23 +++++++++++++++++++++++ tests/test_theme.py | 14 ++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/aai_cli/agent/render.py b/aai_cli/agent/render.py index e64df83f..98f6a2b9 100644 --- a/aai_cli/agent/render.py +++ b/aai_cli/agent/render.py @@ -9,8 +9,13 @@ def _labeled(label: str, body: str, *, style: str = "aai.label") -> Text: - """A line whose `label` prefix is accented in `style` and whose body is default.""" - return Text.assemble((label, style), body) + """A transcript line tinted entirely in `style` — both the `label` prefix and the body. + + Coloring the whole turn (not just the prefix) is what makes a "you:" line and an + "agent:" line read as different colors top to bottom; the two styles resolve to + contrasting hues (see ``theme.aai.you`` / ``theme.aai.agent``). + """ + return Text(f"{label}{body}", style=style) class AgentRenderer(BaseRenderer): diff --git a/aai_cli/ui/theme.py b/aai_cli/ui/theme.py index bb4238d5..5d62c8d3 100644 --- a/aai_cli/ui/theme.py +++ b/aai_cli/ui/theme.py @@ -17,10 +17,18 @@ # Brand accent. Defined once so the whole CLI can be re-tinted here. BRAND = COBOLT_400 -# Secondary accent — the lighter Cobolt 300 used where a second hue is needed (agent -# label, links), so it stays in the brand family yet reads distinct from BRAND. +# Secondary accent — the lighter Cobolt 300 used where a second hue is needed (links), +# so it stays in the brand family yet reads distinct from BRAND. ACCENT = COBOLT_300 +# The agent's voice in a live conversation. A cool teal chosen *outside* the cobolt +# purple family on purpose: "you" keeps the brand purple, and the two were previously +# both purples (Cobolt 400 vs 300) that collapsed to the same hue once a terminal +# downsampled truecolor to its 16-color palette — so "you:" and "agent:" lines looked +# identically colored. Teal downsamples to ANSI cyan, staying clearly distinct from the +# purple/blue "you" on any terminal. +AGENT = "#14B8A6" + # Semantic error red, matching the design system's --color-error. Rich downsamples it # to plain red on terminals without truecolor. ERROR = "#F04438" @@ -55,11 +63,13 @@ "aai.brand": f"bold {BRAND}", "aai.heading": f"bold {BRAND}", "aai.label": BRAND, - # Conversation labels: the human keeps the brand accent (reserved — never reused - # for a diarized speaker, see SPEAKER_STYLES), the agent gets a distinct hue so - # "you:" and "agent:" are easy to tell apart at a glance. + # Conversation colors: a whole transcript turn is tinted in its speaker's hue + # (not just the "you:"/"agent:" prefix), so the two read as different colors top + # to bottom. The human keeps the brand purple (reserved — never reused for a + # diarized speaker, see SPEAKER_STYLES); the agent gets a contrasting teal so the + # two are unmistakable at a glance, even after downsampling (see AGENT). "aai.you": BRAND, - "aai.agent": ACCENT, + "aai.agent": AGENT, # Links/URLs in the lighter Cobolt secondary accent so a clickable target stands # out from prose without shouting (Vercel/Supabase use a cool accent for the same). "aai.url": ACCENT, diff --git a/tests/test_agent_render.py b/tests/test_agent_render.py index 9b65d16a..e6216afe 100644 --- a/tests/test_agent_render.py +++ b/tests/test_agent_render.py @@ -192,3 +192,26 @@ def test_human_you_label_is_colored(): assert "you: " in out assert "what is the time" in out assert "\x1b[" in out + + +def _truecolor_sgr(style_name: str) -> str: + """The foreground SGR a truecolor console emits for a named theme style.""" + color = theme.make_console(color_system="truecolor").get_style(style_name).color + assert color is not None + t = color.get_truecolor() + return f"\x1b[38;2;{t.red};{t.green};{t.blue}m" + + +def test_human_whole_turn_is_tinted_in_speaker_color(): + # The whole line — label *and* body — is wrapped in the speaker's color, and the two + # speakers render in different colors, so a glance separates "you" from "agent". + r, buf = _human(color_system="truecolor") + r.user_final("hello") + r.agent_transcript("hi there", interrupted=False) + r.close() + out = buf.getvalue() + you_sgr, agent_sgr = _truecolor_sgr("aai.you"), _truecolor_sgr("aai.agent") + assert you_sgr != agent_sgr # contrasting hues, not two shades of the same color + # The body text rides inside the colored span (it follows the SGR, before any reset). + assert f"{you_sgr}you: hello" in out + assert f"{agent_sgr}agent: hi there" in out diff --git a/tests/test_theme.py b/tests/test_theme.py index 530254e9..ca5cde7c 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -55,6 +55,20 @@ def test_you_color_reserved_outside_speaker_palette(): assert you_color not in speaker_colors +def test_you_and_agent_stay_distinct_after_downsampling(): + # "you" and "agent" used to be two near-identical cobolt purples that downsampled to + # the *same* 16-color ANSI slot, so a transcript looked single-colored on a basic + # terminal. Assert they're different hues and stay different once downgraded. + from rich.color import ColorSystem + + console = theme.make_console() + you = console.get_style("aai.you").color + agent = console.get_style("aai.agent").color + assert you is not None and agent is not None + assert you != agent + assert you.downgrade(ColorSystem.STANDARD) != agent.downgrade(ColorSystem.STANDARD) + + def test_output_console_is_themed_and_error_is_styled(monkeypatch): from aai_cli.core.errors import CLIError from aai_cli.ui import output, theme From 4fc981ee5dfb0eece6cfaf27a8367d2a6f358c5f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:42:50 +0000 Subject: [PATCH 2/4] ci: harden Ubuntu audio-dep install against slow apt mirrors The Ubuntu `lint + typecheck + tests` cells kept timing out at the "System deps (PortAudio + ffmpeg)" step and getting cancelled before the tests ran, which bounced this PR out of the merge queue. Root cause: a plain `apt-get install ffmpeg` pulls a ~100-package / ~94 MB dependency closure, and against a degraded Azure Ubuntu mirror that download overran the job's 15-min timeout. Windows was unaffected (it installs only ffmpeg, in under a minute). Factor the three identical Ubuntu installs into scripts/ci_install_audio_deps.sh that (1) passes --no-install-recommends to drop the optional codec/VA-API/SDL recommends that balloon the payload, and (2) wraps each apt call in a bounded `timeout` + retry so a stalled mirror connection is killed and retried instead of wedging the whole job. Bump the two Ubuntu test jobs from 15 to 20 minutes (matching the Windows job) to give the retry path headroom. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AFrwHX8EdECmkNsU8LqJNf --- .github/workflows/ci.yml | 11 +++++----- scripts/ci_install_audio_deps.sh | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100755 scripts/ci_install_audio_deps.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a38cff6..ac33ec41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: check: name: lint + typecheck + tests (py${{ matrix.python-version }}) runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 # Test both ends of the supported range: 3.12 is the floor (requires-python), # 3.13 is what the Homebrew formula ships. fail-fast off so one version's # failure doesn't mask the other's. @@ -49,8 +49,9 @@ jobs: # PortAudio backs sounddevice; ffmpeg decodes non-WAV/URL audio (the `--sample` # stream tests build a FileSource for the hosted sample, which needs ffmpeg). + # Slow-mirror resilience (bounded retry + trimmed payload) lives in the script. - name: System deps (PortAudio + ffmpeg) - run: sudo apt-get update && sudo apt-get install -y libportaudio2 ffmpeg + run: ./scripts/ci_install_audio_deps.sh # check.sh lints Markdown and template JS/CSS via Node CLIs; versions are # pinned in scripts/gate_tool_pins.sh (shared with the web session-start @@ -245,7 +246,7 @@ jobs: pre-commit: name: pre-commit runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -257,7 +258,7 @@ jobs: # PortAudio backs sounddevice; ffmpeg decodes the `--sample` stream source. - name: System deps (PortAudio + ffmpeg) - run: sudo apt-get update && sudo apt-get install -y libportaudio2 ffmpeg + run: ./scripts/ci_install_audio_deps.sh # The local pytest hook runs `uv run --frozen python -m pytest`, so the tests # resolve the LOCKED dependency versions (uv.lock) rather than the newest @@ -341,7 +342,7 @@ jobs: # PortAudio + ffmpeg so `assembly --help` (which imports the full command # tree) loads cleanly; also lets install.sh's dep check find them present. - name: System deps (PortAudio + ffmpeg) - run: sudo apt-get update && sudo apt-get install -y libportaudio2 ffmpeg + run: ./scripts/ci_install_audio_deps.sh - name: Run install.sh (editable, from this checkout) run: ./install.sh --install-method git diff --git a/scripts/ci_install_audio_deps.sh b/scripts/ci_install_audio_deps.sh new file mode 100755 index 00000000..59b18fdb --- /dev/null +++ b/scripts/ci_install_audio_deps.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# CI-only helper: install the Linux system audio deps the suite needs — libportaudio2 +# (sounddevice's PortAudio backend) and ffmpeg (decodes non-WAV/URL audio for the +# `--sample` stream tests). Three Ubuntu jobs in .github/workflows/ci.yml install the +# identical pair, so the slow-mirror resilience below lives in one place. +# +# A plain `apt-get install ffmpeg` pulls a ~100-package / ~94 MB dependency closure, and +# against a degraded Azure Ubuntu mirror that download has repeatedly overrun the job's +# timeout — the step hung mid-download and was cancelled before the tests ran, bouncing +# the PR out of the merge queue. Two mitigations, mirroring the Windows ffmpeg step's +# bounded-retry philosophy: +# * --no-install-recommends drops the optional VA-API/VDPAU/SDL/pocketsphinx recommends +# that balloon the download; the ffmpeg binary and the libs the tests load remain. +# * each apt call is wrapped in `timeout` (run under sudo so the killer is root and can +# actually reap apt) and retried, so a connection that stalls on a bad mirror edge is +# killed and retried instead of wedging the whole job; apt's own Acquire::Retries +# handles transient per-file failures within an attempt. +set -euo pipefail + +# Run one bounded apt-get attempt, retrying a stalled/failed call a few times. +apt_retry() { + local attempt + for attempt in 1 2 3; do + if sudo timeout --kill-after=10s 120s apt-get "$@"; then + return 0 + fi + echo "::warning::apt-get $* failed or stalled (attempt ${attempt}/3); retrying" >&2 + sleep "$((attempt * 5))" + done + echo "::error::apt-get $* failed after 3 attempts" >&2 + return 1 +} + +apt_retry update -o Acquire::Retries=3 +apt_retry install -y --no-install-recommends -o Acquire::Retries=3 libportaudio2 ffmpeg From fe5ab3fd1da632c38cbaf083b488630eaf56c71a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:46:14 +0000 Subject: [PATCH 3/4] ci: only log "retrying" when an apt attempt remains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bounded retry loop printed the "retrying" warning (and slept) even on the final failed attempt, where no retry follows — misleading CI diagnostics. Guard the warning/sleep on attempt < 3 so the 3rd failure falls straight through to the terminal error. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AFrwHX8EdECmkNsU8LqJNf --- scripts/ci_install_audio_deps.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/ci_install_audio_deps.sh b/scripts/ci_install_audio_deps.sh index 59b18fdb..c06b72e6 100755 --- a/scripts/ci_install_audio_deps.sh +++ b/scripts/ci_install_audio_deps.sh @@ -24,8 +24,12 @@ apt_retry() { if sudo timeout --kill-after=10s 120s apt-get "$@"; then return 0 fi - echo "::warning::apt-get $* failed or stalled (attempt ${attempt}/3); retrying" >&2 - sleep "$((attempt * 5))" + # Only announce a retry when another attempt actually follows; the 3rd failure + # falls through to the terminal error below. + if [ "$attempt" -lt 3 ]; then + echo "::warning::apt-get $* failed or stalled (attempt ${attempt}/3); retrying" >&2 + sleep "$((attempt * 5))" + fi done echo "::error::apt-get $* failed after 3 attempts" >&2 return 1 From 71e652cdbc45b4152b8cf58fc7ac0f1d42bd6361 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 18:00:36 +0000 Subject: [PATCH 4/4] ci: fetch ffmpeg from GitHub CDN instead of the slow apt mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --no-install-recommends trimmed the apt payload (94 MB -> 62 MB) but not enough: ffmpeg's bulk is hard codec Depends, and the degraded Azure Ubuntu mirror (~100 KB/s) still couldn't deliver 62 MB inside the job timeout, so the bounded-retry apt install exhausted and failed. Stop fighting the mirror for ffmpeg: fetch a static, self-contained build off GitHub's release CDN (BtbN/FFmpeg-Builds — the same origin the Windows job already falls back to) and prepend it to GITHUB_PATH. apt now installs only the tiny libportaudio2 (no portable prebuilt), which a bounded retry rides out. Verified the download/extract and that the static binary runs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AFrwHX8EdECmkNsU8LqJNf --- scripts/ci_install_audio_deps.sh | 56 +++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/scripts/ci_install_audio_deps.sh b/scripts/ci_install_audio_deps.sh index c06b72e6..dcbac81e 100755 --- a/scripts/ci_install_audio_deps.sh +++ b/scripts/ci_install_audio_deps.sh @@ -1,39 +1,57 @@ #!/usr/bin/env bash # CI-only helper: install the Linux system audio deps the suite needs — libportaudio2 # (sounddevice's PortAudio backend) and ffmpeg (decodes non-WAV/URL audio for the -# `--sample` stream tests). Three Ubuntu jobs in .github/workflows/ci.yml install the -# identical pair, so the slow-mirror resilience below lives in one place. +# `--sample` stream tests; the require_ffmpeg probe needs it on PATH). Three Ubuntu jobs +# in .github/workflows/ci.yml need the identical pair, so the slow-mirror resilience +# below lives in one place. # -# A plain `apt-get install ffmpeg` pulls a ~100-package / ~94 MB dependency closure, and -# against a degraded Azure Ubuntu mirror that download has repeatedly overrun the job's -# timeout — the step hung mid-download and was cancelled before the tests ran, bouncing -# the PR out of the merge queue. Two mitigations, mirroring the Windows ffmpeg step's -# bounded-retry philosophy: -# * --no-install-recommends drops the optional VA-API/VDPAU/SDL/pocketsphinx recommends -# that balloon the download; the ffmpeg binary and the libs the tests load remain. -# * each apt call is wrapped in `timeout` (run under sudo so the killer is root and can -# actually reap apt) and retried, so a connection that stalls on a bad mirror edge is -# killed and retried instead of wedging the whole job; apt's own Acquire::Retries -# handles transient per-file failures within an attempt. +# The Azure Ubuntu apt mirror periodically degrades to a crawl (~100 KB/s). A plain +# `apt-get install ffmpeg` pulls ffmpeg's ~62 MB codec dependency closure, which then +# overruns the job timeout mid-download and gets cancelled before the tests run, bouncing +# the PR out of the merge queue. So, mirroring the Windows ffmpeg step's strategy of +# falling back to a static build off GitHub's release CDN: +# * libportaudio2 has no portable prebuilt, so it always comes from apt — but it's tiny, +# so a bounded retry rides out a slow mirror. +# * ffmpeg is fetched as a static build from GitHub's release CDN (BtbN/FFmpeg-Builds — +# the same origin the Windows job uses), bypassing apt's heavy codec chain on the +# flaky mirror entirely, and prepended to GITHUB_PATH so later steps see it. set -euo pipefail -# Run one bounded apt-get attempt, retrying a stalled/failed call a few times. +# Run one bounded apt-get attempt, retrying a stalled/failed call a couple of times. A +# stalled mirror connection is killed by `timeout` (run under sudo so the killer is root +# and can reap apt) rather than wedging the whole job; the 3rd failure falls through. apt_retry() { local attempt for attempt in 1 2 3; do if sudo timeout --kill-after=10s 120s apt-get "$@"; then return 0 fi - # Only announce a retry when another attempt actually follows; the 3rd failure - # falls through to the terminal error below. if [ "$attempt" -lt 3 ]; then echo "::warning::apt-get $* failed or stalled (attempt ${attempt}/3); retrying" >&2 sleep "$((attempt * 5))" fi done - echo "::error::apt-get $* failed after 3 attempts" >&2 + echo "::warning::apt-get $* failed after 3 attempts" >&2 return 1 } -apt_retry update -o Acquire::Retries=3 -apt_retry install -y --no-install-recommends -o Acquire::Retries=3 libportaudio2 ffmpeg +# A stale list is fine (the runner image's apt cache is recent), so don't let a slow +# `update` be fatal; libportaudio2 itself has no CDN fallback, so that one must succeed. +apt_retry update -o Acquire::Retries=3 || true +apt_retry install -y --no-install-recommends -o Acquire::Retries=3 libportaudio2 + +# ffmpeg from GitHub's release CDN, not apt: a static, self-contained build off a reliable +# origin sidesteps the 62 MB codec download the degraded apt mirror kept failing to serve. +url="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz" +dest="${RUNNER_TEMP:-/tmp}/ffmpeg-static" +mkdir -p "$dest" +curl -fsSL --retry 3 --retry-all-errors "$url" | tar -xJ -C "$dest" --strip-components=1 +echo "$dest/bin" >> "${GITHUB_PATH:-/dev/null}" +export PATH="$dest/bin:$PATH" + +command -v ffmpeg >/dev/null || { + echo "::error::ffmpeg unavailable after setup" >&2 + exit 1 +} +ffmpeg -version >/dev/null +echo "audio deps ready: libportaudio2 + ffmpeg ($(command -v ffmpeg))"