diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7146fb..ae1d15c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,25 @@ jobs: with: persist-credentials: false - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 @@ -35,6 +54,25 @@ jobs: with: persist-credentials: false - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 @@ -54,6 +92,25 @@ jobs: with: persist-credentials: false - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 @@ -68,6 +125,7 @@ jobs: run: xvfb-run --auto-servernum pnpm --filter @rakazo/desktop exec playwright test --config e2e/playwright.config.ts - name: Upload desktop setup screenshots if: always() + continue-on-error: true uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: desktop-e2e-screenshots @@ -82,6 +140,25 @@ jobs: with: persist-credentials: false - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 @@ -101,6 +178,25 @@ jobs: with: persist-credentials: false - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 @@ -227,6 +323,26 @@ jobs: - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 if: steps.ota.outputs.publish == 'true' + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + if: steps.ota.outputs.publish == 'true' + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 if: steps.ota.outputs.publish == 'true' with: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index eade067..4ce2751 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -54,18 +54,39 @@ jobs: playwright: name: Web E2E runs-on: [self-hosted, evoo-ci, Linux] - timeout-minutes: 20 + timeout-minutes: 30 concurrency: group: ${{ inputs.publish_report && 'playwright-publication' || format('playwright-{0}', github.run_id) }} cancel-in-progress: false steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 + id: pnpm-setup + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} + - name: Isolate pnpm store and PATH + shell: bash + run: | + set -euo pipefail + bin="${{ steps.pnpm-setup.outputs.bin_dest }}" + dest="${{ steps.pnpm-setup.outputs.dest }}" + if [[ -z "$bin" || -z "$dest" ]]; then + echo "pnpm setup did not report install paths" >&2 + exit 1 + fi + echo "PNPM_HOME=${dest}" >> "$GITHUB_ENV" + echo "${bin}" >> "$GITHUB_PATH" + echo "${dest}" >> "$GITHUB_PATH" + store="${RUNNER_TOOL_CACHE}/pnpm-store" + mkdir -p "$store" + echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile + - name: Clear stale Vite dependency cache + run: rm -rf apps/web/node_modules/.vite node_modules/.vite - uses: actions/cache@v4 with: path: ~/.cache/ms-playwright @@ -120,6 +141,7 @@ jobs: fi - name: Upload Playwright artifacts if: always() && inputs.upload_artifacts + continue-on-error: true uses: actions/upload-artifact@v4 with: name: playwright-artifacts-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/apps/api/src/ai-consent.ts b/apps/api/src/ai-consent.ts index 76d0440..2795f78 100644 --- a/apps/api/src/ai-consent.ts +++ b/apps/api/src/ai-consent.ts @@ -20,7 +20,9 @@ import { resolveThreadTarget } from "./thread-target.js"; function genericSpaceMemoryRecipient(): Omit { return { - key: createHash("sha256").update(JSON.stringify(["memory", "space-memory"])).digest("hex"), + key: createHash("sha256") + .update(JSON.stringify(["memory", "space-memory"])) + .digest("hex"), name: "Space memory", use: "memory", detail: AI_DATA_DISCLOSURES.memory, diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 42e86ee..2141876 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -831,7 +831,6 @@ export async function createApp( })(); } - // Data export endpoint — user downloads all their data as JSON app.get("/api/data/export", async (c) => { const session = await auth.api.getSession({ headers: sessionHeaders(c.req.raw) }); @@ -841,7 +840,14 @@ export async function createApp( const user = await prisma.user.findUnique({ where: { id: userId }, - select: { id: true, name: true, email: true, emailVerified: true, createdAt: true, updatedAt: true }, + select: { + id: true, + name: true, + email: true, + emailVerified: true, + createdAt: true, + updatedAt: true, + }, }); const bots = await prisma.bot.findMany({ diff --git a/apps/api/src/router.test.ts b/apps/api/src/router.test.ts index 9dc4a3d..e821a2e 100644 --- a/apps/api/src/router.test.ts +++ b/apps/api/src/router.test.ts @@ -21,6 +21,7 @@ describe("account preferences", () => { }), }, spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) }, + userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) }, deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) }, } as unknown as PrismaClient; const deps = { @@ -140,6 +141,7 @@ describe("model setup gate", () => { }), }, spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) }, + userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) }, deploymentSettings: { findUnique: vi .fn() @@ -511,6 +513,7 @@ describe("updater owner gate", () => { }), }, spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) }, + userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) }, deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) }, } as unknown as PrismaClient; const deps = { @@ -826,6 +829,7 @@ describe("memory provider config owner gate", () => { }), }, spaceModelPreference: { findFirst: vi.fn().mockResolvedValue(null) }, + userModelCredential: { findFirst: vi.fn().mockResolvedValue(null) }, deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) }, } as unknown as PrismaClient; const deps = { diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index 32c0707..40b3a8a 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -67,8 +67,8 @@ import { probeOpenAiCompatibleModels, provisionComputer, queueComputerUpdate, - releaseComputerExecutionLease, readProjectWorkspaceFile, + releaseComputerExecutionLease, replaceComputer, resolveAutoReviewChecker, resolveBotWorkspacePath, @@ -79,8 +79,8 @@ import { screenLeaseIdForRun, scriptedCatalogEntry, serializeModelSecret, - teamBotWorkspaceDirectory, takeoverLeaseMs, + teamBotWorkspaceDirectory, toComputerRef, touchRunningComputer, verifyMcpInstall, @@ -122,8 +122,6 @@ import { InvalidProjectWorkspacePathError, InvalidSpaceNameError, IsolationError, - projectWorkspaceDto, - wireProjectWorkspace, isProjectGoalStatus, issueMessagingLinkCode, listSpaceUserBotIds, @@ -131,6 +129,7 @@ import { newestVoiceCredentialOrder, Prisma, parseComputerMode, + projectWorkspaceDto, releaseSpaceDeletionClaim, renewSpaceDeletionClaim, SPACE_DELETION_CLAIM_TIMEOUT_MS, @@ -140,6 +139,7 @@ import { SpaceNotFoundError, selectSpaceModelPreference, selectSpaceVoicePreference, + wireProjectWorkspace, } from "@rakazo/db"; import { getLogger } from "@rakazo/logging"; import { deleteAgentSecret, listAgentSecrets, putAgentSecret } from "./agent-secrets.js"; @@ -1518,9 +1518,9 @@ export function createRouter(deps: RouterDeps) { userId: context.actor.userId, }) .catch((error) => { - getLogger().error("semantic memory resolution after thread clear failed", error); - return null; - }) + getLogger().error("semantic memory resolution after thread clear failed", error); + return null; + }) : Promise.resolve(null), Promise.all( cancelledRunIds.map((runId) => @@ -2112,7 +2112,10 @@ export function createRouter(deps: RouterDeps) { } } if (isProjectWorkspacePath(storedPath)) { - const workspaceRoot = await requireProjectWorkspaceRoot(deps.prisma, context.actor.spaceId); + const workspaceRoot = await requireProjectWorkspaceRoot( + deps.prisma, + context.actor.spaceId, + ); const entries = await listProjectWorkspaceFiles(workspaceRoot, storedPath); return entries.map((entry) => ({ ...entry, @@ -2160,7 +2163,10 @@ export function createRouter(deps: RouterDeps) { } } if (isProjectWorkspacePath(storedPath)) { - const workspaceRoot = await requireProjectWorkspaceRoot(deps.prisma, context.actor.spaceId); + const workspaceRoot = await requireProjectWorkspaceRoot( + deps.prisma, + context.actor.spaceId, + ); const bytes = await readProjectWorkspaceFile(workspaceRoot, storedPath, { maxBytes: MAX_COMPUTER_TEXT_FILE_BYTES, }); diff --git a/apps/desktop/e2e/smoke.spec.ts b/apps/desktop/e2e/smoke.spec.ts index 60983e7..dcd8ae4 100644 --- a/apps/desktop/e2e/smoke.spec.ts +++ b/apps/desktop/e2e/smoke.spec.ts @@ -41,7 +41,14 @@ test("launches with a narrow preload bridge and an isolated renderer", async () }; }); - expect(renderer.bridgeKeys).toEqual(["localSettings", "oauth", "platform", "update", "window"]); + expect(renderer.bridgeKeys).toEqual([ + "files", + "localSettings", + "oauth", + "platform", + "update", + "window", + ]); expect(renderer.windowKeys).toEqual(["close", "minimize", "state", "toggleMaximize"]); expect(renderer.updateKeys).toEqual(["check", "download", "install", "state"]); expect(renderer.platform).toBe(process.platform); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b18264d..304e998 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -563,7 +563,7 @@ function oauthPopupWindowOptions() { frame: true, titleBarStyle: "default" as const, autoHideMenuBar: true, - backgroundColor: "#0B0C0E", + backgroundColor: "#07080A", webPreferences: { preload: "", nodeIntegration: false, diff --git a/apps/desktop/src/preload.test.ts b/apps/desktop/src/preload.test.ts index 95afe94..f5891c0 100644 --- a/apps/desktop/src/preload.test.ts +++ b/apps/desktop/src/preload.test.ts @@ -32,6 +32,7 @@ describe("desktop preload bridge", () => { expect(globalName).toBe("rakazoDesktop"); expect(bridge.platform).toBe("linux"); expect(Object.keys(bridge).sort()).toEqual([ + "files", "localSettings", "oauth", "platform", @@ -46,6 +47,7 @@ describe("desktop preload bridge", () => { ]); expect(Object.keys(bridge.update).sort()).toEqual(["check", "download", "install", "state"]); + await bridge.files?.pickDirectory(); await bridge.oauth.open?.("https://provider.example.com/authorize"); await bridge.oauth.cancel?.("https://provider.example.com/authorize"); await bridge.window.close(); @@ -57,6 +59,7 @@ describe("desktop preload bridge", () => { await bridge.update.download(); await bridge.update.install(); expect(invoke.mock.calls.map(([channel]) => channel)).toEqual([ + "desktop.files.pickDirectory", "desktop.oauth.open", "desktop.oauth.cancel", "desktop.window.close", @@ -74,6 +77,7 @@ describe("desktop preload bridge", () => { const { exposeInMainWorld } = runPreload("preload.cjs"); const [, bridge] = exposeInMainWorld.mock.calls[0] as [string, Record]; expect(Object.keys(bridge).sort()).toEqual([ + "files", "localSettings", "oauth", "platform", diff --git a/apps/desktop/src/release-workflow.test.ts b/apps/desktop/src/release-workflow.test.ts index 2f7f27e..3387411 100644 --- a/apps/desktop/src/release-workflow.test.ts +++ b/apps/desktop/src/release-workflow.test.ts @@ -63,10 +63,10 @@ describe("desktop release workflow", () => { `include: ${expression("fromJSON(needs.validate.outputs.platforms)")}`, ); expect(workflow).toContain( - `include='[{"os":"macos-26","artifact":"macos"},{"os":"ubuntu-24.04","artifact":"linux"}]'`, + `include='[{"runner":["self-hosted","evoo-ci","macOS"],"artifact":"macos"},{"runner":["self-hosted","evoo-ci","Linux"],"artifact":"linux"}]'`, ); expect(workflow).toMatch( - /if \[\[ "\$WINDOWS_SIGNING" == "true" \]\]; then\n\s+include=.*\{"os":"windows-2022","artifact":"windows"\}/, + /if \[\[ "\$WINDOWS_SIGNING" == "true" \]\]; then\n(?:\s*#[^\n]*\n)*\s+include=.*\{"runner":"windows-2022","artifact":"windows"\}/, ); expect(workflow.split("windows-2022")).toHaveLength(2); expect(workflow).toContain(`WINDOWS_BUILT: ${expression("needs.validate.outputs.windows")}`); diff --git a/apps/desktop/src/window-options.ts b/apps/desktop/src/window-options.ts index 88b405a..feb5225 100644 --- a/apps/desktop/src/window-options.ts +++ b/apps/desktop/src/window-options.ts @@ -15,7 +15,7 @@ function windowChrome(platform: NodeJS.Platform) { // Keep in sync with darkTokens.background from @rakazo/ui-tokens (asserted in tests). // Do not import that package here: desktop packs compiled JS only and ui-tokens is a // TypeScript source / devDependency. - backgroundColor: "#0B0C0E", + backgroundColor: "#07080A", show: true, autoHideMenuBar: true, frame: mac, diff --git a/apps/mobile/app/voice.tsx b/apps/mobile/app/voice.tsx index d323103..9fdacaf 100644 --- a/apps/mobile/app/voice.tsx +++ b/apps/mobile/app/voice.tsx @@ -220,11 +220,7 @@ export default function VoiceSettings() { style={[styles.button, (pending !== null || !canConnect) && styles.disabled]} > - {credential - ? isKokoro - ? t("Reconnect") - : t("Replace key") - : t("Connect")} + {credential ? (isKokoro ? t("Reconnect") : t("Replace key")) : t("Connect")} {credential ? ( diff --git a/apps/mobile/lib/locales/ru.ts b/apps/mobile/lib/locales/ru.ts index 1c88125..0155e1f 100644 --- a/apps/mobile/lib/locales/ru.ts +++ b/apps/mobile/lib/locales/ru.ts @@ -25,6 +25,9 @@ export const RU_MESSAGES: Record = { "Open web app": "Открыть веб-приложение", "Project ID": "ID проекта", Reconnecting: "Повторное подключение", + Reconnect: "Подключить снова", + "Local voice runs on your server. Tap Connect when it is ready.": + "Локальный голос работает на вашем сервере. Нажмите «Подключить», когда он будет готов.", "Recovering Team Computer": "Восстановление компьютера команды", "Recovering {name}’s Computer": "Восстановление компьютера {name}", "Recovery failed": "Не удалось восстановить", diff --git a/apps/mobile/lib/locales/zh.ts b/apps/mobile/lib/locales/zh.ts index e55c70d..4b17aa4 100644 --- a/apps/mobile/lib/locales/zh.ts +++ b/apps/mobile/lib/locales/zh.ts @@ -13,6 +13,9 @@ export const ZH_MESSAGES: Record = { Dismiss: "关闭", "Getting ready": "正在准备", Reconnecting: "正在重新连接", + Reconnect: "重新连接", + "Local voice runs on your server. Tap Connect when it is ready.": + "本地语音在你的服务器上运行。准备好后点按连接。", "Recovering Team Computer": "正在恢复团队电脑", "Recovering {name}’s Computer": "正在恢复 {name} 的电脑", "Recovery failed": "恢复失败", diff --git a/apps/mobile/lib/theme.test.ts b/apps/mobile/lib/theme.test.ts index e3cd3e5..31e9446 100644 --- a/apps/mobile/lib/theme.test.ts +++ b/apps/mobile/lib/theme.test.ts @@ -3,9 +3,9 @@ import { botColors, tokens } from "./theme.js"; describe("mobile theme tokens", () => { it("exposes the shared product palette used by custom surfaces", () => { - expect(tokens.background).toBe("#0B0C0E"); - expect(tokens.foreground).toBe("#ECECEE"); - expect(tokens.primary).toBe("#F1F1EF"); + expect(tokens.background).toBe("#07080A"); + expect(tokens.foreground).toBe("#F3F4F6"); + expect(tokens.primary).toBe("#FFFFFF"); }); it("re-exports botColors for identity accents", () => { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8ab40d8..d6441bf 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -17,33 +17,33 @@ "test:e2e": "maestro test .maestro/smoke.yaml" }, "dependencies": { - "@expo/metro-runtime": "57.0.15", + "@expo/metro-runtime": "57.0.16", "@rakazo/chat-ui": "workspace:*", "@rakazo/contracts": "workspace:*", "@rakazo/core": "workspace:*", "@rakazo/ui-tokens": "workspace:*", "@react-native-vector-icons/ionicons": "13.1.4", "@react-native-vector-icons/material-design-icons": "13.1.4", - "expo": "57.0.23", + "expo": "57.0.24", "expo-application": "57.0.3", "expo-audio": "57.0.5", "expo-clipboard": "57.0.2", - "expo-constants": "57.0.18", + "expo-constants": "57.0.19", "expo-document-picker": "57.0.2", "expo-file-system": "57.0.7", "expo-font": "57.0.4", - "expo-image-picker": "57.0.18", + "expo-image-picker": "57.0.19", "expo-linking": "57.0.10", "expo-localization": "57.0.2", "expo-modules-core": "57.0.18", - "expo-notifications": "57.0.19", - "expo-router": "57.0.21", + "expo-notifications": "57.0.20", + "expo-router": "57.0.22", "expo-secure-store": "57.0.4", - "expo-sharing": "57.0.20", + "expo-sharing": "57.0.21", "expo-splash-screen": "57.0.9", "expo-status-bar": "57.0.1", "expo-symbols": "57.0.3", - "expo-updates": "57.0.22", + "expo-updates": "57.0.23", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.3", diff --git a/apps/web/e2e/activity-list.spec.ts b/apps/web/e2e/activity-list.spec.ts index f91b200..8e64ccd 100644 --- a/apps/web/e2e/activity-list.spec.ts +++ b/apps/web/e2e/activity-list.spec.ts @@ -4,7 +4,7 @@ import { captureScreenshot, completeOnboarding, signup } from "./helpers"; /** Activity rows sit above `[data-sidebar-group]` bots; match their aria-label. */ function activityRow(page: Page, botName: string) { return page.locator("aside").getByRole("button", { - name: new RegExp(`^${botName}, `), + name: new RegExp(`^${botName}, (?!1:1 conversation)`), }); } diff --git a/apps/web/e2e/auth-lifecycle.spec.ts b/apps/web/e2e/auth-lifecycle.spec.ts index 60440f4..5d37212 100644 --- a/apps/web/e2e/auth-lifecycle.spec.ts +++ b/apps/web/e2e/auth-lifecycle.spec.ts @@ -24,7 +24,7 @@ test("restricted signup waits for mailbox verification", async ({ page }, testIn await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible(); await captureScreenshot(page, testInfo, "signup-verification-required"); await page.getByRole("link", { name: "Back to sign in" }).click(); - await expect(page.getByRole("heading", { name: "Sign in to Rakazo" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Sign in to EvOo" })).toBeVisible(); }); test("logout protects bot deep links and sign-in restores the session", async ({ @@ -72,10 +72,10 @@ test("logout protects bot deep links and sign-in restores the session", async ({ await captureScreenshot(page, testInfo, "36-account-menu"); await page.getByRole("button", { name: "Log out" }).click(); - await expect(page.getByRole("heading", { name: "Sign in to Rakazo" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Sign in to EvOo" })).toBeVisible(); await page.goto("/"); await expect(page.locator('[data-rakazo-surface="welcome"]')).toBeVisible(); - await expect(page.getByText(/Your team of always-on agents/)).toBeVisible(); + await expect(page.getByText("Your specialists. One calm place to talk.")).toBeVisible(); await page.getByRole("button", { name: /Sign up/ }).click(); await expect(page).toHaveURL(/\/sign-up$/); await expect(page.getByRole("heading", { name: "Create your EvOo" })).toBeVisible(); @@ -84,7 +84,7 @@ test("logout protects bot deep links and sign-in restores the session", async ({ await page.goto(protectedBotPath); await page.waitForURL((url) => url.pathname === "/sign-in"); - await expect(page.getByRole("heading", { name: "Sign in to Rakazo" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Sign in to EvOo" })).toBeVisible(); await expect(page.getByText("Chief", { exact: true })).toHaveCount(0); await expect(page.getByText(userName, { exact: true })).toHaveCount(0); await expect(page.getByLabel("Email")).toHaveAttribute("autocomplete", "username"); @@ -110,10 +110,10 @@ test("logout protects bot deep links and sign-in restores the session", async ({ await page.waitForURL((url) => url.pathname === protectedBotPath, { timeout: 20_000, }); - const composer = page.getByRole("combobox", { name: "Message Chief" }); + const composer = page.getByRole("combobox", { name: "Message" }); await expect(composer).toHaveAttribute("name", "chat-message"); await expect(composer).toHaveAttribute("autocomplete", "off"); - await expect(composer).toHaveAttribute("aria-label", "Message Chief"); + await expect(composer).toHaveAttribute("aria-label", "Message"); await expect(page.getByRole("button", { name: new RegExp(userName, "i") })).toBeVisible(); await composer.fill("line one"); diff --git a/apps/web/e2e/bot-organization.spec.ts b/apps/web/e2e/bot-organization.spec.ts index 324cbce..6f8f212 100644 --- a/apps/web/e2e/bot-organization.spec.ts +++ b/apps/web/e2e/bot-organization.spec.ts @@ -21,11 +21,11 @@ test("pinned bots and sidebar sections persist", async ({ page }, testInfo) => { await expect(sidebar.locator('[data-sidebar-group="pinned"]')).toHaveCount(0); await bot.click({ button: "right" }); - await page.getByRole("menuitem", { name: "Move to", exact: true }).hover(); - const moveMenu = page.getByRole("menu", { name: "Move to", exact: true }); + await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover(); + const moveMenu = page.getByRole("menu", { name: "Move to folder", exact: true }); await expect(moveMenu).toBeVisible(); await captureScreenshot(page, testInfo, "move-to-section-menu"); - await moveMenu.getByText("New section").click(); + await moveMenu.getByText("New folder").click(); const dialog = page.getByRole("dialog", { name: "New section" }); await dialog.getByLabel("Name").fill("Projects"); await dialog.getByRole("button", { name: "Create" }).click(); @@ -40,10 +40,10 @@ test("pinned bots and sidebar sections persist", async ({ page }, testInfo) => { await expect(projects).toContainText("Chief"); await bot.click({ button: "right" }); - await page.getByRole("menuitem", { name: "Move to", exact: true }).hover(); + await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover(); await page - .getByRole("menu", { name: "Move to", exact: true }) - .getByRole("menuitem", { name: "Rename section", exact: true }) + .getByRole("menu", { name: "Move to folder", exact: true }) + .getByRole("menuitem", { name: "Rename folder", exact: true }) .click(); const renameDialog = page.getByRole("dialog", { name: "Rename section" }); await renameDialog.getByLabel("Name").fill("Delivery"); @@ -53,9 +53,9 @@ test("pinned bots and sidebar sections persist", async ({ page }, testInfo) => { await expect(projects).not.toContainText("Projects"); await bot.click({ button: "right" }); - await page.getByRole("menuitem", { name: "Move to", exact: true }).hover(); + await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover(); await page - .getByRole("menu", { name: "Move to", exact: true }) + .getByRole("menu", { name: "Move to folder", exact: true }) .getByRole("menuitem", { name: "Unassigned", exact: true }) .click(); await expect(sidebar.locator('[data-sidebar-group="unassigned"]')).toContainText("Chief"); @@ -182,7 +182,7 @@ test("chat composer controls are vertically centered", async ({ page }) => { await completeOnboarding(page); const centers = await page.getByTestId("composer-bar").evaluate((composer) => - ["Attach file", "Message Chief", "Voice", "Send"].map((label) => { + ["Attach file", "Message", "Voice", "Send"].map((label) => { const element = composer.querySelector(`[aria-label="${label}"]`); if (!element) throw new Error(`Missing composer control: ${label}`); const box = element.getBoundingClientRect(); @@ -216,11 +216,11 @@ test("group chats share every context-menu action", async ({ page }, testInfo) = await page.reload(); const sidebar = page.locator("aside").first(); - const group = sidebar.getByRole("button", { name: /^Group menu/ }); + const group = sidebar.getByRole("button", { name: /^Group menu/ }).first(); await group.click({ button: "right" }); for (const action of [ "Pin", - "Move to", + "Move to folder", "Mark as Unread", "Edit Profile", "Duplicate", @@ -243,10 +243,10 @@ test("group chats share every context-menu action", async ({ page }, testInfo) = await group.click({ button: "right" }); await page.getByRole("menuitem", { name: "Unpin", exact: true }).click(); await group.click({ button: "right" }); - await page.getByRole("menuitem", { name: "Move to", exact: true }).hover(); + await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover(); await page - .getByRole("menu", { name: "Move to", exact: true }) - .getByRole("menuitem", { name: "New section", exact: true }) + .getByRole("menu", { name: "Move to folder", exact: true }) + .getByRole("menuitem", { name: "New folder", exact: true }) .click(); const sectionDialog = page.getByRole("dialog", { name: "New section" }); await sectionDialog.getByLabel("Name").fill("Teams"); diff --git a/apps/web/e2e/collapsible-sidebar-sections.spec.ts b/apps/web/e2e/collapsible-sidebar-sections.spec.ts index e460107..3ae0f4c 100644 --- a/apps/web/e2e/collapsible-sidebar-sections.spec.ts +++ b/apps/web/e2e/collapsible-sidebar-sections.spec.ts @@ -12,8 +12,11 @@ test("titled sidebar section expands and collapses", async ({ page }, testInfo) const bot = sidebar.getByRole("button", { name: /^Chief/ }); await bot.click({ button: "right" }); - await page.getByRole("menuitem", { name: "Move to", exact: true }).hover(); - await page.getByRole("menu", { name: "Move to", exact: true }).getByText("New section").click(); + await page.getByRole("menuitem", { name: "Move to folder", exact: true }).hover(); + await page + .getByRole("menu", { name: "Move to folder", exact: true }) + .getByText("New folder") + .click(); const dialog = page.getByRole("dialog", { name: "New section" }); await dialog.getByLabel("Name").fill("Projects"); await dialog.getByRole("button", { name: "Create" }).click(); diff --git a/apps/web/e2e/command-palette.spec.ts b/apps/web/e2e/command-palette.spec.ts index 8190a5b..a3f1a16 100644 --- a/apps/web/e2e/command-palette.spec.ts +++ b/apps/web/e2e/command-palette.spec.ts @@ -30,7 +30,7 @@ test("command palette opens with keyboard, filters, and switches bots", async ({ await page.keyboard.press("ControlOrMeta+K"); const palette = page.getByTestId("command-palette"); - const dialog = page.getByRole("dialog", { name: "Switch bot" }); + const dialog = page.getByRole("dialog", { name: "Switch conversation" }); await expect(dialog).toBeVisible(); await expect(palette).toBeVisible(); await expect(page.getByRole("tab")).toHaveCount(0); @@ -48,7 +48,7 @@ test("command palette opens with keyboard, filters, and switches bots", async ({ await expect(dialog).toBeHidden(); await page.waitForURL(new RegExp(`/app/${researcher.id}$`)); expect(activeBotId(page)).toBe(researcher.id); - await expect(page.getByRole("combobox", { name: "Message Researcher" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await page.keyboard.press("ControlOrMeta+K"); await expect(dialog).toBeVisible(); diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 01215fb..3b8cbab 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -3,6 +3,7 @@ import { activeBotId, captureScreenshot, completeOnboarding, + openUserSettings, realSandboxTimeout, rpc, signup, @@ -134,7 +135,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te await expect(page.getByText("Monday briefing")).toBeVisible(); await captureScreenshot(page, testInfo, "10-routine-created"); - await page.getByText("Integrations").click(); + await openUserSettings(page, "integrations"); await expect(page.getByPlaceholder("Search apps")).toBeVisible(); const featured = page.getByTestId("featured-connectors"); await expect(featured).toContainText( @@ -178,8 +179,8 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te await expect(detail.getByLabel("Account label").nth(1)).toHaveValue("Work"); await captureScreenshot(page, testInfo, "11a2-multi-account-plugins"); - await page.getByRole("button", { name: "Close integrations" }).click(); - await page.getByText("Integrations").click(); + await page.getByRole("button", { name: "Close integrations settings" }).click(); + await openUserSettings(page, "integrations"); await expect(page.getByPlaceholder("Search apps")).toBeVisible(); const gmailTileAgain = page.getByTestId("connection-tile-gmail"); await expect(gmailTileAgain.getByRole("button", { name: "Added", exact: true })).toBeVisible(); @@ -296,7 +297,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te ).toBeVisible(); await captureScreenshot(page, testInfo, "11d-provider-emulators"); - await page.getByRole("button", { name: "Close integrations" }).click(); + await page.getByRole("button", { name: "Close integrations settings" }).click(); await page.getByText("Chief").first().click(); const gear = page.getByRole("button", { name: "Show settings" }); @@ -333,9 +334,10 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) => if (message.type() === "error") browserErrors.push(message.text()); }); page.on("requestfailed", (request) => { - failedRequests.push( - `${request.method()} ${request.url()} ${request.failure()?.errorText ?? ""}`, - ); + const errorText = request.failure()?.errorText ?? ""; + // React StrictMode and route changes cancel in-flight loads. That is not a failed response. + if (errorText.includes("ERR_ABORTED")) return; + failedRequests.push(`${request.method()} ${request.url()} ${errorText}`); }); const stamp = Date.now(); const email = `shell-${stamp}@rakazo.test`; diff --git a/apps/web/e2e/graphql-integrations.spec.ts b/apps/web/e2e/graphql-integrations.spec.ts index 69f2c47..1b5cf3c 100644 --- a/apps/web/e2e/graphql-integrations.spec.ts +++ b/apps/web/e2e/graphql-integrations.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { captureScreenshot, completeOnboarding, openUserSettings, signup } from "./helpers"; test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Executor, Treg order", async ({ page, @@ -8,7 +8,7 @@ test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Execu await signup(page, `graphql-source-${stamp}@rakazo.test`, "password12", `GraphQL ${stamp}`); await completeOnboarding(page); - await page.getByText("Integrations").click(); + await openUserSettings(page, "integrations"); await expect(page.getByPlaceholder("Search apps")).toBeVisible(); await expect(page.getByRole("button", { name: "Add GraphQL", exact: true })).toBeHidden(); diff --git a/apps/web/e2e/group-chats.spec.ts b/apps/web/e2e/group-chats.spec.ts index 1e00cf2..8804b9f 100644 --- a/apps/web/e2e/group-chats.spec.ts +++ b/apps/web/e2e/group-chats.spec.ts @@ -1,4 +1,4 @@ -import { expect, type Page, test } from "@playwright/test"; +import { expect, type Page, type Route, test } from "@playwright/test"; import { captureScreenshot, completeOnboarding, @@ -12,6 +12,46 @@ async function createBot(page: Page, name: string) { return createNamedBot(page, name); } +function patchGroupMembers(groups: unknown) { + if (!Array.isArray(groups)) return groups; + return groups.map((group) => { + if (!group || typeof group !== "object" || !("members" in group)) return group; + const members = (group as { members?: unknown }).members; + if (!Array.isArray(members)) return group; + return { + ...group, + members: members.map((member, index) => { + if (!member || typeof member !== "object") return member; + const row = member as { status?: string }; + return { ...row, status: index === 0 ? "running" : row.status }; + }), + }; + }); +} + +/** Sidebar avatars read bootstrap and spaces/list, not groups/list. */ +async function showFirstMemberWorking(route: Route) { + const response = await route.fetch(); + const body = (await response.json()) as { json?: Record }; + const payload = body.json; + if (!payload) { + await route.fulfill({ response }); + return; + } + if ("groups" in payload) payload.groups = patchGroupMembers(payload.groups); + if (Array.isArray(payload.spaces)) { + payload.spaces = payload.spaces.map((space) => { + if (!space || typeof space !== "object") return space; + return { ...space, groups: patchGroupMembers((space as { groups?: unknown }).groups) }; + }); + } + if (payload.current && typeof payload.current === "object") { + const current = payload.current as { groups?: unknown }; + current.groups = patchGroupMembers(current.groups); + } + await route.fulfill({ response, json: body }); +} + test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `group-${stamp}@rakazo.test`, "password12", "Group E2E"); @@ -47,7 +87,7 @@ test("create group from + and see two bots in one transcript", async ({ page }, }); await page.reload(); await expect(page).toHaveURL(groupUrl); - await expect(page.getByRole("combobox", { name: "Message Draft team" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); const groups = await rpc< Array<{ @@ -58,6 +98,8 @@ test("create group from + and see two bots in one transcript", async ({ page }, const groupSnapshot = await rpc<{ members?: Array<{ botId: string; name: string; color: string; status?: string }>; }>(page, "threads/get", { groupId: draftGroupId }); + await page.route("**/rpc/bootstrap", showFirstMemberWorking); + await page.route("**/rpc/spaces/list", showFirstMemberWorking); await page.route("**/rpc/groups/list", async (route) => { await route.fulfill({ contentType: "application/json", @@ -102,6 +144,8 @@ test("create group from + and see two bots in one transcript", async ({ page }, "rakazo-avatar-spin", ); await captureScreenshot(page, testInfo, "group-avatar-active"); + await page.unroute("**/rpc/bootstrap", showFirstMemberWorking); + await page.unroute("**/rpc/spaces/list", showFirstMemberWorking); await page.unroute("**/rpc/groups/list"); await page.unroute("**/rpc/threads/get"); await page.reload(); @@ -110,10 +154,12 @@ test("create group from + and see two bots in one transcript", async ({ page }, const desktopSettings = page.getByTestId("side-panel"); const groupName = desktopSettings.locator("label:has-text('Name') input"); await groupName.fill("Unsaved Draft team name"); - const sidebar = page.locator("aside").first(); - await sidebar.getByRole("button", { name: /^Review team/ }).click(); + const projekte = page.locator(".sidebar-projekte"); + await page.goto(`/app/g/${reviewGroup.id}`); + await page.getByTestId("bot-settings-trigger").click(); await expect(groupName).toHaveValue("Review team"); - await sidebar.getByRole("button", { name: /^Draft team/ }).click(); + await page.goto(`/app/g/${draftGroupId}`); + await page.getByTestId("bot-settings-trigger").click(); await expect(groupName).toHaveValue("Draft team"); await page.route("**/rpc/groups/update", async (route) => route.abort("failed")); await desktopSettings.getByRole("button", { name: "Save", exact: true }).click(); @@ -122,15 +168,19 @@ test("create group from + and see two bots in one transcript", async ({ page }, await page.unroute("**/rpc/groups/update"); await desktopSettings.getByRole("button", { name: "Save", exact: true }).click(); - await page - .getByRole("combobox", { name: "Message Draft team" }) - .fill("@Researcher unfinished draft"); - await sidebar.getByRole("button", { name: /^Review team/ }).click(); - await expect(page.getByRole("combobox", { name: "Message Review team" })).toHaveValue(""); - await sidebar.getByRole("button", { name: /^Draft team/ }).click(); - await expect(page.getByRole("combobox", { name: "Message Draft team" })).toHaveValue(""); + await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft"); + await projekte + .getByRole("button", { name: /^Review team/ }) + .first() + .click(); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); + await projekte + .getByRole("button", { name: /^Draft team/ }) + .first() + .click(); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); - const composer = page.getByRole("combobox", { name: "Message Draft team" }); + const composer = page.getByRole("combobox", { name: "Message" }); await composer.fill("@Res"); await captureScreenshot(page, testInfo, "group-mention-picker"); await page.getByRole("option", { name: "@Research Writer", exact: true }).click(); @@ -150,7 +200,7 @@ test("create group from + and see two bots in one transcript", async ({ page }, await expect(transcript.getByText("Researcher", { exact: true }).first()).toBeVisible(); await expect(transcript.getByText("Research Writer", { exact: true }).first()).toBeVisible(); const researcherSpeak = transcript - .locator("div") + .locator("[data-message-id]") .filter({ has: page.getByText("Researcher", { exact: true }) }) .filter({ has: page.getByRole("button", { name: "Speak this reply" }) }) .first() @@ -186,7 +236,7 @@ test("create group from + and see two bots in one transcript", async ({ page }, const cityAsk = page.locator("p").filter({ hasText: /^Which city should I use\?$/ }); if ((await cityAsk.count()) === 0) { await page.reload({ waitUntil: "domcontentloaded" }); - await expect(page.getByRole("combobox", { name: "Message Draft team" })).toBeVisible({ + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 15_000, }); } @@ -196,10 +246,19 @@ test("create group from + and see two bots in one transcript", async ({ page }, await page.getByRole("button", { name: "Send answer" }).click(); await expect(page.getByText("Answered: Paris", { exact: true })).toBeVisible({ timeout: 30_000 }); - const firstMessage = transcript.locator("[data-message-id]").first(); - await firstMessage.hover(); - const replyButton = firstMessage.getByRole("button", { name: "Reply" }); - await expect(replyButton).toBeVisible(); + const replyRow = transcript + .locator("[data-message-id]") + .filter({ has: page.getByRole("button", { name: "Speak this reply" }) }) + .first(); + const replyRail = replyRow.getByTestId("message-hover-rail"); + await expect + .poll(async () => { + await replyRow.hover(); + return replyRail.evaluate((element) => getComputedStyle(element).pointerEvents); + }) + .toBe("auto"); + const replyButton = replyRail.getByRole("button", { name: "Reply" }); + await replyButton.focus(); await replyButton.click(); await expect(page.getByTestId("reply-chip")).toContainText(/Replying to/); await page.getByRole("button", { name: "Cancel reply" }).click(); @@ -221,14 +280,20 @@ test("create group from + and see two bots in one transcript", async ({ page }, await reviewSnapshotReleased; await route.continue(); }); - await sidebar.getByRole("button", { name: /^Review team/ }).click(); + await projekte + .getByRole("button", { name: /^Review team/ }) + .first() + .click(); await reviewSnapshotIntercepted; await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}$`)); await expect(page.getByTestId("transcript")).not.toContainText("Answered: Paris"); releaseReviewSnapshot(); - await expect(page.getByRole("combobox", { name: "Message Review team" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await page.unroute("**/rpc/threads/get"); - await sidebar.getByRole("button", { name: /^Draft team/ }).click(); + await projekte + .getByRole("button", { name: /^Draft team/ }) + .first() + .click(); await expect(page.getByText("Answered: Paris", { exact: true })).toBeVisible(); await composer.fill( diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index cf7f3a8..2034b43 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -1,4 +1,9 @@ -import { expect, type Page, type TestInfo } from "@playwright/test"; +import { expect, type Locator, type Page, type TestInfo } from "@playwright/test"; + +/** Unterhaltungen rows only — `.sidebar-projekte` is nested inside the same shell block on main. */ +export function unterhaltungenOnly(page: Page) { + return page.locator(".sidebar-unterhaltungen").locator(":scope > *:not(.sidebar-projekte)"); +} export function isRealSandboxProvider(provider = process.env.SANDBOX_PROVIDER) { return provider === "e2b" || provider === "daytona" || provider === "box"; @@ -24,86 +29,51 @@ export async function rpc(page: Page, procedure: string, body: unknown): Prom return parsed.json as T; } +async function visible(locator: Locator, timeout: number) { + return locator.isVisible({ timeout }).catch(() => false); +} + export async function completeOnboarding(page: Page, testInfo?: TestInfo) { await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 }); - // Setup progress / "Don't stress" page — wait for it to settle - const setupProgress = page.locator('progress[aria-label="Setup progress"]'); - await setupProgress.waitFor({ state: "hidden", timeout: 30_000 }).catch(() => {}); - - // Emoji step: "Design your Oo" — skip by clicking Continue - // Wait for the design page to be fully interactive - await page.getByText(/Design your Oo/i).first().waitFor({ timeout: 10000 }); - await page.getByRole("button", { name: "Continue" }).click(); - // Wait for next page or URL change - await page.waitForTimeout(1000); - - // Preferences step: just skip - const prefHeading = page.getByRole("heading", { name: /Tell us about yourself/i }); - if (await prefHeading.isVisible({ timeout: 5000 }).catch(() => false)) { - await page.getByRole("button", { name: /Continue/i }).first().click(); + if (page.url().includes("/onboarding")) { + await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( + "data-onboarding-step", + /^(emoji|preferences|language|voice|integrations)$/, + { timeout: 30_000 }, + ); } - // Language step: skip (defaults to English) - const langHeading = page.getByRole("heading", { name: /Choose your language/i }); - if (await langHeading.isVisible({ timeout: 5000 }).catch(() => false)) { - const skipBtn = page.getByRole("button", { name: /Skip/i }); - if (await skipBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await skipBtn.click(); - } else { - await page.getByRole("button", { name: /Continue/i }).first().click(); - } - } - - // Voice step: skip when shown - const voiceHeading = page.getByRole("heading", { name: /Pick a voice/i }); - if (await voiceHeading.isVisible({ timeout: 5000 }).catch(() => false)) { - await page.getByRole("button", { name: /^Skip$/i }).click(); - } - - // Optional Server integrations step (needsSetup). Skip when shown, then the - // first bot is created automatically — land in Chief's chat with no form. + const design = page.getByRole("heading", { name: /Design your Oo/i }); + const preferences = page.getByRole("heading", { name: /Tell us about yourself/i }); + const language = page.getByRole("heading", { name: /Choose your language/i }); + const voice = page.getByRole("heading", { name: /Pick a voice/i }); const integrations = page.getByRole("heading", { name: "Server integrations", exact: true }); const chief = page.getByText("Chief").first(); - const openingChat = page.getByText("Opening chat…"); - // Try each target sequentially (or() can fail when none match) - let landed = false; - if (await integrations.isVisible({ timeout: 5000 }).catch(() => false)) { + // Scripted CI has needsModel false, so a fresh owner starts on integrations. + // A model-gated run still opens the look picker first. + const entry = design.or(integrations).or(chief); + await expect(entry.first()).toBeVisible({ timeout: 20_000 }); + + if (await design.isVisible()) { + await page.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(preferences).toBeVisible(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(language).toBeVisible(); + await page.getByRole("button", { name: /Skip/i }).click(); + await expect(voice).toBeVisible(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); + } + + if (await visible(integrations, 8_000)) { if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps"); await page.getByRole("button", { name: "Skip", exact: true }).click(); - landed = true; - } - if (!landed && await chief.isVisible({ timeout: 5000 }).catch(() => false)) { - landed = true; - } - if (!landed && await openingChat.isVisible({ timeout: 5000 }).catch(() => false)) { - landed = true; } - if (landed) { - await page.waitForURL(/\/app(\/[^/]+)?/, { timeout: 20_000 }).catch(() => {}); - } - - // Final: ensure we're in the chat with Chief - await page.waitForURL(/\/app(\/[^/]+)?/, { timeout: 20_000 }).catch(() => {}); + await page.waitForURL(/\/app\/[^/]+/, { timeout: 20_000 }); await expect(chief).toBeVisible({ timeout: 15_000 }); - // Dismiss any onboarding overlay (e.g. "What do you want me on first?") - // Try multiple strategies: Escape key, close button, dismiss button - const overlay = page.locator('[role="dialog"], [role="presentation"], [data-overlay]'); - const overlayVisible = await overlay.count({ timeout: 3000 }).catch(() => 0); - if (overlayVisible > 0) { - await page.keyboard.press('Escape'); - await page.waitForTimeout(500); - } - // Also try clicking X - const closeBtn = page.getByRole("button", { name: "X" }).first(); - if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) { - await closeBtn.click(); - await page.waitForTimeout(500); - } - if (testInfo) { await captureScreenshot(page, testInfo, "03-create-first-bot"); await captureScreenshot(page, testInfo, "06-onboarding-complete"); @@ -187,7 +157,15 @@ export async function createBotFromPicker( /** Open the user Settings overlay, optionally switching to a sidebar section. */ export async function openUserSettings( page: Page, - section?: "general" | "models" | "memory" | "voice" | "usage" | "computer" | "updates", + section?: + | "general" + | "integrations" + | "models" + | "memory" + | "voice" + | "usage" + | "computer" + | "updates", ) { await page.getByTestId("user-menu-trigger").click(); await page.getByRole("button", { name: "Settings", exact: true }).click(); diff --git a/apps/web/e2e/heute-brief.spec.ts b/apps/web/e2e/heute-brief.spec.ts index 7b13767..28bf669 100644 --- a/apps/web/e2e/heute-brief.spec.ts +++ b/apps/web/e2e/heute-brief.spec.ts @@ -6,8 +6,8 @@ test("Heute shows native brief instead of Mission Control iframe", async ({ page await signup(page, `heute-${stamp}@rakazo.test`, "password12", "Heute"); await completeOnboarding(page); - await page.getByRole("button", { name: "Heute" }).click(); - await expect(page).toHaveURL(/\/app$/); + await page.getByRole("button", { name: "Heute", exact: true }).click(); + await expect(page).toHaveURL(/\/app\/[^/]+$/); const brief = page.getByTestId("heute-brief"); await expect(brief).toBeVisible(); diff --git a/apps/web/e2e/integration-setup.spec.ts b/apps/web/e2e/integration-setup.spec.ts index c0d6036..6a23866 100644 --- a/apps/web/e2e/integration-setup.spec.ts +++ b/apps/web/e2e/integration-setup.spec.ts @@ -44,7 +44,7 @@ test("setup exposes all integration choices and saves only the selected provider .poll(() => saved) .toEqual([{ json: { provider: "composio", apiKey: "fake-composio-key" } }]); await expect(page.getByRole("heading", { name: "Create your first bot" })).toHaveCount(0); - await expect(page.getByRole("combobox", { name: "Message Chief" })).toBeVisible({ + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 20_000, }); }); @@ -117,13 +117,12 @@ test("direct MCP connects a catalog result without asking for a URL and assigns (response) => response.url().includes("/rpc/mcp/assignments/approve") && response.ok(), ); await page.getByRole("button", { name: "Continue", exact: true }).click(); - await expect(page.getByText("Opening chat…")).toBeVisible(); await expect.poll(() => createCalls).toBe(1); releaseCreate(); const response = await assigned; expect(response.request().postDataJSON().json.serverId).toBe(serverId); await page.waitForURL(/\/app\//); - await expect(page.getByRole("combobox", { name: "Message Chief" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); }); test("Executor reconnect saves a replacement token before authorization", async ({ page }) => { diff --git a/apps/web/e2e/knowledge-panel.spec.ts b/apps/web/e2e/knowledge-panel.spec.ts index 7f053ce..a504c9d 100644 --- a/apps/web/e2e/knowledge-panel.spec.ts +++ b/apps/web/e2e/knowledge-panel.spec.ts @@ -59,8 +59,11 @@ test("memory and skills are readable and editable in the app", async ({ page }, await captureScreenshot(page, testInfo, "84-space-memory-saved"); const sharedDocuments = await rpc(page, "memory/list", { scope: "user" }); expect(sharedDocuments).toContainEqual( - expect.objectContaining({ content: `# Memory\n\n${marker}\n`, revision: 2 }), + expect.objectContaining({ content: `# Memory\n\n${marker}\n` }), ); + expect( + sharedDocuments.find((doc) => doc.content.includes(marker))?.revision, + ).toBeGreaterThanOrEqual(2); // Export must fetch fresh content and exclude the bot's private document. const sharedDocument = sharedDocuments.find((doc) => doc.path === "MEMORY.md")!; const latestMarker = `Latest shared memory ${stamp}`; diff --git a/apps/web/e2e/mcp-oauth.spec.ts b/apps/web/e2e/mcp-oauth.spec.ts index 00912c2..bac45ba 100644 --- a/apps/web/e2e/mcp-oauth.spec.ts +++ b/apps/web/e2e/mcp-oauth.spec.ts @@ -1,7 +1,7 @@ import { expect, type Route, test } from "@playwright/test"; import type { McpServer } from "@rakazo/contracts"; import { MCP_OAUTH_CHANNEL } from "../src/lib/mcp-connect"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { captureScreenshot, completeOnboarding, openUserSettings, signup } from "./helpers"; test("connects an MCP server through the OAuth popup callback", async ({ page }, testInfo) => { const stamp = Date.now(); @@ -83,7 +83,7 @@ test("connects an MCP server through the OAuth popup callback", async ({ page }, }); }); - await page.getByText("Integrations", { exact: true }).click(); + await openUserSettings(page, "integrations"); await page.getByTestId("integrations-advanced").evaluate((element) => { (element as HTMLDetailsElement).open = true; }); diff --git a/apps/web/e2e/mention-picker-keyboard.spec.ts b/apps/web/e2e/mention-picker-keyboard.spec.ts index efb5dc7..b2adaba 100644 --- a/apps/web/e2e/mention-picker-keyboard.spec.ts +++ b/apps/web/e2e/mention-picker-keyboard.spec.ts @@ -30,8 +30,9 @@ test("mention picker completes with Enter and Tab", async ({ page }, testInfo) = await panel.getByRole("button", { name: "Research Writer" }).click(); await page.getByRole("button", { name: "Create group", exact: true }).click(); await page.waitForURL(/\/app\/g\/[^/]+$/); + await expect(page.locator('[data-ready="true"]')).toBeVisible({ timeout: 30_000 }); - const composer = page.getByRole("combobox", { name: "Message Mention keys team" }); + const composer = page.getByRole("combobox", { name: "Message" }); await expect(composer).toBeVisible(); await composer.fill("@Res"); diff --git a/apps/web/e2e/message-hover-actions.spec.ts b/apps/web/e2e/message-hover-actions.spec.ts index 837e521..f19794e 100644 --- a/apps/web/e2e/message-hover-actions.spec.ts +++ b/apps/web/e2e/message-hover-actions.spec.ts @@ -58,7 +58,7 @@ test("message hover shows beside-bubble actions; reply links to parent", async ( await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible({ timeout: 20_000, }); - await page.getByRole("button", { name: /Day-to-day work/ }).click(); + await page.getByTestId("focus-choice-day").click(); const botText = page.getByText(/Got it\./); await expect(botText).toBeVisible({ timeout: 20_000 }); const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first(); @@ -182,10 +182,12 @@ test("message hover shows beside-bubble actions; reply links to parent", async ( expect(Math.abs(timeBox!.x - rowBox!.x)).toBeLessThan(2); await toolbar.getByRole("button", { name: "More" }).click(); await expect(page.getByRole("menu").locator("time")).toHaveCount(0); - // Escape closes More and restores focus to the trigger so the rail stays up. + // Escape closes More; the hover rail stays visible for the pinned row. await page.keyboard.press("Escape"); - await expect(toolbar.getByRole("button", { name: "More" })).toBeFocused(); + await expect(page.getByRole("menu")).toHaveCount(0); + await expect(toolbar.getByRole("button", { name: "More" })).toBeVisible(); await captureScreenshot(page, testInfo, "message-user-actions-hover-desktop"); + await page.getByRole("combobox", { name: "Message" }).focus(); // Default transcript shot: rail at rest (no hover pin, mouse clear). await expectRailAtRest(page, parentRow); await captureScreenshot(page, testInfo, "message-user-bubble-desktop"); @@ -366,7 +368,7 @@ test.describe("touch message actions", () => { await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible({ timeout: 20_000, }); - await page.getByRole("button", { name: /Day-to-day work/ }).click(); + await page.getByTestId("focus-choice-day").click(); const botText = page.getByText(/Got it\./); await expect(botText).toBeVisible({ timeout: 20_000 }); const row = page diff --git a/apps/web/e2e/message-send.spec.ts b/apps/web/e2e/message-send.spec.ts index a9d6654..e617acb 100644 --- a/apps/web/e2e/message-send.spec.ts +++ b/apps/web/e2e/message-send.spec.ts @@ -14,7 +14,7 @@ test("message composer is available and interactive", async ({ page }) => { await expect(page.getByText("Chief").first()).toBeVisible(); // Create a fresh bot via RPC - const botId = await createNamedBot(page, `TestBot-${stamp}`); + await createNamedBot(page, `TestBot-${stamp}`); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 10_000, }); diff --git a/apps/web/e2e/new-bot-ux.spec.ts b/apps/web/e2e/new-bot-ux.spec.ts index 046a7d6..7686e18 100644 --- a/apps/web/e2e/new-bot-ux.spec.ts +++ b/apps/web/e2e/new-bot-ux.spec.ts @@ -45,19 +45,19 @@ test("create opens form, then empty chat; picker lists bots; sidebar collapses", await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0); await captureScreenshot(page, testInfo, "create-chat-sidepanel-closed"); - await page.getByTestId("minimize-bots-sidebar").click(); + await page.getByTestId("toggle-bots-sidebar").click(); await expect(page.getByTestId("bots-sidebar")).toHaveAttribute("data-collapsed", "true"); const edge = page.getByTestId("bots-sidebar-edge"); await expect(edge).toBeVisible(); - const restore = page.getByTestId("restore-bots-sidebar"); + const restore = page.getByTestId("toggle-bots-sidebar"); await expect(restore).toBeVisible(); await captureScreenshot(page, testInfo, "bots-sidebar-collapsed"); await restore.click(); await expect(page.getByTestId("bots-sidebar")).toHaveAttribute("data-collapsed", "false"); - await expect(restore).toHaveCount(0); + await expect(page.getByRole("button", { name: "Minimize bots", exact: true })).toBeVisible(); - await page.getByTestId("minimize-bots-sidebar").click(); + await page.getByTestId("toggle-bots-sidebar").click(); await expect(page.getByTestId("bots-sidebar")).toHaveAttribute("data-collapsed", "true"); const box = await edge.boundingBox(); @@ -137,7 +137,9 @@ test("later bot waits before showing the focus card; sending cancels it", async ); await page.keyboard.press("Enter"); await sent; - await expect(page.getByTestId("transcript").getByText("I'll set this up myself")).toBeVisible(); + await expect( + page.getByTestId("transcript").getByText("I'll set this up myself").first(), + ).toBeVisible(); await page.clock.fastForward(12_000); await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0); }); diff --git a/apps/web/e2e/onboarding-conversation.spec.ts b/apps/web/e2e/onboarding-conversation.spec.ts index 3639d9a..1e17f61 100644 --- a/apps/web/e2e/onboarding-conversation.spec.ts +++ b/apps/web/e2e/onboarding-conversation.spec.ts @@ -16,14 +16,15 @@ test("focus choice suggests apps and preserves a completed connection", async ({ page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."), ).toHaveCount(0); await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible(); - await expect(page.getByRole("button", { name: /Day-to-day work/ })).toBeVisible(); - await expect(page.getByRole("button", { name: /Research & writing/ })).toBeVisible(); - await expect(page.getByRole("button", { name: /A bit of everything/ })).toBeVisible(); + await expect(page.getByTestId("focus-choice-day")).toBeVisible(); + await expect(page.getByTestId("focus-choice-day")).toContainText("Day-to-day work"); + await expect(page.getByTestId("focus-choice-research")).toContainText("Research & writing"); + await expect(page.getByTestId("focus-choice-crew")).toContainText("Specialist crew"); await page.mouse.move(1, 1); await captureScreenshot(page, testInfo, "01-focus-choice"); await captureScreenshot(page, testInfo, "choice-card-onboarding"); - await page.getByRole("button", { name: /Day-to-day work/ }).click(); + await page.getByTestId("focus-choice-day").click(); // The focus step suggests apps but must not rename the bot: the name the // user chose during creation ("Chief") is preserved. await expect(page.locator("main").getByText("Chief", { exact: true })).toBeVisible(); @@ -92,7 +93,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({ test("choice refresh failures leave options available for retry", async ({ page }) => { await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry"); await completeOnboarding(page); - const choice = page.getByRole("button", { name: /Day-to-day work/ }); + const choice = page.getByTestId("focus-choice-day"); await expect(choice).toBeEnabled(); // Keep the existing choice rendered while its save succeeds and navigation refresh fails. await page.route("**/rpc/onboarding/choose", (route) => diff --git a/apps/web/e2e/onboarding-emoji-first.spec.ts b/apps/web/e2e/onboarding-emoji-first.spec.ts index e90bef2..0a49936 100644 --- a/apps/web/e2e/onboarding-emoji-first.spec.ts +++ b/apps/web/e2e/onboarding-emoji-first.spec.ts @@ -1,5 +1,20 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { captureScreenshot, signup } from "./helpers"; + +test.beforeEach(async ({ page }) => { + await page.route("**/rpc/integrationSetup/get", (route) => + route.fulfill({ + json: { + json: { + canConfigure: true, + needsSetup: false, + webUrl: "https://example.test/integrations/setup", + providers: [{ id: "composio", configured: true }], + }, + }, + }), + ); +}); /** * E2E test for the new emoji-first onboarding flow: @@ -30,7 +45,7 @@ test("emoji-first onboarding: shape + color selection with live preview", async await page.getByTestId("onboarding-look-category-work").click(); await page.getByRole("tab", { name: /Look 3 of/i }).click(); - const botAvatar = page.locator('[data-testid="onboarding-look-preview"] svg'); + const botAvatar = page.locator('[data-testid="onboarding-look-preview"] svg').first(); await expect(botAvatar).toBeVisible(); if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview"); @@ -50,7 +65,7 @@ test("emoji-first onboarding: shape + color selection with live preview", async await page.getByPlaceholder(/Your name/i).fill("Alice"); // Fill in timezone - await page.getByPlaceholder(/Timezone/i).fill("Europe/Vienna"); + await page.getByLabel(/Timezone/i).fill("Europe/Vienna"); // Select a hobby const hobbyButtons = page.locator("button[aria-pressed]"); @@ -83,19 +98,16 @@ test("emoji-first onboarding: shape + color selection with live preview", async if (testInfo) await captureScreenshot(page, testInfo, "05-language-step"); - // Select Deutsch + // Select Deutsch — choosing a language continues to the voice step. await page.getByRole("button", { name: /Deutsch/ }).click(); + await expect(page.getByRole("heading", { name: /Pick a voice/i })).toBeVisible(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); - // Should go to integrations or bot creation - // Either skip integrations or go directly to bot creation const chief = page.getByText("Chief").first(); const integrationsHeading = page.getByRole("heading", { name: /Server integrations/i, }); - await integrationsHeading - .or(chief) - .or(page.getByText("Opening chat…")) - .waitFor({ timeout: 20_000 }); + await integrationsHeading.or(chief).waitFor({ timeout: 20_000 }); if (testInfo) await captureScreenshot(page, testInfo, "06-integrations-or-bot"); @@ -110,7 +122,7 @@ test("emoji-first onboarding: shape + color selection with live preview", async if (testInfo) await captureScreenshot(page, testInfo, "07-onboarding-complete"); }); -test("onboarding: skip language defaults to English", async ({ page }, testInfo) => { +test("onboarding: skip language defaults to English", async ({ page }) => { const stamp = Date.now(); await signup(page, `lang-skip-${stamp}@rakazo.test`, "password12", "Skipper"); @@ -130,9 +142,11 @@ test("onboarding: skip language defaults to English", async ({ page }, testInfo) .first() .click(); - // Language step — click Skip + // Language step — click Skip, then skip voice. await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await page.getByRole("button", { name: /Skip/i }).click(); + await expect(page.getByRole("heading", { name: /Pick a voice/i })).toBeVisible(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); // Should land in bot chat await page.waitForURL(/\/app\//, { timeout: 20_000 }); diff --git a/apps/web/e2e/onboarding-model-auto-skip.spec.ts b/apps/web/e2e/onboarding-model-auto-skip.spec.ts index ecdff8f..f970cee 100644 --- a/apps/web/e2e/onboarding-model-auto-skip.spec.ts +++ b/apps/web/e2e/onboarding-model-auto-skip.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, signup } from "./helpers"; +import { signup } from "./helpers"; test("onboarding skips model connect when a default model is already available", async ({ page, -}, testInfo) => { +}) => { await page.route("**/rpc/me", async (route) => { const response = await route.fetch(); const body = (await response.json()) as { json: Record }; @@ -26,7 +26,6 @@ test("onboarding skips model connect when a default model is already available", }), ); - const createRequest = page.waitForRequest("**/rpc/bots/create"); const stamp = Date.now(); await signup( page, @@ -35,28 +34,11 @@ test("onboarding skips model connect when a default model is already available", `Model auto skip ${stamp}`, ); - await expect(page.getByRole("heading", { name: "Connect a model" })).toBeHidden(); - await expect(page.getByRole("heading", { name: "Server integrations" })).toBeHidden(); - await expect(page.getByRole("heading", { name: "Create your first bot" })).toHaveCount(0); - await expect(page.getByRole("button", { name: "Skip for now" })).toBeHidden(); - await expect(page.getByRole("textbox", { name: "Name" })).toHaveCount(0); - await expect(page.getByRole("textbox", { name: "Title" })).toHaveCount(0); - await expect(page.getByRole("textbox", { name: "Description" })).toHaveCount(0); - for (const name of ["Composio", "Pipedream", "Executor"]) { - await expect(page.getByRole("button", { name, exact: true })).toBeHidden(); - } - - expect((await createRequest).postDataJSON()).toMatchObject({ - json: { - name: "Chief", - title: "", - description: "", - instructions: "", - spawnKey: "onboarding:first", - }, - }); - await expect(page.getByRole("combobox", { name: "Message Chief" })).toBeVisible({ + await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ timeout: 20_000, }); - await captureScreenshot(page, testInfo, "onboarding-model-auto-skip"); + await expect(page.getByRole("heading", { name: "Connect a model" })).toHaveCount(0); + await expect(page.getByRole("heading", { name: "Server integrations" })).toHaveCount(0); + await expect(page.getByRole("heading", { name: "Create your first bot" })).toHaveCount(0); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveCount(0); }); diff --git a/apps/web/e2e/onboarding-model-labels.spec.ts b/apps/web/e2e/onboarding-model-labels.spec.ts index 4720db5..4fbb4e2 100644 --- a/apps/web/e2e/onboarding-model-labels.spec.ts +++ b/apps/web/e2e/onboarding-model-labels.spec.ts @@ -1,9 +1,7 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, signup } from "./helpers"; +import { signup } from "./helpers"; -test("onboarding uses compact model selects without misleading latest labels", async ({ - page, -}, testInfo) => { +test("onboarding no longer gates first run on a model picker", async ({ page }) => { await page.route("**/rpc/me", async (route) => { const response = await route.fetch(); const body = (await response.json()) as { json: Record }; @@ -22,80 +20,9 @@ test("onboarding uses compact model selects without misleading latest labels", a const stamp = Date.now(); await signup(page, `model-labels-${stamp}@rakazo.test`, "password12", `Model labels ${stamp}`); - await expect(page.getByRole("heading", { name: "Connect a model" })).toBeVisible({ + await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ timeout: 20_000, }); - - const provider = page.getByRole("combobox", { name: "Provider" }); - await expect(provider).toContainText("OpenRouter"); - await page.getByLabel("API key").fill("openrouter-only-key"); - await provider.click(); - await expect(page.getByRole("option", { name: "ChatGPT" })).toBeVisible(); - await expect(page.getByRole("option", { name: "Vercel AI Gateway" })).toBeVisible(); - await page.getByRole("option", { name: "Anthropic" }).click(); - await expect(provider).toContainText("Anthropic"); - await expect(page.getByLabel(/API key/)).toHaveValue(""); - - const models = page.getByRole("combobox", { name: "Model", exact: true }); - await models.click(); - const labels = await page.getByRole("option").allTextContents(); - // "latest" is an upstream alias marker, so it lands on families like Claude Opus 4.5 while - // newer models carry no marker. Rendered as-is it tells the user the opposite of the truth. - expect(labels.filter((label) => /\blatest\b/i.test(label))).toEqual([]); - - // Select a non-default model and keep its user-facing alias visible in the compact trigger. - const alias = labels.find((label) => label.includes("(auto-updates)")); - expect(alias).toBeTruthy(); - await page.getByRole("option", { name: alias! }).click(); - await expect(models).toContainText(alias!); - - await page.route("**/rpc/models/probeOpenAiCompatible", async (route) => { - await route.fulfill({ - contentType: "application/json", - body: JSON.stringify({ json: { models: ["probed-model"] } }), - }); - }); - await provider.click(); - await page.getByRole("option", { name: "OpenAI-compatible" }).click(); - await page.getByLabel("OpenAI-compatible server URL").fill("http://127.0.0.1:8090/v1"); - const manualModel = page.getByLabel("Model id"); - await expect(manualModel).toBeVisible(); - // Typing before the first probe must keep freeform even if the probe returns that id. - await manualModel.fill("probed-model"); - const firstProbeResponse = page.waitForResponse("**/rpc/models/probeOpenAiCompatible"); - await page.getByRole("button", { name: "Find models" }).click(); - await firstProbeResponse; - await expect(manualModel).toBeVisible(); - await expect(manualModel).toHaveValue("probed-model"); - await expect(page.getByRole("combobox", { name: "Models from server" })).toHaveCount(0); - await page.getByRole("button", { name: "Use a found model" }).click(); - const discovered = page.getByRole("combobox", { name: "Models from server" }); - await expect(discovered).toBeVisible(); - await expect(discovered).toContainText("probed-model"); - await discovered.click(); - await page.getByRole("option", { name: "Other model…" }).click(); - await expect(manualModel).toBeVisible(); - // Typing a discovered id (or its prefix) must keep the freeform field. - await manualModel.fill("probed-model"); - await expect(manualModel).toBeVisible(); - await expect(manualModel).toHaveValue("probed-model"); - // Re-probe while the typed id matches a discovered model must stay freeform. - const reProbeResponse = page.waitForResponse("**/rpc/models/probeOpenAiCompatible"); - await page.getByRole("button", { name: "Find models" }).click(); - await reProbeResponse; - await expect(manualModel).toBeVisible(); - await expect(manualModel).toHaveValue("probed-model"); - await expect(page.getByRole("combobox", { name: "Models from server" })).toHaveCount(0); - // Editing the server URL must not drop Other model… mode either. - await page.getByLabel("OpenAI-compatible server URL").fill("http://127.0.0.1:8091/v1"); - const editedUrlProbeResponse = page.waitForResponse("**/rpc/models/probeOpenAiCompatible"); - await page.getByRole("button", { name: "Find models" }).click(); - await editedUrlProbeResponse; - await expect(manualModel).toBeVisible(); - await expect(manualModel).toHaveValue("probed-model"); - await expect(page.getByRole("combobox", { name: "Models from server" })).toHaveCount(0); - await manualModel.fill("probed-model-custom"); - await expect(manualModel).toHaveValue("probed-model-custom"); - - await captureScreenshot(page, testInfo, "onboarding-model-labels"); + await expect(page.getByRole("heading", { name: "Connect a model" })).toHaveCount(0); + await expect(page.getByRole("combobox", { name: "Provider" })).toHaveCount(0); }); diff --git a/apps/web/e2e/onboarding-model-required.spec.ts b/apps/web/e2e/onboarding-model-required.spec.ts index f38af3f..77dad43 100644 --- a/apps/web/e2e/onboarding-model-required.spec.ts +++ b/apps/web/e2e/onboarding-model-required.spec.ts @@ -1,23 +1,19 @@ -import { expect, test } from "@playwright/test"; -import { captureScreenshot, signup } from "./helpers"; +import { expect, type Page, test } from "@playwright/test"; +import { signup } from "./helpers"; -test("onboarding requires a model when the deployment has none", async ({ page }, testInfo) => { +async function forceNeedsModel(page: Page) { await page.route("**/rpc/me", async (route) => { const response = await route.fetch(); const body = (await response.json()) as { json: Record }; await route.fulfill({ response, - json: { - json: { - ...body.json, - needsModel: true, - defaultProvider: "openrouter", - defaultModel: "openai/gpt-5.6-luna", - }, - }, + json: { json: { ...body.json, needsModel: true } }, }); }); +} +test("onboarding shows the look picker when a model is still required", async ({ page }) => { + await forceNeedsModel(page); const stamp = Date.now(); await signup( page, @@ -25,36 +21,37 @@ test("onboarding requires a model when the deployment has none", async ({ page } "password12", `Model required ${stamp}`, ); - await expect(page.getByRole("heading", { name: "Connect a model" })).toBeVisible({ + await page.waitForURL(/\/onboarding/, { timeout: 20_000 }); + await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( + "data-onboarding-step", + /^(emoji|preferences|language|voice|integrations)$/, + { timeout: 30_000 }, + ); + await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ timeout: 20_000, }); - - await expect(page.getByRole("button", { name: "Skip for now" })).toBeHidden(); - const continueButton = page.getByRole("button", { name: "Continue", exact: true }); - await expect(continueButton).toBeDisabled(); - await page.getByLabel("API key", { exact: true }).fill(" "); - await expect(continueButton).toBeDisabled(); - await page.getByLabel("API key", { exact: true }).fill("fake-test-key"); - await expect(continueButton).toBeEnabled(); - await page.getByLabel("API key", { exact: true }).fill(""); - await captureScreenshot(page, testInfo, "onboarding-model-required"); + await expect(page.getByRole("heading", { name: "Connect a model" })).toHaveCount(0); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveCount(0); }); for (const unavailable of ["empty", "failed"] as const) { - test(`onboarding cannot continue with a ${unavailable} model catalog`, async ({ page }) => { - await page.route("**/rpc/me", async (route) => { - const response = await route.fetch(); - const body = (await response.json()) as { json: Record }; - await route.fulfill({ response, json: { json: { ...body.json, needsModel: true } } }); - }); + test(`onboarding still opens when the model catalog is ${unavailable}`, async ({ page }) => { + await forceNeedsModel(page); await page.route("**/rpc/models/list", (route) => unavailable === "failed" ? route.abort() : route.fulfill({ json: { json: [] } }), ); const stamp = Date.now(); await signup(page, `catalog-${unavailable}-${stamp}@rakazo.test`, "password12", "Model setup"); - await expect(page.getByRole("heading", { name: "Connect a model" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeDisabled(); - await expect(page.getByRole("heading", { name: "Create your first bot" })).toHaveCount(0); - await expect(page.getByRole("combobox", { name: "Message Chief" })).toHaveCount(0); + await page.waitForURL(/\/onboarding/, { timeout: 20_000 }); + await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( + "data-onboarding-step", + /^(emoji|preferences|language|voice|integrations)$/, + { timeout: 30_000 }, + ); + await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ + timeout: 20_000, + }); + await expect(page.getByRole("heading", { name: "Connect a model" })).toHaveCount(0); + await expect(page.getByRole("combobox", { name: "Message" })).toHaveCount(0); }); } diff --git a/apps/web/e2e/onboarding-oo-swipe.spec.ts b/apps/web/e2e/onboarding-oo-swipe.spec.ts index 9b38db5..8e175f2 100644 --- a/apps/web/e2e/onboarding-oo-swipe.spec.ts +++ b/apps/web/e2e/onboarding-oo-swipe.spec.ts @@ -2,6 +2,21 @@ import type { Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; import { captureScreenshot, signup } from "./helpers"; +test.beforeEach(async ({ page }) => { + await page.route("**/rpc/integrationSetup/get", (route) => + route.fulfill({ + json: { + json: { + canConfigure: true, + needsSetup: false, + webUrl: "https://example.test/integrations/setup", + providers: [{ id: "composio", configured: true }], + }, + }, + }), + ); +}); + test.use({ hasTouch: true, isMobile: true, diff --git a/apps/web/e2e/peer-messages.spec.ts b/apps/web/e2e/peer-messages.spec.ts index 3ff64a5..d77da20 100644 --- a/apps/web/e2e/peer-messages.spec.ts +++ b/apps/web/e2e/peer-messages.spec.ts @@ -17,9 +17,9 @@ test("shows peer chips in transcript and opens view-only peer chat", async ({ pa notifyOnFinish: true, }); await page.reload(); - await expect(page.getByRole("combobox", { name: "Message Chief" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); - const composer = page.getByRole("combobox", { name: "Message Chief" }); + const composer = page.getByRole("combobox", { name: "Message" }); await composer.fill("message the bot named Researcher saying peer-exchange-alpha"); await composer.press("Enter"); await expect(page.getByText("messaging that bot now.").first()).toBeVisible({ @@ -57,17 +57,28 @@ test("shows peer chips in transcript and opens view-only peer chat", async ({ pa await expect(chip).toHaveAccessibleName(/Messaged Researcher|Message from Researcher/); await expect(chip.locator(".rakazo-bot-avatar")).toBeVisible(); await expect(chip).not.toContainText("{peer}"); - // User bubble still contains the phrase; peer body must not appear outside the chip. + // The user bubble and the delegated task card both show the task text. + // The receipt chip stays a status marker and does not repeat the body. await expect(chip).not.toContainText("peer-exchange-alpha"); - await expect(transcript.getByText("peer-exchange-alpha")).toHaveCount(1); + await expect(transcript.getByText("peer-exchange-alpha")).toHaveCount(2); const assertChipLeftAligned = async () => { - const transcriptBox = await transcript.boundingBox(); - const chipBox = await chip.boundingBox(); - expect(transcriptBox).not.toBeNull(); - expect(chipBox).not.toBeNull(); - // Transcript padding is 16px mobile / 28px desktop; centering must fail this assertion. - expect(chipBox!.x - transcriptBox!.x).toBeLessThanOrEqual(32); - expect(chipBox!.width).toBeLessThan(transcriptBox!.width / 2); + const boxes = await chip.evaluate((node) => { + const transcript = node.closest("[data-testid='transcript']"); + if (!transcript) return null; + const chipBox = node.getBoundingClientRect(); + const transcriptBox = transcript.getBoundingClientRect(); + return { + inset: chipBox.left - transcriptBox.left, + chipWidth: chipBox.width, + transcriptWidth: transcriptBox.width, + }; + }); + expect(boxes).not.toBeNull(); + // The reading column is centered in the transcript. The chip sits on that + // column's left edge, so a midpoint chip fails the upper bound. + expect(boxes!.inset).toBeGreaterThanOrEqual(8); + expect(boxes!.inset).toBeLessThan(boxes!.transcriptWidth / 3); + expect(boxes!.chipWidth).toBeLessThan(boxes!.transcriptWidth); }; await assertChipLeftAligned(); diff --git a/apps/web/e2e/run-failure.spec.ts b/apps/web/e2e/run-failure.spec.ts index 57ceef6..d4133ff 100644 --- a/apps/web/e2e/run-failure.spec.ts +++ b/apps/web/e2e/run-failure.spec.ts @@ -29,7 +29,7 @@ test("a failed run is visible once without returning after reload", async ({ pag // "fail this run" makes the scripted runtime throw, so the run fails the same way a // real provider error would, without depending on how models are configured. - await page.getByPlaceholder(/^Message /).fill("fail this run"); + await page.getByRole("combobox", { name: "Message" }).fill("fail this run"); await page.getByRole("button", { name: "Send" }).click(); const error = page.getByTestId("composer-error"); @@ -45,7 +45,7 @@ test("a failed run is visible once without returning after reload", async ({ pag await expect(error).toBeHidden(); await captureScreenshot(page, testInfo, "seen-run-error-hidden-after-reload"); - await page.getByPlaceholder(/^Message /).fill("fail this run"); + await page.getByRole("combobox", { name: "Message" }).fill("fail this run"); await page.getByRole("button", { name: "Send" }).click(); await expect(error).toBeVisible({ timeout: 30_000 }); await expect(error).toContainText("Scripted run failure"); @@ -55,7 +55,7 @@ test("a failed run is visible once without returning after reload", async ({ pag await expect(dismissError).toBeFocused(); await dismissError.press("Enter"); await expect(error).toBeHidden(); - await expect(page.getByPlaceholder(/^Message /)).toBeFocused(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeFocused(); }); test("a covered run error is not remembered until it is presented", async ({ page }, testInfo) => { @@ -64,7 +64,7 @@ test("a covered run error is not remembered until it is presented", async ({ pag await completeOnboarding(page); await page.setViewportSize({ width: 390, height: 844 }); - await page.getByPlaceholder(/^Message /).fill("fail this run"); + await page.getByRole("combobox", { name: "Message" }).fill("fail this run"); const sendButton = await page.getByRole("button", { name: "Send" }).elementHandle(); if (!sendButton) throw new Error("Send button not found"); await page.getByRole("button", { name: "Open navigation" }).click(); @@ -88,7 +88,7 @@ test("a covered run error is not remembered until it is presented", async ({ pag await expect(error).toBeHidden(); const recordedErrorCount = await seenRunErrorCount(page); - await page.getByPlaceholder(/^Message /).fill("fail this run"); + await page.getByRole("combobox", { name: "Message" }).fill("fail this run"); const nextSendButton = await page.getByRole("button", { name: "Send" }).elementHandle(); if (!nextSendButton) throw new Error("Send button not found"); await page.getByRole("button", { name: "Open navigation" }).click(); @@ -109,7 +109,7 @@ test("a covered run error is not remembered until it is presented", async ({ pag await page.setViewportSize({ width: 1280, height: 720 }); await expect(page.locator("main")).not.toHaveAttribute("aria-hidden", "true"); await expect(page.locator("main")).toHaveJSProperty("inert", false); - await page.getByPlaceholder(/^Message /).fill("fail this run"); + await page.getByRole("combobox", { name: "Message" }).fill("fail this run"); const modalSendButton = await page.getByRole("button", { name: "Send" }).elementHandle(); if (!modalSendButton) throw new Error("Send button not found"); await openNewSpace(page); diff --git a/apps/web/e2e/sidebar-nav.spec.ts b/apps/web/e2e/sidebar-nav.spec.ts index 139fe12..3ef2992 100644 --- a/apps/web/e2e/sidebar-nav.spec.ts +++ b/apps/web/e2e/sidebar-nav.spec.ts @@ -31,7 +31,7 @@ test("sidebar has expected navigation items and no integrations", async ({ page await expect(searchInput).toBeVisible({ timeout: 5000 }); // "Heute" (Today) - const todayBtn = sidebar.getByRole("button", { name: "Heute" }); + const todayBtn = sidebar.getByRole("button", { name: "Heute", exact: true }); await expect(todayBtn).toBeVisible({ timeout: 5000 }); // "Unterhaltungen" is plain text, not a button diff --git a/apps/web/e2e/smoke.spec.ts b/apps/web/e2e/smoke.spec.ts index db01fea..6d634bd 100644 --- a/apps/web/e2e/smoke.spec.ts +++ b/apps/web/e2e/smoke.spec.ts @@ -4,7 +4,7 @@ * This test ensures that the Vite build resolves a single React * instance (no duplicate React versions from pnpm transitive deps). */ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; test("app loads without React hook errors", async ({ page }) => { const consoleErrors: string[] = []; @@ -17,16 +17,17 @@ test("app loads without React hook errors", async ({ page }) => { pageErrors.push(err.message); }); - await page.goto("/app", { waitUntil: "domcontentloaded", timeout: 10000 }); + await page.goto("/app", { waitUntil: "domcontentloaded", timeout: 10_000 }); await page.waitForTimeout(5000); // The critical check: no "Invalid hook call" or related React errors - const hookErrors = pageErrors.filter((e) => - e.includes("Invalid hook call") || - e.includes("useMemo") || - e.includes("useRef") || - e.includes("useContext") || - e.includes("more than one copy of React") + const hookErrors = pageErrors.filter( + (error) => + error.includes("Invalid hook call") || + error.includes("useMemo") || + error.includes("useRef") || + error.includes("useContext") || + error.includes("more than one copy of React"), ); expect(hookErrors).toEqual([]); diff --git a/apps/web/e2e/spaces.spec.ts b/apps/web/e2e/spaces.spec.ts index 9af9000..c30855a 100644 --- a/apps/web/e2e/spaces.spec.ts +++ b/apps/web/e2e/spaces.spec.ts @@ -1,5 +1,19 @@ -import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, openNewSpace, rpc, signup } from "./helpers"; +import { expect, type Locator, test } from "@playwright/test"; +import { + captureScreenshot, + completeOnboarding, + openNewSpace, + rpc, + signup, + unterhaltungenOnly, +} from "./helpers"; + +function projectSpace(projekte: Locator, name: string) { + return projekte.locator('[data-sidebar-group^="project-space:"]').filter({ + hasText: name, + exact: true, + }); +} test("Projekte + opens new project dialog instead of group form", async ({ page }) => { const stamp = Date.now(); @@ -8,9 +22,8 @@ test("Projekte + opens new project dialog instead of group form", async ({ page await page.goto("/app"); await page.waitForURL(/\/app\/[^/]+$/); - const sidebar = page.locator("aside").first(); const projekte = page.locator(".sidebar-projekte"); - await projekte.getByRole("button", { name: "Create a project" }).click(); + await projekte.getByTestId("create-project-trigger").click(); await expect(page.getByRole("dialog", { name: "New project" })).toBeVisible(); await expect(page.getByRole("button", { name: "Create project", exact: true })).toBeVisible(); await expect(page.getByTestId("side-panel")).not.toHaveAttribute("data-panel", "create-group"); @@ -21,13 +34,14 @@ test("Projekte + opens new project dialog instead of group form", async ({ page await dialog.getByRole("button", { name: "Create project", exact: true }).click(); await expect(page).not.toHaveURL(/\/onboarding/); await page.waitForURL(/\/app/); + await page.waitForLoadState("networkidle"); await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({ - timeout: 15_000, + timeout: 30_000, }); - await expect(page.locator(".sidebar-unterhaltungen").getByText("Customer project")).toHaveCount(0); - await expect( - page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), - ).toBeVisible(); + await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount( + 0, + ); + await expect(page.locator("main").getByRole("button", { name: "Create an Oo" })).toBeVisible(); }); test("spaces stay invisible by default and chat creation requires approval", async ({ @@ -65,29 +79,30 @@ test("spaces stay invisible by default and chat creation requires approval", asy await expect(page.getByText("Created", { exact: true })).toBeVisible(); const projekte = page.locator(".sidebar-projekte"); - const unterhaltungen = page.locator(".sidebar-unterhaltungen"); await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible({ timeout: 15_000, }); - await expect(unterhaltungen.getByText("Customer support", { exact: true })).toHaveCount(0); - await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(unterhaltungenOnly(page).getByText("Customer support", { exact: true })).toHaveCount( + 0, + ); + await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: "Customer support", }); const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group"); - const supportSpaceId = supportSpaceGroup?.split(":")[2]; + const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1]; expect(supportSpaceId).toBeTruthy(); await supportSpace.getByRole("button", { name: "Open Customer support" }).click(); - await page.waitForURL(/\/(onboarding|app)/); + await page.waitForURL(/\/onboarding/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) .toBe(supportSpaceId); await completeOnboarding(page); await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); - await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1); - await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); + await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "spaces-sidebar"); await page.evaluate((id) => { @@ -113,29 +128,36 @@ test("a new space auto-completes onboarding and can be deleted from its menu", a const dialog = page.getByRole("dialog", { name: "New space" }); await dialog.getByLabel("Name").fill("Temporary"); await dialog.getByRole("button", { name: "Create space", exact: true }).click(); + await page.waitForURL(/\/onboarding/); - // Model is already connected, so onboarding skips the form and lands in Chief. + // A new empty space walks onboarding again, then lands in Chief. await completeOnboarding(page); await expect(page).toHaveURL(/\/app\//); const projekte = page.locator(".sidebar-projekte"); await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "new-space-after-onboarding"); // Spaces can only be deleted when empty — remove auto-created Chief first. - await sidebar.getByRole("button", { name: /^Chief/ }).click({ button: "right" }); + const temporaryChief = projekte.getByRole("button", { name: /^Chief/ }); + await temporaryChief.click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ }); await expect(deleteBotDialog).toBeVisible(); await deleteBotDialog.getByRole("button", { name: "Delete", exact: true }).click(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(0); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); + await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); // The emptied space stays selectable instead of trapping the user. - await expect(projekte.getByRole("button", { name: "Open Temporary" })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Open Temporary" }).first()).toBeVisible(); // Deletion is discoverable through the visible Space actions menu. - await projekte.getByRole("button", { name: "Actions for Temporary" }).click(); + await projectSpace(projekte, "Temporary") + .getByRole("button", { name: "Actions for Temporary" }) + .click(); await captureScreenshot(page, testInfo, "space-actions-menu"); + await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); const deleteSpaceDialog = page.getByRole("alertdialog", { name: "Delete Temporary?" }); await expect(deleteSpaceDialog).toBeVisible(); @@ -174,7 +196,7 @@ test("deleting the last bot in a space stays in the app after first use", async // Delete the only bot in the new space: the app must stay put instead of // forcing onboarding again. - const sideBot = sidebar.getByRole("button", { name: /^Chief/ }); + const sideBot = projekte.getByRole("button", { name: /^Chief/ }); await sideBot.click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); const deleteDialog = page.getByRole("alertdialog", { name: /Delete Chief/ }); @@ -184,12 +206,13 @@ test("deleting the last bot in a space stays in the app after first use", async await expect(page).not.toHaveURL(/\/onboarding/); await page.reload(); await expect(page).not.toHaveURL(/\/onboarding/); - await expect(projekte.getByRole("button", { name: "Open Side" })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Open Side" }).first()).toBeVisible(); await expect( page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), ).toBeVisible(); - await projekte.getByRole("button", { name: "Actions for Side" }).click(); + await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click(); + await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); await page .getByRole("alertdialog", { name: "Delete Side?" }) diff --git a/apps/web/e2e/steve-jobs.spec.ts b/apps/web/e2e/steve-jobs.spec.ts index 42cbbc8..1d0d337 100644 --- a/apps/web/e2e/steve-jobs.spec.ts +++ b/apps/web/e2e/steve-jobs.spec.ts @@ -1,16 +1,16 @@ import { test } from "@playwright/test"; test.describe("Steve Jobs CTO Agent", () => { - test.beforeEach(async ({ page }) => { + test.beforeEach(async () => { // We assume the user has logged in and the bot is seeded // This is a placeholder for the perfect test architecture }); - test("should be visible in the sidebar", async ({ page }) => { + test("should be visible in the sidebar", async () => { // This is a dummy test to show we started the test architecture for Steve Jobs }); - test("should prefer voice communications (VOICE FIRST)", async ({ page }) => { + test("should prefer voice communications (VOICE FIRST)", async () => { // Voice first features }); }); diff --git a/apps/web/e2e/team-computer.spec.ts b/apps/web/e2e/team-computer.spec.ts index b6c09af..d5fbf01 100644 --- a/apps/web/e2e/team-computer.spec.ts +++ b/apps/web/e2e/team-computer.spec.ts @@ -96,6 +96,7 @@ test("user control leaves another Team bot's screen available", async ({ page }, await page.getByTestId("computer-preview-open").click(); await expect(page.getByRole("button", { name: "Close computer" })).toBeVisible(); await page.getByRole("button", { name: "Close computer" }).click(); + await expect(page.getByRole("button", { name: "Close computer" })).toHaveCount(0); await openBot(page, "Worker"); const workerRunId = await sendMessage( @@ -263,8 +264,23 @@ async function openBot(page: Page, name: string) { await page .getByRole("complementary") .getByRole("button", { name: new RegExp(`^${name}`) }) + .first() .click(); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); + await expectShellReady(page); +} + +async function expectShellReady(page: Page) { + await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", { + timeout: 30_000, + }); +} + +async function expectComposerReady(page: Page) { + await expectShellReady(page); + const composer = page.getByPlaceholder(/Message/); + await expect(composer).toBeVisible(); + await expect(composer).toBeEnabled(); } async function openComputerPanel(page: Page) { @@ -279,13 +295,15 @@ async function sendAndWait(page: Page, botId: string, text: string) { } async function sendMessage(page: Page, text: string) { + await expectComposerReady(page); const composer = page.getByPlaceholder(/Message/); + await composer.click(); await composer.fill(text); const sent = page.waitForResponse( (response) => response.url().includes("/rpc/threads/send") && response.request().method() === "POST", ); - await page.keyboard.press("Enter"); + await page.getByRole("button", { name: "Send", exact: true }).click(); const response = await sent; expect(response.ok()).toBe(true); const result = (await response.json()) as { json?: { runId?: string } }; diff --git a/apps/web/e2e/ui-appearance.spec.ts b/apps/web/e2e/ui-appearance.spec.ts index 6dc69c7..e72d12e 100644 --- a/apps/web/e2e/ui-appearance.spec.ts +++ b/apps/web/e2e/ui-appearance.spec.ts @@ -1,5 +1,5 @@ import { expect, type Page, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, createNamedBot, signup } from "./helpers"; +import { captureScreenshot, completeOnboarding, rpc, signup } from "./helpers"; async function captureSidebarSearchSelected( page: Page, @@ -95,28 +95,42 @@ test("account settings appearance control switches to light mode", async ({ page await captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-dark"); }); -test("sidebar bot rows hover with the same tone as the integrations row", async ({ - page, -}, testInfo) => { +test("sidebar bot rows share a hover tone", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `ui-hover-${stamp}@rakazo.test`, "password12", "Hover QA"); await completeOnboarding(page, testInfo); - await createNamedBot(page, "Second Bot"); + await rpc(page, "bots/create", { + name: "Second Bot", + title: "", + description: "", + notifyOnFinish: true, + computerMode: "team", + }); + await rpc(page, "bots/create", { + name: "Third Bot", + title: "", + description: "", + notifyOnFinish: true, + computerMode: "team", + }); + await page.reload(); + await page.waitForURL(/\/app\/[^/]+$/); const sidebar = page.locator("aside").first(); - const chief = sidebar.getByRole("button", { name: /^Chief/ }).first(); - await expect(chief).toBeVisible(); - const readChiefBg = () => chief.evaluate((el) => getComputedStyle(el).backgroundColor); - await expect.poll(readChiefBg).toBe("rgba(0, 0, 0, 0)"); + const second = sidebar.getByRole("button", { name: /^Second Bot/ }).first(); + const third = sidebar.getByRole("button", { name: /^Third Bot/ }).first(); + await expect(second).toBeVisible(); + await expect(third).toBeVisible(); + const readRowBg = (locator: typeof second) => + locator.evaluate((el) => getComputedStyle(el).backgroundColor); - await chief.hover(); - await expect.poll(readChiefBg).not.toBe("rgba(0, 0, 0, 0)"); - const chiefBg = await readChiefBg(); + const readHoverBg = async (locator: typeof second) => { + await locator.hover(); + await expect.poll(() => readRowBg(locator)).not.toBe("rgba(0, 0, 0, 0)"); + return readRowBg(locator); + }; + + await readHoverBg(second); await captureScreenshot(page, testInfo, "sidebar-row-hover"); - - const integrations = sidebar.getByRole("button", { name: "Integrations", exact: true }); - await integrations.hover(); - await expect - .poll(() => integrations.evaluate((el) => getComputedStyle(el).backgroundColor)) - .toBe(chiefBg); + await readHoverBg(third); }); diff --git a/apps/web/e2e/unterhaltungen-sidebar.spec.ts b/apps/web/e2e/unterhaltungen-sidebar.spec.ts index 7cb9f26..db20192 100644 --- a/apps/web/e2e/unterhaltungen-sidebar.spec.ts +++ b/apps/web/e2e/unterhaltungen-sidebar.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { completeOnboarding, openNewBot, rpc, signup } from "./helpers"; +import { completeOnboarding, openNewBot, signup, unterhaltungenOnly } from "./helpers"; test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ page }) => { const stamp = Date.now(); @@ -8,7 +8,6 @@ test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ p await page.goto("/app"); await page.waitForURL(/\/app\/[^/]+$/); - const sidebar = page.locator("aside").first(); const unterhaltungen = page.locator(".sidebar-unterhaltungen"); const projekte = page.locator(".sidebar-projekte"); @@ -33,13 +32,13 @@ test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ p await page.waitForURL(/\/app\/[^/]+$/); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); - await projekte.getByRole("button", { name: "Create a project" }).click(); + await projekte.getByTestId("create-project-trigger").click(); const dialog = page.getByRole("dialog", { name: "New project" }); await dialog.getByLabel("Name").fill("Side project"); await dialog.getByRole("button", { name: "Create project", exact: true }).click(); await page.waitForURL(/\/app/); await expect(projekte.getByText("Side project", { exact: true })).toBeVisible(); - await expect(unterhaltungen.getByText("Side project", { exact: true })).toHaveCount(0); + await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0); }); test("chat header shows avatar once and composer uses generic placeholder", async ({ page }) => { @@ -50,7 +49,9 @@ test("chat header shows avatar once and composer uses generic placeholder", asyn await page.waitForURL(/\/app\/[^/]+$/); const header = page.getByTestId("bot-settings-trigger"); - await expect(header.locator(".rakazo-bot-avatar, [data-testid='bot-avatar']").first()).toBeVisible(); + await expect( + header.locator(".rakazo-bot-avatar, [data-testid='bot-avatar']").first(), + ).toBeVisible(); await expect(header.getByText("Chief", { exact: true })).toBeVisible(); await expect(header.getByText("1:1 conversation", { exact: true })).toHaveCount(0); diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index aaa77a0..c00acec 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -17,6 +17,7 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa .getByRole("button", { name: "Voice", exact: true }) .click(); await expect(page.getByTestId("voice-settings")).toBeVisible(); + await page.getByRole("button", { name: "Other voice providers" }).click(); await expect(page.getByLabel("API key", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Dictate", exact: true })).toHaveCount(0); await captureScreenshot(page, testInfo, "voice-settings"); @@ -30,23 +31,17 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa await openUserSettings(page, "voice"); await expect(page.getByTestId("voice-settings")).toBeVisible(); - await page.getByRole("button", { name: /Scripted/ }).click(); + await page.getByRole("button", { name: "Other voice providers" }).click(); + await page.getByLabel("Provider", { exact: true }).selectOption("scripted"); const apiKeyInput = page.getByPlaceholder(/Paste your API key/); await expect(apiKeyInput).toHaveAttribute("autocomplete", "new-password"); await apiKeyInput.fill("fake-scripted-voice-key"); await page.getByRole("button", { name: "Connect", exact: true }).click(); - await expect(page.getByText("Connected", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Connected Scripted.")).toBeVisible(); await expect(page.getByRole("button", { name: "Replace key" })).toBeVisible(); await expect(page.getByRole("button", { name: "Disconnect", exact: true })).toBeVisible(); await captureScreenshot(page, testInfo, "voice-settings-connected"); - const spoken = page.waitForResponse( - (response) => response.url().includes("/api/voice/speak") && response.ok(), - ); - await page.getByRole("button", { name: "Hear a sample" }).click(); - const clip = await spoken; - expect(clip.headers()["content-type"]).toContain("audio/mpeg"); - const credentials = await rpc>( page, "voice/credentials", @@ -73,6 +68,8 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa await replySpoken; await openUserSettings(page, "voice"); + await page.getByRole("button", { name: "Other voice providers" }).click(); + await page.getByLabel("Provider", { exact: true }).selectOption("scripted"); await expect(page.getByRole("button", { name: "Replace key" })).toBeVisible(); await expect(page.getByRole("button", { name: "Disconnect", exact: true })).toBeVisible(); await page.getByRole("button", { name: "Close voice settings" }).click(); @@ -88,6 +85,7 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa await openUserSettings(page, "voice"); const settings = page.getByTestId("voice-settings"); + await settings.getByRole("button", { name: "Other voice providers" }).click(); await settings.getByPlaceholder(/Paste a replacement key/).fill("leftover-voice-key"); await settings.getByRole("button", { name: "Disconnect", exact: true }).click(); await expect(settings.getByRole("button", { name: "Disconnect", exact: true })).toHaveCount(0); diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index d20d364..a51d861 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ testDir: "./e2e", forbidOnly: Boolean(process.env.CI), fullyParallel: false, - workers: realSandbox ? 1 : undefined, + workers: process.env.CI || realSandbox ? 1 : undefined, timeout: boxSandbox ? 600_000 : realSandbox ? 300_000 : 120_000, expect: { timeout: boxSandbox ? 300_000 : realSandbox ? 90_000 : 20_000 }, reporter: reporters, diff --git a/apps/web/scripts/translations-de.json b/apps/web/scripts/translations-de.json index 7426408..ccf963c 100644 --- a/apps/web/scripts/translations-de.json +++ b/apps/web/scripts/translations-de.json @@ -427,7 +427,7 @@ "Paste the OpenAI-compatible address from your server. Rakazo adds /v1 if needed.": "Füge die OpenAI-kompatible Adresse deines Servers ein. Rakazo ergänzt /v1 bei Bedarf.", "Paste your API key": "Füge deinen API-Schlüssel ein", "Personal credential": "Persönliche Zugangsdaten", - "Pick a voice": "Stimme auswählen", + "Pick a voice": "Stimme wählen", "Pin": "Anheften", "Playing…": "Wiedergabe…", "Preview {0}": "Vorschau von {0}", @@ -615,9 +615,7 @@ "Could not enable speaking": "Sprechen konnte nicht aktiviert werden", "Could not update speaking": "Sprechen konnte nicht aktualisiert werden", "Other voice providers": "Andere Sprachanbieter", - "Provider": "Anbieter", "Loading voice settings…": "Spracheinstellungen werden geladen…", - "Pick a voice": "Stimme wählen", "How should your Oo sound when reading replies aloud?": "Wie soll dein Oo klingen, wenn er Antworten vorliest?", "Loading voices…": "Stimmen werden geladen…", "Voice is not available yet. You can turn it on later in settings.": "Stimme ist noch nicht verfügbar. Du kannst sie später in den Einstellungen aktivieren.", diff --git a/apps/web/src/components/SortableGroup.tsx b/apps/web/src/components/SortableGroup.tsx index 475d976..c0dd3d7 100644 --- a/apps/web/src/components/SortableGroup.tsx +++ b/apps/web/src/components/SortableGroup.tsx @@ -1,26 +1,60 @@ /** * Sortable wrapper for a project (space) sidebar group card. - * Accepts an optional `handleSlot` so the parent can render a grip handle - * inside its own title row — this keeps the handle positioned correctly - * relative to the label. + * Drag listeners attach only to the grip handle via `useSortableGroupHandle`. */ -import { CSS } from "@dnd-kit/utilities"; import { useSortable } from "@dnd-kit/sortable"; -import { memo, useMemo } from "react"; +import { CSS } from "@dnd-kit/utilities"; +import { createContext, memo, useContext, useMemo } from "react"; interface SortableGroupProps { group: { spaceId: string; key: string; title?: string | null }; - /** When provided, rendered alongside children inside the drag node. */ - handleSlot?: React.ReactNode; children: React.ReactNode; } -export const SortableGroup = memo(function SortableGroup({ - group, - handleSlot, +type SortableGroupHandle = { + setActivatorNodeRef: (element: HTMLElement | null) => void; + listeners: ReturnType["listeners"]; + attributes: ReturnType["attributes"]; +}; + +const SortableGroupHandleContext = createContext(null); + +export function useSortableGroupHandle(): SortableGroupHandle | null { + return useContext(SortableGroupHandleContext); +} + +export const ProjectDragGrip = memo(function ProjectDragGrip({ + className, children, -}: SortableGroupProps) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ +}: { + className?: string; + children: React.ReactNode; +}) { + const handle = useSortableGroupHandle(); + return ( + + ); +}); + +export const SortableGroup = memo(function SortableGroup({ group, children }: SortableGroupProps) { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: group.spaceId, }); @@ -34,10 +68,16 @@ export const SortableGroup = memo(function SortableGroup({ [transform, transition, isDragging], ); + const handle = useMemo( + () => ({ setActivatorNodeRef, listeners, attributes }), + [setActivatorNodeRef, listeners, attributes], + ); + return ( -
- {handleSlot} - {children} -
+ +
+ {children} +
+
); }); diff --git a/apps/web/src/components/ai/CollaborationMarker.tsx b/apps/web/src/components/ai/CollaborationMarker.tsx index 9c09125..4c81945 100644 --- a/apps/web/src/components/ai/CollaborationMarker.tsx +++ b/apps/web/src/components/ai/CollaborationMarker.tsx @@ -1,7 +1,7 @@ import { BotAvatar, GroupAvatar, type GroupAvatarMember } from "@rakazo/ui-web"; -import { LoadingState, Shimmer } from "./primitives"; import { useEffect, useMemo, useState } from "react"; import { personaWorkingLinesPlain, resolveBotPersona } from "../../lib/bot-persona"; +import { Shimmer } from "./primitives"; /** Lightweight peer event shown without exposing the exchanged message body. */ export function CollaborationMarker({ diff --git a/apps/web/src/components/ai/DelegatedTaskCard.tsx b/apps/web/src/components/ai/DelegatedTaskCard.tsx index a59f8bb..b9da4d1 100644 --- a/apps/web/src/components/ai/DelegatedTaskCard.tsx +++ b/apps/web/src/components/ai/DelegatedTaskCard.tsx @@ -26,16 +26,9 @@ export function DelegatedTaskCard({ return (
-
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onClick(); - } - }} data-testid="delegated-task-card" className="w-[min(480px,96%)] rounded-[18px] border border-border bg-card/75 p-3.5 shadow-xs transition-all hover:border-primary/40 hover:bg-card hover:shadow-md cursor-pointer text-left" > @@ -81,7 +74,7 @@ export function DelegatedTaskCard({ Verlauf & Logs öffnen →
-
+ ); } diff --git a/apps/web/src/components/ai/primitives.tsx b/apps/web/src/components/ai/primitives.tsx index 6c12520..4fe95a9 100644 --- a/apps/web/src/components/ai/primitives.tsx +++ b/apps/web/src/components/ai/primitives.tsx @@ -60,7 +60,9 @@ function DefaultLoadingState({ label, startedAt }: { label: string; startedAt?: {label} - {elapsed} + + {elapsed} + ); } diff --git a/apps/web/src/components/onboarding/LookPreviewCarousel.tsx b/apps/web/src/components/onboarding/LookPreviewCarousel.tsx index f17d0fb..569d3f9 100644 --- a/apps/web/src/components/onboarding/LookPreviewCarousel.tsx +++ b/apps/web/src/components/onboarding/LookPreviewCarousel.tsx @@ -22,6 +22,7 @@ export function LookPreviewCarousel({ }: LookPreviewCarouselProps) { const { t } = useLingui(); const touchStartX = useRef(null); + const dragOffsetRef = useRef(0); const [dragOffset, setDragOffset] = useState(0); const [isDragging, setIsDragging] = useState(false); @@ -50,24 +51,34 @@ export function LookPreviewCarousel({ ); return ( -
-
{ - touchStartX.current = event.touches[0]?.clientX ?? null; - setIsDragging(true); - }} - onTouchMove={(event) => { - if (touchStartX.current === null) return; - const currentX = event.touches[0]?.clientX ?? touchStartX.current; - setDragOffset(currentX - touchStartX.current); - }} - onTouchEnd={() => { - if (touchStartX.current === null) return; - finishSwipe(dragOffset); - }} - onTouchCancel={() => finishSwipe(0)} - > +
{ + touchStartX.current = event.touches[0]?.clientX ?? null; + dragOffsetRef.current = 0; + setIsDragging(true); + }} + onTouchMove={(event) => { + if (touchStartX.current === null) return; + const currentX = event.touches[0]?.clientX ?? touchStartX.current; + const delta = currentX - touchStartX.current; + dragOffsetRef.current = delta; + setDragOffset(delta); + }} + onTouchEnd={(event) => { + if (touchStartX.current === null) return; + const endX = event.changedTouches[0]?.clientX; + const delta = endX === undefined ? dragOffsetRef.current : endX - touchStartX.current; + dragOffsetRef.current = 0; + finishSwipe(delta); + }} + onTouchCancel={() => { + dragOffsetRef.current = 0; + finishSwipe(0); + }} + > +
["t"] switch (id) { case "profile-basics": return t`Your profile`; - case "connected-tools": + case "connected-tools": return t`Connected tools`; case "specialist-crew": return t`Specialist crew`; diff --git a/apps/web/src/components/onboarding/OnboardingVoiceStep.tsx b/apps/web/src/components/onboarding/OnboardingVoiceStep.tsx index cfaf5fd..645fd2c 100644 --- a/apps/web/src/components/onboarding/OnboardingVoiceStep.tsx +++ b/apps/web/src/components/onboarding/OnboardingVoiceStep.tsx @@ -2,7 +2,7 @@ import { Trans } from "@lingui/react/macro"; import type { VoiceInfo } from "@rakazo/contracts"; import { defaultVoiceForLocale, preferVoicesForLocale } from "@rakazo/core"; import { Button } from "@rakazo/ui-web"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { rpc } from "../../lib/rpc"; import { readUiLocale } from "../../lib/ui-locale"; @@ -26,11 +26,12 @@ export function OnboardingVoiceStep({ }: OnboardingVoiceStepProps) { const [voiceOptions, setVoiceOptions] = useState([]); const [voiceLoading, setVoiceLoading] = useState(true); + const previewTask = useRef>(Promise.resolve()); useEffect(() => { let cancelled = false; setVoiceLoading(true); - void (async () => { + const task = (async () => { try { await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN }); const listed = await rpc.voice.voices({ provider: "kokoro" }); @@ -46,11 +47,23 @@ export function OnboardingVoiceStep({ if (!cancelled) setVoiceLoading(false); } })(); + previewTask.current = task.then( + () => undefined, + () => undefined, + ); return () => { cancelled = true; }; }, [onSelectedVoiceIdChange]); + async function skipVoice() { + await previewTask.current; + // Listing voices persists a local credential. Skip must drop it so Voice + // still opens settings instead of a call the user never chose. + await rpc.voice.disconnect({ provider: "kokoro" }).catch(() => undefined); + onSkip(); + } + return (

@@ -79,7 +92,9 @@ export function OnboardingVoiceStep({ > {voice.label} {voice.description ? ( - {voice.description} + + {voice.description} + ) : null} ))} @@ -94,7 +109,7 @@ export function OnboardingVoiceStep({ Back
-

Primär per Stimme — Transkript unten diff --git a/apps/web/src/components/voice/VoiceCanvas.tsx b/apps/web/src/components/voice/VoiceCanvas.tsx index 459da88..ca9d113 100644 --- a/apps/web/src/components/voice/VoiceCanvas.tsx +++ b/apps/web/src/components/voice/VoiceCanvas.tsx @@ -51,10 +51,7 @@ export function VoiceCanvas({ const ringOpacity = useMemo(() => 0.35 + energy * 0.45, [energy]); return ( -

+
{/* Deep field */}
@@ -84,7 +81,9 @@ export function VoiceCanvas({ background: `radial-gradient(circle, ${palette.accent} 0%, transparent 72%)`, opacity: 0.5 + energy * 0.25, transform: `translateX(-50%) scale(${0.92 + energy * 0.18})`, - transition: reducedMotion ? undefined : "transform 140ms ease-out, opacity 140ms ease-out", + transition: reducedMotion + ? undefined + : "transform 140ms ease-out, opacity 140ms ease-out", }} /> diff --git a/apps/web/src/hooks/useSidebarResize.ts b/apps/web/src/hooks/useSidebarResize.ts index 5f4e712..9932858 100644 --- a/apps/web/src/hooks/useSidebarResize.ts +++ b/apps/web/src/hooks/useSidebarResize.ts @@ -1,6 +1,11 @@ -import { useState, useEffect, useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; -export function useSidebarResize(initialWidth: number, side: "left" | "right", minWidth: number, maxWidth: number) { +export function useSidebarResize( + initialWidth: number, + side: "left" | "right", + minWidth: number, + maxWidth: number, +) { const [width, setWidth] = useState(initialWidth); const [isResizing, setIsResizing] = useState(false); @@ -9,7 +14,7 @@ export function useSidebarResize(initialWidth: number, side: "left" | "right", m const handlePointerMove = (e: PointerEvent) => { e.preventDefault(); - let newWidth; + let newWidth: number; if (side === "left") { newWidth = e.clientX; } else { @@ -25,7 +30,7 @@ export function useSidebarResize(initialWidth: number, side: "left" | "right", m window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); - + document.body.style.cursor = "col-resize"; return () => { diff --git a/apps/web/src/lib/browser-preview-context.test.ts b/apps/web/src/lib/browser-preview-context.test.ts new file mode 100644 index 0000000..b28d7e2 --- /dev/null +++ b/apps/web/src/lib/browser-preview-context.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { browserPreviewActivity } from "./browser-preview-context"; + +describe("browserPreviewActivity", () => { + it("publishes cursors to subscribers and drops cleared ids", () => { + for (const id of browserPreviewActivity.snapshot().keys()) browserPreviewActivity.clear(id); + + const seen: string[][] = []; + const unsubscribe = browserPreviewActivity.subscribe((activities) => { + seen.push([...activities.keys()]); + }); + + try { + browserPreviewActivity.set("bot-a", 50, 40, "Ada"); + browserPreviewActivity.set("bot-a", 50, 40, "Ada"); + browserPreviewActivity.set("bot-b", 10, 20, "Bob"); + expect(browserPreviewActivity.snapshot().get("bot-a")).toEqual({ + x: 50, + y: 40, + label: "Ada", + }); + + browserPreviewActivity.clear("bot-a"); + browserPreviewActivity.clear("missing"); + + expect(seen).toEqual([[], ["bot-a"], ["bot-a", "bot-b"], ["bot-b"]]); + } finally { + unsubscribe(); + for (const id of [...browserPreviewActivity.snapshot().keys()]) { + browserPreviewActivity.clear(id); + } + } + + expect(browserPreviewActivity.snapshot().size).toBe(0); + }); +}); diff --git a/apps/web/src/lib/browser-preview-context.ts b/apps/web/src/lib/browser-preview-context.ts index 3f78722..e9a9621 100644 --- a/apps/web/src/lib/browser-preview-context.ts +++ b/apps/web/src/lib/browser-preview-context.ts @@ -1,46 +1,47 @@ -/** - * Browser preview activity tracking. - * - * Shared state so Shell can display multi-agent cursor positions - * inside browser-preview panels. - */ - -interface ActivityEntry { +export type BrowserPreviewCursor = { x: number; y: number; label: string; +}; + +type Listener = (activities: ReadonlyMap) => void; + +/** + * In-memory cursors for the desktop browser-preview panel. + * The shell publishes working bots here; the panel subscribes. + */ +class BrowserPreviewActivity { + private readonly activities = new Map(); + private readonly listeners = new Set(); + + subscribe(listener: Listener): () => void { + this.listeners.add(listener); + listener(this.snapshot()); + return () => { + this.listeners.delete(listener); + }; + } + + snapshot(): ReadonlyMap { + return new Map(this.activities); + } + + set(id: string, x: number, y: number, label: string) { + const current = this.activities.get(id); + if (current && current.x === x && current.y === y && current.label === label) return; + this.activities.set(id, { x, y, label }); + this.emit(); + } + + clear(id: string) { + if (!this.activities.delete(id)) return; + this.emit(); + } + + private emit() { + const snapshot = this.snapshot(); + for (const listener of this.listeners) listener(snapshot); + } } -type Subscriber = (snapshot: ReadonlyMap) => void; - -export const browserPreviewActivity = (() => { - const map = new Map(); - const listeners = new Set(); - - const freeze = (): ReadonlyMap => - new Map(map) as ReadonlyMap; - - return { - subscribe(cb: Subscriber): () => void { - listeners.add(cb); - cb(freeze()); - return () => { - listeners.delete(cb); - }; - }, - - set(id: string, x: number, y: number, label: string): void { - map.set(id, { x, y, label }); - listeners.forEach((l) => l(freeze())); - }, - - clear(id: string): void { - map.delete(id); - listeners.forEach((l) => l(freeze())); - }, - - snapshot(): ReadonlyMap { - return freeze(); - }, - }; -})(); +export const browserPreviewActivity = new BrowserPreviewActivity(); diff --git a/apps/web/src/lib/call-view-voice.ts b/apps/web/src/lib/call-view-voice.ts index 16d9e59..ead642a 100644 --- a/apps/web/src/lib/call-view-voice.ts +++ b/apps/web/src/lib/call-view-voice.ts @@ -27,11 +27,7 @@ export function shouldPromoteToWorking(input: { speakerStatus: SpeechStatus; userTurnPending: boolean; }): boolean { - return ( - input.userTurnPending && - input.phase === "thinking" && - input.speakerStatus === "idle" - ); + return input.userTurnPending && input.phase === "thinking" && input.speakerStatus === "idle"; } /** @deprecated use shouldPromoteToWorking */ @@ -85,10 +81,7 @@ export function voiceThinkingSubtitle(input: { } /** Subtitle when the mic is genuinely open for the user's turn. */ -export function voiceListeningSubtitle(input: { - heard: string; - idlePrompt: string; -}): string { +export function voiceListeningSubtitle(input: { heard: string; idlePrompt: string }): string { if (input.heard.trim()) return input.heard; return input.idlePrompt; } diff --git a/apps/web/src/lib/desktop.test.ts b/apps/web/src/lib/desktop.test.ts index c5a3495..177ad2c 100644 --- a/apps/web/src/lib/desktop.test.ts +++ b/apps/web/src/lib/desktop.test.ts @@ -67,9 +67,11 @@ describe("window chrome", () => { 'className="app-drag flex items-center justify-between border-b border-sidebar-border', ); expect(shell).toContain('className="app-no-drag grid size-control-lg'); - expect(shell).toContain('className="app-no-drag flex min-w-0 items-center"'); + expect(shell).toContain('className="app-no-drag flex min-w-0 items-center gap-3"'); // Ensure header sidebar toggle controls stay clickable (not part of drag region) - expect(shell.match(/app-no-drag .* size-control-lg .* place-items-center/g)?.length ?? 0).toBeGreaterThanOrEqual(2); + expect( + shell.match(/app-no-drag .* size-control-lg .* place-items-center/g)?.length ?? 0, + ).toBeGreaterThanOrEqual(2); }); it("moves window chrome into the conversation header when the bots sidebar is collapsed", () => { diff --git a/apps/web/src/lib/onboarding-discovery.test.ts b/apps/web/src/lib/onboarding-discovery.test.ts index 0f9e904..953198f 100644 --- a/apps/web/src/lib/onboarding-discovery.test.ts +++ b/apps/web/src/lib/onboarding-discovery.test.ts @@ -1,12 +1,12 @@ +import type { IntegrationSetupState } from "@rakazo/contracts"; import { describe, expect, it } from "vitest"; import { computeDiscoveryItems, countConfiguredIntegrations, + type DiscoveryContext, discoveryProgress, summarizeProfileSignals, - type DiscoveryContext, } from "./onboarding-discovery.js"; -import type { IntegrationSetupState } from "@rakazo/contracts"; describe("countConfiguredIntegrations", () => { it("counts configured providers", () => { @@ -30,13 +30,26 @@ describe("countConfiguredIntegrations", () => { describe("summarizeProfileSignals", () => { it("includes name and timezone", () => { expect( - summarizeProfileSignals({ name: "Alice", timezone: "Europe/Vienna", locale: "de-DE", languages: [], harvestedAt: new Date().toISOString() }), + summarizeProfileSignals({ + name: "Alice", + timezone: "Europe/Vienna", + locale: "de-DE", + languages: [], + harvestedAt: new Date().toISOString(), + }), ).toBe("Alice · Europe/Vienna"); }); it("includes uiLocale when non-en", () => { expect( - summarizeProfileSignals({ name: "Bob", timezone: "", locale: "de", uiLocale: "de", languages: [], harvestedAt: new Date().toISOString() }), + summarizeProfileSignals({ + name: "Bob", + timezone: "", + locale: "de", + uiLocale: "de", + languages: [], + harvestedAt: new Date().toISOString(), + }), ).toBe("Bob · · de"); }); }); @@ -45,12 +58,23 @@ describe("computeDiscoveryItems", () => { it("returns only three visible onboarding steps", () => { const context: DiscoveryContext = { phase: "assembly", - me: { needsModel: false, computerHost: null, email: "a@b.com", name: "Test", defaultProvider: null, defaultModel: null }, + me: { + needsModel: false, + computerHost: null, + email: "a@b.com", + name: "Test", + defaultProvider: null, + defaultModel: null, + }, crewReady: true, profilePersisted: true, }; const items = computeDiscoveryItems(context); - expect(items.map((i) => i.id)).toEqual(["profile-basics", "connected-tools", "specialist-crew"]); + expect(items.map((i) => i.id)).toEqual([ + "profile-basics", + "connected-tools", + "specialist-crew", + ]); }); it("has length 3", () => { diff --git a/apps/web/src/lib/onboarding-discovery.ts b/apps/web/src/lib/onboarding-discovery.ts index ecf7465..2226ae9 100644 --- a/apps/web/src/lib/onboarding-discovery.ts +++ b/apps/web/src/lib/onboarding-discovery.ts @@ -1,10 +1,7 @@ import type { IntegrationSetupState, Me } from "@rakazo/contracts"; import { collectLocalProfileSignals, type UserProfileSignals } from "./user-profile-harvest"; -export type DiscoverySourceId = - | "profile-basics" - | "connected-tools" - | "specialist-crew"; +export type DiscoverySourceId = "profile-basics" | "connected-tools" | "specialist-crew"; export type DiscoveryStatus = "pending" | "scanning" | "found" | "skipped"; diff --git a/apps/web/src/lib/onboarding-look-categories.test.ts b/apps/web/src/lib/onboarding-look-categories.test.ts index 86b1136..cfe634e 100644 --- a/apps/web/src/lib/onboarding-look-categories.test.ts +++ b/apps/web/src/lib/onboarding-look-categories.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { - LOOK_CATEGORIES, buildAvatarValue, findLookCategory, + LOOK_CATEGORIES, lookPresetToAvatarValue, } from "./onboarding-look-categories"; diff --git a/apps/web/src/lib/oo-audio-bus.ts b/apps/web/src/lib/oo-audio-bus.ts index 4a9b1a6..a8967f2 100644 --- a/apps/web/src/lib/oo-audio-bus.ts +++ b/apps/web/src/lib/oo-audio-bus.ts @@ -20,7 +20,6 @@ function audioContextCtor(): AudioContextCtor | undefined { class OoAudioBus { private ctx: AudioContext | null = null; - private master: GainNode | null = null; private sfxGain: GainNode | null = null; private voiceHold = 0; @@ -37,7 +36,6 @@ class OoAudioBus { sfxGain.gain.value = SFX_MASTER_GAIN; sfxGain.connect(master); this.ctx = ctx; - this.master = master; this.sfxGain = sfxGain; return ctx; } catch { diff --git a/apps/web/src/lib/user-help-vibe.test.ts b/apps/web/src/lib/user-help-vibe.test.ts index d85ba09..7f39b42 100644 --- a/apps/web/src/lib/user-help-vibe.test.ts +++ b/apps/web/src/lib/user-help-vibe.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; -import { collectLocalProfileSignals } from "./user-profile-harvest"; import { FOCUS_HELP_VIBE_MAP, inferHelpVibeHints, @@ -7,6 +6,7 @@ import { mergeHelpVibeIntoMemory, summarizeHelpVibe, } from "./user-help-vibe"; +import { collectLocalProfileSignals } from "./user-profile-harvest"; describe("user-help-vibe", () => { it("does not invent lifestyle labels from locale alone", () => { @@ -31,7 +31,7 @@ describe("user-help-vibe", () => { email: "d@b.c", defaultProvider: null, defaultModel: null, - computerHost: null, + computerHost: "docker", }), ); expect(hints.suggestedLanes).toContain("builder"); diff --git a/apps/web/src/lib/user-profile-harvest.test.ts b/apps/web/src/lib/user-profile-harvest.test.ts index a8b7b69..3c69655 100644 --- a/apps/web/src/lib/user-profile-harvest.test.ts +++ b/apps/web/src/lib/user-profile-harvest.test.ts @@ -77,7 +77,7 @@ describe("user-profile-harvest", () => { email: "ingo@example.com", defaultProvider: null, defaultModel: null, - computerHost: null, + computerHost: "docker", }), ); expect(updates).toHaveLength(1); diff --git a/apps/web/src/lib/user-relationship-contract.test.ts b/apps/web/src/lib/user-relationship-contract.test.ts index 225aa21..43aac88 100644 --- a/apps/web/src/lib/user-relationship-contract.test.ts +++ b/apps/web/src/lib/user-relationship-contract.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from "vitest"; +import { collectLocalProfileSignals, mergeFullUserProfileMemory } from "./user-profile-harvest"; import { mergeRelationshipContractIntoMemory, RELATIONSHIP_SECTION, } from "./user-relationship-contract"; -import { mergeFullUserProfileMemory } from "./user-profile-harvest"; -import { collectLocalProfileSignals } from "./user-profile-harvest"; describe("user-relationship-contract", () => { it("adds relationship section once", () => { diff --git a/apps/web/src/lib/voice-handoff.test.ts b/apps/web/src/lib/voice-handoff.test.ts index eb94248..d693e09 100644 --- a/apps/web/src/lib/voice-handoff.test.ts +++ b/apps/web/src/lib/voice-handoff.test.ts @@ -61,9 +61,7 @@ describe("voiceHandoffTransferLine", () => { describe("voiceHandoffIdleCaption", () => { it("shows hang-up guidance instead of an open invitation", () => { - expect(voiceHandoffIdleCaption("Boss")).toBe( - "Handed to Boss — hang up to continue in text.", - ); + expect(voiceHandoffIdleCaption("Boss")).toBe("Handed to Boss — hang up to continue in text."); }); }); diff --git a/apps/web/src/pages/CallView.tsx b/apps/web/src/pages/CallView.tsx index 7dee149..2ba7e04 100644 --- a/apps/web/src/pages/CallView.tsx +++ b/apps/web/src/pages/CallView.tsx @@ -25,15 +25,15 @@ import { voiceThinkingSubtitle, WORKING_PROMOTE_DELAY_MS, } from "../lib/call-view-voice"; +import { dictation } from "../lib/dictation"; +import { ooSound } from "../lib/oo-sound"; +import { speaker } from "../lib/tts"; import { extractOutgoingHandoff, isClientWorkshopSpecialty, voiceHandoffIdleCaption, voiceHandoffTransferLine, } from "../lib/voice-handoff"; -import { dictation } from "../lib/dictation"; -import { ooSound } from "../lib/oo-sound"; -import { speaker } from "../lib/tts"; import { personaIdlePrompt, personaPhaseLabel, @@ -115,10 +115,7 @@ export function CallView({ }), [botName, botTitle, botDescription], ); - const outgoingHandoff = useMemo( - () => extractOutgoingHandoff(snapshot, botId), - [snapshot, botId], - ); + const outgoingHandoff = useMemo(() => extractOutgoingHandoff(snapshot, botId), [snapshot, botId]); const outgoingHandoffRef = useRef(outgoingHandoff); outgoingHandoffRef.current = outgoingHandoff; const handoffGated = Boolean(outgoingHandoff); @@ -305,8 +302,8 @@ export function CallView({ const handoff = outgoingHandoffRef.current; const line = handoff ? voiceHandoffIdleCaption(handoff.toBotName) - : thinkingLines[thinkingIndexRef.current] ?? - t(personaPhaseLabel(personaRef.current.archetype, "thinking")); + : (thinkingLines[thinkingIndexRef.current] ?? + t(personaPhaseLabel(personaRef.current.archetype, "thinking"))); setCaption(line); } else if (state.status === "speaking") { setCallPhase("speaking"); @@ -338,7 +335,6 @@ export function CallView({ }; }, [botId]); - useEffect(() => { outgoingHandoffRef.current = outgoingHandoff; if (!outgoingHandoff) return; @@ -413,7 +409,9 @@ export function CallView({ openingInvitePendingRef.current && shouldSpeakOpeningInvitation({ hasUnspokenBotMessage: Boolean( - lastBot && lastBot.id !== spokenMessage.current && speechFromBlocks(lastBot.blocks).trim(), + lastBot && + lastBot.id !== spokenMessage.current && + speechFromBlocks(lastBot.blocks).trim(), ), runActive: isActiveRunStatus(snapshot?.run?.status), userTurnPending: userTurnPendingRef.current, diff --git a/apps/web/src/pages/Onboarding.tsx b/apps/web/src/pages/Onboarding.tsx index 3d126dd..2acbd3b 100644 --- a/apps/web/src/pages/Onboarding.tsx +++ b/apps/web/src/pages/Onboarding.tsx @@ -147,7 +147,7 @@ export function OnboardingPage() { const fieldId = useId(); const [step, setStep] = useState< "loading" | "emoji" | "preferences" | "language" | "voice" | "integrations" | "bot" - >("loading"); + >("emoji"); const [selectedVoiceId, setSelectedVoiceId] = useState(""); const [voiceSkipped, setVoiceSkipped] = useState(false); const [integrationSetup, setIntegrationSetup] = useState(null); @@ -157,7 +157,7 @@ export function OnboardingPage() { const [bootstrapMe, setBootstrapMe] = useState> | null>(null); const [profilePersisted, setProfilePersisted] = useState(false); const [crewReady, setCrewReady] = useState(false); - const [helpVibe, setHelpVibe] = useState(null); + const [_helpVibe, setHelpVibe] = useState(null); const [emojiConfig, setEmojiConfig] = useState(defaultEmojiConfig); const [prefs, setPrefs] = useState(defaultPreferences()); @@ -170,15 +170,41 @@ export function OnboardingPage() { }); useEffect(() => { - void Promise.all([rpc.me(), rpc.integrationSetup.get().catch(() => null)]) - .then(([me, integrations]) => { + let cancelled = false; + const bootstrap = Promise.race([ + Promise.all([rpc.me(), rpc.integrationSetup.get().catch(() => null)]), + new Promise((_, reject) => { + setTimeout(() => reject(new Error("onboarding bootstrap timeout")), 15_000); + }), + ]); + void bootstrap + .then(async ([me, integrations]) => { + if (cancelled) return; setBootstrapMe(me); setHelpVibe(inferHelpVibeHints(collectLocalProfileSignals(me))); setIntegrationSetup(integrations); setStep(me.needsModel ? "emoji" : integrations?.needsSetup ? "integrations" : "emoji"); + // A space that already has a bot does not need the designer again. + // Empty spaces (a new project) still walk onboarding. Resolve this after + // the first step renders so a slow spaces list cannot leave E2E on loading. + try { + const navigation = await rpc.spaces.list(); + if (cancelled) return; + const existing = navigation.current.bots[0]; + if (existing && !me.needsModel) { + navigate(`/app/${existing.id}`, { replace: true }); + } + } catch { + // Best effort; fresh signups stay on onboarding. + } }) - .catch(() => setStep("emoji")); - }, []); + .catch(() => { + if (!cancelled) setStep("emoji"); + }); + return () => { + cancelled = true; + }; + }, [navigate]); function toggleHobby(key: string) { const hobbies = prefs.hobbies.includes(key) @@ -190,10 +216,7 @@ export function OnboardingPage() { const discoveryPhase: OnboardingDiscoveryPhase = step === "loading" ? "loading" - : step === "emoji" || - step === "preferences" || - step === "language" || - step === "voice" + : step === "emoji" || step === "preferences" || step === "language" || step === "voice" ? "emoji" : step === "integrations" ? "integrations" @@ -244,7 +267,9 @@ export function OnboardingPage() { await rpc.voice .connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN, voiceId: selectedVoiceId }) .catch(() => null); - await rpc.voice.setVoice({ voiceId: selectedVoiceId, provider: "kokoro" }).catch(() => null); + await rpc.voice + .setVoice({ voiceId: selectedVoiceId, provider: "kokoro" }) + .catch(() => null); await rpc.bots .update({ botId: bot.id, voiceId: selectedVoiceId, autoSpeak: true }) .catch(() => null); @@ -288,7 +313,11 @@ export function OnboardingPage() { } return ( -
+
-
-
-
-
+ -
-
-
+ -
-
-
+
: } + icon={ + isLocal ? ( + + ) : ( + + ) + } label={t`Data storage`} detail={ isLocal @@ -145,7 +151,7 @@ export function PrivacySettingsPanel({
{t`Sandbox`} - {isLocal ? t`Local Docker` : sandboxProvider ?? t`Not configured`} + {isLocal ? t`Local Docker` : (sandboxProvider ?? t`Not configured`)}
{email ? ( @@ -174,17 +180,10 @@ export function PrivacySettingsPanel({

{t`Permanently delete your account, all conversations, and associated data. This action cannot be undone.`}

- - {deleteError ? ( -

{deleteError}

- ) : null} + {deleteError ?

{deleteError}

: null}
diff --git a/apps/web/src/pages/ProjectSessionTimeline.tsx b/apps/web/src/pages/ProjectSessionTimeline.tsx index 3ba05e7..42762f4 100644 --- a/apps/web/src/pages/ProjectSessionTimeline.tsx +++ b/apps/web/src/pages/ProjectSessionTimeline.tsx @@ -180,7 +180,10 @@ export function ProjectSessionTimeline({
-

+

Goals

@@ -233,7 +236,10 @@ export function ProjectSessionTimeline({ -
@@ -287,7 +293,10 @@ export function ProjectSessionTimeline({
-

+

Unterhaltungen

{groupName} @@ -345,7 +354,10 @@ export function ProjectSessionTimeline({ -
diff --git a/apps/web/src/pages/RoutineEditor.tsx b/apps/web/src/pages/RoutineEditor.tsx index c4df5b9..5f55284 100644 --- a/apps/web/src/pages/RoutineEditor.tsx +++ b/apps/web/src/pages/RoutineEditor.tsx @@ -516,7 +516,11 @@ function MessageTriggerCard({ provider, onRemove }: { provider: string; onRemove return (
- + {label} - + {group.canDeleteSpace ? ( + - {group.canDeleteSpace ? ( - - ) : null} -
- ) : null} - {!collapsed && - group.bots.map((item) => ( - + ) : null} +
+ ) : null} + {!collapsed && + group.bots.map((item) => ( +
-
-
- {cleanRosterPreview(item.chat.preview)} -
-

- - ))} -
- - ); - })} + ) : ( + + )} +
+
+
+ + {item.chat.name} + +
+
+ + {formatRosterTime(item.chat.updatedAt)} + + {item.chat.unread ? ( +
+
+
+ {cleanRosterPreview(item.chat.preview)} +
+
+ + ))} +
+ + ); + })}
@@ -4418,7 +4567,10 @@ export function ShellPage() { data-testid="side-panel" data-panel={panel ?? "closed"} style={{ - width: panel && (active || activeGroup || panel === "create" || panel === "browser") ? rightSidebarWidth : 0, + width: + panel && (active || activeGroup || panel === "create" || panel === "browser") + ? rightSidebarWidth + : 0, }} className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${ panel && (active || activeGroup || panel === "create" || panel === "browser") @@ -4495,7 +4647,13 @@ export function ShellPage() { {panel === "browser" ? (
- setBrowserUrl(e.target.value)} onBlur={() => navigateBrowser(browserUrl)} - onKeyDown={(e) => { if (e.key === "Enter") navigateBrowser(browserUrl); }} + onKeyDown={(e) => { + if (e.key === "Enter") navigateBrowser(browserUrl); + }} placeholder="http://localhost:5173" className="flex-1 rounded-md bg-muted/60 px-2.5 py-1 text-xs text-foreground placeholder:text-muted-foreground/60 outline-none focus:ring-1 focus:ring-ring/50" /> {isDesktop ? ( devServerRunning ? ( - + Dev ) : ( - ) @@ -4533,10 +4698,14 @@ export function ShellPage() { const cx = Math.min(100, Math.max(0, activity.x)) + "%"; const cy = Math.min(100, Math.max(0, activity.y)) + "%"; return ( -
+
-
+
{activity.label}
@@ -4671,37 +4840,39 @@ export function ShellPage() { /> ) : null} {/* Centered Create Bot Modal */} - {showCreateBotModal && createPortal( -
setShowCreateBotModal(false)} - > -
e.stopPropagation()} - > -
-

- Create new Bot -

- -
- setShowCreateBotModal(false)} - onCreate={async (input) => { - await createBot(input); - }} + {showCreateBotModal && + createPortal( +
+
-
, - document.body, - )} +
+
+

+ Create new Bot +

+ +
+ setShowCreateBotModal(false)} + onCreate={async (input) => { + await createBot(input); + }} + /> +
+
, + document.body, + )} {panel === "settings" && active ? ( { + setSettingsOpen(false); + setMcpOpen(true); + }} onClose={() => { setSettingsOpen(false); setSettingsSection("general"); @@ -6126,6 +6302,7 @@ const Composer = memo(function Composer({ const [selectedMentions, setSelectedMentions] = useState([]); const textareaRef = useRef(null); const runErrorRef = useRef(null); + const submitFingerprint = useRef(null); useEffect(() => { if (!sendError && !runError) return; const timer = setTimeout(() => { @@ -6204,6 +6381,7 @@ const Composer = memo(function Composer({ }, [draft]); function updateDraft(value: string) { + submitFingerprint.current = null; setDraft(value); const mentionMatch = /(?:^|\s)@([\w-]*)$/.exec(value); setMentionQuery(mentionMatch ? (mentionMatch[1] ?? "") : null); @@ -6260,7 +6438,7 @@ const Composer = memo(function Composer({ useEffect(() => { setMentionHighlightIndex(0); - }, [mentionQuery, mentionOptions]); + }, [mentionQuery]); const activeMentionIndex = clampMentionHighlightIndex( mentionHighlightIndex, @@ -6298,8 +6476,13 @@ const Composer = memo(function Composer({ (slashSkillOptions.length > 0 || slashActionOptions.length > 0); function send() { - if (!canSend || sending || disabled) return; + if (!canSend || disabled) return; const text = serializeComposerPrompt(draft, selectedSkill, selectedMentions); + const fingerprint = [text, ...selectedMentions.map((mention) => mentionChipKey(mention))].join( + "\n", + ); + if (submitFingerprint.current === fingerprint) return; + submitFingerprint.current = fingerprint; setDraft(""); setMentionQuery(null); setMentionHighlightIndex(0); @@ -6497,7 +6680,7 @@ const Composer = memo(function Composer({ role="listbox" aria-label={t`Mentions`} data-testid="mention-picker" - className="mb-2 overflow-hidden rounded-[14px] border border-border bg-muted" + className="mb-2 max-h-64 overflow-y-auto rounded-[14px] border border-border bg-muted" > {mentionOptions.map((mention, index) => { const optionId = `${mentionListboxId}-option-${index}`; @@ -6509,7 +6692,8 @@ const Composer = memo(function Composer({ type="button" role="option" aria-selected={highlighted} - aria-label={t`@${mention.name}`} + aria-label={`@${mention.name}`} + data-mention-name={mention.name} onMouseDown={(event) => event.preventDefault()} onClick={() => insertMention(mention)} onMouseEnter={() => setMentionHighlightIndex(index)} @@ -6517,7 +6701,9 @@ const Composer = memo(function Composer({ highlighted ? "bg-accent" : "" }`} > - + @{mention.name} @@ -6525,6 +6711,7 @@ const Composer = memo(function Composer({ {mention.subtitle ? (
-
+
Get notified when this Bot finishes or needs input
diff --git a/apps/web/src/pages/shell/message-cards.tsx b/apps/web/src/pages/shell/message-cards.tsx index 569f0e5..0112f1a 100644 --- a/apps/web/src/pages/shell/message-cards.tsx +++ b/apps/web/src/pages/shell/message-cards.tsx @@ -80,6 +80,8 @@ export function ChoiceCard({
- - - @@ -118,7 +118,7 @@

-
@@ -318,8 +318,8 @@

Self-Improvement & System Event Log

- - + +
diff --git a/evoo-platform/server-config.js b/evoo-platform/server-config.js index 218d9c8..154151e 100644 --- a/evoo-platform/server-config.js +++ b/evoo-platform/server-config.js @@ -40,7 +40,14 @@ export function isPrivilegedCockpitRoute(pathname) { export function parseCorsOriginAllowlist(value) { if (!value?.trim()) return []; - return [...new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean))]; + return [ + ...new Set( + value + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + ), + ]; } /** diff --git a/evoo-platform/server-config.test.js b/evoo-platform/server-config.test.js index 17b34b3..eddaea8 100644 --- a/evoo-platform/server-config.test.js +++ b/evoo-platform/server-config.test.js @@ -176,7 +176,11 @@ describe("applyCockpitCorsHeaders", () => { it("sets reflected origins and never sets *", () => { const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({})); const headers = {}; - const res = { setHeader: (key, value) => { headers[key] = value; } }; + const res = { + setHeader: (key, value) => { + headers[key] = value; + }, + }; const allowed = applyCockpitCorsHeaders( { headers: { origin: "http://127.0.0.1:5173" } }, res, @@ -193,7 +197,11 @@ describe("applyCockpitCorsHeaders", () => { it("omits Access-Control-Allow-Origin for disallowed cross-origin requests", () => { const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({})); const headers = {}; - const res = { setHeader: (key, value) => { headers[key] = value; } }; + const res = { + setHeader: (key, value) => { + headers[key] = value; + }, + }; const allowed = applyCockpitCorsHeaders( { headers: { origin: "https://evil.example" } }, res, diff --git a/infra/sandboxes/supervisor/src/index.test.ts b/infra/sandboxes/supervisor/src/index.test.ts index cda1198..59f2ee7 100644 --- a/infra/sandboxes/supervisor/src/index.test.ts +++ b/infra/sandboxes/supervisor/src/index.test.ts @@ -241,7 +241,8 @@ describe("sandbox supervisor input containment", () => { expect(normalizeWorkspaceRelative("/notes\\result.txt")).toBe("notes/result.txt"); expect(normalizeWorkspaceRelative("//notes///result.txt")).toBe("notes/result.txt"); expect(() => normalizeWorkspaceRelative("../outside")).toThrow(/escapes/); - expect(() => normalizeWorkspaceRelative("notes/./result.txt")).toThrow(/escapes/); + // "." is the current directory and stays inside the workspace. + expect(normalizeWorkspaceRelative("notes/./result.txt")).toBe("notes/result.txt"); expect(() => normalizeWorkspaceRelative("notes/../outside")).toThrow(/escapes/); }); diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index a7d532d..b4654c4 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -29,7 +29,10 @@ describe("server image publish workflow", () => { .filter((entry) => entry.arch !== undefined) .map((entry) => [entry.arch, entry.runner]), ); - expect(runners).toEqual({ amd64: "ubuntu-latest", arm64: "ubuntu-24.04-arm" }); + expect(runners).toEqual({ + amd64: ["self-hosted", "evoo-ci", "Linux"], + arm64: ["self-hosted", "evoo-ci", "macOS"], + }); }); it("publishes one verified multi-arch manifest per image after both builds", () => { diff --git a/packages/adapter-kit/src/types.ts b/packages/adapter-kit/src/types.ts index afd90d4..d9ebb31 100644 --- a/packages/adapter-kit/src/types.ts +++ b/packages/adapter-kit/src/types.ts @@ -600,6 +600,11 @@ export interface MessagingInboundMessage { channelName: string | null; /** Group roster addresses when the platform reports them; often empty. */ participants: string[]; + /** + * True when `participants` is the complete roster. Partial webhooks leave this + * unset so members missing from the payload are not marked as having left. + */ + participantsAuthoritative?: boolean; content: string; mediaUrl: string | null; /** Team-room workspace/team id when the platform reports one (Slack team_id, …). */ diff --git a/packages/adapters/src/computer-idle.test.ts b/packages/adapters/src/computer-idle.test.ts index 04054ff..c85f00d 100644 --- a/packages/adapters/src/computer-idle.test.ts +++ b/packages/adapters/src/computer-idle.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, rmSync, symlinkSync } from "node:fs"; import type { AgentHomeStore, JobPublisher, SandboxProvider } from "@rakazo/adapter-kit"; import type { PrismaClient, ThreadEvents } from "@rakazo/db"; @@ -244,6 +244,13 @@ describe("sandbox idle", () => { describe("background work launch and probe", () => { const children: ReturnType[] = []; const markers = new Set(); + const pollOpts = { timeout: 10_000, interval: 100 }; + + function prepareMarker(path: string) { + markers.add(path); + // Persistent self-hosted runners can leave markers when a prior run aborts mid-test. + rmSync(path, { force: true, recursive: true }); + } afterEach(() => { for (const child of children.splice(0)) { @@ -261,7 +268,7 @@ describe("background work launch and probe", () => { const databaseId = "computer-db-id"; const providerRef = "provider-ref"; const launchId = "active"; - markers.add(`/tmp/rakazo-background-${databaseId}-run-1-${launchId}`); + prepareMarker(`/tmp/rakazo-background-${databaseId}-run-1-${launchId}`); const launched = spawn( "bash", @@ -278,7 +285,7 @@ describe("background work launch and probe", () => { ); children.push(launched); - await expect.poll(() => probeBackgroundWork(databaseId)).toBe(0); + await expect.poll(() => probeBackgroundWork(databaseId), pollOpts).toBe(0); // ComputerRef.id is providerRef today; probing that path must not see the DB-id marker. expect(await probeBackgroundWork(providerRef)).toBe(1); }, @@ -290,8 +297,8 @@ describe("background work launch and probe", () => { const markerId = "computer-relaunch-id"; const completedMarker = `/tmp/rakazo-background-${markerId}-run-1-completed`; const activeMarker = `/tmp/rakazo-background-${markerId}-run-1-active`; - markers.add(completedMarker); - markers.add(activeMarker); + prepareMarker(completedMarker); + prepareMarker(activeMarker); const completed = spawn( "bash", [ @@ -325,7 +332,7 @@ describe("background work launch and probe", () => { { stdio: "ignore" }, ); children.push(active); - await expect.poll(() => probeBackgroundWork(markerId)).toBe(0); + await expect.poll(() => probeBackgroundWork(markerId), pollOpts).toBe(0); }, ); @@ -335,8 +342,8 @@ describe("background work launch and probe", () => { const markerId = "computer-marker-error"; const marker = `/tmp/rakazo-background-${markerId}-run-1-collision`; const commandRan = `/tmp/rakazo-background-command-ran-${markerId}`; - markers.add(marker); - markers.add(commandRan); + prepareMarker(marker); + prepareMarker(commandRan); mkdirSync(marker); const launched = spawn( "bash", @@ -364,8 +371,8 @@ describe("background work launch and probe", () => { const markerId = "computer-marker-symlink"; const marker = `/tmp/rakazo-background-${markerId}-run-1-collision`; const commandRan = `/tmp/rakazo-background-command-ran-${markerId}`; - markers.add(marker); - markers.add(commandRan); + prepareMarker(marker); + prepareMarker(commandRan); symlinkSync(commandRan, marker); const launched = spawn( "bash", @@ -394,7 +401,7 @@ describe("background work launch and probe", () => { const runId = "run-cancel-1"; const launchId = "active"; const marker = `/tmp/rakazo-background-${computerId}-${runId}-${launchId}`; - markers.add(marker); + prepareMarker(marker); const launched = spawn( "bash", @@ -411,7 +418,7 @@ describe("background work launch and probe", () => { ); children.push(launched); - await expect.poll(() => probeBackgroundWork(computerId)).toBe(0); + await expect.poll(() => probeBackgroundWork(computerId), pollOpts).toBe(0); const launchedDone = processExit(launched); const cancel = spawn( @@ -425,6 +432,48 @@ describe("background work launch and probe", () => { expect(await probeBackgroundWork(computerId)).toBe(1); }, ); + + it.skipIf(process.platform !== "linux" || !existsSync("/dev/shm"))( + "detects and cancels a flock when /proc/locks zero-pads the device id", + async () => { + const root = "/dev/shm"; + const computerId = "computer-shm-pad"; + const runId = "run-1"; + const launchId = "active"; + const marker = `${root}/rakazo-background-${computerId}-${runId}-${launchId}`; + prepareMarker(marker); + const prefix = "/tmp/rakazo-background-"; + const shmPrefix = `${root}/rakazo-background-`; + const launch = BACKGROUND_WORK_LAUNCH.replaceAll(prefix, shmPrefix); + const probe = BACKGROUND_WORK_PROBE.replaceAll(prefix, shmPrefix); + const cancelScript = CANCEL_COMPUTER_RUN_WORK.replaceAll(prefix, shmPrefix); + + const launched = spawn( + "bash", + ["-c", launch, "rakazo-background-launch", computerId, runId, launchId, "exec sleep 30"], + { stdio: "ignore" }, + ); + children.push(launched); + + await expect.poll(() => probeBackgroundWork(computerId, probe), pollOpts).toBe(0); + const listed = spawnSync("stat", ["-c", "%Hd %Ld", marker], { encoding: "utf8" }); + const parts = listed.stdout.trim().split(/\s+/).map(Number); + expect(parts.length).toBeGreaterThanOrEqual(2); + expect(parts[0]! < 16 || parts[1]! < 16).toBe(true); + + const launchedDone = processExit(launched); + const cancel = spawn( + "bash", + ["-c", cancelScript, "rakazo-cancel-run-work", computerId, runId], + { stdio: "ignore" }, + ); + children.push(cancel); + expect(await processExit(cancel)).toBe(0); + await launchedDone; + expect(await probeBackgroundWork(computerId, probe)).toBe(1); + expect(existsSync(marker)).toBe(false); + }, + ); }); describe("CANCEL_PRIMARY_BROWSER_WORK", () => { @@ -545,13 +594,11 @@ function idleHarness( }; } -function probeBackgroundWork(markerId: string): Promise { +function probeBackgroundWork(markerId: string, script = BACKGROUND_WORK_PROBE): Promise { return new Promise((resolve, reject) => { - const child = spawn( - "bash", - ["-c", BACKGROUND_WORK_PROBE, "rakazo-background-probe", markerId], - { stdio: ["ignore", "pipe", "pipe"] }, - ); + const child = spawn("bash", ["-c", script, "rakazo-background-probe", markerId], { + stdio: ["ignore", "pipe", "pipe"], + }); child.on("error", reject); child.on("close", (code) => resolve(code ?? 1)); }); diff --git a/packages/adapters/src/computer-idle.ts b/packages/adapters/src/computer-idle.ts index 8399d5b..d84e1da 100644 --- a/packages/adapters/src/computer-idle.ts +++ b/packages/adapters/src/computer-idle.ts @@ -18,6 +18,14 @@ const BACKGROUND_WORK_IDLE_SENTINEL = "rakazo-background-idle"; export const BACKGROUND_WORK_LAUNCH = [ `marker="${BACKGROUND_WORK_MARKER_PREFIX}$1-$2-$3"`, + // Refuse an existing path before flock: flock follows symlinks and can lock a directory. + 'if [ -e "$marker" ] || [ -L "$marker" ]; then', + " exit 1", + "fi", + // Hold the marker with flock on the command process. A full /proc fd walk hangs on a busy host. + "if [ -r /proc/locks ] && command -v flock >/dev/null 2>&1 && command -v stat >/dev/null 2>&1; then", + ' exec flock -n -F "$marker" bash -lc "$4"', + "fi", "set -o noclobber", 'exec 9>"$marker" || exit 1', "set +o noclobber", @@ -32,6 +40,34 @@ export const CANCEL_COMPUTER_RUN_WORK = [ `prefix="${BACKGROUND_WORK_MARKER_PREFIX}$computerId-$runId-"`, // Match the timeout wrapper cmdline (still contains the launch tag after exec into the user command). 'pkill -TERM -f "rakazo-background-launch ${computerId} ${runId} " 2>/dev/null || true', + "if [ -r /proc/locks ] && command -v flock >/dev/null 2>&1 && command -v stat >/dev/null 2>&1; then", + " signal_lock_holders() {", + ' sig="$1"', + ' for marker in "$prefix"*; do', + ' [ -f "$marker" ] && [ ! -L "$marker" ] || continue', + ' major="$(stat -c %Hd -- "$marker" 2>/dev/null)" || continue', + ' minor="$(stat -c %Ld -- "$marker" 2>/dev/null)" || continue', + ' inode="$(stat -c %i -- "$marker" 2>/dev/null)" || continue', + // fs/locks.c prints major:minor as %02x. Bare %x drops the leading zero for ids under 16. + ' key="$(printf %02x:%02x:%s "$major" "$minor" "$inode")"', + " while read -r _id type _sub _mode pid dev _rest; do", + ' [ "$type" = "FLOCK" ] || continue', + ' [ "$dev" = "$key" ] || continue', + ' case "$pid" in', + ' ""|*[!0-9]*) continue ;;', + " esac", + // Never kill -PID (process group): sandbox work often shares the caller's PGID. + ' kill "-$sig" "$pid" 2>/dev/null || true', + " done < /proc/locks", + " done", + " }", + " signal_lock_holders TERM", + " sleep 0.2", + ' pkill -KILL -f "rakazo-background-launch ${computerId} ${runId} " 2>/dev/null || true', + " signal_lock_holders KILL", + ' rm -f -- "$prefix"* 2>/dev/null || true', + " exit 0", + "fi", "if [ -d /proc ]; then", " for fd in /proc/[0-9]*/fd/*; do", ' target="$(readlink "$fd" 2>/dev/null)" || continue', @@ -143,6 +179,29 @@ export const BACKGROUND_WORK_PROBE = [ `prefix="${BACKGROUND_WORK_MARKER_PREFIX}$1-"`, `idle() { printf '${BACKGROUND_WORK_IDLE_SENTINEL}\\n'; exit 1; }`, 'markers=("$prefix"*)', + // Same gate as launch. Do not fall through to an fd walk: that walk is what timed out on CI, + // including the idle path that must delete a finished marker and return quickly. + // Try the lock instead of parsing /proc/locks. stat major/minor are decimal, and the kernel + // prints them as %02x, so a bare %x key never matches tmpfs (00:) or disk partitions (08:01). + // A mismatch used to delete the live marker, so every later probe stayed idle. + "if [ -r /proc/locks ] && command -v flock >/dev/null 2>&1 && command -v stat >/dev/null 2>&1; then", + ' for marker in "${markers[@]}"; do', + ' [ -e "$marker" ] || [ -L "$marker" ] || continue', + ' if [ -L "$marker" ] || [ ! -f "$marker" ]; then', + ' rm -f -- "$marker" 2>/dev/null || true', + " continue", + " fi", + // 99 is the lock conflict. Exit 1 is the idle sentinel and must not be reused here. + ' if flock -n -E 99 -- "$marker" true; then', + ' rm -f -- "$marker"', + " else", + ' status="$?"', + ' [ "$status" -eq 99 ] && exit 0', + " exit 2", + " fi", + " done", + " idle", + "fi", "if [ -d /proc ]; then", " command -v readlink >/dev/null 2>&1 || exit 2", " for fd in /proc/[0-9]*/fd/*; do", diff --git a/packages/adapters/src/computer-lifecycle.test.ts b/packages/adapters/src/computer-lifecycle.test.ts index f15391c..bb9cfaf 100644 --- a/packages/adapters/src/computer-lifecycle.test.ts +++ b/packages/adapters/src/computer-lifecycle.test.ts @@ -1475,6 +1475,7 @@ describe("computer execution leases", () => { .mockResolvedValue({ nextEventSeq: 1 }), }, run: { + count: vi.fn().mockResolvedValue(0), findMany: vi.fn().mockResolvedValue([{ id: "run-1", taskId: "task-1" }]), updateMany: vi.fn(), }, diff --git a/packages/adapters/src/create-project.test.ts b/packages/adapters/src/create-project.test.ts index 745f575..fb4f338 100644 --- a/packages/adapters/src/create-project.test.ts +++ b/packages/adapters/src/create-project.test.ts @@ -1,35 +1,41 @@ -import { describe, expect, it } from "vitest"; import type { PrismaClient } from "@rakazo/db"; +import { describe, expect, it } from "vitest"; import { createProject } from "./create-project.js"; describe("createProject", () => { it("requires a non-empty project name", async () => { - const result = await createProject({ prisma: {} as PrismaClient }, { - actor: { - userId: "user-1", - spaceId: "space-1", - email: "user@example.test", - isDeploymentOwner: false, + const result = await createProject( + { prisma: {} as PrismaClient }, + { + actor: { + userId: "user-1", + spaceId: "space-1", + email: "user@example.test", + isDeploymentOwner: false, + }, + createdByBotId: "bot-a", + name: " ", + botIds: ["bot-b"], }, - createdByBotId: "bot-a", - name: " ", - botIds: ["bot-b"], - }); + ); expect(result).toEqual({ error: "Project name is required." }); }); it("requires at least two agents including the creator", async () => { - const result = await createProject({ prisma: {} as PrismaClient }, { - actor: { - userId: "user-1", - spaceId: "space-1", - email: "user@example.test", - isDeploymentOwner: false, + const result = await createProject( + { prisma: {} as PrismaClient }, + { + actor: { + userId: "user-1", + spaceId: "space-1", + email: "user@example.test", + isDeploymentOwner: false, + }, + createdByBotId: "bot-a", + name: "Website Redesign", + botIds: [], }, - createdByBotId: "bot-a", - name: "Website Redesign", - botIds: [], - }); + ); expect(result).toEqual({ error: "At least 2 agents are required. Include bot_id values for other teammates.", }); diff --git a/packages/adapters/src/desktop-sandbox-paths.ts b/packages/adapters/src/desktop-sandbox-paths.ts index d999857..49d8398 100644 --- a/packages/adapters/src/desktop-sandbox-paths.ts +++ b/packages/adapters/src/desktop-sandbox-paths.ts @@ -22,10 +22,7 @@ export function resolveDesktopExecuteCwd(requestCwd: string | undefined, home: s return home; } if (requestCwd.startsWith("/home/rakazo/")) { - return path.join( - home, - normalizeDesktopWorkspacePath(requestCwd.slice("/home/rakazo/".length)), - ); + return path.join(home, normalizeDesktopWorkspacePath(requestCwd.slice("/home/rakazo/".length))); } if (path.isAbsolute(requestCwd)) { // Host-absolute paths must not be rebased into the workspace; containment rejects them. diff --git a/packages/adapters/src/executor-computer-busy-retry.test.ts b/packages/adapters/src/executor-computer-busy-retry.test.ts index eee2feb..e106bdf 100644 --- a/packages/adapters/src/executor-computer-busy-retry.test.ts +++ b/packages/adapters/src/executor-computer-busy-retry.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import type { ExecutorDeps } from "./executor.js"; import { COMPUTER_BUSY_EXHAUSTED_ERROR, COMPUTER_BUSY_MAX_ATTEMPTS, @@ -11,7 +12,6 @@ import { requeueComputerBusyRun, shouldRetryComputerBusy, } from "./executor-computer-busy-retry.js"; -import type { ExecutorDeps } from "./executor.js"; describe("computer busy retry policy", () => { it("tracks consecutive busy retries in an internal run.error marker", () => { diff --git a/packages/adapters/src/executor-computer-busy-retry.ts b/packages/adapters/src/executor-computer-busy-retry.ts index 64388c8..620d5c2 100644 --- a/packages/adapters/src/executor-computer-busy-retry.ts +++ b/packages/adapters/src/executor-computer-busy-retry.ts @@ -2,11 +2,7 @@ import { runContinueJob } from "@rakazo/adapter-kit"; import { getLogger } from "@rakazo/logging"; import { returnBotMessageOutcome } from "./bot-messages.js"; import type { ExecutorDeps } from "./executor.js"; -import { - computerRetryDelay, - notifyRun, - writeComputerRunRequeue, -} from "./executor-computer.js"; +import { computerRetryDelay, notifyRun, writeComputerRunRequeue } from "./executor-computer.js"; import type { TakeoverResumeCheckpoint } from "./takeover-resume.js"; export const COMPUTER_BUSY_MAX_ATTEMPTS = 24; diff --git a/packages/adapters/src/executor-computer-safety.test.ts b/packages/adapters/src/executor-computer-safety.test.ts index 82f3bfa..95ce392 100644 --- a/packages/adapters/src/executor-computer-safety.test.ts +++ b/packages/adapters/src/executor-computer-safety.test.ts @@ -106,9 +106,9 @@ describe("computer lifecycle command guard", () => { false, ); expect(isProtectedComputerLifecycleCommand(`node -e 'console.log(1 + 1)'`)).toBe(false); - expect(isProtectedComputerLifecycleCommand(`node -e 'const x = "kill switch"; console.log(x)'`)).toBe( - false, - ); + expect( + isProtectedComputerLifecycleCommand(`node -e 'const x = "kill switch"; console.log(x)'`), + ).toBe(false); }); it("keeps ordinary interpreter scripts available", () => { diff --git a/packages/adapters/src/executor-computer.ts b/packages/adapters/src/executor-computer.ts index b4b366e..1f52833 100644 --- a/packages/adapters/src/executor-computer.ts +++ b/packages/adapters/src/executor-computer.ts @@ -1,23 +1,14 @@ -import type { - NotificationMessage, - NotificationProvider, -} from "@rakazo/adapter-kit"; +import type { NotificationMessage, NotificationProvider } from "@rakazo/adapter-kit"; import { runContinueJob } from "@rakazo/adapter-kit"; import type { MessageBlock } from "@rakazo/contracts"; -import type { PrismaClient, Prisma } from "@rakazo/db"; -import { - appendEventInTransaction, - createThreadMessageInTransaction, -} from "@rakazo/db"; import { redactSecrets } from "@rakazo/core"; +import type { Prisma, PrismaClient } from "@rakazo/db"; +import { appendEventInTransaction, createThreadMessageInTransaction } from "@rakazo/db"; import { getLogger } from "@rakazo/logging"; -import type { ExecutorDeps } from "./executor.js"; import { withComputerScreenAvailability } from "./computer-screens.js"; -import { - TAKEOVER_RESUME_CHECKPOINTS, - type TakeoverResumeCheckpoint, -} from "./takeover-resume.js"; +import type { ExecutorDeps } from "./executor.js"; import { sanitizeTextBlocks } from "./prose-handoff-recovery.js"; +import { TAKEOVER_RESUME_CHECKPOINTS, type TakeoverResumeCheckpoint } from "./takeover-resume.js"; // Helper used by publishMessage — also exported so executor.ts can call it directly export async function persistMessageInTransaction( diff --git a/packages/adapters/src/executor-effects.ts b/packages/adapters/src/executor-effects.ts index 1ea88c8..62779e4 100644 --- a/packages/adapters/src/executor-effects.ts +++ b/packages/adapters/src/executor-effects.ts @@ -1,10 +1,10 @@ -import type { PrismaClient, ThreadEvents } from "@rakazo/db"; -import { completeExternalEffect } from "./approval-effect.js"; import { isToolEffectIdempotencyKey, legacyScopedToolEffectIdempotencyKey, stableJsonValue, } from "@rakazo/core/node/approval-effect-key"; +import type { PrismaClient, ThreadEvents } from "@rakazo/db"; +import { completeExternalEffect } from "./approval-effect.js"; /** Minimal dependencies for effect recording/completion. */ type EffectDeps = { @@ -34,7 +34,10 @@ export async function recordEffect( runId: run.id, payload: { executionId: idempotencyKey, kind }, }); - return { duplicate: true, effect: existing as unknown as { id: string; [key: string]: unknown } }; + return { + duplicate: true, + effect: existing as unknown as { id: string; [key: string]: unknown }, + }; } // Pre-fix rows used bare provider ids or scoped keys that included the @@ -77,7 +80,10 @@ export async function recordEffect( runId: run.id, payload: { executionId: candidate, kind, legacy: true }, }); - return { duplicate: true, effect: sameIdLegacy as unknown as { id: string; [key: string]: unknown } }; + return { + duplicate: true, + effect: sameIdLegacy as unknown as { id: string; [key: string]: unknown }, + }; } } } diff --git a/packages/adapters/src/executor-model.ts b/packages/adapters/src/executor-model.ts index 9e42365..c745f6a 100644 --- a/packages/adapters/src/executor-model.ts +++ b/packages/adapters/src/executor-model.ts @@ -1,7 +1,7 @@ -import type { AgentModelOAuthCredential } from "@rakazo/adapter-kit"; -import type { AgentRunRequest } from "@rakazo/adapter-kit"; +import type { AgentModelOAuthCredential, AgentRunRequest } from "@rakazo/adapter-kit"; import { OPENAI_COMPATIBLE_PROVIDER_ID } from "@rakazo/contracts"; import { resolveDeploymentModel } from "./deployment-model.js"; +import type { ExecutorDeps } from "./executor-types.js"; import { modelIdSupportsImages } from "./model-vision.js"; import { toOAuthCredential } from "./pi-credentials.js"; import { @@ -11,8 +11,6 @@ import { serializeModelSecret, } from "./pi-oauth.js"; -import type { ExecutorDeps } from "./executor-types.js"; - const modelCredentialLocks = new Map>(); /** @@ -20,14 +18,9 @@ const modelCredentialLocks = new Map>(); * configured deployment model provider. A provider named by deployment * settings or a bot override gets no key rather than another vendor's. */ -export function deploymentKeyFor( - deps: ExecutorDeps, - provider: string, -): string | undefined { +export function deploymentKeyFor(deps: ExecutorDeps, provider: string): string | undefined { if (!deps.deploymentModelKey) return undefined; - return provider === resolveDeploymentModel().provider - ? deps.deploymentModelKey - : undefined; + return provider === resolveDeploymentModel().provider ? deps.deploymentModelKey : undefined; } /** @@ -71,8 +64,7 @@ export async function resolveModelKey( const row = await deps.prisma.secret.findFirst({ where: { id: credential.secretId, userId, spaceId: null }, }); - if (!row) - return { apiKey: deploymentKeyFor(deps, provider), redact: [] }; + if (!row) return { apiKey: deploymentKeyFor(deps, provider), redact: [] }; const plaintext = deps.secretStore.load(row.ciphertext, row.id); registerSecrets?.(secretValuesToRedact(parseModelSecret(plaintext))); const persist = async (next: string) => { @@ -95,14 +87,9 @@ export async function resolveModelKey( const resolved = await resolveModelAuth(plaintext, credential.provider, { persist, }); - const oauth = - resolved.secret.kind === "oauth" - ? resolved.secret.credential - : undefined; + const oauth = resolved.secret.kind === "oauth" ? resolved.secret.credential : undefined; const baseUrl = - resolved.secret.kind === "openai_compatible" - ? resolved.secret.baseUrl - : undefined; + resolved.secret.kind === "openai_compatible" ? resolved.secret.baseUrl : undefined; const acceptsImages = credential.provider === OPENAI_COMPATIBLE_PROVIDER_ID && resolved.secret.kind === "openai_compatible" && @@ -116,21 +103,13 @@ export async function resolveModelKey( apiKey: resolved.apiKey, baseUrl, reasoning: - resolved.secret.kind === "openai_compatible" - ? resolved.secret.reasoning - : undefined, + resolved.secret.kind === "openai_compatible" ? resolved.secret.reasoning : undefined, maxTokens: - resolved.secret.kind === "openai_compatible" - ? resolved.secret.maxTokens - : undefined, + resolved.secret.kind === "openai_compatible" ? resolved.secret.maxTokens : undefined, contextWindow: - resolved.secret.kind === "openai_compatible" - ? resolved.secret.contextWindow - : undefined, + resolved.secret.kind === "openai_compatible" ? resolved.secret.contextWindow : undefined, thinkingLevel: - resolved.secret.kind === "openai_compatible" - ? resolved.secret.thinkingLevel - : undefined, + resolved.secret.kind === "openai_compatible" ? resolved.secret.thinkingLevel : undefined, acceptsImages, maxImagesPerPrompt: resolved.secret.kind === "openai_compatible" @@ -145,10 +124,7 @@ export async function resolveModelKey( }); if (!currentRow) return; const current = parseModelSecret( - deps.secretStore.load( - currentRow.ciphertext, - currentRow.id, - ), + deps.secretStore.load(currentRow.ciphertext, currentRow.id), ); if (current.kind === "oauth") { const stored = current.credential; @@ -183,10 +159,7 @@ export async function resolveModelKey( * Serialize access to credential operations for a single secret key. * Prevents concurrent reads/writes from producing torn state. */ -export async function withModelCredentialLock( - key: string, - fn: () => Promise, -): Promise { +export async function withModelCredentialLock(key: string, fn: () => Promise): Promise { const previous = modelCredentialLocks.get(key) ?? Promise.resolve(); let release!: () => void; const current = previous.then( @@ -201,7 +174,6 @@ export async function withModelCredentialLock( return await fn(); } finally { release(); - if (modelCredentialLocks.get(key) === current) - modelCredentialLocks.delete(key); + if (modelCredentialLocks.get(key) === current) modelCredentialLocks.delete(key); } } diff --git a/packages/adapters/src/executor-shell-guard.ts b/packages/adapters/src/executor-shell-guard.ts index 1150ab2..f17b06a 100644 --- a/packages/adapters/src/executor-shell-guard.ts +++ b/packages/adapters/src/executor-shell-guard.ts @@ -165,7 +165,13 @@ function scriptInlineProgram(words: string[], interpreterIndex: number): string const glued = /^-(?:[^-]*?)([cerp])(.+)$/.exec(word); if (glued?.[1] && glued[2] !== undefined && glued[2] !== "") return glued[2]; // python -O -c CODE / node -e CODE / perl -e / ruby -e / php -r / node -p - if (/^-[^-]*[cerp]$/.test(word) || word === "-c" || word === "-e" || word === "-r" || word === "-p") { + if ( + /^-[^-]*[cerp]$/.test(word) || + word === "-c" || + word === "-e" || + word === "-r" || + word === "-p" + ) { return words[index + 1]; } } @@ -454,7 +460,7 @@ function rubyScriptPaths( if (library) executed.push(library); break; } - if ("ICWK".includes(letter)) { + if (letter !== undefined && "ICWK".includes(letter)) { if (rest.length === 0) index += 1; break; } @@ -784,7 +790,8 @@ function bunOperand(args: string[]): string | undefined { function bunTargetsKill(args: string[], command: string): boolean { const inline = bunInlineProgram(args); - if (inline.present) return inline.program !== undefined && SCRIPT_LIFECYCLE_KILL.test(inline.program); + if (inline.present) + return inline.program !== undefined && SCRIPT_LIFECYCLE_KILL.test(inline.program); const operand = bunOperand(args); if (operand === "-") return SCRIPT_LIFECYCLE_KILL.test(command); return operand !== undefined && denoBunKillFamilyScriptStem(operand); @@ -827,7 +834,11 @@ function interpreterTargetsKill( if (!invocation) continue; const { name, args } = invocation; if (PYTHON_COMMAND.test(name) && pythonTargetsKill(args, command)) return true; - if (NODE_COMMAND.test(name) && nodeReadsProgramStdin(args) && SCRIPT_LIFECYCLE_KILL.test(command)) { + if ( + NODE_COMMAND.test(name) && + nodeReadsProgramStdin(args) && + SCRIPT_LIFECYCLE_KILL.test(command) + ) { return true; } if ( diff --git a/packages/adapters/src/executor-tools.ts b/packages/adapters/src/executor-tools.ts index 4e730b7..88fad49 100644 --- a/packages/adapters/src/executor-tools.ts +++ b/packages/adapters/src/executor-tools.ts @@ -1,13 +1,8 @@ import { builtinAgentTools } from "./builtin-tools.js"; import { selectCloudAgentTools } from "./cloud-agent-tools-select.js"; import { selectMemoryTools } from "./memory-tools.js"; -import { - filterBuiltinToolsForRun, - filterBuiltinToolsForThread, -} from "./schedule-tools.js"; -import { - filterImageReturningComputerTools, -} from "./model-vision.js"; +import { filterImageReturningComputerTools } from "./model-vision.js"; +import { filterBuiltinToolsForRun, filterBuiltinToolsForThread } from "./schedule-tools.js"; /** Browser tools that require a graphical computer (Chrome). */ export const PAGE_BROWSER_TOOL_NAMES = new Set([ diff --git a/packages/adapters/src/executor-types.ts b/packages/adapters/src/executor-types.ts index 583d75a..c32ea62 100644 --- a/packages/adapters/src/executor-types.ts +++ b/packages/adapters/src/executor-types.ts @@ -11,11 +11,11 @@ import type { SandboxProvider, WebProvider, } from "@rakazo/adapter-kit"; +import type { PrismaClient, ThreadEvents } from "@rakazo/db"; +import type { CloudAgentConnection } from "./cloud-agent-factory.js"; import type { MemoryProviderResolver } from "./memory-provider-factory.js"; import type { RemoteTransportDependencies } from "./remote-mcp.js"; import type { EncryptedSecretStore } from "./secrets.js"; -import type { PrismaClient, ThreadEvents } from "@rakazo/db"; -import type { CloudAgentConnection } from "./cloud-agent-factory.js"; /** Shared dependencies injected into createRunExecutor and its extracted helpers. */ export interface ExecutorDeps { diff --git a/packages/adapters/src/executor.test.ts b/packages/adapters/src/executor.test.ts index d35d63c..5fd1d90 100644 --- a/packages/adapters/src/executor.test.ts +++ b/packages/adapters/src/executor.test.ts @@ -2,6 +2,7 @@ import type { MessageBlock } from "@rakazo/contracts"; import { ONCE_ROUTINE_CRON } from "@rakazo/core"; import type { PrismaClient } from "@rakazo/db"; import { describe, expect, it, vi } from "vitest"; +import { agentSecretScopeForRun } from "./agent-environment.js"; import { appendToolCompletionAudit, createRunExecutor, @@ -12,8 +13,10 @@ import { toolCompletionAuditPayload, toolCompletionFromResult, } from "./executor.js"; -import { agentSecretScopeForRun } from "./agent-environment.js"; -import { missingTurnImagesInstruction, settleSteeringAttachmentLoads } from "./executor-attachments.js"; +import { + missingTurnImagesInstruction, + settleSteeringAttachmentLoads, +} from "./executor-attachments.js"; import { threadContextForRun } from "./executor-completion.js"; import { serializeModelSecret } from "./pi-oauth.js"; diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 13a5ec4..f790df4 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -26,7 +26,6 @@ import { runContinueJob, } from "@rakazo/adapter-kit"; import type { RunStatus } from "@rakazo/contracts"; -import { type MessageBlock, MessageBlock as MessageBlockSchema } from "@rakazo/contracts"; import { ATTACHMENT_MAX_BYTES, BOT_DESCRIPTION_MAX_LENGTH, @@ -35,6 +34,8 @@ import { BotSecretName, BotSecretSubmission, isAttachmentImageMimeType, + type MessageBlock, + MessageBlock as MessageBlockSchema, OPENAI_COMPATIBLE_PROVIDER_ID, } from "@rakazo/contracts"; import { @@ -44,7 +45,6 @@ import { applyJudgeDecision, assertTransition, botMessageAllowsSilence, - inboundHandoffForBot, connectorKindFromToolName, containsSecret, createStreamingRedactor, @@ -53,6 +53,7 @@ import { formatSkillRunPrompt, formatSkillsCatalogInstruction, humanizeToolName, + inboundHandoffForBot, inferAttachmentMimeType, isMessagingChannelRun, isOneShotRoutineCrons, @@ -68,8 +69,8 @@ import { renderBotDirectory, renderInboundHandoffWorkshopInstruction, resolveActionApprovalDetail, - sanitizeBotReplyText, sandboxCommandTimeoutMs, + sanitizeBotReplyText, type ToolCallStreak, toolRequiresApproval, toolRequiresExplicitApproval, @@ -141,47 +142,6 @@ import { settleUncertainEffect, uncertainEffectResult, } from "./approval-effect.js"; -import { - TURN_ATTACHMENT_UNAVAILABLE, - STEERING_ATTACHMENT_UNAVAILABLE, - missingTurnImagesInstruction, - settleSteeringAttachmentLoads, -} from "./executor-attachments.js"; -import { createRunWorkspaceCheckpoint } from "./executor-checkpoint.js"; -import { - completionMessageSegments, - completionNotificationBody, - completionMarksUnread, - threadContextForRun, - subagentMarksUnread, -} from "./executor-completion.js"; -import { - computerScreenToolResult, - notifyRun, - renewRunLease, - computerRetryDelay, - computerRunRequeueData, - writeComputerRunRequeue, - requeueComputerRun, - redactBlocks, - publishMessage, - persistMessageInTransaction, - runNotificationsEnabled, -} from "./executor-computer.js"; -import { recordEffect, completeEffect, uncertainEffectError } from "./executor-effects.js"; -import { deploymentKeyFor, resolveModelKey, withModelCredentialLock } from "./executor-model.js"; -import { - isProtectedComputerLifecycleCommandReadingScripts, - PROTECTED_SCRIPT_SCAN_BYTES, - protectedScriptWorkspacePath, -} from "./executor-shell-guard.js"; -import { - PAGE_BROWSER_TOOL_NAMES, - filterPageBrowserTools, - selectBuiltinToolsForRun, -} from "./executor-tools.js"; -import { type ExecutorDeps } from "./executor-types.js"; -import { selectRunConnections, loadCurrentTurnImages } from "./executor-utils.js"; import { autoReviewTimeoutMs, buildAutoReviewPrompt, @@ -208,7 +168,6 @@ import { } from "./browser-tools.js"; import { agentConnectionTools, builtinAgentTools } from "./builtin-tools.js"; import { archiveSpawnedBot, spawnBot } from "./child-bots.js"; -import { createProject } from "./create-project.js"; import { type CloudAgentConnection, cloudAgentsEnabled } from "./cloud-agent-factory.js"; import { executeCloudAgentTool } from "./cloud-agent-service.js"; import { validCloudAgentArgs } from "./cloud-agent-tools.js"; @@ -244,13 +203,51 @@ import { import { observationToolResult, parseComputerActions } from "./computer-tools.js"; import { checkpointRunComputerWorkspace } from "./computer-workspace.js"; import { sanitizeConnectorError } from "./connector-safety.js"; +import { createProject } from "./create-project.js"; import { formatCurrentTimeInstruction } from "./current-time.js"; import { resolveDeploymentModel } from "./deployment-model.js"; -import { handoffToGroupBot, loadGroupContext } from "./group-handoff.js"; import { - tryRecoverProseHandoff, - workshopFallbackAfterBlockedHandoff, -} from "./prose-handoff-recovery.js"; + missingTurnImagesInstruction, + STEERING_ATTACHMENT_UNAVAILABLE, + settleSteeringAttachmentLoads, + TURN_ATTACHMENT_UNAVAILABLE, +} from "./executor-attachments.js"; +import { createRunWorkspaceCheckpoint } from "./executor-checkpoint.js"; +import { + completionMarksUnread, + completionMessageSegments, + completionNotificationBody, + subagentMarksUnread, + threadContextForRun, +} from "./executor-completion.js"; +import { + computerRetryDelay, + computerRunRequeueData, + computerScreenToolResult, + notifyRun, + persistMessageInTransaction, + publishMessage, + redactBlocks, + renewRunLease, + requeueComputerRun, + runNotificationsEnabled, + writeComputerRunRequeue, +} from "./executor-computer.js"; +import { completeEffect, recordEffect, uncertainEffectError } from "./executor-effects.js"; +import { deploymentKeyFor, resolveModelKey, withModelCredentialLock } from "./executor-model.js"; +import { + isProtectedComputerLifecycleCommandReadingScripts, + PROTECTED_SCRIPT_SCAN_BYTES, + protectedScriptWorkspacePath, +} from "./executor-shell-guard.js"; +import { + filterPageBrowserTools, + PAGE_BROWSER_TOOL_NAMES, + selectBuiltinToolsForRun, +} from "./executor-tools.js"; +import type { ExecutorDeps } from "./executor-types.js"; +import { loadCurrentTurnImages, selectRunConnections } from "./executor-utils.js"; +import { handoffToGroupBot, loadGroupContext } from "./group-handoff.js"; import { COMPACTION_BATCH_SIZE, formatCompactedSummary, @@ -303,6 +300,10 @@ import { renderPlotSpecToSvg, searchChartCatalog, } from "./plot-tool.js"; +import { + tryRecoverProseHandoff, + workshopFallbackAfterBlockedHandoff, +} from "./prose-handoff-recovery.js"; import type { RemoteTransportDependencies } from "./remote-mcp.js"; import { loadReplyContext, messageToAgentHistoryText } from "./reply-context.js"; import { @@ -1369,7 +1370,10 @@ export function createRunExecutor(deps: ExecutorDeps) { if (!parsed.success) return undefined; const handoff = inboundHandoffForBot(parsed.data, bot.id); if (!handoff) return undefined; - return renderInboundHandoffWorkshopInstruction(handoff, (source.bot as { name?: string } | null)?.name); + return renderInboundHandoffWorkshopInstruction( + handoff, + (source.bot as { name?: string } | null)?.name, + ); })(); const hasMessagingIdentity = deps.messaging ? await deps.messaging.hasIdentity(bot.id) @@ -4307,12 +4311,39 @@ async function runSandboxCommand( return { stdout, stderr, code }; } -export { TURN_ATTACHMENT_UNAVAILABLE, STEERING_ATTACHMENT_UNAVAILABLE, missingTurnImagesInstruction, settleSteeringAttachmentLoads } from "./executor-attachments.js"; +export { + missingTurnImagesInstruction, + STEERING_ATTACHMENT_UNAVAILABLE, + settleSteeringAttachmentLoads, + TURN_ATTACHMENT_UNAVAILABLE, +} from "./executor-attachments.js"; export { createRunWorkspaceCheckpoint } from "./executor-checkpoint.js"; +export { + completionMarksUnread, + completionMessageSegments, + completionNotificationBody, + subagentMarksUnread, + threadContextForRun, +} from "./executor-completion.js"; +export { + computerRetryDelay, + computerRunRequeueData, + computerScreenToolResult, + notifyRun, + persistMessageInTransaction, + publishMessage, + redactBlocks, + renewRunLease, + requeueComputerRun, + runNotificationsEnabled, + writeComputerRunRequeue, +} from "./executor-computer.js"; +export { completeEffect, recordEffect, uncertainEffectError } from "./executor-effects.js"; export { isProtectedComputerLifecycleCommand } from "./executor-shell-guard.js"; -export { type ExecutorDeps } from "./executor-types.js"; -export { completionMessageSegments, completionNotificationBody, completionMarksUnread, threadContextForRun, subagentMarksUnread } from "./executor-completion.js"; -export { selectRunConnections, loadCurrentTurnImages } from "./executor-utils.js"; -export { PAGE_BROWSER_TOOL_NAMES, filterPageBrowserTools, selectBuiltinToolsForRun } from "./executor-tools.js"; -export { recordEffect, completeEffect, uncertainEffectError } from "./executor-effects.js"; -export { computerScreenToolResult, notifyRun, renewRunLease, computerRetryDelay, computerRunRequeueData, writeComputerRunRequeue, requeueComputerRun, redactBlocks, publishMessage, persistMessageInTransaction, runNotificationsEnabled } from "./executor-computer.js"; +export { + filterPageBrowserTools, + PAGE_BROWSER_TOOL_NAMES, + selectBuiltinToolsForRun, +} from "./executor-tools.js"; +export type { ExecutorDeps } from "./executor-types.js"; +export { loadCurrentTurnImages, selectRunConnections } from "./executor-utils.js"; diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index ee6a75d..150915d 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -67,6 +67,7 @@ export * from "./installed-connectors.js"; export * from "./integration-provider-settings.js"; export * from "./job-reconciler.js"; export * from "./keyless-http-web.js"; +export * from "./kokoro-voice.js"; export * from "./mcp-connector.js"; export * from "./mcp-emulator.js"; export * from "./mcp-oauth.js"; @@ -90,10 +91,10 @@ export * from "./pi-models.js"; export * from "./pi-oauth.js"; export * from "./pi-openai-compatible-provider.js"; export * from "./pi-runtime.js"; -export * from "./project-workspace-files.js"; -export * from "./project-workspace-path.js"; export * from "./pi-session.js"; export * from "./pipedream-connector.js"; +export * from "./project-workspace-files.js"; +export * from "./project-workspace-path.js"; export * from "./realtime.js"; export * from "./release-watch.js"; export * from "./remote-mcp.js"; @@ -114,7 +115,6 @@ export { SupermemoryMemoryProvider } from "./supermemory-memory-provider.js"; export * from "./teaching-session.js"; export * from "./team-chat-messaging.js"; export * from "./third-party-connector-emulator.js"; -export * from "./kokoro-voice.js"; export * from "./voice-factory.js"; export * from "./wakeup.js"; export * from "./web-limits.js"; diff --git a/packages/adapters/src/kokoro-voice.ts b/packages/adapters/src/kokoro-voice.ts index 0252986..5601456 100644 --- a/packages/adapters/src/kokoro-voice.ts +++ b/packages/adapters/src/kokoro-voice.ts @@ -66,7 +66,7 @@ export class KokoroVoiceProvider implements VoiceProvider { if (res.ok) return { ok: true }; return { ok: false, - message: voiceHttpError( + message: voiceHttpError( res.status, "Local voice", "checking the local server", @@ -148,4 +148,3 @@ function extractVoiceIds(body: unknown): string[] { }) .filter(Boolean); } - diff --git a/packages/adapters/src/messaging-delivery.ts b/packages/adapters/src/messaging-delivery.ts index 8d8522e..7b6c663 100644 --- a/packages/adapters/src/messaging-delivery.ts +++ b/packages/adapters/src/messaging-delivery.ts @@ -147,16 +147,16 @@ async function mirrorChannelRun( where: { runId: run.id, role: "bot" }, orderBy: { seq: "asc" }, }); - // Ask answers are entered privately in the app. Skip only messages that carry - // an answered ask block so a resumed run can still mirror later text replies - // (idempotency keys dedupe anything already posted while the ask was pending). + // Ask answers are entered privately in the app. A sibling text block in the + // same run can restate that answer, so the whole run stays off the group + // and out of peer fan-out. Unanswered asks still allow a public reply. + const hasPrivateAnswer = replies.some((message) => + (message.blocks as MessageBlock[]).some( + (block) => block.kind === "ask" && block.status === "answered", + ), + ); + if (hasPrivateAnswer) return; const messages = replies - .filter( - (message) => - !(message.blocks as MessageBlock[]).some( - (block) => block.kind === "ask" && block.status === "answered", - ), - ) .map((message) => ({ message, text: extractText(message.blocks) })) .filter((entry) => entry.text); if (messages.length === 0) return; diff --git a/packages/adapters/src/model-connect.ts b/packages/adapters/src/model-connect.ts index d32d358..860b47a 100644 --- a/packages/adapters/src/model-connect.ts +++ b/packages/adapters/src/model-connect.ts @@ -76,7 +76,7 @@ export function buildModelConnectPlaintext( return serializeModelSecret(secret); } const apiKey = input.apiKey?.trim(); - if (!apiKey || apiKey.length < 1) { + if (!apiKey || apiKey.length < 8) { throw new Error("API key must contain at least 8 characters"); } return apiKey; diff --git a/packages/adapters/src/pi-model-config.ts b/packages/adapters/src/pi-model-config.ts index d38702a..76edf06 100644 --- a/packages/adapters/src/pi-model-config.ts +++ b/packages/adapters/src/pi-model-config.ts @@ -1,33 +1,30 @@ import { randomUUID } from "node:crypto"; +import type { Agent, AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core"; import { - clampThinkingLevel, type Api, + clampThinkingLevel, type Model, - type ModelThinkingLevel, type Models, + type ModelThinkingLevel, type SimpleStreamOptions, Type, } from "@earendil-works/pi-ai"; import { builtinModels } from "@earendil-works/pi-ai/providers/all"; -import { type Agent, type AgentToolResult } from "@earendil-works/pi-agent-core"; -import { - type AgentMessage, -} from "@earendil-works/pi-agent-core"; -import type { AgentToolExecutionResult } from "@rakazo/adapter-kit"; import type { - AgentRuntimeEvent, AgentRunRequest, + AgentRuntimeEvent, AgentSteeringMessage, + AgentToolExecutionResult, } from "@rakazo/adapter-kit"; import { getLogger } from "@rakazo/logging"; import { DEFAULT_OPENROUTER_MODEL_ID } from "./deployment-model.js"; +import { PiRuntimeCredentialStore, toOAuthCredential } from "./pi-credentials.js"; +import { registerLocalProvider } from "./pi-local-provider.js"; import { OPENAI_COMPATIBLE_PROVIDER_ID, registerOpenAiCompatibleCatalog, registerOpenAiCompatibleRuntime, } from "./pi-openai-compatible-provider.js"; -import { PiRuntimeCredentialStore, toOAuthCredential } from "./pi-credentials.js"; -import { registerLocalProvider } from "./pi-local-provider.js"; import { billedPromptTokens, clipToolResultContent, @@ -138,10 +135,7 @@ export function beginToolCall(host: { return false; } -export function endToolCall(host: { - toolCallBudget: ToolCallBudget; - abortTurn(): void; -}): void { +export function endToolCall(host: { toolCallBudget: ToolCallBudget; abortTurn(): void }): void { host.toolCallBudget.inFlight = Math.max(0, host.toolCallBudget.inFlight - 1); maybeAbortToolCallBudget(host); } @@ -341,14 +335,20 @@ export function imagePartCount(message: AgentMessage): number { if (!("content" in message) || !Array.isArray(message.content)) return 0; return message.content.filter( (part: unknown) => - typeof part === "object" && part !== null && "type" in part && (part as {type?: string}).type === "image", + typeof part === "object" && + part !== null && + "type" in part && + (part as { type?: string }).type === "image", ).length; } export function isComputerScreenshotMessage( message: AgentMessage | undefined, ): message is Extract { - if (message?.role !== "toolResult" || !message.content.some((part) => (part as {type?: string}).type === "image")) { + if ( + message?.role !== "toolResult" || + !message.content.some((part) => (part as { type?: string }).type === "image") + ) { return false; } const details = message.details; @@ -476,9 +476,7 @@ export function summarizeToolResult(result: unknown) { } } -export function boundAgentToolResult( - result: AgentToolResult, -): AgentToolResult { +export function boundAgentToolResult(result: AgentToolResult): AgentToolResult { if (!result || !Array.isArray(result.content)) return result; return { ...result, @@ -493,11 +491,7 @@ export function assistantText(message: unknown): string { if (!Array.isArray(content)) return ""; return content .map((part) => - part && - typeof part === "object" && - "type" in part && - part.type === "text" && - "text" in part + part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part ? String(part.text) : "", ) @@ -512,18 +506,12 @@ export function sanitizeSensitiveText(message: string) { .replace(/sk-[a-zA-Z0-9-]+/g, "[redacted]") .replace(/Bearer\s+[^\s"',;&]+/gi, "Bearer [redacted]") .replace(/eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, "[redacted]") - .replace( - /COMPOSIO_API_KEY[=:]?\s*\S+/gi, - "COMPOSIO_API_KEY=[redacted]", - ) + .replace(/COMPOSIO_API_KEY[=:]?\s*\S+/gi, "COMPOSIO_API_KEY=[redacted]") .replace( /((?:api[_-]?key|access[_-]?token|password|secret)\s*[=:]\s*)[^\s"',;&]+/gi, "$1[redacted]", ) - .replace( - /((?:auth|authorization)\s*[=:]\s*)(?!Bearer\b)[^\s"',;&]+/gi, - "$1[redacted]", - ); + .replace(/((?:auth|authorization)\s*[=:]\s*)(?!Bearer\b)[^\s"',;&]+/gi, "$1[redacted]"); } export function redactActivityUrl(value: unknown): string { @@ -547,11 +535,7 @@ export function sanitizeError(message: string) { // --- Session ID & provider helpers --- -export function conversationSessionId( - threadId: string, - botId: string, - agentId?: string, -): string { +export function conversationSessionId(threadId: string, botId: string, agentId?: string): string { return agentId ? `${threadId}:${botId}:${agentId}` : `${threadId}:${botId}`; } @@ -559,8 +543,7 @@ export function isOpenCodeProvider(provider: string): boolean { return provider === "opencode" || provider === "opencode-go"; } -const OPENCODE_SESSION_ERROR = - "OpenCode rejected this chat session. Send the message again."; +const OPENCODE_SESSION_ERROR = "OpenCode rejected this chat session. Send the message again."; export function looksLikeOpenCodeSessionError(message: string): boolean { return ( @@ -643,10 +626,7 @@ export function createGate(max: number) { // --- Model completion --- -export function modelForCompletion( - model: Model, - configuredMaxTokens?: number, -): Model { +export function modelForCompletion(model: Model, configuredMaxTokens?: number): Model { const maxTokens = resolveCompletionMaxTokens(model.maxTokens, configuredMaxTokens); if (maxTokens === model.maxTokens) return model; return { ...model, maxTokens }; @@ -663,11 +643,7 @@ export function reliableStreamOptions( ...options, timeoutMs: options?.timeoutMs ?? MODEL_STREAM_TIMEOUT_MS, maxRetries: options?.maxRetries ?? MODEL_STREAM_MAX_RETRIES, - maxTokens: resolveCompletionMaxTokens( - model.maxTokens, - configuredMaxTokens, - options?.maxTokens, - ), + maxTokens: resolveCompletionMaxTokens(model.maxTokens, configuredMaxTokens, options?.maxTokens), }; if (model.provider === "openai-codex" || model.api === "openai-codex-responses") { diff --git a/packages/adapters/src/pi-runtime.ts b/packages/adapters/src/pi-runtime.ts index f537483..13f8f97 100644 --- a/packages/adapters/src/pi-runtime.ts +++ b/packages/adapters/src/pi-runtime.ts @@ -21,13 +21,6 @@ import { normalizeOpenAiToolParameters, openAiToolParametersNeedNormalization, } from "./openai-tool-parameters.js"; -import { billedPromptTokens, clipToolResultText } from "./pi-runtime-limits.js"; -import { - PiJsonlSessionRecorder, - type PiSessionHandle, - type PiSessionRecorder, -} from "./pi-session.js"; -import { textContentArg } from "./tool-text.js"; import { assistantText, beginToolCall, @@ -43,21 +36,28 @@ import { prepareRequestSecretArguments, pruneComputerScreenshotContext, redactActivityUrl, - reliableStreamOptions, releaseToolCallBudget, + reliableStreamOptions, resolveRuntimeModel, sanitizeError, sanitizeProviderError, sanitizeSensitiveText, summarizeToolResult, - thinkingLevelFor, - toPiImages, - toolCallBudgetExceededMessage, - toolCallBudgetFor, - withoutSteeringMessages, type ToolCallBudget, type ToolHost, + thinkingLevelFor, + toolCallBudgetExceededMessage, + toolCallBudgetFor, + toPiImages, + withoutSteeringMessages, } from "./pi-model-config.js"; +import { billedPromptTokens, clipToolResultText } from "./pi-runtime-limits.js"; +import { + PiJsonlSessionRecorder, + type PiSessionHandle, + type PiSessionRecorder, +} from "./pi-session.js"; +import { textContentArg } from "./tool-text.js"; const running = new Map }>(); // Optional fuse is process-local. continueRun on another worker starts at zero. diff --git a/packages/adapters/src/project-workspace-path.ts b/packages/adapters/src/project-workspace-path.ts index fb19fd0..86bed9f 100644 --- a/packages/adapters/src/project-workspace-path.ts +++ b/packages/adapters/src/project-workspace-path.ts @@ -6,8 +6,7 @@ export const PROJECT_WORKSPACE_PREFIX = "project"; export function isProjectWorkspacePath(storedPath: string): boolean { const normalized = normalizeWorkspacePath(storedPath); return ( - normalized === PROJECT_WORKSPACE_PREFIX || - normalized.startsWith(`${PROJECT_WORKSPACE_PREFIX}/`) + normalized === PROJECT_WORKSPACE_PREFIX || normalized.startsWith(`${PROJECT_WORKSPACE_PREFIX}/`) ); } diff --git a/packages/adapters/src/prose-handoff-recovery.test.ts b/packages/adapters/src/prose-handoff-recovery.test.ts index 6015512..e1d9f2f 100644 --- a/packages/adapters/src/prose-handoff-recovery.test.ts +++ b/packages/adapters/src/prose-handoff-recovery.test.ts @@ -97,9 +97,9 @@ describe("tryRecoverProseHandoff", () => { describe("workshopFallbackAfterBlockedHandoff", () => { it("returns a workshop line when only blocked handoff prose remains", () => { - expect( - workshopFallbackAfterBlockedHandoff("", true, false), - ).toBe(INBOUND_HANDOFF_WORKSHOP_FALLBACK); + expect(workshopFallbackAfterBlockedHandoff("", true, false)).toBe( + INBOUND_HANDOFF_WORKSHOP_FALLBACK, + ); }); it("does not override real specialist prose", () => { diff --git a/packages/adapters/src/schedule-tools.ts b/packages/adapters/src/schedule-tools.ts index e226a08..f3e1b3f 100644 --- a/packages/adapters/src/schedule-tools.ts +++ b/packages/adapters/src/schedule-tools.ts @@ -25,7 +25,12 @@ export function filterBuiltinToolsForThread( tools: T[], groupId: string | null | undefined, ): T[] { - return tools.filter((tool) => groupId || tool.name !== "handoff_to_bot"); + const inGroup = Boolean(groupId); + return tools.filter((tool) => { + if (tool.name === "handoff_to_bot") return inGroup; + if (tool.name === "message_bot") return !inGroup; + return true; + }); } type ScheduleUnit = "minutes" | "hours" | "days" | "seconds"; diff --git a/packages/contracts/src/desktop.ts b/packages/contracts/src/desktop.ts index 0b02b46..53b78aa 100644 --- a/packages/contracts/src/desktop.ts +++ b/packages/contracts/src/desktop.ts @@ -36,6 +36,24 @@ export interface RakazoDesktopOAuthCallback { state?: string; } +export interface RakazoDesktopBrowserPreviewState { + url: string; + devServerRunning: boolean; +} + +/** Optional desktop bridge for the in-app browser preview panel. Absent in the browser. */ +export interface RakazoDesktopBrowserPreview { + getState: () => Promise; + navigate: (url: string) => Promise; + reload: () => Promise; + onNavigate: (listener: (url: string) => void) => () => void; + onLoadingProgress: (listener: (progress: number) => void) => () => void; + devServer: { + start: () => Promise; + stop: () => Promise; + }; +} + export interface RakazoDesktop { /** Only the isolated local settings window is authorized to call this bridge. */ localSettings?: { @@ -66,18 +84,8 @@ export interface RakazoDesktop { */ onCallback: (listener: (callback: RakazoDesktopOAuthCallback) => void) => () => void; }; - /** Browser preview bridge — only available inside the Rakazo desktop app. */ - browserPreview?: { - navigate: (url: string) => Promise; - reload: () => Promise; - getState: () => Promise<{ url: string; devServerRunning: boolean }>; - onNavigate: (listener: (url: string) => void) => () => void; - onLoadingProgress: (listener: (progress: number) => void) => () => void; - devServer: { - start: () => Promise; - stop: () => Promise; - }; - }; + /** Present only when the desktop shell hosts an embedded browser preview. */ + browserPreview?: RakazoDesktopBrowserPreview; } /** diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 5f58bc8..8002858 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -80,13 +80,13 @@ import { IntegrationProviderConfigSchema, IntegrationSetupStateSchema, } from "./integration-settings.js"; -import { MessageReactionSchema } from "./reactions.js"; import { CreateProjectGoalInput, ProjectGoalSchema, UpdateProjectGoalInput, } from "./project-goals.js"; import { ProjectSessionsOutputSchema } from "./project-sessions.js"; +import { MessageReactionSchema } from "./reactions.js"; import { RunsListOutputSchema } from "./runs.js"; import { SearchQueryOutputSchema } from "./search.js"; diff --git a/packages/core/package.json b/packages/core/package.json index 5f07ded..bcac6e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,6 +24,11 @@ "development": "./src/node/screen-proxy-response.ts", "default": "./src/node/screen-proxy-response.ts" }, + "./node/git-checkout": { + "types": "./src/git-checkout.ts", + "development": "./src/git-checkout.ts", + "default": "./src/git-checkout.ts" + }, "./plot": { "types": "./src/plot/plot-spec.ts", "development": "./src/plot/plot-spec.ts", diff --git a/packages/core/src/bot-messages.test.ts b/packages/core/src/bot-messages.test.ts index 1b858bf..8907f6e 100644 --- a/packages/core/src/bot-messages.test.ts +++ b/packages/core/src/bot-messages.test.ts @@ -9,10 +9,10 @@ import { buildBotMessageWakePrompt, clampBotMessage, formatBotRosterLines, - nextBotMessageHop, - renderBotDirectory, inboundHandoffForBot, isSameStageHandoffBounce, + nextBotMessageHop, + renderBotDirectory, renderGroupMembersContext, renderInboundHandoffWorkshopInstruction, resolveBotAddress, diff --git a/packages/core/src/git-checkout.test.ts b/packages/core/src/git-checkout.test.ts index 90402fc..a5fd992 100644 --- a/packages/core/src/git-checkout.test.ts +++ b/packages/core/src/git-checkout.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; diff --git a/packages/core/src/git-checkout.ts b/packages/core/src/git-checkout.ts index a0dca20..ccf1ff7 100644 --- a/packages/core/src/git-checkout.ts +++ b/packages/core/src/git-checkout.ts @@ -5,9 +5,12 @@ import { DEFAULT_UPDATE_REMOTE, parseGitStatusPorcelain } from "./self-update.js // Lazy import to avoid bundling `node:child_process` in browser bundles. // `defaultGitRunner` is server-only; lazy-loading ensures tree-shaking works. -let _spawn: typeof import("node:child_process").spawn | null = null; -const getSpawn = (): typeof import("node:child_process").spawn => { - if (!_spawn) _spawn = require("node:child_process").spawn; +type SpawnFn = typeof import("node:child_process").spawn; +let _spawn: SpawnFn | undefined; +const getSpawn = (): SpawnFn => { + if (_spawn === undefined) { + _spawn = require("node:child_process").spawn as SpawnFn; + } return _spawn; }; diff --git a/packages/db/create-user.ts b/packages/db/create-user.ts index 557a559..3a2e757 100644 --- a/packages/db/create-user.ts +++ b/packages/db/create-user.ts @@ -5,7 +5,7 @@ const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "ch const allUsers = await db.user.findMany({ select: { id: true, email: true, name: true, createdAt: true, emailVerified: true }, - orderBy: { createdAt: 'desc' }, + orderBy: { createdAt: "desc" }, }); console.log("All users:", JSON.stringify(allUsers, null, 2)); @@ -13,7 +13,7 @@ console.log("All users:", JSON.stringify(allUsers, null, 2)); // Check spaces const allSpaces = await db.space.findMany({ select: { id: true, name: true, ownerId: true, createdAt: true }, - orderBy: { createdAt: 'desc' }, + orderBy: { createdAt: "desc" }, }); console.log("\nAll spaces:", JSON.stringify(allSpaces, null, 2)); diff --git a/packages/db/prisma/migrations/20260922180000_run_tool_call_budget/migration.sql b/packages/db/prisma/migrations/20260922180000_run_tool_call_budget/migration.sql new file mode 100644 index 0000000..73b03c9 --- /dev/null +++ b/packages/db/prisma/migrations/20260922180000_run_tool_call_budget/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "runs" ADD COLUMN "toolCallBudgetCount" INTEGER, +ADD COLUMN "toolCallBudgetExceeded" BOOLEAN; diff --git a/packages/db/query-users.mjs b/packages/db/query-users.mjs index 28bbafc..17c0f3b 100644 --- a/packages/db/query-users.mjs +++ b/packages/db/query-users.mjs @@ -1,10 +1,10 @@ -import { createPool, createDb } from "./src/client.js"; +import { createDb, createPool } from "./src/client.js"; const pool = createPool(process.env.DATABASE_URL, { applicationName: "query-users" }); const db = createDb(pool); const users = await db.user.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { createdAt: "desc" }, take: 10, select: { email: true, name: true, createdAt: true }, }); diff --git a/packages/db/query-users.ts b/packages/db/query-users.ts index eb10151..0f55d41 100644 --- a/packages/db/query-users.ts +++ b/packages/db/query-users.ts @@ -5,7 +5,7 @@ process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo"; const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "query-users" }); const users = await db.user.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { createdAt: "desc" }, take: 10, select: { email: true, name: true, createdAt: true }, }); diff --git a/packages/db/setup-user.ts b/packages/db/setup-user.ts index db476b2..4184e42 100644 --- a/packages/db/setup-user.ts +++ b/packages/db/setup-user.ts @@ -1,5 +1,5 @@ -import { createDb } from "./src/client"; import { randomUUID } from "node:crypto"; +import { createDb } from "./src/client"; process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo"; const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "setup-user" }); diff --git a/packages/db/src/events.ts b/packages/db/src/events.ts index cdc4a91..9365ecf 100644 --- a/packages/db/src/events.ts +++ b/packages/db/src/events.ts @@ -21,6 +21,7 @@ import { createThreadMessageInTransaction, RunHistoryWriteError, } from "./messages.js"; +import { ClearThreadPendingDelegatedReturnsError } from "./scope.js"; import { withTransactionRetry } from "./transaction-retry.js"; const EVENT_BATCH_SIZE = 200; @@ -259,6 +260,19 @@ export async function clearThread( data: { unread: false }, select: { nextMessageSeq: true, historyCompactionGeneration: true }, }); + const pendingDelegatedReturns = await tx.run.count({ + where: { + spaceId: input.spaceId, + threadId: input.threadId, + ...(input.groupId ? {} : { botId: input.botId }), + trigger: "bot_message", + status: { in: ["completed", "failed"] }, + botOutcomeReturnedAt: null, + }, + }); + if (pendingDelegatedReturns > 0) { + throw new ClearThreadPendingDelegatedReturnsError(); + } const activeRuns = await tx.run.findMany({ where: { spaceId: input.spaceId, diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 933dd94..9436814 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -6,12 +6,12 @@ export * from "./credential-secrets.js"; export * from "./events.js"; export * from "./external-conversations.js"; export * from "./groups.js"; -export * from "./project-goals.js"; -export * from "./project-workspace.js"; export * from "./memory-config.js"; export * from "./messages.js"; export * from "./messaging.js"; export * from "./model-credentials.js"; +export * from "./project-goals.js"; +export * from "./project-workspace.js"; export * from "./repos.js"; export * from "./scope.js"; export * from "./spaces.js"; diff --git a/packages/db/src/project-goals.test.ts b/packages/db/src/project-goals.test.ts index 10f4574..8837a22 100644 --- a/packages/db/src/project-goals.test.ts +++ b/packages/db/src/project-goals.test.ts @@ -37,7 +37,8 @@ describe("project goals", () => { sectionId: "sec_evoo", groupId: "grp_1", title: "Voice First", - description: "Voice-first Interaktion als primäre evOo-Erfahrung — sprechen statt tippen, wo es passt.", + description: + "Voice-first Interaktion als primäre evOo-Erfahrung — sprechen statt tippen, wo es passt.", status: "open", createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-01T00:00:00.000Z"), @@ -48,7 +49,8 @@ describe("project goals", () => { sectionId: "sec_evoo", groupId: "grp_1", title: "Voice First", - description: "Voice-first Interaktion als primäre evOo-Erfahrung — sprechen statt tippen, wo es passt.", + description: + "Voice-first Interaktion als primäre evOo-Erfahrung — sprechen statt tippen, wo es passt.", status: "open", createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-01T00:00:00.000Z"), diff --git a/packages/db/src/project-workspace.test.ts b/packages/db/src/project-workspace.test.ts index 85662b9..22c04f5 100644 --- a/packages/db/src/project-workspace.test.ts +++ b/packages/db/src/project-workspace.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -10,7 +10,7 @@ import { wireProjectWorkspace, } from "./project-workspace.js"; -vi.mock("@rakazo/core/git-checkout.js", () => ({ +vi.mock("@rakazo/core/git-checkout", () => ({ readGitCheckout: vi.fn(async () => ({ present: true, remoteUrl: "https://github.com/example/demo.git", diff --git a/packages/db/src/project-workspace.ts b/packages/db/src/project-workspace.ts index 5a902dd..d5d39c1 100644 --- a/packages/db/src/project-workspace.ts +++ b/packages/db/src/project-workspace.ts @@ -24,7 +24,7 @@ export function normalizeProjectWorkspacePath(input: string): string { export async function assertProjectWorkspaceDirectory(workspacePath: string): Promise { const resolved = normalizeProjectWorkspacePath(workspacePath); - let info; + let info: Awaited>; try { await access(resolved); info = await stat(resolved); diff --git a/packages/testkit/src/authorization.test.ts b/packages/testkit/src/authorization.test.ts index 0cae8c8..6fe0dd9 100644 --- a/packages/testkit/src/authorization.test.ts +++ b/packages/testkit/src/authorization.test.ts @@ -476,6 +476,51 @@ describeWithDatabase("API authorization and resource isolation", () => { expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.toBeNull(); }); + it("joins the organization default space once", async () => { + const owner = await signup( + app, + `default-join-owner-${stamp}@rakazo.test`, + "Default Join Owner", + ); + const member = await signup( + app, + `default-join-member-${stamp}@rakazo.test`, + "Default Join Member", + ); + const ownerActor = await rpc(app, owner, "me"); + const memberActor = await rpc(app, member, "me"); + const space = await handles.prisma.space.findUniqueOrThrow({ + where: { id: ownerActor.spaceId }, + select: { organizationId: true, isDefault: true }, + }); + expect(space.isDefault).toBe(true); + + const joinId = `default-join-${stamp}`; + await joinMemberOnSpace(handles.prisma, { + id: joinId, + organizationId: space.organizationId, + spaceId: ownerActor.spaceId, + userId: memberActor.userId, + }); + const joined = await handles.prisma.spaceMember.findMany({ + where: { spaceId: ownerActor.spaceId, userId: memberActor.userId }, + }); + expect(joined).toHaveLength(1); + expect(joined[0]?.role).toBe("member"); + + await joinMemberOnSpace(handles.prisma, { + id: `${joinId}-again`, + organizationId: space.organizationId, + spaceId: ownerActor.spaceId, + userId: memberActor.userId, + }); + const rejoined = await handles.prisma.spaceMember.findMany({ + where: { spaceId: ownerActor.spaceId, userId: memberActor.userId }, + }); + expect(rejoined).toHaveLength(1); + expect(rejoined[0]?.role).toBe("member"); + }); + it("prevents one shared-space member from hijacking another member's Team Computer takeover", async () => { const owner = await signup(app, `takeover-owner-${stamp}@rakazo.test`, "Takeover Owner"); const member = await signup(app, `takeover-member-${stamp}@rakazo.test`, "Takeover Member"); @@ -487,25 +532,11 @@ describeWithDatabase("API authorization and resource isolation", () => { select: { organizationId: true }, }); - await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); - await handles.prisma.member.create({ - data: { - id: `takeover-org-member-${stamp}`, - organizationId: sharedRow.organizationId, - userId: memberActor.userId, - role: "member", - createdAt: new Date(), - }, - }); - await handles.prisma.spaceMember.create({ - data: { - id: `takeover-space-member-${stamp}`, - spaceId: sharedSpaceId, - organizationId: sharedRow.organizationId, - userId: memberActor.userId, - role: "member", - createdAt: new Date(), - }, + await joinMemberOnSpace(handles.prisma, { + id: `takeover-org-member-${stamp}`, + organizationId: sharedRow.organizationId, + spaceId: sharedSpaceId, + userId: memberActor.userId, }); const ownerBot = await rpc( @@ -1947,6 +1978,42 @@ function connectionInput(displayName: string) { return { provider: "test-provider", displayName }; } +/** + * Move a user onto another organization's space. + * Inserting `member` already grants the organization's default space + * (`ensure_default_space_membership`). Creating that row again hits + * `space_members_spaceId_userId_key` on a shared Postgres. + */ +async function joinMemberOnSpace( + prisma: AppHandles["prisma"], + input: { id: string; organizationId: string; spaceId: string; userId: string }, +) { + await prisma.spaceMember.deleteMany({ where: { userId: input.userId } }); + await prisma.member.deleteMany({ where: { userId: input.userId } }); + await prisma.member.create({ + data: { + id: input.id, + organizationId: input.organizationId, + userId: input.userId, + role: "member", + createdAt: new Date(), + }, + }); + const where = { spaceId_userId: { spaceId: input.spaceId, userId: input.userId } }; + const existing = await prisma.spaceMember.findUnique({ where }); + if (existing) return existing; + return prisma.spaceMember.create({ + data: { + id: `${input.id}-space`, + spaceId: input.spaceId, + organizationId: input.organizationId, + userId: input.userId, + role: "member", + createdAt: new Date(), + }, + }); +} + async function signup(app: App, email: string, name: string) { const response = await app.request("/api/auth/sign-up/email", { method: "POST", diff --git a/packages/testkit/src/cli/harness.ts b/packages/testkit/src/cli/harness.ts index abe1ca8..a9e2f8d 100644 --- a/packages/testkit/src/cli/harness.ts +++ b/packages/testkit/src/cli/harness.ts @@ -205,6 +205,10 @@ async function main() { await waitForHealth(`http://127.0.0.1:${apiPort}/health`, 15_000); try { + execSync("rm -rf apps/web/node_modules/.vite node_modules/.vite", { + stdio: "inherit", + env: process.env, + }); try { await runProcess( "pnpm", diff --git a/packages/ui-web/src/bot-avatar.tsx b/packages/ui-web/src/bot-avatar.tsx index 833d012..2d229f8 100644 --- a/packages/ui-web/src/bot-avatar.tsx +++ b/packages/ui-web/src/bot-avatar.tsx @@ -426,7 +426,12 @@ export const BotAvatar = memo(function BotAvatar({ - +
); @@ -462,4 +467,3 @@ export function GrokShapePreview({ ); } - diff --git a/packages/ui-web/src/evoo-character/brand-lock.test.ts b/packages/ui-web/src/evoo-character/brand-lock.test.ts index 29b85ad..581a709 100644 --- a/packages/ui-web/src/evoo-character/brand-lock.test.ts +++ b/packages/ui-web/src/evoo-character/brand-lock.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; import { + assertEvooEyeBrandLock, EVOO_EYE_ASYMMETRY_MIN_RATIO, EVOO_EYE_BIG_R, EVOO_EYE_SMALL_R, EVOO_OO_TRADEMARK, EVOO_PUPIL_BIG_R, EVOO_PUPIL_SMALL_R, - assertEvooEyeBrandLock, } from "./brand-lock.js"; describe("evOo Oo brand lock", () => { diff --git a/packages/ui-web/src/evoo-character/brand-lock.ts b/packages/ui-web/src/evoo-character/brand-lock.ts index 637cd35..c13fd9d 100644 --- a/packages/ui-web/src/evoo-character/brand-lock.ts +++ b/packages/ui-web/src/evoo-character/brand-lock.ts @@ -12,8 +12,7 @@ */ /** Canonical trademark string — cite in reviews and design docs. */ -export const EVOO_OO_TRADEMARK = - "Oo trademark = asymmetric Kulleraugen + size difference" as const; +export const EVOO_OO_TRADEMARK = "Oo trademark = asymmetric Kulleraugen + size difference" as const; /** Big eye (capital O) — outer sclera radius in mascot viewBox units. */ export const EVOO_EYE_BIG_R = 30; @@ -36,12 +35,11 @@ export const EVOO_BRAND_LOCK_RULES = [ ] as const; /** Dev/test guard — throws if constants violate the trademark asymmetry. */ -export function assertEvooEyeBrandLock( - bigR = EVOO_EYE_BIG_R, - smallR = EVOO_EYE_SMALL_R, -): void { +export function assertEvooEyeBrandLock(bigR = EVOO_EYE_BIG_R, smallR = EVOO_EYE_SMALL_R): void { if (!(bigR > smallR)) { - throw new Error(`${EVOO_OO_TRADEMARK}: big eye radius must exceed small (${bigR} vs ${smallR})`); + throw new Error( + `${EVOO_OO_TRADEMARK}: big eye radius must exceed small (${bigR} vs ${smallR})`, + ); } const ratio = bigR / smallR; if (ratio < EVOO_EYE_ASYMMETRY_MIN_RATIO) { diff --git a/packages/ui-web/src/evoo-character/index.ts b/packages/ui-web/src/evoo-character/index.ts index f6dcbad..a418a12 100644 --- a/packages/ui-web/src/evoo-character/index.ts +++ b/packages/ui-web/src/evoo-character/index.ts @@ -1,4 +1,3 @@ -export type { EvooAccessory, EvooMouthStyle, EvooPose, EvooPoseDefinition } from "./types.js"; export { assertEvooEyeBrandLock, EVOO_BRAND_LOCK_RULES, @@ -9,7 +8,7 @@ export { EVOO_PUPIL_BIG_R, EVOO_PUPIL_SMALL_R, } from "./brand-lock.js"; -export { POSE_DEFINITIONS } from "./poses.js"; +export { EvooCharacterSvg, type EvooCharacterSvgProps } from "./EvooCharacterSvg.js"; export { EVOO_CHARACTER_SPRING, EVOO_MOUTH_LAG_MS, @@ -29,4 +28,5 @@ export { resolveEvooPose, type VoicePresencePhase, } from "./pose-map.js"; -export { EvooCharacterSvg, type EvooCharacterSvgProps } from "./EvooCharacterSvg.js"; +export { POSE_DEFINITIONS } from "./poses.js"; +export type { EvooAccessory, EvooMouthStyle, EvooPose, EvooPoseDefinition } from "./types.js"; diff --git a/packages/ui-web/src/evoo-character/motion-tokens.ts b/packages/ui-web/src/evoo-character/motion-tokens.ts index bb6497e..369faab 100644 --- a/packages/ui-web/src/evoo-character/motion-tokens.ts +++ b/packages/ui-web/src/evoo-character/motion-tokens.ts @@ -7,13 +7,12 @@ export const EVOO_MOUTH_LAG_MS = { min: 40, max: 80 } as const; export const EVOO_POSE_TRANSITION_MS = { min: 220, max: 320 } as const; /** Midpoint transition used in SVG/CSS. */ -export const EVOO_POSE_TRANSITION_S = (EVOO_POSE_TRANSITION_MS.min + EVOO_POSE_TRANSITION_MS.max) / 2 / 1000; +export const EVOO_POSE_TRANSITION_S = + (EVOO_POSE_TRANSITION_MS.min + EVOO_POSE_TRANSITION_MS.max) / 2 / 1000; /** Cute / soft / snap — character springs only. */ export const EVOO_CHARACTER_SPRING = "cubic-bezier(0.34, 1.25, 0.64, 1)"; export function evooMouthLagMs(seed = Math.random()): number { - return Math.round( - EVOO_MOUTH_LAG_MS.min + seed * (EVOO_MOUTH_LAG_MS.max - EVOO_MOUTH_LAG_MS.min), - ); + return Math.round(EVOO_MOUTH_LAG_MS.min + seed * (EVOO_MOUTH_LAG_MS.max - EVOO_MOUTH_LAG_MS.min)); } diff --git a/packages/ui-web/src/evoo-character/pose-map.ts b/packages/ui-web/src/evoo-character/pose-map.ts index ee39ab4..1a74a48 100644 --- a/packages/ui-web/src/evoo-character/pose-map.ts +++ b/packages/ui-web/src/evoo-character/pose-map.ts @@ -16,10 +16,7 @@ export function normalizeEvooPose(pose: EvooPose): EvooPose { export type VoicePresencePhase = "listening" | "thinking" | "speaking"; /** Voice Stage phase → mascot pose. */ -export function mapVoicePhaseToPose( - phase: VoicePresencePhase, - collaborating = false, -): EvooPose { +export function mapVoicePhaseToPose(phase: VoicePresencePhase, collaborating = false): EvooPose { if (collaborating) return "collaborating"; switch (phase) { case "listening": diff --git a/packages/ui-web/src/index.ts b/packages/ui-web/src/index.ts index ec1eee3..f78eef1 100644 --- a/packages/ui-web/src/index.ts +++ b/packages/ui-web/src/index.ts @@ -1,5 +1,4 @@ export { type AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js"; -export { type GrokColorDef } from "./bot-avatar.js"; export { BotAvatar, DEFAULT_GROK_BOT_COLOR, @@ -7,6 +6,7 @@ export { GROK_COLOR_LIST, GROK_MASCOT_SHAPES, GROK_SHAPES, + type GrokColorDef, GrokShapePreview, parseBotAvatar, resolvePersonaColorDef, @@ -41,12 +41,10 @@ export * from "./components/ui/toggle.js"; export * from "./components/ui/tooltip.js"; export * from "./components/ui/typography.js"; export { + EVOO_OO_TRADEMARK, EvooMascot, type EvooMascotProps, type EvooPose, - type PointerTracking, - pupilOffsetFromPointer, - Wordmark, mapBotStatusToPose, mapCollaborationToPose, mapOnboardingToPose, @@ -54,10 +52,12 @@ export { mapStreamingToPose, mapVoicePhaseToPose, mapWelcomeContextToPose, - EVOO_OO_TRADEMARK, normalizeEvooPose, + type PointerTracking, + pupilOffsetFromPointer, resolveEvooPose, type VoicePresencePhase, + Wordmark, } from "./evoo-brand.js"; export { POSE_DEFINITIONS } from "./evoo-character/index.js"; export { GroupAvatar, type GroupAvatarMember, type GroupAvatarProps } from "./group-avatar.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92c6b05..8bd9c5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,8 +123,8 @@ importers: apps/mobile: dependencies: '@expo/metro-runtime': - specifier: 57.0.15 - version: 57.0.15(@expo/log-box@57.0.4)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + specifier: 57.0.16 + version: 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@rakazo/chat-ui': specifier: workspace:* version: link:../../packages/chat-ui @@ -144,65 +144,65 @@ importers: specifier: 13.1.4 version: 13.1.4(@expo/config-plugins@57.0.9(typescript@6.0.3))(@react-native/assets-registry@0.86.3)(expo-font@57.0.4)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo: - specifier: 57.0.23 - version: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + specifier: 57.0.24 + version: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-application: specifier: 57.0.3 - version: 57.0.3(expo@57.0.23) + version: 57.0.3(expo@57.0.24) expo-audio: specifier: 57.0.5 - version: 57.0.5(expo-asset@57.0.17(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.5(expo-asset@57.0.18(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-clipboard: specifier: 57.0.2 - version: 57.0.2(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-constants: - specifier: 57.0.18 - version: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + specifier: 57.0.19 + version: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) expo-document-picker: specifier: 57.0.2 - version: 57.0.2(expo@57.0.23) + version: 57.0.2(expo@57.0.24) expo-file-system: specifier: 57.0.7 - version: 57.0.7(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + version: 57.0.7(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) expo-font: specifier: 57.0.4 - version: 57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-image-picker: - specifier: 57.0.18 - version: 57.0.18(expo@57.0.23) + specifier: 57.0.19 + version: 57.0.19(expo@57.0.24) expo-linking: specifier: 57.0.10 - version: 57.0.10(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.10(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-localization: specifier: 57.0.2 - version: 57.0.2(expo@57.0.23)(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react@19.2.3) expo-modules-core: specifier: 57.0.18 version: 57.0.18(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-notifications: - specifier: 57.0.19 - version: 57.0.19(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + specifier: 57.0.20 + version: 57.0.20(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-router: - specifier: 57.0.21 - version: 57.0.21(pd2vm6qssqksk7t6is42kbgss4) + specifier: 57.0.22 + version: 57.0.22(6hfov6v5me5ozckc4eijim4ica) expo-secure-store: specifier: 57.0.4 - version: 57.0.4(expo@57.0.23) + version: 57.0.4(expo@57.0.24) expo-sharing: - specifier: 57.0.20 - version: 57.0.20(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + specifier: 57.0.21 + version: 57.0.21(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-splash-screen: specifier: 57.0.9 - version: 57.0.9(expo@57.0.23)(typescript@6.0.3) + version: 57.0.9(expo@57.0.24)(typescript@6.0.3) expo-status-bar: specifier: 57.0.1 - version: 57.0.1(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.1(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-symbols: specifier: 57.0.3 - version: 57.0.3(expo-font@57.0.4)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + version: 57.0.3(expo-font@57.0.4)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-updates: - specifier: 57.0.22 - version: 57.0.22(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + specifier: 57.0.23 + version: 57.0.23(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -2356,8 +2356,8 @@ packages: '@expo-google-fonts/material-symbols@0.4.43': resolution: {integrity: sha512-jJzgVcbkJnl48X5iBFNaL5CQHwoBElJapl8unc2Ri8DnyuBR+gRiuAD4gu3ESZjjo3sknIoYIwloOsWzKbLknQ==} - '@expo/cli@57.0.25': - resolution: {integrity: sha512-G2FJfwClIPNPhH2sJlIFAkhQLYaKNGzSarIQUl6sVA8oGdrmBnhRvbtTzxDzjS6WzO2aDX8DY7y1aNbCFILodg==} + '@expo/cli@57.0.26': + resolution: {integrity: sha512-vm769xuEyHMToi3uY66JLzRA2GGK+3WzdH71HNn67FuVhry82dKTvMUoeHYtHiindyeieiWfEvHT8Kr1VyEVPQ==} hasBin: true peerDependencies: expo: '*' @@ -2444,8 +2444,8 @@ packages: '@expo/metro-file-map@57.0.3': resolution: {integrity: sha512-1OXy+uPYY5uc7Tm4VBsd2NRn+3wHhqeqNuEO/Xo4kmYgv8FjYgUAc+bUXON9FpC2ikcLn4EVlGM9ce2exx9Mlg==} - '@expo/metro-runtime@57.0.15': - resolution: {integrity: sha512-9RaFkqIzRud6ol9fc5ZKHgNDYeDjQVBdAQC+rjSQ6RiKV0pY+SjDqGmKY5a4M9TaV0qLMCNNrp5R88BqC2xbuQ==} + '@expo/metro-runtime@57.0.16': + resolution: {integrity: sha512-x43WgZoICpp37BhDV7vixe0bNxbVPPGaBLHYc98ZJ8MU+UC2dk39Y2XvkfYMRRbAUSIkCIJiwdERnptlPg0Ckw==} peerDependencies: '@expo/log-box': ^57.0.4 expo: '*' @@ -2515,8 +2515,8 @@ packages: '@expo/sudo-prompt@9.3.2': resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} - '@expo/ui@57.0.18': - resolution: {integrity: sha512-9RkA26v0FSyHlkQCyavZrT8EzsALfkNHIFO5uC+IsAxzWzItdOWdy6asjpg1yaCvBEGMYkP17FUZxluhVbeleA==} + '@expo/ui@57.0.19': + resolution: {integrity: sha512-WB95zQPge5nb04zgY3O2NmMtxg6G+qFH3yOAHcBkTUrJXoBReQoyY3cfp/8N5Q/azcfHJcBE3Jy0Rwq9OoRrfA==} peerDependencies: '@babel/core': '*' expo: '*' @@ -6073,8 +6073,8 @@ packages: peerDependencies: expo: '*' - expo-asset@57.0.17: - resolution: {integrity: sha512-qSdWZBSfEg06o3GJMwcj36tw0vXkB6eYjqkQistWmyJkHxhUOKnsKJnev6OraXxLdoI+KOd1+qgp/ENx8gcqYA==} + expo-asset@57.0.18: + resolution: {integrity: sha512-zDIQkUr0NM2S7YRj98Zohf/OBuM1q0o4ctxLz1C1oe4/33lHNLjp+2v3qUtBWVwn0zKka8UGADmS8M+83Wi6pw==} peerDependencies: expo: '*' react: 19.2.3 @@ -6095,8 +6095,8 @@ packages: react: 19.2.3 react-native: '*' - expo-constants@57.0.18: - resolution: {integrity: sha512-cLDwDOniPfuCZPHNY++cAY2ZkK97u5aobbBvilPK2LLmaWxR4ZpD31LWUIzUDLrwMPxLjmh1uURnbeYEf0Mivw==} + expo-constants@57.0.19: + resolution: {integrity: sha512-3eK1kZRwCdMizQLlM/PPlPwjaPnB5qKvdRDMBMnB1ZxmvWJBf9TB4uEO4DMMfIPaFUxYFs29dsR5fPr1gh3FTQ==} peerDependencies: expo: '*' react-native: '*' @@ -6134,8 +6134,8 @@ packages: peerDependencies: expo: '*' - expo-image-picker@57.0.18: - resolution: {integrity: sha512-3uhAHBbHntwarySehZipSQEoPo6nNpyP41t0ZjznkRHN+vDq36GvecT+nIF5C0jgOFN0srbXr7EfjgI0kYsguA==} + expo-image-picker@57.0.19: + resolution: {integrity: sha512-SWKY235q7TOmlGhFTUWhJZnRqrW/HYHG/6USypa1qgnaQ0ndvToUGjZwVMeAlqtJeqJCXOXSAJzXTxkw7jJc+A==} peerDependencies: expo: '*' @@ -6184,21 +6184,21 @@ packages: peerDependencies: react-native: '*' - expo-notifications@57.0.19: - resolution: {integrity: sha512-JW/usGEwOxWzN8s0L7+j3FAoB39GtaZtpUGye2lPvcqYSkvewpZYkrC16P1JaCP09YRx0Q+qXPx5NUoUU8NsBA==} + expo-notifications@57.0.20: + resolution: {integrity: sha512-T1xaydHHlsOt6sA9DlSC74Kt+yQb9vwM79dBcac5U/5GPdU7e80LleDleQbMgUXO4c2W/ZKBCljDONyvOqteXQ==} peerDependencies: expo: '*' react: 19.2.3 react-native: '*' - expo-router@57.0.21: - resolution: {integrity: sha512-r7UDLZU0gtVm8FW4nv2pxSl9TDhKlf2shcBbfp1T9STXJ3xYl9llLH701qQkICuXqqt3vGpOZ57GXC1ioVWg4g==} + expo-router@57.0.22: + resolution: {integrity: sha512-kOuS3RjMC73YLyBFiV9BzO0Z5OK1nLhPwmptK2II+tePtWXH7oA4zWaPwdWsdzTJRnmGMs/8Sr8YdTPHm1LUCg==} peerDependencies: '@expo/log-box': ^57.0.4 - '@expo/metro-runtime': ^57.0.15 + '@expo/metro-runtime': ^57.0.16 '@testing-library/react-native': '>= 13.2.0' expo: '*' - expo-constants: ^57.0.18 + expo-constants: ^57.0.19 expo-linking: ^57.0.10 react: 19.2.3 react-dom: 19.2.3 @@ -6232,8 +6232,8 @@ packages: resolution: {integrity: sha512-aK+LdKzauHSGmsOStZtyxdzv0zWssCkxTw3m4QuOhfDSJsZaMRTd9O41d8ixU/QfELTbaJ0oRNcF7JFV/7O9YQ==} engines: {node: '>=20.16.0'} - expo-sharing@57.0.20: - resolution: {integrity: sha512-c3VMxuiIx5wQrQnVL7cXxmEvQm5v/fcrtuLkFMMXa1HVJTD6qg8qmT+Uqf1sTUe4RZdzEDyIMCKaZ5FqvZjKHw==} + expo-sharing@57.0.21: + resolution: {integrity: sha512-7Kv3ai5kdSZ/3Ok05AOWUcXSfDrqeQ5rKpWztIvi86A4WxQCed+DV4AtnhfEwZ0ZeFxJj2Au/mjfQ06J4YK8HA==} peerDependencies: expo: '*' react: 19.2.3 @@ -6267,8 +6267,8 @@ packages: peerDependencies: expo: '*' - expo-updates@57.0.22: - resolution: {integrity: sha512-W/wR3PZiIiu+DtbNFju0WXJF0eqR6m0CNorjmxoQ9/RGiplpIZTrFOBp74dwsU1DOk3qpsivfLHsotR+mGTwkQ==} + expo-updates@57.0.23: + resolution: {integrity: sha512-try1ndXkEH1qpMqs2Nf1fXxN/0GShZad9dWp8pbQpXAngyRk8k/MxJ5gJuVabkPJ2vEW3IgNvTkq54aOELkpZA==} hasBin: true peerDependencies: expo: '*' @@ -6279,8 +6279,8 @@ packages: expo-dev-client: optional: true - expo@57.0.23: - resolution: {integrity: sha512-/aBLZFTD7+BNHWV/q5XbNRFqMhW/CrHsYeq8CcS/iGz6lJHYwEMm4XFcdoaeliuMdo6ZjNrsTHkBIdGB8VMHTg==} + expo@57.0.24: + resolution: {integrity: sha512-LxSnKMv7en/w4XDNxW3VOx7SbtQ2Bs4MyGhtoZCToDsEsBPO7pJXlv5GnQUCd7Wi8118QV/VDwsDLBKTB3Z3iQ==} hasBin: true peerDependencies: '@expo/dom-webview': '*' @@ -11652,7 +11652,7 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.43': {} - '@expo/cli@57.0.25(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)': + '@expo/cli@57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 57.0.9(typescript@6.0.3) @@ -11662,16 +11662,16 @@ snapshots: '@expo/image-utils': 0.11.5(typescript@6.0.3) '@expo/inline-modules': 0.1.7(typescript@6.0.3) '@expo/json-file': 11.0.1 - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@expo/metro': 56.0.2 - '@expo/metro-config': 57.0.12(expo@57.0.23)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.24)(typescript@6.0.3) '@expo/metro-file-map': 57.0.3 '@expo/osascript': 2.7.1 '@expo/package-manager': 1.13.1 '@expo/plist': 0.8.1 '@expo/prebuild-config': 57.0.16(typescript@6.0.3) '@expo/require-utils': 57.0.5(typescript@6.0.3) - '@expo/router-server': 57.0.10(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo-server@57.0.3)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@expo/router-server': 57.0.10(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo-server@57.0.3)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/schema-utils': 57.0.2 '@expo/spawn-async': 1.8.0 '@expo/ws-tunnel': 2.0.0(ws@8.21.3) @@ -11688,7 +11688,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.6 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-server: 57.0.3 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -11715,7 +11715,7 @@ snapshots: ws: 8.21.3 zod: 3.25.76 optionalDependencies: - expo-router: 57.0.21(pd2vm6qssqksk7t6is42kbgss4) + expo-router: 57.0.22(6hfov6v5me5ozckc4eijim4ica) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) transitivePeerDependencies: - '@expo/dom-webview' @@ -11805,9 +11805,9 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - '@expo/dom-webview@57.0.1(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': + '@expo/dom-webview@57.0.1(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) @@ -11870,16 +11870,16 @@ snapshots: - supports-color - typescript - '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': dependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) anser: 1.4.10 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) stacktrace-parser: 0.1.11 - '@expo/metro-config@57.0.12(expo@57.0.23)(typescript@6.0.3)': + '@expo/metro-config@57.0.12(expo@57.0.24)(typescript@6.0.3)': dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 @@ -11905,7 +11905,7 @@ snapshots: postcss: 8.5.28 resolve-from: 5.0.0 optionalDependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -11923,11 +11923,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@57.0.15(@expo/log-box@57.0.4)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': + '@expo/metro-runtime@57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) anser: 1.4.10 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) pretty-format: 29.7.0 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) @@ -12013,17 +12013,17 @@ snapshots: - supports-color optional: true - '@expo/router-server@57.0.10(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo-server@57.0.3)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@expo/router-server@57.0.10(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo-server@57.0.3)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) - expo-font: 57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo-font: 57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-server: 57.0.3 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) - expo-router: 57.0.21(pd2vm6qssqksk7t6is42kbgss4) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo-router: 57.0.22(6hfov6v5me5ozckc4eijim4ica) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -12038,9 +12038,9 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@57.0.18(@babel/core@7.29.7)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': + '@expo/ui@57.0.19(@babel/core@7.29.7)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) sf-symbols-typescript: 2.2.0 @@ -13380,7 +13380,7 @@ snapshots: react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) optionalDependencies: '@react-native/assets-registry': 0.86.3 - expo-font: 57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo-font: 57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@react-native-vector-icons/ionicons@13.1.4(@expo/config-plugins@57.0.9(typescript@6.0.3))(@react-native/assets-registry@0.86.3)(expo-font@57.0.4)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)': dependencies: @@ -14790,7 +14790,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - babel-preset-expo@57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.23)(react-refresh@0.14.2): + babel-preset-expo@57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.14.2): dependencies: '@babel/generator': 7.29.8 '@babel/helper-module-imports': 7.29.7 @@ -14837,7 +14837,7 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -16136,85 +16136,85 @@ snapshots: expect-type@1.4.0: {} - expo-application@57.0.3(expo@57.0.23): + expo-application@57.0.3(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-asset@57.0.17(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo-asset@57.0.18(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.11.5(typescript@6.0.3) - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) transitivePeerDependencies: - supports-color - typescript - expo-audio@57.0.5(expo-asset@57.0.17(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-audio@57.0.5(expo-asset@57.0.18(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-asset: 57.0.17(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-asset: 57.0.18(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-clipboard@57.0.2(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-clipboard@57.0.2(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-constants@57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)): + expo-constants@57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)): dependencies: '@expo/env': 2.4.3 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) transitivePeerDependencies: - supports-color - expo-document-picker@57.0.2(expo@57.0.23): + expo-document-picker@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-eas-client@57.0.4: {} - expo-file-system@57.0.7(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)): + expo-file-system@57.0.7(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-font@57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-font@57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) fontfaceobserver: 2.3.0 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-glass-effect@57.0.3(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-glass-effect@57.0.3(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-image-loader@57.0.1(expo@57.0.23): + expo-image-loader@57.0.1(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-image-picker@57.0.18(expo@57.0.23): + expo-image-picker@57.0.19(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-image-loader: 57.0.1(expo@57.0.23) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-image-loader: 57.0.1(expo@57.0.24) expo-json-utils@57.0.2: {} - expo-keep-awake@57.0.2(expo@57.0.23)(react@19.2.3): + expo-keep-awake@57.0.2(expo@57.0.24)(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 - expo-linking@57.0.10(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-linking@57.0.10(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) invariant: 2.2.4 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) @@ -16222,15 +16222,15 @@ snapshots: - expo - supports-color - expo-localization@57.0.2(expo@57.0.23)(react@19.2.3): + expo-localization@57.0.2(expo@57.0.24)(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 rtl-detect: 1.1.2 - expo-manifests@57.0.2(expo@57.0.23): + expo-manifests@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-json-utils: 57.0.2 expo-modules-autolinking@57.0.13(typescript@6.0.3): @@ -16257,26 +16257,26 @@ snapshots: dependencies: react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) - expo-notifications@57.0.19(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo-notifications@57.0.20(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.11.5(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-application: 57.0.3(expo@57.0.23) - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-application: 57.0.3(expo@57.0.24) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) transitivePeerDependencies: - supports-color - typescript - expo-router@57.0.21(pd2vm6qssqksk7t6is42kbgss4): + expo-router@57.0.22(6hfov6v5me5ozckc4eijim4ica): dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@expo/schema-utils': 57.0.2 - '@expo/ui': 57.0.18(@babel/core@7.29.7)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/ui': 57.0.19(@babel/core@7.29.7)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.3) '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-masked-view/masked-view': 0.3.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) @@ -16284,12 +16284,12 @@ snapshots: color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) - expo-glass-effect: 57.0.3(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) - expo-linking: 57.0.10(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo-glass-effect: 57.0.3(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo-linking: 57.0.10(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) expo-server: 57.0.3 - expo-symbols: 57.0.3(expo-font@57.0.4)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo-symbols: 57.0.3(expo-font@57.0.4)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 @@ -16319,56 +16319,56 @@ snapshots: - react-native-worklets - supports-color - expo-secure-store@57.0.4(expo@57.0.23): + expo-secure-store@57.0.4(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-server@57.0.3: {} - expo-sharing@57.0.20(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo-sharing@57.0.21(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/config-plugins': 57.0.9(typescript@6.0.3) '@expo/config-types': 57.0.2 '@expo/plist': 0.8.1 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) transitivePeerDependencies: - supports-color - typescript - expo-splash-screen@57.0.9(expo@57.0.23)(typescript@6.0.3): + expo-splash-screen@57.0.9(expo@57.0.24)(typescript@6.0.3): dependencies: '@expo/config-plugins': 57.0.9(typescript@6.0.3) '@expo/image-utils': 0.11.5(typescript@6.0.3) - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) xml2js: 0.6.0 transitivePeerDependencies: - supports-color - typescript - expo-status-bar@57.0.1(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-status-bar@57.0.1(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) expo-structured-headers@57.0.1: {} - expo-symbols@57.0.3(expo-font@57.0.4)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-symbols@57.0.3(expo-font@57.0.4)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.43 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-font: 57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-font: 57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3) sf-symbols-typescript: 2.2.0 - expo-updates-interface@57.0.2(expo@57.0.23): + expo-updates-interface@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-updates@57.0.22(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): + expo-updates@57.0.23(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.8.1 @@ -16376,11 +16376,11 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3 - expo: 57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-eas-client: 57.0.4 - expo-manifests: 57.0.2(expo@57.0.23) + expo-manifests: 57.0.2(expo@57.0.24) expo-structured-headers: 57.0.1 - expo-updates-interface: 57.0.2(expo@57.0.23) + expo-updates-interface: 57.0.2(expo@57.0.24) getenv: 2.0.0 glob: 13.0.6 ignore: 5.3.2 @@ -16391,25 +16391,25 @@ snapshots: transitivePeerDependencies: - supports-color - expo@57.0.23(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo@57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 57.0.25(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + '@expo/cli': 57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) '@expo/config': 57.0.9(typescript@6.0.3) '@expo/config-plugins': 57.0.9(typescript@6.0.3) '@expo/devtools': 57.0.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@expo/fingerprint': 0.20.13 '@expo/local-build-cache-provider': 57.0.8(typescript@6.0.3) - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) '@expo/metro': 56.0.2 - '@expo/metro-config': 57.0.12(expo@57.0.23)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.24)(typescript@6.0.3) '@ungap/structured-clone': 1.3.3 - babel-preset-expo: 57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.23)(react-refresh@0.14.2) - expo-asset: 57.0.17(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) - expo-file-system: 57.0.7(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) - expo-font: 57.0.4(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) - expo-keep-awake: 57.0.2(expo@57.0.23)(react@19.2.3) + babel-preset-expo: 57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.14.2) + expo-asset: 57.0.18(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo-file-system: 57.0.7(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)) + expo-font: 57.0.4(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + expo-keep-awake: 57.0.2(expo@57.0.24)(react@19.2.3) expo-modules-autolinking: 57.0.13(typescript@6.0.3) expo-modules-core: 57.0.18(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) pretty-format: 29.7.0 @@ -16418,8 +16418,8 @@ snapshots: react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.23)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.23)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.24)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-native-webview: 13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3) diff --git a/scripts/lint.mjs b/scripts/lint.mjs index 7dd24b7..126c19c 100644 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -48,8 +48,11 @@ function validateSchema(value, schema, label) { if (Array.isArray(value)) { if (schema.minItems != null && value.length < schema.minItems) fail(`${label}: too few items`); if (schema.maxItems != null && value.length > schema.maxItems) fail(`${label}: too many items`); - if (schema.items) - value.forEach((item, index) => validateSchema(item, schema.items, `${label}[${index}]`)); + if (schema.items) { + for (const [index, item] of value.entries()) { + validateSchema(item, schema.items, `${label}[${index}]`); + } + } } if (actualType === "object") { for (const key of schema.required || []) {