mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
* fix(app): seat the subagent sheet's parent callout like the composer The sheet tab body padded the bottom safe-area inset, and the composer dock padded it again, so the parent callout in a subagent sheet floated a full navigation-bar height above where the composer sits in a tab. The sheet body (SheetBottomInset) now owns the inset: it pads it once and tells its content none is left, so the dock stays host-agnostic. Overlay sheets (adaptive modal sheet, menu sheet, combobox sheet) read the inset on the far side of their portal, so ones opened from sheet content still clear the screen edge. The callout takes the composer's rail and shows the parent's provider icon through StatusIcon: no icon box, with the running ring, attention dot or needs-input alert on the icon. * fix(app): give the attachment lightbox its own safe area An RN Modal passes its call site's context straight through, so a lightbox opened from a sheet tab read the bottom inset the sheet body had already spent (0) and drew over the navigation bar. The modal is its own window: it now gets its own SafeAreaProvider, which measures that window, and the image area reads its insets below it. floating-panels.md records the rule for overlays: read insets where the overlay draws. * feat(app): open a subagent in place of the subagents list on compact On phones, choosing a subagent from the subagents list now replaces the list inside the same sheet instead of stacking a second sheet over it. Back returns to the list, Close closes both, Android Back steps subagent -> list -> closed, and Open parent lands on the pane the list was opened from (for a nested subagent, its parent's sheet). - Menu engine: fill pages (`fill: true`) take the full sheet height, Back pops a page via `goBack`, and a page whose definition disappears falls back to the list. - One sheet pane (`screens/workspace/sheet-pane`) renders the header, body, bottom inset and focus for both containers: the workspace's detached sheet tabs and the in-list menu page. Only the container differs. - The header drags the sheet through `SheetDragArea`; compact menu sheets use the shared `SheetBackground`/`SheetHandle` chrome. - `selectSheetTab` describes the sheet a reveal would show without opening it. * fix(app): put the parent callout on one row The Open parent button already says what the callout is, so the Parent label goes. The parent's title shrinks to one line before the button does, so Open parent never wraps or leaves the row. * refactor(app): register panels once in the workspace screen Every reader of the panel registry called ensurePanelsRegistered, which imports every panel module. The panels render those readers (the agent panel's subagent track and parent callout, the new-tab panel's launcher, the diff panel's review surface), so each reader closed an import cycle through register-panels. The workspace screen, the only route that renders a registry reader, now registers the panels before anything under it renders, and the readers import only the leaf panel-registry. The pane content test registers a probe panel for real instead of mocking the registry. * test(app): transform the markdown WASM factory when Vite versions its URL Importing the sheet pane barrel in retained-chat-content.browser.test.tsx made Vite request md4c.js with a ?v= query, which the CommonJS-to-ESM transform did not match, so the test file failed to import.
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import { defineConfig, configDefaults } from "vitest/config";
|
|
import { playwright } from "@vitest/browser-playwright";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import { transformSync } from "@babel/core";
|
|
import { build } from "esbuild";
|
|
|
|
// Set this before Vite resolves dependencies and Vitest starts its workers.
|
|
Object.assign(process.env, { NODE_ENV: "test" });
|
|
|
|
const appNodeModules = path.resolve(__dirname, "node_modules");
|
|
const rootNodeModules = path.resolve(__dirname, "../../node_modules");
|
|
const resolvePackageEntry = (packageName: string) => {
|
|
const appPackagePath = path.resolve(appNodeModules, packageName);
|
|
return fs.existsSync(appPackagePath)
|
|
? appPackagePath
|
|
: path.resolve(rootNodeModules, packageName);
|
|
};
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
{
|
|
name: "markdown-wasm-commonjs-export",
|
|
transform(source, id) {
|
|
// The ESM package contains a CommonJS WASM factory. Metro handles the
|
|
// interop; Vite needs an ESM export instead of the CommonJS trailer.
|
|
// Vite can request the file with a version query (`md4c.js?v=<hash>`), so match the
|
|
// path without it.
|
|
if (!id.split("?")[0].endsWith("/lib/module/web/wasm/md4c.js")) return;
|
|
const trailer = source.lastIndexOf("\nif (typeof exports === 'object'");
|
|
if (trailer < 0) throw new Error("Markdown WASM CommonJS trailer was not found");
|
|
return {
|
|
code: `${source.slice(0, trailer)}\nexport default createMd4cModule;`,
|
|
map: null,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: "compile-usage-components",
|
|
enforce: "pre",
|
|
transform(source, id) {
|
|
// These labels must react to time and language in the compiled app too.
|
|
if (!id.includes("/src/usage/") || !id.endsWith(".tsx") || id.includes(".test.")) return;
|
|
const result = transformSync(source, {
|
|
filename: id,
|
|
babelrc: false,
|
|
configFile: false,
|
|
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
plugins: [["babel-plugin-react-compiler", { target: "19" }]],
|
|
sourceMaps: true,
|
|
});
|
|
if (result?.code) return { code: result.code, map: result.map };
|
|
},
|
|
},
|
|
{
|
|
name: "bundle-i18n-resources",
|
|
// Metro resolves literal requires lazily. Vite's TS transform leaves them
|
|
// to Node, which cannot resolve the resource modules. Bundle this boundary
|
|
// so unit and browser consumers exercise the same deferred module loading.
|
|
async transform(_source, id) {
|
|
if (path.resolve(id) !== path.resolve(__dirname, "src/i18n/i18next.ts")) return;
|
|
const result = await build({
|
|
entryPoints: [id],
|
|
bundle: true,
|
|
format: "esm",
|
|
platform: "neutral",
|
|
external: ["i18next", "react-i18next"],
|
|
write: false,
|
|
});
|
|
return { code: result.outputFiles[0].text, map: null };
|
|
},
|
|
},
|
|
],
|
|
test: {
|
|
environment: "node",
|
|
exclude: [...configDefaults.exclude, "e2e/**"],
|
|
projects: [
|
|
{
|
|
extends: true,
|
|
test: {
|
|
name: "unit",
|
|
environment: "node",
|
|
include: [
|
|
"src/**/*.{test,spec}.{ts,tsx}",
|
|
"native-release-version.test.ts",
|
|
"app.config.test.ts",
|
|
"vitest-environment.test.ts",
|
|
],
|
|
setupFiles: [path.resolve(__dirname, "vitest.setup.ts")],
|
|
exclude: [...configDefaults.exclude, "e2e/**", "src/**/*.browser.{test,spec}.{ts,tsx}"],
|
|
},
|
|
},
|
|
{
|
|
extends: true,
|
|
test: {
|
|
name: "browser",
|
|
fileParallelism: false,
|
|
include: ["src/**/*.browser.{test,spec}.{ts,tsx}"],
|
|
browser: {
|
|
enabled: true,
|
|
provider: playwright({
|
|
// Exercise real WebGL glyph rendering on GPU-less CI runners.
|
|
launchOptions: { args: ["--enable-unsafe-swiftshader"] },
|
|
}),
|
|
headless: true,
|
|
connectTimeout: 180_000,
|
|
instances: [{ browser: "chromium" }],
|
|
screenshotDirectory: ".vitest-screenshots",
|
|
},
|
|
globalSetup: path.resolve(__dirname, "src/runtime/websocket-test-global-setup.ts"),
|
|
},
|
|
},
|
|
],
|
|
/**
|
|
* Expo pulls in native tooling (xcode, etc.) that executes files relying on `process.send`.
|
|
* Vitest's default worker pool uses worker_threads, which intentionally stub that API and
|
|
* immediately throw `Unexpected call to process.send`. Running the suite in forked processes
|
|
* keeps `process.send` intact so the app tests can boot before hitting the intentional failures.
|
|
*/
|
|
pool: "forks",
|
|
maxWorkers: 2,
|
|
server: {
|
|
deps: {
|
|
fallbackCJS: true,
|
|
inline: [
|
|
"react-native-enriched-markdown",
|
|
"zustand",
|
|
"@tanstack/react-query",
|
|
"react-native-web",
|
|
"react-native-reanimated",
|
|
"react-native-worklets",
|
|
"react-native-gesture-handler",
|
|
"react-native-keyboard-controller",
|
|
],
|
|
},
|
|
},
|
|
},
|
|
// Reanimated and gesture-handler pick platform files by extension
|
|
// (e.g. `GestureHandlerRootView.web.js`). Vite's optimizer does not apply `resolve.extensions`,
|
|
// so it scans the native files and dies on imports react-native-web has no answer for.
|
|
// Unbundled, the same imports go through the resolver below and land on the web files.
|
|
optimizeDeps: {
|
|
// Expo's public navigation barrel also loads screens. Match Metro's web-first resolution
|
|
// while optimizing its CJS dependencies, including DebugContainer.web.js.
|
|
esbuildOptions: {
|
|
resolveExtensions: [
|
|
".web.js",
|
|
".web.jsx",
|
|
".web.ts",
|
|
".web.tsx",
|
|
".js",
|
|
".jsx",
|
|
".ts",
|
|
".tsx",
|
|
".json",
|
|
],
|
|
},
|
|
// Bundle the CJS dependencies of the excluded gesture-handler package for the browser.
|
|
include: [
|
|
"react/jsx-runtime",
|
|
"react/compiler-runtime",
|
|
"expo-router/react-navigation",
|
|
"@testing-library/react",
|
|
"@tanstack/react-query",
|
|
"react-i18next",
|
|
"i18next",
|
|
"@marijn/find-cluster-break",
|
|
"tiny-invariant",
|
|
"react/jsx-dev-runtime",
|
|
"react-native-gesture-handler > hoist-non-react-statics",
|
|
"react-native-gesture-handler > invariant",
|
|
],
|
|
exclude: [
|
|
"react-native-enriched-markdown",
|
|
"react-native-reanimated",
|
|
"react-native-gesture-handler",
|
|
"react-native-keyboard-controller",
|
|
],
|
|
},
|
|
// babel-preset-expo compiles JSX with the automatic runtime, so app modules don't import React.
|
|
esbuild: { jsx: "automatic" },
|
|
// The globals a React Native bundler defines, which esbuild is no longer there to supply for
|
|
// the package excluded above.
|
|
define: {
|
|
"process.env.JEST_WORKER_ID": "undefined",
|
|
__DEV__: "false",
|
|
global: "globalThis",
|
|
},
|
|
resolve: {
|
|
extensions: [
|
|
".web.mjs",
|
|
".web.js",
|
|
".web.mts",
|
|
".web.ts",
|
|
".web.jsx",
|
|
".web.tsx",
|
|
".mjs",
|
|
".js",
|
|
".mts",
|
|
".ts",
|
|
".jsx",
|
|
".tsx",
|
|
".json",
|
|
],
|
|
alias: [
|
|
{
|
|
find: /^@getpaseo\/relay\/e2ee$/,
|
|
replacement: path.resolve(__dirname, "../relay/src/e2ee.ts"),
|
|
},
|
|
{
|
|
find: /^@getpaseo\/relay$/,
|
|
replacement: path.resolve(__dirname, "../relay/src/index.ts"),
|
|
},
|
|
{ find: "@", replacement: path.resolve(__dirname, "src") },
|
|
// Keep keyboard-controller's imports in Vite so native aliases and platform extensions apply.
|
|
{
|
|
find: /^react-native-keyboard-controller$/,
|
|
replacement: path.resolve(
|
|
resolvePackageEntry("react-native-keyboard-controller"),
|
|
"lib/module/index.js",
|
|
),
|
|
},
|
|
// The CJS entry bypasses Vite's React Native alias and web-extension resolution.
|
|
{
|
|
find: /^react-native-gesture-handler$/,
|
|
replacement: path.resolve(
|
|
rootNodeModules,
|
|
"react-native-gesture-handler/lib/module/index.js",
|
|
),
|
|
},
|
|
// Must precede the `react-native` alias: a string `find` matches by prefix, so this subpath
|
|
// would otherwise resolve inside a react-native-web *file* and break the dependency scan.
|
|
// Reanimated only imports it on the native path, which no test takes.
|
|
{
|
|
find: /^react-native\/Libraries\/Renderer\/shims\/ReactFabric$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/react-native-fabric-shim.ts"),
|
|
},
|
|
// Point to the ESM build so Vite can transform its imports and apply the
|
|
// react alias below (the CJS build uses require('react') which bypasses
|
|
// Vite alias resolution).
|
|
{
|
|
find: "react-native",
|
|
replacement: path.resolve(rootNodeModules, "react-native-web/dist/index.js"),
|
|
},
|
|
{ find: "react", replacement: resolvePackageEntry("react") },
|
|
{
|
|
find: "react-dom",
|
|
replacement: resolvePackageEntry("react-dom"),
|
|
},
|
|
{
|
|
find: /^@xterm\/addon-ligatures\/lib\/addon-ligatures\.mjs$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/xterm-addon-ligatures.ts"),
|
|
},
|
|
{
|
|
find: /^@xterm\/addon-ligatures$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/xterm-addon-ligatures.ts"),
|
|
},
|
|
{
|
|
find: /^react-native-unistyles$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/react-native-unistyles.ts"),
|
|
},
|
|
{
|
|
find: /^react-native-svg$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/react-native-svg.ts"),
|
|
},
|
|
// Both ship untranspiled Flow and fail to parse on import, which takes out any test that
|
|
// mounts a menu surface.
|
|
{
|
|
find: /^react-native-safe-area-context$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/react-native-safe-area-context.ts"),
|
|
},
|
|
{
|
|
find: /^@gorhom\/bottom-sheet$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/gorhom-bottom-sheet.ts"),
|
|
},
|
|
{
|
|
find: /^react-native-reanimated\/scripts\/validate-worklets-version$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/reanimated-validate-worklets-version.ts"),
|
|
},
|
|
{
|
|
find: /^expo-clipboard$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/expo-clipboard.ts"),
|
|
},
|
|
{
|
|
find: /^expo-linking$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/expo-linking.ts"),
|
|
},
|
|
// No Node implementation: every call on the real module rejects, so a test
|
|
// touching a persisted store drowns in unhandled rejections.
|
|
{
|
|
find: /^@react-native-async-storage\/async-storage$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/async-storage.ts"),
|
|
},
|
|
{
|
|
find: /^lucide-react-native$/,
|
|
replacement: path.resolve(__dirname, "test-stubs/lucide-react-native.ts"),
|
|
},
|
|
],
|
|
},
|
|
});
|