diff --git a/docs/client/troubleshooting.md b/docs/client/troubleshooting.md
index aa46a92..56e53b8 100644
--- a/docs/client/troubleshooting.md
+++ b/docs/client/troubleshooting.md
@@ -113,10 +113,11 @@ before changing audio timing based on those observations.
In `OPENAI`, check the configured endpoint/model/key and provider logs; HTTP
reasoning does not use the native session's tool array.
-For `TALK`/`TALKFIRST`, the two-second warmup does not override browser
-microphone/audio permissions. If automatic startup fails, use the unlocker
-gesture and inspect the permission error rather than increasing the timer
-without evidence.
+`TALK`/`TALKFIRST` attempt startup as soon as configuration and caller context
+are ready. Browser microphone/audio policies can still require a gesture. If
+audio is blocked, tap the main microphone button when the inline hint appears.
+If microphone access fails, allow it in the browser's site permissions and use
+the same button to retry. No startup dialog is shown.
## Local validation
diff --git a/docs/client/usage.md b/docs/client/usage.md
index b7095ad..9ff2b10 100644
--- a/docs/client/usage.md
+++ b/docs/client/usage.md
@@ -148,12 +148,15 @@ loading window before the interface allows a generic session.
| Value | Behavior |
|---|---|
| Empty/default | User taps to start |
-| `TALK` | After a 2-second warmup, show a dismissible startup dialog; a user gesture starts the session and greeting |
-| `TALKFIRST` | After a 2-second warmup, attempt automatic start and inject the Spanish wake phrase; microphone failure falls back to the startup dialog |
+| `TALK` | Attempt automatic start and inject the Spanish wake phrase as soon as configuration and caller context are ready |
+| `TALKFIRST` | Same automatic startup behavior as `TALK` |
-The warmup starts after configuration and caller-context loading settle;
-optional reports and tools can still be loading. Browser permission and audio
-policies can still require a gesture.
+Automatic startup runs once per page load, without a startup dialog or artificial
+delay; optional reports and tools can still be loading. Browser permission and
+audio policies can still require a gesture. When audio is blocked, an inline hint
+asks the user to tap the microphone button. Permission or connection failures
+show the error and leave the same button available to retry. Stopping a session
+does not start it again automatically.
The greeting uses the same `hola vocero` input as wake-word handling.
On browsers with speech recognition, **Activar con «hola vocero»** opts into
diff --git a/vocero-api/CHANGELOG.md b/vocero-api/CHANGELOG.md
index 5a6124a..46a6526 100644
--- a/vocero-api/CHANGELOG.md
+++ b/vocero-api/CHANGELOG.md
@@ -31,6 +31,10 @@ the implementation at the recorded date; current operating instructions live in
### Changed
+- Start `TALK` and `TALKFIRST` conversations as soon as configuration and caller
+ context are ready. Remove the startup dialog and two-second delay; use an
+ inline hint and the existing microphone button when a browser requires a tap.
+
- Replace the frontend legacy stylesheet with Tailwind v4 semantic theme tokens,
shared form/dialog controls and component-owned layout utilities. Preserve both
configured themes, responsive layouts, sanitized Markdown and audio-driven
diff --git a/vocero-frontend/src/App.test.tsx b/vocero-frontend/src/App.test.tsx
index 68df72c..e367e52 100644
--- a/vocero-frontend/src/App.test.tsx
+++ b/vocero-frontend/src/App.test.tsx
@@ -4,6 +4,7 @@ import { StrictMode } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import App from "./App";
+import { VoiceController } from "./realtime/controller";
const bootstrap = { theme: "white", s2sUrl: "ws://localhost:8420/v1/realtime", apipilot: true };
beforeEach(() => {
@@ -43,6 +44,53 @@ afterEach(() => {
});
describe("React application", () => {
+ it.each(["TALK", "TALKFIRST"])(
+ "attempts %s immediately without a startup dialog under StrictMode",
+ async (startSessionMode) => {
+ const original = vi.mocked(fetch).getMockImplementation();
+ if (!original) {
+ throw new Error("Missing fetch mock");
+ }
+ vi.mocked(fetch).mockImplementation((input, init) =>
+ input === "/api/config"
+ ? Promise.resolve(new Response(JSON.stringify({ ...bootstrap, startSessionMode })))
+ : original(input, init),
+ );
+ const start = vi.spyOn(VoiceController.prototype, "start").mockResolvedValue(undefined);
+ render(
+
{state.notice}
++ {state.notice} +
)} {active && ({state.notice}
} + {state.notice && ( ++ {state.notice} +
+ )} > )} {(status === "queued" || status === "your-turn") && ( diff --git a/vocero-frontend/src/realtime/controller.test.ts b/vocero-frontend/src/realtime/controller.test.ts index da557a5..fb24a77 100644 --- a/vocero-frontend/src/realtime/controller.test.ts +++ b/vocero-frontend/src/realtime/controller.test.ts @@ -3,8 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vite import type { ClientOptions } from "./client"; import { VoiceController } from "./controller"; -const { MockClient, closeContext } = vi.hoisted(() => { +const { MockClient, closeContext, createContext } = vi.hoisted(() => { const closeContext = vi.fn().mockResolvedValue(undefined); + const createContext = vi.fn(() => ({ state: "running", close: closeContext })); class MockClient { static instances: MockClient[] = []; readonly options: ClientOptions; @@ -41,16 +42,17 @@ const { MockClient, closeContext } = vi.hoisted(() => { sendToolOutput = vi.fn(); join = vi.fn(); } - return { MockClient, closeContext }; + return { MockClient, closeContext, createContext }; }); vi.mock("./client", async (importOriginal) => ({ ...(await importOriginal()), RealtimeClient: MockClient })); -vi.mock("../audio/engine", () => ({ createAudioContext: () => ({ state: "running", close: closeContext }) })); +vi.mock("../audio/engine", () => ({ createAudioContext: createContext })); let pendingReasoning: ((value: Response) => void) | undefined; let controller: VoiceController; let fetchMock: Mock<(path: string, init?: RequestInit) => Promise