diff --git a/.env.example b/.env.example index c0b913d746..e15489f8c6 100644 --- a/.env.example +++ b/.env.example @@ -206,7 +206,8 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz # app launch while keeping the current identity and relay data. # VITE_BUZZ_FORCE_FRESH_ONBOARDING=true -# Makes the default-off Bestie experiment available in eligible builds. +# Protected internal builds only: selects the module graph that contains the +# default-off Bestie experiment. Official OSS builds must leave this unset. # VITE_BUZZ_BESTIE=1 # ── Subscription & filtering ───────────────────────────────────────────────── diff --git a/desktop/package.json b/desktop/package.json index 1e93fd76a8..e810d2bc28 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && node ./scripts/build-protected-feature-artifacts.mjs", "build:e2e": "tsc && vite build --mode e2e", "typecheck": "tsc --noEmit", "check:file-sizes": "node ./scripts/check-file-sizes.mjs", diff --git a/desktop/scripts/build-protected-feature-artifacts.mjs b/desktop/scripts/build-protected-feature-artifacts.mjs new file mode 100644 index 0000000000..1fd17f6fdb --- /dev/null +++ b/desktop/scripts/build-protected-feature-artifacts.mjs @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const desktopRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +const selectedInternalVariant = process.env.VITE_BUZZ_BESTIE === "1"; +const scratchRoot = mkdtempSync( + path.join(tmpdir(), "buzz-protected-feature-artifacts-"), +); +const selectedOutput = path.join(desktopRoot, "dist"); +const alternateOutput = path.join(scratchRoot, "alternate"); +const viteBinary = process.platform === "win32" ? "vite.cmd" : "vite"; + +function buildVariant({ internal, output }) { + const env = { ...process.env }; + if (internal) { + env.VITE_BUZZ_BESTIE = "1"; + } else { + delete env.VITE_BUZZ_BESTIE; + } + + const result = spawnSync( + viteBinary, + ["build", "--outDir", output, "--emptyOutDir"], + { + cwd: desktopRoot, + env, + stdio: "inherit", + }, + ); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `${internal ? "internal" : "OSS"} desktop build failed with status ${result.status}`, + ); + } +} + +function emittedText(root) { + const chunks = []; + const visit = (candidate) => { + const stat = statSync(candidate); + if (stat.isDirectory()) { + for (const child of readdirSync(candidate)) { + visit(path.join(candidate, child)); + } + return; + } + if (/\.(?:css|html|js|json)$/u.test(candidate)) { + chunks.push(readFileSync(candidate, "utf8")); + } + }; + visit(root); + return chunks.join("\n"); +} + +function assertArtifactContract({ ossOutput, internalOutput }) { + const ossText = emittedText(ossOutput); + const internalText = emittedText(internalOutput); + const protectedContent = /\bbestie\b|chief of staff|builtin:bestie/iu; + const internalManifestMarker = + "Try a personal agent that is always close at hand"; + + if (protectedContent.test(ossText)) { + throw new Error( + "Official OSS desktop artifact contains protected Bestie/Chief content", + ); + } + if (!internalText.includes(internalManifestMarker)) { + throw new Error( + "Protected internal desktop artifact is missing the Bestie manifest", + ); + } +} + +try { + // Build the unselected variant outside dist first, then leave the requested + // variant in dist for Vite/Tauri's ordinary packaging contract. + buildVariant({ + internal: !selectedInternalVariant, + output: alternateOutput, + }); + buildVariant({ + internal: selectedInternalVariant, + output: selectedOutput, + }); + + assertArtifactContract({ + ossOutput: selectedInternalVariant ? alternateOutput : selectedOutput, + internalOutput: selectedInternalVariant ? selectedOutput : alternateOutput, + }); + console.log( + `Protected feature artifact matrix passed; dist contains the ${selectedInternalVariant ? "internal" : "OSS"} variant.`, + ); +} finally { + rmSync(scratchRoot, { recursive: true, force: true }); +} diff --git a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx index db08e8b702..50052c014f 100644 --- a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx +++ b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx @@ -1,5 +1,5 @@ import { setAgentManagedProfiles } from "@/shared/api/tauri"; -import { availableDesktopFeatures, useFeatureToggle } from "@/shared/features"; +import { desktopFeatures, useFeatureToggle } from "@/shared/features"; import type { FeatureDefinition } from "@/shared/features"; import { Switch } from "@/shared/ui/switch"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; @@ -42,7 +42,7 @@ function FeatureRow({ feature }: { feature: FeatureDefinition }) { export function ExperimentalFeaturesCard() { // Manifest is preview-only by definition; every desktop entry is a preview // feature. - const previewFeatures = availableDesktopFeatures; + const previewFeatures = desktopFeatures; return (
diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index b3bcaf7586..d242faf618 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -9,7 +9,7 @@ import { } from "@/shared/api/relayMembers"; import { getFeature, - resolveFeatureEnabledInThisBuild, + resolveEnabled, useFeatureSnapshot, } from "@/shared/features"; import { topChromeBackdrop } from "@/shared/layout/chromeLayout"; @@ -139,7 +139,7 @@ export function SettingsView({ const feature = getFeature(s.featureGate); if ( feature && - !resolveFeatureEnabledInThisBuild(feature, featureState) + !resolveEnabled(s.featureGate, featureState, feature.defaultEnabled) ) { return false; } diff --git a/desktop/src/protectedFeatures/internal.ts b/desktop/src/protectedFeatures/internal.ts new file mode 100644 index 0000000000..7f9f6b551e --- /dev/null +++ b/desktop/src/protectedFeatures/internal.ts @@ -0,0 +1,11 @@ +import type { FeatureDefinition } from "@/shared/features/types"; + +/** Definitions available only in the protected internal application build. */ +export const protectedFeatureDefinitions: FeatureDefinition[] = [ + { + id: "bestie", + name: "Bestie", + description: "Try a personal agent that is always close at hand", + platforms: ["desktop"], + }, +]; diff --git a/desktop/src/protectedFeatures/protectedFeatures.test.mjs b/desktop/src/protectedFeatures/protectedFeatures.test.mjs new file mode 100644 index 0000000000..fca8cc698a --- /dev/null +++ b/desktop/src/protectedFeatures/protectedFeatures.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { protectedFeatureDefinitions as internalDefinitions } from "./internal.ts"; +import { protectedFeatureDefinitions as publicDefinitions } from "./public.ts"; + +describe("protected feature build variants", () => { + it("keeps protected definitions out of the OSS module", () => { + assert.deepEqual(publicDefinitions, []); + }); + + it("adds Bestie only through the internal module", () => { + assert.deepEqual( + internalDefinitions.map((feature) => feature.id), + ["bestie"], + ); + assert.equal(internalDefinitions[0]?.defaultEnabled, undefined); + }); +}); diff --git a/desktop/src/protectedFeatures/public.ts b/desktop/src/protectedFeatures/public.ts new file mode 100644 index 0000000000..90c1e59624 --- /dev/null +++ b/desktop/src/protectedFeatures/public.ts @@ -0,0 +1,7 @@ +import type { FeatureDefinition } from "@/shared/features/types"; + +/** + * Protected feature definitions compiled into the official OSS application. + * Keep this module free of protected product names, metadata, and imports. + */ +export const protectedFeatureDefinitions: FeatureDefinition[] = []; diff --git a/desktop/src/shared/features/buildAvailability.test.mjs b/desktop/src/shared/features/buildAvailability.test.mjs deleted file mode 100644 index 688afe59bf..0000000000 --- a/desktop/src/shared/features/buildAvailability.test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { - filterBuildAvailableFeatures, - isFeatureBuildAvailable, -} from "./buildAvailability.ts"; - -const publicFeature = { - id: "projects", - name: "Projects", - description: "", -}; -const bestie = { - id: "bestie", - name: "Bestie", - description: "", - requiredBuildFlag: "bestie", -}; - -describe("feature build availability", () => { - it("keeps features without a required build flag available", () => { - assert.equal( - isFeatureBuildAvailable(publicFeature, { bestie: false }), - true, - ); - }); - - it("does not allow a required feature into an ineligible build", () => { - assert.equal(isFeatureBuildAvailable(bestie, { bestie: false }), false); - }); - - it("shows a required feature when its build opts in", () => { - assert.equal(isFeatureBuildAvailable(bestie, { bestie: true }), true); - }); - - it("filters unavailable features out of the experiment picker", () => { - assert.deepEqual( - filterBuildAvailableFeatures([publicFeature, bestie], { bestie: false }), - [publicFeature], - ); - }); - - it("includes the Bestie experiment only in an eligible build", () => { - assert.deepEqual( - filterBuildAvailableFeatures([publicFeature, bestie], { bestie: true }), - [publicFeature, bestie], - ); - }); -}); diff --git a/desktop/src/shared/features/buildAvailability.ts b/desktop/src/shared/features/buildAvailability.ts deleted file mode 100644 index 3c6b3bcb44..0000000000 --- a/desktop/src/shared/features/buildAvailability.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { FeatureBuildFlag, FeatureDefinition } from "./types"; - -export type FeatureBuildFlags = Record; - -/** - * Build capabilities are deliberately separate from user preference. A local - * storage override cannot make an unavailable feature visible. - */ -export function isFeatureBuildAvailable( - feature: FeatureDefinition, - buildFlags: FeatureBuildFlags, -): boolean { - const requiredFlag = feature.requiredBuildFlag; - return requiredFlag ? buildFlags[requiredFlag] : true; -} - -export function filterBuildAvailableFeatures( - features: FeatureDefinition[], - buildFlags: FeatureBuildFlags, -): FeatureDefinition[] { - return features.filter((feature) => - isFeatureBuildAvailable(feature, buildFlags), - ); -} diff --git a/desktop/src/shared/features/index.ts b/desktop/src/shared/features/index.ts index e53c1d1ad4..9f6db1b156 100644 --- a/desktop/src/shared/features/index.ts +++ b/desktop/src/shared/features/index.ts @@ -1,13 +1,8 @@ export { FeatureGate } from "./FeatureGate"; export { allFeatures, desktopFeatures, getFeature, manifest } from "./manifest"; -export { - availableDesktopFeatures, - resolveFeatureEnabledInThisBuild, -} from "./runtimeBuildAvailability"; export { getOverrides, setOverride, clearOverride } from "./store"; export type { FeatureDefinition, - FeatureBuildFlag, FeaturesManifest, FeaturePlatform, } from "./types"; @@ -16,4 +11,5 @@ export { useFeatureToggle, useFeatureSnapshot, usePreviewFeatureWarning, + resolveEnabled, } from "./useFeatureEnabled"; diff --git a/desktop/src/shared/features/manifest.ts b/desktop/src/shared/features/manifest.ts index 8ebdae5c06..423fbc3b36 100644 --- a/desktop/src/shared/features/manifest.ts +++ b/desktop/src/shared/features/manifest.ts @@ -1,4 +1,5 @@ import manifestJson from "@features-manifest"; +import { protectedFeatureDefinitions } from "@protected-features"; import { z } from "zod"; import type { FeatureDefinition, FeaturesManifest } from "./types"; @@ -8,7 +9,6 @@ import type { FeatureDefinition, FeaturesManifest } from "./types"; // The app keeps working; gated UI stays hidden; nothing accidentally leaks. const FeaturePlatformSchema = z.enum(["desktop", "mobile"]); -const FeatureBuildFlagSchema = z.enum(["bestie"]); const FeatureDefinitionSchema = z.object({ id: z.string().min(1), @@ -16,7 +16,6 @@ const FeatureDefinitionSchema = z.object({ description: z.string(), defaultEnabled: z.boolean().optional(), platforms: z.array(FeaturePlatformSchema).optional(), - requiredBuildFlag: FeatureBuildFlagSchema.optional(), }); const FeaturesManifestSchema = z.object({ @@ -27,7 +26,10 @@ const FeaturesManifestSchema = z.object({ const EMPTY_MANIFEST: FeaturesManifest = { version: 1, features: [] }; function loadManifest(): FeaturesManifest { - const result = FeaturesManifestSchema.safeParse(manifestJson); + const result = FeaturesManifestSchema.safeParse({ + ...manifestJson, + features: [...manifestJson.features, ...protectedFeatureDefinitions], + }); if (!result.success) { console.warn( "[FeatureFlags] preview-features.json failed schema validation; falling back to empty manifest.", diff --git a/desktop/src/shared/features/resolveEnabled.test.mjs b/desktop/src/shared/features/resolveEnabled.test.mjs index bf08878a78..d1639f6e4f 100644 --- a/desktop/src/shared/features/resolveEnabled.test.mjs +++ b/desktop/src/shared/features/resolveEnabled.test.mjs @@ -4,74 +4,30 @@ import { describe, it } from "node:test"; import { resolveEnabled } from "./resolveEnabled.ts"; describe("resolveEnabled (preview-only)", () => { - const workflows = { - id: "workflows", - name: "Workflows", - description: "", - }; - it("returns false by default (no override)", () => { - assert.equal(resolveEnabled(workflows, {}, true), false); + assert.equal(resolveEnabled("workflows", {}), false); }); it("returns true when user opts in", () => { - assert.equal(resolveEnabled(workflows, { workflows: true }, true), true); + assert.equal(resolveEnabled("workflows", { workflows: true }), true); }); it("uses an enabled manifest default when no override exists", () => { - assert.equal( - resolveEnabled( - { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, - {}, - true, - ), - true, - ); + assert.equal(resolveEnabled("defaultOnFeature", {}, true), true); }); it("lets an explicit opt-out override an enabled default", () => { assert.equal( - resolveEnabled( - { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, - { defaultOnFeature: false }, - true, - ), + resolveEnabled("defaultOnFeature", { defaultOnFeature: false }, true), false, ); }); it("returns false when user explicitly opts out", () => { - assert.equal(resolveEnabled(workflows, { workflows: false }, true), false); + assert.equal(resolveEnabled("workflows", { workflows: false }), false); }); it("ignores overrides for unrelated ids", () => { - assert.equal(resolveEnabled(workflows, { pulse: true }, true), false); - }); - - it("requires both build availability and a user opt-in", () => { - const cases = [ - { buildAvailable: false, optedIn: false, expected: false }, - { buildAvailable: false, optedIn: true, expected: false }, - { buildAvailable: true, optedIn: false, expected: false }, - { buildAvailable: true, optedIn: true, expected: true }, - ]; - - for (const { buildAvailable, optedIn, expected } of cases) { - assert.equal( - resolveEnabled( - { ...workflows, id: "bestie" }, - { bestie: optedIn }, - buildAvailable, - ), - expected, - ); - } - }); - - it("fails closed when a JavaScript caller omits build availability", () => { - assert.equal( - resolveEnabled({ ...workflows, id: "bestie" }, { bestie: true }), - false, - ); + assert.equal(resolveEnabled("workflows", { pulse: true }), false); }); }); diff --git a/desktop/src/shared/features/resolveEnabled.ts b/desktop/src/shared/features/resolveEnabled.ts index e89b06544e..e3c544ec50 100644 --- a/desktop/src/shared/features/resolveEnabled.ts +++ b/desktop/src/shared/features/resolveEnabled.ts @@ -1,26 +1,19 @@ /** * Pure resolution logic for preview-feature visibility. - * No side effects — safe to test in isolation. + * No side effects, no imports — safe to test in isolation. * * The manifest (`preview-features.json`) lists only preview features. * Anything not in the manifest is stable and resolves true elsewhere * (see `useFeatureEnabled`). Once you're inside `resolveEnabled`, the * feature IS in the manifest — preview by definition. * - * Build availability is mandatory. Within an eligible build, an explicit user - * override wins; otherwise the feature's manifest default is used (false when - * omitted). + * An explicit user override wins; otherwise the feature's manifest default is + * used (false when omitted). */ -import type { FeatureDefinition } from "./types"; - export function resolveEnabled( - feature: FeatureDefinition, + featureId: string, overrides: Record, - buildAvailable: boolean, + defaultEnabled = false, ): boolean { - // Strict equality is intentional: JavaScript callers that omit the - // capability argument fail closed instead of recreating a build-gate side - // door outside TypeScript. - if (buildAvailable !== true) return false; - return overrides[feature.id] ?? feature.defaultEnabled ?? false; + return overrides[featureId] ?? defaultEnabled; } diff --git a/desktop/src/shared/features/runtimeBuildAvailability.ts b/desktop/src/shared/features/runtimeBuildAvailability.ts deleted file mode 100644 index 76a8689115..0000000000 --- a/desktop/src/shared/features/runtimeBuildAvailability.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - filterBuildAvailableFeatures, - isFeatureBuildAvailable, - type FeatureBuildFlags, -} from "./buildAvailability"; -import { desktopFeatures } from "./manifest"; -import { resolveEnabled } from "./resolveEnabled"; -import type { FeatureDefinition } from "./types"; - -const runtimeBuildFlags: FeatureBuildFlags = { - bestie: import.meta.env.VITE_BUZZ_BESTIE === "1", -}; - -export function isFeatureAvailableInThisBuild( - feature: Parameters[0], -): boolean { - return isFeatureBuildAvailable(feature, runtimeBuildFlags); -} - -/** Canonical preview resolution for the running build. */ -export function resolveFeatureEnabledInThisBuild( - feature: FeatureDefinition, - overrides: Record, -): boolean { - return resolveEnabled( - feature, - overrides, - isFeatureAvailableInThisBuild(feature), - ); -} - -export const availableDesktopFeatures = filterBuildAvailableFeatures( - desktopFeatures, - runtimeBuildFlags, -); diff --git a/desktop/src/shared/features/types.ts b/desktop/src/shared/features/types.ts index 4fdf07f2af..c452cd54b4 100644 --- a/desktop/src/shared/features/types.ts +++ b/desktop/src/shared/features/types.ts @@ -1,9 +1,6 @@ /** Platforms a feature is available on */ export type FeaturePlatform = "desktop" | "mobile"; -/** Build-time capabilities that can make a preview available to users. */ -export type FeatureBuildFlag = "bestie"; - /** * A single feature definition from the manifest. * @@ -19,8 +16,6 @@ export interface FeatureDefinition { defaultEnabled?: boolean; /** If omitted, feature is available on all platforms */ platforms?: FeaturePlatform[]; - /** If present, the build must opt in before the user can see or enable it */ - requiredBuildFlag?: FeatureBuildFlag; } /** The root manifest schema */ diff --git a/desktop/src/shared/features/useFeatureEnabled.ts b/desktop/src/shared/features/useFeatureEnabled.ts index 0326fe51db..1be1e5e30e 100644 --- a/desktop/src/shared/features/useFeatureEnabled.ts +++ b/desktop/src/shared/features/useFeatureEnabled.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore, useCallback, useEffect } from "react"; import { getFeature } from "./manifest"; -import { resolveFeatureEnabledInThisBuild } from "./runtimeBuildAvailability"; +import { resolveEnabled } from "./resolveEnabled"; import { getOverrides, setOverride, OVERRIDES_KEY } from "./store"; type Listener = () => void; @@ -102,9 +102,11 @@ export function useFeatureEnabled(featureId: string): boolean { return true; } - return resolveFeatureEnabledInThisBuild(feature, overrides); + return resolveEnabled(featureId, overrides, feature.defaultEnabled); } +export { resolveEnabled } from "./resolveEnabled"; + /** * Hook to toggle a feature override. Returns [enabled, toggle]. */ diff --git a/desktop/test-loader-hooks.mjs b/desktop/test-loader-hooks.mjs index 06c44ae213..d473587adf 100644 --- a/desktop/test-loader-hooks.mjs +++ b/desktop/test-loader-hooks.mjs @@ -89,6 +89,12 @@ export function resolve(specifier, context, nextResolve) { const resolved = path.join(repoRoot, "preview-features.json"); return nextResolve(toFileSpecifier(resolved), context); } + if (specifier === "@protected-features") { + const variant = + process.env.VITE_BUZZ_BESTIE === "1" ? "internal.ts" : "public.ts"; + const resolved = path.join(srcRoot, "protectedFeatures", variant); + return nextResolve(toFileSpecifier(resolved), context); + } if (specifier === "@model-capabilities-manifest") { const resolved = path.join(repoRoot, "scripts", "model-capabilities.json"); return nextResolve(toFileSpecifier(resolved), context); diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index a2a57c66ef..feb7e7590f 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -8,6 +8,7 @@ "paths": { "@/*": ["./src/*"], "@features-manifest": ["../preview-features.json"], + "@protected-features": ["./src/protectedFeatures/public.ts"], "@model-capabilities-manifest": ["../scripts/model-capabilities.json"] }, diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index 5a5de19120..257c8382bb 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -1,56 +1,71 @@ import path from "node:path"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ -export default defineConfig(async () => ({ - plugins: [ - tanstackRouter({ - target: "react", - routesDirectory: "./src/app/routes", - generatedRouteTree: "./src/app/routeTree.gen.ts", - virtualRouteConfig: "./src/app/routes.ts", - quoteStyle: "double", - semicolons: true, - routeTreeFileHeader: [ - "// biome-ignore-all lint: generated by TanStack Router", - ], - }), - react(), - ], - resolve: { - alias: { - "@": "/src", - "@features-manifest": path.resolve(__dirname, "../preview-features.json"), - "@model-capabilities-manifest": path.resolve( - __dirname, - "../scripts/model-capabilities.json", - ), - }, - }, +export default defineConfig(async ({ mode }) => { + const modeEnv = loadEnv(mode, __dirname, ""); + const protectedFeaturesEnabled = + (process.env.VITE_BUZZ_BESTIE ?? modeEnv.VITE_BUZZ_BESTIE) === "1"; - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent Vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: parseInt(process.env.VITE_PORT || "1420", 10), - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: parseInt(process.env.VITE_HMR_PORT || "1421", 10), - } - : undefined, - watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], + return { + plugins: [ + tanstackRouter({ + target: "react", + routesDirectory: "./src/app/routes", + generatedRouteTree: "./src/app/routeTree.gen.ts", + virtualRouteConfig: "./src/app/routes.ts", + quoteStyle: "double", + semicolons: true, + routeTreeFileHeader: [ + "// biome-ignore-all lint: generated by TanStack Router", + ], + }), + react(), + ], + resolve: { + alias: { + "@": "/src", + "@features-manifest": path.resolve( + __dirname, + "../preview-features.json", + ), + "@protected-features": path.resolve( + __dirname, + protectedFeaturesEnabled + ? "./src/protectedFeatures/internal.ts" + : "./src/protectedFeatures/public.ts", + ), + "@model-capabilities-manifest": path.resolve( + __dirname, + "../scripts/model-capabilities.json", + ), + }, }, - }, -})); + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: parseInt(process.env.VITE_PORT || "1420", 10), + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: parseInt(process.env.VITE_HMR_PORT || "1421", 10), + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, + }; +}); diff --git a/preview-features.json b/preview-features.json index 79460c3cad..388f1c39b0 100644 --- a/preview-features.json +++ b/preview-features.json @@ -30,13 +30,6 @@ "name": "Agent-managed profiles", "description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy", "platforms": ["desktop"] - }, - { - "id": "bestie", - "name": "Bestie", - "description": "Try a personal agent that is always close at hand", - "platforms": ["desktop"], - "requiredBuildFlag": "bestie" } ] }