@agent-native/core 0.79.19 → 0.79.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/docs/content/deployment.md +133 -0
  4. package/corpus/core/docs/content/multi-app-workspace.md +2 -0
  5. package/corpus/core/package.json +2 -1
  6. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +5 -10
  7. package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +5 -0
  8. package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
  9. package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +5 -0
  10. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +20 -4
  11. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -2
  12. package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +5 -0
  13. package/corpus/templates/clips/changelog/2026-06-27-desktop-storage-setup-flow.md +6 -0
  14. package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +5 -0
  15. package/corpus/templates/clips/desktop/src/app.tsx +32 -3
  16. package/corpus/templates/clips/desktop/src/styles.css +67 -0
  17. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -1
  18. package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +5 -0
  19. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  20. package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +5 -0
  21. package/dist/collab/awareness.d.ts +2 -2
  22. package/dist/collab/awareness.d.ts.map +1 -1
  23. package/dist/observability/routes.d.ts +3 -3
  24. package/dist/progress/routes.d.ts +1 -1
  25. package/dist/resources/handlers.d.ts +2 -2
  26. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  27. package/dist/server/transcribe-voice.d.ts +1 -1
  28. package/docs/content/deployment.md +133 -0
  29. package/docs/content/multi-app-workspace.md +2 -0
  30. package/package.json +2 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2031
31
- - template files: 4416
31
+ - template files: 4423
@@ -1,5 +1,18 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.79.21
4
+
5
+ ### Patch Changes
6
+
7
+ - 3acd677: Clarify workspace Docker Compose deployment for self-hosted VPS installs.
8
+
9
+ ## 0.79.20
10
+
11
+ ### Patch Changes
12
+
13
+ - 8bcc21d: Declare the Sentry OpenTelemetry trace dependency directly so the published CLI installs reliably through npx.
14
+ - 8bcc21d: Use a compact popover for the shared language picker so icon-only chrome can style it reliably.
15
+
3
16
  ## 0.79.19
4
17
 
5
18
  ### Patch Changes
@@ -66,6 +66,139 @@ background processors. Local `--build-only` artifact checks still run without it
66
66
 
67
67
  Per-app independent deploy is still supported — just `cd apps/<name> && npx @agent-native/core@latest build` like a standalone scaffold.
68
68
 
