From a2c7182ed68c7fd8c31103201d51fc3349f450ed Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:41:11 +0200 Subject: [PATCH] Side panel Routinen: list toggles, hide when empty (#180) * feat(web): routine list toggles and hide empty Routinen section Show Routinen in the Oo side panel only when the bot has routines; remove the panel create control. Each list row gets an active switch with optimistic updates via routines.update. E2E covers visibility, toggle persistence, and updates existing routine specs to create via RPC like chat. Co-authored-by: cynarAI * fix(web): sync lingui catalogs after routine panel UI change Co-authored-by: cynarAI * fix(web): refresh routine list optimistically after save Co-authored-by: cynarAI * test(e2e): wait for routine list after RPC create in golden path Co-authored-by: cynarAI * style: format golden e2e after routine list wait Co-authored-by: cynarAI * fix(web): show sign-up while session lookup is pending Co-authored-by: cynarAI --------- Co-authored-by: Cursor Agent Co-authored-by: cynarAI --- apps/web/e2e/golden.spec.ts | 38 +++++--- apps/web/e2e/helpers.ts | 34 +++++++ apps/web/e2e/routine-crud.spec.ts | 57 +++++++----- apps/web/e2e/routine-execution.spec.ts | 113 +++++++++++------------ apps/web/e2e/routine-list-toggle.spec.ts | 50 ++++++++++ apps/web/src/App.tsx | 8 +- apps/web/src/lib/session-gate.test.ts | 9 ++ apps/web/src/lib/session-gate.ts | 11 +++ apps/web/src/locales/de/messages.po | 4 - apps/web/src/locales/en/messages.po | 4 - apps/web/src/locales/es/messages.po | 4 - apps/web/src/locales/hi/messages.po | 4 - apps/web/src/locales/ko/messages.po | 4 - apps/web/src/locales/pt-BR/messages.po | 4 - apps/web/src/locales/ru/messages.po | 4 - apps/web/src/locales/tr/messages.po | 4 - apps/web/src/locales/zh-CN/messages.po | 4 - apps/web/src/pages/RoutineEditor.tsx | 35 ++++--- apps/web/src/pages/Shell.tsx | 101 ++++++++++++++------ 19 files changed, 314 insertions(+), 178 deletions(-) create mode 100644 apps/web/e2e/routine-list-toggle.spec.ts diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 447a582..48ac601 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -119,22 +119,30 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te }); await captureScreenshot(page, testInfo, "09a-computer-takeover-skipped"); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.locator("label:has-text('Name') input").fill("Monday briefing"); - await page - .locator("label:has-text('Instruction') textarea") - .fill("write a file in your home called notes/result.txt that says routine-ok"); - await page.getByRole("button", { name: "Add trigger" }).click(); - await page.getByRole("menuitem", { name: "On a schedule" }).hover(); - await page.getByRole("menuitem", { name: "Every day", exact: true }).click(); - const savedRoutine = page.waitForResponse( - (response) => response.url().includes("/rpc/routines/create") && response.ok(), + const botId = activeBotId(page); + await rpc(page, "routines/create", { + botId, + name: "Monday briefing", + prompt: "write a file in your home called notes/result.txt that says routine-ok", + crons: ["0 9 * * *"], + timezone: "UTC", + active: true, + notify: true, + webhookEnabled: false, + githubEnabled: false, + messageProvider: null, + }); + if ((await sidePanel.getAttribute("data-panel")) === "computer") { + await page.getByTitle("Agent computer").click(); + } + const routinesListed = page.waitForResponse( + (response) => response.url().includes("/rpc/routines/list") && response.ok(), ); - await page.getByRole("button", { name: "Save" }).click(); - await savedRoutine; - await expect(page.getByRole("button", { name: "Save" })).toBeEnabled(); - await page.getByRole("button", { name: "Back" }).click(); - await expect(page.getByText("Monday briefing")).toBeVisible(); + await page.getByTitle("Agent computer").click(); + await routinesListed; + await expect( + sidePanel.getByTestId("routine-list-row").getByText("Monday briefing"), + ).toBeVisible(); await captureScreenshot(page, testInfo, "10-routine-created"); await openUserSettings(page, "integrations"); diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 8e2d7ea..ea65103 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -1,4 +1,5 @@ import { expect, type Locator, type Page, type Route, type TestInfo } from "@playwright/test"; +import type { Routine } from "@rakazo/contracts"; type RpcEnvelope = { json?: Record }; @@ -26,6 +27,39 @@ export function activeBotId(page: Page) { return id; } +export async function createTestRoutine( + page: Page, + botId: string, + overrides: { + name?: string; + prompt?: string; + crons?: string[]; + timezone?: string; + active?: boolean; + webhookEnabled?: boolean; + githubEnabled?: boolean; + messageProvider?: string | null; + } = {}, +) { + return rpc(page, "routines/create", { + botId, + name: "Test routine", + prompt: "Run the check-in", + crons: ["0 9 * * *"], + timezone: "UTC", + active: true, + notify: true, + webhookEnabled: false, + githubEnabled: false, + messageProvider: null, + ...overrides, + }); +} + +export async function openAgentComputerPanel(page: Page) { + await page.getByTitle("Agent computer").click(); +} + export async function rpc(page: Page, procedure: string, body: unknown): Promise { const spaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id")); const response = await page.request.post(`/rpc/${procedure}`, { diff --git a/apps/web/e2e/routine-crud.spec.ts b/apps/web/e2e/routine-crud.spec.ts index 4362410..e4c138b 100644 --- a/apps/web/e2e/routine-crud.spec.ts +++ b/apps/web/e2e/routine-crud.spec.ts @@ -5,6 +5,8 @@ import { captureScreenshot, completeOnboarding, createNamedBot, + createTestRoutine, + openAgentComputerPanel, rpc, signup, } from "./helpers"; @@ -29,8 +31,10 @@ test("routine active switch keeps its thumb inside the track", async ({ page }, const stamp = Date.now(); await signup(page, `routine-toggle-${stamp}@rakazo.test`, "password12", "Routine Toggle"); await completeOnboarding(page); - await page.getByTitle("Agent computer").click(); - await page.getByRole("button", { name: "Create Routine" }).click(); + const botId = activeBotId(page); + await createTestRoutine(page, botId, { name: "Toggle styling" }); + await openAgentComputerPanel(page); + await page.getByRole("button", { name: /Toggle styling/ }).click(); const toggle = page.getByRole("switch", { name: "Active" }); const thumb = toggle.locator("span"); @@ -133,23 +137,27 @@ test("routine editing updates in place, preserves timezone, and deletion persist await expect(updatedButton).toHaveCount(0); }); -test("invalid advanced cron is rejected without creating a routine", async ({ page }, testInfo) => { +test("invalid advanced cron is rejected without creating a routine", async ({ page }) => { const stamp = Date.now(); await signup(page, `routine-invalid-${stamp}@rakazo.test`, "password12", "Invalid Routine"); await completeOnboarding(page); const botId = activeBotId(page); - await page.getByTitle("Agent computer").click(); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.locator("label:has-text('Name') input").fill("Broken schedule"); - await page.locator("label:has-text('Instruction') textarea").fill("This should never run"); - await addScheduleTrigger(page, "Advanced..."); - await page.getByLabel("Cron expression").fill("61 25 * * *"); - await page.getByRole("button", { name: "Save", exact: true }).click(); - - await expect(page.getByRole("alert")).toContainText("Enter a valid cron expression."); + await expect( + rpc(page, "routines/create", { + botId, + name: "Broken schedule", + prompt: "This should never run", + crons: ["61 25 * * *"], + timezone: "UTC", + active: true, + notify: true, + webhookEnabled: false, + githubEnabled: false, + messageProvider: null, + }), + ).rejects.toThrow(); expect(await rpc(page, "routines/list", { botId })).toEqual([]); - await captureScreenshot(page, testInfo, "invalid-cron-rejected"); }); test("a successful routine create is not reported as failed when refresh fails", async ({ @@ -160,26 +168,30 @@ test("a successful routine create is not reported as failed when refresh fails", await completeOnboarding(page); const botId = activeBotId(page); - await page.getByTitle("Agent computer").click(); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.locator("label:has-text('Name') input").fill("Persisted routine"); - await page.locator("label:has-text('Instruction') textarea").fill("Run once each morning"); - await addScheduleTrigger(page, "Every day"); + const created = await createTestRoutine(page, botId, { + name: "Persisted routine", + prompt: "Run once each morning", + }); + await openAgentComputerPanel(page); + await page.getByRole("button", { name: /Persisted routine/ }).click(); + await page + .locator("label:has-text('Instruction') textarea") + .fill("Run once each morning, revised"); await page.route( "**/rpc/routines/list", (route) => route.fulfill({ status: 500, body: "refresh failed" }), { times: 1 }, ); - const createResponse = page.waitForResponse( - (response) => response.url().includes("/rpc/routines/create") && response.ok(), + const updateResponse = page.waitForResponse( + (response) => response.url().includes("/rpc/routines/update") && response.ok(), ); const failedRefresh = page.waitForResponse( (response) => response.url().includes("/rpc/routines/list") && response.status() === 500, ); await page.getByRole("button", { name: "Save", exact: true }).click(); - await createResponse; + await updateResponse; await failedRefresh; await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "routine"); await expect(page.getByRole("alert")).toHaveCount(0); @@ -187,7 +199,8 @@ test("a successful routine create is not reported as failed when refresh fails", await page.unroute("**/rpc/routines/list"); const routines = await rpc(page, "routines/list", { botId }); expect(routines).toHaveLength(1); - expect(routines[0]?.name).toBe("Persisted routine"); + expect(routines[0]?.id).toBe(created.id); + expect(routines[0]?.prompt).toBe("Run once each morning, revised"); }); test("switching bots while a routine save is pending does not reopen stale state", async ({ diff --git a/apps/web/e2e/routine-execution.spec.ts b/apps/web/e2e/routine-execution.spec.ts index 478d054..fa3dcf5 100644 --- a/apps/web/e2e/routine-execution.spec.ts +++ b/apps/web/e2e/routine-execution.spec.ts @@ -1,6 +1,14 @@ import { expect, test } from "@playwright/test"; import type { Routine } from "@rakazo/contracts"; -import { activeBotId, captureScreenshot, completeOnboarding, rpc, signup } from "./helpers"; +import { + activeBotId, + captureScreenshot, + completeOnboarding, + createTestRoutine, + openAgentComputerPanel, + rpc, + signup, +} from "./helpers"; test("Slack message trigger uses the mounted messaging provider and persists", async ({ page, @@ -18,26 +26,21 @@ test("Slack message trigger uses the mounted messaging provider and persists", a await completeOnboarding(page); const botId = activeBotId(page); - await page.getByTitle("Agent computer").click(); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.getByPlaceholder("Name this routine").fill("Triage Slack updates"); - await page - .getByPlaceholder("What should this routine do each time it runs?") - .fill("Review the verified message event"); - await page.getByRole("button", { name: "Add trigger" }).click(); - await page.getByRole("menuitem", { name: "Slack message", exact: true }).click(); - + await rpc(page, "routines/create", { + botId, + name: "Triage Slack updates", + prompt: "Review the verified message event", + crons: [], + timezone: "UTC", + active: true, + notify: true, + webhookEnabled: false, + githubEnabled: false, + messageProvider: "slack", + }); + await openAgentComputerPanel(page); const panel = page.getByTestId("side-panel"); await expect(panel.getByText("Slack message", { exact: true })).toBeVisible(); - await expect( - panel.getByText("Runs when this bot receives a verified message from this provider."), - ).toBeVisible(); - - const saved = page.waitForResponse( - (response) => response.url().includes("/rpc/routines/create") && response.ok(), - ); - await page.getByRole("button", { name: "Save", exact: true }).click(); - await saved; const [routine] = await rpc(page, "routines/list", { botId }); expect(routine).toMatchObject({ name: "Triage Slack updates", @@ -57,26 +60,25 @@ test("GitHub event trigger exposes signed delivery settings and persists", async await completeOnboarding(page); const botId = activeBotId(page); - await page.getByTitle("Agent computer").click(); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.getByPlaceholder("Name this routine").fill("Review repository events"); - await page - .getByPlaceholder("What should this routine do each time it runs?") - .fill("Inspect the signed GitHub event"); - await page.getByRole("button", { name: "Add trigger" }).click(); - await page.getByRole("menuitem", { name: "Git event", exact: true }).click(); - + await rpc(page, "routines/create", { + botId, + name: "Review repository events", + prompt: "Inspect the signed GitHub event", + crons: [], + timezone: "UTC", + active: true, + notify: true, + webhookEnabled: false, + githubEnabled: true, + messageProvider: null, + }); + await openAgentComputerPanel(page); + await page.getByRole("button", { name: /Review repository events/ }).click(); await expect( page.getByTestId("side-panel").getByText("Git event", { exact: true }), ).toBeVisible(); await expect(page.getByText(new RegExp(`/api/v1/bots/${botId}/github$`))).toBeVisible(); await expect(page.getByText("X-Hub-Signature-256: sha256=…", { exact: true })).toBeVisible(); - - const saved = page.waitForResponse( - (response) => response.url().includes("/rpc/routines/create") && response.ok(), - ); - await page.getByRole("button", { name: "Save", exact: true }).click(); - await saved; const [routine] = await rpc(page, "routines/list", { botId }); expect(routine).toMatchObject({ name: "Review repository events", @@ -102,14 +104,15 @@ test("Korean webhook routine keeps technical field labels in English", async ({ await settings.getByRole("option", { name: "한국어", exact: true }).click(); await page.getByRole("button", { name: "계정 설정 닫기" }).click(); + const botId = activeBotId(page); + await createTestRoutine(page, botId, { + name: "한국어 웹훅 확인", + prompt: "웹훅을 확인합니다.", + crons: [], + webhookEnabled: true, + }); await page.getByTitle("Agent 컴퓨터").click(); - await page.getByRole("button", { name: "자동 실행 만들기" }).click(); - await page.getByPlaceholder("이 루틴의 이름을 정하세요").fill("한국어 웹훅 확인"); - await page - .getByPlaceholder("이 루틴이 실행될 때마다 무엇을 해야 하나요?") - .fill("웹훅을 확인합니다."); - await page.getByRole("button", { name: "트리거 추가" }).click(); - await page.getByRole("menuitem", { name: "웹훅", exact: true }).click(); + await page.getByRole("button", { name: /한국어 웹훅 확인/ }).click(); await expect(page.getByText("웹훅이 실행될 때", { exact: true })).toBeVisible(); await expect(page.getByText("POST 대상")).toBeVisible(); @@ -122,27 +125,17 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo const stamp = Date.now(); await signup(page, `routine-${stamp}@rakazo.test`, "password12", "Routine"); await completeOnboarding(page); + const botId = activeBotId(page); - await page.getByTitle("Agent computer").click(); + await openAgentComputerPanel(page); await expect(page.getByRole("button", { name: "Test run" })).toHaveCount(0); - await page.getByRole("button", { name: "Create Routine" }).click(); - await page.locator("label:has-text('Name') input").fill("Daily verification"); - await page - .locator("label:has-text('Instruction') textarea") - .fill("write routine-run-now-ok into the durable task result"); - await page.getByRole("button", { name: "Add trigger" }).click(); - await page.getByRole("menuitem", { name: "On a schedule" }).hover(); - await page.getByRole("menuitem", { name: "Weekdays", exact: true }).click(); - await expect(page.getByLabel("How often")).toHaveValue("Weekdays"); - await captureScreenshot(page, testInfo, "32-routine-configured"); - - const saved = page.waitForResponse( - (response) => response.url().includes("/rpc/routines/create") && response.ok(), - ); - await page.getByRole("button", { name: "Save" }).click(); - await saved; - await expect(page.getByRole("button", { name: "Save" })).toBeEnabled(); - await page.getByRole("button", { name: "Back" }).click(); + await createTestRoutine(page, botId, { + name: "Daily verification", + prompt: "write routine-run-now-ok into the durable task result", + crons: ["0 9 * * 1-5"], + }); + await page.reload(); + await openAgentComputerPanel(page); const routine = page.getByRole("button", { name: /Daily verification/ }); await expect(routine).toContainText("Weekdays at 9:00 AM"); await captureScreenshot(page, testInfo, "33-routine-scheduled"); diff --git a/apps/web/e2e/routine-list-toggle.spec.ts b/apps/web/e2e/routine-list-toggle.spec.ts new file mode 100644 index 0000000..244071c --- /dev/null +++ b/apps/web/e2e/routine-list-toggle.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; +import type { Routine } from "@rakazo/contracts"; +import { + activeBotId, + completeOnboarding, + createTestRoutine, + openAgentComputerPanel, + rpc, + signup, +} from "./helpers"; + +test("routines section is hidden until the Oo has one, then list toggle persists", async ({ + page, +}) => { + const stamp = Date.now(); + await signup(page, `routine-list-${stamp}@rakazo.test`, "password12", "Routine List"); + await completeOnboarding(page); + const botId = activeBotId(page); + + await openAgentComputerPanel(page); + const panel = page.getByTestId("side-panel"); + await expect(panel.getByText("Routines", { exact: true })).toHaveCount(0); + await expect(page.getByTestId("routine-create-button")).toHaveCount(0); + + await createTestRoutine(page, botId, { name: "Morning check-in" }); + await page.reload(); + await openAgentComputerPanel(page); + await expect(panel.getByText("Routines", { exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: /Morning check-in/ })).toBeVisible(); + + const toggle = page.getByTestId("routine-list-active-toggle"); + await expect(toggle).toHaveAttribute("aria-checked", "true"); + await expect(page.getByText("Every day", { exact: false })).toBeVisible(); + + const updated = page.waitForResponse( + (response) => response.url().includes("/rpc/routines/update") && response.ok(), + ); + await toggle.click(); + await expect(toggle).toHaveAttribute("aria-checked", "false"); + await updated; + + const [routine] = await rpc(page, "routines/list", { botId }); + expect(routine?.active).toBe(false); + await expect(page.getByText("Paused", { exact: true })).toBeVisible(); + + await toggle.click(); + await expect(toggle).toHaveAttribute("aria-checked", "true"); + const [reactivated] = await rpc(page, "routines/list", { botId }); + expect(reactivated?.active).toBe(true); +}); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3b1e012..9888b12 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,13 +2,14 @@ import { Trans, useLingui } from "@lingui/react/macro"; import { LOCAL_SETTINGS_PAGE } from "@rakazo/contracts"; import { Button, Skeleton } from "@rakazo/ui-web"; import { Suspense, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Navigate, Route, Routes, useSearchParams } from "react-router-dom"; +import { Navigate, Route, Routes, useLocation, useSearchParams } from "react-router-dom"; import { LoadingState } from "./components/ai/primitives"; import { authClient } from "./lib/auth"; import { authReturnPath } from "./lib/auth-next"; import { lazyWithRecovery } from "./lib/lazy-recovery"; import { markAfterPaint, markOnce } from "./lib/performance"; import { + allowsAnonymousEntryWhileSessionPending, holdUnreachableGate, sessionGate, sessionRetryDelayMs, @@ -47,6 +48,7 @@ export function App() { function SessionApp() { const [searchParams] = useSearchParams(); + const { pathname } = useLocation(); const signInDestination = authReturnPath(searchParams.get("next")); const session = authClient.useSession(); const gate = sessionGate(session); @@ -63,8 +65,8 @@ function SessionApp() { if (showSessionUnavailable(gate, nextHolding)) { return ; } - if (gate === "loading") { - return window.location.pathname.startsWith("/app") ? ( + if (gate === "loading" && !allowsAnonymousEntryWhileSessionPending(pathname)) { + return pathname.startsWith("/app") ? ( ) : (
{ expect(sessionRetryDelayMs(3)).toBe(8_000); expect(sessionRetryDelayMs(20)).toBe(15_000); }); + + it("allows public entry routes while the session lookup is pending", () => { + expect(allowsAnonymousEntryWhileSessionPending("/sign-up")).toBe(true); + expect(allowsAnonymousEntryWhileSessionPending("/sign-in")).toBe(true); + expect(allowsAnonymousEntryWhileSessionPending("/")).toBe(true); + expect(allowsAnonymousEntryWhileSessionPending("/onboarding")).toBe(false); + expect(allowsAnonymousEntryWhileSessionPending("/app")).toBe(false); + }); }); diff --git a/apps/web/src/lib/session-gate.ts b/apps/web/src/lib/session-gate.ts index 9a6ba78..1d0f446 100644 --- a/apps/web/src/lib/session-gate.ts +++ b/apps/web/src/lib/session-gate.ts @@ -37,6 +37,17 @@ export function showSessionUnavailable(gate: SessionGate, holding: boolean): boo return gate === "unreachable" || (holding && gate === "loading"); } +/** Sign-in/up and marketing entry stay usable while the first session lookup is in flight. */ +export function allowsAnonymousEntryWhileSessionPending(pathname: string): boolean { + return ( + pathname === "/" || + pathname === "/sign-in" || + pathname === "/sign-up" || + pathname === "/forgot-password" || + pathname === "/reset-password" + ); +} + const RETRY_BASE_MS = 1_000; const RETRY_MAX_MS = 15_000; diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index e5ea528..0db6e3d 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -1879,10 +1879,6 @@ msgstr "Neuen Space erstellen" msgid "Create project" msgstr "Projekt erstellen" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "Routine erstellen" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index ebd4e55..ffe2027 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -1879,10 +1879,6 @@ msgstr "Create new Space" msgid "Create project" msgstr "Create project" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "Create Routine" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 8b196d4..4786a24 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -1879,10 +1879,6 @@ msgstr "Crear nuevo space" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "Crear rutina" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index 51bfffd..4faeaae 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -1879,10 +1879,6 @@ msgstr "नया स्पेस बनाएं" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index a927ff7..b612da3 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -1879,10 +1879,6 @@ msgstr "새 스페이스 만들기" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "자동 실행 만들기" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index 69732b9..f8cd472 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -1879,10 +1879,6 @@ msgstr "Criar novo space" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 2ffa8c0..4626dcc 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -1879,10 +1879,6 @@ msgstr "Создать новое пространство" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "Создать задачу" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index f6e9605..9913c6b 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -1879,10 +1879,6 @@ msgstr "Yeni space oluştur" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index a66a4d7..9a2806b 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -1879,10 +1879,6 @@ msgstr "新建工作区" msgid "Create project" msgstr "" -#: src/pages/RoutineEditor.tsx -msgid "Create Routine" -msgstr "" - #: src/components/AskCard.tsx #: src/pages/shell/dialogs.tsx msgid "Create space" diff --git a/apps/web/src/pages/RoutineEditor.tsx b/apps/web/src/pages/RoutineEditor.tsx index 29cc9d9..2a3a202 100644 --- a/apps/web/src/pages/RoutineEditor.tsx +++ b/apps/web/src/pages/RoutineEditor.tsx @@ -110,23 +110,12 @@ export function routineTriggerSummary(routine: Routine): string { return parts.length > 0 ? parts.join(" · ") : t`No trigger`; } -export function RoutineListHeader({ onCreate }: { onCreate: () => void }) { - const { t } = useLingui(); +export function RoutineListHeader() { return ( -
+
Routines
-
); } @@ -136,14 +125,23 @@ export function RoutineListRow({ running, onOpen, onStop, + onActiveChange, + togglingActive, }: { routine: Routine; running: boolean; onOpen: () => void; onStop: () => void; + onActiveChange: (active: boolean) => void; + togglingActive?: boolean; }) { + const { t } = useLingui(); return ( -
+
) : null} + event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + />
); } diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index 7ce6742..691bfc7 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -397,6 +397,8 @@ export function ShellPage() { } | null>(null); const [routines, setRoutines] = useState([]); const [routinesBotId, setRoutinesBotId] = useState(null); + const [routineActiveToggleId, setRoutineActiveToggleId] = useState(null); + const routineActiveToggleRequest = useRef(0); const [taughtSkills, setTaughtSkills] = useState([]); const [taughtSkillsBotId, setTaughtSkillsBotId] = useState(null); const [teachBusy, setTeachBusy] = useState(false); @@ -2407,6 +2409,43 @@ export function ShellPage() { setEditingRoutine(null); setPanel("routine"); }, []); + const setRoutineActive = useCallback( + async (routine: Routine, active: boolean) => { + const botId = activeBotId.current; + if (!botId || routine.botId !== botId) return; + const request = ++routineActiveToggleRequest.current; + const previousActive = routine.active; + setRoutineActiveToggleId(routine.id); + setRoutines((current) => + routinesBotId === botId + ? current.map((item) => (item.id === routine.id ? { ...item, active } : item)) + : current, + ); + try { + const saved = await rpc.routines.update({ routineId: routine.id, active }); + if (routineActiveToggleRequest.current !== request || activeBotId.current !== botId) return; + setRoutines((current) => + routinesBotId === botId + ? current.map((item) => (item.id === saved.id ? saved : item)) + : current, + ); + } catch { + if (routineActiveToggleRequest.current !== request || activeBotId.current !== botId) return; + setRoutines((current) => + routinesBotId === botId + ? current.map((item) => + item.id === routine.id ? { ...item, active: previousActive } : item, + ) + : current, + ); + } finally { + if (routineActiveToggleRequest.current === request) { + setRoutineActiveToggleId((current) => (current === routine.id ? null : current)); + } + } + }, + [routinesBotId], + ); const speakingMessageIdRef = useRef(speakingMessageId); speakingMessageIdRef.current = speakingMessageId; const speakMessage = useCallback((message: ThreadMessage) => { @@ -3541,34 +3580,32 @@ export function ShellPage() { sandboxProvider={bootstrapMe?.sandboxProvider} onOpen={() => void openComputer()} /> - { - setRoutineDraft(emptyRoutineDraft()); - setRoutineWebhookSecret(null); - setEditingRoutine(null); - setRoutineError(null); - setPanel("routine"); - }} - /> - {activeRoutines.map((routine) => { - const routineRunning = - snapshot?.run?.routineId === routine.id && isActive(snapshot.run.status); - return ( - { - setRoutineDraft(draftFromRoutine(routine)); - setRoutineWebhookSecret(null); - setEditingRoutine(routine); - setRoutineError(null); - setPanel("routine"); - }} - onStop={() => void stopRun()} - /> - ); - })} + {activeRoutines.length > 0 ? ( + <> + + {activeRoutines.map((routine) => { + const routineRunning = + snapshot?.run?.routineId === routine.id && isActive(snapshot.run.status); + return ( + void setRoutineActive(routine, next)} + onOpen={() => { + setRoutineDraft(draftFromRoutine(routine)); + setRoutineWebhookSecret(null); + setEditingRoutine(routine); + setRoutineError(null); + setPanel("routine"); + }} + onStop={() => void stopRun()} + /> + ); + })} + + ) : null}
) : null} {panel === "create-group" ? ( @@ -3746,6 +3783,14 @@ export function ShellPage() { ) { setEditingRoutine(saved); setRoutineDraft(draftFromRoutine(saved)); + setRoutines((current) => + routinesBotId === targetBotId || routinesBotId === null + ? current.some((item) => item.id === saved.id) + ? current.map((item) => (item.id === saved.id ? saved : item)) + : [...current, saved] + : current, + ); + setRoutinesBotId(targetBotId); } } catch (error) { if (