Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
abbd4e0
fix(react): harden managed head scripts
kojiwakayama Jul 28, 2026
90ab87c
fix(react): escape SSR document metadata
kojiwakayama Jul 28, 2026
1e8972a
fix(react): stabilize color mode and head collection
kojiwakayama Jul 28, 2026
c4a17a6
fix(react): preserve SSR lifecycle contracts
kojiwakayama Jul 28, 2026
7d0bea2
fix(react): bound buffered SSR streams
kojiwakayama Jul 28, 2026
9d0a798
fix(react): fail closed after SSR stream failures
kojiwakayama Aug 1, 2026
e9f708b
fix(react): reject unsafe managed-head properties
kojiwakayama Aug 1, 2026
7df7232
build(react): refresh framework style candidates
kojiwakayama Aug 1, 2026
e0c0331
fix(react): report SSR failures exactly once
kojiwakayama Aug 1, 2026
71fa4f9
fix(react): bound SSR setup and buffering
kojiwakayama Aug 1, 2026
2561f62
build(react): refresh SSR runtime bundles
kojiwakayama Aug 1, 2026
999faac
fix(react): unify managed head ownership
kojiwakayama Jul 28, 2026
66ba5e1
fix(build): generate client styles from source
kojiwakayama Aug 2, 2026
8c4cb8c
test(react): assert shell head provenance
kojiwakayama Aug 2, 2026
c09b9eb
checkpoint(react): harden SSR and head lifecycles
kojiwakayama Aug 2, 2026
440e9ac
fix(react): make route head handoff commit-aware
kojiwakayama Aug 2, 2026
2b36a9b
build(react): refresh route lifecycle bundles
kojiwakayama Aug 2, 2026
1335adf
fix(react): preserve third-party route metadata
kojiwakayama Aug 2, 2026
3d4ab51
fix(routing): retain title across metadata handoff
kojiwakayama Aug 2, 2026
09df6de
fix(html): harden route head snapshot fallback
kojiwakayama Aug 2, 2026
7ee7667
test(client): model document title ownership
kojiwakayama Aug 2, 2026
5062077
fix(react): close chat and client lifecycle races (#3219)
kojiwakayama Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions cli/templates/files/docs-agent/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@

import { Chat } from 'veryfront/chat'

const ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'

export default function DocsChat() {
return (
<Chat
agentId="rag"
api="/api/ag-ui"
uploadApi="/api/uploads"
attachAccept={ACCEPT}
className="flex-1 min-h-0"
placeholder="Ask anything about your documents..."
/>
Expand Down
27 changes: 19 additions & 8 deletions cli/templates/files/docs-agent/app/uploads/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ const ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.

export default function UploadsPage(): React.JSX.Element {
const uploads = useUploadsRegistry({ url: UPLOAD_API, storageKey: 'rag-uploads' })
const error = uploads.uploadError
?? uploads.refreshError
?? uploads.removeError
?? uploads.storageError

return (
<AttachmentsPanel
uploads={uploads.items}
loading={uploads.isLoading}
onAttach={uploads.upload}
onRemoveUpload={uploads.remove}
attachAccept={ACCEPT}
className="flex-1 min-h-0"
/>
<>
{error && (
<p role="alert" className="m-4 rounded-md bg-red-50 p-3 text-sm text-red-800">
{error.message}
</p>
)}
<AttachmentsPanel
uploads={uploads.items}
loading={uploads.isLoading}
onAttach={uploads.upload}
onRemoveUpload={uploads.remove}
attachAccept={ACCEPT}
className="flex-1 min-h-0"
/>
</>
)
}
10 changes: 9 additions & 1 deletion cli/templates/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,17 @@ describe("cli/templates", () => {

assertEquals(page.includes("useChat"), false);
assertEquals(page.includes('agentId="rag"'), true);
assertEquals(page.includes('uploadApi="/api/uploads"'), true);
assertEquals(
page.includes("uploadApi"),
false,
"the RAG ingestion route does not return a runtime-fetchable Chat attachment URL",
);
assertEquals(uploadsPage.includes("AttachmentsPanel"), true);
assertEquals(uploadsPage.includes("useUploadsRegistry"), true);
assertEquals(uploadsPage.includes('role="alert"'), true);
assertEquals(uploadsPage.includes("refreshError"), true);
assertEquals(uploadsPage.includes("removeError"), true);
assertEquals(uploadsPage.includes("storageError"), true);
assertEquals(agent.includes("suggestions:"), true);
});

Expand Down
4 changes: 2 additions & 2 deletions cli/templates/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"app/api/ingest/route.ts": "import { store } from \"../../../store.ts\";\n\nexport async function POST() {\n await store.indexContentDir();\n return Response.json({ ok: true });\n}\n",
"app/api/uploads/route.ts": "import { createUploadHandler } from \"veryfront/embedding\";\nimport { store } from \"../../../store.ts\";\n\nexport const { POST, GET, DELETE } = createUploadHandler(store, {\n auth: { type: \"none\", allowUnauthenticated: true },\n});\n",
"app/layout.tsx": "'use client'\n\nimport \"../globals.css\";\nimport {\n AppShell,\n ChatSidebar,\n ChatThemeScope,\n ConversationsProvider,\n Tabs,\n TabsItem,\n} from \"veryfront/chat\";\nimport { Head } from \"veryfront/head\";\nimport { useRouter } from \"veryfront/router\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }): React.ReactNode {\n const router = useRouter();\n const activeTab = router.pathname.startsWith(\"/uploads\") ? \"uploads\" : \"chat\";\n\n return (\n <>\n <Head>\n <title>Docs Agent</title>\n <link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\" />\n </Head>\n <ChatThemeScope className=\"flex flex-col h-screen\">\n <ConversationsProvider storageKey=\"rag-conversations\">\n <AppShell className=\"flex-1 min-h-0\">\n <AppShell.Sidebar side=\"left\" className=\"border-r border-[var(--outline-border)]\">\n <AppShell.SidebarContent className=\"p-0\">\n <ChatSidebar.Root>\n <ChatSidebar.NewButton />\n <ChatSidebar.List />\n </ChatSidebar.Root>\n </AppShell.SidebarContent>\n </AppShell.Sidebar>\n <AppShell.Main>\n <AppShell.Header border className=\"h-16 gap-3 px-3\">\n <AppShell.Trigger side=\"left\" />\n <div className=\"flex flex-1 justify-center\">\n <Tabs\n value={activeTab}\n onValueChange={(value: string) =>\n router.push(value === \"uploads\" ? \"/uploads\" : \"/\")}\n >\n <TabsItem value=\"chat\">Chat</TabsItem>\n <TabsItem value=\"uploads\">Uploads</TabsItem>\n </Tabs>\n </div>\n </AppShell.Header>\n <AppShell.Content className=\"flex flex-col min-h-0 pt-3\">\n {children}\n </AppShell.Content>\n </AppShell.Main>\n </AppShell>\n </ConversationsProvider>\n </ChatThemeScope>\n </>\n );\n}\n",
"app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\n\nconst ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'\n\nexport default function DocsChat() {\n return (\n <Chat\n agentId=\"rag\"\n api=\"/api/ag-ui\"\n uploadApi=\"/api/uploads\"\n attachAccept={ACCEPT}\n className=\"flex-1 min-h-0\"\n placeholder=\"Ask anything about your documents...\"\n />\n )\n}\n",
"app/uploads/page.tsx": "'use client'\n\nimport { AttachmentsPanel, useUploadsRegistry } from 'veryfront/chat'\n\nconst UPLOAD_API = '/api/uploads'\nconst ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'\n\nexport default function UploadsPage(): React.JSX.Element {\n const uploads = useUploadsRegistry({ url: UPLOAD_API, storageKey: 'rag-uploads' })\n\n return (\n <AttachmentsPanel\n uploads={uploads.items}\n loading={uploads.isLoading}\n onAttach={uploads.upload}\n onRemoveUpload={uploads.remove}\n attachAccept={ACCEPT}\n className=\"flex-1 min-h-0\"\n />\n )\n}\n",
"app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\n\nexport default function DocsChat() {\n return (\n <Chat\n agentId=\"rag\"\n api=\"/api/ag-ui\"\n className=\"flex-1 min-h-0\"\n placeholder=\"Ask anything about your documents...\"\n />\n )\n}\n",
"app/uploads/page.tsx": "'use client'\n\nimport { AttachmentsPanel, useUploadsRegistry } from 'veryfront/chat'\n\nconst UPLOAD_API = '/api/uploads'\nconst ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'\n\nexport default function UploadsPage(): React.JSX.Element {\n const uploads = useUploadsRegistry({ url: UPLOAD_API, storageKey: 'rag-uploads' })\n const error = uploads.uploadError\n ?? uploads.refreshError\n ?? uploads.removeError\n ?? uploads.storageError\n\n return (\n <>\n {error && (\n <p role=\"alert\" className=\"m-4 rounded-md bg-red-50 p-3 text-sm text-red-800\">\n {error.message}\n </p>\n )}\n <AttachmentsPanel\n uploads={uploads.items}\n loading={uploads.isLoading}\n onAttach={uploads.upload}\n onRemoveUpload={uploads.remove}\n attachAccept={ACCEPT}\n className=\"flex-1 min-h-0\"\n />\n </>\n )\n}\n",
"content/architecture.md": "# Architecture\n\nAcme Platform uses a modular, event-driven architecture.\n\n## Core Components\n\n### API Gateway\nRoutes incoming requests to the appropriate microservice. Handles authentication, rate limiting, and request validation.\n\n### Event Bus\nAsynchronous message broker connecting all services. Supports pub/sub and point-to-point messaging patterns.\n\n### Data Layer\nMulti-tenant data storage with automatic sharding. Supports PostgreSQL for relational data and Redis for caching.\n\n## Request Flow\n\n1. Client sends request to API Gateway\n2. Gateway validates authentication token\n3. Request is routed to the target service\n4. Service processes request and publishes events\n5. Response is returned through the gateway\n\n## Scaling\n\nEach component scales independently. The API Gateway uses horizontal scaling with load balancing. Services auto-scale based on queue depth and CPU utilization.\n\n## Security\n\n- All inter-service communication uses mTLS\n- API tokens are rotated every 24 hours\n- Data at rest is encrypted with AES-256\n- Audit logs are retained for 90 days\n",
"content/getting-started.md": "# Getting Started\n\nWelcome to Acme Platform. This guide covers initial setup and core concepts.\n\n## Installation\n\nInstall the CLI globally:\n\n```bash\nnpm install -g @acme/cli\n```\n\n## Creating a Project\n\nRun the init command to scaffold a new project:\n\n```bash\nacme init my-project\ncd my-project\n```\n\n## Project Structure\n\n- `src/`: Application source code\n- `config/`: Configuration files\n- `tests/`: Test suite\n- `docs/`: Documentation\n\n## Configuration\n\nCreate an `acme.config.ts` file in your project root:\n\n```ts\nexport default {\n name: \"my-project\",\n region: \"us-east-1\",\n features: [\"auth\", \"storage\"],\n};\n```\n\n## Next Steps\n\n- Read the [Architecture Guide](./architecture) to understand the system design\n- Check [API Reference](./api-reference) for available endpoints\n- Join our Discord community for support\n",
"globals.css": "@import \"tailwindcss\";\n",
Expand Down
71 changes: 58 additions & 13 deletions docs/guides/build-a-rag-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ Set `contentDir: "knowledge"` to index `knowledge/` instead of `content/`.
Create upload routes that share the same store:

```ts title="lib/upload-auth.ts"
export function authorizeUploads(request: Request): boolean {
const token = Deno.env.get("UPLOAD_TOKEN");
return token !== undefined &&
request.headers.get("authorization") === `Bearer ${token}`;
}
import { authorizeSession } from "./session.ts";

// Verify your app's signed, same-origin session cookie. The browser sends that
// cookie for useUploadsRegistry requests without exposing a bearer token to
// client code.
export const authorizeUploads = (request: Request) => authorizeSession(request);
```

```ts title="app/api/uploads/route.ts"
Expand All @@ -75,6 +76,41 @@ document. For local-only prototypes, pass
`auth: { type: "none", allowUnauthenticated: true }` to explicitly allow
unauthenticated upload routes.

The `auth` policy is required and fails closed. An `authorize` callback must
return the literal value `true` to permit a request; `false` or an accidental
missing return denies it with `401`. Return a `Response` when the route needs a
custom denial or authentication-challenge response. Calling
`createUploadHandler(store)` without an explicit auth policy now fails when the
handler is created.

The handler bounds each source file to 10 MiB, the complete multipart body to
the file limit plus 64 KiB, and extracted UTF-8 text to 5 MiB by default. Set
`maxFileSize`, `maxBodySize`, or `maxExtractedTextBytes` only when your
deployment has a different, still-bounded budget. Invalid configuration fails
when the handler is created. Request cancellation is propagated through
multipart reading, extraction, and RAG ingestion.

`GET /api/uploads?limit=25&offset=0` returns only upload-origin documents,
newest first:

```json
{
"items": [],
"total": 0,
"offset": 0,
"limit": 25,
"hasMore": false
}
```

The default and maximum page sizes are controlled by `maxListItems` (100 by
default, at most 1,000). `useUploadsRegistry()` follows `hasMore` pages and
publishes the new server snapshot only after every bounded page validates.
`DELETE` accepts an `id` route parameter or query parameter and refuses to
delete documents that did not originate from the upload route. In Cloud mode,
a `502` means the RAG entry was removed but source blob cleanup is incomplete;
retry the same deletion safely.

## Understand ingestion

Upload ingestion does three things:
Expand Down Expand Up @@ -178,29 +214,38 @@ import { AttachmentsPanel, Chat, useUploadsRegistry } from "veryfront/chat";

export default function RagPage() {
const uploads = useUploadsRegistry({ url: "/api/uploads" });
const uploadError = uploads.uploadError ??
uploads.refreshError ??
uploads.removeError ??
uploads.storageError;

return (
<main className="flex min-h-screen">
<Chat
agentId="rag"
api="/api/ag-ui"
uploadApi="/api/uploads"
placeholder="Ask about your documents..."
/>

<AttachmentsPanel
uploads={uploads.items}
loading={uploads.isLoading}
onAttach={uploads.upload}
onRemoveUpload={uploads.remove}
/>
<aside>
{uploadError && <p role="alert">{uploadError.message}</p>}
<AttachmentsPanel
uploads={uploads.items}
loading={uploads.isLoading}
onAttach={uploads.upload}
onRemoveUpload={uploads.remove}
/>
</aside>
</main>
);
}
```

The `docs-agent` template includes a fuller upload panel. Use this smaller
example when you want the minimum wiring.
example when you want the minimum wiring. Upload documents through the panel:
the RAG ingestion route returns document metadata, not the runtime-fetchable
`url` required by `Chat.uploadApi`. Composer attachments use a separate
`createChatUploadHandler` route when you want them stored durably.

## Use Veryfront Cloud mode

Expand Down
Loading