web: move the 1:1 Oo status into the chat header (#193)

The strip under the header of a running 1:1 chat repeated what the header
already carried (a "Present" pill plus a persona line). The header's second
line now shows what the Oo is doing (Working, Thinking, waiting for input,
needs attention, or a delegation), and the bot's role while it rests. The
stage only appears in an empty chat and no longer repeats the name.


Claude-Session: https://claude.ai/code/session_01H3buE7EyYCfz8nYccLCcct

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-10 09:55:01 +02:00 committed by GitHub
parent a746783c48
commit 81c4bfd7e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 308 additions and 244 deletions

View File

@ -0,0 +1,30 @@
import { expect, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, signup } from "./helpers";
test("a 1:1 chat keeps name and status in the header instead of a banner", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await signup(page, `oo-header-${Date.now()}@rakazo.test`, "password12", "Header QA");
await completeOnboarding(page);
const composer = page.getByRole("combobox", { name: /^Message/ });
await composer.fill("Please review `shared/PROJECT_CHECKPOINT_WRAPUP.md`.");
await composer.press("Enter");
await expect(
page
.getByTestId("transcript")
.locator("[data-message-id]")
.filter({ hasText: "done. i handled:" }),
).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId("oo-stage")).toHaveCount(0);
await expect(page.getByTestId("bot-settings-trigger")).toBeVisible();
await page.emulateMedia({ colorScheme: "light" });
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await captureScreenshot(page, testInfo, "oo-chat-header-light");
await page.emulateMedia({ colorScheme: "dark" });
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await captureScreenshot(page, testInfo, "oo-chat-header-dark");
});

View File

