First-run onboarding UX: naming, locale, voices, Heute route, mobile drawer (#171)

* Fix first-run onboarding UX: naming, locale, voices, Heute route, mobile drawer

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome import order and formatting

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome formatting on OoStage and E2E specs

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix E2E specs for onboarding name step and Heute reload route

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German onboarding heading matcher in E2E

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German signup E2E, Escape menu stacking, and Heute rhythm click

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Make onboarding E2E helpers locale-aware for German UI

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix completeOnboarding language skip for Skip · locale label

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Use language-step Skip · locale button in completeOnboarding

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German E2E: Nachricht composer, voice loading wait, Heute rhythm

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Stabilize Heute reload URL and German voice E2E assertions

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix Heute bootstrap redirect and German onboarding voices

When Kokoro lists no locale-matched voices, onboarding now uses offline
presets instead of showing English-only options. Shell bootstrap skips
bot redirects while the user is on /app/heute.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Relax German voice E2E name matching for voice labels

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Harden Heute route and German voice onboarding E2E

Rebased onto main with Expo SDK 57 typecheck fixes. Voice options expose
stable test ids; Shell keeps /app/heute across bootstrap and redirects
legacy /app/heute bot URLs. Heute coach E2E asserts URL after reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Format Shell after Heute route helper

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
cynarAI 2026-10-06 20:15:36 +02:00 committed by GitHub
parent ba89574c83
commit 0e1036b2ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 770 additions and 72 deletions

View File

@ -307,6 +307,10 @@ test("group chats share every context-menu action", async ({ page }, testInfo) =
await group.click({ button: "right" }); await group.click({ button: "right" });
await captureScreenshot(page, testInfo, "group-context-menu-mobile"); await captureScreenshot(page, testInfo, "group-context-menu-mobile");
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
if ((await page.getByRole("menu").count()) > 0) {
await page.keyboard.press("Escape");
}
await expect(page.getByRole("menu")).toHaveCount(0);
await page.setViewportSize({ width: 1280, height: 800 }); await page.setViewportSize({ width: 1280, height: 800 });
await group.click({ button: "right" }); await group.click({ button: "right" });

View File

@ -0,0 +1,87 @@
import { expect, test } from "@playwright/test";
import { completeOnboarding, rpc, signup } from "./helpers";
test("onboarding names the first Oo and shows it in chat", async ({ page }) => {
const stamp = Date.now();
await signup(page, `name-oo-${stamp}@rakazo.test`, "password12", "Namer");
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
await page.getByTestId("onboarding-oo-name-input").fill("Luna");
await page
.getByTestId("onboarding-name-step")
.getByRole("button", { name: /^(Continue|Weiter)$/i })
.click();
await page.getByRole("button", { name: /^(Skip|Überspringen ·)/i }).click();
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
await expect(page).toHaveURL(/\/app\/g\/[^/]+/, { timeout: 30_000 });
await expect(
page.getByTestId("bot-settings-trigger").getByText("Luna", { exact: true }),
).toBeVisible();
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Luna");
});
test("German UI keeps Deutsch on language skip label", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
localStorage.setItem("rakazo.uiLocale", "de");
});
const stamp = Date.now();
await signup(page, `de-skip-${stamp}@rakazo.test`, "password12", "Deutsch");
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
await expect(
page.getByRole("heading", { name: /Wähle deine Sprache|Choose your language/i }),
).toBeVisible();
await expect(page.getByRole("button", { name: /Überspringen · Deutsch/i })).toBeVisible();
});
test("German voice step lists German voices after choosing Deutsch", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
localStorage.setItem("rakazo.uiLocale", "de");
});
const stamp = Date.now();
await signup(page, `de-voice-${stamp}@rakazo.test`, "password12", "Stimme");
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
await page.getByRole("button", { name: "Deutsch", exact: true }).click();
await expect(page.getByText(/Loading voices|Stimmen werden geladen/i)).toHaveCount(0, {
timeout: 30_000,
});
await expect(page.getByTestId("onboarding-voice-df_eva")).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId("onboarding-voice-dm_bernd")).toBeVisible();
await expect(page.getByTestId("onboarding-voice-af_alloy")).toHaveCount(0);
await expect(page.getByTestId("onboarding-voice-df_eva")).toHaveAttribute("aria-pressed", "true");
});
test("main conversation does not show a group subtitle for one Oo", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
localStorage.setItem("rakazo.uiLocale", "de");
});
const stamp = Date.now();
await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
await completeOnboarding(page, undefined, { mainConversation: true });
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung");
await expect(page.getByTestId("oo-group-stage")).toContainText(/1:1/i);
});
test.describe("mobile shell", () => {
test.use({
hasTouch: true,
isMobile: true,
viewport: { width: 390, height: 844 },
});
test("Escape closes the mobile navigation drawer", async ({ page }) => {
const stamp = Date.now();
await signup(page, `mobile-esc-${stamp}@rakazo.test`, "password12", "Escape");
await completeOnboarding(page);
await page.getByRole("button", { name: "Open navigation" }).click();
await expect(page.getByRole("button", { name: "Close navigation" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "Close navigation" })).toHaveCount(0);
await expect(page.getByTestId("open-navigation")).toBeFocused();
});
});

View File

