Merge pull request #2 from cynarAI/cursor/onboarding-nav-settings-728e

Settings-first navigation: integrations and live trading banner
This commit is contained in:
cynarAI 2026-09-21 19:18:50 +02:00 committed by GitHub
commit b9c0bace03
24 changed files with 384 additions and 302 deletions

View File

@ -50,8 +50,8 @@ test("logout protects bot deep links and sign-in restores the session", async ({
const protectedBotPath = new URL(page.url()).pathname; const protectedBotPath = new URL(page.url()).pathname;
await expect(page.getByPlaceholder("Message Chief")).toBeVisible(); await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
await page.getByRole("button", { name: new RegExp(userName, "i") }).click(); await expect(page.getByTestId("settings-entry")).toBeVisible();
await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible(); await page.getByTestId("user-menu-trigger").click();
await expect(page.getByRole("button", { name: "Usage", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Usage", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Log out" })).toBeVisible(); await expect(page.getByRole("button", { name: "Log out" })).toBeVisible();
await expect( await expect(
@ -152,8 +152,7 @@ test("changes and recovers an email password", async ({ page }, testInfo) => {
await completeOnboarding(page); await completeOnboarding(page);
await page.waitForURL(/\/app\/[^/]+$/); await page.waitForURL(/\/app\/[^/]+$/);
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
await expect(page.getByTestId("sidebar-search").locator("input")).toHaveAttribute( await expect(page.getByTestId("sidebar-search").locator("input")).toHaveAttribute(

View File

@ -3,6 +3,7 @@ import {
activeBotId, activeBotId,
captureScreenshot, captureScreenshot,
completeOnboarding, completeOnboarding,
openIntegrations,
realSandboxTimeout, realSandboxTimeout,
rpc, rpc,
signup, signup,
@ -134,7 +135,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
await expect(page.getByText("Monday briefing")).toBeVisible(); await expect(page.getByText("Monday briefing")).toBeVisible();
await captureScreenshot(page, testInfo, "10-routine-created"); await captureScreenshot(page, testInfo, "10-routine-created");
await page.getByText("Integrations").click(); await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible(); await expect(page.getByPlaceholder("Search apps")).toBeVisible();
const featured = page.getByTestId("featured-connectors"); const featured = page.getByTestId("featured-connectors");
await expect(featured).toContainText( await expect(featured).toContainText(
@ -178,8 +179,8 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
await expect(detail.getByLabel("Account label").nth(1)).toHaveValue("Work"); await expect(detail.getByLabel("Account label").nth(1)).toHaveValue("Work");
await captureScreenshot(page, testInfo, "11a2-multi-account-plugins"); await captureScreenshot(page, testInfo, "11a2-multi-account-plugins");
await page.getByRole("button", { name: "Close integrations" }).click(); await page.getByRole("button", { name: "Close integrations settings" }).click();
await page.getByText("Integrations").click(); await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible(); await expect(page.getByPlaceholder("Search apps")).toBeVisible();
const gmailTileAgain = page.getByTestId("connection-tile-gmail"); const gmailTileAgain = page.getByTestId("connection-tile-gmail");
await expect(gmailTileAgain.getByRole("button", { name: "Added", exact: true })).toBeVisible(); await expect(gmailTileAgain.getByRole("button", { name: "Added", exact: true })).toBeVisible();
@ -296,7 +297,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
).toBeVisible(); ).toBeVisible();
await captureScreenshot(page, testInfo, "11d-provider-emulators"); await captureScreenshot(page, testInfo, "11d-provider-emulators");
await page.getByRole("button", { name: "Close integrations" }).click(); await page.getByRole("button", { name: "Close integrations settings" }).click();
await page.getByText("Chief").first().click(); await page.getByText("Chief").first().click();
const gear = page.getByRole("button", { name: "Show settings" }); const gear = page.getByRole("button", { name: "Show settings" });

View File

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, signup } from "./helpers"; import { captureScreenshot, completeOnboarding, openIntegrations, signup } from "./helpers";
test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Executor, Treg order", async ({ test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Executor, Treg order", async ({
page, page,
@ -8,7 +8,7 @@ test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Execu
await signup(page, `graphql-source-${stamp}@rakazo.test`, "password12", `GraphQL ${stamp}`); await signup(page, `graphql-source-${stamp}@rakazo.test`, "password12", `GraphQL ${stamp}`);
await completeOnboarding(page); await completeOnboarding(page);
await page.getByText("Integrations").click(); await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible(); await expect(page.getByPlaceholder("Search apps")).toBeVisible();
await expect(page.getByRole("button", { name: "Add GraphQL", exact: true })).toBeHidden(); await expect(page.getByRole("button", { name: "Add GraphQL", exact: true })).toBeHidden();

View File

@ -12,6 +12,23 @@ async function createBot(page: Page, name: string) {
return createNamedBot(page, name); return createNamedBot(page, name);
} }
test("start group conversation lives under Conversations, not Projekte", async ({ page }) => {
const stamp = Date.now();
await signup(page, `group-ia-${stamp}@rakazo.test`, "password12", "Group IA");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const conversations = page.locator(".sidebar-conversations");
await expect(conversations.getByText("Conversations", { exact: true })).toBeVisible();
const startGroup = conversations.getByTestId("start-group-conversation");
await expect(startGroup).toBeVisible();
await expect(page.locator(".sidebar-projekte").getByTestId("start-group-conversation")).toHaveCount(
0,
);
await expect(page.locator(".sidebar-projekte")).not.toContainText("Start a group conversation");
});
test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => { test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => {
const stamp = Date.now(); const stamp = Date.now();
await signup(page, `group-${stamp}@rakazo.test`, "password12", "Group E2E"); await signup(page, `group-${stamp}@rakazo.test`, "password12", "Group E2E");

View File

@ -127,10 +127,9 @@ export async function createBotFromPicker(
/** Open the user Settings overlay, optionally switching to a sidebar section. */ /** Open the user Settings overlay, optionally switching to a sidebar section. */
export async function openUserSettings( export async function openUserSettings(
page: Page, page: Page,
section?: "general" | "models" | "memory" | "voice" | "usage" | "computer" | "updates", section?: "general" | "integrations" | "models" | "memory" | "voice" | "usage" | "computer" | "updates",
) { ) {
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
if (section && section !== "general") { if (section && section !== "general") {
@ -139,6 +138,11 @@ export async function openUserSettings(
return settings; return settings;
} }
/** Open Integrations inside the Settings overlay. */
export async function openIntegrations(page: Page) {
return openUserSettings(page, "integrations");
}
/** Create a named bot via RPC for test setup (skips the + picker). */ /** Create a named bot via RPC for test setup (skips the + picker). */
export async function createNamedBot( export async function createNamedBot(
page: Page, page: Page,

View File

@ -245,8 +245,7 @@ test("configured server owners manage providers from settings", async ({ page },
await signup(page, `configured-owner-${Date.now()}@rakazo.test`, "password12", "Server Owner"); await signup(page, `configured-owner-${Date.now()}@rakazo.test`, "password12", "Server Owner");
await expect(page.getByRole("heading", { name: "Server integrations" })).toBeHidden(); await expect(page.getByRole("heading", { name: "Server integrations" })).toBeHidden();
await completeOnboarding(page); await completeOnboarding(page);
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
const link = settings.getByRole("link", { name: "Server integrations", exact: true }); const link = settings.getByRole("link", { name: "Server integrations", exact: true });
await expect(link).toBeVisible(); await expect(link).toBeVisible();

View File

@ -83,7 +83,8 @@ test("connects an MCP server through the OAuth popup callback", async ({ page },
}); });
}); });
await page.getByText("Integrations", { exact: true }).click(); await page.getByTestId("settings-entry").click();
await page.getByTestId("settings-nav-integrations").click();
await page.getByTestId("integrations-advanced").evaluate((element) => { await page.getByTestId("integrations-advanced").evaluate((element) => {
(element as HTMLDetailsElement).open = true; (element as HTMLDetailsElement).open = true;
}); });

View File

@ -9,12 +9,12 @@ test("settings shell is two-pane and deep-links Models Memory Voice Usage", asyn
await signup(page, `settings-shell-${stamp}@rakazo.test`, "password12", userName); await signup(page, `settings-shell-${stamp}@rakazo.test`, "password12", userName);
await completeOnboarding(page); await completeOnboarding(page);
await expect(page.getByTestId("settings-entry")).toBeVisible();
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("user-menu-trigger").click();
const menu = page.locator('[data-slot="popover-content"]'); const menu = page.locator('[data-slot="popover-content"]');
await expect(menu.getByRole("button", { name: "Settings", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Usage", exact: true })).toBeVisible(); await expect(menu.getByRole("button", { name: "Usage", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Log out", exact: true })).toBeVisible(); await expect(menu.getByRole("button", { name: "Log out", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Models", exact: true })).toHaveCount(0); await expect(menu.getByRole("button", { name: "Settings", exact: true })).toHaveCount(0);
await captureScreenshot(page, testInfo, "settings-account-menu-lean"); await captureScreenshot(page, testInfo, "settings-account-menu-lean");
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
@ -25,9 +25,16 @@ test("settings shell is two-pane and deep-links Models Memory Voice Usage", asyn
"page", "page",
); );
await expect(settings.getByRole("heading", { name: "General", exact: true })).toBeVisible(); await expect(settings.getByRole("heading", { name: "General", exact: true })).toBeVisible();
await expect(settings.getByTestId("live-trading-banner")).toBeVisible();
await expect(settings.getByRole("heading", { name: "Account", exact: true })).toBeVisible(); await expect(settings.getByRole("heading", { name: "Account", exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "settings-shell-general"); await captureScreenshot(page, testInfo, "settings-shell-general");
await settings.getByTestId("settings-nav-integrations").click();
await expect(settings).toHaveAttribute("data-settings-section", "integrations");
await expect(settings.getByRole("heading", { name: "Integrations", exact: true })).toBeVisible();
await expect(settings.getByTestId("integrations-settings")).toBeVisible();
await captureScreenshot(page, testInfo, "settings-shell-integrations");
await settings.getByTestId("settings-nav-models").click(); await settings.getByTestId("settings-nav-models").click();
await expect(settings).toHaveAttribute("data-settings-section", "models"); await expect(settings).toHaveAttribute("data-settings-section", "models");
await expect(settings.getByRole("heading", { name: "Models", exact: true })).toBeVisible(); await expect(settings.getByRole("heading", { name: "Models", exact: true })).toBeVisible();

View File

@ -49,8 +49,7 @@ test("account settings appearance control switches to light mode", async ({ page
await signup(page, `ui-appearance-${stamp}@rakazo.test`, "password12", "Appearance QA"); await signup(page, `ui-appearance-${stamp}@rakazo.test`, "password12", "Appearance QA");
await completeOnboarding(page, testInfo); await completeOnboarding(page, testInfo);
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
await expect(settings.getByRole("heading", { name: "Appearance", exact: true })).toBeVisible(); await expect(settings.getByRole("heading", { name: "Appearance", exact: true })).toBeVisible();
@ -84,8 +83,7 @@ test("account settings appearance control switches to light mode", async ({ page
await expect(inlinePath).toHaveCSS("color", "rgb(26, 26, 26)"); await expect(inlinePath).toHaveCSS("color", "rgb(26, 26, 26)");
await captureScreenshot(page, testInfo, "inline-code-light"); await captureScreenshot(page, testInfo, "inline-code-light");
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
await settings.getByTestId("ui-appearance-dark").click(); await settings.getByTestId("ui-appearance-dark").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
@ -114,9 +112,9 @@ test("sidebar bot rows hover with the same tone as the integrations row", async
const chiefBg = await readChiefBg(); const chiefBg = await readChiefBg();
await captureScreenshot(page, testInfo, "sidebar-row-hover"); await captureScreenshot(page, testInfo, "sidebar-row-hover");
const integrations = sidebar.getByRole("button", { name: "Integrations", exact: true }); const settingsEntry = sidebar.getByTestId("settings-entry");
await integrations.hover(); await settingsEntry.hover();
await expect await expect
.poll(() => integrations.evaluate((el) => getComputedStyle(el).backgroundColor)) .poll(() => settingsEntry.evaluate((el) => getComputedStyle(el).backgroundColor))
.toBe(chiefBg); .toBe(chiefBg);
}); });

View File

@ -77,8 +77,7 @@ test("account settings language picker includes Russian and persists it", async
await signup(page, `ui-locale-ru-${stamp}@rakazo.test`, "password12", "Locale QA"); await signup(page, `ui-locale-ru-${stamp}@rakazo.test`, "password12", "Locale QA");
await completeOnboarding(page, testInfo); await completeOnboarding(page, testInfo);
await page.getByTestId("user-menu-trigger").click(); await page.getByTestId("settings-entry").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();

View File

@ -477,6 +477,17 @@
"Server URL": "Server-URL", "Server URL": "Server-URL",
"Set up voice to call": "Stimme für Anrufe einrichten", "Set up voice to call": "Stimme für Anrufe einrichten",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"Conversations": "Unterhaltungen",
"One-to-one and group threads": "Einzel- und Gruppengespräche",
"No conversations yet.": "Noch keine Unterhaltungen.",
"Add an Oo": "Oo hinzufügen",
"Start a group conversation": "Gruppengespräch starten",
"No projects yet.": "Noch keine Projekte.",
"Create a project": "Projekt erstellen",
"Threads by topic · folders": "Threads nach Thema · Ordner",
"Discover now": "Jetzt entdecken",
"Live Trading": "Live Trading",
"Close integrations settings": "Integrationen-Einstellungen schließen",
"Settings: General": "Einstellungen: Allgemein", "Settings: General": "Einstellungen: Allgemein",
"Settings: Usage": "Einstellungen: Nutzung", "Settings: Usage": "Einstellungen: Nutzung",
"Setup help": "Einrichtungshilfe", "Setup help": "Einrichtungshilfe",

View File

@ -200,9 +200,10 @@ msgstr "Aktionen für {0}"
msgid "Actions for space" msgid "Actions for space"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx:76
#: src/pages/RoutineEditor.tsx:297 #: src/pages/RoutineEditor.tsx:297
msgid "Active" msgid "Active"
msgstr "" msgstr "Aktiv"
#: src/pages/ModelSettingsOverlay.tsx:393 #: src/pages/ModelSettingsOverlay.tsx:393
msgid "Active model" msgid "Active model"
@ -220,6 +221,9 @@ msgid "Add"
msgstr "Hinzufügen" msgstr "Hinzufügen"
#: src/components/ApprovalRulesSettings.tsx:151 #: src/components/ApprovalRulesSettings.tsx:151
msgid "Add an Oo"
msgstr "Oo hinzufügen"
msgid "Add a model in Settings to use this." msgid "Add a model in Settings to use this."
msgstr "" msgstr ""
@ -814,6 +818,10 @@ msgstr "Bildvorschau schließen"
msgid "Close integrations" msgid "Close integrations"
msgstr "Integrationen schließen" msgstr "Integrationen schließen"
#: src/pages/SettingsOverlay.tsx:108
msgid "Close integrations settings"
msgstr "Integrationen-Einstellungen schließen"
#: src/pages/McpServersOverlay.tsx:258 #: src/pages/McpServersOverlay.tsx:258
msgid "Close MCP servers" msgid "Close MCP servers"
msgstr "MCP-Server schließen" msgstr "MCP-Server schließen"
@ -1017,6 +1025,9 @@ msgstr "Die Verbindung zu {0} steht noch aus. Du kannst dieses Fenster schließe
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Unterhaltungen"
msgid "Context limit" msgid "Context limit"
msgstr "Kontextlimit" msgstr "Kontextlimit"
@ -1422,6 +1433,10 @@ msgstr "Neue Gruppe erstellen"
msgid "Create new Space" msgid "Create new Space"
msgstr "Neuen Space erstellen" msgstr "Neuen Space erstellen"
#: src/pages/Shell.tsx:3093
msgid "Create a project"
msgstr "Projekt erstellen"
#: src/pages/RoutineEditor.tsx:122 #: src/pages/RoutineEditor.tsx:122
#: src/pages/RoutineEditor.tsx:123 #: src/pages/RoutineEditor.tsx:123
msgid "Create Routine" msgid "Create Routine"
@ -2098,6 +2113,10 @@ msgstr ""
msgid "Listen" msgid "Listen"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx:72
msgid "Live Trading"
msgstr "Live Trading"
#: src/pages/ExternalConversationSettings.tsx:93 #: src/pages/ExternalConversationSettings.tsx:93
msgid "Listening" msgid "Listening"
msgstr "" msgstr ""
@ -2522,6 +2541,13 @@ msgstr "Noch keine Nachrichten mit {peerBotName}."
msgid "No model catalog is available." msgid "No model catalog is available."
msgstr "Kein Modellkatalog verfügbar." msgstr "Kein Modellkatalog verfügbar."
#: src/pages/Shell.tsx:3086
msgid "No conversations yet."
msgstr "Noch keine Unterhaltungen."
msgid "No projects yet."
msgstr "Noch keine Projekte."
#: src/pages/ModelSettingsOverlay.tsx:453 #: src/pages/ModelSettingsOverlay.tsx:453
msgid "No providers found." msgid "No providers found."
msgstr "Keine Anbieter gefunden." msgstr "Keine Anbieter gefunden."
@ -3402,6 +3428,9 @@ msgid "Speaking…"
msgstr "Spricht…" msgstr "Spricht…"
#: src/components/teach/TeachComputerOverlay.tsx:250 #: src/components/teach/TeachComputerOverlay.tsx:250
msgid "Start a group conversation"
msgstr "Gruppengespräch starten"
msgid "Start recording" msgid "Start recording"
msgstr "Aufnahme starten" msgstr "Aufnahme starten"
@ -3595,6 +3624,9 @@ msgid "To:"
msgstr "An:" msgstr "An:"
#: src/pages/PluginsOverlay.tsx:1111 #: src/pages/PluginsOverlay.tsx:1111
msgid "Threads by topic · folders"
msgstr "Threads nach Thema · Ordner"
msgid "Tool sources" msgid "Tool sources"
msgstr "Toolquellen" msgstr "Toolquellen"
@ -3864,11 +3896,18 @@ msgstr "Hilfe & Vibe-Passung"
msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot." msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
msgstr "Vertrau uns: Es sind viele von uns. Einfach anfangen — Thermomix oder harte Mathe, gleicher ruhiger Einstieg — mit Experten-Spezialisten wenn du sie brauchst, kein mittelmäßiger Alleskönner." msgstr "Vertrau uns: Es sind viele von uns. Einfach anfangen — Thermomix oder harte Mathe, gleicher ruhiger Einstieg — mit Experten-Spezialisten wenn du sie brauchst, kein mittelmäßiger Alleskönner."
#: src/pages/AccountSettingsOverlay.tsx:88
msgid "Discover now"
msgstr "Jetzt entdecken"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx:96 #: src/components/onboarding/OnboardingDiscoveryShell.tsx:96
msgid "Discovery" msgid "Discovery"
msgstr "Entdeckung" msgstr "Entdeckung"
#: src/pages/Onboarding.tsx:428 #: src/pages/Onboarding.tsx:428
msgid "One-to-one and group threads"
msgstr "Einzel- und Gruppengespräche"
msgid "One connection — then we take it from here" msgid "One connection — then we take it from here"
msgstr "Eine Verbindung — danach übernehmen wir" msgstr "Eine Verbindung — danach übernehmen wir"

View File

@ -220,6 +220,9 @@ msgid "Add"
msgstr "Add" msgstr "Add"
#: src/components/ApprovalRulesSettings.tsx:151 #: src/components/ApprovalRulesSettings.tsx:151
msgid "Add an Oo"
msgstr "Add an Oo"
msgid "Add a model in Settings to use this." msgid "Add a model in Settings to use this."
msgstr "Add a model in Settings to use this." msgstr "Add a model in Settings to use this."
@ -814,6 +817,10 @@ msgstr "Close image preview"
msgid "Close integrations" msgid "Close integrations"
msgstr "Close integrations" msgstr "Close integrations"
#: src/pages/SettingsOverlay.tsx:108
msgid "Close integrations settings"
msgstr "Close integrations settings"
#: src/pages/McpServersOverlay.tsx:258 #: src/pages/McpServersOverlay.tsx:258
msgid "Close MCP servers" msgid "Close MCP servers"
msgstr "Close MCP servers" msgstr "Close MCP servers"
@ -1017,6 +1024,9 @@ msgstr "Connection to {0} is still pending. You can close this and check again."
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversations"
msgid "Context limit" msgid "Context limit"
msgstr "Context limit" msgstr "Context limit"
@ -1422,6 +1432,10 @@ msgstr "Create new Group"
msgid "Create new Space" msgid "Create new Space"
msgstr "Create new Space" msgstr "Create new Space"
#: src/pages/Shell.tsx:3093
msgid "Create a project"
msgstr "Create a project"
#: src/pages/RoutineEditor.tsx:122 #: src/pages/RoutineEditor.tsx:122
#: src/pages/RoutineEditor.tsx:123 #: src/pages/RoutineEditor.tsx:123
msgid "Create Routine" msgid "Create Routine"
@ -2098,6 +2112,10 @@ msgstr "Link a chat app"
msgid "Listen" msgid "Listen"
msgstr "Listen" msgstr "Listen"
#: src/pages/AccountSettingsOverlay.tsx:72
msgid "Live Trading"
msgstr "Live Trading"
#: src/pages/ExternalConversationSettings.tsx:93 #: src/pages/ExternalConversationSettings.tsx:93
msgid "Listening" msgid "Listening"
msgstr "Listening" msgstr "Listening"
@ -2523,6 +2541,12 @@ msgid "No model catalog is available."
msgstr "No model catalog is available." msgstr "No model catalog is available."
#: src/pages/ModelSettingsOverlay.tsx:453 #: src/pages/ModelSettingsOverlay.tsx:453
msgid "No conversations yet."
msgstr "No conversations yet."
msgid "No projects yet."
msgstr "No projects yet."
msgid "No providers found." msgid "No providers found."
msgstr "No providers found." msgstr "No providers found."
@ -3402,6 +3426,9 @@ msgid "Speaking…"
msgstr "Speaking…" msgstr "Speaking…"
#: src/components/teach/TeachComputerOverlay.tsx:250 #: src/components/teach/TeachComputerOverlay.tsx:250
msgid "Start a group conversation"
msgstr "Start a group conversation"
msgid "Start recording" msgid "Start recording"
msgstr "Start recording" msgstr "Start recording"
@ -3595,6 +3622,9 @@ msgid "To:"
msgstr "To:" msgstr "To:"
#: src/pages/PluginsOverlay.tsx:1111 #: src/pages/PluginsOverlay.tsx:1111
msgid "Threads by topic · folders"
msgstr "Threads by topic · folders"
msgid "Tool sources" msgid "Tool sources"
msgstr "Tool sources" msgstr "Tool sources"
@ -3864,11 +3894,18 @@ msgstr "Help & vibe fit"
msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot." msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
msgstr "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot." msgstr "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
#: src/pages/AccountSettingsOverlay.tsx:88
msgid "Discover now"
msgstr "Discover now"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx:96 #: src/components/onboarding/OnboardingDiscoveryShell.tsx:96
msgid "Discovery" msgid "Discovery"
msgstr "Discovery" msgstr "Discovery"
#: src/pages/Onboarding.tsx:428 #: src/pages/Onboarding.tsx:428
msgid "One-to-one and group threads"
msgstr "One-to-one and group threads"
msgid "One connection — then we take it from here" msgid "One connection — then we take it from here"
msgstr "One connection — then we take it from here" msgstr "One connection — then we take it from here"

View File

@ -1017,6 +1017,9 @@ msgstr "La conexión con {0} sigue pendiente. Puedes cerrar esto y volver a comp
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversaciones"
msgid "Context limit" msgid "Context limit"
msgstr "Límite de contexto" msgstr "Límite de contexto"

View File

@ -1017,6 +1017,9 @@ msgstr "{0} से कनेक्शन अभी लंबित है। आ
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "बातचीत"
msgid "Context limit" msgid "Context limit"
msgstr "कॉन्टेक्स्ट सीमा" msgstr "कॉन्टेक्स्ट सीमा"

View File

@ -1017,6 +1017,9 @@ msgstr "{0} 연결이 아직 진행 중입니다. 이 창을 닫고 나중에
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "대화"
msgid "Context limit" msgid "Context limit"
msgstr "컨텍스트 한도" msgstr "컨텍스트 한도"

View File

@ -1017,6 +1017,9 @@ msgstr "A conexão com o {0} ainda está pendente. Você pode fechar isso e veri
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversas"
msgid "Context limit" msgid "Context limit"
msgstr "Limite de contexto" msgstr "Limite de contexto"

View File

@ -1017,6 +1017,9 @@ msgstr "Соединение с {0} все еще не завершено. Вы
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Беседы"
msgid "Context limit" msgid "Context limit"
msgstr "" msgstr ""

View File

@ -1017,6 +1017,9 @@ msgstr "{0} bağlantısı hâlâ beklemede. Bunu kapatıp daha sonra tekrar kont
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Sohbetler"
msgid "Context limit" msgid "Context limit"
msgstr "Bağlam sınırı" msgstr "Bağlam sınırı"

View File

@ -1017,6 +1017,9 @@ msgstr "与 {0} 的连接仍在等待。可以关闭此窗口稍后再看。"
#: src/pages/ModelSettingsOverlay.tsx:590 #: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519 #: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "对话"
msgid "Context limit" msgid "Context limit"
msgstr "上下文限制" msgstr "上下文限制"

View File

@ -1,7 +1,7 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import type { AvatarStyle } from "@rakazo/contracts"; import type { AvatarStyle } from "@rakazo/contracts";
import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web"; import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web";
import { ChevronDown } from "lucide-react"; import { Activity, ChevronDown } from "lucide-react";
import { import {
type KeyboardEvent as ReactKeyboardEvent, type KeyboardEvent as ReactKeyboardEvent,
type RefObject, type RefObject,
@ -33,8 +33,66 @@ export type SettingsGeneralProps = {
messagingEnabled?: boolean; messagingEnabled?: boolean;
onOpenMessaging?: () => void; onOpenMessaging?: () => void;
isDeploymentOwner?: boolean; isDeploymentOwner?: boolean;
tradingMode?: "live" | "paper";
onDiscoverLiveTrading?: () => void;
}; };
export function LiveTradingBanner({
tradingMode,
onDiscover,
}: {
tradingMode: "live" | "paper";
onDiscover: () => void;
}) {
const isLive = tradingMode === "live";
return (
<section
className={`rounded-xl border px-4 py-4 ${
isLive ? "border-amber-500/30 bg-amber-500/5" : "border-border bg-card/40"
}`}
data-testid="live-trading-banner"
>
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<span
className={`grid size-10 shrink-0 place-items-center rounded-lg ${
isLive
? "bg-amber-500/10 text-amber-500 shadow-[0_0_12px_rgba(245,158,11,0.2)]"
: "bg-muted text-muted-foreground"
}`}
>
<Activity
strokeWidth={1.8}
className={`size-5 ${isLive ? "animate-pulse" : ""}`}
/>
</span>
<div className="min-w-0">
<h3 className="text-ui-body font-medium text-foreground">
<Trans>Live Trading</Trans>
</h3>
{isLive ? (
<p className="mt-0.5 text-ui-body-sm text-muted-foreground/70">
<Trans>Active</Trans>
</p>
) : null}
</div>
</div>
{!isLive ? (
<Button
type="button"
variant="secondary"
className="shrink-0 rounded-full"
onClick={onDiscover}
>
<Trans>Discover now</Trans>
</Button>
) : null}
</div>
</section>
);
}
export function GeneralSettingsPanels({ export function GeneralSettingsPanels({
email, email,
name, name,
@ -43,6 +101,8 @@ export function GeneralSettingsPanels({
messagingEnabled = false, messagingEnabled = false,
onOpenMessaging, onOpenMessaging,
isDeploymentOwner = false, isDeploymentOwner = false,
tradingMode,
onDiscoverLiveTrading,
}: SettingsGeneralProps) { }: SettingsGeneralProps) {
const { t } = useLingui(); const { t } = useLingui();
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale()); const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
@ -78,6 +138,9 @@ export function GeneralSettingsPanels({
return ( return (
<div className="space-y-5"> <div className="space-y-5">
{tradingMode && onDiscoverLiveTrading ? (
<LiveTradingBanner tradingMode={tradingMode} onDiscover={onDiscoverLiveTrading} />
) : null}
<section className="rounded-xl border border-border px-4 py-4"> <section className="rounded-xl border border-border px-4 py-4">
<h3 className="text-ui-body font-medium text-foreground"> <h3 className="text-ui-body font-medium text-foreground">
<Trans>Account</Trans> <Trans>Account</Trans>

View File

@ -73,10 +73,13 @@ export function PluginsOverlay({
onClose, onClose,
onOpenMcp, onOpenMcp,
activeBotId, activeBotId,
embedded = false,
}: { }: {
onClose: () => void; onClose: () => void;
onOpenMcp?: () => void; onOpenMcp?: () => void;
activeBotId?: string; activeBotId?: string;
/** Render panel body only for the shared Settings shell. */
embedded?: boolean;
}) { }) {
const { t } = useLingui(); const { t } = useLingui();
const [setupOpen, setSetupOpen] = useState(false); const [setupOpen, setSetupOpen] = useState(false);
@ -663,44 +666,27 @@ export function PluginsOverlay({
); );
} }
return ( const searchField = !detailItem ? (
<Dialog <div className={embedded ? "px-6 pt-1 sm:px-8" : "px-8 pt-4"}>
open <Input
onOpenChange={(open) => { value={query}
if (!open) onClose(); onChange={(event) => {
}} setQuery(event.target.value);
> setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE);
<DialogContent }}
showCloseButton={false} aria-label={t`Search apps`}
className="flex h-[760px] max-h-[calc(100%-2rem)] w-[1080px] max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden rounded-2xl bg-card p-0 sm:max-w-[1080px]" placeholder={t`Search apps`}
className="h-11 rounded-xl px-4 md:text-ui-body"
/>
</div>
) : null;
const catalogBody = (
<>
<div
id="integration-list"
className={`rk-scroll flex-1 overflow-y-auto py-6 ${embedded ? "px-6 sm:px-8" : "px-8"}`}
> >
<DialogHeader className="flex-row items-start justify-between px-8 pt-7">
<DialogTitle className="text-2xl text-foreground">
<Trans>Integrations</Trans>
</DialogTitle>
<DialogClose
render={<Button variant="ghost" size="icon-sm" aria-label={t`Close integrations`} />}
>
<X />
</DialogClose>
</DialogHeader>
{!detailItem ? (
<div className="px-8 pt-4">
<Input
value={query}
onChange={(event) => {
setQuery(event.target.value);
setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE);
}}
aria-label={t`Search apps`}
placeholder={t`Search apps`}
className="h-11 rounded-xl px-4 md:text-ui-body"
/>
</div>
) : null}
<div id="integration-list" className="rk-scroll flex-1 overflow-y-auto px-8 py-6">
<Button <Button
variant="outline" variant="outline"
className="mb-4" className="mb-4"
@ -1165,6 +1151,43 @@ export function PluginsOverlay({
</> </>
)} )}
</div> </div>
</>
);
if (embedded) {
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="integrations-settings">
{searchField}
{catalogBody}
</div>
);
}
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent
showCloseButton={false}
className="flex h-[760px] max-h-[calc(100%-2rem)] w-[1080px] max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden rounded-2xl bg-card p-0 sm:max-w-[1080px]"
>
<DialogHeader className="flex-row items-start justify-between px-8 pt-7">
<DialogTitle className="text-2xl text-foreground">
<Trans>Integrations</Trans>
</DialogTitle>
<DialogClose
render={<Button variant="ghost" size="icon-sm" aria-label={t`Close integrations`} />}
>
<X />
</DialogClose>
</DialogHeader>
{searchField}
{catalogBody}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -1,7 +1,17 @@
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import type { AvatarStyle, SpaceMemoryConfig } from "@rakazo/contracts"; import type { AvatarStyle, SpaceMemoryConfig } from "@rakazo/contracts";
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import { Brain, CloudDownload, Cpu, Gauge, Monitor, Settings, Volume2, XIcon } from "lucide-react"; import {
Brain,
CloudDownload,
Cpu,
Gauge,
LayoutGrid,
Monitor,
Settings,
Volume2,
XIcon,
} from "lucide-react";
import { type ComponentType, useEffect, useRef, useState } from "react"; import { type ComponentType, useEffect, useRef, useState } from "react";
import { computersAreUnavailable } from "../components/ComputersUnavailableHint"; import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
import { import {
@ -12,10 +22,12 @@ import {
} from "./AccountSettingsOverlay"; } from "./AccountSettingsOverlay";
import { MemorySettingsOverlay } from "./MemorySettingsOverlay"; import { MemorySettingsOverlay } from "./MemorySettingsOverlay";
import { ModelSettingsOverlay } from "./ModelSettingsOverlay"; import { ModelSettingsOverlay } from "./ModelSettingsOverlay";
import { PluginsOverlay } from "./PluginsOverlay";
import { VoiceSettingsOverlay } from "./VoiceSettingsOverlay"; import { VoiceSettingsOverlay } from "./VoiceSettingsOverlay";
export type SettingsSection = export type SettingsSection =
| "general" | "general"
| "integrations"
| "models" | "models"
| "memory" | "memory"
| "voice" | "voice"
@ -44,6 +56,10 @@ export function SettingsOverlay({
onMemoryConfigChange, onMemoryConfigChange,
onClose, onClose,
onVoiceStatusMaybeChanged, onVoiceStatusMaybeChanged,
activeBotId,
onOpenMcp,
tradingMode,
onDiscoverLiveTrading,
}: { }: {
email?: string | null; email?: string | null;
name: string; name: string;
@ -59,6 +75,10 @@ export function SettingsOverlay({
onMemoryConfigChange: (config: SpaceMemoryConfig | null) => void; onMemoryConfigChange: (config: SpaceMemoryConfig | null) => void;
onClose: () => void; onClose: () => void;
onVoiceStatusMaybeChanged?: () => void | Promise<void>; onVoiceStatusMaybeChanged?: () => void | Promise<void>;
activeBotId?: string;
onOpenMcp?: () => void;
tradingMode?: "live" | "paper";
onDiscoverLiveTrading?: () => void;
}) { }) {
const { t } = useLingui(); const { t } = useLingui();
const panelRef = useRef<HTMLDivElement>(null); const panelRef = useRef<HTMLDivElement>(null);
@ -81,6 +101,7 @@ export function SettingsOverlay({
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ id: "general", label: t`General`, icon: Settings }, { id: "general", label: t`General`, icon: Settings },
{ id: "integrations", label: t`Integrations`, icon: LayoutGrid },
{ id: "models", label: t`Models`, icon: Cpu }, { id: "models", label: t`Models`, icon: Cpu },
{ id: "memory", label: t`Memory`, icon: Brain }, { id: "memory", label: t`Memory`, icon: Brain },
{ id: "voice", label: t`Voice`, icon: Volume2 }, { id: "voice", label: t`Voice`, icon: Volume2 },
@ -100,7 +121,9 @@ export function SettingsOverlay({
? t`Close memory settings` ? t`Close memory settings`
: section === "voice" : section === "voice"
? t`Close voice settings` ? t`Close voice settings`
: t`Close user settings`; : section === "integrations"
? t`Close integrations settings`
: t`Close user settings`;
async function refreshVoiceStatus() { async function refreshVoiceStatus() {
await onVoiceStatusMaybeChanged?.(); await onVoiceStatusMaybeChanged?.();
@ -115,7 +138,7 @@ export function SettingsOverlay({
leaveSettings(onClose); leaveSettings(onClose);
} }
const widePane = section === "models" || section === "voice"; const widePane = section === "models" || section === "voice" || section === "integrations";
return ( return (
<Dialog <Dialog
@ -189,7 +212,10 @@ export function SettingsOverlay({
<div <div
className={`min-h-0 flex-1 ${ className={`min-h-0 flex-1 ${
section === "models" || section === "voice" || section === "memory" section === "models" ||
section === "voice" ||
section === "memory" ||
section === "integrations"
? "flex flex-col overflow-hidden" ? "flex flex-col overflow-hidden"
: "rk-scroll overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8" : "rk-scroll overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8"
}`} }`}
@ -205,6 +231,16 @@ export function SettingsOverlay({
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
} }
isDeploymentOwner={isDeploymentOwner} isDeploymentOwner={isDeploymentOwner}
tradingMode={tradingMode}
onDiscoverLiveTrading={onDiscoverLiveTrading}
/>
) : null}
{section === "integrations" ? (
<PluginsOverlay
embedded
activeBotId={activeBotId}
onClose={requestClose}
onOpenMcp={onOpenMcp ? () => leaveSettings(onOpenMcp) : undefined}
/> />
) : null} ) : null}
{section === "usage" ? ( {section === "usage" ? (

View File

@ -93,7 +93,6 @@ import {
Copy, Copy,
ExternalLink, ExternalLink,
Gauge, Gauge,
LayoutGrid,
Lock, Lock,
LogOut, LogOut,
Maximize2, Maximize2,
@ -116,6 +115,7 @@ import {
Square, Square,
TextQuote, TextQuote,
Trash2, Trash2,
Users,
Volume2, Volume2,
X, X,
} from "lucide-react"; } from "lucide-react";
@ -266,9 +266,6 @@ const SettingsOverlay = lazy(() =>
const PeerMessagesOverlay = lazy(() => const PeerMessagesOverlay = lazy(() =>
import("./PeerMessagesOverlay").then((module) => ({ default: module.PeerMessagesOverlay })), import("./PeerMessagesOverlay").then((module) => ({ default: module.PeerMessagesOverlay })),
); );
const PluginsOverlay = lazy(() =>
import("./PluginsOverlay").then((module) => ({ default: module.PluginsOverlay })),
);
const McpServersOverlay = lazy(() => const McpServersOverlay = lazy(() =>
import("./McpServersOverlay").then((module) => ({ default: module.McpServersOverlay })), import("./McpServersOverlay").then((module) => ({ default: module.McpServersOverlay })),
); );
@ -358,15 +355,14 @@ export function ShellPage() {
return "live"; return "live";
}); });
const toggleTradingMode = useCallback(() => { const discoverLiveTrading = useCallback(() => {
setTradingMode((prev) => { setTradingMode("live");
const next = prev === "live" ? "paper" : "live"; if (typeof window !== "undefined" && window.localStorage) {
if (typeof window !== "undefined" && window.localStorage) { window.localStorage.setItem("evoo_trading_mode", "live");
window.localStorage.setItem("evoo_trading_mode", next); }
} setActiveNavPillar("heute");
return next; navigate("/app");
}); }, [navigate]);
}, []);
useEffect(() => { useEffect(() => {
if (botId) setActiveNavPillar("team"); if (botId) setActiveNavPillar("team");
@ -539,7 +535,6 @@ export function ShellPage() {
function updateSnapshot(update: (prev: ThreadSnapshot | null) => ThreadSnapshot | null) { function updateSnapshot(update: (prev: ThreadSnapshot | null) => ThreadSnapshot | null) {
commitSnapshot(update(snapshotRef.current)); commitSnapshot(update(snapshotRef.current));
} }
const [pluginsOpen, setPluginsOpen] = useState(false);
const [mcpOpen, setMcpOpen] = useState(false); const [mcpOpen, setMcpOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsSection, setSettingsSection] = useState<SettingsSection>("general"); const [settingsSection, setSettingsSection] = useState<SettingsSection>("general");
@ -3049,205 +3044,47 @@ export function ShellPage() {
<div> <div>
<div className="rk-shell-section-label">{t`Projekte`}</div> <div className="rk-shell-section-label">{t`Projekte`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70"> <p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
{t`Group conversations · folders`} {t`Threads by topic · folders`}
</p> </p>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
type="button" type="button"
aria-label="Einstellungen" aria-label={t`Create a project`}
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors" onClick={() => setNewSpaceOpen(true)}
>
<Settings strokeWidth={2} className="size-icon-sm" />
</button>
<button
type="button"
aria-label="Neues Projekt"
onClick={() => setPanel("create-group")}
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors" className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
> >
<Plus strokeWidth={2.5} className="size-icon-md" /> <Plus strokeWidth={2.5} className="size-icon-md" />
</button> </button>
</div> </div>
</div> </div>
{sidebarGroups {spaces.filter((space) => !space.isDefault).length === 0 ? (
.map((group) => ({
...group,
bots: group.bots.filter(
(b) => b.kind === "group" && b.chat.id !== hauptthreadGroup?.id,
) as any[],
}))
.filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? (
<div className="p-4 text-center text-ui-label text-muted-foreground"> <div className="p-4 text-center text-ui-label text-muted-foreground">
<p>{t`No project conversations yet.`}</p> <p>{t`No projects yet.`}</p>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="mt-2 text-ui-label" className="mt-2 text-ui-label"
onClick={() => setPanel("create-group")} onClick={() => setNewSpaceOpen(true)}
> >
<Plus className="mr-1 size-icon-sm" /> <Plus className="mr-1 size-icon-sm" />
{t`Start a group conversation`} {t`Create a project`}
</Button> </Button>
</div> </div>
) : null} ) : null}
{sidebarGroups
.map((group) => ({
...group,
bots: group.bots.filter(
(b) => b.kind === "group" && b.chat.id !== hauptthreadGroup?.id,
) as any[],
}))
.filter((group) => group.bots.length > 0 || group.emptySpaceId)
.map((group) => {
const collapsed =
Boolean(group.title) && collapsedSidebarSections.has(group.key);
return (
<div key={`proj-${group.key}`} data-sidebar-group={group.key}>
{group.title ? (
<div className="flex items-center pt-3 pb-0.5">
<button
type="button"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
openSpaceChat(group.emptySpaceId, "/onboarding");
return;
}
toggleSidebarSection(group.key);
}}
onContextMenu={
group.sectionId
? (event) => {
event.preventDefault();
const sections =
group.spaceId === bootstrapMe?.spaceId
? botSections
: (spaces.find((space) => space.id === group.spaceId)
?.botSections ?? []);
const section = sections.find(
(item) => item.id === group.sectionId,
);
if (!section) return;
sectionMenuAnchor.current = event.currentTarget;
setSectionMenu({
section,
spaceId: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: undefined
}
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
>
<span className="flex min-w-0 items-center gap-1.5 truncate">
{group.showLock ? (
<Lock
strokeWidth={2}
aria-hidden="true"
className="size-icon-sm"
/>
) : null}
<span className="truncate">{group.title}</span>
</span>
{group.emptySpaceId ? null : (
<ChevronDown
strokeWidth={1.8}
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
aria-hidden="true"
/>
)}
</button>
</div>
) : null}
{!collapsed &&
group.bots.map((item) => (
<button
key={`${item.kind}:${item.chat.id}`}
type="button"
aria-label={t`Group conversation ${item.chat.name}`}
onClick={() => {
openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`);
}}
onContextMenu={(event) => {
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
event.preventDefault();
botMenuAnchor.current = event.currentTarget;
setBotMenu({
kind: item.kind,
id: item.chat.id,
position: { x: event.clientX, y: event.clientY },
});
}}
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
item.kind === "group" && inGroup && activeGroup?.id === item.chat.id
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.05] border border-transparent"
}`}
>
<GroupAvatar
members={
item.chat.id === activeSnapshot?.groupId
? (activeSnapshot?.members ?? item.chat.members)
: item.chat.members
}
size={40}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
item.chat.unread
? "font-bold text-white"
: "font-semibold text-white/95"
}`}
>
{item.chat.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
{formatRosterTime(item.chat.updatedAt)}
</span>
{item.chat.unread ? (
<span
aria-hidden="true"
className="inline-block h-2 w-2 rounded-full bg-emerald-400"
/>
) : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${
item.chat.unread ? "font-medium text-white" : "text-zinc-300"
}`}
>
{cleanRosterPreview(item.chat.preview) ||
item.chat.members.map((member: any) => member.name).join(", ")}
</div>
</div>
</button>
))}
</div>
);
})}
</div> </div>
<div className="sidebar-team"> <div className="sidebar-conversations">
{/* TEAM SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 px-2.5"> <div className="flex items-center justify-between pt-4 pb-1 px-2.5">
<div> <div>
<div className="rk-shell-section-label">{t`Team`}</div> <div className="rk-shell-section-label">{t`Conversations`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70"> <p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
{t`Your Oos — open a 1:1 conversation`} {t`One-to-one and group threads`}
</p> </p>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<button <button
type="button" type="button"
aria-label="Neues Teammitglied" aria-label={t`Add an Oo`}
onClick={() => setPanel("create")} onClick={() => setPanel("create")}
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors" className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
> >
@ -3255,14 +3092,34 @@ export function ShellPage() {
</button> </button>
</div> </div>
</div> </div>
<button
type="button"
data-testid="start-group-conversation"
onClick={() => {
setMobileSidebarOpen(false);
setPanel("create-group");
}}
className="mx-0 mb-2 flex w-full items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent"
>
<span className="grid h-[30px] w-[30px] place-items-center rounded-lg bg-accent text-foreground/80">
<Users strokeWidth={1.8} className="size-icon-md" />
</span>
<span className="text-ui-body font-medium text-foreground/90">
<Trans>Start a group conversation</Trans>
</span>
</button>
{sidebarGroups {sidebarGroups
.map((group) => ({ .map((group) => ({
...group, ...group,
bots: group.bots.filter((b) => b.kind === "bot") as any[], bots: group.bots.filter(
(b) =>
b.kind === "bot" ||
(b.kind === "group" && b.chat.id !== hauptthreadGroup?.id),
) as any[],
})) }))
.filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? ( .filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? (
<div className="p-4 text-center text-ui-label text-muted-foreground"> <div className="p-4 text-center text-ui-label text-muted-foreground">
<p>{t`No Oos in your team yet.`}</p> <p>{t`No conversations yet.`}</p>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -3277,7 +3134,11 @@ export function ShellPage() {
{sidebarGroups {sidebarGroups
.map((group) => ({ .map((group) => ({
...group, ...group,
bots: group.bots.filter((b) => b.kind === "bot") as any[], bots: group.bots.filter(
(b) =>
b.kind === "bot" ||
(b.kind === "group" && b.chat.id !== hauptthreadGroup?.id),
) as any[],
})) }))
.filter((group) => group.bots.length > 0 || group.emptySpaceId) .filter((group) => group.bots.length > 0 || group.emptySpaceId)
.map((group) => { .map((group) => {
@ -3666,35 +3527,16 @@ export function ShellPage() {
</div> </div>
<button <button
type="button" type="button"
onClick={toggleTradingMode} data-testid="settings-entry"
className="mx-3 lg:mx-5 2xl:mx-7 mb-1 flex items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent group" aria-label={t`Settings`}
> onClick={() => openSettings("general")}
<span
className={`grid h-[30px] w-[30px] place-items-center rounded-lg transition-colors ${
tradingMode === "live"
? "bg-amber-500/10 text-amber-500 shadow-[0_0_12px_rgba(245,158,11,0.2)]"
: "bg-indigo-500/10 text-indigo-400"
}`}
>
<Activity
strokeWidth={1.8}
className={`size-icon-md ${tradingMode === "live" ? "animate-pulse" : ""}`}
/>
</span>
<span className="text-ui-body font-medium text-foreground/90">
{tradingMode === "live" ? "Live Trading" : "Paper Sim"}
</span>
</button>
<button
type="button"
onClick={() => setPluginsOpen(true)}
className="mx-3 lg:mx-5 2xl:mx-7 mb-1 flex items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent" className="mx-3 lg:mx-5 2xl:mx-7 mb-1 flex items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent"
> >
<span className="grid h-[30px] w-[30px] place-items-center rounded-lg bg-accent text-foreground/80"> <span className="grid h-[30px] w-[30px] place-items-center rounded-lg bg-accent text-foreground/80">
<LayoutGrid strokeWidth={1.8} className="size-icon-md" /> <Settings strokeWidth={1.8} className="size-icon-md" />
</span> </span>
<span className="text-ui-body font-medium text-foreground/90"> <span className="text-ui-body font-medium text-foreground/90">
<Trans>Integrations</Trans> <Trans>Settings</Trans>
</span> </span>
</button> </button>
<Popover open={menuOpen} onOpenChange={setMenuOpen}> <Popover open={menuOpen} onOpenChange={setMenuOpen}>
@ -3713,18 +3555,6 @@ export function ShellPage() {
align="start" align="start"
className="w-[calc(316px-1.5rem)] max-w-[calc(100vw-3rem)] gap-0 p-1 data-closed:animate-none" className="w-[calc(316px-1.5rem)] max-w-[calc(100vw-3rem)] gap-0 p-1 data-closed:animate-none"
> >
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Settings`}
onClick={() => {
setMenuOpen(false);
openSettings("general");
}}
>
<Settings className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Settings</Trans>
</Button>
<Button <Button
variant="ghost" variant="ghost"
className="w-full justify-start font-normal" className="w-full justify-start font-normal"
@ -4841,16 +4671,6 @@ export function ShellPage() {
/> />
) : null} ) : null}
{pluginsOpen ? (
<PluginsOverlay
activeBotId={activeBotId.current}
onClose={() => setPluginsOpen(false)}
onOpenMcp={() => {
setPluginsOpen(false);
setMcpOpen(true);
}}
/>
) : null}
{mcpOpen ? <McpServersOverlay onClose={() => setMcpOpen(false)} /> : null} {mcpOpen ? <McpServersOverlay onClose={() => setMcpOpen(false)} /> : null}
{messagingSettingsOpen ? ( {messagingSettingsOpen ? (
<MessagingSettingsOverlay onClose={() => setMessagingSettingsOpen(false)} /> <MessagingSettingsOverlay onClose={() => setMessagingSettingsOpen(false)} />
@ -4891,6 +4711,13 @@ export function ShellPage() {
setVoiceStatus(null); setVoiceStatus(null);
} }
}} }}
activeBotId={activeBotId.current}
tradingMode={tradingMode}
onDiscoverLiveTrading={discoverLiveTrading}
onOpenMcp={() => {
setSettingsOpen(false);
setMcpOpen(true);
}}
onClose={() => { onClose={() => {
setSettingsOpen(false); setSettingsOpen(false);
setSettingsSection("general"); setSettingsSection("general");