@ -26,7 +26,9 @@ import { personaIdlePrompt, personaThinkingLines } from "../../lib/voice-persona
import { VoiceAwarenessStrip } from "../voice/VoiceAwarenessStrip";
import { StageHero } from "./StageHero";
import { TextStageCanvas } from "./TextStageCanvas";
import { useOoPresence } from "./use-oo-presence";
/** The empty 1:1 chat: the Oo greets. Name and live status live in the chat header. */
export function OoStage({
botId,
botName,
@ -36,7 +38,6 @@ export function OoStage({
botStatus,
snapshot,
running,
hasMessages,
}: {
botId: string;
botName: string;
@ -46,117 +47,24 @@ export function OoStage({
botStatus?: string;
snapshot: ThreadSnapshot | null;
running: boolean;
hasMessages: boolean;
}) {
const { t } = useLingui();
const [reducedMotion, setReducedMotion] = useState(false);
const [thinkingIndex, setThinkingIndex] = useState(0);
const persona = useMemo(
() =>
resolveBotPersona({
name: botName,
title: botTitle,
description: botDescription,
}),
[botName, botTitle, botDescription],
);
const streaming = useMemo(
() =>
(snapshot?.messages ?? []).some(
(message) =>
message.id.startsWith("progress:") &&
message.blocks.some((block) => block.kind === "progress" || block.kind === "text"),
),
[snapshot?.messages],
);
const awarenessEvent = useMemo(
() => extractVoiceAwareness(snapshot, botName),
[snapshot, botName],
);
const collaborating = Boolean(awarenessEvent);
const phase = resolveTextStagePhase({
running,
const { persona, phase, pose, subtitle, energy, reducedMotion, awarenessEvent, awarenessLabel } =
useOoPresence({
botName,
botTitle,
botDescription,
botStatus,
streaming,
hasMessages,
awareness: awarenessEvent,
snapshot,
running,
hasMessages: false,
});
const pose =
phase === "idle" && !hasMessages
? "greeting"
: personaTextPose(
persona,
phase,
collaborating &&
(awarenessEvent?.kind === "subagent" ||
awarenessEvent?.kind === "handoff" ||
awarenessEvent?.kind === "delegation"),
);
const thinkingLines = useMemo(
() => personaThinkingLines(persona.archetype).map((line) => t(line)),
[persona.archetype, t],
);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
function sync() {
setReducedMotion(media.matches);
}
sync();
media.addEventListener("change", sync);
return () => media.removeEventListener("change", sync);
}, []);
useEffect(() => {
if (phase !== "working" && phase !== "thinking") return;
const interval = setInterval(() => {
setThinkingIndex((index) => (index + 1) % Math.max(thinkingLines.length, 1));
}, cadencePulseMs(persona.cadence));
return () => clearInterval(interval);
}, [phase, persona.cadence, thinkingLines.length]);
const awarenessLabel = awarenessEvent
? narrateAwarenessForPersona(persona, awarenessEvent)
: null;
const subtitle = useMemo(() => {
if (awarenessLabel) return awarenessLabel;
if (phase === "error") return t`Something needs your attention.`;
if (phase === "working" || phase === "thinking") {
return thinkingLines[thinkingIndex] ?? t`Working on your request…`;
}
if (phase === "listening") return t`Waiting for your input…`;
if (phase === "speaking") return t`Composing a reply…`;
return t(personaIdlePrompt(persona.archetype));
}, [awarenessLabel, phase, persona.archetype, t, thinkingIndex, thinkingLines]);
const energy =
phase === "speaking" ? 0.65 : phase === "working" || phase === "thinking" ? 0.48 : 0.18;
const phaseLabel =
phase === "working"
? t`Working`
: phase === "listening"
? t`Listening`
: phase === "thinking"
? t`Thinking`
: phase === "speaking"
? t`Speaking`
: phase === "error"
? t`Needs attention`
: t`Present`;
return (
<section
data-testid="oo-stage"
aria-label={t`1:1 conversation`}
className={`relative shrink-0 overflow-hidden border-b border-border/60 ${hasMessages ? "max-h-[34vh]" : ""}`}
className="relative shrink-0 overflow-hidden border-b border-border/60"
>
<TextStageCanvas
phase={phase}
@ -165,50 +73,7 @@ export function OoStage({
energy={energy}
/>
<div
className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}
>
{(hasMessages || phase !== "idle") && (
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
<div
role="status"
className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/5 px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
style={
reducedMotion
? undefined
: {
animation: `voicePrimaryPulse ${cadencePulseMs(persona.cadence)}ms ease-in-out infinite`,
}
}
>
<span
className={`size-1.5 rounded-full ${
phase === "listening"
? "bg-foreground"
: phase === "speaking"
? "bg-success"
: phase === "error"
? "bg-destructive"
: "bg-foreground/70"
} ${reducedMotion ? "" : "animate-pulse"}`}
/>
<span>{phaseLabel}</span>
</div>
</div>
)}
{!hasMessages ? (
<div className="mb-3 text-center">
<h2 className="truncate text-ui-title font-semibold tracking-tight text-foreground">
{botName}
</h2>
{persona.roleLine ? (
<p className="mt-0.5 truncate text-ui-body-sm text-muted-foreground">
{persona.roleLine}
</p>
) : null}
</div>
) : null}
<div className="relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 py-5 md:px-8 md:py-6">
{awarenessLabel && awarenessEvent ? (
<div className="mb-3 w-full">
<VoiceAwarenessStrip
@ -220,8 +85,6 @@ export function OoStage({
</div>
) : null}
{!hasMessages ? (
<>
<StageHero
botId={botId}
botColor={botColor}
@ -239,17 +102,77 @@ export function OoStage({
>
{subtitle}
</p>
</>
) : (
<p className="mt-1 w-full max-w-xl truncate text-center text-ui-caption text-muted-foreground">
{subtitle}
</p>
)}
</div>
</section>
);
}
/** Second header line of a 1:1 chat: what the Oo is doing, or its role while it rests. */
export function OoHeaderStatus({
botName,
botTitle,
botDescription,
botStatus,
snapshot,
running,
hasMessages,
}: {
botName: string;
botTitle: string;
botDescription: string;
botStatus?: string;
snapshot: ThreadSnapshot | null;
running: boolean;
hasMessages: boolean;
}) {
const { t } = useLingui();
const { phase, awarenessLabel, reducedMotion } = useOoPresence({
botName,
botTitle,
botDescription,
botStatus,
snapshot,
running,
hasMessages,
});
const text =
phase === "idle"
? botTitle.trim()
: (awarenessLabel ??
(phase === "working"
? t`Working`
: phase === "thinking"
? t`Thinking`
: phase === "speaking"
? t`Speaking`
: phase === "listening"
? t`Waiting for your input…`
: t`Needs attention`));
if (!text) return null;
return (
<span
role="status"
data-testid="oo-header-status"
data-phase={phase}
className="flex min-w-0 items-center gap-1.5 text-ui-body-sm font-medium text-muted-foreground/80 leading-snug"
>
{phase === "idle" ? null : (
<span
aria-hidden="true"
className={`size-1.5 shrink-0 rounded-full ${
phase === "speaking"
? "bg-success"
: phase === "error"
? "bg-destructive"
: "bg-foreground/70"
} ${reducedMotion ? "" : "animate-pulse"}`}
/>
)}
<span className="truncate">{text}</span>
</span>
);
}
function StageEnsemble({
members,
workingBotIds,

View File

@ -0,0 +1,133 @@
import { useLingui } from "@lingui/react/macro";
import type { ThreadSnapshot } from "@rakazo/contracts";
import { useEffect, useMemo, useState } from "react";
import {
cadencePulseMs,
extractVoiceAwareness,
narrateAwarenessForPersona,
personaTextPose,
resolveBotPersona,
resolveTextStagePhase,
} from "../../lib/bot-persona";
import { personaIdlePrompt, personaThinkingLines } from "../../lib/voice-persona-copy";
/** What a 1:1 Oo is doing right now, shared by the chat header and the empty-chat stage. */
export function useOoPresence({
botName,
botTitle,
botDescription,
botStatus,
snapshot,
running,
hasMessages,
}: {
botName: string;
botTitle: string;
botDescription: string;
botStatus?: string;
snapshot: ThreadSnapshot | null;
running: boolean;
hasMessages: boolean;
}) {
const { t } = useLingui();
const [reducedMotion, setReducedMotion] = useState(false);
const [thinkingIndex, setThinkingIndex] = useState(0);
const persona = useMemo(
() =>
resolveBotPersona({
name: botName,
title: botTitle,
description: botDescription,
}),
[botName, botTitle, botDescription],
);
const streaming = useMemo(
() =>
(snapshot?.messages ?? []).some(
(message) =>
message.id.startsWith("progress:") &&
message.blocks.some((block) => block.kind === "progress" || block.kind === "text"),
),
[snapshot?.messages],
);
const awarenessEvent = useMemo(
() => extractVoiceAwareness(snapshot, botName),
[snapshot, botName],
);
const collaborating = Boolean(awarenessEvent);
const phase = resolveTextStagePhase({
running,
botStatus,
streaming,
hasMessages,
awareness: awarenessEvent,
});
const pose =
phase === "idle" && !hasMessages
? "greeting"
: personaTextPose(
persona,
phase,
collaborating &&
(awarenessEvent?.kind === "subagent" ||
awarenessEvent?.kind === "handoff" ||
awarenessEvent?.kind === "delegation"),
);
const thinkingLines = useMemo(
() => personaThinkingLines(persona.archetype).map((line) => t(line)),
[persona.archetype, t],
);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
function sync() {
setReducedMotion(media.matches);
}
sync();
media.addEventListener("change", sync);
return () => media.removeEventListener("change", sync);
}, []);
useEffect(() => {
if (phase !== "working" && phase !== "thinking") return;
const interval = setInterval(() => {
setThinkingIndex((index) => (index + 1) % Math.max(thinkingLines.length, 1));
}, cadencePulseMs(persona.cadence));
return () => clearInterval(interval);
}, [phase, persona.cadence, thinkingLines.length]);
const awarenessLabel = awarenessEvent
? narrateAwarenessForPersona(persona, awarenessEvent)
: null;
const subtitle = useMemo(() => {
if (awarenessLabel) return awarenessLabel;
if (phase === "error") return t`Something needs your attention.`;
if (phase === "working" || phase === "thinking") {
return thinkingLines[thinkingIndex] ?? t`Working on your request…`;
}
if (phase === "listening") return t`Waiting for your input…`;
if (phase === "speaking") return t`Composing a reply…`;
return t(personaIdlePrompt(persona.archetype));
}, [awarenessLabel, phase, persona.archetype, t, thinkingIndex, thinkingLines]);
const energy =
phase === "speaking" ? 0.65 : phase === "working" || phase === "thinking" ? 0.48 : 0.18;
return {
persona,
phase,
pose,
subtitle,
energy,
reducedMotion,
awarenessEvent,
awarenessLabel,
} as const;
}

View File

@ -1040,10 +1040,6 @@ msgstr "Hase"
msgid "Bushy"
msgstr "Buschig"
#: src/pages/Shell.tsx
msgid "Busy"
msgstr "Beschäftigt"
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr "Ruhige Hinweistöne für erledigte Arbeit, Sprachphasen und Übergaben. Berücksichtigt reduzierte Bewegung."
@ -1409,7 +1405,7 @@ msgstr "Kommunikationsstil"
msgid "Complete"
msgstr "Abschließen"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr "Schreibt eine Antwort…"
@ -4855,7 +4851,7 @@ msgstr "Zart"
msgid "Software update"
msgstr "Software-Update"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr "Etwas braucht deine Aufmerksamkeit."
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "Warte, bis die API wieder da ist…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr "Wartet auf deine Eingabe…"
@ -5706,7 +5703,7 @@ msgstr "Arbeitet"
msgid "Working in my lane…"
msgstr "Arbeite in meinem Fachgebiet…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr "Arbeitet an deiner Anfrage…"

View File

@ -1040,10 +1040,6 @@ msgstr "Bunny"
msgid "Bushy"
msgstr "Bushy"
#: src/pages/Shell.tsx
msgid "Busy"
msgstr "Busy"
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
@ -1409,7 +1405,7 @@ msgstr "Communication style"
msgid "Complete"
msgstr "Complete"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr "Composing a reply…"
@ -4855,7 +4851,7 @@ msgstr "Soft"
msgid "Software update"
msgstr "Software update"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr "Something needs your attention."
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "Waiting for the API to come back…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr "Waiting for your input…"
@ -5706,7 +5703,7 @@ msgstr "Working"
msgid "Working in my lane…"
msgstr "Working in my lane…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr "Working on your request…"

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "Completado"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr "Actualización de software"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "Esperando a que vuelva la API…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "पूर्ण"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "완료"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr "소프트웨어 업데이트"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "API가 다시 작동하기를 기다리는 중…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "Concluir"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "Завершить"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr "Обновление программного обеспечения"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "Ждём возвращения API…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "Tamamlandı"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -1040,10 +1040,6 @@ msgstr ""
msgid "Bushy"
msgstr ""
#: src/pages/Shell.tsx
msgid "Busy"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "Calm audio cues for finished work, voice phases, and handoffs. Respects reduced motion."
msgstr ""
@ -1409,7 +1405,7 @@ msgstr ""
msgid "Complete"
msgstr "完成"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Composing a reply…"
msgstr ""
@ -4855,7 +4851,7 @@ msgstr ""
msgid "Software update"
msgstr "软件更新"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Something needs your attention."
msgstr ""
@ -5553,6 +5549,7 @@ msgid "Waiting for the API to come back…"
msgstr "正在等待 API 恢复…"
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Waiting for your input…"
msgstr ""
@ -5706,7 +5703,7 @@ msgstr ""
msgid "Working in my lane…"
msgstr ""
#: src/components/stage/OoStage.tsx
#: src/components/stage/use-oo-presence.ts
msgid "Working on your request…"
msgstr ""

View File

@ -91,7 +91,7 @@ import { ArtifactPane } from "../components/ArtifactPane";
import { InAppBrowser } from "../components/browser/InAppBrowser";
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
import { MainInbox } from "../components/MainInbox";
import { OoGroupStage, OoStage } from "../components/stage/OoStage";
import { OoGroupStage, OoHeaderStatus, OoStage } from "../components/stage/OoStage";
import { useSidebarResize } from "../hooks/useSidebarResize";
import { useWideLayout } from "../hooks/useWideLayout";
import { readActivityMode, writeActivityMode } from "../lib/activity-mode";
@ -3357,10 +3357,16 @@ export function ShellPage() {
? (activeGroup?.name ?? activeSnapshot?.groupName ?? t`Group`)
: (active?.name ?? t`Choose a conversation`)}
</span>
{!inGroup && active && (composerRunning || active.status !== "idle") ? (
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{composerRunning || active.status === "running" ? t`Busy` : active.status}
</span>
{!inGroup && active ? (
<OoHeaderStatus
botName={active.name}
botTitle={active.title}
botDescription={active.description}
botStatus={stageWaitingStatus ?? active.status}
snapshot={activeSnapshot}
running={sending || stageRunning}
hasMessages={transcriptMessages.length > 0}
/>
) : multiOoInGroup && projectSessionKey ? (
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{t`Group conversation`}
@ -3459,7 +3465,7 @@ export function ShellPage() {
</div>
) : (
<>
{!callOpen && active && !inGroup ? (
{!callOpen && active && !inGroup && transcriptMessages.length === 0 ? (
<OoStage
botId={active.id}
botName={active.name}
@ -3469,7 +3475,6 @@ export function ShellPage() {
botStatus={stageWaitingStatus ?? active.status}
snapshot={activeSnapshot}
running={stageRunning}
hasMessages={transcriptMessages.length > 0}
/>
) : null}
{!callOpen && inGroup ? (