@ -87,6 +87,16 @@ async function visible(locator: Locator, timeout: number) {
return locator.isVisible({ timeout }).catch(() => false); return locator.isVisible({ timeout }).catch(() => false);
} }
const onboardingContinueButton = /^(Continue|Weiter)$/i;
const onboardingSkipButton = /^(Skip|Überspringen)$/i;
const onboardingLanguageSkipButton = /^(Skip|Überspringen) ·/i;
/** After the look picker, skip the optional Oo naming step (keeps the default name). */
export async function skipOnboardingNameStep(page: Page) {
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
await page.getByRole("button", { name: onboardingSkipButton }).click();
}
/** /**
* Walks the first-run wizard, which lands in the main conversation with the main bot's first * Walks the first-run wizard, which lands in the main conversation with the main bot's first
* onboarding question pending (the composer works regardless). Unless `mainConversation` is * onboarding question pending (the composer works regardless). Unless `mainConversation` is
@ -95,23 +105,29 @@ async function visible(locator: Locator, timeout: number) {
export async function completeOnboarding( export async function completeOnboarding(
page: Page, page: Page,
testInfo?: TestInfo, testInfo?: TestInfo,
options: { mainConversation?: boolean } = {}, options: { mainConversation?: boolean; ooName?: string } = {},
) { ) {
await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 }); await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 });
if (page.url().includes("/onboarding")) { if (page.url().includes("/onboarding")) {
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
"data-onboarding-step", "data-onboarding-step",
/^(emoji|language|voice|integrations|scan|bot)$/, /^(emoji|name|language|voice|integrations|scan|bot)$/,
{ timeout: 30_000 }, { timeout: 30_000 },
); );
} }
const design = page.getByRole("heading", { name: /Design your Oo/i }); const design = page.getByRole("heading", { name: /Design your Oo|Gestalte dein Oo/i });
const language = page.getByRole("heading", { name: /Choose your language/i }); const naming = page.getByTestId("onboarding-name-step");
const voice = page.getByRole("heading", { name: /Pick a voice/i }); const language = page.getByRole("heading", {
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true }); name: /Choose your language|Wähle deine Sprache/i,
const chief = page.getByText("Chief").first(); });
const voice = page.getByRole("heading", { name: /Pick a voice|Wähle eine Stimme/i });
const integrations = page.getByRole("heading", {
name: /Server integrations|Server-Integrationen/i,
});
const expectedOoName = options.ooName ?? "Chief";
const chief = page.getByText(expectedOoName).first();
// Scripted CI has needsModel false, so a fresh owner starts on integrations. // Scripted CI has needsModel false, so a fresh owner starts on integrations.
// A model-gated run still opens the look picker first. // A model-gated run still opens the look picker first.
@ -119,23 +135,30 @@ export async function completeOnboarding(
await expect(entry.first()).toBeVisible({ timeout: 20_000 }); await expect(entry.first()).toBeVisible({ timeout: 20_000 });
if (await design.isVisible()) { if (await design.isVisible()) {
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: onboardingContinueButton }).click();
await expect(naming).toBeVisible();
if (options.ooName) {
await page.getByTestId("onboarding-oo-name-input").fill(options.ooName);
await naming.getByRole("button", { name: onboardingContinueButton }).click();
} else {
await page.getByRole("button", { name: onboardingSkipButton }).click();
}
await expect(language).toBeVisible(); await expect(language).toBeVisible();
await page.getByRole("button", { name: /Skip/i }).click(); await page.getByRole("button", { name: onboardingLanguageSkipButton }).click();
await expect(voice).toBeVisible(); await expect(voice).toBeVisible();
await page.getByRole("button", { name: "Skip", exact: true }).click(); await page.getByRole("button", { name: onboardingSkipButton }).click();
} }
if (await visible(integrations, 8_000)) { if (await visible(integrations, 8_000)) {
if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps"); if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps");
await page.getByRole("button", { name: "Skip", exact: true }).click(); await page.getByRole("button", { name: onboardingSkipButton }).click();
} }
// The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding // The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding
// then lands in the main conversation. // then lands in the main conversation.
const skipScan = page const skipScan = page
.locator('[data-onboarding-step="scan"]') .locator('[data-onboarding-step="scan"]')
.getByRole("button", { name: "Skip", exact: true }); .getByRole("button", { name: onboardingSkipButton });
await expect(async () => { await expect(async () => {
if (await skipScan.isVisible()) await skipScan.click(); if (await skipScan.isVisible()) await skipScan.click();
expect(page.url()).toMatch(/\/app\/g\/[^/]+$/); expect(page.url()).toMatch(/\/app\/g\/[^/]+$/);
@ -171,15 +194,17 @@ export async function signup(
) { ) {
await expect(async () => { await expect(async () => {
await page.goto("/sign-up", { waitUntil: "domcontentloaded" }); await page.goto("/sign-up", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Create your evOo" })).toBeVisible({ await expect(
page.getByRole("heading", { name: /Create your evOo|Dein evOo erstellen/i }),
).toBeVisible({
timeout: 5_000, timeout: 5_000,
}); });
}).toPass({ timeout: 45_000 }); }).toPass({ timeout: 45_000 });
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up"); if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
await page.getByPlaceholder("Your name").fill(name); await page.getByPlaceholder(/Your name|Dein Name/i).fill(name);
await page.getByPlaceholder("Your email address").fill(email); await page.getByPlaceholder(/Your email address|Deine E-Mail-Adresse/i).fill(email);
await page.getByPlaceholder("Password").fill(password); await page.getByPlaceholder(/^Password$|^Passwort$/i).fill(password);
await page.getByRole("button", { name: "Create account" }).click(); await page.getByRole("button", { name: /Create account|Konto erstellen/i }).click();
} }
export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) { export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) {
@ -274,7 +299,7 @@ export async function waitForChatComposer(page: Page) {
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", { await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
timeout: 30_000, timeout: 30_000,
}); });
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByRole("combobox", { name: /^Message$|^Nachricht$/i })).toBeVisible();
} }
/** Create a named bot via RPC for test setup (skips the + picker). */ /** Create a named bot via RPC for test setup (skips the + picker). */

View File

