Merge claude/media-smart-cuts into claude/media-brand-overlay
Brings in main with the squashed #158. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
commit
95538982d1
@ -78,7 +78,7 @@ test("sidebar Now and Recent surface active and terminal runs", async ({ page },
|
||||
await captureActivitySidebar(page, testInfo, "58-activity-now");
|
||||
|
||||
await page.getByRole("button", { name: "Stop", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.reload();
|
||||
await expect(activityToggle).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
@ -195,15 +195,24 @@ test("chat composer controls are vertically centered", async ({ page }) => {
|
||||
await signup(page, `composer-layout-${stamp}@rakazo.test`, "password12", "Composer Layout");
|
||||
await completeOnboarding(page);
|
||||
|
||||
const centers = await page.getByTestId("composer-bar").evaluate((composer) =>
|
||||
["Add", "Message", "Voice", "Send"].map((label) => {
|
||||
const bar = page.getByTestId("composer-bar");
|
||||
const centersOf = (labels: string[]) =>
|
||||
bar.evaluate(
|
||||
(composer, controls) =>
|
||||
controls.map((label) => {
|
||||
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
|
||||
if (!element) throw new Error(`Missing composer control: ${label}`);
|
||||
const box = element.getBoundingClientRect();
|
||||
return box.top + box.height / 2;
|
||||
}),
|
||||
labels,
|
||||
);
|
||||
|
||||
const idle = await centersOf(["Add", "Message", "Voice"]);
|
||||
await page.getByRole("combobox", { name: "Message" }).fill("hello");
|
||||
const typing = await centersOf(["Add", "Message", "Send"]);
|
||||
const centers = [...idle, ...typing];
|
||||
|
||||
expect(Math.max(...centers) - Math.min(...centers)).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
|
||||
@ -378,12 +378,14 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
|
||||
await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0);
|
||||
await expect(page.getByText(/^Steer /)).toHaveCount(0);
|
||||
await expect(composer).toHaveAttribute("placeholder", "Message…");
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
||||
// An empty draft offers voice in the send slot; typing turns it into Send.
|
||||
const voice = page
|
||||
.getByTestId("composer-bar")
|
||||
.getByRole("button", { name: "Voice", exact: true });
|
||||
await expect(voice).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toHaveCount(0);
|
||||
await composer.fill("Use the newer report and keep the answer short.");
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(
|
||||
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
|
||||
).toBeFocused();
|
||||
await expect(voice).toHaveCount(0);
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
@ -393,7 +395,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
|
||||
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "14-active-bot-work");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
||||
await expect(voice).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
|
||||
await expect(composer).toHaveAttribute("placeholder", "Message…");
|
||||
await captureScreenshot(page, testInfo, "14-active-bot-work-mobile");
|
||||
@ -414,14 +416,13 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
|
||||
});
|
||||
await page.getByRole("button", { name: "Stop", exact: true }).click();
|
||||
await stopRequestStarted;
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeDisabled();
|
||||
await expect(voice).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeDisabled();
|
||||
releaseStopRequest();
|
||||
// Idle Send stays disabled with an empty draft; wait for Stop to leave instead.
|
||||
await expect(page.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
||||
await expect(voice).toBeVisible();
|
||||
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/sign-in");
|
||||
|
||||
@ -45,7 +45,7 @@ test("shows peer chips in transcript and opens view-only peer chat", async ({ pa
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
const transcript = page.getByTestId("transcript");
|
||||
const chip = transcript
|
||||
|
||||
@ -150,7 +150,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
|
||||
await routine.click();
|
||||
await page.getByRole("button", { name: "Test run" }).click();
|
||||
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 30_000 });
|
||||
await captureScreenshot(page, testInfo, "34-routine-run-completed");
|
||||
|
||||
await page.reload();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { captureScreenshot, completeOnboarding, signup } from "./helpers";
|
||||
import { activeBotId, captureScreenshot, completeOnboarding, rpc, signup } from "./helpers";
|
||||
|
||||
test("teach a task records interaction and saves a draft", async ({ page }, testInfo) => {
|
||||
const stamp = Date.now();
|
||||
@ -17,11 +17,15 @@ test("teach a task records interaction and saves a draft", async ({ page }, test
|
||||
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
|
||||
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
|
||||
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible();
|
||||
// Showing the panel never boots the computer; its Open action is visible without hover.
|
||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
||||
await expect(openButton).toBeVisible();
|
||||
const botId = activeBotId(page);
|
||||
expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped");
|
||||
await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
|
||||
|
||||
const preview = sidePanel.getByTestId("computer-preview");
|
||||
await preview.hover();
|
||||
const openButton = sidePanel.getByTestId("computer-preview-open");
|
||||
await expect(openButton).toBeVisible();
|
||||
await expect(openButton.getByText("Open", { exact: true })).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");
|
||||
|
||||
@ -54,6 +54,10 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa
|
||||
|
||||
const composer = page.getByPlaceholder(/Message/);
|
||||
await composer.fill("say hello");
|
||||
await expect(
|
||||
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
|
||||
).toHaveCount(0);
|
||||
await captureScreenshot(page, testInfo, "voice-composer-typing");
|
||||
await page.keyboard.press("Enter");
|
||||
// A reply can render more than one text bubble; speak the latest one.
|
||||
const speakReply = page.getByRole("button", { name: "Speak this reply" }).last();
|
||||
|
||||
@ -24,7 +24,6 @@ import {
|
||||
import { personaIdlePrompt, personaThinkingLines } from "../../lib/voice-persona-copy";
|
||||
import { VoiceAwarenessStrip } from "../voice/VoiceAwarenessStrip";
|
||||
import { StageHero } from "./StageHero";
|
||||
import { StageVoiceEntry } from "./StageVoiceEntry";
|
||||
import { TextStageCanvas } from "./TextStageCanvas";
|
||||
|
||||
export function OoStage({
|
||||
@ -37,8 +36,6 @@ export function OoStage({
|
||||
snapshot,
|
||||
running,
|
||||
hasMessages,
|
||||
voiceReady = false,
|
||||
onStartVoice,
|
||||
}: {
|
||||
botId: string;
|
||||
botName: string;
|
||||
@ -49,8 +46,6 @@ export function OoStage({
|
||||
snapshot: ThreadSnapshot | null;
|
||||
running: boolean;
|
||||
hasMessages: boolean;
|
||||
voiceReady?: boolean;
|
||||
onStartVoice?: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const [reducedMotion, setReducedMotion] = useState(false);
|
||||
@ -242,13 +237,6 @@ export function OoStage({
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
{onStartVoice ? (
|
||||
<StageVoiceEntry
|
||||
voiceReady={voiceReady}
|
||||
botColor={botColor}
|
||||
onStartVoice={onStartVoice}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-1 w-full max-w-xl truncate text-center text-ui-caption text-muted-foreground">
|
||||
@ -306,14 +294,10 @@ export function OoGroupStage({
|
||||
groupName,
|
||||
snapshot,
|
||||
enrichMember,
|
||||
voiceReady = false,
|
||||
onStartVoice,
|
||||
}: {
|
||||
groupName?: string;
|
||||
snapshot: ThreadSnapshot | null;
|
||||
enrichMember?: (botId: string) => Pick<GroupStageMember, "title" | "description"> | undefined;
|
||||
voiceReady?: boolean;
|
||||
onStartVoice?: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const [reducedMotion, setReducedMotion] = useState(false);
|
||||
@ -552,14 +536,6 @@ export function OoGroupStage({
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{onStartVoice && !hasMessages ? (
|
||||
<StageVoiceEntry
|
||||
voiceReady={voiceReady}
|
||||
botColor={speaker?.color}
|
||||
onStartVoice={onStartVoice}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { Button } from "@rakazo/ui-web";
|
||||
import { Mic } from "lucide-react";
|
||||
import { VoiceAuroraRim } from "../voice/VoiceAurora";
|
||||
|
||||
export function StageVoiceEntry({
|
||||
voiceReady,
|
||||
botColor,
|
||||
onStartVoice,
|
||||
}: {
|
||||
voiceReady: boolean;
|
||||
botColor?: string;
|
||||
onStartVoice: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<div className="mt-5 flex w-full max-w-md flex-col items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onStartVoice}
|
||||
data-testid="stage-voice-primary"
|
||||
aria-label={voiceReady ? t`Start talking` : t`Set up voice`}
|
||||
title={voiceReady ? t`Start talking` : t`Set up voice`}
|
||||
className={`tactile-press h-control-lg w-full max-w-sm gap-2.5 rounded-full px-6 text-ui-body font-semibold transition-all ${
|
||||
voiceReady
|
||||
? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10"
|
||||
: "border-border bg-foreground/5 text-foreground/80 hover:bg-foreground/10"
|
||||
}`}
|
||||
>
|
||||
{voiceReady ? (
|
||||
<VoiceAuroraRim botColor={botColor} className="inset-x-[12%] h-0.5 blur-[3px]" />
|
||||
) : null}
|
||||
<Mic strokeWidth={2} className="size-icon-lg shrink-0" aria-hidden="true" />
|
||||
{voiceReady ? <Trans>Start talking</Trans> : <Trans>Set up voice</Trans>}
|
||||
</Button>
|
||||
<p className="text-center text-ui-caption text-muted-foreground">
|
||||
<Trans>Voice first — transcript below</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -9,9 +9,8 @@ import {
|
||||
activeThreadRuns,
|
||||
applyThreadSendReceipt,
|
||||
clearActiveThreadRuns,
|
||||
computerPanelAutoBoot,
|
||||
computerPanelAutoUsesBoot,
|
||||
computerPanelNeedsMaintenance,
|
||||
computerPanelReconnects,
|
||||
computerTakeoverBlocked,
|
||||
isThreadSnapshotEvent,
|
||||
mergeThreadSnapshot,
|
||||
@ -1513,25 +1512,19 @@ describe("computer event reduction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-boots stopped computers and recovers a running screen that has no URL", () => {
|
||||
expect(computerPanelAutoBoot("stopped")).toBe("boot");
|
||||
expect(computerPanelAutoBoot("error")).toBe("boot");
|
||||
expect(computerPanelAutoBoot(undefined)).toBe("boot");
|
||||
expect(computerPanelAutoBoot("running", "https://screen.example")).toBe("wait");
|
||||
expect(computerPanelAutoBoot("running", null)).toBe("recover-screen");
|
||||
expect(computerPanelAutoBoot("booting")).toBe("wait");
|
||||
expect(computerPanelAutoBoot("suspended")).toBe("wait");
|
||||
it("reconnects only a running screen that has no URL and never boots on open", () => {
|
||||
expect(computerPanelReconnects("stopped")).toBe(false);
|
||||
expect(computerPanelReconnects("error")).toBe(false);
|
||||
expect(computerPanelReconnects(undefined)).toBe(false);
|
||||
expect(computerPanelReconnects("booting")).toBe(false);
|
||||
expect(computerPanelReconnects("suspended")).toBe(false);
|
||||
expect(computerPanelReconnects("running", "https://screen.example")).toBe(false);
|
||||
expect(computerPanelReconnects("running", null)).toBe(true);
|
||||
});
|
||||
|
||||
it("maps recover-screen to computer.boot, not computer.recover", () => {
|
||||
expect(computerPanelAutoUsesBoot("recover-screen")).toBe(true);
|
||||
expect(computerPanelAutoUsesBoot("boot")).toBe(true);
|
||||
expect(computerPanelAutoUsesBoot("wait")).toBe(false);
|
||||
});
|
||||
|
||||
it("shows maintenance only after a stopped or errored computer finishes booting", () => {
|
||||
it("shows maintenance only for an errored computer that is not booting", () => {
|
||||
expect(computerPanelNeedsMaintenance("error", false)).toBe(true);
|
||||
expect(computerPanelNeedsMaintenance("stopped", false)).toBe(true);
|
||||
expect(computerPanelNeedsMaintenance("stopped", false)).toBe(false);
|
||||
expect(computerPanelNeedsMaintenance("error", true)).toBe(false);
|
||||
expect(computerPanelNeedsMaintenance("running", false)).toBe(false);
|
||||
expect(computerPanelNeedsMaintenance(undefined, false)).toBe(false);
|
||||
@ -1541,7 +1534,7 @@ describe("computer event reduction", () => {
|
||||
const panel = "computer";
|
||||
const booting = false;
|
||||
const showInSidePanel = (computerOpen: boolean) =>
|
||||
panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("stopped", booting);
|
||||
panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("error", booting);
|
||||
|
||||
expect(showInSidePanel(false)).toBe(true);
|
||||
expect(showInSidePanel(true)).toBe(false);
|
||||
|
||||
@ -539,27 +539,23 @@ export function computerTakeoverBlocked(
|
||||
return isActive(runStatus as RunStatus);
|
||||
}
|
||||
|
||||
export function computerPanelAutoBoot(
|
||||
/**
|
||||
* Opening the side panel only reconnects a computer that already runs. Starting a stopped one
|
||||
* is the user's choice (Open), because a boot is slow and heavy on the host.
|
||||
* The reconnect must use computer.boot — never computer.recover (that destroys the sandbox).
|
||||
*/
|
||||
export function computerPanelReconnects(
|
||||
state: ComputerStatus["state"] | undefined,
|
||||
screenUrl?: string | null,
|
||||
): "boot" | "recover-screen" | "wait" {
|
||||
if (state === "booting" || state === "suspended") return "wait";
|
||||
if (state === "running") return screenUrl ? "wait" : "recover-screen";
|
||||
return "boot";
|
||||
}
|
||||
|
||||
/** Auto panel reconnect must use computer.boot — never computer.recover (that destroys the sandbox). */
|
||||
export function computerPanelAutoUsesBoot(
|
||||
action: ReturnType<typeof computerPanelAutoBoot>,
|
||||
): boolean {
|
||||
return action === "boot" || action === "recover-screen";
|
||||
return state === "running" && !screenUrl;
|
||||
}
|
||||
|
||||
export function computerPanelNeedsMaintenance(
|
||||
state: ComputerStatus["state"] | undefined,
|
||||
booting: boolean,
|
||||
): boolean {
|
||||
return !booting && (state === "error" || state === "stopped");
|
||||
return !booting && state === "error";
|
||||
}
|
||||
|
||||
export function reduceComputerStatus(
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Servername"
|
||||
msgid "Server URL"
|
||||
msgstr "Server-URL"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr "Stimme einrichten"
|
||||
@ -4834,10 +4833,6 @@ msgstr "Chat starten"
|
||||
msgid "Start recording"
|
||||
msgstr "Aufnahme starten"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr "Sprechen starten"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr "Starte die Unterhaltung — {activeName} steht oben auf der Bühne."
|
||||
@ -5378,10 +5373,6 @@ msgstr "Stimme"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr "Stimme „{0}“ nicht gefunden."
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr "Primär per Stimme — Transkript unten"
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr "Die Stimme ist noch nicht verfügbar. Du kannst sie später in den Einstellungen aktivieren."
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Server name"
|
||||
msgid "Server URL"
|
||||
msgstr "Server URL"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr "Set up voice"
|
||||
@ -4834,10 +4833,6 @@ msgstr "Start chat"
|
||||
msgid "Start recording"
|
||||
msgstr "Start recording"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr "Start talking"
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr "Start the conversation — {activeName} is on stage above."
|
||||
@ -5378,10 +5373,6 @@ msgstr "Voice"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr "Voice \"{0}\" not found."
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr "Voice first — transcript below"
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr "Voice is not available yet. You can turn it on later in settings."
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Nombre del servidor"
|
||||
msgid "Server URL"
|
||||
msgstr "URL del servidor"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "Iniciar grabación"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "Voz"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "सर्वर का नाम"
|
||||
msgid "Server URL"
|
||||
msgstr "सर्वर URL"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "रिकॉर्डिंग शुरू करें"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "आवाज़"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "서버 이름"
|
||||
msgid "Server URL"
|
||||
msgstr "서버 URL"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "작업 녹화 시작"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "음성"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Nome do servidor"
|
||||
msgid "Server URL"
|
||||
msgstr "URL do servidor"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "Começar a gravar"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "Voz"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Имя сервера"
|
||||
msgid "Server URL"
|
||||
msgstr "URL-адрес сервера"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "Начать запись"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "Голос"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "Sunucu adı"
|
||||
msgid "Server URL"
|
||||
msgstr "Sunucu URL'si"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "Kaydı başlat"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "Ses"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -4579,7 +4579,6 @@ msgstr "服务器名称"
|
||||
msgid "Server URL"
|
||||
msgstr "服务器 URL"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
#: src/pages/shell/composer.tsx
|
||||
msgid "Set up voice"
|
||||
msgstr ""
|
||||
@ -4834,10 +4833,6 @@ msgstr ""
|
||||
msgid "Start recording"
|
||||
msgstr "开始录制"
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Start talking"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/shell/transcript.tsx
|
||||
msgid "Start the conversation — {activeName} is on stage above."
|
||||
msgstr ""
|
||||
@ -5378,10 +5373,6 @@ msgstr "语音"
|
||||
msgid "Voice \"{0}\" not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/stage/StageVoiceEntry.tsx
|
||||
msgid "Voice first — transcript below"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||
msgid "Voice is not available yet. You can turn it on later in settings."
|
||||
msgstr ""
|
||||
|
||||
@ -124,8 +124,7 @@ import {
|
||||
activeThreadRuns,
|
||||
applyThreadSendReceipt,
|
||||
clearActiveThreadRuns,
|
||||
computerPanelAutoBoot,
|
||||
computerPanelAutoUsesBoot,
|
||||
computerPanelReconnects,
|
||||
computerTakeoverBlocked,
|
||||
isComputerStatusEvent,
|
||||
prependThreadMessagePage,
|
||||
@ -551,7 +550,6 @@ export function ShellPage() {
|
||||
outputTokens: number;
|
||||
runs: number;
|
||||
} | null>(null);
|
||||
const autoBooted = useRef<string | null>(null);
|
||||
const routineSavePending = useRef(false);
|
||||
const webhookSecretProvisionRef = useRef(new Map<string, Promise<string>>());
|
||||
const ensureWebhookSecret = (botId: string) =>
|
||||
@ -2503,11 +2501,7 @@ export function ShellPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (panel !== "computer") {
|
||||
autoBooted.current = null;
|
||||
return;
|
||||
}
|
||||
if (!active) return;
|
||||
if (panel !== "computer" || !active) return;
|
||||
const botId = active.id;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
@ -2518,19 +2512,10 @@ export function ShellPage() {
|
||||
const state = snap?.computer?.state;
|
||||
const screen = state === "running" ? await refreshComputerScreen(botId) : null;
|
||||
if (cancelled || activeBotId.current !== botId) return;
|
||||
const action = computerPanelAutoBoot(state, screen);
|
||||
if (action === "wait") {
|
||||
if (state === "running") autoBooted.current = botId;
|
||||
return;
|
||||
}
|
||||
if (action === "boot" && autoBooted.current === botId) return;
|
||||
autoBooted.current = botId;
|
||||
if (!computerPanelAutoUsesBoot(action)) return;
|
||||
await bootComputer({
|
||||
takeControl: false,
|
||||
overlay: action === "boot",
|
||||
force: true,
|
||||
}).catch(() => undefined);
|
||||
if (!computerPanelReconnects(state, screen)) return;
|
||||
await bootComputer({ takeControl: false, overlay: false, force: true }).catch(
|
||||
() => undefined,
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@ -3323,8 +3308,6 @@ export function ShellPage() {
|
||||
snapshot={activeSnapshot}
|
||||
running={transcriptRunning}
|
||||
hasMessages={transcriptMessages.length > 0}
|
||||
voiceReady={Boolean(voiceStatus?.ready)}
|
||||
onStartVoice={startVoiceEncounter}
|
||||
/>
|
||||
) : null}
|
||||
{!callOpen && inGroup ? (
|
||||
@ -3332,8 +3315,6 @@ export function ShellPage() {
|
||||
groupName={activeGroup?.name ?? activeSnapshot?.groupName}
|
||||
snapshot={activeSnapshot}
|
||||
enrichMember={enrichStageMember}
|
||||
voiceReady={Boolean(voiceStatus?.ready)}
|
||||
onStartVoice={startVoiceEncounter}
|
||||
/>
|
||||
) : null}
|
||||
<Transcript
|
||||
|
||||
@ -135,6 +135,38 @@ export const Composer = memo(function Composer({
|
||||
selectedSkill !== null ||
|
||||
selectedMentions.length > 0 ||
|
||||
pendingAttachments.length > 0;
|
||||
// One slot, two jobs: an empty draft talks, a written one sends.
|
||||
const voiceInSlot = Boolean(onVoice) && !canSend;
|
||||
const primaryAction = voiceInSlot ? (
|
||||
<Button
|
||||
key="voice"
|
||||
size="icon"
|
||||
aria-label={t`Voice`}
|
||||
title={voiceReady ? t`Start voice conversation` : t`Set up voice`}
|
||||
disabled={disabled}
|
||||
onClick={onVoice}
|
||||
data-testid="composer-voice-primary"
|
||||
className="tactile-press relative size-10 shrink-0 overflow-hidden rounded-full transition-all active:scale-90 animate-in fade-in zoom-in-75 duration-150 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
{voiceReady ? (
|
||||
<VoiceAuroraRim botColor={voiceColor} className="inset-x-[22%] h-0.5 blur-[3px]" />
|
||||
) : null}
|
||||
<Mic strokeWidth={2.2} className="size-icon-lg" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="send"
|
||||
size="icon"
|
||||
aria-label={t`Send`}
|
||||
disabled={!canSend || disabled}
|
||||
onClick={send}
|
||||
className={`tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer ${
|
||||
onVoice ? "animate-in fade-in zoom-in-75 duration-150" : ""
|
||||
}`}
|
||||
>
|
||||
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runError || !runErrorId) return;
|
||||
@ -743,40 +775,9 @@ export const Composer = memo(function Composer({
|
||||
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal"
|
||||
/>
|
||||
</div>
|
||||
{onVoice ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={t`Voice`}
|
||||
title={voiceReady ? t`Start voice conversation` : t`Set up voice`}
|
||||
disabled={disabled}
|
||||
onClick={onVoice}
|
||||
data-testid="composer-voice-primary"
|
||||
className={`tactile-press h-control-lg shrink-0 gap-2 rounded-full px-4 sm:px-5 flex items-center justify-center cursor-pointer transition-all ${
|
||||
voiceReady
|
||||
? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10"
|
||||
: "border-border bg-foreground/5 text-foreground/80 hover:bg-foreground/10"
|
||||
}`}
|
||||
>
|
||||
{voiceReady ? (
|
||||
<VoiceAuroraRim botColor={voiceColor} className="inset-x-[14%] h-0.5 blur-[3px]" />
|
||||
) : null}
|
||||
<Mic strokeWidth={1.9} className="size-icon-lg shrink-0" />
|
||||
<span className="hidden text-ui-body font-semibold sm:inline">
|
||||
<Trans>Voice</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
) : null}
|
||||
{running ? (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="icon"
|
||||
aria-label={t`Send`}
|
||||
disabled={!canSend || disabled}
|
||||
onClick={send}
|
||||
className="tactile-press size-10 rounded-full transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
||||
</Button>
|
||||
{primaryAction}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@ -789,15 +790,7 @@ export const Composer = memo(function Composer({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
aria-label={t`Send`}
|
||||
disabled={!canSend || disabled}
|
||||
onClick={send}
|
||||
className="tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
||||
</Button>
|
||||
primaryAction
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,6 +35,12 @@ export function ComputerPanelPreview({
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
// Opening the panel never starts the computer, so a resting one shows its Open action outright.
|
||||
const resting =
|
||||
!booting &&
|
||||
computer?.state !== "running" &&
|
||||
computer?.state !== "booting" &&
|
||||
!computersAreUnavailable(sandboxProvider);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -77,7 +83,8 @@ export function ComputerPanelPreview({
|
||||
<button
|
||||
type="button"
|
||||
data-testid="computer-preview-open"
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center bg-overlay/40 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
|
||||
data-resting={resting ? "" : undefined}
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-4 data-resting:opacity-100"
|
||||
aria-label={t`Open`}
|
||||
onClick={onOpen}
|
||||
>
|
||||
|
||||
@ -286,10 +286,13 @@ git_fetch_and_checkout() {
|
||||
log "previous HEAD: $previous_sha"
|
||||
log "fetching $EVO_GIT_REMOTE…"
|
||||
run git -C "$EVO_DEPLOY_DIR" fetch "$EVO_GIT_REMOTE" --prune >&2
|
||||
if git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_DEPLOY_REF}^{commit}")"
|
||||
elif git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||
# Prefer the freshly fetched remote branch: the local `main` in the deploy
|
||||
# checkout never moves (we reset HEAD, not the branch), so resolving it first
|
||||
# would redeploy a stale commit.
|
||||
if git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}")"
|
||||
elif git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_DEPLOY_REF}^{commit}")"
|
||||
else
|
||||
die "cannot resolve ref: $EVO_DEPLOY_REF"
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user