69
+ ### Self-hosted workspace with Docker Compose {#workspace-docker-compose}
70
+
71
+ The Dockerfile above is for one standalone app. A workspace is not one Nitro
72
+ server process today: each app still builds to its own `.output/` directory and
73
+ runs its own Node/Nitro server. For a VPS, run one container per workspace app,
74
+ point every app at the same Postgres database and shared production secrets, and
75
+ put a reverse proxy in front that routes path prefixes (`/mail`, `/calendar`,
76
+ ...) to the matching app container.
77
+
78
+ Build each app with the same base path that the proxy will use at runtime. The
79
+ base path is a build-time input for the client bundle and a runtime input for
80
+ the server:
81
+
82
+ ```bash
83
+ APP_BASE_PATH=/mail VITE_APP_BASE_PATH=/mail npx @agent-native/core@latest build
84
+ node .output/server/index.mjs
85
+ ```
86
+
87
+ A reusable workspace Dockerfile can take the app name and path prefix as build
88
+ arguments:
89
+
90
+ ```dockerfile
91
+ FROM node:24-slim AS build
92
+ WORKDIR /workspace
93
+ RUN corepack enable
94
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
95
+ COPY packages ./packages
96
+ COPY apps ./apps
97
+ ARG APP_NAME
98
+ ARG APP_BASE_PATH
99
+ ENV APP_BASE_PATH=$APP_BASE_PATH
100
+ ENV VITE_APP_BASE_PATH=$APP_BASE_PATH
101
+ RUN pnpm install --frozen-lockfile
102
+ RUN pnpm --dir apps/$APP_NAME build
103
+
104
+ FROM node:24-slim
105
+ WORKDIR /app
106
+ ARG APP_NAME
107
+ ARG APP_BASE_PATH
108
+ ENV NODE_ENV=production
109
+ ENV PORT=3000
110
+ ENV APP_BASE_PATH=$APP_BASE_PATH
111
+ COPY --from=build /workspace/apps/$APP_NAME/.output .output
112
+ EXPOSE 3000
113
+ CMD ["node", ".output/server/index.mjs"]
114
+ ```
115
+
116
+ Then compose Postgres, the app containers, and your proxy. This example uses
117
+ Caddy because it can terminate HTTP locally or sit behind Cloudflare's SSL
118
+ proxy, but the important part is the path-prefix routing:
119
+
120
+ ```yaml
121
+ services:
122
+ postgres:
123
+ image: postgres:17
124
+ restart: unless-stopped
125
+ environment:
126
+ POSTGRES_DB: agent_native
127
+ POSTGRES_USER: agent_native
128
+ POSTGRES_PASSWORD: change-me
129
+ volumes:
130
+ - postgres-data:/var/lib/postgresql/data
131
+
132
+ mail:
133
+ build:
134
+ context: .
135
+ dockerfile: Dockerfile.workspace-app
136
+ args:
137
+ APP_NAME: mail
138
+ APP_BASE_PATH: /mail
139
+ restart: unless-stopped
140
+ environment:
141
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
142
+ BETTER_AUTH_URL: https://agents.example.com/mail
143
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
144
+ A2A_SECRET: ${A2A_SECRET}
145
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
146
+ depends_on:
147
+ - postgres
148
+
149
+ calendar:
150
+ build:
151
+ context: .
152
+ dockerfile: Dockerfile.workspace-app
153
+ args:
154
+ APP_NAME: calendar
155
+ APP_BASE_PATH: /calendar
156
+ restart: unless-stopped
157
+ environment:
158
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
159
+ BETTER_AUTH_URL: https://agents.example.com/calendar
160
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
161
+ A2A_SECRET: ${A2A_SECRET}
162
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
163
+ depends_on:
164
+ - postgres
165
+
166
+ proxy:
167
+ image: caddy:2
168
+ restart: unless-stopped
169
+ ports:
170
+ - "80:80"
171
+ - "443:443"
172
+ volumes:
173
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
174
+ depends_on:
175
+ - mail
176
+ - calendar
177
+
178
+ volumes:
179
+ postgres-data:
180
+ ```
181
+
182
+ ```caddyfile
183
+ agents.example.com {
184
+ reverse_proxy /mail* mail:3000
185
+ reverse_proxy /calendar* calendar:3000
186
+ }
187
+ ```
188
+
189
+ Notes for this shape:
190
+
191
+ - Keep `DATABASE_URL`, `BETTER_AUTH_SECRET`, `A2A_SECRET`, and other production
192
+ secrets identical across app containers when you want shared auth, shared
193
+ credentials, and cross-app A2A.
194
+ - Set `BETTER_AUTH_URL` to the public URL for that app path. OAuth callback URLs
195
+ should use the same public URL.
196
+ - Do not rely on the container filesystem for production data; use Postgres (or
197
+ another persistent SQL backend) for `DATABASE_URL`.
198
+ - If Cloudflare terminates TLS in front of the VPS, configure Cloudflare to send
199
+ normal `X-Forwarded-Proto` / `X-Forwarded-Host` headers and route the public
200
+ host to the proxy container.
201
+
69
202
  ## How It Works {#how-it-works}
70
203
 
71
204
  When you run `npx @agent-native/core@latest build`, Nitro builds both the client SPA and the server API into `.output/`:
@@ -289,6 +289,8 @@ These settings only affect read-only page navigation. Framework tools, agent cha
289
289
 
290
290
  Prefer each app on its own domain (`mail.company.com`, `calendar.company.com`)? Every app in the workspace is still an independent deployable — `cd apps/mail && npx @agent-native/core@latest build` behaves exactly like a standalone scaffold. Cross-app A2A then goes through the standard JWT-signed path with a shared `A2A_SECRET`. Cross-domain SSO between separately-deployed apps is handled by identity federation with Dispatch as the hub — see [Cross-App SSO](/docs/cross-app-sso); the unified single-origin deploy avoids needing it.