@ -18,7 +18,10 @@ test("Heute shows native brief instead of Mission Control iframe", async ({ page
await completeOnboarding(page); await completeOnboarding(page);
await openHeute(page); await openHeute(page);
await expect(page).toHaveURL(/\/app\/[^/]+$/); await expect(page).toHaveURL(/\/app\/heute$/);
await page.reload();
await expect(page.getByTestId("heute-brief")).toBeVisible();
await expect(page).toHaveURL(/\/app\/heute$/);
const brief = page.getByTestId("heute-brief"); const brief = page.getByTestId("heute-brief");
await expect(brief).toBeVisible(); await expect(brief).toBeVisible();
@ -81,12 +84,21 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days"); await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days");
await captureScreenshot(page, testInfo, "heute-coach"); await captureScreenshot(page, testInfo, "heute-coach");
await page.reload(); await page.reload({ waitUntil: "domcontentloaded" });
await openHeute(page); await expect(page).toHaveURL(/\/app\/heute$/);
await expect(page.getByTestId("heute-brief")).toBeVisible();
await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked(); await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked();
// Check-in times live behind one compact control. // Check-in times live behind one compact control.
await page.getByTestId("heute-rhythm").click(); const rhythm = page.getByTestId("heute-rhythm");
await expect(page.getByTestId("heute-coach-brief")).toBeVisible();
await rhythm.scrollIntoViewIfNeeded();
await expect(rhythm).toBeVisible({ timeout: 30_000 });
const rhythmLoaded = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/rhythm/get") && response.ok(),
);
await rhythm.click();
await rhythmLoaded;
const panel = page.getByTestId("heute-rhythm-panel"); const panel = page.getByTestId("heute-rhythm-panel");
await expect(panel).toBeVisible(); await expect(panel).toBeVisible();
const morning = panel.getByTestId("heute-rhythm-morning"); const morning = panel.getByTestId("heute-rhythm-morning");

View File

@ -11,7 +11,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({
const stamp = Date.now(); const stamp = Date.now();
await signup(page, `onboarding-${stamp}@rakazo.test`, "password12", "Robin"); await signup(page, `onboarding-${stamp}@rakazo.test`, "password12", "Robin");
// The main bot greets the user in the main conversation, where onboarding lands. // The main bot greets the user in the main conversation, where onboarding lands.
await completeOnboarding(page, undefined, { mainConversation: true }); await completeOnboarding(page, undefined, { mainConversation: true, ooName: "Robin" });
await expect( await expect(
page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."), page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."),
@ -31,12 +31,11 @@ test("focus choice suggests apps and preserves a completed connection", async ({
await captureScreenshot(page, testInfo, "choice-card-onboarding"); await captureScreenshot(page, testInfo, "choice-card-onboarding");
await page.getByTestId("focus-choice-day").click(); await page.getByTestId("focus-choice-day").click();
// The focus step suggests apps but must not rename the bot: the name the // The focus step must not rename the bot: onboarding keeps the chosen name.
// user chose during creation ("Chief") is preserved.
await expect( await expect(
page.getByTestId("bot-settings-trigger").getByText("Chief", { exact: true }), page.getByTestId("bot-settings-trigger").getByText("Robin", { exact: true }),
).toBeVisible(); ).toBeVisible();
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Chief"); expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Robin");
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
// The app cards speak for themselves; onboarding adds no text around them. // The app cards speak for themselves; onboarding adds no text around them.
await expect(page.getByText(/^Got it/)).toHaveCount(0); await expect(page.getByText(/^Got it/)).toHaveCount(0);
@ -103,7 +102,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({
test("choice refresh failures leave options available for retry", async ({ page }) => { test("choice refresh failures leave options available for retry", async ({ page }) => {
await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry"); await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry");
await completeOnboarding(page, undefined, { mainConversation: true }); await completeOnboarding(page, undefined, { mainConversation: true, ooName: "Robin" });
const choice = page.getByTestId("focus-choice-day"); const choice = page.getByTestId("focus-choice-day");
await expect(choice).toBeEnabled(); await expect(choice).toBeEnabled();
// Keep the existing choice rendered while its save succeeds and navigation refresh fails. // Keep the existing choice rendered while its save succeeds and navigation refresh fails.

View File

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { captureScreenshot, signup } from "./helpers"; import { captureScreenshot, signup, skipOnboardingNameStep } from "./helpers";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.route("**/rpc/integrationSetup/get", (route) => await page.route("**/rpc/integrationSetup/get", (route) =>
@ -49,11 +49,12 @@ test("emoji-first onboarding: shape + color selection with live preview", async
if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview"); if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview");
// Click Continue → goes to the language step // Click Continue → naming, then language.
await page await page
.getByRole("button", { name: /Continue/i }) .getByRole("button", { name: /Continue/i })
.first() .first()
.click(); .click();
await skipOnboardingNameStep(page);
// Step 2: Language selection // Step 2: Language selection
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
@ -96,6 +97,7 @@ test("onboarding: skip language defaults to English", async ({ page }) => {
.getByRole("button", { name: /Continue/i }) .getByRole("button", { name: /Continue/i })
.first() .first()
.click(); .click();
await skipOnboardingNameStep(page);
// Language step — click Skip, then skip voice. // Language step — click Skip, then skip voice.
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();

View File

@ -1,6 +1,6 @@
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { captureScreenshot, signup } from "./helpers"; import { captureScreenshot, signup, skipOnboardingNameStep } from "./helpers";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.route("**/rpc/integrationSetup/get", (route) => await page.route("**/rpc/integrationSetup/get", (route) =>
@ -77,5 +77,6 @@ test("mobile onboarding: category pick and swipe look previews", async ({ page }
.getByRole("button", { name: /Continue/i }) .getByRole("button", { name: /Continue/i })
.first() .first()
.click(); .click();
await skipOnboardingNameStep(page);
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
}); });

View File

@ -4,7 +4,14 @@
* with its origin, confirms a pending one, and forgets one or all of them. * with its origin, confirms a pending one, and forgets one or all of them.
*/ */
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { captureScreenshot, openUserSettings, rpc, signup, waitForChatComposer } from "./helpers"; import {
captureScreenshot,
openUserSettings,
rpc,
signup,
skipOnboardingNameStep,
waitForChatComposer,
} from "./helpers";
test.use({ timezoneId: "Europe/Vienna" }); test.use({ timezoneId: "Europe/Vienna" });
@ -34,6 +41,7 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
timeout: 30_000, timeout: 30_000,
}); });
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: "Continue", exact: true }).click();
await skipOnboardingNameStep(page);
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
await page.getByRole("button", { name: "English", exact: true }).click(); await page.getByRole("button", { name: "English", exact: true }).click();

View File

@ -120,6 +120,10 @@ function SessionApp() {
} }
/> />
<Route path="/app" element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />} /> <Route path="/app" element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />} />
<Route
path="/app/heute"
element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />}
/>
<Route <Route
path="/app/g/:groupId" path="/app/g/:groupId"
element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />} element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />}

View File

@ -0,0 +1,98 @@
import { Trans, useLingui } from "@lingui/react/macro";
import type { EvooPose } from "@rakazo/ui-web";
import { BotAvatar, Button } from "@rakazo/ui-web";
import { useId, useState } from "react";
import type { OnboardingEmojiConfig } from "./OnboardingOoDesigner";
export const DEFAULT_FIRST_OO_NAME = "Chief";
type OnboardingNameStepProps = {
config: OnboardingEmojiConfig;
name: string;
onNameChange: (name: string) => void;
onBack: () => void;
onContinue: () => void;
};
export function OnboardingNameStep({
config,
name,
onNameChange,
onBack,
onContinue,
}: OnboardingNameStepProps) {
const { t } = useLingui();
const inputId = useId();
const [draft, setDraft] = useState(name);
function commitAndContinue() {
const trimmed = draft.trim() || DEFAULT_FIRST_OO_NAME;
onNameChange(trimmed);
onContinue();
}
return (
<div className="mx-auto max-w-xl" data-testid="onboarding-name-step">
<h2 className="text-xl font-semibold tracking-tight text-foreground">
<Trans>Name your Oo</Trans>
</h2>
<p className="mt-1 text-sm text-muted-foreground">
<Trans>Something short and yours — or keep the default and rename later.</Trans>
</p>
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
<BotAvatar
color={config.avatarValue}
size={120}
identity="onboarding-name-preview"
pose={config.pose as EvooPose}
interactive
className="shrink-0"
/>
<div className="w-full min-w-0 flex-1">
<label htmlFor={inputId} className="sr-only">
<Trans>Oo name</Trans>
</label>
<input
id={inputId}
data-testid="onboarding-oo-name-input"
type="text"
maxLength={48}
autoComplete="off"
autoCapitalize="words"
placeholder={DEFAULT_FIRST_OO_NAME}
value={draft}
onChange={(event) => setDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") commitAndContinue();
}}
className="w-full rounded-2xl border-2 border-border bg-card px-4 py-3 text-lg font-semibold tracking-tight text-foreground shadow-sm outline-none transition-colors placeholder:text-muted-foreground/60 focus:border-primary"
/>
<p className="mt-2 text-xs text-muted-foreground">
{t`Press Enter or Continue — skip keeps ${DEFAULT_FIRST_OO_NAME}.`}
</p>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<Button variant="ghost" size="sm" onClick={onBack}>
<Trans>Back</Trans>
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => {
onNameChange(DEFAULT_FIRST_OO_NAME);
onContinue();
}}
>
<Trans>Skip</Trans>
</Button>
<Button onClick={commitAndContinue}>
<Trans>Continue</Trans>
</Button>
</div>
</div>
</div>
);
}

View File

@ -1,15 +1,20 @@
import { Trans } from "@lingui/react/macro"; import { Trans } from "@lingui/react/macro";
import type { VoiceInfo } from "@rakazo/contracts"; import type { VoiceInfo } from "@rakazo/contracts";
import { defaultVoiceForLocale, voicesForLocale } from "@rakazo/core";
import { Button } from "@rakazo/ui-web"; import { Button } from "@rakazo/ui-web";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import {
defaultOnboardingVoiceId,
offlineOnboardingVoices,
onboardingVoicesFromList,
} from "../../lib/onboarding-voices";
import { rpc } from "../../lib/rpc"; import { rpc } from "../../lib/rpc";
import { readUiLocale } from "../../lib/ui-locale"; import type { UiLocale } from "../../lib/ui-locale";
/** Must match packages/adapters KokoroVoiceProvider placeholder. */ /** Must match packages/adapters KokoroVoiceProvider placeholder. */
export const KOKORO_LOCAL_TOKEN = "local-kokoro"; export const KOKORO_LOCAL_TOKEN = "local-kokoro";
type OnboardingVoiceStepProps = { type OnboardingVoiceStepProps = {
uiLocale: UiLocale;
selectedVoiceId: string; selectedVoiceId: string;
onSelectedVoiceIdChange: (voiceId: string) => void; onSelectedVoiceIdChange: (voiceId: string) => void;
onBack: () => void; onBack: () => void;
@ -18,6 +23,7 @@ type OnboardingVoiceStepProps = {
}; };
export function OnboardingVoiceStep({ export function OnboardingVoiceStep({
uiLocale,
selectedVoiceId, selectedVoiceId,
onSelectedVoiceIdChange, onSelectedVoiceIdChange,
onBack, onBack,
@ -36,13 +42,17 @@ export function OnboardingVoiceStep({
await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN }); await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN });
const listed = await rpc.voice.voices({ provider: "kokoro" }); const listed = await rpc.voice.voices({ provider: "kokoro" });
if (cancelled) return; if (cancelled) return;
const locale = readUiLocale(); const ordered = onboardingVoicesFromList(listed, uiLocale);
const ordered = voicesForLocale(listed, locale).slice(0, 6);
setVoiceOptions(ordered); setVoiceOptions(ordered);
const defaultVoice = defaultVoiceForLocale(listed, locale); const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
if (defaultVoice) onSelectedVoiceIdChange(defaultVoice.id); if (nextId) onSelectedVoiceIdChange(nextId);
} catch { } catch {
if (!cancelled) setVoiceOptions([]); if (!cancelled) {
const fallback = offlineOnboardingVoices(uiLocale);
setVoiceOptions(fallback);
const nextId = defaultOnboardingVoiceId(uiLocale, fallback);
if (nextId) onSelectedVoiceIdChange(nextId);
}
} finally { } finally {
if (!cancelled) setVoiceLoading(false); if (!cancelled) setVoiceLoading(false);
} }
@ -54,7 +64,7 @@ export function OnboardingVoiceStep({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [onSelectedVoiceIdChange]); }, [onSelectedVoiceIdChange, uiLocale]);
async function skipVoice() { async function skipVoice() {
await previewTask.current; await previewTask.current;
@ -82,6 +92,7 @@ export function OnboardingVoiceStep({
<button <button
key={voice.id} key={voice.id}
type="button" type="button"
data-testid={`onboarding-voice-${voice.id}`}
onClick={() => onSelectedVoiceIdChange(voice.id)} onClick={() => onSelectedVoiceIdChange(voice.id)}
aria-pressed={selectedVoiceId === voice.id} aria-pressed={selectedVoiceId === voice.id}
className={`rounded-xl border-2 px-3 py-3 text-left text-sm font-medium transition-all ${ className={`rounded-xl border-2 px-3 py-3 text-left text-sm font-medium transition-all ${

View File

@ -12,6 +12,7 @@ import {
resolveBotPersona, resolveBotPersona,
resolveTextStagePhase, resolveTextStagePhase,
} from "../../lib/bot-persona"; } from "../../lib/bot-persona";
import { isMultiOoGroupChat } from "../../lib/group-chat";
import { import {
extractGroupSpark, extractGroupSpark,
type GroupStageMember, type GroupStageMember,
@ -317,6 +318,7 @@ export function OoGroupStage({
[snapshot, speaker, enrichMember], [snapshot, speaker, enrichMember],
); );
const roster = snapshot?.members ?? []; const roster = snapshot?.members ?? [];
const multiOo = isMultiOoGroupChat(roster.length);
const groupSpark = useMemo( const groupSpark = useMemo(
() => extractGroupSpark(snapshot, workingBots.length), () => extractGroupSpark(snapshot, workingBots.length),
[snapshot, workingBots.length], [snapshot, workingBots.length],
@ -405,7 +407,7 @@ export function OoGroupStage({
if (sparkLabel && groupSpark?.kind === "peer-reply") return sparkLabel; if (sparkLabel && groupSpark?.kind === "peer-reply") return sparkLabel;
if (awarenessLabel) return awarenessLabel; if (awarenessLabel) return awarenessLabel;
if (sparkLabel) return sparkLabel; if (sparkLabel) return sparkLabel;
if (!speaker) return t`Group conversation`; if (!speaker) return multiOo ? t`Group conversation` : t`1:1 conversation`;
if (running && workingBots.length > 1) { if (running && workingBots.length > 1) {
return t`${workingBots.length} Oos are working — ${speaker.name} has the floor`; return t`${workingBots.length} Oos are working — ${speaker.name} has the floor`;
} }
@ -429,8 +431,15 @@ export function OoGroupStage({
thinkingIndex, thinkingIndex,
thinkingLines, thinkingLines,
workingBots.length, workingBots.length,
multiOo,
]); ]);
const headerSubtitle = multiOo
? t`Group conversation`
: speaker
? t`1:1 with ${speaker.name}`
: t`1:1 conversation`;
const energy = groupStageEnergy(phase, groupSpark, workingBots.length); const energy = groupStageEnergy(phase, groupSpark, workingBots.length);
const phaseLabel = showEnsembleOnly const phaseLabel = showEnsembleOnly
@ -454,7 +463,7 @@ export function OoGroupStage({
return ( return (
<section <section
data-testid="oo-group-stage" data-testid="oo-group-stage"
aria-label={t`Group conversation`} aria-label={multiOo ? t`Group conversation` : t`1:1 conversation`}
className={`relative shrink-0 overflow-hidden border-b border-border/60 ${hasMessages ? "max-h-[28vh]" : ""}`} className={`relative shrink-0 overflow-hidden border-b border-border/60 ${hasMessages ? "max-h-[28vh]" : ""}`}
> >
<TextStageCanvas <TextStageCanvas
@ -479,7 +488,7 @@ export function OoGroupStage({
<h2 className="mt-1.5 truncate text-ui-title font-semibold tracking-tight text-foreground"> <h2 className="mt-1.5 truncate text-ui-title font-semibold tracking-tight text-foreground">
{groupName ?? t`Group`} {groupName ?? t`Group`}
</h2> </h2>
<p className="truncate text-ui-body-sm text-muted-foreground">{t`Group conversation`}</p> <p className="truncate text-ui-body-sm text-muted-foreground">{headerSubtitle}</p>
</div> </div>
{awarenessLabel && awarenessEvent && speaker ? ( {awarenessLabel && awarenessEvent && speaker ? (

View File

@ -0,0 +1,5 @@
export const HEUTE_APP_PATH = "/app/heute";
export function isHeuteAppPath(pathname: string): boolean {
return pathname === HEUTE_APP_PATH || pathname.endsWith(`${HEUTE_APP_PATH}`);
}

View File

@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { isMultiOoGroupChat } from "./group-chat.js";
describe("isMultiOoGroupChat", () => {
it("treats a single Oo as 1:1", () => {
expect(isMultiOoGroupChat(0)).toBe(false);
expect(isMultiOoGroupChat(1)).toBe(false);
});
it("treats two or more Oos as a group", () => {
expect(isMultiOoGroupChat(2)).toBe(true);
expect(isMultiOoGroupChat(5)).toBe(true);
});
});

View File

@ -0,0 +1,4 @@
/** True when the encounter has more than one Oo (group chat), not a 1:1 thread. */
export function isMultiOoGroupChat(memberCount: number): boolean {
return memberCount > 1;
}

View File

@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import {
defaultOnboardingVoiceId,
offlineOnboardingVoices,
onboardingVoicesFromList,
} from "./onboarding-voices.js";
describe("offlineOnboardingVoices", () => {
it("offers German voices for de", () => {
expect(offlineOnboardingVoices("de").map((voice) => voice.id)).toEqual([
"df_eva",
"dm_bernd",
"df_victoria",
"dm_martin",
]);
});
it("offers English voices for en", () => {
const ids = offlineOnboardingVoices("en").map((voice) => voice.id);
expect(ids).toContain("af_heart");
expect(ids).not.toContain("df_eva");
});
});
describe("onboardingVoicesFromList", () => {
it("uses offline German presets when the server only lists English voices", () => {
const englishOnly = [{ id: "af_alloy", label: "Alloy", description: "neutral" }];
expect(onboardingVoicesFromList(englishOnly, "de").map((voice) => voice.id)).toEqual([
"df_eva",
"dm_bernd",
"df_victoria",
"dm_martin",
]);
});
});
describe("defaultOnboardingVoiceId", () => {
it("prefers df_eva for German", () => {
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");
});
});

View File

@ -0,0 +1,53 @@
import type { VoiceInfo } from "@rakazo/contracts";
import { defaultVoiceForLocale, GERMAN_VOICE_PRESETS, voicesForLocale } from "@rakazo/core";
/** English presets when the local voice server is unreachable during onboarding. */
const ENGLISH_ONBOARDING_VOICES: VoiceInfo[] = [
{ id: "af_heart", label: "Heart", description: "warm" },
{ id: "af_bella", label: "Bella", description: "expressive" },
{ id: "af_sky", label: "Sky", description: "bright" },
{ id: "af_nova", label: "Nova", description: "energetic" },
{ id: "am_adam", label: "Adam", description: "clear" },
{ id: "am_michael", label: "Michael", description: "steady" },
];
const OFFLINE_CATALOG = [...GERMAN_VOICE_PRESETS, ...ENGLISH_ONBOARDING_VOICES];
const KOKORO_LANGUAGE_PREFIXES: Record<string, string> = {
de: "d",
en: "ab",
es: "e",
fr: "f",
hi: "h",
it: "i",
ja: "j",
pt: "p",
zh: "z",
};
function listedVoicesSpeakLocale(listed: VoiceInfo[], locale: string): boolean {
const language = locale.toLowerCase().split("-")[0] ?? "";
const prefixes = KOKORO_LANGUAGE_PREFIXES[language];
if (!prefixes) return listed.length > 0;
return listed.some((voice) => {
const prefix = /^([a-z])[fm]_/.exec(voice.id)?.[1];
return Boolean(prefix && prefixes.includes(prefix));
});
}
/** Locale-aware voice choices for onboarding when listing from the server fails. */
export function offlineOnboardingVoices(locale: string): VoiceInfo[] {
return voicesForLocale(OFFLINE_CATALOG, locale).slice(0, 6);
}
/** Prefer server voices that speak the UI language; otherwise use offline presets. */
export function onboardingVoicesFromList(listed: VoiceInfo[], locale: string): VoiceInfo[] {
if (listedVoicesSpeakLocale(listed, locale)) {
return voicesForLocale(listed, locale).slice(0, 6);
}
return offlineOnboardingVoices(locale);
}
export function defaultOnboardingVoiceId(locale: string, voices: VoiceInfo[]): string {
return defaultVoiceForLocale(voices, locale)?.id ?? "";
}

View File

@ -303,6 +303,13 @@ msgstr "1:1-Unterhaltung"
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "1:1-Unterhaltung mit {title}" msgstr "1:1-Unterhaltung mit {title}"
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr "1:1 mit {0}"
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "Über Gruppen" msgstr "Über Gruppen"
@ -783,6 +790,7 @@ msgstr "Verfügbar, sobald die Routine gespeichert ist"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "Kontextlimit" msgstr "Kontextlimit"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Gruppe benennen"
msgid "Name this routine" msgid "Name this routine"
msgstr "Routine benennen" msgstr "Routine benennen"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr "Gib deinem Oo einen Namen"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "Hals" msgstr "Hals"
@ -3627,6 +3640,10 @@ msgstr "Nur leere Spaces können gelöscht werden. Lösche zuerst die Bots und G
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "Gelesen werden nur Datei- und Ordnernamen." msgstr "Gelesen werden nur Datei- und Ordnernamen."
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr "Name des Oo"
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr "Vorlieben"
msgid "Present" msgid "Present"
msgstr "Anwesend" msgstr "Anwesend"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr "Enter oder Weiter — Überspringen behält Chief."
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Skills" msgstr "Skills"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,9 +4603,10 @@ msgstr "Skills"
msgid "Skip" msgid "Skip"
msgstr "Überspringen" msgstr "Überspringen"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "Überspringen · Englisch" msgstr "Überspringen · {0}"
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
msgid "Skip or deploy key" msgid "Skip or deploy key"
@ -4614,6 +4637,10 @@ msgstr "Software-Update"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "Etwas braucht deine Aufmerksamkeit." msgstr "Etwas braucht deine Aufmerksamkeit."
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr "Kurz und persönlich — oder Standard behalten und später umbenennen."
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "Sortiere die Prioritäten…" msgstr "Sortiere die Prioritäten…"

View File

@ -303,6 +303,13 @@ msgstr "1:1 conversation"
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "1:1 conversation with {title}" msgstr "1:1 conversation with {title}"
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr "1:1 with {0}"
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "About groups" msgstr "About groups"
@ -783,6 +790,7 @@ msgstr "Available after the routine is saved"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "Context limit" msgstr "Context limit"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Name this group"
msgid "Name this routine" msgid "Name this routine"
msgstr "Name this routine" msgstr "Name this routine"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr "Name your Oo"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "Neck" msgstr "Neck"
@ -3627,6 +3640,10 @@ msgstr "Only empty spaces can be deleted. Delete its bots and groups first."
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "Only file and folder names are read." msgstr "Only file and folder names are read."
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr "Oo name"
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr "Preferences"
msgid "Present" msgid "Present"
msgstr "Present" msgstr "Present"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Skills" msgstr "Skills"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,9 +4603,10 @@ msgstr "Skills"
msgid "Skip" msgid "Skip"
msgstr "Skip" msgstr "Skip"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "Skip · English" msgstr "Skip · {0}"
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
msgid "Skip or deploy key" msgid "Skip or deploy key"
@ -4614,6 +4637,10 @@ msgstr "Software update"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "Something needs your attention." msgstr "Something needs your attention."
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr "Something short and yours — or keep the default and rename later."
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "Sorting priorities…" msgstr "Sorting priorities…"

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr "Disponible después de guardar la rutina"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "Límite de contexto" msgstr "Límite de contexto"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Nombra este grupo"
msgid "Name this routine" msgid "Name this routine"
msgstr "Nombra esta rutina" msgstr "Nombra esta rutina"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Habilidades" msgstr "Habilidades"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "Habilidades"
msgid "Skip" msgid "Skip"
msgstr "Omitir" msgstr "Omitir"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr "Actualización de software"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "कॉन्टेक्स्ट सीमा" msgstr "कॉन्टेक्स्ट सीमा"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "इस समूह को नाम दें"
msgid "Name this routine" msgid "Name this routine"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "स्किल्स" msgstr "स्किल्स"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "स्किल्स"
msgid "Skip" msgid "Skip"
msgstr "छोड़ें" msgstr "छोड़ें"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr ""
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr "루틴을 저장한 후 사용할 수 있습니다"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "컨텍스트 한도" msgstr "컨텍스트 한도"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "그룹 이름"
msgid "Name this routine" msgid "Name this routine"
msgstr "이 루틴의 이름을 정하세요" msgstr "이 루틴의 이름을 정하세요"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "스킬" msgstr "스킬"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "스킬"
msgid "Skip" msgid "Skip"
msgstr "건너뛰기" msgstr "건너뛰기"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr "소프트웨어 업데이트"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "Limite de contexto" msgstr "Limite de contexto"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Dar nome ao grupo"
msgid "Name this routine" msgid "Name this routine"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Habilidades" msgstr "Habilidades"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "Habilidades"
msgid "Skip" msgid "Skip"
msgstr "Pular" msgstr "Pular"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr ""
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "О группах" msgstr "О группах"
@ -783,6 +790,7 @@ msgstr "Доступно после сохранения задачи"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "" msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Назовите эту группу"
msgid "Name this routine" msgid "Name this routine"
msgstr "Назовите эту задачу" msgstr "Назовите эту задачу"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr "Удалить можно только пустые пространст
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Навыки" msgstr "Навыки"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "Навыки"
msgid "Skip" msgid "Skip"
msgstr "Пропустить" msgstr "Пропустить"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr "Обновление программного обеспечения"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "Bağlam sınırı" msgstr "Bağlam sınırı"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "Gruba bir ad ver"
msgid "Name this routine" msgid "Name this routine"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "Beceriler" msgstr "Beceriler"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "Beceriler"
msgid "Skip" msgid "Skip"
msgstr "Atla" msgstr "Atla"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr ""
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -303,6 +303,13 @@ msgstr ""
msgid "1:1 conversation with {title}" msgid "1:1 conversation with {title}"
msgstr "" msgstr ""
#. placeholder {0}: mainConversationEntry.bot.name
#. placeholder {0}: speaker.name
#: src/components/stage/OoStage.tsx
#: src/pages/Shell.tsx
msgid "1:1 with {0}"
msgstr ""
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About groups" msgid "About groups"
msgstr "" msgstr ""
@ -783,6 +790,7 @@ msgstr "保存例行任务后可用"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
#: src/pages/PluginsOverlay.tsx #: src/pages/PluginsOverlay.tsx
@ -1423,6 +1431,7 @@ msgid "Context limit"
msgstr "上下文限制" msgstr "上下文限制"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingOoDesigner.tsx #: src/components/onboarding/OnboardingOoDesigner.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
@ -3317,6 +3326,10 @@ msgstr "给这个群组命名"
msgid "Name this routine" msgid "Name this routine"
msgstr "给这个例行任务命名" msgstr "给这个例行任务命名"
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Name your Oo"
msgstr ""
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Neck" msgid "Neck"
msgstr "" msgstr ""
@ -3627,6 +3640,10 @@ msgstr ""
msgid "Only file and folder names are read." msgid "Only file and folder names are read."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Oo name"
msgstr ""
#: src/pages/ScratchpadSection.tsx #: src/pages/ScratchpadSection.tsx
#: src/pages/shell/computer-panel-preview.tsx #: src/pages/shell/computer-panel-preview.tsx
msgid "Open" msgid "Open"
@ -3833,6 +3850,10 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
msgstr ""
#. placeholder {0}: props.name #. placeholder {0}: props.name
#: src/components/ArtifactFileCard.tsx #: src/components/ArtifactFileCard.tsx
msgid "Preview {0}" msgid "Preview {0}"
@ -4574,6 +4595,7 @@ msgid "Skills"
msgstr "技能" msgstr "技能"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
#: src/components/onboarding/OnboardingNameStep.tsx
#: src/components/onboarding/OnboardingScanStep.tsx #: src/components/onboarding/OnboardingScanStep.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
@ -4581,8 +4603,9 @@ msgstr "技能"
msgid "Skip" msgid "Skip"
msgstr "跳过" msgstr "跳过"
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
msgid "Skip · English" msgid "Skip · {0}"
msgstr "" msgstr ""
#: src/lib/localized-provider-hint.ts #: src/lib/localized-provider-hint.ts
@ -4614,6 +4637,10 @@ msgstr "软件更新"
msgid "Something needs your attention." msgid "Something needs your attention."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingNameStep.tsx
msgid "Something short and yours — or keep the default and rename later."
msgstr ""
#: src/lib/voice-persona-copy.ts #: src/lib/voice-persona-copy.ts
msgid "Sorting priorities…" msgid "Sorting priorities…"
msgstr "" msgstr ""

View File

@ -5,6 +5,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { IntegrationSetup } from "../components/integrations/IntegrationSetup"; import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
import { OnboardingDiscoveryShell } from "../components/onboarding/OnboardingDiscoveryShell"; import { OnboardingDiscoveryShell } from "../components/onboarding/OnboardingDiscoveryShell";
import {
DEFAULT_FIRST_OO_NAME,
OnboardingNameStep,
} from "../components/onboarding/OnboardingNameStep";
import { import {
type OnboardingEmojiConfig, type OnboardingEmojiConfig,
OnboardingOoDesigner, OnboardingOoDesigner,
@ -23,7 +27,12 @@ import {
import { findLookCategory, lookPresetToAvatarValue } from "../lib/onboarding-look-categories"; import { findLookCategory, lookPresetToAvatarValue } from "../lib/onboarding-look-categories";
import { onboardingProfileUpdate } from "../lib/onboarding-profile"; import { onboardingProfileUpdate } from "../lib/onboarding-profile";
import { rpc, selectSpace } from "../lib/rpc"; import { rpc, selectSpace } from "../lib/rpc";
import { persistUiLocale, UI_LOCALE_LABELS, type UiLocale } from "../lib/ui-locale"; import {
persistUiLocale,
resolveUiLocale,
UI_LOCALE_LABELS,
type UiLocale,
} from "../lib/ui-locale";
import { collectLocalProfileSignals, persistUserProfileHarvest } from "../lib/user-profile-harvest"; import { collectLocalProfileSignals, persistUserProfileHarvest } from "../lib/user-profile-harvest";
/** Selects the main conversation's space and returns its route. */ /** Selects the main conversation's space and returns its route. */
@ -44,8 +53,9 @@ export function OnboardingPage() {
const { t } = useLingui(); const { t } = useLingui();
const navigate = useNavigate(); const navigate = useNavigate();
const [step, setStep] = useState< const [step, setStep] = useState<
"loading" | "emoji" | "language" | "voice" | "integrations" | "scan" | "bot" "loading" | "emoji" | "name" | "language" | "voice" | "integrations" | "scan" | "bot"
>("emoji"); >("emoji");
const [ooName, setOoName] = useState(DEFAULT_FIRST_OO_NAME);
const [selectedVoiceId, setSelectedVoiceId] = useState(""); const [selectedVoiceId, setSelectedVoiceId] = useState("");
const [voiceSkipped, setVoiceSkipped] = useState(false); const [voiceSkipped, setVoiceSkipped] = useState(false);
const [integrationSetup, setIntegrationSetup] = useState<IntegrationSetupState | null>(null); const [integrationSetup, setIntegrationSetup] = useState<IntegrationSetupState | null>(null);
@ -57,14 +67,9 @@ export function OnboardingPage() {
const [crewReady, setCrewReady] = useState(false); const [crewReady, setCrewReady] = useState(false);
const [emojiConfig, setEmojiConfig] = useState<OnboardingEmojiConfig>(defaultEmojiConfig); const [emojiConfig, setEmojiConfig] = useState<OnboardingEmojiConfig>(defaultEmojiConfig);
const [currentLocale, setCurrentLocale] = useState<string>(() => { const [currentLocale, setCurrentLocale] = useState<UiLocale>(() => resolveUiLocale());
try {
return localStorage.getItem("rakazo.uiLocale") ?? "en";
} catch {
return "en";
}
});
const [languageChoice, setLanguageChoice] = useState<UiLocale | null>(null); const [languageChoice, setLanguageChoice] = useState<UiLocale | null>(null);
const activeUiLocale = languageChoice ?? currentLocale;
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -110,7 +115,7 @@ export function OnboardingPage() {
const discoveryPhase: OnboardingDiscoveryPhase = const discoveryPhase: OnboardingDiscoveryPhase =
step === "loading" step === "loading"
? "loading" ? "loading"
: step === "emoji" || step === "language" || step === "voice" : step === "emoji" || step === "name" || step === "language" || step === "voice"
? "emoji" ? "emoji"
: step === "integrations" : step === "integrations"
? "integrations" ? "integrations"
@ -145,7 +150,10 @@ export function OnboardingPage() {
setError(null); setError(null);
try { try {
const bot = await rpc.bots.ensureMain(); const bot = await rpc.bots.ensureMain();
await rpc.bots.update({ botId: bot.id, color: emojiConfig.avatarValue }).catch(() => null); const trimmedName = ooName.trim() || DEFAULT_FIRST_OO_NAME;
await rpc.bots
.update({ botId: bot.id, color: emojiConfig.avatarValue, name: trimmedName })
.catch(() => null);
setCrewReady(true); setCrewReady(true);
await rpc.profile await rpc.profile
@ -213,7 +221,11 @@ export function OnboardingPage() {
<OnboardingDiscoveryShell <OnboardingDiscoveryShell
items={discoveryItems} items={discoveryItems}
compact={ compact={
step === "emoji" || step === "language" || step === "voice" || step === "integrations" step === "emoji" ||
step === "name" ||
step === "language" ||
step === "voice" ||
step === "integrations"
} }
> >
{error && step !== "bot" ? <p className="mb-4 text-sm text-destructive">{error}</p> : null} {error && step !== "bot" ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}
@ -222,6 +234,16 @@ export function OnboardingPage() {
<OnboardingOoDesigner <OnboardingOoDesigner
config={emojiConfig} config={emojiConfig}
onChange={setEmojiConfig} onChange={setEmojiConfig}
onContinue={() => setStep("name")}
/>
) : null}
{step === "name" ? (
<OnboardingNameStep
config={emojiConfig}
name={ooName}
onNameChange={setOoName}
onBack={() => setStep("emoji")}
onContinue={() => setStep("language")} onContinue={() => setStep("language")}
/> />
) : null} ) : null}
@ -241,7 +263,7 @@ export function OnboardingPage() {
type="button" type="button"
onClick={() => { onClick={() => {
persistUiLocale(code as UiLocale); persistUiLocale(code as UiLocale);
setCurrentLocale(code); setCurrentLocale(code as UiLocale);
setLanguageChoice(code as UiLocale); setLanguageChoice(code as UiLocale);
setStep("voice"); setStep("voice");
}} }}
@ -256,11 +278,18 @@ export function OnboardingPage() {
))} ))}
</div> </div>
<div className="mt-8 flex items-center justify-between"> <div className="mt-8 flex items-center justify-between">
<Button variant="ghost" size="sm" onClick={() => setStep("emoji")}> <Button variant="ghost" size="sm" onClick={() => setStep("name")}>
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
<Button onClick={() => setStep("voice")}> <Button
<Trans>Skip · English</Trans> onClick={() => {
const locale = resolveUiLocale();
setLanguageChoice(locale);
setCurrentLocale(locale);
setStep("voice");
}}
>
{t`Skip · ${UI_LOCALE_LABELS[activeUiLocale]}`}
</Button> </Button>
</div> </div>
</div> </div>
@ -268,6 +297,7 @@ export function OnboardingPage() {
{step === "voice" ? ( {step === "voice" ? (
<OnboardingVoiceStep <OnboardingVoiceStep
uiLocale={activeUiLocale}
selectedVoiceId={selectedVoiceId} selectedVoiceId={selectedVoiceId}
onSelectedVoiceIdChange={setSelectedVoiceId} onSelectedVoiceIdChange={setSelectedVoiceId}
onBack={() => setStep("language")} onBack={() => setStep("language")}

View File

@ -84,7 +84,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { ArtifactPane } from "../components/ArtifactPane"; import { ArtifactPane } from "../components/ArtifactPane";
import { InAppBrowser } from "../components/browser/InAppBrowser"; import { InAppBrowser } from "../components/browser/InAppBrowser";
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress"; import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
@ -95,6 +95,7 @@ import { useWideLayout } from "../hooks/useWideLayout";
import { readActivityMode, writeActivityMode } from "../lib/activity-mode"; import { readActivityMode, writeActivityMode } from "../lib/activity-mode";
import { rosterActivitySubtitle } from "../lib/agent-activity"; import { rosterActivitySubtitle } from "../lib/agent-activity";
import { agentPointerFeed } from "../lib/agent-pointer"; import { agentPointerFeed } from "../lib/agent-pointer";
import { HEUTE_APP_PATH, isHeuteAppPath } from "../lib/app-routes";
import type { ArtifactTarget } from "../lib/artifact-open"; import type { ArtifactTarget } from "../lib/artifact-open";
import { attachmentMaxBytes, uploadAttachment } from "../lib/artifact-upload"; import { attachmentMaxBytes, uploadAttachment } from "../lib/artifact-upload";
import { authClient } from "../lib/auth"; import { authClient } from "../lib/auth";
@ -103,6 +104,7 @@ import { requestBrowserNotificationPermission } from "../lib/browser-notificatio
import { loadComputerScreen } from "../lib/computer-screen"; import { loadComputerScreen } from "../lib/computer-screen";
import { scheduleFocusPrompt } from "../lib/focus-prompt"; import { scheduleFocusPrompt } from "../lib/focus-prompt";
import { formatBytes } from "../lib/format-bytes"; import { formatBytes } from "../lib/format-bytes";
import { isMultiOoGroupChat } from "../lib/group-chat";
import { import {
isWorkingRunStatus, isWorkingRunStatus,
resolveActiveGroupSpeaker, resolveActiveGroupSpeaker,
@ -267,14 +269,26 @@ import { SortableGroup } from "../components/SortableGroup";
export function ShellPage() { export function ShellPage() {
const { t } = useLingui(); const { t } = useLingui();
const { botId, groupId } = useParams(); const { botId, groupId } = useParams();
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const onHeuteRoute = isHeuteAppPath(location.pathname);
useEffect(() => {
if (botId === "heute") {
navigate(HEUTE_APP_PATH, { replace: true });
}
}, [botId, navigate]);
const [activeNavPillar, setActiveNavPillar] = useState<NavPillar>(() => { const [activeNavPillar, setActiveNavPillar] = useState<NavPillar>(() => {
if (isHeuteAppPath(location.pathname)) return "heute";
if (groupId) return "projekte"; if (groupId) return "projekte";
if (botId) return "team"; if (botId) return "team";
return "heute"; return "heute";
}); });
useEffect(() => {
if (isHeuteAppPath(location.pathname)) setActiveNavPillar("heute");
}, [location.pathname]);
useEffect(() => { useEffect(() => {
if (botId) setActiveNavPillar("team"); if (botId) setActiveNavPillar("team");
}, [botId]); }, [botId]);
@ -611,6 +625,11 @@ export function ShellPage() {
const recordingSkill = activeTaughtSkills.find((skill) => skill.status === "recording") ?? null; const recordingSkill = activeTaughtSkills.find((skill) => skill.status === "recording") ?? null;
const routeBotId = useRef<string | undefined>(botId); const routeBotId = useRef<string | undefined>(botId);
routeBotId.current = botId; routeBotId.current = botId;
const routePathname = useRef(location.pathname);
routePathname.current = location.pathname;
const routeOnHeute = useRef(onHeuteRoute);
routeOnHeute.current = onHeuteRoute;
const holdingHeuteRoute = () => routeOnHeute.current || isHeuteAppPath(routePathname.current);
const routeGroupId = useRef<string | undefined>(groupId); const routeGroupId = useRef<string | undefined>(groupId);
routeGroupId.current = groupId; routeGroupId.current = groupId;
const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id); const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id);
@ -745,6 +764,7 @@ export function ShellPage() {
return; return;
} }
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) { if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
if (holdingHeuteRoute()) return;
navigate(firstThreadRoute(list, groupList, inProject), { replace: true }); navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
} }
} finally { } finally {
@ -992,9 +1012,11 @@ export function ShellPage() {
return; return;
} }
if (inProject && !botId) { if (inProject && !botId) {
if (holdingHeuteRoute()) return;
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true }); navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
return; return;
} }
if (holdingHeuteRoute()) return;
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id; const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
if (selectedBotId && selectedBotId !== botId) { if (selectedBotId && selectedBotId !== botId) {
navigate(`/app/${selectedBotId}`, { replace: true }); navigate(`/app/${selectedBotId}`, { replace: true });
@ -1580,6 +1602,9 @@ export function ShellPage() {
: snapshot?.botId === active?.id : snapshot?.botId === active?.id
? snapshot ? snapshot
: null; : null;
const activeGroupMemberCount =
activeGroup?.members?.length ?? activeSnapshot?.members?.length ?? 0;
const multiOoInGroup = inGroup && isMultiOoGroupChat(activeGroupMemberCount);
const activeReplyTarget = const activeReplyTarget =
replyTarget && activeSnapshot?.messages.some((message) => message.id === replyTarget.id) replyTarget && activeSnapshot?.messages.some((message) => message.id === replyTarget.id)
? replyTarget ? replyTarget
@ -2796,6 +2821,7 @@ export function ShellPage() {
ooSound.play("interact"); ooSound.play("interact");
setActiveNavPillar("heute"); setActiveNavPillar("heute");
setMobileSidebarOpen(false); setMobileSidebarOpen(false);
navigate(HEUTE_APP_PATH);
}} }}
className={rosterRowClass(activeNavPillar === "heute")} className={rosterRowClass(activeNavPillar === "heute")}
> >
@ -3213,10 +3239,17 @@ export function ShellPage() {
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug"> <span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{composerRunning || active.status === "running" ? t`Busy` : active.status} {composerRunning || active.status === "running" ? t`Busy` : active.status}
</span> </span>
) : inGroup && projectSessionKey ? ( ) : multiOoInGroup && projectSessionKey ? (
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug"> <span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{t`Group conversation`} {t`Group conversation`}
</span> </span>
) : inGroup &&
activeGroup?.kind === "main" &&
activeGroupMemberCount <= 1 &&
mainConversationEntry ? (
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{t`1:1 with ${mainConversationEntry.bot.name}`}
</span>
) : inGroup && activeGroup?.kind !== "main" ? ( ) : inGroup && activeGroup?.kind !== "main" ? (
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug"> <span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
{t`Project · sessions & folders`} {t`Project · sessions & folders`}

View File

@ -20,6 +20,7 @@ export function SidebarToggleButtons({
{collapsed && desktopBridge() ? <WindowChrome /> : null} {collapsed && desktopBridge() ? <WindowChrome /> : null}
<button <button
type="button" type="button"
data-testid="open-navigation"
aria-label={t`Open navigation`} aria-label={t`Open navigation`}
onClick={onOpenNavigation} onClick={onOpenNavigation}
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden" className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"

View File

@ -17,6 +17,20 @@ export function useMobileSidebar() {
return () => desktop.removeEventListener("change", closeMobileSidebar); return () => desktop.removeEventListener("change", closeMobileSidebar);
}, []); }, []);
useEffect(() => {
if (!mobileSidebarOpen) return;
function onKeyDown(event: KeyboardEvent) {
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
event.preventDefault();
setMobileSidebarOpen(false);
queueMicrotask(() => {
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
});
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [mobileSidebarOpen]);
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) { function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
if ( if (
mobileSidebarOpen || mobileSidebarOpen ||