diff --git a/apps/web/e2e/activity-list.spec.ts b/apps/web/e2e/activity-list.spec.ts index 05ee392..1c7e360 100644 --- a/apps/web/e2e/activity-list.spec.ts +++ b/apps/web/e2e/activity-list.spec.ts @@ -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"); diff --git a/apps/web/e2e/bot-organization.spec.ts b/apps/web/e2e/bot-organization.spec.ts index 510cb52..c32a4cd 100644 --- a/apps/web/e2e/bot-organization.spec.ts +++ b/apps/web/e2e/bot-organization.spec.ts @@ -195,14 +195,23 @@ 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 element = composer.querySelector(`[aria-label="${label}"]`); - if (!element) throw new Error(`Missing composer control: ${label}`); - const box = element.getBoundingClientRect(); - return box.top + box.height / 2; - }), - ); + const bar = page.getByTestId("composer-bar"); + const centersOf = (labels: string[]) => + bar.evaluate( + (composer, controls) => + controls.map((label) => { + const element = composer.querySelector(`[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); }); diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 17ace91..447a582 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -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"); diff --git a/apps/web/e2e/peer-messages.spec.ts b/apps/web/e2e/peer-messages.spec.ts index d77da20..8852b55 100644 --- a/apps/web/e2e/peer-messages.spec.ts +++ b/apps/web/e2e/peer-messages.spec.ts @@ -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 diff --git a/apps/web/e2e/routine-execution.spec.ts b/apps/web/e2e/routine-execution.spec.ts index 7f24174..478d054 100644 --- a/apps/web/e2e/routine-execution.spec.ts +++ b/apps/web/e2e/routine-execution.spec.ts @@ -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(); diff --git a/apps/web/e2e/teach-task.spec.ts b/apps/web/e2e/teach-task.spec.ts index c4f456d..835d31c 100644 --- a/apps/web/e2e/teach-task.spec.ts +++ b/apps/web/e2e/teach-task.spec.ts @@ -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"); diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index ee2295f..94e275e 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -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(); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index c6b5d23..5cbdb9e 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -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}

- {onStartVoice ? ( - - ) : null} ) : (

@@ -306,14 +294,10 @@ export function OoGroupStage({ groupName, snapshot, enrichMember, - voiceReady = false, - onStartVoice, }: { groupName?: string; snapshot: ThreadSnapshot | null; enrichMember?: (botId: string) => Pick | undefined; - voiceReady?: boolean; - onStartVoice?: () => void; }) { const { t } = useLingui(); const [reducedMotion, setReducedMotion] = useState(false); @@ -552,14 +536,6 @@ export function OoGroupStage({ {subtitle}

)} - - {onStartVoice && !hasMessages ? ( - - ) : null} ); diff --git a/apps/web/src/components/stage/StageVoiceEntry.tsx b/apps/web/src/components/stage/StageVoiceEntry.tsx deleted file mode 100644 index dd32f8b..0000000 --- a/apps/web/src/components/stage/StageVoiceEntry.tsx +++ /dev/null @@ -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 ( -
- -

- Voice first — transcript below -

-
- ); -} diff --git a/apps/web/src/lib/thread-events.test.ts b/apps/web/src/lib/thread-events.test.ts index ab0da23..22c3b8d 100644 --- a/apps/web/src/lib/thread-events.test.ts +++ b/apps/web/src/lib/thread-events.test.ts @@ -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); diff --git a/apps/web/src/lib/thread-events.ts b/apps/web/src/lib/thread-events.ts index 705e910..82f1775 100644 --- a/apps/web/src/lib/thread-events.ts +++ b/apps/web/src/lib/thread-events.ts @@ -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, ): 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( diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index cd2bc38..dec4450 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -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." diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index 6e1b187..e7f8af5 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -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." diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index eace18d..d7e1dfc 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -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 "" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index 25e8955..5f3916b 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -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 "" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index ad7e9ff..d80d4a0 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -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 "" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index 7c2ba5e..a053432 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -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 "" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 500d5f8..cee96f9 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -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 "" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index e7392ad..4f13318 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -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 "" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 7b40090..b8db110 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -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 "" diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index e023af9..765575c 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -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(null); const routineSavePending = useRef(false); const webhookSecretProvisionRef = useRef(new Map>()); 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} 0 || pendingAttachments.length > 0; + // One slot, two jobs: an empty draft talks, a written one sends. + const voiceInSlot = Boolean(onVoice) && !canSend; + const primaryAction = voiceInSlot ? ( + + ) : ( + + ); 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" /> - {onVoice ? ( - - ) : null} {running ? (
- + {primaryAction}
) : ( - + primaryAction )} diff --git a/apps/web/src/pages/shell/computer-panel-preview.tsx b/apps/web/src/pages/shell/computer-panel-preview.tsx index 93f0da1..c591690 100644 --- a/apps/web/src/pages/shell/computer-panel-preview.tsx +++ b/apps/web/src/pages/shell/computer-panel-preview.tsx @@ -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 ( <>
diff --git a/ops/evo-deploy/deploy.sh b/ops/evo-deploy/deploy.sh index 054261c..3abcf17 100755 --- a/ops/evo-deploy/deploy.sh +++ b/ops/evo-deploy/deploy.sh @@ -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