291
291
 
292
+ For a self-hosted VPS or Docker Compose setup on one origin, build and run one container per app with its own path prefix (`APP_BASE_PATH=/mail`, `APP_BASE_PATH=/calendar`, ...), then route those prefixes through a reverse proxy. See [Deployment — Self-hosted workspace with Docker Compose](/docs/deployment#workspace-docker-compose) for the Dockerfile and compose example.
293
+
292
294
  ### Shared database, shared credentials
293
295
 
294
296
  Whatever you pick, point every app at the same `DATABASE_URL` for cross-app state out of the box: one set of user accounts, one set of organizations, one set of shared settings. If each app has its own database, the workspace pattern still works — you just lose that shared-state story.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.79.19",
3
+ "version": "0.79.21",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -204,6 +204,7 @@
204
204
  "@modelcontextprotocol/sdk": "^1.29.0",
205
205
  "@mozilla/readability": "0.6.0",
206
206
  "@neondatabase/serverless": "^1.1.0",
207
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
207
208
  "@radix-ui/react-dialog": "1.1.15",
208
209
  "@radix-ui/react-dropdown-menu": "^2.1.16",
209
210
  "@radix-ui/react-hover-card": "^1.1.15",
@@ -1,5 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
- import { IconMenu, IconChartBar } from "@tabler/icons-react";
2
+ import { IconMenu } from "@tabler/icons-react";
3
3
  import { useState, useEffect } from "react";
4
4
  import { useLocation } from "react-router";
5
5
 
@@ -18,7 +18,7 @@ export function MobileNav() {
18
18
  }, [location.pathname]);
19
19
 
