fix(web,core,db): resolve responsive layout overflows, dialog clipping, i18n gaps, and runtime stability

This commit is contained in:
cynarAI 2026-10-19 18:57:04 +02:00
parent ac9fcce0fb
commit 66dbf0046e
38 changed files with 585 additions and 244 deletions

View File

@ -14,6 +14,13 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible(); await expect(unterhaltungen.getByText("Talk to your Oos", { exact: true })).toBeVisible();
await expect(projekte.getByText("Projects", { exact: true })).toBeVisible(); await expect(projekte.getByText("Projects", { exact: true })).toBeVisible();
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible(); await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible();
const [projekteBox, unterhaltungenBox] = await Promise.all([
projekte.boundingBox(),
unterhaltungen.boundingBox(),
]);
expect(projekteBox).not.toBeNull();
expect(unterhaltungenBox).not.toBeNull();
expect(projekteBox!.y).toBeLessThan(unterhaltungenBox!.y);
await unterhaltungen.getByTestId("conversation-picker-trigger").click(); await unterhaltungen.getByTestId("conversation-picker-trigger").click();
const picker = page.getByTestId("conversation-bot-picker"); const picker = page.getByTestId("conversation-bot-picker");

View File

@ -254,10 +254,10 @@ export function OoAtelierDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
className="max-h-[92vh] w-[420px] max-w-full gap-3 overflow-y-auto rounded-3xl p-5 sm:max-w-[420px]" className="flex max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[420px] flex-col gap-3 overflow-hidden rounded-3xl p-5 sm:max-w-[420px]"
data-testid="oo-atelier" data-testid="oo-atelier"
> >
<DialogHeader className="flex-row items-center justify-between space-y-0"> <DialogHeader className="shrink-0 flex-row items-center justify-between space-y-0">
<DialogTitle className="text-ui-body font-semibold tracking-tight"> <DialogTitle className="text-ui-body font-semibold tracking-tight">
<Trans>Atelier</Trans> <Trans>Atelier</Trans>
</DialogTitle> </DialogTitle>
@ -274,19 +274,19 @@ export function OoAtelierDialog({
</button> </button>
</DialogHeader> </DialogHeader>
<div className="flex justify-center py-1" data-testid="oo-atelier-preview"> <div className="flex shrink-0 justify-center py-1" data-testid="oo-atelier-preview">
<BotAvatar <BotAvatar
color={isPicture && !draft ? value : encoded} color={isPicture && !draft ? value : encoded}
identity={identity} identity={identity}
status={status} status={status}
size={128} size={120}
moment={preview} moment={preview}
interactive interactive
/> />
</div> </div>
<Tabs defaultValue="body"> <Tabs defaultValue="body" className="flex min-h-0 flex-1 flex-col">
<TabsList className="w-full"> <TabsList className="w-full shrink-0">
<TabsTrigger value="body"> <TabsTrigger value="body">
<Trans>Body</Trans> <Trans>Body</Trans>
</TabsTrigger> </TabsTrigger>
@ -303,7 +303,11 @@ export function OoAtelierDialog({
) : null} ) : null}
</TabsList> </TabsList>
<TabsContent value="body" className="space-y-4 pt-2" data-testid="oo-atelier-body"> <TabsContent
value="body"
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
data-testid="oo-atelier-body"
>
<OptionGroup <OptionGroup
title={t`Species`} title={t`Species`}
values={OO_SPECIES} values={OO_SPECIES}
@ -397,7 +401,11 @@ export function OoAtelierDialog({
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="eyes" className="space-y-4 pt-2" data-testid="oo-atelier-eyes"> <TabsContent
value="eyes"
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
data-testid="oo-atelier-eyes"
>
<OptionGroup <OptionGroup
title={t`Lashes`} title={t`Lashes`}
values={OO_LASHES} values={OO_LASHES}
@ -456,7 +464,11 @@ export function OoAtelierDialog({
/> />
</TabsContent> </TabsContent>
<TabsContent value="extras" className="space-y-4 pt-2" data-testid="oo-atelier-extras"> <TabsContent
value="extras"
className="rk-scroll min-h-0 flex-1 space-y-4 overflow-y-auto pt-2 pr-1"
data-testid="oo-atelier-extras"
>
<OptionGroup <OptionGroup
title={t`Glasses`} title={t`Glasses`}
values={OO_EYEWEAR} values={OO_EYEWEAR}
@ -492,7 +504,7 @@ export function OoAtelierDialog({
</TabsContent> </TabsContent>
{allowUpload ? ( {allowUpload ? (
<TabsContent value="upload" className="pt-2"> <TabsContent value="upload" className="min-h-0 flex-1 overflow-y-auto pt-2">
<button <button
type="button" type="button"
aria-label={t`Image upload area`} aria-label={t`Image upload area`}
@ -534,7 +546,7 @@ export function OoAtelierDialog({
) : null} ) : null}
</Tabs> </Tabs>
<DialogFooter className="flex-row items-center justify-between border-border sm:justify-between"> <DialogFooter className="-mx-5 -mb-5 shrink-0 flex-row items-center justify-between border-border px-5 py-3.5 sm:justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"

View File

@ -15,14 +15,14 @@ type OnboardingDiscoveryShellProps = {
compact?: boolean; compact?: boolean;
}; };
function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]): string { function sourceLabel(id: DiscoverySourceId): ReactNode {
switch (id) { switch (id) {
case "profile-basics": case "profile-basics":
return t`Your profile`; return <Trans>Your profile</Trans>;
case "connected-tools": case "connected-tools":
return t`Connected tools`; return <Trans>Connected tools</Trans>;
case "specialist-crew": case "specialist-crew":
return t`Specialist crew`; return <Trans>Specialist crew</Trans>;
default: { default: {
const never: never = id; const never: never = id;
return never; return never;
@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({
> >
{active ? ( {active ? (
<> <>
{sourceLabel(active.id, t)} {sourceLabel(active.id)}
{active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? ( {active.status === "found" && active.detail === DISCOVERY_MAIN_BOT_ROLE ? (
<> <>
{" · "} {" · "}

View File

@ -120,7 +120,7 @@ export function OnboardingOoDesigner({ config, onChange, onContinue }: Onboardin
<Trans>What feels like you?</Trans> <Trans>What feels like you?</Trans>
</p> </p>
<div <div
className="mt-3 flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" className="mt-3 flex flex-wrap gap-2 pb-1"
role="tablist" role="tablist"
aria-label={t`Look families`} aria-label={t`Look families`}
> >

View File

@ -429,7 +429,7 @@ msgstr "Über Gruppen"
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "About spaces" msgid "About spaces"
msgstr "Über Spaces" msgstr "Über Projekte"
#: src/components/UserProfileSettings.tsx #: src/components/UserProfileSettings.tsx
msgid "About you" msgid "About you"
@ -1001,7 +1001,7 @@ msgstr "Körper"
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Live-Desktop wird gestartet…" msgstr "Live-Desktop wird gestartet…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Computer von {0} wird gestartet" msgstr "Computer von {0} wird gestartet"
@ -1494,6 +1494,10 @@ msgstr "Computer"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen." msgstr "Computer sind aus. Setze SANDBOX_PROVIDER=docker mit SANDBOX_SUPERVISOR_TOKEN, oder setze SANDBOX_PROVIDER auf e2b, daytona oder box mit dem passenden API-Key. Stack nach der Änderung von .env neu erstellen."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr "Richte auf dem Server einen Plugin-Katalog ein, um Apps zu verbinden."
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Durch die Bereitstellung konfiguriert" msgstr "Durch die Bereitstellung konfiguriert"
@ -1585,6 +1589,10 @@ msgstr "Verbunden, {0} wird verwendet."
msgid "Connected app" msgid "Connected app"
msgstr "Verbundene App" msgstr "Verbundene App"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Verbundene Tools"
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar." msgstr "Verbunden. Die Tools sind ab deiner nächsten Nachricht verfügbar."
@ -2078,7 +2086,7 @@ msgstr "Neue Gruppe erstellen"
#: src/pages/shell/bot-picker.tsx #: src/pages/shell/bot-picker.tsx
msgid "Create new Space" msgid "Create new Space"
msgstr "Neuen Space erstellen" msgstr "Neues Projekt erstellen"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Create project" msgid "Create project"
@ -2087,7 +2095,7 @@ msgstr "Projekt erstellen"
#: src/components/AskCard.tsx #: src/components/AskCard.tsx
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Create space" msgid "Create space"
msgstr "Space erstellen" msgstr "Projekt erstellen"
#: src/components/integrations/GithubConnect.tsx #: src/components/integrations/GithubConnect.tsx
msgid "Create token on GitHub" msgid "Create token on GitHub"
@ -2242,7 +2250,7 @@ msgstr "Erinnerungen ebenfalls löschen"
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Delete space" msgid "Delete space"
msgstr "Space löschen" msgstr "Projekt löschen"
#: src/pages/PrivacySettingsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx
msgid "Delete your account?" msgid "Delete your account?"
@ -3777,7 +3785,7 @@ msgstr "Neuer Skill"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "New space" msgid "New space"
msgstr "Neuer Space" msgstr "Neues Projekt"
#: src/components/CookingMode.tsx #: src/components/CookingMode.tsx
msgid "Next" msgid "Next"
@ -3891,6 +3899,10 @@ msgstr "Noch keine Projekte."
msgid "No providers found." msgid "No providers found."
msgstr "Keine Anbieter gefunden." msgstr "Keine Anbieter gefunden."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "Keine letzte Aktivität"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "Keine Remote-MCP-Server gefunden" msgstr "Keine Remote-MCP-Server gefunden"
@ -5149,7 +5161,7 @@ msgstr "Space-Standard"
#: src/pages/shell/dialogs.tsx #: src/pages/shell/dialogs.tsx
msgid "Spaces" msgid "Spaces"
msgstr "Spaces" msgstr "Projekte"
#: src/components/OoAtelier.tsx #: src/components/OoAtelier.tsx
msgid "Sparkle" msgid "Sparkle"
@ -5185,6 +5197,7 @@ msgstr "Vorlesen an oder aus"
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "Spreche mit Bedacht…" msgstr "Spreche mit Bedacht…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "Spezialisten-Team" msgstr "Spezialisten-Team"
@ -5526,10 +5539,6 @@ msgstr "Diese Abonnementanmeldung ist in Rakazo noch nicht verfügbar. Verwende
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht." msgstr "Dadurch werden dieses Projekt und alle zugehörigen Unterhaltungen, Bots und Erinnerungen dauerhaft gelöscht."
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr "Unterhaltungen nach Thema · Ordner"
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr "Bis wann?"
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Aktuell" msgstr "Aktuell"
@ -6100,6 +6110,10 @@ msgstr "Wenn das an ist, spricht dein Oo ganz natürlich mit dir."
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern." msgstr "Dein Oo kommuniziert in dieser Sprache. Du kannst sie später ändern."
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr "Dein Profil"
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden." msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden."

View File

@ -1001,7 +1001,7 @@ msgstr "Body"
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Booting live desktop…" msgstr "Booting live desktop…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Booting up {0}’s computer" msgstr "Booting up {0}’s computer"
@ -1494,6 +1494,10 @@ msgstr "Computers"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgstr "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr "Configure a plugin catalog on the server to connect apps."
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configured by deployment" msgstr "Configured by deployment"
@ -1585,6 +1589,10 @@ msgstr "Connected and using {0}."
msgid "Connected app" msgid "Connected app"
msgstr "Connected app" msgstr "Connected app"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Connected tools"
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Connected. Its tools are available from your next message." msgstr "Connected. Its tools are available from your next message."
@ -3891,6 +3899,10 @@ msgstr "No projects yet."
msgid "No providers found." msgid "No providers found."
msgstr "No providers found." msgstr "No providers found."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "No recent activity"
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "No remote MCP servers found" msgstr "No remote MCP servers found"
@ -5185,6 +5197,7 @@ msgstr "Speaking on or off"
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "Speaking with intent…" msgstr "Speaking with intent…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "Specialist crew" msgstr "Specialist crew"
@ -5526,10 +5539,6 @@ msgstr "This subscription sign-in is not available in Rakazo yet. Use a deployme
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "This will permanently delete this project and all its conversations, bots, and memory." msgstr "This will permanently delete this project and all its conversations, bots, and memory."
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr "Threads by topic · folders"
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr "Until when?"
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Up to date" msgstr "Up to date"
@ -6100,6 +6110,10 @@ msgstr "Your Oo talks with you naturally when this is on."
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "Your Oo will communicate in this language. You can change it later." msgstr "Your Oo will communicate in this language. You can change it later."
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr "Your profile"
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "Your specialists. One calm place to talk." msgstr "Your specialists. One calm place to talk."

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Iniciando escritorio en vivo…" msgstr "Iniciando escritorio en vivo…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Iniciando la computadora de {0}" msgstr "Iniciando la computadora de {0}"
@ -1494,6 +1494,10 @@ msgstr "Computadoras"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env." msgstr "Las computadoras están apagadas. Configura SANDBOX_PROVIDER=docker con SANDBOX_SUPERVISOR_TOKEN, o establece SANDBOX_PROVIDER en e2b, daytona o box con su clave de API. Recrea el stack después de cambiar .env."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configurado por el despliegue" msgstr "Configurado por el despliegue"
@ -1585,6 +1589,10 @@ msgstr "Conectado y usando {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje." msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "No se encontraron proveedores." msgstr "No se encontraron proveedores."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "Este inicio de sesión por suscripción aún no está disponible en Raka
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Actualizado" msgstr "Actualizado"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "लाइव डेस्कटॉप शुरू हो रहा है…" msgstr "लाइव डेस्कटॉप शुरू हो रहा है…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0} का कंप्यूटर शुरू हो रहा है" msgstr "{0} का कंप्यूटर शुरू हो रहा है"
@ -1494,6 +1494,10 @@ msgstr "कंप्यूटर"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।" msgstr "कंप्यूटर बंद हैं। SANDBOX_PROVIDER=docker और SANDBOX_SUPERVISOR_TOKEN सेट करें, या SANDBOX_PROVIDER को e2b, daytona, या box पर सेट कर उसका API key दें। .env बदलने के बाद स्टैक फिर बनाएँ।"
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर" msgstr "डिप्लॉयमेंट द्वारा कॉन्फ़िगर"
@ -1585,6 +1589,10 @@ msgstr "{0} कनेक्ट किया गया और उपयोग म
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।" msgstr "कनेक्टेड। इसके टूल आपके अगले संदेश से उपलब्ध हैं।"
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "कोई प्रोवाइडर नहीं मिला।" msgstr "कोई प्रोवाइडर नहीं मिला।"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "यह सब्सक्रिप्शन साइन-इन अभ
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "라이브 데스크톱 시작 중…" msgstr "라이브 데스크톱 시작 중…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0}의 컴퓨터를 켜는 중" msgstr "{0}의 컴퓨터를 켜는 중"
@ -1494,6 +1494,10 @@ msgstr "컴퓨터"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요." msgstr "컴퓨터가 꺼져 있습니다. SANDBOX_PROVIDER=docker와 SANDBOX_SUPERVISOR_TOKEN을 설정하거나, SANDBOX_PROVIDER를 e2b, daytona, box 중 하나로 두고 해당 API 키를 넣으세요. .env 변경 후 스택을 다시 만드세요."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "서버에서 설정됨" msgstr "서버에서 설정됨"
@ -1585,6 +1589,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다." msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "검색된 AI 서비스가 없습니다." msgstr "검색된 AI 서비스가 없습니다."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "최신 상태" msgstr "최신 상태"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Iniciando a área de trabalho ao vivo..." msgstr "Iniciando a área de trabalho ao vivo..."
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Inicializando o computador do {0}" msgstr "Inicializando o computador do {0}"
@ -1494,6 +1494,10 @@ msgstr "Computadores"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env." msgstr "Computadores estão desligados. Defina SANDBOX_PROVIDER=docker com SANDBOX_SUPERVISOR_TOKEN, ou SANDBOX_PROVIDER como e2b, daytona ou box com a chave de API. Recrie o stack após alterar o .env."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Configurado por implantação" msgstr "Configurado por implantação"
@ -1585,6 +1589,10 @@ msgstr "Conectado e usando {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem." msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Nenhum provedor encontrado" msgstr "Nenhum provedor encontrado"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "Este login de assinatura ainda não está disponível no Rakazo. Use uma
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Загрузка рабочего стола…" msgstr "Загрузка рабочего стола…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "Загрузка компьютера {0}" msgstr "Загрузка компьютера {0}"
@ -1494,6 +1494,10 @@ msgstr "Компьютеры"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env." msgstr "Компьютеры выключены. Установите SANDBOX_PROVIDER=docker с помощью SANDBOX_SUPERVISOR_TOKEN или установите для SANDBOX_PROVIDER значение e2b, Daytona или Box с его ключом API. Воссоздайте стек после изменения .env."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Настраивается путем развертывания" msgstr "Настраивается путем развертывания"
@ -1585,6 +1589,10 @@ msgstr "Подключено и используется {0}."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения." msgstr "Подключено. Его инструменты доступны из вашего следующего сообщения."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Поставщики не найдены." msgstr "Поставщики не найдены."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "Удаленные MCP-серверы не найдены" msgstr "Удаленные MCP-серверы не найдены"
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "Вход по этой подписке пока недоступен в
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "Актуально" msgstr "Актуально"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "Canlı masaüstü başlatılıyor…" msgstr "Canlı masaüstü başlatılıyor…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "{0} adlı botun bilgisayarı başlatılıyor" msgstr "{0} adlı botun bilgisayarı başlatılıyor"
@ -1494,6 +1494,10 @@ msgstr "Bilgisayarlar"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun." msgstr "Bilgisayarlar kapalı. SANDBOX_PROVIDER=docker ve SANDBOX_SUPERVISOR_TOKEN ayarlayın; ya da SANDBOX_PROVIDER değerini e2b, daytona veya box yapıp API anahtarını ekleyin. .env değişince stacki yeniden oluşturun."
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "Dağıtım tarafından yapılandırıldı" msgstr "Dağıtım tarafından yapılandırıldı"
@ -1585,6 +1589,10 @@ msgstr "Bağlandı, {0} kullanılıyor."
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir." msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "Sağlayıcı bulunamadı." msgstr "Sağlayıcı bulunamadı."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "Bu abonelik girişi Rakazo'da henüz kullanılamıyor. Bir dağıtım ki
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…" msgid "Booting live desktop…"
msgstr "正在启动实时桌面…" msgstr "正在启动实时桌面…"
#. placeholder {0}: active?.name #. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx #: src/pages/Shell.tsx
msgid "Booting up {0}’s computer" msgid "Booting up {0}’s computer"
msgstr "正在启动 {0} 的电脑" msgstr "正在启动 {0} 的电脑"
@ -1494,6 +1494,10 @@ msgstr "电脑"
msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env." msgid "Computers are off. Set SANDBOX_PROVIDER=docker with SANDBOX_SUPERVISOR_TOKEN, or set SANDBOX_PROVIDER to e2b, daytona, or box with its API key. Recreate the stack after changing .env."
msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。" msgstr "电脑未开启。请设置 SANDBOX_PROVIDER=docker 并提供 SANDBOX_SUPERVISOR_TOKEN,或将 SANDBOX_PROVIDER 设为 e2b、daytona 或 box 并提供对应 API 密钥。更改 .env 后请重建环境。"
#: src/pages/PluginsOverlay.tsx
msgid "Configure a plugin catalog on the server to connect apps."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx #: src/pages/ModelSettingsOverlay.tsx
msgid "Configured by deployment" msgid "Configured by deployment"
msgstr "由部署配置" msgstr "由部署配置"
@ -1585,6 +1589,10 @@ msgstr "已连接并使用 {0}。"
msgid "Connected app" msgid "Connected app"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx #: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message." msgid "Connected. Its tools are available from your next message."
msgstr "已连接,下一条消息起即可使用其工具。" msgstr "已连接,下一条消息起即可使用其工具。"
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found." msgid "No providers found."
msgstr "未找到提供方。" msgstr "未找到提供方。"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx #: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found" msgid "No remote MCP servers found"
msgstr "" msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…" msgid "Speaking with intent…"
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts #: src/lib/onboarding-copy.ts
msgid "Specialist crew" msgid "Specialist crew"
msgstr "" msgstr ""
@ -5526,10 +5539,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选
msgid "This will permanently delete this project and all its conversations, bots, and memory." msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr "" msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx #: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx #: src/pages/shell/bot-panel.tsx
msgid "Tiers" msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx #: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx #: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date" msgid "Up to date"
msgstr "已是最新" msgstr "已是最新"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later." msgid "Your Oo will communicate in this language. You can change it later."
msgstr "" msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx #: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk." msgid "Your specialists. One calm place to talk."
msgstr "" msgstr ""

View File

@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
<div className="space-y-5"> <div className="space-y-5">
<DesktopUpdateSection /> <DesktopUpdateSection />
<SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} /> <SoftwareUpdateSection isDeploymentOwner={isDeploymentOwner} />
<section className="hidden rounded-xl border border-border px-4 py-4 only:block">
<p className="text-ui-body-sm text-muted-foreground">
<Trans>Up to date</Trans>
</p>
</section>
</div> </div>
); );
} }

View File

@ -30,7 +30,13 @@ export function ActivityList({ onOpenRun }: ActivityListProps) {
); );
} }
if (activeRuns.length === 0 && recentRuns.length === 0) return null; if (activeRuns.length === 0 && recentRuns.length === 0) {
return (
<div className="mb-2 border-b border-border px-2.5 py-2 text-ui-body-sm text-muted-foreground/80">
<Trans>No recent activity</Trans>
</div>
);
}
return ( return (
<div className="mb-2 border-b border-border pb-2"> <div className="mb-2 border-b border-border pb-2">

View File

@ -10,7 +10,6 @@ import {
abortableDelay, abortableDelay,
buildFeaturedConnectorTiles, buildFeaturedConnectorTiles,
CONNECTION_CATALOG_PAGE_SIZE, CONNECTION_CATALOG_PAGE_SIZE,
EMPTY_PLUGIN_CATALOG_MESSAGE,
filterConnectionCatalogItems, filterConnectionCatalogItems,
humanizeToolName, humanizeToolName,
isGithubInstall, isGithubInstall,
@ -730,7 +729,7 @@ export function PluginsOverlay({
<div className="mb-6" data-testid="featured-connectors"> <div className="mb-6" data-testid="featured-connectors">
{!loading && catalog.length === 0 ? ( {!loading && catalog.length === 0 ? (
<p className="text-ui-body leading-6 text-muted-foreground/80"> <p className="text-ui-body leading-6 text-muted-foreground/80">
{EMPTY_PLUGIN_CATALOG_MESSAGE} <Trans>Configure a plugin catalog on the server to connect apps.</Trans>
</p> </p>
) : ( ) : (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">

View File

@ -176,7 +176,7 @@ export function SettingsOverlay({
<nav <nav
data-testid="settings-nav" data-testid="settings-nav"
aria-label={t`Settings`} aria-label={t`Settings`}
className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[200px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4" className="flex shrink-0 flex-row gap-1 overflow-x-auto border-b border-border px-3 py-3 md:w-[220px] md:flex-col md:overflow-y-auto md:border-b-0 md:border-e md:px-3 md:py-4"
> >
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;

View File

@ -2937,15 +2937,20 @@ export function ShellPage() {
overlay: boolean; overlay: boolean;
force?: boolean; force?: boolean;
}) { }) {
if (!active) return; const targetBot = active ?? coordinatorBot;
if (!targetBot) return;
const needsBoot = force || computer?.state !== "running" || !screenUrl; const needsBoot = force || computer?.state !== "running" || !screenUrl;
if (overlay && needsBoot) setBooting(true); if (overlay && needsBoot) setBooting(true);
setComputerError(null); setComputerError(null);
setComputerErrorFromScreen(false); setComputerErrorFromScreen(false);
try { try {
if (needsBoot) await rpc.computer.boot({ botId: active.id }); if (needsBoot) await rpc.computer.boot({ botId: targetBot.id });
if (takeControl) await rpc.computer.takeover({ botId: active.id }); if (takeControl) await rpc.computer.takeover({ botId: targetBot.id });
await refreshThread(active.id); if (active) {
await refreshThread(targetBot.id);
} else {
await refreshComputerScreen(targetBot.id);
}
} catch (error) { } catch (error) {
setComputerError(error instanceof Error ? error.message : t`Could not take control`); setComputerError(error instanceof Error ? error.message : t`Could not take control`);
setComputerErrorFromScreen(false); setComputerErrorFromScreen(false);
@ -3080,8 +3085,9 @@ export function ShellPage() {
}, [panel, computerOpen, active?.id, computer?.state]); }, [panel, computerOpen, active?.id, computer?.state]);
async function openComputer() { async function openComputer() {
if (!active) return; const targetBot = active ?? coordinatorBot;
const needsTakeover = !userHoldsComputerControl(computer, active.id); if (!targetBot) return;
const needsTakeover = !userHoldsComputerControl(computer, targetBot.id);
const blocked = computerTakeoverBlocked(computer, snapshot?.run?.status); const blocked = computerTakeoverBlocked(computer, snapshot?.run?.status);
try { try {
await bootComputer({ await bootComputer({
@ -3290,12 +3296,7 @@ export function ShellPage() {
<div className="sidebar-projekte"> <div className="sidebar-projekte">
{/* PROJEKTE SECTION */} {/* PROJEKTE 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 className="rk-shell-section-label">{t`Projects`}</div>
<div className="rk-shell-section-label">{t`Projects`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption rk-sidebar-muted">
{t`Threads by topic · folders`}
</p>
</div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
type="button" type="button"
@ -3309,18 +3310,9 @@ export function ShellPage() {
</div> </div>
</div> </div>
{projectSidebarGroups.length === 0 ? ( {projectSidebarGroups.length === 0 ? (
<div className="p-4 text-center text-ui-label rk-sidebar-muted"> <p className="px-2.5 py-1.5 text-ui-label rk-sidebar-muted">
<p>{t`No projects yet.`}</p> {t`No projects yet.`}
<Button </p>
variant="outline"
size="sm"
className="mt-2 text-ui-label"
onClick={() => openNewSpaceDialog("project")}
>
<Plus className="mr-1 size-icon-sm" />
{t`Create a project`}
</Button>
</div>
) : null} ) : null}
<DndContext <DndContext
sensors={sensors} sensors={sensors}
@ -3481,6 +3473,7 @@ export function ShellPage() {
{conversationPickerOpen ? ( {conversationPickerOpen ? (
<PopoverContent <PopoverContent
align="end" align="end"
collisionPadding={12}
className="w-auto gap-0 overflow-hidden p-0 data-closed:animate-none" className="w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
> >
<ConversationBotPicker <ConversationBotPicker
@ -3677,8 +3670,8 @@ export function ShellPage() {
: "100%", : "100%",
display: dualSurface.layoutMode === "canvas" ? "none" : "flex", display: dualSurface.layoutMode === "canvas" ? "none" : "flex",
flexDirection: "column" as const, flexDirection: "column" as const,
minWidth: dualSurface.layoutMode === "canvas" ? 0 : 360, minWidth: 0,
flexShrink: 0, flexShrink: 1,
transition: "width 200ms ease", transition: "width 200ms ease",
} }
: undefined; : undefined;
@ -3693,8 +3686,8 @@ export function ShellPage() {
: "0%", : "0%",
opacity: showCanvasOnDesktop ? 1 : 0, opacity: showCanvasOnDesktop ? 1 : 0,
pointerEvents: showCanvasOnDesktop ? ("auto" as const) : ("none" as const), pointerEvents: showCanvasOnDesktop ? ("auto" as const) : ("none" as const),
minWidth: showCanvasOnDesktop ? 360 : 0, minWidth: 0,
flexShrink: 0, flexShrink: 1,
transition: "width 200ms ease, opacity 150ms ease", transition: "width 200ms ease, opacity 150ms ease",
overflow: showCanvasOnDesktop ? ("visible" as const) : ("hidden" as const), overflow: showCanvasOnDesktop ? ("visible" as const) : ("hidden" as const),
} }
@ -3706,9 +3699,7 @@ export function ShellPage() {
panel === "create" || panel === "create" ||
panel === "create-group" || panel === "create-group" ||
panel === "browser") panel === "browser")
? workspacePaneOpen ? "100%"
? workspacePaneWidth
: rightSidebarWidth
: 0, : 0,
}; };
@ -4243,9 +4234,9 @@ export function ShellPage() {
data-panel={panel ?? "closed"} data-panel={panel ?? "closed"}
data-auto={panel !== null && panel === autoPanel ? "" : undefined} data-auto={panel !== null && panel === autoPanel ? "" : undefined}
style={canvasStyle} style={canvasStyle}
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:relative md:border-0 md:rounded-none md:shadow-none ${ className={`absolute inset-0 z-30 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:relative md:inset-auto md:z-20 md:border-0 md:rounded-none md:shadow-none ${
isPanelVisible isPanelVisible
? "max-w-[384px] md:max-w-none opacity-100" ? "w-full max-w-none md:max-w-none opacity-100"
: "pointer-events-none w-0 opacity-0 md:opacity-0 md:w-0" : "pointer-events-none w-0 opacity-0 md:opacity-0 md:w-0"
} ${!(workspacePaneOpen ? isResizingWorkspacePane : isResizingRight) ? "transition-[width,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none" : ""}`} } ${!(workspacePaneOpen ? isResizingWorkspacePane : isResizingRight) ? "transition-[width,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none" : ""}`}
> >
@ -4399,6 +4390,7 @@ export function ShellPage() {
) : panel === "browser" || (effectiveTab === "browser" && !isOverlayPanel) ? ( ) : panel === "browser" || (effectiveTab === "browser" && !isOverlayPanel) ? (
<div className="flex-1 min-h-0 flex flex-col"> <div className="flex-1 min-h-0 flex flex-col">
<InAppBrowser <InAppBrowser
hideCloseButton
onClose={() => { onClose={() => {
setPanel(null); setPanel(null);
if (!inProject) dualSurface.setLayoutMode("stream"); if (!inProject) dualSurface.setLayoutMode("stream");
@ -5292,15 +5284,15 @@ export function ShellPage() {
{booting ? ( {booting ? (
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center gap-[22px] bg-background/95"> <div className="absolute inset-0 z-30 flex flex-col items-center justify-center gap-[22px] bg-background/95">
<div className="text-[19px] font-medium text-foreground"> <div className="text-[19px] font-medium text-foreground">
<Trans>Booting up {active?.name}’s computer</Trans> <Trans>Booting up {(active ?? coordinatorBot)?.name}’s computer</Trans>
</div> </div>
<div className="h-[5px] w-[min(420px,70%)] overflow-hidden rounded-full bg-accent"> <div className="h-[5px] w-[min(420px,70%)] overflow-hidden rounded-full bg-accent">
<div className="h-full w-2/3 rounded-full bg-primary" /> <div className="h-full w-2/3 rounded-full bg-primary" />
</div> </div>
</div> </div>
) : computerOpen && active ? ( ) : computerOpen && (active ?? coordinatorBot) ? (
<FullscreenComputerOverlay <FullscreenComputerOverlay
active={active} active={(active ?? coordinatorBot)!}
computer={computer} computer={computer}
computerViewport={computerViewport} computerViewport={computerViewport}
embeddedScreenUrl={embeddedScreenUrl} embeddedScreenUrl={embeddedScreenUrl}

View File

@ -46,14 +46,14 @@ export function ComputerPanelPreview({
<> <>
<div <div
data-testid="computer-preview" data-testid="computer-preview"
className="group relative aspect-[16/10] overflow-hidden rounded-[14px] bg-background" className="group relative aspect-[16/10] overflow-hidden rounded-[14px] border border-border bg-muted/35"
> >
{computerOpen ? ( {computerOpen ? (
<div className="grid h-full place-items-center text-sm text-muted-foreground/80"> <div className="grid h-full place-items-center text-sm text-muted-foreground/80">
<Trans>Open in full window</Trans> <Trans>Open in full window</Trans>
</div> </div>
) : computer?.kind === "desktop" ? ( ) : computer?.kind === "desktop" ? (
<DesktopKindEmptyState className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80" /> <DesktopKindEmptyState className="grid h-full place-items-center px-6 pb-10 text-center text-sm text-muted-foreground/80" />
) : computer?.state === "running" && embeddedScreenUrl && !computerScreenError ? ( ) : computer?.state === "running" && embeddedScreenUrl && !computerScreenError ? (
<> <>
<iframe <iframe
@ -67,7 +67,11 @@ export function ComputerPanelPreview({
<AgentCursor botId={active.id} color={active.color} name={active.name} /> <AgentCursor botId={active.id} color={active.color} name={active.name} />
</> </>
) : ( ) : (
<div className="grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80"> <div
className={`grid h-full place-items-center px-6 text-center text-sm text-muted-foreground/80 ${
resting && !computerScreenError ? "pb-11" : ""
}`}
>
{computerScreenError ?? {computerScreenError ??
(computersAreUnavailable(sandboxProvider) ? ( (computersAreUnavailable(sandboxProvider) ? (
<ComputersUnavailableHint /> <ComputersUnavailableHint />
@ -85,7 +89,7 @@ export function ComputerPanelPreview({
type="button" type="button"
data-testid="computer-preview-open" data-testid="computer-preview-open"
data-resting={resting ? "" : undefined} data-resting={resting ? "" : undefined}
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-4 data-resting:opacity-100" className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-3 data-resting:opacity-100"
aria-label={t`Open`} aria-label={t`Open`}
onClick={onOpen} onClick={onOpen}
> >

View File

@ -108,14 +108,20 @@ export function NewSpaceDialog({
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}> <Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
className={isProject ? "max-h-[90vh] overflow-y-auto max-w-xl" : undefined} className={
isProject
? "flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
: undefined
}
> >
<DialogHeader> <DialogHeader
<DialogTitle className="flex items-center gap-2.5"> className={isProject ? "shrink-0 border-b border-border px-5 py-4" : undefined}
>
<DialogTitle className="flex min-w-0 items-center gap-2.5">
{isProject ? ( {isProject ? (
<> <>
<ProjectIcon icon={icon} color={color} size={30} /> <ProjectIcon icon={icon} color={color} size={30} />
<span> <span className="truncate">
<Trans>New project</Trans> <Trans>New project</Trans>
</span> </span>
</> </>
@ -133,7 +139,13 @@ export function NewSpaceDialog({
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 pt-1"> <div
className={
isProject
? "rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4"
: "min-w-0 space-y-4 pt-1"
}
>
<label htmlFor={nameId} className="block text-ui-body text-foreground/75"> <label htmlFor={nameId} className="block text-ui-body text-foreground/75">
<Trans>Name</Trans> <Trans>Name</Trans>
<Input <Input
@ -179,7 +191,7 @@ export function NewSpaceDialog({
<div <div
role="radiogroup" role="radiogroup"
aria-label={t`Icon`} aria-label={t`Icon`}
className="grid max-h-36 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1" className="grid max-h-36 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
> >
{PROJECT_ICONS.map((option) => { {PROJECT_ICONS.map((option) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id]; const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -304,8 +316,12 @@ export function NewSpaceDialog({
) : null} ) : null}
</div> </div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null} {error ? (
<DialogFooter className="mt-4"> <p className={`text-ui-body text-destructive ${isProject ? "px-5 pt-2" : "mt-2"}`}>
{error}
</p>
) : null}
<DialogFooter className={isProject ? "mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5" : "mt-4"}>
<Button variant="outline" disabled={saving} onClick={onCancel}> <Button variant="outline" disabled={saving} onClick={onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>

View File

@ -84,11 +84,11 @@ export function FullscreenComputerOverlay({
)} )}
{!recordingSkill && hasControl ? ( {!recordingSkill && hasControl ? (
computer?.takeoverRequested ? ( computer?.takeoverRequested ? (
<span className="rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning"> <span className="shrink-0 whitespace-nowrap rounded-full bg-warning/15 px-[11px] py-1 text-ui-body-sm text-warning">
<Trans>Needs you</Trans> <Trans>Needs you</Trans>
</span> </span>
) : ( ) : (
<span className="rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success"> <span className="shrink-0 whitespace-nowrap rounded-full bg-success/15 px-[11px] py-1 text-ui-body-sm text-success">
<Trans>You have control</Trans> <Trans>You have control</Trans>
</span> </span>
) )

View File

@ -95,16 +95,16 @@ export function ProjectLookDialog({
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
data-testid="project-look-dialog" data-testid="project-look-dialog"
className="max-h-[90vh] overflow-y-auto max-w-xl" className="flex max-h-[88dvh] w-[calc(100vw-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
> >
<DialogHeader> <DialogHeader className="shrink-0 border-b border-border px-5 py-4">
<DialogTitle className="flex items-center gap-3"> <DialogTitle className="flex min-w-0 items-center gap-3">
<ProjectIcon icon={icon} color={color} size={36} /> <ProjectIcon icon={icon} color={color} size={36} />
<span className="min-w-0 truncate">{name.trim() || initial.name}</span> <span className="min-w-0 truncate">{name.trim() || initial.name}</span>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 pt-1"> <div className="rk-scroll min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
<label htmlFor={nameId} className="block text-ui-body text-foreground/75"> <label htmlFor={nameId} className="block text-ui-body text-foreground/75">
<Trans>Name</Trans> <Trans>Name</Trans>
<Input <Input
@ -147,7 +147,7 @@ export function ProjectLookDialog({
<div <div
role="radiogroup" role="radiogroup"
aria-label={t`Icon`} aria-label={t`Icon`}
className="grid max-h-40 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-border p-1" className="grid max-h-40 grid-cols-6 gap-1 overflow-y-auto rounded-lg border border-border p-1.5 sm:grid-cols-8"
> >
{PROJECT_ICONS.map((option) => { {PROJECT_ICONS.map((option) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id]; const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -243,8 +243,8 @@ export function ProjectLookDialog({
</label> </label>
</div> </div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null} {error ? <p className="px-5 pt-2 text-ui-body text-destructive">{error}</p> : null}
<DialogFooter className="mt-4"> <DialogFooter className="mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5">
<Button variant="outline" disabled={saving} onClick={onCancel}> <Button variant="outline" disabled={saving} onClick={onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>

View File

@ -77,6 +77,7 @@ export function SidebarHeader({
{createMenuOpen ? ( {createMenuOpen ? (
<PopoverContent <PopoverContent
align="end" align="end"
collisionPadding={12}
className="app-no-drag w-auto gap-0 overflow-hidden p-0 data-closed:animate-none" className="app-no-drag w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
> >
<BotCreatePicker <BotCreatePicker
@ -132,7 +133,7 @@ export function SidebarHeader({
} }
onCollapseSidebar(); onCollapseSidebar();
}} }}
className="app-no-drag rk-touch-target flex size-control-sm items-center justify-center rounded-full rk-sidebar-muted hover:text-foreground/75" className="app-no-drag rk-touch-target flex size-control-sm items-center justify-center rounded-full rk-sidebar-muted hover:text-foreground/75 md:hidden"
> >
<PanelLeftClose strokeWidth={1.8} aria-hidden="true" className="size-icon-md" /> <PanelLeftClose strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
</button> </button>

View File

@ -935,21 +935,9 @@ const MessageView = memo(function MessageView({
</span> </span>
</div> </div>
</button> </button>
<span className="text-ui-label font-mono text-muted-foreground tabular-nums">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div> </div>
) : message.role === "user" && !encounterMode ? ( ) : message.role === "user" && !encounterMode ? (
<div className="mb-1.5 flex items-center justify-end gap-2"> <div className="mb-1.5 flex items-center justify-end gap-2">
<span className="text-ui-label font-mono text-muted-foreground tabular-nums">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="text-ui-body-sm font-semibold text-foreground tracking-tight"> <span className="text-ui-body-sm font-semibold text-foreground tracking-tight">
{t`You`} {t`You`}
</span> </span>

View File

@ -13,6 +13,7 @@ import path from "node:path";
import type Docker from "dockerode"; import type Docker from "dockerode";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { import {
assertProjectWorkspaceBindPath,
assertVolumeSubpathSupport, assertVolumeSubpathSupport,
COMPUTER_IMAGE, COMPUTER_IMAGE,
computerHomeStorage, computerHomeStorage,
@ -873,4 +874,12 @@ describe("computer home storage", () => {
// Steady state waits on Xvfb instead of polling, so the trap runs immediately. // Steady state waits on Xvfb instead of polling, so the trap runs immediately.
expect(start).toMatch(/^wait "\$XVFB_PID"$/m); expect(start).toMatch(/^wait "\$XVFB_PID"$/m);
}); });
it("skips local statSync when the supervisor runs inside a container", () => {
const missingHostPath = "/srv/host-only/workspace-not-in-container";
expect(() => assertProjectWorkspaceBindPath(missingHostPath, false)).toThrow(
/does not exist on the host/,
);
expect(assertProjectWorkspaceBindPath(missingHostPath, true)).toBe(missingHostPath);
});
}); });

View File

@ -1,5 +1,5 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { statSync } from "node:fs"; import { existsSync, statSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { MAX_DESKTOP_DISPLAY, screenPorts } from "@rakazo/core/node/desktop-runtime"; import { MAX_DESKTOP_DISPLAY, screenPorts } from "@rakazo/core/node/desktop-runtime";
import type Docker from "dockerode"; import type Docker from "dockerode";
@ -217,9 +217,17 @@ export function homeVolumeMatches(
); );
} }
/** Validates a host folder before bind-mounting it into a project computer. */ /**
export function assertProjectWorkspaceBindPath(hostPath: string): string { * Validates a host folder before bind-mounting it into a project computer.
* When the supervisor itself runs inside a container, host workspace folders are
* validated by the API on the host and by the Docker daemon when mounting.
*/
export function assertProjectWorkspaceBindPath(
hostPath: string,
inContainer = existsSync("/.dockerenv"),
): string {
const resolved = path.resolve(hostPath); const resolved = path.resolve(hostPath);
if (inContainer) return resolved;
let stat: ReturnType<typeof statSync>; let stat: ReturnType<typeof statSync>;
try { try {
stat = statSync(resolved); stat = statSync(resolved);

View File

@ -322,7 +322,10 @@ app.post("/computers", async (c) => {
: hostGid; : hostGid;
await assertComputerHomeWritable(serviceHomePath, effectiveUid, effectiveGid); await assertComputerHomeWritable(serviceHomePath, effectiveUid, effectiveGid);
const projectWorkspacePath = body.projectWorkspacePath const projectWorkspacePath = body.projectWorkspacePath
? assertProjectWorkspaceBindPath(body.projectWorkspacePath) ? assertProjectWorkspaceBindPath(
body.projectWorkspacePath,
Boolean(runtimeInfo) || existsSync("/.dockerenv"),
)
: undefined; : undefined;
const name = containerNameFor(body.botId); const name = containerNameFor(body.botId);
const createdNetwork = const createdNetwork =

View File

@ -80,6 +80,7 @@ import type { McpServer, Prisma, PrismaClient, ThreadEvents } from "@rakazo/db";
import { import {
createSpaceForMember, createSpaceForMember,
effectiveMemoryScope, effectiveMemoryScope,
ensureComputerRecord,
ensureMainConversation, ensureMainConversation,
findDefaultModelCredential, findDefaultModelCredential,
findModelCredential, findModelCredential,
@ -1039,7 +1040,20 @@ export function createRunExecutor(deps: ExecutorDeps) {
where: { id: run.botId }, where: { id: run.botId },
select: { computerId: true, computerSwitching: true }, select: { computerId: true, computerSwitching: true },
}); });
if (!leaseTarget.computerId) throw new Error("Bot has no computer"); let computerId = leaseTarget.computerId;
if (!computerId) {
const teamComputer = await ensureComputerRecord(deps.prisma, {
mode: "team",
spaceId: run.spaceId,
userId: run.userId,
kind: deps.sandbox.describe().id,
});
await deps.prisma.bot.update({
where: { id: run.botId },
data: { computerId: teamComputer.id },
});
computerId = teamComputer.id;
}
if (leaseTarget.computerSwitching) { if (leaseTarget.computerSwitching) {
await requeueComputerRun(deps, runId, workerId, fence, resumeCheckpoint, heldForTakeover); await requeueComputerRun(deps, runId, workerId, fence, resumeCheckpoint, heldForTakeover);
return; return;
@ -1047,7 +1061,7 @@ export function createRunExecutor(deps: ExecutorDeps) {
let computerLease: ComputerExecutionLease | null = null; let computerLease: ComputerExecutionLease | null = null;
try { try {
computerLease = await acquireComputerExecutionLease(deps.prisma, { computerLease = await acquireComputerExecutionLease(deps.prisma, {
computerId: leaseTarget.computerId, computerId,
runId, runId,
botId: run.botId, botId: run.botId,
resumeHeldLease, resumeHeldLease,

View File

@ -798,11 +798,14 @@ function toAgentTool(tool: ConnectorTool, host: ToolHost, exposedName: string):
let steeringPoll: NodeJS.Timeout | undefined; let steeringPoll: NodeJS.Timeout | undefined;
if (host.request.claimSteering) { if (host.request.claimSteering) {
let steeringPollInFlight = false;
steeringPoll = setInterval(async () => { steeringPoll = setInterval(async () => {
if (toolController.signal.aborted) { if (toolController.signal.aborted) {
if (steeringPoll) clearInterval(steeringPoll); if (steeringPoll) clearInterval(steeringPoll);
return; return;
} }
if (steeringPollInFlight) return;
steeringPollInFlight = true;
try { try {
const steering = await host.request.claimSteering?.([...(host.seenSteeringIds ?? [])]); const steering = await host.request.claimSteering?.([...(host.seenSteeringIds ?? [])]);
if (steering && steering.length > 0) { if (steering && steering.length > 0) {
@ -822,6 +825,8 @@ function toAgentTool(tool: ConnectorTool, host: ToolHost, exposedName: string):
} }
} catch { } catch {
// Background poll failure is non-fatal // Background poll failure is non-fatal
} finally {
steeringPollInFlight = false;
} }
}, 80); }, 80);
} }

View File

@ -80,6 +80,28 @@ describe("runFailureError", () => {
expect(long).toHaveLength(301); expect(long).toHaveLength(301);
expect(long?.endsWith("…")).toBe(true); expect(long?.endsWith("…")).toBe(true);
}); });
it("strips Prisma source frames and file paths while keeping the database error reason", () => {
const prismaError = [
"Invalid `tx.event.create()` invocation in",
"/workspace/app/packages/db/src/events.ts:1235:19",
"",
" 1232 await assertRunCanWriteHistory(tx, input.runId);",
" 1233 // Unpaired UTF-16 surrogates (e.g. a split emoji high half) are invalid JSON for Postgres.",
" 1234 const payload = sanitizeJsonValue(input.payload);",
"→ 1235 return tx.event.create(",
"Unsupported Unicode escape sequence \\u0000 cannot be converted to text.",
].join("\n");
expect(runFailureError({ type: "run.failed", payload: { error: prismaError } })).toBe(
"Invalid `tx.event.create()` invocation: Unsupported Unicode escape sequence \\u0000 cannot be converted to text.",
);
const preTruncated =
"Invalid `tx.event.create()` invocation in /workspace/app/packages/db/src/events.ts:1235:19 1232 await assertRunCanWriteHistory(tx, input.runId); 1233 // Unpaired UTF-16 surrogates (e.g. a split emoji high half) are invalid JSON for Postgres. 1234 const payload = sanitizeJsonV…";
expect(runFailureError({ type: "run.failed", payload: { error: preTruncated } })).toBe(
"Invalid `tx.event.create()` invocation",
);
});
}); });
describe("projectMessages", () => { describe("projectMessages", () => {
@ -732,4 +754,24 @@ describe("sanitizeUtf16ForJson", () => {
}, },
}); });
}); });
it("strips NUL bytes and normalizes Date and non-finite numbers for Postgres jsonb", () => {
const timestamp = new Date("2026-10-18T21:00:00.000Z");
expect(sanitizeUtf16ForJson("hello\u0000world")).toBe("helloworld");
expect(
sanitizeJsonValue({
delta: "line1\u0000line2",
scheduledFor: timestamp,
invalidDate: new Date(Number.NaN),
nanValue: Number.NaN,
infValue: Number.POSITIVE_INFINITY,
}),
).toEqual({
delta: "line1line2",
scheduledFor: "2026-10-18T21:00:00.000Z",
invalidDate: null,
nanValue: null,
infValue: null,
});
});
}); });

View File

@ -171,6 +171,37 @@ export function isRunTerminalEvent(event: { type: string }): boolean {
const RUN_FAILURE_ERROR_MAX = 300; const RUN_FAILURE_ERROR_MAX = 300;
/**
* Strip file paths and numbered source-code frames from Prisma invocation errors so
* user-visible run failure messages keep the actual database error reason at the end
* without leaking server filesystem paths.
*/
export function stripPrismaInvocationFrame(message: string): string {
const match = /^Invalid `([^`]+)` invocation\b/.exec(message);
if (!match) return message;
const fallback = `Invalid \`${match[1]}\` invocation`;
const lines = message
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (lines.length > 1) {
const arrowIndex = lines.findIndex((line) => /^→\s*\d+\b/.test(line));
const tail = arrowIndex >= 0 ? lines.slice(arrowIndex + 1) : lines.slice(1);
const reason = [...tail]
.reverse()
.find(
(line) =>
!/^(?:→\s*)?\d+\b/.test(line) &&
!/^(?:\/|[A-Za-z]:\\).+:\d+:\d+$/.test(line) &&
!/^[{}()[\],.:;]+$/.test(line),
);
return reason ? `${fallback}: ${reason}` : fallback;
}
const inlineReason = /^Invalid `[^`]+` invocation:\s*(.+)$/.exec(lines[0] ?? "");
if (inlineReason?.[1]) return `${fallback}: ${inlineReason[1]}`;
return fallback;
}
/** Reason a run failed, clamped for display, or null when there is no usable error to show. */ /** Reason a run failed, clamped for display, or null when there is no usable error to show. */
export function runFailureError(event: { export function runFailureError(event: {
type: string; type: string;
@ -179,7 +210,7 @@ export function runFailureError(event: {
if (event.type !== "run.failed") return null; if (event.type !== "run.failed") return null;
const error = event.payload?.error; const error = event.payload?.error;
if (typeof error !== "string" || !error.trim()) return null; if (typeof error !== "string" || !error.trim()) return null;
const message = error.trim(); const message = stripPrismaInvocationFrame(error.trim());
// A provider can fail with a stack or a whole response body; the run record keeps the full // A provider can fail with a stack or a whole response body; the run record keeps the full
// text while the UI shows a bounded first line. // text while the UI shows a bounded first line.
return message.length > RUN_FAILURE_ERROR_MAX return message.length > RUN_FAILURE_ERROR_MAX
@ -384,12 +415,16 @@ function endsWithHighSurrogate(value: string): boolean {
} }
/** /**
* Replace unpaired UTF-16 surrogates so the string is safe for JSON (Postgres rejects them). * Replace unpaired UTF-16 surrogates and strip NUL bytes so the string is safe for JSON
* (Postgres jsonb and text columns reject both).
*/ */
export function sanitizeUtf16ForJson(value: string): string { export function sanitizeUtf16ForJson(value: string): string {
let result = ""; let result = "";
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i); const code = value.charCodeAt(i);
if (code === 0) {
continue;
}
if (isHighSurrogate(code)) { if (isHighSurrogate(code)) {
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : -1; const next = i + 1 < value.length ? value.charCodeAt(i + 1) : -1;
if (isLowSurrogate(next)) { if (isLowSurrogate(next)) {
@ -407,9 +442,13 @@ export function sanitizeUtf16ForJson(value: string): string {
return result; return result;
} }
/** Deep-sanitize string leaves so event payloads never carry unpaired surrogates into JSON. */ /** Deep-sanitize JSON values so payloads never carry NUL bytes, unpaired surrogates, or raw Dates into Postgres jsonb. */
export function sanitizeJsonValue<T>(value: T): T { export function sanitizeJsonValue<T>(value: T): T {
if (typeof value === "string") return sanitizeUtf16ForJson(value) as T; if (typeof value === "string") return sanitizeUtf16ForJson(value) as T;
if (typeof value === "number") return (Number.isFinite(value) ? value : null) as T;
if (value instanceof Date) {
return (Number.isFinite(value.getTime()) ? value.toISOString() : null) as T;
}
if (Array.isArray(value)) return value.map((item) => sanitizeJsonValue(item)) as T; if (Array.isArray(value)) return value.map((item) => sanitizeJsonValue(item)) as T;
if (value && typeof value === "object") { if (value && typeof value === "object") {
const out: Record<string, unknown> = {}; const out: Record<string, unknown> = {};

View File

@ -2092,4 +2092,49 @@ describe("appendEvent", () => {
expect(persisted.delta).not.toMatch(/[\uD800-\uDFFF]/); expect(persisted.delta).not.toMatch(/[\uD800-\uDFFF]/);
expect(() => JSON.stringify(persisted)).not.toThrow(); expect(() => JSON.stringify(persisted)).not.toThrow();
}); });
it("strips NUL bytes and retries transient serialization conflicts when appending events", async () => {
const conflict = Object.assign(new Error("serialization conflict"), { code: "P2034" });
const fanout = new TestFanout();
const created = {
...event(4),
type: "thread.progress",
runId: "run-4",
payload: { delta: "clean text", streaming: true },
};
const tx = {
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 5 }) },
run: { findUnique: vi.fn().mockResolvedValue({ status: "running" }) },
event: { create: vi.fn().mockResolvedValue(created) },
};
const transaction = vi
.fn()
.mockRejectedValueOnce(conflict)
.mockImplementation(async (callback: (client: typeof tx) => unknown) => callback(tx));
const prisma = {
$transaction: transaction,
} as unknown as PrismaClient;
await expect(
appendEvent(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
type: "thread.progress",
runId: "run-4",
payload: { delta: "clean\u0000 text", streaming: true },
},
fanout,
),
).resolves.toMatchObject({ type: "thread.progress", runId: "run-4" });
expect(transaction).toHaveBeenCalledTimes(2);
expect(tx.event.create).toHaveBeenCalledWith({
data: expect.objectContaining({
payload: { delta: "clean text", streaming: true },
}),
});
});
}); });

View File

@ -476,99 +476,101 @@ export async function claimSteering(
input: ClaimSteeringInput, input: ClaimSteeringInput,
realtime?: RealtimeFanout, realtime?: RealtimeFanout,
): Promise<ClaimedSteeringMessage[]> { ): Promise<ClaimedSteeringMessage[]> {
const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => { const result = await withTransactionRetry(() =>
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${input.threadId} FOR UPDATE`; prisma.$transaction(async (tx: Prisma.TransactionClient) => {
const run = await tx.run.findFirst({ await tx.$queryRaw`SELECT id FROM threads WHERE id = ${input.threadId} FOR UPDATE`;
where: { const run = await tx.run.findFirst({
id: input.runId, where: {
threadId: input.threadId, id: input.runId,
botId: input.botId,
status: "running",
leaseOwner: input.leaseOwner,
leaseFence: input.leaseFence,
},
select: {
id: true,
spaceId: true,
trigger: true,
sourceMessage: { select: { blocks: true } },
},
});
if (!run) return null;
const channelId =
run.trigger === "messaging"
? messagingChannelId(run.sourceMessage?.blocks as MessageBlock[] | undefined)
: undefined;
const steering = await tx.steeringMessage.findMany({
where: {
botId: input.botId,
claimedAt: null,
id: input.seenIds.length ? { notIn: input.seenIds } : undefined,
OR: [{ runId: null }, { runId: input.runId }],
message: {
threadId: input.threadId, threadId: input.threadId,
// Private follow-ups remain unclaimed for the existing private continuation. botId: input.botId,
...(channelId status: "running",
? { blocks: { array_contains: [{ kind: "channel_message", channelId }] } } leaseOwner: input.leaseOwner,
: {}), leaseFence: input.leaseFence,
}, },
}, select: {
include: { message: { select: { blocks: true, seq: true } } }, id: true,
orderBy: [{ message: { seq: "asc" } }, { id: "asc" }], spaceId: true,
}); trigger: true,
if (steering.length === 0) return null; sourceMessage: { select: { blocks: true } },
const claimed = await tx.steeringMessage.updateMany({ },
where: { id: { in: steering.map((item) => item.id) }, claimedAt: null }, });
data: { runId: input.runId, claimedAt: new Date() }, if (!run) return null;
}); const channelId =
if (claimed.count === 0) return null; run.trigger === "messaging"
? messagingChannelId(run.sourceMessage?.blocks as MessageBlock[] | undefined)
: undefined;
const steering = await tx.steeringMessage.findMany({
where: {
botId: input.botId,
claimedAt: null,
id: input.seenIds.length ? { notIn: input.seenIds } : undefined,
OR: [{ runId: null }, { runId: input.runId }],
message: {
threadId: input.threadId,
// Private follow-ups remain unclaimed for the existing private continuation.
...(channelId
? { blocks: { array_contains: [{ kind: "channel_message", channelId }] } }
: {}),
},
},
include: { message: { select: { blocks: true, seq: true } } },
orderBy: [{ message: { seq: "asc" } }, { id: "asc" }],
});
if (steering.length === 0) return null;
const claimed = await tx.steeringMessage.updateMany({
where: { id: { in: steering.map((item) => item.id) }, claimedAt: null },
data: { runId: input.runId, claimedAt: new Date() },
});
if (claimed.count === 0) return null;
const notifiedSeqs: number[] = []; const notifiedSeqs: number[] = [];
if ((tx as any).message?.findUnique && (tx as any).message?.create && tx.thread && tx.event) { if ((tx as any).message?.findUnique && (tx as any).message?.create && tx.thread && tx.event) {
for (const item of steering) { for (const item of steering) {
const clientNonce = `bot-ack:${input.runId}:${item.id}`; const clientNonce = `bot-ack:${input.runId}:${item.id}`;
const existing = await tx.message.findUnique({ const existing = await tx.message.findUnique({
where: { threadId_clientNonce: { threadId: input.threadId, clientNonce } }, where: { threadId_clientNonce: { threadId: input.threadId, clientNonce } },
select: { id: true }, select: { id: true },
});
if (!existing) {
const blocks: MessageBlock[] = [{ kind: "text", text: "👀" }];
const message = await createThreadMessageInTransaction(tx, {
threadId: input.threadId,
role: "bot",
botId: input.botId,
blocks,
replyToMessageId: item.messageId,
clientNonce,
markUnread: false,
}); });
const event = await appendEventInTransaction(tx, { if (!existing) {
spaceId: run.spaceId ?? "default", const blocks: MessageBlock[] = [{ kind: "text", text: "👀" }];
threadId: input.threadId, const message = await createThreadMessageInTransaction(tx, {
botId: input.botId, threadId: input.threadId,
type: "thread.message.created",
payload: {
messageId: message.id,
role: "bot", role: "bot",
botId: input.botId,
blocks, blocks,
replyToMessageId: item.messageId, replyToMessageId: item.messageId,
}, clientNonce,
}); markUnread: false,
notifiedSeqs.push(event.seq); });
const event = await appendEventInTransaction(tx, {
spaceId: run.spaceId ?? "default",
threadId: input.threadId,
botId: input.botId,
type: "thread.message.created",
payload: {
messageId: message.id,
role: "bot",
blocks,
replyToMessageId: item.messageId,
},
});
notifiedSeqs.push(event.seq);
}
} }
} }
}
return { return {
claimedMessages: steering.map((item) => ({ claimedMessages: steering.map((item) => ({
id: item.id, id: item.id,
messageId: item.messageId, messageId: item.messageId,
text: blocksToAgentHistoryText(item.message.blocks as MessageBlock[]), text: blocksToAgentHistoryText(item.message.blocks as MessageBlock[]),
blocks: item.message.blocks as MessageBlock[], blocks: item.message.blocks as MessageBlock[],
})), })),
notifiedSeqs, notifiedSeqs,
}; };
}); }),
);
if (!result) return []; if (!result) return [];
for (const seq of result.notifiedSeqs) { for (const seq of result.notifiedSeqs) {
@ -1008,8 +1010,8 @@ export async function appendEvent(
input: AppendEventInput, input: AppendEventInput,
realtime?: RealtimeFanout, realtime?: RealtimeFanout,
): Promise<ProductEvent> { ): Promise<ProductEvent> {
const event = await prisma.$transaction((tx: Prisma.TransactionClient) => const event = await withTransactionRetry(() =>
appendEventInTransaction(tx, input), prisma.$transaction((tx: Prisma.TransactionClient) => appendEventInTransaction(tx, input)),
); );
const productEvent = mapProductEvent(event); const productEvent = mapProductEvent(event);
await notifyRealtime(realtime, event.threadId, event.seq); await notifyRealtime(realtime, event.threadId, event.seq);

View File

@ -1,4 +1,5 @@
import type { MessageBlock } from "@rakazo/contracts"; import type { MessageBlock } from "@rakazo/contracts";
import { sanitizeJsonValue, sanitizeUtf16ForJson } from "@rakazo/core";
import type { Prisma, PrismaClient } from "./client.js"; import type { Prisma, PrismaClient } from "./client.js";
/** Group turns use channel inputs and their own outputs, never private thread history. */ /** Group turns use channel inputs and their own outputs, never private thread history. */
@ -70,15 +71,16 @@ export async function createThreadMessageInTransaction(
select: { nextMessageSeq: true }, select: { nextMessageSeq: true },
}); });
await assertRunCanWriteHistory(tx, input.runId); await assertRunCanWriteHistory(tx, input.runId);
const blocks = sanitizeJsonValue(input.blocks);
return tx.message.create({ return tx.message.create({
data: { data: {
threadId: input.threadId, threadId: input.threadId,
seq: thread.nextMessageSeq - 1, seq: thread.nextMessageSeq - 1,
role: input.role, role: input.role,
blocks: input.blocks as Prisma.InputJsonValue, blocks: blocks as Prisma.InputJsonValue,
botId: input.botId, botId: input.botId,
replyToMessageId: input.replyToMessageId, replyToMessageId: input.replyToMessageId,
replyQuote: input.replyQuote, replyQuote: input.replyQuote ? sanitizeUtf16ForJson(input.replyQuote) : undefined,
runId: input.runId, runId: input.runId,
clientNonce: input.clientNonce, clientNonce: input.clientNonce,
}, },

View File

@ -1,5 +1,5 @@
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs"; import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os"; import { homedir, tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { PrismaClient } from "./client.js"; import type { PrismaClient } from "./client.js";
@ -36,9 +36,13 @@ describe("project workspace wiring", () => {
return root; return root;
} }
it("normalizes absolute workspace paths", () => { it("normalizes absolute workspace paths and expands leading ~", () => {
const root = tempDir("project-workspace-normalize-"); const root = tempDir("project-workspace-normalize-");
expect(normalizeProjectWorkspacePath(root)).toBe(path.resolve(root)); expect(normalizeProjectWorkspacePath(root)).toBe(path.resolve(root));
expect(normalizeProjectWorkspacePath("~")).toBe(path.resolve(homedir()));
expect(normalizeProjectWorkspacePath("~/Projects/demo")).toBe(
path.resolve(homedir(), "Projects/demo"),
);
}); });
it("rejects missing folders", async () => { it("rejects missing folders", async () => {

View File

@ -1,4 +1,5 @@
import { realpath, stat } from "node:fs/promises"; import { realpath, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { readGitCheckout } from "@rakazo/core/git-checkout"; import { readGitCheckout } from "@rakazo/core/git-checkout";
import type { PrismaClient } from "./client.js"; import type { PrismaClient } from "./client.js";
@ -19,7 +20,13 @@ export function normalizeProjectWorkspacePath(input: string): string {
if (trimmed.includes("\0")) { if (trimmed.includes("\0")) {
throw new InvalidProjectWorkspacePathError("That folder path is not valid."); throw new InvalidProjectWorkspacePathError("That folder path is not valid.");
} }
return path.resolve(trimmed); const expanded =
trimmed === "~"
? os.homedir()
: trimmed.startsWith("~/") || trimmed.startsWith("~\\")
? path.join(os.homedir(), trimmed.slice(2))
: trimmed;
return path.resolve(expanded);
} }
/** The real folder behind a path, so a symlink cannot later point the link somewhere else. */ /** The real folder behind a path, so a symlink cannot later point the link somewhere else. */

View File

@ -16,16 +16,21 @@ function PopoverContent({
className, className,
align = "center", align = "center",
alignOffset = 0, alignOffset = 0,
collisionPadding = 12,
side = "bottom", side = "bottom",
sideOffset = 4, sideOffset = 4,
...props ...props
}: PopoverPrimitive.Popup.Props & }: PopoverPrimitive.Popup.Props &
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) { Pick<
PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "collisionPadding" | "side" | "sideOffset"
>) {
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner <PopoverPrimitive.Positioner
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
collisionPadding={collisionPadding}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
className="isolate z-50" className="isolate z-50"
@ -33,7 +38,7 @@ function PopoverContent({
<PopoverPrimitive.Popup <PopoverPrimitive.Popup
data-slot="popover-content" data-slot="popover-content"
className={cn( className={cn(
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", "z-50 flex w-72 max-w-[calc(100vw-1.5rem)] origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}

View File

@ -314,7 +314,7 @@ export const EvooMascot = memo(function EvooMascot({
viewBox="0 0 140 120" viewBox="0 0 140 120"
width={size} width={size}
height={size * 0.86} height={size * 0.86}
className="overflow-visible" className="overflow-visible drop-shadow-[0_1px_2px_rgba(15,23,42,0.18)] dark:drop-shadow-none"
aria-hidden="true" aria-hidden="true"
> >
<defs> <defs>