fix(web,core,db): resolve responsive layout overflows, dialog clipping, i18n gaps, and runtime stability
This commit is contained in:
parent
ac9fcce0fb
commit
66dbf0046e
@ -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");
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ? (
|
||||
<>
|
||||
{" · "}
|
||||
|
||||
@ -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`}
|
||||
>
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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> = {};
|
||||
|
||||
@ -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 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user