20
20
  return (
21
- <div className="flex h-12 shrink-0 items-center border-b border-border bg-sidebar px-4 md:hidden">
21
+ <div className="flex h-12 shrink-0 items-center border-b border-border bg-background px-4 md:hidden">
22
22
  <button
23
23
  onClick={() => setOpen(true)}
24
24
  className="me-3 p-2.5 -ms-1 rounded-md hover:bg-sidebar-accent/50"
@@ -26,14 +26,9 @@ export function MobileNav() {
26
26
  >
27
27
  <IconMenu className="h-5 w-5 text-foreground" />
28
28
  </button>
29
- <div className="flex items-center gap-2">
30
- <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
31
- <IconChartBar className="h-4 w-4" />
32
- </div>
33
- <span className="text-base font-bold tracking-tight">
34
- {t("navigation.brand")}
35
- </span>
36
- </div>
29
+ <span className="text-base font-bold tracking-tight">
30
+ {t("navigation.brand")}
31
+ </span>
37
32
 
38
33
  <Sheet open={open} onOpenChange={setOpen}>
39
34
  <SheetContent side="left" className="p-0 w-[280px]">
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+ Improved mobile navigation chrome and sidebar drawer motion.
@@ -87,7 +87,7 @@ export function Layout({ children }: LayoutProps) {
87
87
  )}
88
88
  <div
89
89
  className={cn(
90
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 md:static md:z-auto",
90
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
91
91
  mobileSidebarOpen
92
92
  ? "translate-x-0"
93
93
  : "-translate-x-full md:translate-x-0",
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+ Improved mobile navigation chrome and sidebar drawer motion.
@@ -41,6 +41,7 @@ import {
41
41
  SelectValue,
42
42
  } from "@/components/ui/select";
43
43
  import { Switch } from "@/components/ui/switch";
44
+ import { useIsMobile } from "@/hooks/use-mobile";
44
45
  import {
45
46
  loadRecorderPreferences,
46
47
  saveRecorderPreferences,
@@ -192,6 +193,7 @@ export function PreRecordPanel({
192
193
  error: null,
193
194
  hasPreview: false,
194
195
  });
196
+ const isMobile = useIsMobile();
195
197
 
196
198
  const modeOptions = useMemo<ModeOption[]>(
197
199
  () => [
@@ -213,6 +215,14 @@ export function PreRecordPanel({
213
215
  ],
214
216
  [t],
215
217
  );
218
+ const visibleModeOptions = useMemo(
219
+ () =>
220
+ isMobile
221
+ ? modeOptions.filter((option) => option.value === "camera")
222
+ : modeOptions,
223
+ [isMobile, modeOptions],
224
+ );
225
+
216
226
  const surfaceOptions = useMemo<SurfaceOption[]>(
217
227
  () => [
218
228
  {
@@ -238,8 +248,14 @@ export function PreRecordPanel({
238
248
  );
239
249
 
240
250
  useEffect(() => {
251
+ if (isMobile) {
252
+ // Mobile browsers do not support the screen-capture choices this panel
253
+ // offers on desktop, so keep the setup focused on recording face video.
254
+ setMode("camera");
255
+ return;
256
+ }
241
257
  if (initialMode) setMode(initialMode);
242
- }, [initialMode]);
258
+ }, [initialMode, isMobile]);
243
259
 
244
260
  useEffect(() => {
245
261
  if (initialDisplaySurface) setDisplaySurface(initialDisplaySurface);
@@ -586,7 +602,7 @@ export function PreRecordPanel({
586
602
  <div className="mx-auto w-full max-w-lg overflow-hidden rounded-2xl border border-border bg-card shadow-lg">
587
603
  <div className="p-6">
588
604
  <div className="grid gap-2 sm:grid-cols-3">
589
- {modeOptions.map((opt) => {
605
+ {visibleModeOptions.map((opt) => {
590
606
  const Icon = opt.icon;
591
607
  const active = opt.value === mode;
592
608
  return (
@@ -600,7 +616,7 @@ export function PreRecordPanel({
600
616
  className={cn(
601
617
  "flex min-h-20 min-w-0 flex-col justify-between rounded-xl border p-3 text-start transition-colors",
602
618
  active
603
- ? "border-primary bg-primary text-primary-foreground shadow-sm"
619
+ ? "border-primary/55 bg-primary/[0.12] text-foreground shadow-sm ring-1 ring-primary/15"
604
620
  : "border-border bg-background text-foreground hover:border-foreground/30 hover:bg-muted/45",
605
621
  )}
606
622
  aria-pressed={active}
@@ -609,7 +625,7 @@ export function PreRecordPanel({
609
625
  className={cn(
610
626
  "mb-3 flex h-9 w-9 items-center justify-center rounded-full",
611
627
  active
612
- ? "bg-primary-foreground/15 text-primary-foreground"
628
+ ? "bg-primary/[0.18] text-foreground"
613
629
  : "bg-muted text-muted-foreground",
614
630
  )}
615
631
  >
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
+ import { useIsMobile } from "@/hooks/use-mobile";
4
+
3
5
  const DISMISSED_KEY = "clips.desktop-promo.dismissed";
4
6
 
5
7
  function detectDesktopApp(): boolean {
@@ -17,6 +19,7 @@ function detectDesktopApp(): boolean {
17
19
  }
18
20
 
19
21
  export function useDesktopPromo() {
22
+ const isMobile = useIsMobile();
20
23
  const [isDesktopApp, setIsDesktopApp] = useState(false);
21
24
  const [dismissed, setDismissed] = useState(false);
22
25
 
@@ -40,8 +43,9 @@ export function useDesktopPromo() {
40
43
 
41
44
  return {
42
45
  isDesktopApp,
43
- shouldShowPromo: !isDesktopApp && !dismissed,
44
- shouldShowSidebarLink: !isDesktopApp,
46
+ isMobile,
47
+ shouldShowPromo: !isMobile && !isDesktopApp && !dismissed,
48
+ shouldShowSidebarLink: !isMobile && !isDesktopApp,
45
49
  dismiss,
46
50
  };
47
51
  }
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-27
4
+ ---
5
+ Clips no longer shows desktop app prompts on mobile screens.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+
6
+ Desktop recording now guides you to connect storage with a friendly setup flow instead of a red error.
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+ Recorder setup keeps screen-capture choices desktop-only on mobile and tones down the selected mode highlight.
@@ -1687,9 +1687,7 @@ export function App() {
1687
1687
  const openVideoStorageSetup = useCallback(
1688
1688
  (targetServerUrl?: string) => {
1689
1689
  const base = (targetServerUrl?.trim() || serverUrl).replace(/\/+$/, "");
1690
- setRecError(
1691
- "Connect storage before recording from desktop: Builder.io (free tier storage + AI) or S3-compatible storage. Opening storage setup...",
1692
- );
1690
+ setRecError(STORAGE_SETUP_HELP_TEXT);
1693
1691
  void openExternal(`${base}/record`).catch((err) => {
1694
1692
  setRecError(
1695
1693
  err instanceof Error
@@ -1924,6 +1922,11 @@ export function App() {
1924
1922
  setRecError(MACOS_CAPTURE_PERMISSION_MESSAGE);
1925
1923
  return;
1926
1924
  }
1925
+ if (isStorageSetupFailureMessage(message)) {
1926
+ setRecError(STORAGE_SETUP_HELP_TEXT);
1927
+ openVideoStorageSetup();
1928
+ return;
1929
+ }
1927
1930
  setRecError(message);
1928
1931
  }
1929
1932
 
@@ -2306,6 +2309,8 @@ export function App() {
2306
2309
  panes={["speech", "microphone"]}
2307
2310
  onRetry={handleStartRecording}
2308
2311
  />
2312
+ ) : isStorageSetupFailureMessage(recError) ? (
2313
+ <StorageConnectionBanner onConnect={() => openVideoStorageSetup()} />
2309
2314
  ) : (
2310
2315
  <div className="error-banner">{recError}</div>
2311
2316
  )
@@ -2450,6 +2455,30 @@ function PermissionRecoveryBanner({
2450
2455
  );
2451
2456
  }
2452
2457
 
2458
+ function StorageConnectionBanner({ onConnect }: { onConnect: () => void }) {
2459
+ return (
2460
+ <div className="storage-flow-banner">
2461
+ <div className="storage-flow-icon" aria-hidden>
2462
+ <IconUpload size={17} stroke={1.8} />
2463
+ </div>
2464
+ <div className="storage-flow-copy">
2465
+ <div className="storage-flow-title">
2466
+ Connect storage to keep recording
2467
+ </div>
2468
+ <div className="storage-flow-sub">{STORAGE_SETUP_HELP_TEXT}</div>
2469
+ </div>
2470
+ <button
2471
+ type="button"
2472
+ className="storage-flow-connect"
2473
+ onClick={onConnect}
2474
+ >
2475
+ <IconExternalLink size={14} stroke={2} />
2476
+ Connect
2477
+ </button>
2478
+ </div>
2479
+ );
2480
+ }
2481
+
2453
2482
  function PendingUploadBanner({
2454
2483
  uploads,
2455
2484
  retryingUploadId,
@@ -1680,6 +1680,73 @@ body[data-clips-route="recording-pill"] #root {
1680
1680
  background: var(--surface-hover);
1681
1681
  }
1682
1682
 
1683
+ .storage-flow-banner {
1684
+ display: grid;
1685
+ grid-template-columns: auto minmax(0, 1fr);
1686
+ align-items: start;
1687
+ gap: 10px;
1688
+ padding: 10px;
1689
+ border: 1px solid #bfdbfe;
1690
+ border-radius: var(--radius);
1691
+ background: #eff6ff;
1692
+ color: #1e3a8a;
1693
+ }
1694
+
1695
+ .storage-flow-icon {
1696
+ width: 28px;
1697
+ height: 28px;
1698
+ display: inline-flex;
1699
+ align-items: center;
1700
+ justify-content: center;
1701
+ border-radius: 8px;
1702
+ background: rgba(59, 130, 246, 0.12);
1703
+ color: #1d4ed8;
1704
+ flex-shrink: 0;
1705
+ }
1706
+
1707
+ .storage-flow-copy {
1708
+ min-width: 0;
1709
+ }
1710
+
1711
+ .storage-flow-title {
1712
+ font-size: 12px;
1713
+ font-weight: 700;
1714
+ line-height: 1.25;
1715
+ }
1716
+
1717
+ .storage-flow-sub {
1718
+ margin-top: 3px;
1719
+ color: #3b5f9a;
1720
+ font-size: 11px;
1721
+ line-height: 1.35;
1722
+ }
1723
+
1724
+ .storage-flow-connect {
1725
+ grid-column: 2;
1726
+ justify-self: start;
1727
+ height: 28px;
1728
+ border: 1px solid #bfdbfe;
1729
+ border-radius: var(--radius-sm);
1730
+ background: white;
1731
+ color: #1d4ed8;
1732
+ cursor: pointer;
1733
+ display: inline-flex;
1734
+ align-items: center;
1735
+ justify-content: center;
1736
+ gap: 5px;
1737
+ padding: 0 9px;
1738
+ font-size: 11px;
1739
+ font-weight: 700;
1740
+ transition:
1741
+ background 120ms,
1742
+ border-color 120ms;
1743
+ }
1744
+
1745
+ .storage-flow-connect:hover {
1746
+ background: #dbeafe;
1747
+ border-color: #93c5fd;
1748
+ }
1749
+
1683
1750
  .pending-upload-banner {
1684
1751
  display: grid;
1685
1752
  grid-template-columns: auto minmax(0, 1fr) auto;
@@ -120,7 +120,7 @@ export function Layout({ children }: LayoutProps) {
120
120
  )}
121
121
  <div
122
122
  className={cn(
123
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 md:static md:z-auto",
123
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
124
124
  mobileSidebarOpen
125
125
  ? "translate-x-0"
126
126
  : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+ Improved mobile navigation chrome and sidebar drawer motion.
@@ -87,7 +87,7 @@ export function Layout({ children }: LayoutProps) {
87
87
  )}
88
88
  <div
89
89
  className={cn(
90
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 md:static md:z-auto",
90
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
91
91
  sidebarOpen
92
92
  ? "translate-x-0"
93
93
  : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+ Improved mobile navigation chrome and sidebar drawer motion.
@@ -49,11 +49,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
49
49
  error: string;
50
50
  states?: undefined;
51
51
  } | {
52
+ error?: undefined;
52
53
  states: {
53
54
  clientId: number;
54
55
  state: string;
55
56
  }[];
56
- error?: undefined;
57
57
  }>>;
58
58
  /**
59
59
  * GET /_agent-native/collab/:docId/users
@@ -64,10 +64,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
64
64
  error: string;
65
65
  users?: undefined;
66
66
  } | {
67
+ error?: undefined;
67
68
  users: {
68
69
  clientId: number;
69
70
  lastSeen: number;
70
71
  }[];
71
- error?: undefined;
72
72
  }>>;
73
73
  //# sourceMappingURL=awareness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;kBAuCa,MAAM;eAAS,MAAM;;;GAa1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;kBAWM,MAAM;kBAAY,MAAM;;;GAMvD,CAAC"}
1
+ {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;kBAuCa,MAAM;eAAS,MAAM;;GAa1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;kBAWM,MAAM;kBAAY,MAAM;;GAMvD,CAAC"}
@@ -41,22 +41,22 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ ok?: undefined;
44
45
  summary: import("./types.js").TraceSummary;
45
46
  spans: import("./types.js").TraceSpan[];
46
- ok?: undefined;
47
47
  error?: undefined;
48
48
  id?: undefined;
49
49
  } | {
50
+ ok?: undefined;
50
51
  summary?: undefined;
51
52
  spans?: undefined;
52
53
  id: string;
53
- ok?: undefined;
54
54
  error?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  error: any;
59
- ok?: undefined;
60
60
  id?: undefined;
61
61
  } | {
62
62
  summary?: undefined;
@@ -15,7 +15,7 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
21
21
  //# sourceMappingURL=routes.d.ts.map
@@ -52,8 +52,8 @@ export declare function handleDeleteResource(event: any): Promise<{
52
52
  error: string;
53
53
  ok?: undefined;
54
54
  } | {
55
- ok: boolean;
56
55
  error?: undefined;
56
+ ok: boolean;
57
57
  }>;
58
58
  /** POST /_agent-native/resources/upload — upload a file as a resource */
59
59
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,9 +73,9 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
+ error?: undefined;
76
77
  url: string;
77
78
  provider: string;
78
- error?: undefined;
79
79
  }>;
80
80
  export {};
81
81
  //# sourceMappingURL=handlers.d.ts.map
@@ -23,10 +23,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
23
23
  error: string;
24
24
  }>;
25
25
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
26
+ ok?: undefined;
26
27
  error: any;
27
28
  key?: undefined;
28
29
  scope?: undefined;
29
- ok?: undefined;
30
30
  } | {
31
31
  ok: boolean;
32
32
  key: string;
@@ -20,7 +20,7 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
26
26
  //# sourceMappingURL=transcribe-voice.d.ts.map
@@ -66,6 +66,139 @@ background processors. Local `--build-only` artifact checks still run without it
66
66
 
67
67
  Per-app independent deploy is still supported — just `cd apps/<name> && npx @agent-native/core@latest build` like a standalone scaffold.
68
68
 
69
+ ### Self-hosted workspace with Docker Compose {#workspace-docker-compose}
70
+
71
+ The Dockerfile above is for one standalone app. A workspace is not one Nitro
72
+ server process today: each app still builds to its own `.output/` directory and
73
+ runs its own Node/Nitro server. For a VPS, run one container per workspace app,
74
+ point every app at the same Postgres database and shared production secrets, and
75
+ put a reverse proxy in front that routes path prefixes (`/mail`, `/calendar`,
76
+ ...) to the matching app container.
77
+
78
+ Build each app with the same base path that the proxy will use at runtime. The
79
+ base path is a build-time input for the client bundle and a runtime input for
80
+ the server:
81
+
82
+ ```bash
83
+ APP_BASE_PATH=/mail VITE_APP_BASE_PATH=/mail npx @agent-native/core@latest build
84
+ node .output/server/index.mjs
85
+ ```
86
+
87
+ A reusable workspace Dockerfile can take the app name and path prefix as build
88
+ arguments:
89
+
90
+ ```dockerfile
91
+ FROM node:24-slim AS build
92
+ WORKDIR /workspace
93
+ RUN corepack enable
94
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
95
+ COPY packages ./packages
96
+ COPY apps ./apps
97
+ ARG APP_NAME
98
+ ARG APP_BASE_PATH
99
+ ENV APP_BASE_PATH=$APP_BASE_PATH
100
+ ENV VITE_APP_BASE_PATH=$APP_BASE_PATH
101
+ RUN pnpm install --frozen-lockfile
102
+ RUN pnpm --dir apps/$APP_NAME build
103
+
104
+ FROM node:24-slim
105
+ WORKDIR /app
106
+ ARG APP_NAME
107
+ ARG APP_BASE_PATH
108
+ ENV NODE_ENV=production
109
+ ENV PORT=3000
110
+ ENV APP_BASE_PATH=$APP_BASE_PATH
111
+ COPY --from=build /workspace/apps/$APP_NAME/.output .output
112
+ EXPOSE 3000
113
+ CMD ["node", ".output/server/index.mjs"]
114
+ ```
115
+
116
+ Then compose Postgres, the app containers, and your proxy. This example uses
117
+ Caddy because it can terminate HTTP locally or sit behind Cloudflare's SSL
118
+ proxy, but the important part is the path-prefix routing:
119
+
120
+ ```yaml
121
+ services:
122
+ postgres:
123
+ image: postgres:17
124
+ restart: unless-stopped
125
+ environment:
126
+ POSTGRES_DB: agent_native
127
+ POSTGRES_USER: agent_native
128
+ POSTGRES_PASSWORD: change-me
129
+ volumes:
130
+ - postgres-data:/var/lib/postgresql/data
131
+
132
+ mail:
133
+ build:
134
+ context: .
135
+ dockerfile: Dockerfile.workspace-app
136
+ args:
137
+ APP_NAME: mail
138
+ APP_BASE_PATH: /mail
139
+ restart: unless-stopped
140
+ environment:
141
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
142
+ BETTER_AUTH_URL: https://agents.example.com/mail
143
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
144
+ A2A_SECRET: ${A2A_SECRET}
145
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
146
+ depends_on:
147
+ - postgres
148
+
149
+ calendar:
150
+ build:
151
+ context: .
152
+ dockerfile: Dockerfile.workspace-app
153
+ args:
154
+ APP_NAME: calendar
155
+ APP_BASE_PATH: /calendar
156
+ restart: unless-stopped
157
+ environment:
158
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
159
+ BETTER_AUTH_URL: https://agents.example.com/calendar
160
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
161
+ A2A_SECRET: ${A2A_SECRET}
162
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
163
+ depends_on:
164
+ - postgres
165
+
166
+ proxy:
167
+ image: caddy:2
168
+ restart: unless-stopped
169
+ ports:
170
+ - "80:80"
171
+ - "443:443"
172
+ volumes:
173
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
174
+ depends_on:
175
+ - mail
176
+ - calendar
177
+
178
+ volumes:
179
+ postgres-data:
180
+ ```
181
+
182
+ ```caddyfile
183
+ agents.example.com {
184
+ reverse_proxy /mail* mail:3000
185
+ reverse_proxy /calendar* calendar:3000
186
+ }
187
+ ```
188
+
189
+ Notes for this shape:
190
+
191
+ - Keep `DATABASE_URL`, `BETTER_AUTH_SECRET`, `A2A_SECRET`, and other production
192
+ secrets identical across app containers when you want shared auth, shared
193
+ credentials, and cross-app A2A.
194
+ - Set `BETTER_AUTH_URL` to the public URL for that app path. OAuth callback URLs
195
+ should use the same public URL.
196
+ - Do not rely on the container filesystem for production data; use Postgres (or
197
+ another persistent SQL backend) for `DATABASE_URL`.
198
+ - If Cloudflare terminates TLS in front of the VPS, configure Cloudflare to send
199
+ normal `X-Forwarded-Proto` / `X-Forwarded-Host` headers and route the public
200
+ host to the proxy container.
201
+
69
202
  ## How It Works {#how-it-works}
70
203
 
71
204
  When you run `npx @agent-native/core@latest build`, Nitro builds both the client SPA and the server API into `.output/`:
@@ -289,6 +289,8 @@ These settings only affect read-only page navigation. Framework tools, agent cha
289
289
 
290
290
  Prefer each app on its own domain (`mail.company.com`, `calendar.company.com`)? Every app in the workspace is still an independent deployable — `cd apps/mail && npx @agent-native/core@latest build` behaves exactly like a standalone scaffold. Cross-app A2A then goes through the standard JWT-signed path with a shared `A2A_SECRET`. Cross-domain SSO between separately-deployed apps is handled by identity federation with Dispatch as the hub — see [Cross-App SSO](/docs/cross-app-sso); the unified single-origin deploy avoids needing it.
291
291
 
292
+ For a self-hosted VPS or Docker Compose setup on one origin, build and run one container per app with its own path prefix (`APP_BASE_PATH=/mail`, `APP_BASE_PATH=/calendar`, ...), then route those prefixes through a reverse proxy. See [Deployment — Self-hosted workspace with Docker Compose](/docs/deployment#workspace-docker-compose) for the Dockerfile and compose example.
293
+
292
294
  ### Shared database, shared credentials
293
295
 
294
296
  Whatever you pick, point every app at the same `DATABASE_URL` for cross-app state out of the box: one set of user accounts, one set of organizations, one set of shared settings. If each app has its own database, the workspace pattern still works — you just lose that shared-state story.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.79.19",
3
+ "version": "0.79.21",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -196,6 +196,7 @@
196
196
  "@modelcontextprotocol/sdk": "^1.29.0",
197
197
  "@mozilla/readability": "0.6.0",
198
198
  "@neondatabase/serverless": "^1.1.0",
199
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
199
200
  "@radix-ui/react-dialog": "1.1.15",
200
201
  "@radix-ui/react-dropdown-menu": "^2.1.16",
201
202
  "@radix-ui/react-hover-card": "^1.1.15",