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(projekte.getByText("Projects", { exact: true })).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();
const picker = page.getByTestId("conversation-bot-picker");

View File

@ -254,10 +254,10 @@ export function OoAtelierDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
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"
>
<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">
<Trans>Atelier</Trans>
</DialogTitle>
@ -274,19 +274,19 @@ export function OoAtelierDialog({
</button>
</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
color={isPicture && !draft ? value : encoded}
identity={identity}
status={status}
size={128}
size={120}
moment={preview}
interactive
/>
</div>
<Tabs defaultValue="body">
<TabsList className="w-full">
<Tabs defaultValue="body" className="flex min-h-0 flex-1 flex-col">
<TabsList className="w-full shrink-0">
<TabsTrigger value="body">
<Trans>Body</Trans>
</TabsTrigger>
@ -303,7 +303,11 @@ export function OoAtelierDialog({
) : null}
</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
title={t`Species`}
values={OO_SPECIES}
@ -397,7 +401,11 @@ export function OoAtelierDialog({
</div>
</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
title={t`Lashes`}
values={OO_LASHES}
@ -456,7 +464,11 @@ export function OoAtelierDialog({
/>
</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
title={t`Glasses`}
values={OO_EYEWEAR}
@ -492,7 +504,7 @@ export function OoAtelierDialog({
</TabsContent>
{allowUpload ? (
<TabsContent value="upload" className="pt-2">
<TabsContent value="upload" className="min-h-0 flex-1 overflow-y-auto pt-2">
<button
type="button"
aria-label={t`Image upload area`}
@ -534,7 +546,7 @@ export function OoAtelierDialog({
) : null}
</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">
<button
type="button"

View File

@ -15,14 +15,14 @@ type OnboardingDiscoveryShellProps = {
compact?: boolean;
};
function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]): string {
function sourceLabel(id: DiscoverySourceId): ReactNode {
switch (id) {
case "profile-basics":
return t`Your profile`;
return <Trans>Your profile</Trans>;
case "connected-tools":
return t`Connected tools`;
return <Trans>Connected tools</Trans>;
case "specialist-crew":
return t`Specialist crew`;
return <Trans>Specialist crew</Trans>;
default: {
const never: never = id;
return never;
@ -83,7 +83,7 @@ export function OnboardingDiscoveryShell({
>
{active ? (
<>
{sourceLabel(active.id, t)}
{sourceLabel(active.id)}
{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>
</p>
<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"
aria-label={t`Look families`}
>

View File

@ -429,7 +429,7 @@ msgstr "Über Gruppen"
#: src/pages/shell/bot-picker.tsx
msgid "About spaces"
msgstr "Über Spaces"
msgstr "Über Projekte"
#: src/components/UserProfileSettings.tsx
msgid "About you"
@ -1001,7 +1001,7 @@ msgstr "Körper"
msgid "Booting live desktop…"
msgstr "Live-Desktop wird gestartet…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "Durch die Bereitstellung konfiguriert"
@ -1585,6 +1589,10 @@ msgstr "Verbunden, {0} wird verwendet."
msgid "Connected app"
msgstr "Verbundene App"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Verbundene Tools"
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
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
msgid "Create new Space"
msgstr "Neuen Space erstellen"
msgstr "Neues Projekt erstellen"
#: src/pages/shell/dialogs.tsx
msgid "Create project"
@ -2087,7 +2095,7 @@ msgstr "Projekt erstellen"
#: src/components/AskCard.tsx
#: src/pages/shell/dialogs.tsx
msgid "Create space"
msgstr "Space erstellen"
msgstr "Projekt erstellen"
#: src/components/integrations/GithubConnect.tsx
msgid "Create token on GitHub"
@ -2242,7 +2250,7 @@ msgstr "Erinnerungen ebenfalls löschen"
#: src/pages/Shell.tsx
msgid "Delete space"
msgstr "Space löschen"
msgstr "Projekt löschen"
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Delete your account?"
@ -3777,7 +3785,7 @@ msgstr "Neuer Skill"
#: src/pages/shell/dialogs.tsx
msgid "New space"
msgstr "Neuer Space"
msgstr "Neues Projekt"
#: src/components/CookingMode.tsx
msgid "Next"
@ -3891,6 +3899,10 @@ msgstr "Noch keine Projekte."
msgid "No providers found."
msgstr "Keine Anbieter gefunden."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "Keine letzte Aktivität"
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr "Keine Remote-MCP-Server gefunden"
@ -5149,7 +5161,7 @@ msgstr "Space-Standard"
#: src/pages/shell/dialogs.tsx
msgid "Spaces"
msgstr "Spaces"
msgstr "Projekte"
#: src/components/OoAtelier.tsx
msgid "Sparkle"
@ -5185,6 +5197,7 @@ msgstr "Vorlesen an oder aus"
msgid "Speaking with intent…"
msgstr "Spreche mit Bedacht…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
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."
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/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr "Bis wann?"
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
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."
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
msgid "Your specialists. One calm place to talk."
msgstr "Deine Spezialisten. Ein ruhiger Ort zum Reden."

View File

@ -1001,7 +1001,7 @@ msgstr "Body"
msgid "Booting live desktop…"
msgstr "Booting live desktop…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "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."
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
msgid "Configured by deployment"
msgstr "Configured by deployment"
@ -1585,6 +1589,10 @@ msgstr "Connected and using {0}."
msgid "Connected app"
msgstr "Connected app"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr "Connected tools"
#: src/pages/shell/message-cards.tsx
msgid "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."
msgstr "No providers found."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr "No recent activity"
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr "No remote MCP servers found"
@ -5185,6 +5197,7 @@ msgstr "Speaking on or off"
msgid "Speaking with intent…"
msgstr "Speaking with intent…"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "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."
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/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr "Until when?"
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "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."
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
msgid "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…"
msgstr "Iniciando escritorio en vivo…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "Configurado por el despliegue"
@ -1585,6 +1589,10 @@ msgstr "Conectado y usando {0}."
msgid "Connected app"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
msgstr "Conectado: sus herramientas estarán disponibles desde tu próximo mensaje."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found."
msgstr "No se encontraron proveedores."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
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."
msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
msgstr "Actualizado"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later."
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk."
msgstr ""

View File

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

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…"
msgstr "라이브 데스크톱 시작 중…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "서버에서 설정됨"
@ -1585,6 +1589,10 @@ msgstr "{0}에 연결하고 기본 모델로 설정했습니다."
msgid "Connected app"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
msgstr "연결되었습니다. 다음 메시지부터 이 도구를 사용할 수 있습니다."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found."
msgstr "검색된 AI 서비스가 없습니다."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
msgstr ""
@ -5526,10 +5539,6 @@ msgstr "이 구독 로그인 방식은 아직 Rakazo에서 지원되지 않습
msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
msgstr "최신 상태"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later."
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk."
msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…"
msgstr "Iniciando a área de trabalho ao vivo..."
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "Configurado por implantação"
@ -1585,6 +1589,10 @@ msgstr "Conectado e usando {0}."
msgid "Connected app"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
msgstr "Conectado. Suas ferramentas estão disponíveis na sua próxima mensagem."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found."
msgstr "Nenhum provedor encontrado"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
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."
msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
msgstr ""
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later."
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk."
msgstr ""

View File

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

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…"
msgstr "Canlı masaüstü başlatılıyor…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "Dağıtım tarafından yapılandırıldı"
@ -1585,6 +1589,10 @@ msgstr "Bağlandı, {0} kullanılıyor."
msgid "Connected app"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
msgstr "Bağlandı. Araçları bir sonraki mesajından itibaren kullanılabilir."
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found."
msgstr "Sağlayıcı bulunamadı."
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
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."
msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
msgstr ""
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later."
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk."
msgstr ""

View File

@ -1001,7 +1001,7 @@ msgstr ""
msgid "Booting live desktop…"
msgstr "正在启动实时桌面…"
#. placeholder {0}: active?.name
#. placeholder {0}: (active ?? coordinatorBot)?.name
#: src/pages/Shell.tsx
msgid "Booting up {0}’s computer"
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."
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
msgid "Configured by deployment"
msgstr "由部署配置"
@ -1585,6 +1589,10 @@ msgstr "已连接并使用 {0}。"
msgid "Connected app"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Connected tools"
msgstr ""
#: src/pages/shell/message-cards.tsx
msgid "Connected. Its tools are available from your next message."
msgstr "已连接,下一条消息起即可使用其工具。"
@ -3891,6 +3899,10 @@ msgstr ""
msgid "No providers found."
msgstr "未找到提供方。"
#: src/pages/ActivityList.tsx
msgid "No recent activity"
msgstr ""
#: src/components/integrations/IntegrationSetup.tsx
msgid "No remote MCP servers found"
msgstr ""
@ -5185,6 +5197,7 @@ msgstr ""
msgid "Speaking with intent…"
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
#: src/lib/onboarding-copy.ts
msgid "Specialist crew"
msgstr ""
@ -5526,10 +5539,6 @@ msgstr "此订阅登录在 Rakazo 中尚不可用。请使用部署凭据或选
msgid "This will permanently delete this project and all its conversations, bots, and memory."
msgstr ""
#: src/pages/Shell.tsx
msgid "Threads by topic · folders"
msgstr ""
#: src/pages/ModelTierSettings.tsx
#: src/pages/shell/bot-panel.tsx
msgid "Tiers"
@ -5715,6 +5724,7 @@ msgstr ""
#: src/components/DesktopUpdates.tsx
#: src/components/SoftwareUpdateSection.tsx
#: src/pages/AccountSettingsOverlay.tsx
msgid "Up to date"
msgstr "已是最新"
@ -6100,6 +6110,10 @@ msgstr ""
msgid "Your Oo will communicate in this language. You can change it later."
msgstr ""
#: src/components/onboarding/OnboardingDiscoveryShell.tsx
msgid "Your profile"
msgstr ""
#: src/pages/Welcome.tsx
msgid "Your specialists. One calm place to talk."
msgstr ""

View File

@ -188,6 +188,11 @@ export function UpdatesSettingsPanel({
<div className="space-y-5">
<DesktopUpdateSection />
<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>
);
}

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 (
<div className="mb-2 border-b border-border pb-2">

View File

@ -10,7 +10,6 @@ import {
abortableDelay,
buildFeaturedConnectorTiles,
CONNECTION_CATALOG_PAGE_SIZE,
EMPTY_PLUGIN_CATALOG_MESSAGE,
filterConnectionCatalogItems,
humanizeToolName,
isGithubInstall,
@ -730,7 +729,7 @@ export function PluginsOverlay({
<div className="mb-6" data-testid="featured-connectors">
{!loading && catalog.length === 0 ? (
<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>
) : (
<div className="grid grid-cols-2 gap-2">

View File

@ -176,7 +176,7 @@ export function SettingsOverlay({
<nav
data-testid="settings-nav"
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) => {
const Icon = item.icon;

View File

@ -2937,15 +2937,20 @@ export function ShellPage() {
overlay: boolean;
force?: boolean;
}) {
if (!active) return;
const targetBot = active ?? coordinatorBot;
if (!targetBot) return;
const needsBoot = force || computer?.state !== "running" || !screenUrl;
if (overlay && needsBoot) setBooting(true);
setComputerError(null);
setComputerErrorFromScreen(false);
try {
if (needsBoot) await rpc.computer.boot({ botId: active.id });
if (takeControl) await rpc.computer.takeover({ botId: active.id });
await refreshThread(active.id);
if (needsBoot) await rpc.computer.boot({ botId: targetBot.id });
if (takeControl) await rpc.computer.takeover({ botId: targetBot.id });
if (active) {
await refreshThread(targetBot.id);
} else {
await refreshComputerScreen(targetBot.id);
}
} catch (error) {
setComputerError(error instanceof Error ? error.message : t`Could not take control`);
setComputerErrorFromScreen(false);
@ -3080,8 +3085,9 @@ export function ShellPage() {
}, [panel, computerOpen, active?.id, computer?.state]);
async function openComputer() {
if (!active) return;
const needsTakeover = !userHoldsComputerControl(computer, active.id);
const targetBot = active ?? coordinatorBot;
if (!targetBot) return;
const needsTakeover = !userHoldsComputerControl(computer, targetBot.id);
const blocked = computerTakeoverBlocked(computer, snapshot?.run?.status);
try {
await bootComputer({
@ -3290,12 +3296,7 @@ export function ShellPage() {
<div className="sidebar-projekte">
{/* PROJEKTE SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 px-2.5">
<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="rk-shell-section-label">{t`Projects`}</div>
<div className="flex items-center gap-1.5">
<button
type="button"
@ -3309,18 +3310,9 @@ export function ShellPage() {
</div>
</div>
{projectSidebarGroups.length === 0 ? (
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
<p>{t`No projects yet.`}</p>
<Button
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>
<p className="px-2.5 py-1.5 text-ui-label rk-sidebar-muted">
{t`No projects yet.`}
</p>
) : null}
<DndContext
sensors={sensors}
@ -3481,6 +3473,7 @@ export function ShellPage() {
{conversationPickerOpen ? (
<PopoverContent
align="end"
collisionPadding={12}
className="w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
>
<ConversationBotPicker
@ -3677,8 +3670,8 @@ export function ShellPage() {
: "100%",
display: dualSurface.layoutMode === "canvas" ? "none" : "flex",
flexDirection: "column" as const,
minWidth: dualSurface.layoutMode === "canvas" ? 0 : 360,
flexShrink: 0,
minWidth: 0,
flexShrink: 1,
transition: "width 200ms ease",
}
: undefined;
@ -3693,8 +3686,8 @@ export function ShellPage() {
: "0%",
opacity: showCanvasOnDesktop ? 1 : 0,
pointerEvents: showCanvasOnDesktop ? ("auto" as const) : ("none" as const),
minWidth: showCanvasOnDesktop ? 360 : 0,
flexShrink: 0,
minWidth: 0,
flexShrink: 1,
transition: "width 200ms ease, opacity 150ms ease",
overflow: showCanvasOnDesktop ? ("visible" as const) : ("hidden" as const),
}
@ -3706,9 +3699,7 @@ export function ShellPage() {
panel === "create" ||
panel === "create-group" ||
panel === "browser")
? workspacePaneOpen
? workspacePaneWidth
: rightSidebarWidth
? "100%"
: 0,
};
@ -4243,9 +4234,9 @@ export function ShellPage() {
data-panel={panel ?? "closed"}
data-auto={panel !== null && panel === autoPanel ? "" : undefined}
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
? "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"
} ${!(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) ? (
<div className="flex-1 min-h-0 flex flex-col">
<InAppBrowser
hideCloseButton
onClose={() => {
setPanel(null);
if (!inProject) dualSurface.setLayoutMode("stream");
@ -5292,15 +5284,15 @@ export function ShellPage() {
{booting ? (
<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">
<Trans>Booting up {active?.name}’s computer</Trans>
<Trans>Booting up {(active ?? coordinatorBot)?.name}’s computer</Trans>
</div>
<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>
</div>
) : computerOpen && active ? (
) : computerOpen && (active ?? coordinatorBot) ? (
<FullscreenComputerOverlay
active={active}
active={(active ?? coordinatorBot)!}
computer={computer}
computerViewport={computerViewport}
embeddedScreenUrl={embeddedScreenUrl}

View File

@ -46,14 +46,14 @@ export function ComputerPanelPreview({
<>
<div
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 ? (
<div className="grid h-full place-items-center text-sm text-muted-foreground/80">
<Trans>Open in full window</Trans>
</div>
) : 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 ? (
<>
<iframe
@ -67,7 +67,11 @@ export function ComputerPanelPreview({
<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 ??
(computersAreUnavailable(sandboxProvider) ? (
<ComputersUnavailableHint />
@ -85,7 +89,7 @@ export function ComputerPanelPreview({
type="button"
data-testid="computer-preview-open"
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`}
onClick={onOpen}
>

View File

@ -108,14 +108,20 @@ export function NewSpaceDialog({
<Dialog open onOpenChange={closeUnlessBusy(saving, onCancel)}>
<DialogContent
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>
<DialogTitle className="flex items-center gap-2.5">
<DialogHeader
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 ? (
<>
<ProjectIcon icon={icon} color={color} size={30} />
<span>
<span className="truncate">
<Trans>New project</Trans>
</span>
</>
@ -133,7 +139,13 @@ export function NewSpaceDialog({
</DialogTitle>
</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">
<Trans>Name</Trans>
<Input
@ -179,7 +191,7 @@ export function NewSpaceDialog({
<div
role="radiogroup"
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) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -304,8 +316,12 @@ export function NewSpaceDialog({
) : null}
</div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
<DialogFooter className="mt-4">
{error ? (
<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}>
<Trans>Cancel</Trans>
</Button>

View File

@ -84,11 +84,11 @@ export function FullscreenComputerOverlay({
)}
{!recordingSkill && hasControl ? (
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>
</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>
</span>
)

View File

@ -95,16 +95,16 @@ export function ProjectLookDialog({
<DialogContent
showCloseButton={false}
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>
<DialogTitle className="flex items-center gap-3">
<DialogHeader className="shrink-0 border-b border-border px-5 py-4">
<DialogTitle className="flex min-w-0 items-center gap-3">
<ProjectIcon icon={icon} color={color} size={36} />
<span className="min-w-0 truncate">{name.trim() || initial.name}</span>
</DialogTitle>
</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">
<Trans>Name</Trans>
<Input
@ -147,7 +147,7 @@ export function ProjectLookDialog({
<div
role="radiogroup"
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) => {
const Glyph = PROJECT_ICON_COMPONENTS[option.id];
@ -243,8 +243,8 @@ export function ProjectLookDialog({
</label>
</div>
{error ? <p className="text-ui-body text-destructive mt-2">{error}</p> : null}
<DialogFooter className="mt-4">
{error ? <p className="px-5 pt-2 text-ui-body text-destructive">{error}</p> : null}
<DialogFooter className="mx-0 mb-0 mt-0 shrink-0 px-5 py-3.5">
<Button variant="outline" disabled={saving} onClick={onCancel}>
<Trans>Cancel</Trans>
</Button>

View File

@ -77,6 +77,7 @@ export function SidebarHeader({
{createMenuOpen ? (
<PopoverContent
align="end"
collisionPadding={12}
className="app-no-drag w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
>
<BotCreatePicker
@ -132,7 +133,7 @@ export function SidebarHeader({
}
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" />
</button>

View File

@ -935,21 +935,9 @@ const MessageView = memo(function MessageView({
</span>
</div>
</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>
) : message.role === "user" && !encounterMode ? (
<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">
{t`You`}
</span>

View File

@ -13,6 +13,7 @@ import path from "node:path";
import type Docker from "dockerode";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
assertProjectWorkspaceBindPath,
assertVolumeSubpathSupport,
COMPUTER_IMAGE,
computerHomeStorage,
@ -873,4 +874,12 @@ describe("computer home storage", () => {
// Steady state waits on Xvfb instead of polling, so the trap runs immediately.
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 { statSync } from "node:fs";
import { existsSync, statSync } from "node:fs";
import path from "node:path";
import { MAX_DESKTOP_DISPLAY, screenPorts } from "@rakazo/core/node/desktop-runtime";
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);
if (inContainer) return resolved;
let stat: ReturnType<typeof statSync>;
try {
stat = statSync(resolved);

View File

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

View File

@ -80,6 +80,7 @@ import type { McpServer, Prisma, PrismaClient, ThreadEvents } from "@rakazo/db";
import {
createSpaceForMember,
effectiveMemoryScope,
ensureComputerRecord,
ensureMainConversation,
findDefaultModelCredential,
findModelCredential,
@ -1039,7 +1040,20 @@ export function createRunExecutor(deps: ExecutorDeps) {
where: { id: run.botId },
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) {
await requeueComputerRun(deps, runId, workerId, fence, resumeCheckpoint, heldForTakeover);
return;
@ -1047,7 +1061,7 @@ export function createRunExecutor(deps: ExecutorDeps) {
let computerLease: ComputerExecutionLease | null = null;
try {
computerLease = await acquireComputerExecutionLease(deps.prisma, {
computerId: leaseTarget.computerId,
computerId,
runId,
botId: run.botId,
resumeHeldLease,

View File

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

View File

@ -80,6 +80,28 @@ describe("runFailureError", () => {
expect(long).toHaveLength(301);
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", () => {
@ -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;
/**
* 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. */
export function runFailureError(event: {
type: string;
@ -179,7 +210,7 @@ export function runFailureError(event: {
if (event.type !== "run.failed") return null;
const error = event.payload?.error;
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
// text while the UI shows a bounded first line.
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 {
let result = "";
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code === 0) {
continue;
}
if (isHighSurrogate(code)) {
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : -1;
if (isLowSurrogate(next)) {
@ -407,9 +442,13 @@ export function sanitizeUtf16ForJson(value: string): string {
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 {
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 (value && typeof value === "object") {
const out: Record<string, unknown> = {};

View File

@ -2092,4 +2092,49 @@ describe("appendEvent", () => {
expect(persisted.delta).not.toMatch(/[\uD800-\uDFFF]/);
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,
realtime?: RealtimeFanout,
): Promise<ClaimedSteeringMessage[]> {
const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${input.threadId} FOR UPDATE`;
const run = await tx.run.findFirst({
where: {
id: input.runId,
threadId: input.threadId,
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: {
const result = await withTransactionRetry(() =>
prisma.$transaction(async (tx: Prisma.TransactionClient) => {
await tx.$queryRaw`SELECT id FROM threads WHERE id = ${input.threadId} FOR UPDATE`;
const run = await tx.run.findFirst({
where: {
id: input.runId,
threadId: input.threadId,
// Private follow-ups remain unclaimed for the existing private continuation.
...(channelId
? { blocks: { array_contains: [{ kind: "channel_message", channelId }] } }
: {}),
botId: input.botId,
status: "running",
leaseOwner: input.leaseOwner,
leaseFence: input.leaseFence,
},
},
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;
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,
// 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[] = [];
if ((tx as any).message?.findUnique && (tx as any).message?.create && tx.thread && tx.event) {
for (const item of steering) {
const clientNonce = `bot-ack:${input.runId}:${item.id}`;
const existing = await tx.message.findUnique({
where: { threadId_clientNonce: { threadId: input.threadId, clientNonce } },
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 notifiedSeqs: number[] = [];
if ((tx as any).message?.findUnique && (tx as any).message?.create && tx.thread && tx.event) {
for (const item of steering) {
const clientNonce = `bot-ack:${input.runId}:${item.id}`;
const existing = await tx.message.findUnique({
where: { threadId_clientNonce: { threadId: input.threadId, clientNonce } },
select: { id: true },
});
const event = await appendEventInTransaction(tx, {
spaceId: run.spaceId ?? "default",
threadId: input.threadId,
botId: input.botId,
type: "thread.message.created",
payload: {
messageId: message.id,
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,
},
});
notifiedSeqs.push(event.seq);
clientNonce,
markUnread: false,
});
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 {
claimedMessages: steering.map((item) => ({
id: item.id,
messageId: item.messageId,
text: blocksToAgentHistoryText(item.message.blocks as MessageBlock[]),
blocks: item.message.blocks as MessageBlock[],
})),
notifiedSeqs,
};
});
return {
claimedMessages: steering.map((item) => ({
id: item.id,
messageId: item.messageId,
text: blocksToAgentHistoryText(item.message.blocks as MessageBlock[]),
blocks: item.message.blocks as MessageBlock[],
})),
notifiedSeqs,
};
}),
);
if (!result) return [];
for (const seq of result.notifiedSeqs) {
@ -1008,8 +1010,8 @@ export async function appendEvent(
input: AppendEventInput,
realtime?: RealtimeFanout,
): Promise<ProductEvent> {
const event = await prisma.$transaction((tx: Prisma.TransactionClient) =>
appendEventInTransaction(tx, input),
const event = await withTransactionRetry(() =>
prisma.$transaction((tx: Prisma.TransactionClient) => appendEventInTransaction(tx, input)),
);
const productEvent = mapProductEvent(event);
await notifyRealtime(realtime, event.threadId, event.seq);

View File

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

View File

@ -1,5 +1,5 @@
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 { afterEach, describe, expect, it, vi } from "vitest";
import type { PrismaClient } from "./client.js";
@ -36,9 +36,13 @@ describe("project workspace wiring", () => {
return root;
}
it("normalizes absolute workspace paths", () => {
it("normalizes absolute workspace paths and expands leading ~", () => {
const root = tempDir("project-workspace-normalize-");
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 () => {

View File

@ -1,4 +1,5 @@
import { realpath, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { readGitCheckout } from "@rakazo/core/git-checkout";
import type { PrismaClient } from "./client.js";
@ -19,7 +20,13 @@ export function normalizeProjectWorkspacePath(input: string): string {
if (trimmed.includes("\0")) {
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. */

View File

@ -16,16 +16,21 @@ function PopoverContent({
className,
align = "center",
alignOffset = 0,
collisionPadding = 12,
side = "bottom",
sideOffset = 4,
...props
}: PopoverPrimitive.Popup.Props &
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
Pick<
PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "collisionPadding" | "side" | "sideOffset"
>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
collisionPadding={collisionPadding}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
@ -33,7 +38,7 @@ function PopoverContent({
<PopoverPrimitive.Popup
data-slot="popover-content"
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,
)}
{...props}

View File

@ -314,7 +314,7 @@ export const EvooMascot = memo(function EvooMascot({
viewBox="0 0 140 120"
width={size}
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"
>
<defs>