Initial commit from agent-native create
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
---
|
||||
name: actions
|
||||
description: >-
|
||||
How to create and run agent actions. Actions are the single source of truth
|
||||
for app operations — the agent calls them as tools and frontend code calls
|
||||
them through client hooks. Use when creating a new action, adding an API
|
||||
integration, or wiring up frontend data fetching.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Agent Actions
|
||||
|
||||
## Rule
|
||||
|
||||
Actions in `actions/` are the **single source of truth** for app operations. The agent calls them as tools, and the frontend calls them through `useActionQuery` / `useActionMutation`. The framework owns the HTTP transport behind those hooks. No duplicate `/api/` routes needed.
|
||||
|
||||
Before creating any custom REST/API route for app data, inspect `actions/` and the action table in `AGENTS.md`. If an action already exists, call it directly from the agent or with `useActionQuery` / `useActionMutation` from the UI. If the capability is missing, create or update a `defineAction`. Do not add `/api/*`, `server/routes/*`, or other pass-through endpoints whose main job is to call, repackage, or re-export an action.
|
||||
|
||||
## Why
|
||||
|
||||
Actions give the agent callable tools with structured input/output, AND they give the frontend a typed client contract through hooks. One implementation serves both the agent and the UI. They keep the agent's chat context clean, they're reusable, and they can be tested independently.
|
||||
|
||||
## Keep the Action Surface Small and Orthogonal
|
||||
|
||||
Every agent-exposed action is a tool in the model's context window. There is a real cost to each one: more tools means more for the model to read, disambiguate, and choose between, which degrades tool-selection quality. Treat the action list like an API you have to maintain — add the fewest, most orthogonal actions that cover the capability, not one per UI affordance.
|
||||
|
||||
- **Prefer one CRUD-style `update` over N per-field actions.** A single `update-<thing>` that takes a patch of optional fields beats `update-<thing>-name`, `update-<thing>-order`, `update-<thing>-color`, … The agent (and the UI) pass only the fields that change. Same for `create`/`delete` — one orthogonal action per resource, not one per code path.
|
||||
- **Reach for a generic query / escape hatch before minting a new read action.** If the agent needs more or different data, do not add `get-<thing>-by-x`, `list-<thing>-filtered-by-y`, etc. For provider data, expose the shared `provider-api-catalog` / `provider-api-docs` / `provider-api-request` trio (see `templates/dispatch/actions/`) so the agent can hit any endpoint or filter without a new action each time. For app data in dev, the `db-query` tool already answers arbitrary read questions.
|
||||
- **Hide UI-only or purely programmatic actions from the model with `agentTool: false`.** An action that only the frontend or an HTTP/cron caller needs should not spend a slot in the model's tool list. `agentTool: false` keeps it callable from `useActionMutation` / `callAction` / `/_agent-native/actions/<name>` while removing it from every agent tool surface (in-app assistant, MCP, A2A).
|
||||
- **`agentTool: false` is NOT `toolCallable: false`.** They are different switches:
|
||||
- `agentTool: false` → hidden from the **model entirely** (it is no longer a tool the agent can see or call). Still frontend/HTTP-callable.
|
||||
- `toolCallable: false` → only blocks the **sandboxed extension ("tools") iframe bridge** (`appAction(...)`). The action stays fully visible to the model, the UI, the CLI, MCP, and A2A. Use it for high-blast-radius operations (account/org/auth changes), not for trimming the tool list.
|
||||
- **Remove or hide stale actions.** When the UI stops using an action, delete it or set `agentTool: false` — do not leave it exposed to the model as dead tool weight. The advisory audit below helps you spot these.
|
||||
|
||||
### Audit Script (Advisory)
|
||||
|
||||
`pnpm actions:audit [template ...]` (or `node scripts/audit-template-actions.mjs`) statically scans a template's `actions/` and prints two kinds of suggestions:
|
||||
|
||||
1. **Likely UI-dead** — HTTP-exposed mutating actions whose name is never referenced under `app/` (candidates to delete or mark `agentTool: false`).
|
||||
2. **Likely redundant clusters** — groups like `update-foo-name` / `update-foo-order` that could collapse into one orthogonal `update-foo`.
|
||||
|
||||
It is **advisory only**: it always exits 0, never fails CI, and uses conservative heuristics, so expect some false positives (e.g. an action the agent calls but the UI doesn't). Use it as a prompt to review, not a gate.
|
||||
|
||||
## How to Create an Action
|
||||
|
||||
Use `defineAction` with a Zod schema (required for new actions):
|
||||
|
||||
```ts
|
||||
// actions/list-meals.ts
|
||||
import { z } from "zod";
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
import { getDb } from "../server/db/index.js";
|
||||
import { meals } from "../server/db/schema.js";
|
||||
|
||||
export default defineAction({
|
||||
description: "List all meals",
|
||||
schema: z.object({
|
||||
date: z.string().describe("Filter by date (YYYY-MM-DD)"),
|
||||
}),
|
||||
http: { method: "GET" },
|
||||
run: async (args) => {
|
||||
// args is fully typed: { date: string }
|
||||
const db = getDb();
|
||||
const rows = await db.select().from(meals);
|
||||
return rows; // Return objects/arrays, NOT JSON.stringify()
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `schema` field accepts a Zod schema (or any Standard Schema-compatible library). It provides runtime validation with clear error messages (400 for HTTP, error result for agent), full TypeScript type inference for `run()` args, and auto-generated JSON Schema for the agent's tool definition. `zod` is a dependency of all templates.
|
||||
|
||||
When an action reads or writes app data, use Drizzle's query builder and portable operators from `drizzle-orm`. Do not use raw SQL, `getDbExec()`, or dialect-specific schema imports in normal actions unless there is a documented reason Drizzle cannot express the query.
|
||||
|
||||
When an action calls an external service, never hardcode API keys, bearer
|
||||
tokens, webhook URLs, signing secrets, OAuth refresh tokens, private
|
||||
Builder/internal data, or customer data. Read user/org/workspace credentials
|
||||
from `readAppSecret`, `resolveCredential`, OAuth token helpers, or the provider
|
||||
API credential adapter. Use `process.env` only for explicitly deploy-level
|
||||
configuration, and keep examples to obvious placeholders.
|
||||
|
||||
Tips:
|
||||
- Use `.describe()` for parameter descriptions
|
||||
- Use `.optional()` for optional params
|
||||
- Use `z.coerce.number()` for numeric params that arrive as strings from HTTP.
|
||||
For booleans, use an explicit string parser/helper instead of
|
||||
`z.coerce.boolean()` because JavaScript treats any non-empty string,
|
||||
including `"false"`, as truthy.
|
||||
- Use `z.enum(["draft", "published"])` for constrained values
|
||||
|
||||
The legacy `parameters` field (plain JSON Schema object) still works as a fallback but does not provide runtime validation or type inference.
|
||||
|
||||
## Decision Order
|
||||
|
||||
When you need app data or a mutation:
|
||||
|
||||
1. **Use an existing action** if one already performs the operation.
|
||||
2. **Create or extend a `defineAction`** when the agent and UI both need a new operation.
|
||||
3. **Create a custom route only for route-only concerns** such as uploads, streaming, webhooks, OAuth callbacks, or a non-JSON protocol.
|
||||
|
||||
Do not build an umbrella REST API to make actions "easier" to call. Actions are already callable by agents, CLIs, React hooks, HTTP, MCP/A2A exposure, and external hosts through the framework.
|
||||
|
||||
## Flexible Provider APIs
|
||||
|
||||
For provider integrations used in ad hoc analysis, querying, reporting, or
|
||||
cross-source research, do not hardcode every provider endpoint as a separate
|
||||
rigid action and do not encode one lookback window, filter shape, or pagination
|
||||
strategy as the only path the agent can take. Expose the shared provider API
|
||||
action trio instead:
|
||||
|
||||
- `provider-api-catalog`: lists provider base URLs, auth style, credential keys,
|
||||
docs/spec URLs, placeholders, and examples without exposing secrets.
|
||||
- `provider-api-docs`: fetches public provider docs/spec/changelog URLs when
|
||||
the exact endpoint, filter operator, payload shape, or pagination contract is
|
||||
uncertain. Registered docs URLs are curated starting points. Use
|
||||
`responseMode: "markdown"` for clean readable docs, or
|
||||
`responseMode: "matches"` with `search: { query | terms | regex }` for
|
||||
compact snippets instead of flooding context with raw HTML.
|
||||
- `provider-api-request`: makes a constrained authenticated HTTP request to the
|
||||
provider host, injects configured credentials, blocks private/internal URLs,
|
||||
and redacts secrets.
|
||||
|
||||
Use `@agent-native/core/provider-api` as the shared substrate, and build these
|
||||
actions as thin factory imports rather than copying `defineAction` schemas and
|
||||
handlers into each template:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createProviderApiCatalogAction,
|
||||
createProviderApiDocsAction,
|
||||
createProviderApiRequestAction,
|
||||
} from "@agent-native/core/provider-api/actions/provider-api";
|
||||
import {
|
||||
createDeleteStagedDatasetAction,
|
||||
createListStagedDatasetsAction,
|
||||
createQueryStagedDatasetAction,
|
||||
} from "@agent-native/core/provider-api/actions/staged-datasets";
|
||||
```
|
||||
|
||||
Pass the app's existing provider runtime to the provider factories, and pass
|
||||
its app id to the staged-dataset factories. Keep app-specific descriptions,
|
||||
provider allow-lists, HTTP/tool-callability settings, and credential adapters
|
||||
as factory options. Only add a thin credential adapter when the app has
|
||||
app-specific credential lookup rules. If the app stores a built-in provider's
|
||||
OAuth grant under a narrower local provider id, use the runtime's
|
||||
`oauthProviderOverrides` instead of duplicating provider config. If credentials
|
||||
are stored on shareable/resource rows rather than in the shared credential or
|
||||
OAuth-token stores, build a resolver that enforces those access checks before
|
||||
exposing raw provider requests. Keep `provider-api-request` `http: false`
|
||||
unless a separate UI permission model authorizes arbitrary provider writes.
|
||||
Specific actions such as `search-records`, `search-emails`, or `sync-source`
|
||||
are convenience shortcuts, not capability limits; agents should fall back to
|
||||
the provider API trio when a question requires an endpoint or filter that the
|
||||
shortcut does not model.
|
||||
|
||||
This is a framework tenet. The safety boundary should be provider host
|
||||
allow-listing, credential scoping, auth injection, private-network blocking,
|
||||
secret redaction, and user/org access checks, not an artificially small set of
|
||||
hand-authored read actions. If the upstream provider API supports a capability,
|
||||
the agent should normally be able to reach it through `provider-api-request`
|
||||
with the user's configured credentials. For large responses, expose staging
|
||||
(`stageAs`, `itemsPath`, pagination, and `query-staged-dataset`) or sandboxed
|
||||
code execution so the agent can reduce data without flooding context.
|
||||
|
||||
For broad provider questions, cross-source joins, corpus-wide mention/search
|
||||
work, classification, or any answer where absence matters, design the action
|
||||
surface for full coverage instead of convenience-only samples. The agent should
|
||||
be able to fetch every relevant page or an explicitly bounded cohort, stage or
|
||||
save the raw provider response outside chat, and then use
|
||||
`query-staged-dataset`, `run-code`, or provider-side search to count, join,
|
||||
grep, classify, and aggregate. Tool descriptions and AGENTS.md guidance should
|
||||
teach agents to report source, filters, time window, row/record counts,
|
||||
pagination status, truncation, failed pages, and uncovered gaps. They must not
|
||||
turn default limits, sampled rows, truncated excerpts, or aborted calls into a
|
||||
confident "none found", "all records", or exhaustive conclusion.
|
||||
|
||||
For public web pages and docs, prefer the token-efficient path: `web-search`
|
||||
to find likely URLs, `web-request` or `provider-api-docs` with clean
|
||||
`responseMode` output to read a page, and `run-code` with `webRead()` /
|
||||
`webFetch()` when you need to grep, aggregate, or compare many pages before
|
||||
returning a small result.
|
||||
|
||||
For LONG compute, `run-code` supports durable background executions: pass
|
||||
`background: true` and the code is enqueued to a `sandbox_executions` row and
|
||||
executed out-of-band with a generous budget (default 10 min), surviving the
|
||||
hosted agent run's ~40s soft timeout. The call returns
|
||||
`{ executionId, status: "queued" }` immediately with polling guidance; check
|
||||
progress with `run-code` `{ executionId }` (or the `get-code-execution` tool
|
||||
where registered) — results persist after completion. Use background for big
|
||||
cross-source joins, multi-page provider sweeps, and heavy analysis scripts;
|
||||
keep quick scripts in the default foreground mode. Agents should continue
|
||||
other work between polls, and on `failed`/`timed_out` split the computation or
|
||||
persist intermediate progress with `workspaceWrite` and re-run.
|
||||
|
||||
### The `http` Option
|
||||
|
||||
Controls how the action is exposed as an HTTP endpoint:
|
||||
|
||||
| Value | Behavior | Use for |
|
||||
| ------------------------- | ----------------------------------------------------------- | -------------------------------- |
|
||||
| _(omitted)_ | Auto-exposed as `POST /_agent-native/actions/:name` | Write operations (default) |
|
||||
| `{ method: "GET" }` | Auto-exposed as `GET /_agent-native/actions/:name` | Read-only queries |
|
||||
| `{ method: "PUT" }` | Auto-exposed as `PUT /_agent-native/actions/:name` | Update operations |
|
||||
| `{ method: "DELETE" }` | Auto-exposed as `DELETE /_agent-native/actions/:name` | Delete operations |
|
||||
| `{ method: "GET", path: "custom" }` | Auto-exposed as `GET /_agent-native/actions/custom` | Custom route path |
|
||||
| `false` | Agent-only, never exposed as HTTP | `navigate`, `view-screen`, internal actions |
|
||||
|
||||
### Screen Refresh (automatic)
|
||||
|
||||
The framework auto-refreshes the UI after any successful mutating action. On completion of a non-`GET` action, the framework emits a change event with `source: "action"` that the client's `useDbSync` picks up and uses to invalidate `["action"]` React Query keys — so `list-*` / `get-*` hooks refetch without a full page reload. In-process calls emit directly; dev-mode `pnpm action ...` calls also write a durable marker so the web server sees child-process action changes.
|
||||
|
||||
Rules:
|
||||
|
||||
- `http: { method: "GET" }` → read-only, does NOT trigger a refresh (inferred automatically).
|
||||
- Any other action (default `POST`, `PUT`, `DELETE`, or `http: false`) → treated as mutating, triggers a refresh on success.
|
||||
- To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
|
||||
- To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
|
||||
|
||||
Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
|
||||
|
||||
### Return Values
|
||||
|
||||
Actions should return **structured data** (objects, arrays) — not `JSON.stringify()`. The framework serializes the response automatically. If you return a string, the framework tries to parse it as JSON for a clean response.
|
||||
|
||||
```ts
|
||||
// Good — return structured data
|
||||
run: async (args) => {
|
||||
const events = await fetchEvents(args.from, args.to);
|
||||
return events;
|
||||
}
|
||||
|
||||
// Bad — don't stringify
|
||||
run: async (args) => {
|
||||
const events = await fetchEvents(args.from, args.to);
|
||||
return JSON.stringify(events, null, 2);
|
||||
}
|
||||
```
|
||||
|
||||
### Returning Images the Agent Can See (`_agentImages`)
|
||||
|
||||
An action's return object may include the well-known optional field
|
||||
`_agentImages` to attach vision images (screenshots, chart previews, rendered
|
||||
designs) to the tool result. The agent literally sees them — enabling visual
|
||||
self-review loops — while the field itself is stripped from the JSON text the
|
||||
model reads.
|
||||
|
||||
```ts
|
||||
run: async ({ dashboardId }) => {
|
||||
const shot = await renderDashboardPng(dashboardId); // Buffer
|
||||
return {
|
||||
dashboardId,
|
||||
panelCount: 6,
|
||||
_agentImages: [
|
||||
// Either a public https URL (preferred — the provider fetches it)…
|
||||
{ url: "https://cdn.example.com/previews/dash-1.png", label: "overview" },
|
||||
// …or base64 without a data: prefix (mediaType required; a full
|
||||
// data:image/png;base64,… URL in `data` is also accepted and parsed).
|
||||
{ data: shot.toString("base64"), mediaType: "image/png" },
|
||||
],
|
||||
};
|
||||
},
|
||||
```
|
||||
|
||||
Rules and limits:
|
||||
|
||||
- Shape: `Array<{ url?: string; data?: string; mediaType?: string; label?: string }>`.
|
||||
Each entry needs `url` (https only) **or** `data`. Supported media types:
|
||||
`image/jpeg`, `image/png`, `image/gif`, `image/webp`.
|
||||
- Caps: max **4 images** per result; max **~2MB of base64** per image.
|
||||
Over-cap or invalid entries never fail the call — they become text notes in
|
||||
the result telling the model what was dropped and why.
|
||||
- Persistence: the run ledger stores only the string result plus compact
|
||||
`[image: …]` notes (URLs verbatim; base64 as a byte-count placeholder) —
|
||||
never the payload. Images are re-attached only for the live turn; replayed
|
||||
history is text-only, so prefer stable `url` images the model can re-request.
|
||||
- Engine support: native Anthropic and vision-capable AI-SDK providers
|
||||
(anthropic, openai, google, openrouter) receive real image blocks; other
|
||||
paths (Builder gateway, non-vision providers) degrade to the text notes.
|
||||
- External MCP tools need no changes — standard MCP `image` content parts are
|
||||
converted automatically under the same caps.
|
||||
|
||||
### Validating Return Values (`outputSchema`)
|
||||
|
||||
`schema` validates inputs; `outputSchema` validates what the action **returns**. Pass any Standard Schema-compatible schema (Zod, Valibot, ArkType) and the framework validates the result _after_ `run()` resolves — input validated before `run`, output after.
|
||||
|
||||
```ts
|
||||
export default defineAction({
|
||||
description: "Summarize a thread.",
|
||||
schema: z.object({ threadId: z.string() }),
|
||||
outputSchema: z.object({ summary: z.string(), messageCount: z.number() }),
|
||||
outputErrorStrategy: "warn", // default; "strict" | "fallback"
|
||||
// outputFallback: { summary: "", messageCount: 0 }, // used only by "fallback"
|
||||
run: async ({ threadId }) => {
|
||||
/* ... */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- `"warn"` (default) — `console.warn` the issues and return the **original** result unchanged. Non-breaking.
|
||||
- `"strict"` — throw a clear error so a buggy action surfaces loudly.
|
||||
- `"fallback"` — return `outputFallback` in place of the invalid result.
|
||||
|
||||
On success the validated value is returned, so coercion/defaults on `outputSchema` apply. Omit `outputSchema` and behavior is byte-for-byte unchanged (no wrapping).
|
||||
|
||||
### Human-in-the-Loop Approval (`needsApproval`)
|
||||
|
||||
For high-consequence, outward-facing, hard-to-undo actions (sending an email, charging a card, deleting an account), set `needsApproval` so the agent **cannot** run the action without a human approving the specific call:
|
||||
|
||||
```ts
|
||||
export default defineAction({
|
||||
description: "Send an email via Gmail.",
|
||||
schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
|
||||
needsApproval: true, // boolean, or (args, ctx) => boolean | Promise<boolean>
|
||||
run: async (args) => {
|
||||
/* ...actually send... */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
||||
|
||||
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
||||
|
||||
## Frontend Hooks
|
||||
|
||||
The frontend calls actions using React Query hooks from `@agent-native/core/client`. Components should not hand-write `fetch("/_agent-native/actions/...")`; add or reuse a client hook/helper instead. Use `callAction` from the same package for imperative cases that do not fit a hook, such as debounced search, prefetching, or non-React event handlers.
|
||||
|
||||
### `useActionQuery` — for GET actions
|
||||
|
||||
```ts
|
||||
import { useActionQuery } from "@agent-native/core/client/hooks";
|
||||
|
||||
function MealList() {
|
||||
// Types are auto-inferred from the action's schema + return type — no manual generic needed
|
||||
const { data: meals } = useActionQuery("list-meals", {
|
||||
date: "2025-01-01",
|
||||
});
|
||||
return <ul>{meals?.map((m) => <li key={m.id}>{m.name}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
### `useActionMutation` — for POST/PUT/DELETE actions
|
||||
|
||||
```ts
|
||||
import { useActionMutation } from "@agent-native/core/client/hooks";
|
||||
|
||||
function AddMealButton() {
|
||||
// Types are auto-inferred — no manual generic needed
|
||||
const { mutate } = useActionMutation("log-meal");
|
||||
return (
|
||||
<button onClick={() => mutate({ name: "Salad", calories: 350 })}>
|
||||
Log Meal
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Do NOT use manual type generics** like `useActionQuery<Meal[]>(...)`. Types are inferred automatically from `.generated/action-types.d.ts`, which is auto-generated by a Vite plugin.
|
||||
|
||||
Mutations automatically invalidate all `["action"]` query keys on success, so GET queries refetch.
|
||||
|
||||
### `callAction` — for imperative client code
|
||||
|
||||
```ts
|
||||
import { callAction } from "@agent-native/core/client/hooks";
|
||||
|
||||
const people = await callAction("search-people", { query }, { method: "GET" });
|
||||
```
|
||||
|
||||
Prefer hooks in React data flows. Use `callAction` when a hook would be awkward;
|
||||
do not hand-write action route fetches in components.
|
||||
|
||||
## How to Run (Agent)
|
||||
|
||||
```bash
|
||||
pnpm action my-action --input data/source.json --output data/result.json
|
||||
```
|
||||
|
||||
## Action Dispatcher
|
||||
|
||||
The default template uses core's `runScript()` in `actions/run.ts`:
|
||||
|
||||
```ts
|
||||
import { runScript } from "@agent-native/core";
|
||||
runScript();
|
||||
```
|
||||
|
||||
This is the canonical approach for new apps. Action names must be lowercase with hyphens only (e.g., `my-action`).
|
||||
|
||||
## When You Still Need Custom `/api/` Routes
|
||||
|
||||
Most operations should be actions. You only need custom routes in `server/routes/api/` for:
|
||||
|
||||
- **File uploads** — actions receive JSON params, not multipart form data
|
||||
- **Streaming responses** — SSE or chunked responses that need direct H3 control
|
||||
- **Webhooks** — external services POST to a specific URL
|
||||
- **OAuth callbacks** — redirect-based flows that need specific URL patterns
|
||||
|
||||
When the agent needs a durable image or file URL, call the core `upload-image`
|
||||
action or use `uploadFile()` in server code. Do not write base64 into SQL,
|
||||
markdown, deck/design JSON, or action results. `_agentImages` on action results
|
||||
is for ephemeral vision previews only, not persistence.
|
||||
|
||||
If it's a standard CRUD operation, data query, or a wrapper around an action, use the action instead.
|
||||
|
||||
## Legacy Pattern (bare export)
|
||||
|
||||
Older actions use a bare async function export with `parseArgs`:
|
||||
|
||||
```ts
|
||||
import { parseArgs, loadEnv, fail } from "@agent-native/core";
|
||||
|
||||
export default async function myAction(args: string[]) {
|
||||
loadEnv();
|
||||
const parsed = parseArgs(args);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This still works but is not auto-exposed as HTTP. Prefer `defineAction` for all new actions.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **One action, one job.** Keep actions focused on a single operation. The agent composes multiple action calls for complex operations.
|
||||
- **Return structured data.** Return objects/arrays, not `JSON.stringify()`.
|
||||
- **Use `http: { method: "GET" }`** for read-only actions. Default is POST.
|
||||
- **Use `http: false`** for agent-only actions (`navigate`, `view-screen`).
|
||||
- **Use `agentTool: false`** for UI-only / programmatic actions that should NOT be a tool in the model's context window. It stays frontend/HTTP-callable but is hidden from the agent. Distinct from `toolCallable: false`, which only blocks the sandboxed extension iframe bridge.
|
||||
- **Document reusable actions.** If a new action should be called by agents outside one narrow screen, update `AGENTS.md` with when to use it, important args, and which return fields to preserve.
|
||||
- **Promote workflow-heavy actions to skills.** If the action is part of a provider-backed, cross-app, MCP/A2A, or multi-step workflow, create or update a skill in `.agents/skills/` and add app-skill visibility (`internal`, `exported`, or `both`) when it should ship through a marketplace.
|
||||
- **Use `loadEnv()`** only for deploy-level configuration. User/org/workspace
|
||||
credentials belong in the encrypted secrets/credential/OAuth stores, never as
|
||||
hardcoded literals or shared env fallbacks.
|
||||
- **Use `fail()`** for user-friendly error messages (exits with message, no stack trace).
|
||||
- **Import action primitives from `@agent-native/core/action`** and CLI helpers such as `parseArgs()` from `@agent-native/core` — do not redefine framework utilities locally.
|
||||
- **Do not re-export actions as REST.** The mounted `/_agent-native/actions/:name` endpoint is the REST surface; duplicating it under `/api/*` creates drift and hides the operation from agents.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Read action (GET):**
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
|
||||
export default defineAction({
|
||||
description: "List calendar events",
|
||||
schema: z.object({
|
||||
from: z.string().describe("Start date"),
|
||||
to: z.string().describe("End date"),
|
||||
}),
|
||||
http: { method: "GET" },
|
||||
run: async (args) => {
|
||||
return await fetchEvents(args.from, args.to);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Write action (POST, default):**
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
|
||||
export default defineAction({
|
||||
description: "Log a meal",
|
||||
schema: z.object({
|
||||
name: z.string().describe("Meal name"),
|
||||
calories: z.coerce.number().describe("Calorie count"),
|
||||
}),
|
||||
run: async (args) => {
|
||||
// args.calories is a number — z.coerce.number() handles string-to-number conversion from HTTP
|
||||
const meal = await insertMeal(args);
|
||||
return meal;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Agent-only action:**
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
|
||||
export default defineAction({
|
||||
description: "Navigate the UI to a view",
|
||||
schema: z.object({
|
||||
view: z.string().describe("Target view"),
|
||||
}),
|
||||
http: false,
|
||||
run: async (args) => {
|
||||
await writeAppState("navigate", { command: "go", view: args.view });
|
||||
return "Navigated";
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Action not found** — Check that the filename matches the command name exactly. `pnpm action foo-bar` looks for `actions/foo-bar.ts`.
|
||||
- **Args not parsing** — Ensure args use `--key value` or `--key=value` format. Boolean flags use `--flag` (sets value to `"true"`).
|
||||
- **Frontend getting 405** — The action's `http.method` doesn't match the hook. Use `useActionQuery` for GET actions, `useActionMutation` for POST/PUT/DELETE.
|
||||
- **Frontend getting undefined** — Make sure the action returns structured data, not `JSON.stringify()`.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **storing-data** — Actions read/write data in SQL
|
||||
- **delegate-to-agent** — The agent invokes actions via `pnpm action <name>`
|
||||
- **real-time-sync** — Database writes from actions trigger change events to update the UI
|
||||
- **adding-a-feature** — Actions are area 2 of the four-area checklist
|
||||
- **client-methods** — Client code uses named helpers/hooks instead of raw REST calls
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: adding-a-feature
|
||||
description: >-
|
||||
The four-area checklist every new feature must complete. Use when adding any
|
||||
feature, integration, or capability to ensure the agent and UI stay in parity.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Adding a Feature — The Four-Area Checklist
|
||||
|
||||
## Rule
|
||||
|
||||
Every new feature MUST update all four areas. Skipping any one breaks the agent-native contract — the agent and UI must always be equal partners.
|
||||
|
||||
## Why
|
||||
|
||||
Agent-native apps are defined by parity: everything the UI can do, the agent can do, and vice versa. A feature that only has UI is invisible to the agent. A feature that only has scripts is invisible to the user. A feature without app-state sync means the agent is blind to what the user is doing.
|
||||
|
||||
## The Checklist
|
||||
|
||||
When you add a new feature, work through these four areas in order:
|
||||
|
||||
### 1. UI Component
|
||||
|
||||
Build the user-facing interface — a page, component, dialog, or route. Use `useActionQuery` and `useActionMutation` from `@agent-native/core/client` to call actions for data fetching and mutations. Do not create a custom REST endpoint just so React can call action-backed data; the action endpoint already exists.
|
||||
|
||||
**Auto-refresh on agent writes is non-negotiable** — when the agent mutates data, the UI must reflect the change without a manual refresh. There are two paths, and you must pick the right one:
|
||||
|
||||
- **`useActionQuery` / `useActionMutation`** — covered automatically. The framework's `useDbSync` invalidates `["action"]` on every change event, so every `useActionQuery` hook refetches on agent activity. No extra wiring required. **Prefer this path.**
|
||||
- **Raw `useQuery` with custom keys** — needs explicit wiring. Fold `useChangeVersions([<source>, "action"])` from `@agent-native/core/client` into the `queryKey` and set `placeholderData: (prev) => prev`. The `action` source is the reliable signal (the agent runner emits it after every successful tool call); the resource-specific source (`"dashboards"`, `"analyses"`, `"settings"`, etc.) is bonus when emitted. Without this wiring, agent writes will be invisible until manual refresh — that breaks the framework's #1 promise.
|
||||
|
||||
```tsx
|
||||
import { useChangeVersions } from "@agent-native/core/client/hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const v = useChangeVersions(["dashboards", "action"]);
|
||||
useQuery({
|
||||
queryKey: ["dashboard", id, v],
|
||||
queryFn: () => fetchDashboard(id),
|
||||
placeholderData: (prev) => prev, // no flicker on refetch
|
||||
});
|
||||
```
|
||||
|
||||
See the `real-time-sync` skill for the full pattern and source catalog.
|
||||
|
||||
### 2. Action
|
||||
|
||||
Create an action in `actions/` using `defineAction`. This serves double duty: the agent calls it as a tool, and the UI calls it through `useActionQuery` / `useActionMutation` while the framework owns the HTTP transport. Set `http: { method: "GET" }` for read actions, leave default for writes, or set `http: false` for agent-only actions like `navigate` and `view-screen`.
|
||||
|
||||
Before adding a new route or endpoint, inspect the existing actions. Reuse an
|
||||
action if it already covers the business operation, extend it if the shared
|
||||
contract is incomplete, or create a new `defineAction` if the agent and UI both
|
||||
need the capability. Do not add pass-through `/api/*` routes that re-export
|
||||
actions. If client code needs a new framework/app route, expose a named helper
|
||||
or hook first and use that helper from components and docs.
|
||||
|
||||
For provider-backed analysis/query/reporting integrations, do not turn every
|
||||
provider endpoint or filter into a rigid action. Prefer the shared
|
||||
`provider-api-catalog` / `provider-api-docs` / `provider-api-request` pattern
|
||||
from `@agent-native/core/provider-api`, then add narrow convenience actions only
|
||||
for workflows that truly deserve a first-class shortcut. Treat this as a
|
||||
capability requirement, not a nice-to-have: convenience actions must not become
|
||||
the ceiling of what the agent can ask the provider to do. Pair broad provider
|
||||
access with staging or sandboxed code execution when responses may be too large
|
||||
for chat context. If provider credentials live on resource/share rows, add the
|
||||
scoped resolver first so broad access preserves the same ownership boundary as
|
||||
the app UI.
|
||||
|
||||
If the feature needs credentials, design the credential path in the same change.
|
||||
Never hardcode API keys, tokens, webhook URLs, signing secrets, private
|
||||
Builder/internal data, or customer data in the action, UI, seed data, fixtures,
|
||||
docs, prompts, or generated extension/app content. Register required secrets,
|
||||
use OAuth helpers, or read scoped values from the vault/credential store.
|
||||
|
||||
If the feature involves attachments, images, recordings, screenshots, exports,
|
||||
or other file-like payloads, design the upload path in the same change:
|
||||
provider upload first, then URL/id/blob handle in SQL. Do not add base64/binary
|
||||
columns or stuff files into `application_state`.
|
||||
|
||||
**If the action produces or lists a navigable resource**, add a `link` builder that returns `{ url: buildDeepLink({ app, view, params }), label }`. External coding agents and MCP hosts (Claude / ChatGPT / Claude Code / Cowork / Codex, over MCP/A2A) then surface an "Open in … →" deep link that drops the user back into the running UI focused on the record — for free. If a compatible MCP host should render an inline review/edit surface, also add `mcpApp` with `embedApp()` so the action embeds the real React app route instead of a one-off HTML UI. The `link` builder and `mcpApp` metadata must be pure and synchronous (no I/O). Any external-agent read/ingest action must be `http: { method: "GET" }` + `readOnly: true` + `publicAgent: { expose: true, readOnly: true, requiresAuth: true }`. See the `external-agents` skill.
|
||||
|
||||
### 3. Skills / Instructions
|
||||
|
||||
Update `AGENTS.md` and/or create a skill in `.agents/skills/` if the feature introduces patterns the agent needs to know. At minimum, add the new actions to the action table in the template's `AGENTS.md`.
|
||||
|
||||
Reusable actions are part of the app contract, not just implementation detail. When an action is useful outside one screen, update agent instructions in the same change so app agents know when to call it, which arguments matter, and what output to preserve. If the capability is workflow-heavy, cross-app, provider-backed, or has a non-obvious sequence of actions, add or update a skill instead of burying the behavior in one long `AGENTS.md` paragraph.
|
||||
|
||||
Instruction examples may name secret keys like `SLACK_WEBHOOK`, but must use
|
||||
placeholders such as `${keys.SLACK_WEBHOOK}` or `<SLACK_WEBHOOK>`. Do not paste
|
||||
real keys, internal data, or customer data into instructions as examples.
|
||||
|
||||
If the feature adds or changes visible UI copy, prompts, toasts, labels, empty
|
||||
states, or formatting, read `internationalization` and update the app's i18n
|
||||
catalogs in the same change.
|
||||
|
||||
For app-backed skills, declare skill visibility in the app-skill manifest:
|
||||
|
||||
- `internal` — only the app's own agents should use it.
|
||||
- `exported` — marketplace installs receive it, but the app does not need it loaded internally.
|
||||
- `both` — shared between the app's internal agents and exported marketplace bundles.
|
||||
|
||||
### 4. Application State Sync
|
||||
|
||||
Expose navigation and selection state so the agent knows what the user is looking at. Write to the `navigation` app-state key on route changes. Update the `view-screen` action to fetch relevant data for the new feature. Add a `navigate` command if the agent needs to open the new view.
|
||||
|
||||
## Examples
|
||||
|
||||
### Adding "compose email" to a mail app
|
||||
|
||||
| Area | What to build |
|
||||
| --------------- | ---------------------------------------------------------------------------------------- |
|
||||
| UI | Compose panel with tabs, to/cc/bcc fields, body editor. Use `useActionQuery`/`useActionMutation` for data. |
|
||||
| Action | `manage-draft` action (create/update/delete drafts), `send-email` action |
|
||||
| Skills/AGENTS | Document compose state shape, draft lifecycle, action args in AGENTS.md |
|
||||
| App-state sync | `compose-{id}` keys for each draft tab, `navigation` includes compose state |
|
||||
|
||||
### Adding "create form" to a forms app
|
||||
|
||||
| Area | What to build |
|
||||
| --------------- | ---------------------------------------------------------------------------------------- |
|
||||
| UI | Form builder page with drag-and-drop fields, preview, settings. Use `useActionQuery` for lists. |
|
||||
| Action | `create-form` action, `update-form` action, `list-forms` action (GET) |
|
||||
| Skills/AGENTS | Document form schema shape, field types, validation rules in AGENTS.md |
|
||||
| App-state sync | `navigation` includes `{ view: "form-builder", formId: "..." }`, `view-screen` fetches form data |
|
||||
|
||||
### Adding "chart type" to an analytics app
|
||||
|
||||
| Area | What to build |
|
||||
| --------------- | ---------------------------------------------------------------------------------------- |
|
||||
| UI | New chart component, chart type selector in dashboard |
|
||||
| Action | `create-chart` or `update-dashboard` action that sets chart type and config |
|
||||
| Skills/AGENTS | Document supported chart types, config options, data requirements |
|
||||
| App-state sync | `navigation` includes selected chart/dashboard, `view-screen` returns chart config |
|
||||
|
||||
## Adding a new route
|
||||
|
||||
Templates are single-page apps with client-side routing. The app shell (AgentSidebar + top-level nav) MUST persist across navigation — it is mounted once, either in `root.tsx` around `<Outlet />` or via a pathless `_app.tsx` layout route that all authed routes nest under.
|
||||
|
||||
**Never wrap each new route in its own `<AppLayout>` / `<Layout>`.** That causes React to unmount the entire app shell on every navigation, reloading the agent sidebar and destroying in-progress work.
|
||||
|
||||
- If the template has `<AppLayout>` in `root.tsx` — just render page content in your new route file, nothing else.
|
||||
- If the template has `app/routes/_app.tsx` (pathless layout) — name your new route `_app.<segment>.tsx` to inherit the shell, or bare `<segment>.tsx` for public routes that should NOT have the shell.
|
||||
- If a page needs per-route data (e.g. highlighting the active item in the sidebar), read it in the layout from `useParams()` / `useLocation()`. Don't pass it as a prop through every route file.
|
||||
|
||||
See the "Client-Side Routing" section in the root `CLAUDE.md` for full details.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- **Per-route `<AppLayout>` wrappers** — Every route file wraps its content in `<AppLayout>` or `<Layout>`. React sees a different component at the outlet on each nav and unmounts the whole shell, causing the agent sidebar to reload on every click. Mount the shell once above `<Outlet />` (root.tsx or `_app.tsx` pathless layout).
|
||||
- **UI without actions** — The user can create forms but the agent cannot. The agent says "I don't have access to that" when it should be able to do it.
|
||||
- **Actions without AGENTS.md** — The actions exist but the agent doesn't know about them because they're not documented. The agent reinvents solutions instead of using the actions.
|
||||
- **Duplicate API routes** — Creating `/api/` routes for operations that actions already handle, including pass-through routes that just call or repackage an action. Use `useActionQuery`/`useActionMutation` instead.
|
||||
- **Raw client route calls** — Teaching or adding `fetch("/_agent-native/...")`,
|
||||
`fetch(agentNativePath(...))`, or template `/api/*` calls in components for
|
||||
normal app work. Add a named client helper/hook and call that instead.
|
||||
- **Features without app-state** — The agent cannot see that the user is looking at a specific form, email, or chart. It asks "which one?" instead of acting on the current selection.
|
||||
- **Actions without UI** — The agent can do something the user cannot. This is less common but still breaks parity.
|
||||
|
||||
## Verification
|
||||
|
||||
After completing all four areas, verify:
|
||||
|
||||
1. Can the user perform the operation from the UI?
|
||||
2. Can the agent perform the same operation via actions?
|
||||
3. Does `pnpm action view-screen` show the relevant state when the user is using the feature?
|
||||
4. Can the agent navigate to the feature view via the `navigate` action?
|
||||
5. Is the feature documented in AGENTS.md with action names and args?
|
||||
6. Are credentials and sensitive data supplied only through approved runtime
|
||||
channels, with no hardcoded real keys, tokens, webhook URLs, Builder/internal
|
||||
data, or customer data?
|
||||
|
||||
## One more area — sharing
|
||||
|
||||
If the feature stores **user-authored resources** (documents, dashboards, forms, decks, etc.), make them ownable so they get private-by-default semantics and a share dialog for free. See the `sharing` skill.
|
||||
|
||||
TL;DR: spread `ownableColumns()` into the resource table, pair it with `createSharesTable(...)`, call `registerShareableResource(...)`, wrap list/read queries with `accessFilter`, guard writes with `assertAccess`, and drop `<ShareButton>` in the resource header. The `share-resource`, `unshare-resource`, `list-resource-shares`, and `set-resource-visibility` actions are auto-mounted framework-wide.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **sharing** — How to make a new resource ownable (private by default, share with users/orgs/public)
|
||||
- **context-awareness** — How to expose UI state to the agent (area 4 in detail)
|
||||
- **actions** — How to create actions with `defineAction` and the `http` option (area 2 in detail)
|
||||
- **external-agents** — Add a `link` builder so external agents (MCP/A2A) get an "Open in … →" deep link
|
||||
- **create-skill** — How to create skills for new patterns (area 3 in detail)
|
||||
- **storing-data** — Where to store the feature's data
|
||||
- **real-time-sync** — How the UI stays in sync when the agent writes data
|
||||
- **internationalization** — How to update localized UI copy and catalogs
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: agent-native-docs
|
||||
description: >-
|
||||
How to find version-matched Agent Native framework docs and source bundled in
|
||||
node_modules. Use before implementing or answering questions about
|
||||
@agent-native/core APIs, generated apps, workspaces, templates, or advanced
|
||||
features.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Agent Native Docs Lookup
|
||||
|
||||
## Rule
|
||||
|
||||
Before implementing or explaining non-trivial Agent Native behavior, read the
|
||||
version-matched docs installed with `@agent-native/core`. When examples,
|
||||
imports, or implementation details matter, inspect the packaged source corpus
|
||||
too.
|
||||
|
||||
## Why
|
||||
|
||||
Generated apps and workspaces may be on a different framework version than the
|
||||
public docs or model memory. The installed package is the source that matches
|
||||
the app in front of you. It also includes a source-only corpus of core and
|
||||
first-party templates so agents can replicate current best-practice patterns
|
||||
without needing the framework monorepo checkout.
|
||||
|
||||
## How
|
||||
|
||||
From a generated app directory:
|
||||
|
||||
```bash
|
||||
pnpm action docs-search --query "<feature>"
|
||||
pnpm action docs-search --slug <slug>
|
||||
pnpm action docs-search --list
|
||||
pnpm action source-search --query "<pattern>"
|
||||
pnpm action source-search --path templates/plan/AGENTS.md
|
||||
pnpm action source-search --path toolkit/src/index.ts
|
||||
pnpm action source-search --list
|
||||
```
|
||||
|
||||
The headless `pnpm agent` loop and built-in app agent also expose a read-only
|
||||
`docs-search` tool with the same `query`, `slug`, and `list` options, plus a
|
||||
read-only `source-search` tool with `query`, `path`, and `list`.
|
||||
|
||||
If the action runner is unavailable, search the package directly:
|
||||
|
||||
```bash
|
||||
rg -n "actions|automations|a2a|sharing" node_modules/@agent-native/core/docs
|
||||
rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
|
||||
```
|
||||
|
||||
Then read `node_modules/@agent-native/core/docs/AGENTS.md` or the matching file
|
||||
under `node_modules/@agent-native/core/docs/content/`. For source examples,
|
||||
read files under `node_modules/@agent-native/core/corpus/core/` or
|
||||
`node_modules/@agent-native/core/corpus/templates/`.
|
||||
|
||||
Toolkit source is searchable at `toolkit/` in the Core corpus and also ships as
|
||||
readable TypeScript under `node_modules/@agent-native/toolkit/src/`. Read
|
||||
`customizing-agent-native` before taking ownership of a shared component: inspect package
|
||||
source as a read-only reference, then configure, compose, or eject the smallest
|
||||
supported unit into app-owned source. Preserve public actions, application
|
||||
state, auth, and agent-chat runtime contracts. Never edit `node_modules` or
|
||||
deep-import its private source. Manual copying is only the fallback described by
|
||||
an unknown third-party package's add-style blueprint.
|
||||
|
||||
## Reuse Proven Patterns (rg + cp)
|
||||
|
||||
Version-matched installed source outranks web docs or memory: it is the exact
|
||||
code shipping with this app. `node_modules/@agent-native/core/corpus/templates/`
|
||||
holds source for every first-party template, not just the one this app started
|
||||
from, so a pattern from the mail template is fair game for a tasks app. Grep
|
||||
across it, then copy a whole file as a starting point instead of writing the
|
||||
pattern from scratch:
|
||||
|
||||
```bash
|
||||
# Find how other templates solved a similar problem
|
||||
rg -n "drag.*drop|reorder" node_modules/@agent-native/core/corpus/templates
|
||||
|
||||
# Grab a proven action file as a starting point, then adapt names/schema
|
||||
cp node_modules/@agent-native/core/corpus/templates/mail/actions/archive-email.ts \
|
||||
actions/archive-item.ts
|
||||
|
||||
# Read the full framework source behind an API, not just the corpus copy
|
||||
rg -n "defineAction" node_modules/@agent-native/core/src/action.ts
|
||||
```
|
||||
|
||||
Copying template-level app code (actions, components, skill files) is the
|
||||
expected reuse path — templates exist to be forked. This is different from
|
||||
copying `core`/`toolkit` **runtime internals**: for those, follow
|
||||
`customizing-agent-native`'s configure/compose/eject ladder instead of
|
||||
hand-duplicating framework logic.
|
||||
|
||||
## Useful Slugs
|
||||
|
||||
| Need | Slugs |
|
||||
| ------------------------------ | ------------------------------------------------------------- |
|
||||
| Actions and typed client calls | `actions`, `client` |
|
||||
| SQL, auth, access, sharing | `database`, `authentication`, `security`, `sharing` |
|
||||
| UI state visible to the agent | `context-awareness` |
|
||||
| Headless and chat-first apps | `pure-agent-apps`, `agent-surfaces`, `using-your-agent` |
|
||||
| Automations and schedules | `automations`, `recurring-jobs` |
|
||||
| Cross-app and external agents | `a2a-protocol`, `external-agents`, `mcp-protocol`, `mcp-apps` |
|
||||
| Skills and instructions | `skills-guide`, `writing-agent-instructions` |
|
||||
|
||||
## Don't
|
||||
|
||||
- Do not rely on memory for framework APIs when package docs are present.
|
||||
- Do not add custom REST wrappers for app data before reading `actions`.
|
||||
- Do not add inline LLM calls before reading `using-your-agent` and
|
||||
`agent-surfaces`.
|
||||
- Do not copy framework runtime internals when a public API or narrow UI copy
|
||||
will do; read `customizing-agent-native` for the supported override ladder.
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: agent-native-toolkit
|
||||
description: >-
|
||||
Inventory and ownership rules for shared Agent Native workspace UI. Use
|
||||
before building app chrome, settings, navigation, sharing, collaboration,
|
||||
setup, history, comments, chat rails, agent UX, or repeated workspace behavior.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Agent-Native Toolkit
|
||||
|
||||
Use this skill when deciding whether app chrome, settings, collaboration,
|
||||
sharing, navigation, organization, setup, history, comments, or agent UX should
|
||||
be built app-locally or moved into reusable framework/toolkit pieces.
|
||||
|
||||
## Core Rule
|
||||
|
||||
Apps own domain models, domain actions, and product-specific workflows. The
|
||||
framework and `@agent-native/toolkit` own repeated workspace behavior users
|
||||
expect to work the same everywhere.
|
||||
|
||||
Move behavior into shared toolkit primitives when it is:
|
||||
|
||||
- workspace-wide, such as settings, nav, search, org membership, or setup
|
||||
- agent-visible, such as context, actions, run progress, or proof-of-done
|
||||
- governed, such as secrets, permissions, sharing, audit, or billing
|
||||
- repeated by two or more apps
|
||||
- not tied to one domain model
|
||||
|
||||
Keep behavior app-local when the abstraction would hide important domain
|
||||
language or make a simple app-specific workflow harder to understand.
|
||||
|
||||
## Discover Before Building
|
||||
|
||||
Before creating an app-local version of repeated workspace or agent UI:
|
||||
|
||||
1. Check the reusable kits below and the installed package documentation.
|
||||
2. Search installed public components and source with `docs-search` and
|
||||
`source-search`.
|
||||
3. Run `agent-native eject --list` to see the version-matched units published
|
||||
by the packages installed in this app.
|
||||
4. Read `customizing-agent-native` and configure, compose, or eject the
|
||||
smallest unit instead of recreating shared behavior from memory.
|
||||
|
||||
Use public package exports at runtime. Published source and ejection manifests
|
||||
are discovery and ownership-transfer mechanisms, not private runtime APIs.
|
||||
|
||||
## Design-System Boundary
|
||||
|
||||
Every app keeps an explicit design-system seam in `app/design-system.ts` using
|
||||
`defineDesignSystem` from `@agent-native/toolkit/design-system`, and supplies it
|
||||
to `ToolkitProvider`. The semantic contract contains:
|
||||
|
||||
- nine leaf components: `ActionButton`, `IconButton`, `TextField`, `TextArea`,
|
||||
`Spinner`, `Skeleton`, `Status`, `Surface`, and `Avatar`
|
||||
- eight behavior components: `Tooltip`, `Menu`, `Popover`, `Dialog`, `Picker`,
|
||||
`Checkbox`, `Switch`, and `Tabs`
|
||||
|
||||
These are semantic contracts, not styling contracts. An adapter may use
|
||||
Tailwind/shadcn, MUI-style theme providers, React Aria, CSS modules, CSS-in-JS,
|
||||
or another React design system. Do not assume CVA, utility classes, or even a
|
||||
`className`; behavior adapters may supply their overlay and focus
|
||||
implementation wholesale while honoring portal, focus-restoration, keyboard,
|
||||
dismissal, ARIA, and z-index interoperability.
|
||||
|
||||
Pages, routes, and domain components import ordinary controls through the app's
|
||||
local adapter layer, usually `@/components/ui/*`. They must not import
|
||||
`@agent-native/toolkit/ui/*` directly. Toolkit feature exports are still the
|
||||
right home for shared workspace behavior; their presentation flows through the
|
||||
registered semantic components, feature controller, and product-level slots.
|
||||
|
||||
Customer adapter packages are normal npm packages imported explicitly by the
|
||||
app. Never auto-detect them or load React components from JSON. Run the adapter
|
||||
against `@agent-native/toolkit/conformance` in customer CI before adopting it.
|
||||
|
||||
## Settings Direction
|
||||
|
||||
Durable settings belong in the Settings app or a registered settings route. The
|
||||
agent sidebar should not become a second settings app. It can show contextual
|
||||
quick controls and deep links such as:
|
||||
|
||||
- `/settings/ai`
|
||||
- `/settings/connections`
|
||||
- `/settings/secrets`
|
||||
- `/settings/usage`
|
||||
- `/settings/apps/:appId`
|
||||
|
||||
The shared Account section is the canonical profile surface at
|
||||
`/settings#account`. It owns the editable display name and existing avatar
|
||||
control through the authenticated `get-user-profile` and `update-user-profile`
|
||||
actions. Shared workspace chrome such as `OrgSwitcher` should link to this
|
||||
surface rather than creating an app-local profile page.
|
||||
|
||||
When adding a new API key, OAuth grant, provider connection, model selector, app
|
||||
preference, notification preference, or usage/billing surface, register it as a
|
||||
settings tab or app settings panel first. Only add sidebar UI when it is needed
|
||||
in the moment of agent use.
|
||||
|
||||
## Reusable Kits
|
||||
|
||||
- **Settings kit**: a searchable settings page with account, workspace, AI
|
||||
models, LLM keys, connections, secrets, usage, notifications, changelog, and
|
||||
app-specific panels. Search is on by default; register a `SettingsSearchEntry`
|
||||
per control so users find settings by name across tabs.
|
||||
- **Collaboration kit**: Yjs docs, presence, agent presence, live cursors,
|
||||
remote selections, recent edit highlights, real-time sync indicators, and
|
||||
undo/redo grouping.
|
||||
- **Sharing kit**: private/workspace/org/public-link access, invites, roles,
|
||||
expirations, agent-readable links, and resource registration.
|
||||
- **Navigation and command kit**: app shell, side nav, breadcrumbs, app switcher,
|
||||
command palette entries, recent resources, pinned resources, and global search.
|
||||
- **Organization kit**: folders, tags, favorites, archive, trash, ownership,
|
||||
membership, and common resource metadata.
|
||||
- **Setup and connections kit**: declarative setup requirements, model readiness,
|
||||
missing-secret states, OAuth grants, and provider connection health.
|
||||
- **Agent UX kit**: sidebar, composer, staged context, mentions, voice, human
|
||||
approval, generative UI, progress, and screen-state exposure.
|
||||
- **Chat history kit**: presentational chat lists and recent-chat rails belong
|
||||
in Toolkit; Core keeps thread persistence, agent execution, transport, and
|
||||
page-to-sidebar handoff. Use Toolkit's `ChatHistoryRail` for the standard
|
||||
five-item sidebar preview and a footer row with New chat followed by an
|
||||
ellipsis disclosure up to fifteen. Apps inject routing, labels, and domain
|
||||
actions.
|
||||
- **Agent page kit**: the full-page `/agent` surface (`AgentTabsPage` from
|
||||
`@agent-native/core/client`) with Context, Files, Connections, Jobs, and
|
||||
Access tabs plus a Personal/Organization scope toggle. The canonical home
|
||||
for context transparency, MCP servers, A2A remote agents, recurring
|
||||
jobs/automations, and external-client connect flows. See the `agent-page`
|
||||
skill.
|
||||
- **History and recovery kit**: audit log, activity feed, version history,
|
||||
checkpoints, undo, redo, restore, and proof-of-done.
|
||||
- **Comments and review kit**: anchored comments, pins, mentions, review
|
||||
requests, resolved threads, agent follow-up tasks, and notifications.
|
||||
- **Workflow and observability kit**: notifications, approvals, scheduled work,
|
||||
background runs, recurring jobs, traces, evals, feedback, and run timelines.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
When adding or refactoring one of these areas:
|
||||
|
||||
1. Search existing framework and template code for duplicated UI or actions.
|
||||
2. Decide the shared contract: data shape, action API, feature-level headless
|
||||
controller, default view, semantic components, and product-level render
|
||||
slots.
|
||||
3. Keep shared data provider-agnostic and scoped by auth/sharing rules.
|
||||
4. Expose the same capability to the UI and agent through actions or documented
|
||||
client helpers.
|
||||
5. Register app-specific labels, routes, resource adapters, and settings panels
|
||||
instead of hardcoding app names in core UI.
|
||||
6. Update docs and relevant skills so future apps discover the shared path.
|
||||
7. Keep the component easy to adopt piecemeal: expose props/slots first and
|
||||
ship readable source plus a complete ejection unit so apps can take ownership
|
||||
of the smallest feature when needed. See `customizing-agent-native` for the
|
||||
configure → compose → eject → propose seam ladder.
|
||||
8. Keep one controller as the source of truth for the default and custom render
|
||||
paths. A custom design must not fork actions, analytics, async state, or
|
||||
accessibility behavior.
|
||||
9. Verify the default adapter and at least one non-Tailwind adapter with the
|
||||
conformance kit, including focus and portal stacking across mixed overlay
|
||||
implementations.
|
||||
|
||||
## Related Skills
|
||||
|
||||
Read these alongside this skill when the work touches the specific area:
|
||||
|
||||
- `sharing`
|
||||
- `real-time-collab`
|
||||
- `real-time-sync`
|
||||
- `client-side-routing`
|
||||
- `context-awareness`
|
||||
- `onboarding`
|
||||
- `secrets`
|
||||
- `audit-log`
|
||||
- `observability`
|
||||
- `frontend-design`
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: capture-learnings
|
||||
description: >-
|
||||
Capture and apply accumulated knowledge via structured memory. Use when the
|
||||
user gives feedback, shares preferences, corrects a mistake, or when you
|
||||
discover something worth remembering for future conversations.
|
||||
user-invocable: false
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Capture Learnings
|
||||
|
||||
This is background knowledge, not a slash command. **Organization learnings and your personal memory index are loaded at the start of every conversation, including Slack/integration turns.** Capture durable context proactively when you learn something worth remembering.
|
||||
|
||||
## How to Read & Write Memories
|
||||
|
||||
Memories are stored as **resources** in the SQL database, not as files on disk.
|
||||
|
||||
- **Team/organization knowledge:** shared `LEARNINGS.md`. Read it first, merge the new fact, then write the full updated file with the `resources` tool using `scope: "shared"`. Shared scope resolves to the active organization; it must never leak to another organization in the same deployment.
|
||||
- **Personal knowledge:** `memory/MEMORY.md` plus `memory/<name>.md`, written with `save-memory`.
|
||||
|
||||
- **Save a memory:** `save-memory --name <name> --type <type> --description "..." --content "..."`
|
||||
- **Read a memory:** `resource-read --path memory/<name>.md`
|
||||
- **Delete a memory:** `delete-memory --name <name>`
|
||||
- **List all memories:** `resource-list --prefix memory/`
|
||||
|
||||
## Memory Types
|
||||
|
||||
| Type | Use for |
|
||||
|------|---------|
|
||||
| `user` | Preferences, role, personal context, contacts |
|
||||
| `feedback` | Corrections, confirmed approaches, things to avoid or repeat |
|
||||
| `project` | Ongoing work context, decisions, deadlines, status |
|
||||
| `reference` | Pointers to external systems, URLs, API details |
|
||||
|
||||
## When to Capture
|
||||
|
||||
### Team and organization knowledge (`LEARNINGS.md`, shared scope)
|
||||
- Canonical destinations and workflows (for example, which Content database receives a type of Slack request)
|
||||
- Required intake fields, ownership, prioritization conventions, metric definitions, and approved terminology
|
||||
- Durable external references that the whole team needs, including the canonical page/database URL
|
||||
- Corrections or decisions that should affect every organization member's future Slack and app conversations
|
||||
|
||||
Store the fact and a concise provenance link when available. Do not paste full private conversations, customer data, credentials, or secrets into learnings. Put stable always-on policy in shared `AGENTS.md`; put learned facts and evolving conventions in shared `LEARNINGS.md`.
|
||||
|
||||
### User Preferences & Memory (`user`)
|
||||
- **Tone and style** — "I prefer casual tone", "don't use emojis", "keep replies short"
|
||||
- **Personal context** — contacts, relationships, habits ("my wife's email is...", "I'm in PST timezone")
|
||||
- **Workflow preferences** — "always CC my assistant", "I like to review before sending"
|
||||
- **Role and expertise** — "I'm a data scientist", "new to React"
|
||||
|
||||
### Feedback & Corrections (`feedback`)
|
||||
- **Corrections** — user says "no, do it this way" → capture the right way
|
||||
- **Confirmed approaches** — user validates a non-obvious choice ("yes, that's perfect")
|
||||
- **Repeated friction** — you hit the same issue twice; save it
|
||||
|
||||
### Project Context (`project`)
|
||||
- **Ongoing work** — who is doing what, why, by when
|
||||
- **Decisions** — why something is done a certain way
|
||||
- **Status** — current state of initiatives
|
||||
|
||||
### References (`reference`)
|
||||
- **External systems** — "bugs are tracked in Linear project INGEST"
|
||||
- **URLs** — dashboards, documentation, tools
|
||||
- **API quirks** — undocumented behavior, version-specific gotchas
|
||||
|
||||
### Don't Capture
|
||||
- Things obvious from reading the code
|
||||
- Standard language/framework behavior
|
||||
- Temporary debugging notes
|
||||
- Anything already in AGENTS.md, shared LEARNINGS.md, or skills
|
||||
- Ephemeral task details (use tasks/plans instead)
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Save proactively — don't ask permission.** When you learn something durable, save it immediately at the correct scope.
|
||||
2. **Choose scope by audience.** Organization workflow or reference → shared `LEARNINGS.md`; one person's preference/context → personal memory.
|
||||
3. **One memory per topic** — e.g. `coding-style`, `project-alpha`, not one giant dump
|
||||
4. **Read before updating** — if a memory exists, read it first and merge, don't overwrite
|
||||
5. **Keep descriptions concise** — the index is loaded every conversation
|
||||
6. **Memories are SQL-backed** — they persist across sessions and are not in git; still minimize sensitive content
|
||||
|
||||
## Graduation
|
||||
|
||||
When a memory is referenced repeatedly, it may belong in AGENTS.md or a skill:
|
||||
- Saving a personal memory is lightweight (auto-apply, personal scope)
|
||||
- Shared LEARNINGS.md is the lightweight organization knowledge layer
|
||||
- Updating AGENTS.md or a skill is heavier (affects all users/agents)
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
name: create-skill
|
||||
description: >-
|
||||
How to create new skills for an agent-native app. Use when adding a new
|
||||
skill, documenting a pattern the agent should follow, or creating reusable
|
||||
guidance for the agent.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Create a Skill
|
||||
|
||||
## When to Use
|
||||
|
||||
Create a new skill when:
|
||||
|
||||
- There's a pattern the agent should follow repeatedly.
|
||||
- A multi-step workflow needs reliable, step-by-step guidance.
|
||||
- You want to scaffold files from a template.
|
||||
|
||||
Don't create a skill when:
|
||||
|
||||
- The guidance already exists in another skill — extend it instead.
|
||||
- You're documenting something the agent already knows (e.g., how to write
|
||||
TypeScript).
|
||||
- It's a one-off — put it in `AGENTS.md` (for everyone) or `memory/MEMORY.md`
|
||||
(personal, per-user). See **capture-learnings**.
|
||||
|
||||
## Interview
|
||||
|
||||
Before writing the skill, answer these:
|
||||
|
||||
1. **What should this skill enable?** — The core purpose in one sentence.
|
||||
2. **Which of the four areas does it serve?** — UI, actions, skills/instructions,
|
||||
or application state (see the **adding-a-feature** skill). Most skills are
|
||||
about how to touch one or more of these correctly.
|
||||
3. **When should it trigger?** — Describe the situations in natural language.
|
||||
Be slightly pushy — over-triggering is better than under-triggering.
|
||||
4. **Does it involve context awareness?** — Does the agent need to know what the
|
||||
user is looking at? If so, reference the `navigation` application-state key and
|
||||
the `view-screen` action pattern. See the **context-awareness** skill.
|
||||
5. **What type of skill?** — Pattern, Workflow, or Generator (see below).
|
||||
6. **Does it need supporting files?** — References (read-only context) or none.
|
||||
Keep it minimal; push depth into `references/`.
|
||||
|
||||
## Skill Types and Templates
|
||||
|
||||
### Pattern (architectural rule)
|
||||
|
||||
For documenting how things should be done:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-pattern
|
||||
description: >-
|
||||
[Under 40 words. What it covers AND when it should trigger.]
|
||||
---
|
||||
|
||||
# [Pattern Name]
|
||||
|
||||
## Rule
|
||||
|
||||
[One sentence: what must be true]
|
||||
|
||||
## Why
|
||||
|
||||
[Why this rule exists]
|
||||
|
||||
## How
|
||||
|
||||
[How to follow it, with code examples]
|
||||
|
||||
## Don't
|
||||
|
||||
[Common violations]
|
||||
|
||||
## Related Skills
|
||||
|
||||
[Which skills compose with this one]
|
||||
```
|
||||
|
||||
### Workflow (step-by-step)
|
||||
|
||||
For multi-step implementation tasks:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-workflow
|
||||
description: >-
|
||||
[Under 40 words. What it covers AND when it should trigger.]
|
||||
---
|
||||
|
||||
# [Workflow Name]
|
||||
|
||||
## Prerequisites
|
||||
|
||||
[What must be in place first]
|
||||
|
||||
## Steps
|
||||
|
||||
[Numbered steps with code examples]
|
||||
|
||||
## Verification
|
||||
|
||||
[How to confirm it worked]
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
[Common issues and fixes]
|
||||
|
||||
## Related Skills
|
||||
```
|
||||
|
||||
### Generator (scaffolding)
|
||||
|
||||
For creating files from templates:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-generator
|
||||
description: >-
|
||||
[Under 40 words. What it covers AND when it should trigger.]
|
||||
---
|
||||
|
||||
# [Generator Name]
|
||||
|
||||
## Usage
|
||||
|
||||
[How to invoke — what args/inputs are needed]
|
||||
|
||||
## What Gets Created
|
||||
|
||||
[List of files and their purpose]
|
||||
|
||||
## Template
|
||||
|
||||
[The template content with placeholders]
|
||||
|
||||
## After Generation
|
||||
|
||||
[What to do next — wire up sync, add routes, register the action, etc.]
|
||||
|
||||
## Related Skills
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Hyphen-case only: `[a-z0-9-]`, max 64 characters.
|
||||
- Pattern skills: descriptive names (`storing-data`, `delegate-to-agent`).
|
||||
- Workflow/generator skills: verb-noun (`create-skill`, `capture-learnings`).
|
||||
- The directory name must match the `name` in frontmatter.
|
||||
|
||||
## Skill Scope (runtime vs dev)
|
||||
|
||||
An optional `scope` frontmatter field controls which agent loads the skill:
|
||||
|
||||
- `both` (default when omitted) — loaded by connected repo agents and the
|
||||
in-app runtime agent. Use for any skill both audiences should follow.
|
||||
- `runtime` — loaded only by the in-app runtime agent.
|
||||
- `dev` — meant for the human's coding agent (e.g. Claude Code) only. **Excluded
|
||||
from the runtime agent everywhere**: not in the system-prompt skills block and
|
||||
not in `docs-search` results.
|
||||
|
||||
Use `scope: dev` for internal-only skills that should guide connected repo
|
||||
agents such as Codex or Claude Code, but should not affect the deployed
|
||||
production agent. Do not use `metadata.internal` for runtime visibility; that
|
||||
field is catalog/package metadata and is intentionally not treated as
|
||||
production exclusion.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: release-checklist
|
||||
description: >-
|
||||
Steps for cutting a release. Use when preparing or publishing a new version.
|
||||
scope: dev
|
||||
---
|
||||
```
|
||||
|
||||
Leave `scope` off for normal skills — the default (`both`) keeps them loading at
|
||||
runtime, so this is fully backward compatible. To make a dev-only skill visible
|
||||
to your coding agent but hidden from the runtime agent, mark it `scope: dev` and
|
||||
optionally mirror it under `.claude/skills/<name>/SKILL.md` (Claude Code reads
|
||||
`.claude/skills/` independently of the runtime's `.agents/skills/`).
|
||||
|
||||
## Tips
|
||||
|
||||
- **Keep descriptions under 40 words** — they load into context on every
|
||||
conversation. State what the skill does AND when to trigger it.
|
||||
- **Keep SKILL.md lean (under ~500 lines)** — move detailed content to
|
||||
`references/` files (progressive disclosure).
|
||||
- **Use standard markdown headings** — no XML tags or custom formats.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- **Inline LLM calls** — skills must not call LLMs directly. All AI work goes
|
||||
through the agent chat (see **delegate-to-agent**).
|
||||
- **Introducing databases** — data lives in SQL via Drizzle (see **storing-data**).
|
||||
- **Ignoring sync** — if a skill creates data, mention wiring `useDbSync` /
|
||||
`useActionQuery` so the UI updates (see **real-time-sync**).
|
||||
- **Vague descriptions** — "Helps with development" won't trigger. Be specific
|
||||
about _when_.
|
||||
- **Pure documentation** — skills should guide action, not just explain concepts.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
.agents/skills/my-skill/
|
||||
├── SKILL.md # Main skill (required)
|
||||
└── references/ # Optional supporting context
|
||||
└── detailed-guide.md
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **adding-a-feature** — The four-area model every skill ultimately serves.
|
||||
- **writing-agent-instructions** — How to write AGENTS.md and skills well for
|
||||
apps and templates you ship to others.
|
||||
- **capture-learnings** — When a learning graduates to reusable guidance, create
|
||||
a skill; one-offs go to `AGENTS.md` or `memory/MEMORY.md`.
|
||||
- **self-modifying-code** — The agent can create new skills (Tier 2 modification).
|
||||
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: customizing-agent-native
|
||||
description: >-
|
||||
How to configure, compose, or eject Agent Native features into app-owned
|
||||
code. Use when overriding shared components or integrations, customizing a
|
||||
template, adding UI to chat or headless apps, or inspecting package source.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Customizing Agent Native
|
||||
|
||||
## Rule
|
||||
|
||||
Start with the app's registered design system and local UI adapters, then use
|
||||
the public feature's props, semantic components, controller, product slots,
|
||||
and callbacks. If those seams are not enough, use the eject CLI to transfer
|
||||
the smallest supported feature into the app and make that copy app-owned.
|
||||
Never edit `node_modules`, deep-import a private source file at runtime, or
|
||||
patch an `@agent-native/*` package.
|
||||
|
||||
Use this order:
|
||||
|
||||
1. Set brand tokens with the build-time theme configuration when tokens are
|
||||
enough.
|
||||
2. Register company components in `app/design-system.ts` with
|
||||
`defineDesignSystem`, then pass that definition to `ToolkitProvider`.
|
||||
3. Configure a public feature through its semantic components, headless
|
||||
controller, and product-level render slots.
|
||||
4. Compose app primitives behind the app's local UI adapter layer.
|
||||
5. Eject the smallest feature into app-owned source.
|
||||
6. Propose a new Toolkit seam when the same override is useful in two apps.
|
||||
|
||||
Ejection is for intentional product customization, not for hiding an upgrade
|
||||
failure or replacing Core runtime behavior.
|
||||
|
||||
## Use A Company Design System
|
||||
|
||||
Keep the explicit, typed registration seam in `app/design-system.ts`:
|
||||
|
||||
```tsx
|
||||
import { defineDesignSystem } from "@agent-native/toolkit/design-system";
|
||||
import {
|
||||
AcmeActionButtonAdapter,
|
||||
AcmeDialogAdapter,
|
||||
} from "./design-system/acme-adapter";
|
||||
|
||||
export const designSystem = defineDesignSystem({
|
||||
name: "Acme",
|
||||
components: {
|
||||
ActionButton: AcmeActionButtonAdapter,
|
||||
Dialog: AcmeDialogAdapter,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Adapters translate the semantic Toolkit contract into company component props.
|
||||
They are ordinary React components and may use MUI-style providers, React Aria,
|
||||
CSS modules, CSS-in-JS, or another styling runtime. `className` and `style` are
|
||||
optional interoperability hooks, not Tailwind or CVA requirements.
|
||||
|
||||
The contract has nine leaf components (`ActionButton`, `IconButton`,
|
||||
`TextField`, `TextArea`, `Spinner`, `Skeleton`, `Status`, `Surface`, `Avatar`)
|
||||
and eight behavior components (`Tooltip`, `Menu`, `Popover`, `Dialog`,
|
||||
`Picker`, `Checkbox`, `Switch`, `Tabs`). `Picker` covers select and combobox
|
||||
behavior, not date picking. Behavior adapters own their portal, focus,
|
||||
keyboard, dismissal, and stacking implementation while honoring the semantic
|
||||
props and `portalContainer` interop contract.
|
||||
|
||||
App product code imports standard controls from its local adapter path, usually
|
||||
`@/components/ui/*`. Do not import `@agent-native/toolkit/ui/*` directly from
|
||||
pages, routes, or domain components. That bypasses the app seam and makes a
|
||||
future design-system replacement incomplete. Toolkit feature packages remain
|
||||
valid imports; configure their presentation through the registered semantic
|
||||
components, controller, and product slots.
|
||||
|
||||
Run the published conformance kit against a complete adapter in its own CI:
|
||||
|
||||
```tsx
|
||||
import { assertDesignSystemConformance } from "@agent-native/toolkit/conformance";
|
||||
import { DESIGN_SYSTEM_CONTRACT_VERSION } from "@agent-native/toolkit/design-system";
|
||||
|
||||
await assertDesignSystemConformance({
|
||||
adapterName: "Acme",
|
||||
components,
|
||||
contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
|
||||
});
|
||||
```
|
||||
|
||||
New components and optional props are minor contract changes. Required props,
|
||||
removed APIs, or behavioral changes require a new contract major.
|
||||
|
||||
## Customize A Shared Feature
|
||||
|
||||
Prefer feature-level headless controllers over rebuilding individual widgets.
|
||||
One controller must power both the Toolkit default view and every custom render
|
||||
path so behavior, actions, analytics, accessibility state, and error handling
|
||||
cannot drift. Use a product-level render slot to replace the view while keeping
|
||||
that controller. Eject only when the controller and slots cannot express the
|
||||
required product behavior.
|
||||
|
||||
## Eject A Feature
|
||||
|
||||
Discover and inspect the ejection units published by installed first-party
|
||||
packages before changing source:
|
||||
|
||||
```bash
|
||||
agent-native eject --list
|
||||
agent-native eject inspect <unit>
|
||||
agent-native eject <unit> --app <app>
|
||||
agent-native eject <unit> --app <app> --apply
|
||||
```
|
||||
|
||||
The command is dry-run by default. It prints the file closure, consumer import
|
||||
rewrites, protected package contracts, and verification commands before
|
||||
writing. `--apply` copies the package-version-matched source into the app and
|
||||
rewrites only imports covered by the unit manifest.
|
||||
|
||||
Every first-party public ejection unit must have a complete manifest. If one is
|
||||
missing, treat that as a framework coverage bug instead of inventing a copy
|
||||
recipe. For an unknown third-party package, use the emitted add-style blueprint
|
||||
as a starting point. Protected runtime behavior is never copied; follow the
|
||||
reported configuration, adapter, or extension seam instead.
|
||||
|
||||
Applied ejections are recorded in the committed
|
||||
`agent-native.ejections.json`, including package version, manifest digest,
|
||||
target hashes, and import rewrites. Use the recorded state to review drift or
|
||||
undo an unchanged ejection:
|
||||
|
||||
```bash
|
||||
agent-native eject diff <unit> --app <app>
|
||||
agent-native eject restore <unit> --app <app>
|
||||
agent-native eject restore <unit> --app <app> --apply
|
||||
```
|
||||
|
||||
Restore is hash-gated. It refuses to remove locally edited files or reverse
|
||||
changed imports and prints their diff instead. Keep an edited ejection as
|
||||
app-owned code, or reconcile those edits before restoring it.
|
||||
|
||||
## Find The Installed Implementation
|
||||
|
||||
Use the source that matches the installed package version:
|
||||
|
||||
```bash
|
||||
pnpm action docs-search --query "<component or feature>"
|
||||
pnpm action source-search --query "<component or symbol>"
|
||||
rg -n "<component or symbol>" node_modules/@agent-native/toolkit/src
|
||||
rg -n "<component or symbol>" node_modules/@agent-native/core/corpus
|
||||
```
|
||||
|
||||
- Toolkit publishes readable TypeScript under
|
||||
`node_modules/@agent-native/toolkit/src/` for selective UI adoption.
|
||||
- Core and first-party template source lives under
|
||||
`node_modules/@agent-native/core/corpus/core/` and
|
||||
`node_modules/@agent-native/core/corpus/templates/`.
|
||||
- Treat those trees as read-only references. Prefer `agent-native eject` so the
|
||||
package manifest selects the complete source closure and rewrites imports.
|
||||
Manual inspection is still useful for deciding whether to configure,
|
||||
compose, eject, or propose a shared seam.
|
||||
|
||||
Do not manually guess at sibling dependencies. The ejection manifest owns the
|
||||
required file closure and keeps protected contracts on public package imports.
|
||||
|
||||
## Preserve The Agent-Native Contract
|
||||
|
||||
UI ownership may change; product contracts should not:
|
||||
|
||||
- Keep app operations in `defineAction` actions and call them through
|
||||
`useActionQuery`, `useActionMutation`, or another named client helper.
|
||||
- Keep navigation, selection, and focused-object state visible through the
|
||||
existing application-state keys.
|
||||
- Keep AI work in the shared agent chat instead of adding direct LLM calls.
|
||||
- Keep auth, access checks, persistence, chat transport, and agent execution in
|
||||
Core. Do not copy those runtimes into the app.
|
||||
- Keep local adapters narrow so package upgrades still improve every surface
|
||||
the app has not intentionally taken ownership of.
|
||||
- Keep page, route, and domain code on local UI adapter imports. Never reach
|
||||
around the registered design system with direct Toolkit UI primitive imports.
|
||||
|
||||
## App Shapes
|
||||
|
||||
- **Template app:** begin with `app/design-system.ts`, its local UI adapters,
|
||||
and domain UI. Use Toolkit features for repeated workspace behavior; eject
|
||||
only the unit being customized.
|
||||
- **Chat app:** keep `AgentChatSurface`, thread state, and chat transport in
|
||||
Core. Compose or eject Toolkit presentation such as chat-history UI around it.
|
||||
- **Headless app:** stay action-first while no UI is needed. When adding a UI,
|
||||
use the Chat template as the on-ramp or add Toolkit components without
|
||||
replacing the existing actions.
|
||||
- **Workspace:** put one-app overrides in that app. Promote a local component
|
||||
to `packages/shared` only when multiple workspace apps use it.
|
||||
|
||||
## After Ejecting
|
||||
|
||||
- Commit `agent-native.ejections.json` with the app-owned files and rewrites.
|
||||
- Remove unused dependencies and imports from the ejected files.
|
||||
- Keep visible text in the app's localization catalogs.
|
||||
- Run the manifest verification commands plus the app's formatter, typecheck,
|
||||
and focused tests.
|
||||
- Re-check the installed source during future package upgrades; the app-owned
|
||||
ejection does not receive upstream fixes automatically. Use `eject diff` to
|
||||
distinguish recorded output from subsequent local edits.
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't edit or import from `node_modules/@agent-native/*/src` at runtime.
|
||||
- Don't add `pnpm.overrides`, patches, or resolutions for Agent Native packages.
|
||||
- Don't copy Core auth, DB, action, agent-loop, or transport internals.
|
||||
- Don't manually copy a first-party unit with a missing recipe; fix its manifest.
|
||||
- Don't eject a full package when a prop, slot, wrapper, or smaller unit works.
|
||||
- Don't fork feature state into a custom view; consume the feature's controller.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `agent-native-docs` — version-matched docs and source lookup
|
||||
- `agent-native-toolkit` — shared-vs-app-owned architecture boundary
|
||||
- `self-modifying-code` — safe app source edits
|
||||
- `upgrade-agent-native` — supported package upgrade path
|
||||
- `adding-a-feature` — UI/action/instructions/application-state parity
|
||||
@@ -0,0 +1,263 @@
|
||||
---
|
||||
name: delegate-to-agent
|
||||
description: >-
|
||||
How to delegate all AI work to the agent chat. Use when delegating AI work
|
||||
from UI or scripts to the agent, when a user asks for agent behavior or
|
||||
LLM-powered features, when tempted to add inline LLM calls, or when sending
|
||||
messages to the agent from application code.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Delegate All AI to the Agent
|
||||
|
||||
## Rule
|
||||
|
||||
The UI never calls an LLM directly. Product workflows are delegated to the
|
||||
agent through the chat bridge so users can see, steer, and audit the work.
|
||||
Server-side one-shot model calls are an explicit escape hatch for narrow text
|
||||
transforms only; use `completeText()` from `@agent-native/core/server` when the
|
||||
work intentionally does not need tools, chat history, or run state.
|
||||
|
||||
## Why
|
||||
|
||||
The agent is the single AI interface. It has context about the full project, can read/write any file, and can run scripts. Inline LLM calls bypass this — they create a shadow AI that doesn't know what the agent knows and can't coordinate with it.
|
||||
|
||||
## How
|
||||
|
||||
**From the UI (client):**
|
||||
|
||||
```ts
|
||||
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
||||
|
||||
sendToAgentChat({
|
||||
message: "Generate a summary of this document",
|
||||
context: documentContent, // optional hidden context (not shown in chat UI)
|
||||
submit: true, // auto-submit to the agent
|
||||
});
|
||||
```
|
||||
|
||||
**From the UI, in the background:**
|
||||
|
||||
```ts
|
||||
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
||||
|
||||
sendToAgentChat({
|
||||
message: "Analyze this import and create any missing records",
|
||||
context: `Import batch id: ${batchId}`,
|
||||
submit: true,
|
||||
newTab: true,
|
||||
background: true,
|
||||
openSidebar: false,
|
||||
});
|
||||
```
|
||||
|
||||
This is still a full agent run: tools, actions, thread state, and run tracking
|
||||
all remain active. It simply does not focus or open the sidebar.
|
||||
|
||||
**From scripts (Node):**
|
||||
|
||||
```ts
|
||||
import { agentChat } from "@agent-native/core";
|
||||
|
||||
agentChat.submit("Process the uploaded images and create thumbnails");
|
||||
```
|
||||
|
||||
**For narrow server-side text transforms:**
|
||||
|
||||
```ts
|
||||
import { completeText } from "@agent-native/core/server";
|
||||
|
||||
const result = await completeText({
|
||||
systemPrompt: "Return exactly one sentiment label.",
|
||||
input: messageBody,
|
||||
maxOutputTokens: 12,
|
||||
temperature: 0,
|
||||
});
|
||||
```
|
||||
|
||||
Wrap user-facing uses in actions so the UI and agent share the same operation.
|
||||
Do not call provider SDKs directly.
|
||||
|
||||
**From the UI, detecting when agent is done:**
|
||||
|
||||
```ts
|
||||
import { useAgentChatGenerating } from "@agent-native/core/client/agent-chat";
|
||||
|
||||
function MyComponent() {
|
||||
const isGenerating = useAgentChatGenerating();
|
||||
// Show loading state while agent is working
|
||||
}
|
||||
```
|
||||
|
||||
## `submit` vs Prefill
|
||||
|
||||
The `submit` option controls whether the message is sent automatically or placed in the chat input for user review:
|
||||
|
||||
| `submit` value | Behavior | Use when |
|
||||
| -------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `true` | Auto-submits to the agent immediately | Routine operations the user has already approved |
|
||||
| `false` | Prefills the chat input for user review | High-stakes operations (deleting data, modifying code, API calls with side effects) |
|
||||
| omitted | Uses the project's default setting | General-purpose delegation |
|
||||
|
||||
```ts
|
||||
// Auto-submit: routine operation
|
||||
sendToAgentChat({ message: "Update the project summary", submit: true });
|
||||
|
||||
// Prefill: let user review before sending
|
||||
sendToAgentChat({
|
||||
message: "Delete all projects older than 30 days",
|
||||
submit: false,
|
||||
});
|
||||
```
|
||||
|
||||
## Capture user input first when generating from a prompt
|
||||
|
||||
Buttons that produce new content ("New Design", "Create Dashboard", "Make Deck", "Generate Form") need the user's prompt as input. **Never hardcode a generic message** — the result will be a generic generation the user didn't actually ask for.
|
||||
|
||||
**Bad** — auto-submits a placeholder message; the user never said what they wanted:
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
onClick={() =>
|
||||
sendToAgentChat({ message: "make a design", submit: true })
|
||||
}
|
||||
>
|
||||
New Design
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Good** — Popover anchored to the button captures the prompt, then submits it:
|
||||
|
||||
```tsx
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button>New Design</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-96">
|
||||
<Textarea
|
||||
autoFocus
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="What do you want to design?"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
sendToAgentChat({ message: prompt, submit: true });
|
||||
setOpen(false);
|
||||
setPrompt("");
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
**Always ask for input first when** the output depends on a prompt the user must provide — "design what?", "deck about what?", "dashboard for which metric?", "form for which use case?".
|
||||
|
||||
**Auto-submit without input is fine when intent is unambiguous:**
|
||||
|
||||
- "Try to fix" on a tool error — submits the error details with a clear fix instruction
|
||||
- "Retry the last operation" after a transient failure
|
||||
- Single-purpose buttons where there is nothing meaningful for the user to add
|
||||
|
||||
If you find yourself writing `submit: true` with a hardcoded creative verb (`"design a..."`, `"write a..."`, `"build a..."`), stop and add a Popover.
|
||||
|
||||
## Delegating to a Sub-Agent (Agent Teams)
|
||||
|
||||
`sendToAgentChat()` delegates from app code _to_ the agent. The other axis of
|
||||
delegation is the agent handing work _to a sub-agent_ through the Agent Teams
|
||||
run-manager. The main chat stays the orchestrator: it spawns sub-agents, then
|
||||
reads and integrates their results.
|
||||
|
||||
### When to spawn a sub-agent vs do it yourself
|
||||
|
||||
- **Do it yourself** when the work is small, on the critical path, or tightly
|
||||
coupled to what you're already doing. Sub-agent overhead and coordination risk
|
||||
outweigh the benefit.
|
||||
- **Spawn a sub-agent** for a self-contained unit of work that can run
|
||||
independently — a disjoint investigation, an isolated implementation slice, a
|
||||
long-running search — especially when it frees the main thread to keep
|
||||
orchestrating.
|
||||
|
||||
### Briefing contract
|
||||
|
||||
Every sub-agent brief must specify four things, or the sub-agent will guess:
|
||||
|
||||
- **Objective** — the one concrete outcome it owns, in a sentence.
|
||||
- **Context** — the facts it needs (paths, prior findings, constraints) so it
|
||||
doesn't re-derive them.
|
||||
- **Output** — the exact shape you want back (a summary, a file edited, a list
|
||||
of paths, a yes/no with rationale).
|
||||
- **Boundaries** — what it must NOT touch (files, branches, side effects) and
|
||||
when to stop and report rather than push forward.
|
||||
|
||||
### Fan-out discipline
|
||||
|
||||
- **Default to a single sub-agent.** Most delegation is one focused task.
|
||||
- **Spawn multiple only for genuinely independent units** that don't share state
|
||||
or files. Never parallelize coupled work — if B needs A's output, run them in
|
||||
sequence.
|
||||
- **Cap parallel fan-out at ~3.** More sub-agents means more synthesis cost and
|
||||
more chance of conflicting edits to the same area.
|
||||
|
||||
### Synthesis discipline
|
||||
|
||||
- **Read every result** before concluding — don't act on the first one back.
|
||||
- **Reconcile conflicts** between sub-agent findings explicitly; decide which is
|
||||
right rather than averaging or ignoring.
|
||||
- **Integrate into one answer.** The main thread produces the single coherent
|
||||
result; it never just forwards raw sub-agent transcripts to the user.
|
||||
|
||||
Background sub-agents must use the core run-manager / Agent Teams infrastructure
|
||||
rather than ad-hoc LLM calls.
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't `import Anthropic from "@anthropic-ai/sdk"` in client or server code
|
||||
- Don't `import OpenAI from "openai"` in client or server code
|
||||
- Don't make direct API calls to any LLM provider
|
||||
- Don't use AI SDK functions like `generateText()`, `streamText()`, etc.
|
||||
- Don't build "AI features" that bypass the agent chat
|
||||
- Don't auto-submit a hardcoded prompt for generative actions — capture user input first (see above)
|
||||
- Don't use `completeText()` for workflows that need tools, database writes,
|
||||
auditability, user steering, or multi-step reasoning. Use the agent chat
|
||||
instead, optionally with `background: true`.
|
||||
|
||||
## Exception
|
||||
|
||||
Scripts may call external APIs (image generation, search, etc.) — but the AI
|
||||
reasoning and orchestration still goes through the agent. A script is a tool
|
||||
the agent uses, not a replacement for the agent.
|
||||
|
||||
`completeText()` is allowed for small server-side transforms such as
|
||||
classification, extraction, rewriting a short string, or normalizing messy
|
||||
provider text. It deliberately runs with `tools: []` and does not create chat
|
||||
thread state.
|
||||
|
||||
## When to Use A2A Instead
|
||||
|
||||
`sendToAgentChat()` delegates work to the **local** agent — the one running alongside your app. When the work should go to a **different** agent entirely (e.g., asking an analytics agent for data, or a calendar agent for availability), use the A2A (agent-to-agent) protocol instead.
|
||||
|
||||
```ts
|
||||
import { callAgent } from "@agent-native/core/a2a";
|
||||
|
||||
// Call a different agent — not the local agent chat
|
||||
const stats = await callAgent(
|
||||
"https://analytics.example.com",
|
||||
"What were last week's signups?",
|
||||
{ apiKey: process.env.ANALYTICS_A2A_KEY },
|
||||
);
|
||||
```
|
||||
|
||||
See the **a2a-protocol** skill for the full pattern.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **a2a-protocol** — When the work goes to a different agent, not the local one
|
||||
- **actions** — The agent invokes actions via `pnpm action <name>` to perform complex operations
|
||||
- **self-modifying-code** — The agent operates through the chat bridge to make code changes
|
||||
- **storing-data** — The agent writes results to the database after processing requests
|
||||
- **real-time-sync** — The UI updates automatically when the agent writes data
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: feature-flags
|
||||
description: >-
|
||||
Declare, evaluate, manage, and remove framework feature flags. Use when
|
||||
shipping a capability gradually, targeting users or organizations, or
|
||||
replacing a compile-time rollout switch with a production-safe runtime flag.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Feature Flags
|
||||
|
||||
A feature flag is a boolean declared in app code, evaluated locally by Core,
|
||||
and managed from the Analytics fleet control plane. Code owns whether a flag
|
||||
exists. Runtime settings own only its rollout state.
|
||||
|
||||
Flags let an app deploy dormant code and turn it on in the real environment
|
||||
without another deployment. They are not experiments: do not add variants,
|
||||
hypotheses, conversion metrics, exposure tracking, or lifecycle states.
|
||||
|
||||
## When to use one
|
||||
|
||||
Use a flag for a reversible rollout of a user-facing capability whose dormant
|
||||
code is safe to deploy. Flags are useful for production dogfooding, exact-user
|
||||
or organization pilots, and deterministic percentage rollouts.
|
||||
|
||||
Do not use a flag for authentication, authorization, secrets, audit enablement,
|
||||
SSR cache behavior, or another security boundary. Client hiding is presentation
|
||||
only; every guarded server action must evaluate the same registered flag.
|
||||
|
||||
## Agent workflow
|
||||
|
||||
### 1. Declare
|
||||
|
||||
Keep definitions in a shared TypeScript module so server and client code use the
|
||||
same stable key. Flags are boolean and default-off.
|
||||
|
||||
```ts
|
||||
import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
|
||||
|
||||
export const FULL_APP_BUILDING = defineFeatureFlag({
|
||||
key: "full-app-building",
|
||||
displayName: "Full app building",
|
||||
description: "Create and edit Fusion-backed applications.",
|
||||
});
|
||||
```
|
||||
|
||||
Keys are immutable, never reused, and contain only letters, numbers, dots,
|
||||
underscores, or hyphens. Prefer a concise app-owned name. Do not create flag
|
||||
definitions or rollout rows from Analytics.
|
||||
|
||||
### 2. Register
|
||||
|
||||
Register app definitions from a Nitro plugin before actions are discovered.
|
||||
Do not add app-specific flags to a Core registry.
|
||||
|
||||
```ts
|
||||
import { createFeatureFlagsPlugin } from "@agent-native/core/server";
|
||||
|
||||
import { FULL_APP_BUILDING } from "../../shared/feature-flags.js";
|
||||
|
||||
export default createFeatureFlagsPlugin({ flags: [FULL_APP_BUILDING] });
|
||||
```
|
||||
|
||||
### 3. Guard server and client
|
||||
|
||||
The server action is the enforcement boundary:
|
||||
|
||||
```ts
|
||||
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
||||
|
||||
run: async (args, ctx) => {
|
||||
if (!(await isFeatureFlagEnabled(FULL_APP_BUILDING, ctx))) {
|
||||
throw new Error("Full app building is not enabled for this account.");
|
||||
}
|
||||
// guarded operation
|
||||
}
|
||||
```
|
||||
|
||||
Use the client hook only to hide or reveal hydrated UI:
|
||||
|
||||
```tsx
|
||||
import { useFeatureFlag } from "@agent-native/core/client/feature-flags";
|
||||
|
||||
const enabled = useFeatureFlag(FULL_APP_BUILDING.key);
|
||||
return enabled ? <FullAppOption /> : null;
|
||||
```
|
||||
|
||||
The client hook intentionally returns false while loading or for an unknown
|
||||
flag. Never replace that fail-closed behavior with app-local bucketing or a
|
||||
compile-time fallback. Never evaluate personalized flags in the public SSR
|
||||
shell; it is shared and cached for every visitor.
|
||||
|
||||
### 4. Verify and roll out
|
||||
|
||||
1. Verify the off path before changing rollout state.
|
||||
2. Confirm the registered flag appears in **Analytics → Feature flags** for the
|
||||
app and is Off by default.
|
||||
3. Use **Enable for me** for initial production dogfood.
|
||||
4. Expand to exact emails, organization IDs, or a percentage only from
|
||||
Analytics.
|
||||
5. Confirm the client presentation and authoritative server action agree.
|
||||
|
||||
## Management contract
|
||||
|
||||
Core mounts three actions in registered apps:
|
||||
|
||||
| Action | Purpose |
|
||||
| --- | --- |
|
||||
| `get-feature-flags` | Return the current caller's evaluated boolean values. |
|
||||
| `list-feature-flags` | Return definitions and rollout metadata to an authorized operator. |
|
||||
| `set-feature-flag` | Atomically turn a flag off, enable it for the operator, or replace targeting rules. |
|
||||
|
||||
Analytics calls the app-local operator actions through narrowly scoped A2A
|
||||
delegation. Tokens require an exact audience, organization, scope, operator
|
||||
role, and audit correlation id. Management is permission-checked and audited
|
||||
by the target app. Never manage flags through generic settings routes, raw SQL,
|
||||
or per-app toggle UIs.
|
||||
|
||||
## Rollout semantics
|
||||
|
||||
The operator modes are **Off**, **Targeted**, and **Everyone**. Core stores them
|
||||
as `off`, `rules`, and `on`.
|
||||
|
||||
Targeted rules combine exact normalized emails, exact organization IDs, and a
|
||||
percentage with OR semantics. Exact matches are checked first. Percentage
|
||||
buckets use Core's stable hash of the flag key and authenticated user identity;
|
||||
anonymous callers fail closed. Raising a percentage preserves the users already
|
||||
included at a lower percentage. Do not implement bucketing in app code.
|
||||
|
||||
Unknown definitions, missing state, malformed state, storage errors, and
|
||||
evaluation errors all return the code default (`false` in v1). Explicit Off
|
||||
wins over every target; Everyone enables every authenticated caller.
|
||||
|
||||
## Remove a flag
|
||||
|
||||
After a rollout is permanent:
|
||||
|
||||
1. Replace guarded branches with the chosen behavior.
|
||||
2. Delete the server and client gates.
|
||||
3. Delete the definition and registration entry.
|
||||
4. Verify the flag disappears from the Analytics fleet.
|
||||
5. Remove stale tests and rollout instructions.
|
||||
|
||||
A permanent flag is just an if statement with a pension plan.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- Unknown and unregistered keys evaluate false.
|
||||
- UI hiding and server enforcement use the same registered key.
|
||||
- Exact-user, organization, deterministic percentage, Everyone, and Off paths
|
||||
have focused tests.
|
||||
- Increasing a percentage is monotonic; anonymous percentage evaluation is off.
|
||||
- Unauthorized callers cannot list targeting details or mutate flags.
|
||||
- Mutations are atomic, read back stored state, emit refresh, and appear in the
|
||||
audit log with the flag key.
|
||||
- Analytics represents ready, no-definition, unsupported, forbidden, legacy,
|
||||
and unreachable directory apps honestly.
|
||||
- Future agents can find this skill from root `AGENTS.md`, and
|
||||
`pnpm guard:workspace-skills` passes after syncing generated copies.
|
||||
|
||||
## Related skills
|
||||
|
||||
- **adding-a-feature** — preserve UI/action/instruction/application-state parity
|
||||
- **actions** — define and call guarded app operations
|
||||
- **audit-log** — inspect automatic action mutation history
|
||||
- **reliable-mutations** — make rollout changes atomic and provable
|
||||
- **security** — keep security controls out of feature flags
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: >-
|
||||
Create distinctive, production-grade frontend interfaces with high design
|
||||
quality. Use when building web components, pages, artifacts, posters, or
|
||||
applications (websites, landing pages, dashboards, React components,
|
||||
HTML/CSS layouts, or when styling/beautifying any web UI). Generates
|
||||
creative, polished UI that avoids generic AI aesthetics.
|
||||
scope: dev
|
||||
license: Complete terms in LICENSE.txt
|
||||
source: https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Frontend Design
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces. Implement real working code with strong product judgment, excellent accessibility, and a clear visual point of view.
|
||||
|
||||
The user may ask for a component, page, full app, dashboard, marketing surface, or restyle. Before coding, understand the audience and pick a direction that fits the product instead of defaulting to generic SaaS polish.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, decide:
|
||||
|
||||
- **Purpose**: What workflow does this surface make easier? What is the primary action?
|
||||
- **Audience**: Who will use it repeatedly, and what should feel fast, calm, playful, premium, editorial, technical, or utilitarian?
|
||||
- **Tone**: Choose a concrete aesthetic direction: refined minimal, dense operations console, editorial, playful, industrial, warm handmade, high-contrast data tool, etc.
|
||||
- **Information hierarchy**: What must be visible in the first five seconds, and what should be progressively disclosed?
|
||||
- **Differentiation**: What makes this feel designed for this exact domain?
|
||||
|
||||
Then implement working code that is cohesive, accessible, responsive, and polished in small details: typography, spacing, copy, motion, empty states, loading states, focus states, and error states.
|
||||
|
||||
## Minimalism And Progressive Disclosure
|
||||
|
||||
Default to Apple/Linear-level restraint: make the primary workflow obvious, then remove everything that does not help that workflow right now. A polished UI often has fewer visible controls, fewer borders, fewer labels, and fewer explanatory surfaces than the first reasonable implementation.
|
||||
|
||||
### Progressive Disclosure Is A Design Requirement
|
||||
|
||||
Treat an all-at-once interface as a defect to fix during design, not as a
|
||||
styling preference. Before coding, inventory every piece of content and action
|
||||
on the surface, then assign each one to the smallest useful visibility level:
|
||||
|
||||
1. **Immediate** — the page title, current state, one primary action, and the
|
||||
compact context needed to choose what to do next.
|
||||
2. **Expanded** — the details needed for the selected item or active workflow.
|
||||
3. **On demand** — advanced settings, diagnostics, credentials, metadata,
|
||||
destructive actions, documentation, and rarely used tools.
|
||||
|
||||
Use single-select accordions or simple disclosure rows for sibling panels when
|
||||
the user is choosing one item at a time. Inside an expanded panel, keep another
|
||||
layer for independent concerns instead of dumping every form, explanation, and
|
||||
secondary action into the first reveal. Prefer one flat panel with alignment,
|
||||
dividers, and whitespace over nested cards; provider or product icons should
|
||||
not receive decorative borders or containers unless the container communicates
|
||||
state or interaction. A collapsed row should still show the item name, status,
|
||||
and a concise summary so the user can scan the whole surface without opening
|
||||
everything.
|
||||
|
||||
Before shipping, ask: “What can disappear until the user asks for it?” Then
|
||||
verify collapsed, expanded, loading, empty, error, and narrow-width states.
|
||||
If the first viewport contains multiple forms, repeated explanatory copy,
|
||||
documentation links, and controls for unrelated tasks, the surface has not
|
||||
passed this requirement yet.
|
||||
|
||||
- **Start by subtracting**: Before adding a visible control, banner, toolbar row, card, or explanatory block, ask what can be removed, merged, renamed, or moved into an existing affordance.
|
||||
- **One primary action**: Each surface should have one dominant next action. Secondary actions belong in menus, popovers, command palettes, disclosure rows, or contextual hover/focus states unless they are used constantly.
|
||||
- **Progressively disclose rare work**: Advanced options, diagnostics, metadata, settings, import/export, destructive actions, and inspection tools should stay tucked away until requested. Prefer small icon triggers with tooltips, popovers, drawers, or detail panels over permanent chrome.
|
||||
- **Keep chrome quiet**: Avoid new always-visible bars, badges, callouts, helper text, and counters unless they prevent mistakes or are central to repeated use. Status can often be a dot, ring, muted count, or tooltip.
|
||||
- **Favor content over containers**: Do not wrap every section in a card. Use whitespace, alignment, typography, dividers, and full-width bands before adding boxes.
|
||||
- **Design for repeated use**: Production app UI should feel calm after the hundredth use. If a control shouts, animates, explains itself, or occupies a full row for an occasional action, hide or compress it.
|
||||
- **Make absence intentional**: Empty states should be sparse and action-oriented. Do not fill blank space with marketing copy, decorative art, or lists of features just because the screen feels empty.
|
||||
- **Use familiar primitives**: Icon buttons need clear tooltips. Menus, popovers, tabs, switches, and segmented controls should carry complexity instead of exposing every option at once.
|
||||
|
||||
## Aesthetic Guidelines
|
||||
|
||||
- **Typography**: Use the product's existing type system first. For net-new public pages, choose characterful but readable type and keep sizing appropriate to the surface.
|
||||
- **Color and theme**: Use semantic tokens and CSS variables. Avoid one-note palettes and default purple/blue gradients unless the brand demands them.
|
||||
- **Motion**: Prefer purposeful transitions and small state changes. Use CSS transitions/keyframes unless the app already uses a motion library. Never `transition-all` — list the properties that actually change (e.g. `transition-[opacity,transform]`). Use the shared easing tokens defined in `packages/core/src/styles/agent-native.css` instead of hand-typing curves: `var(--ease-drawer)` (260ms, drawers/app chrome), `var(--ease-collapse)` (200ms, expand/collapse), `var(--ease-out-strong)` (snappy entrances) — in Tailwind, `ease-[var(--ease-collapse)]`. Enter/exit with ease-out, never `ease-in`. Overlays that zoom in must set the Radix origin var (e.g. `origin-[--radix-popover-content-transform-origin]`). Animate `transform`/`opacity`, not width/height/padding/box-shadow. Gate looping or large-movement animations with `motion-reduce:`. Command palettes and keyboard-triggered actions get no animation.
|
||||
- **Composition**: Match the workflow. Operational apps should be dense and scannable; marketing or portfolio pages can be more immersive.
|
||||
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
||||
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
||||
|
||||
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
||||
|
||||
## Agent-Native UI Rules
|
||||
|
||||
- Agent-native apps use React and Vite. The default adapter uses Tailwind CSS,
|
||||
shadcn/ui, and `@tabler/icons-react`, but an app may register a different
|
||||
company design system in `app/design-system.ts`.
|
||||
- **Use the app's design-system seam for standard UI.** Inspect
|
||||
`app/design-system.ts`, `ToolkitProvider`, and the local UI adapter directory
|
||||
before choosing a primitive. Use shadcn primitives when they are the active
|
||||
adapter; use the registered company components when they are not.
|
||||
- **When touching shadcn/ui components, also read `shadcn-ui` if it exists.** That skill covers `components.json`, CLI docs, component composition, theming, and registry workflows.
|
||||
- Check `app/components/ui/` before importing a shadcn component. If a primitive is missing, add it from the app root with `pnpm dlx shadcn@latest add <component>`, then review the generated file.
|
||||
- Pages, routes, and domain components must import controls through the app's
|
||||
local adapter path, usually `@/components/ui/*`. Never import
|
||||
`@agent-native/toolkit/ui/*` directly in app product code.
|
||||
- Toolkit/Core feature presentation flows through the semantic components from
|
||||
`@agent-native/toolkit/design-system`. Their props express intent, emphasis,
|
||||
size, controlled values, and behavior; they do not require Tailwind, CVA, or
|
||||
`className`.
|
||||
- For deeper feature customization, consume the feature-level headless
|
||||
controller through its product render slot. The same controller must power
|
||||
the default and custom views. Eject the smallest supported unit only after
|
||||
tokens, semantic components, controllers, and slots are insufficient.
|
||||
- Do not build custom dropdowns, menus, popovers, modals, or confirmations with manual absolute positioning and click-outside effects.
|
||||
- Never use browser dialogs (`window.alert`, `window.confirm`, `window.prompt`). Use `AlertDialog`, `Dialog`, or app-specific confirmation UI.
|
||||
- Use Tabler icons for all first-party UI icons. Do not add Lucide, Heroicons, inline SVG icon sets, or emoji icons.
|
||||
- Use `useActionQuery` and `useActionMutation` from `@agent-native/core/client` for action-backed UI. Standard CRUD should go through actions, not custom `/api/` routes.
|
||||
- Keep UI optimistic where possible: update cache and navigation immediately, then reconcile or roll back on mutation result.
|
||||
- Custom styles belong in Tailwind classes, component CSS, or the existing global CSS theme file; avoid inline styles.
|
||||
|
||||
## shadcn/ui Design Rules
|
||||
|
||||
- Use built-in component variants first (`variant`, `size`) before overriding classes.
|
||||
- Use semantic tokens (`bg-background`, `text-muted-foreground`, `border-border`, `bg-primary`) instead of raw Tailwind colors for app chrome and reusable components.
|
||||
- Use `gap-*` in flex/grid layouts instead of `space-x-*` or `space-y-*`.
|
||||
- Use `size-*` when width and height are equal, and `truncate` instead of spelling out overflow/ellipsis/nowrap.
|
||||
- Use `cn()` from the local utils alias for conditional classes.
|
||||
- Dialog, Sheet, Drawer, and AlertDialog content must have an accessible title. Use `sr-only` only when the visible design already communicates the title.
|
||||
- Put menu/list items inside their group primitives: `SelectGroup`, `DropdownMenuGroup`, `CommandGroup`, and equivalents.
|
||||
- Use full `Card` composition when the content has a title, description, content, or actions. Do not dump complex cards into a single `CardContent`.
|
||||
- Use `ToggleGroup` for small option sets, `Switch` for binary settings, `Checkbox` for multi-select, `RadioGroup` for one-of-many, and `Slider`/inputs for numeric values.
|
||||
- For forms, prefer the app's existing shadcn form pattern. If newer `Field`, `FieldGroup`, or `InputGroup` primitives are installed or appropriate to add, use them instead of raw layout divs.
|
||||
- Loading states use `Skeleton`, `Progress`, `Spinner`, or the app's existing loading primitives. Empty states should have one clear next action.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Avoid:
|
||||
|
||||
- Generic AI aesthetics: purple gradients, glassy cards everywhere, vague sparkle language, decorative blobs, and context-free hero sections.
|
||||
- Custom reimplementations of shadcn primitives.
|
||||
- Raw color overrides on shared components when semantic tokens or variants would work.
|
||||
- New always-visible controls for rare actions. Prefer menus, popovers, sheets, tabs, collapsibles, or advanced sections.
|
||||
- Full-width banners, persistent helper rows, decorative cards, or explanatory chrome for status that could be a compact affordance.
|
||||
- Treating progressive disclosure as optional. If a control is not part of the main daily workflow, hide it until context, hover, focus, or explicit user intent makes it relevant.
|
||||
- UI cards nested inside other cards.
|
||||
- Text or icons that resize or shift fixed-format UI on hover/loading.
|
||||
|
||||
## Verification
|
||||
|
||||
For substantial frontend work:
|
||||
|
||||
1. Run the relevant formatter/checks.
|
||||
2. Start the dev server when the app needs one.
|
||||
3. Verify with browser screenshots at desktop and mobile widths.
|
||||
4. Check interactive states: hover, focus, loading, empty, error, and destructive confirmations.
|
||||
5. When registering or changing a company adapter, run
|
||||
`@agent-native/toolkit/conformance`, including mixed-overlay focus,
|
||||
`portalContainer`, and z-index stacking checks.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **shadcn-ui** — shadcn CLI, component docs, composition rules, theming, and registries
|
||||
- **customizing-agent-native** — Design-system registration, feature controllers, product slots, conformance, and ejection
|
||||
- **self-modifying-code** — The agent can edit source code to apply design changes
|
||||
- **storing-data** — All data lives in SQL; use actions for data access
|
||||
- **actions** — `useActionQuery`/`useActionMutation` hooks for frontend data fetching
|
||||
@@ -0,0 +1,232 @@
|
||||
---
|
||||
name: real-time-sync
|
||||
description: >-
|
||||
How to keep the UI in sync with agent changes via SSE plus polling fallback.
|
||||
Use when wiring query invalidation for new data models, debugging UI not
|
||||
updating, or understanding jitter prevention.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Real-Time Sync
|
||||
|
||||
## Rule
|
||||
|
||||
The UI stays in sync with agent/script changes through `useDbSync()`. In-process writes stream over `/_agent-native/events` first; `/_agent-native/poll` remains the cross-process/serverless fallback. When the agent writes to the database, the UI detects the change and updates automatically — no manual refresh needed.
|
||||
|
||||
## Why
|
||||
|
||||
The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same-process writes immediately; polling bridges anything SSE cannot see, such as another serverless invocation, cron job, or external script. Every visible write increments a version counter, `useDbSync()` receives the change, and React Query invalidates the relevant caches. This is what makes database writes feel real-time without relying on aggressive polling.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Server** increments a version counter on every database write. In-process events stream through the authenticated `/_agent-native/events` endpoint.
|
||||
|
||||
2. **Client** listens for sync events and updates per-source change counters:
|
||||
|
||||
```ts
|
||||
import { useDbSync } from "@agent-native/core/client/hooks";
|
||||
useDbSync({ queryClient });
|
||||
```
|
||||
|
||||
For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active `useActionQuery` observers under the `["action"]` prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with `actionInvalidatePredicate`, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.
|
||||
|
||||
3. **Templates fold per-source counters into their query keys.** This is the pattern that makes "agent writes show up without a manual refresh" reliable:
|
||||
|
||||
```ts
|
||||
import { useChangeVersion } from "@agent-native/core/client/hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const v = useChangeVersion("dashboards");
|
||||
const dashboard = useQuery({
|
||||
queryKey: ["dashboard", id, v],
|
||||
queryFn: () => fetchDashboard(id),
|
||||
placeholderData: (prev) => prev, // no flicker on refetch
|
||||
});
|
||||
```
|
||||
|
||||
When the agent writes (`update-dashboard` action → server emits `source: "dashboards"`), the counter advances, the queryKey changes, and React Query refetches that one query. The old data stays on screen during the refetch thanks to `placeholderData`.
|
||||
|
||||
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
||||
|
||||
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
||||
|
||||
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't create manual polling loops — `useDbSync()` handles SSE plus fallback polling
|
||||
- Don't create your own fetch-based polling alongside `useDbSync` — use the `onEvent` callback for custom handling
|
||||
- Don't open your own `EventSource` to `/_agent-native/events`. A tab must hold exactly ONE SSE connection no matter how many features listen — extra streams eat the browser's per-origin connection budget and can starve ordinary data fetches (worst on HTTP/1.1 dev servers). Subscribe to the shared transport instead:
|
||||
|
||||
```ts
|
||||
import { subscribeSyncEvents } from "@agent-native/core/client/hooks";
|
||||
|
||||
const unsubscribe = subscribeSyncEvents({
|
||||
onEvents: (events) => {
|
||||
// filter by event.source and handle push-style updates
|
||||
},
|
||||
// Optional: relax your own fallback cadence while push is healthy.
|
||||
onSseStateChange: (connected) => {},
|
||||
});
|
||||
```
|
||||
|
||||
`useDbSync` and every `subscribeSyncEvents` subscriber share one `EventSource` and one fallback poll loop per tab — this is how collaborative documents receive doc updates and cursor/awareness events. Hidden tabs keep that shared transport alive by default: SSE stays connected and active fallback polling relaxes to a 10-second floor (idle polling remains once per minute). Pass `pauseWhenHidden: true` only for a consumer that explicitly must stop all hidden-tab sync.
|
||||
|
||||
## Which sources to depend on
|
||||
|
||||
Common sources you'll fold into query keys:
|
||||
|
||||
| Source | Bumped by |
|
||||
| ----------------- | --------------------------------------------------------------------------- |
|
||||
| `action` | The agent runner after every successful mutating action tool call |
|
||||
| `app-state` | Writes to `application_state` (navigation, selections, ephemeral UI state) |
|
||||
| `settings` | Writes to the `settings` table |
|
||||
| `dashboards` | Dashboard CRUD via `upsertDashboard` / `archiveDashboard` etc. |
|
||||
| `analyses` | Analysis CRUD |
|
||||
| `extensions` | Extension CRUD |
|
||||
| `collab` | Yjs collaborative-doc updates |
|
||||
| `screen-refresh` | Explicit `refresh-screen` agent tool call |
|
||||
|
||||
If a query reads data the agent can mutate via more than one path, depend on multiple sources with `useChangeVersions`:
|
||||
|
||||
```ts
|
||||
const v = useChangeVersions(["dashboards", "action"]);
|
||||
useQuery({ queryKey: ["dashboard", id, v], ... });
|
||||
```
|
||||
|
||||
`useChangeVersions` returns a single integer that advances whenever any of the listed sources advance.
|
||||
|
||||
## Tuning refetch behavior
|
||||
|
||||
To prevent cache thrashing during rapid agent writes, set `staleTime` on your queries:
|
||||
|
||||
```ts
|
||||
useQuery({
|
||||
queryKey: ["items"],
|
||||
queryFn: fetchItems,
|
||||
staleTime: 2000, // don't refetch within 2 seconds
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| UI not updating after agent writes | Is `useDbSync` called with the correct `queryClient`? Does the affected query have an active observer? |
|
||||
| Poll endpoint not responding | Is `/_agent-native/poll` accessible? Is the server running? |
|
||||
| SSE not connecting | Is `/_agent-native/events` accessible and authenticated? Polling should still keep the UI fresh as fallback. |
|
||||
| High CPU / event storms | Use targeted source keys, settle bursty list counters, and avoid broad action invalidation. |
|
||||
|
||||
## Jitter Prevention
|
||||
|
||||
When the agent writes to application-state via script helpers (`writeAppState`, `deleteAppState`), the write is automatically tagged with `requestSource: "agent"`. This prevents the UI from overwriting active user edits when it receives the change event.
|
||||
|
||||
### How it works
|
||||
|
||||
1. **Agent writes** are tagged: the script helpers in `@agent-native/core/application-state` pass `{ requestSource: "agent" }` to the store.
|
||||
2. **UI writes** are tagged: templates send a per-tab ID via the `X-Request-Source` header on PUT/DELETE requests to application-state endpoints.
|
||||
3. **Sync filters**: `useDbSync()` accepts an `ignoreSource` option. The UI passes its own tab ID so it ignores events from its own writes — but still picks up events from agents, other tabs, and scripts.
|
||||
|
||||
### Template setup
|
||||
|
||||
```ts
|
||||
// app/lib/tab-id.ts
|
||||
export const TAB_ID = `tab-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// app/root.tsx
|
||||
import { TAB_ID } from "@/lib/tab-id";
|
||||
|
||||
useDbSync({
|
||||
queryClient,
|
||||
ignoreSource: TAB_ID,
|
||||
});
|
||||
```
|
||||
|
||||
The `use-navigation-state.ts` hook sends the same `TAB_ID` in the `X-Request-Source` header when writing navigation state, so the tab that wrote the state does not refetch it.
|
||||
|
||||
### Why this matters
|
||||
|
||||
Without jitter prevention, a cycle occurs: the UI writes state, sync detects the change, the UI refetches and re-renders, potentially overwriting what the user is actively editing. With `ignoreSource`, the UI only reacts to changes from other sources (agent scripts, other browser tabs, other users).
|
||||
|
||||
## Action Routes and Live Sync
|
||||
|
||||
Actions work with the same sync system. When a mutating action writes to the database, the version counter increments and `useDbSync` picks up the change. Frontend mutations via `useActionMutation` automatically invalidate `["action"]` query keys on success, triggering refetches of `useActionQuery` hooks. Client components should call actions through those hooks, not with raw action-route fetches.
|
||||
|
||||
For custom apps, the best out-of-the-box path is:
|
||||
|
||||
1. Put read actions in `actions/` with `defineAction({ http: { method: "GET" } })`.
|
||||
2. Put write actions in `actions/` with the default POST/PUT/DELETE behavior.
|
||||
3. Call reads from React with `useActionQuery` and writes with `useActionMutation`.
|
||||
|
||||
This avoids duplicate `/api/*` JSON CRUD routes and makes agent-created records show up automatically. Raw `useQuery` can still work, but it should include `useChangeVersions(["action", "<domain-source>"])` in the query key for targeted refreshes.
|
||||
|
||||
### Auto-emit on mutating actions
|
||||
|
||||
The framework emits a change event with `source: "action"` whenever any non-read-only action runs to completion — whether called via HTTP (`/_agent-native/actions/:name`) or as an agent tool call. Read-only actions (`http: { method: "GET" }` or explicit `readOnly: true`) are skipped.
|
||||
|
||||
This means UIs don't need the agent to remember to call `refresh-screen` after every mutation. A listener like this (used in the `macros` template) will refresh after any mutating agent call:
|
||||
|
||||
```ts
|
||||
useDbSync({
|
||||
queryClient,
|
||||
ignoreSource: TAB_ID,
|
||||
onEvent: (data) => {
|
||||
if (data.requestSource === TAB_ID) return;
|
||||
// Invalidate all useActionQuery caches so list-*, get-*, etc. refetch
|
||||
queryClient.invalidateQueries({ queryKey: ["action"] });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`refresh-screen` remains available for unusual cases — e.g. the agent mutated data via a path the framework can't see (external system the app mirrors), or the agent wants to pass a `scope` hint for narrower invalidation.
|
||||
|
||||
## Keeping Stateful Components In Sync
|
||||
|
||||
The `useChangeVersion` / `useActionQuery` pattern above keeps the **query layer** fresh. But components that copy a server value into local React state still go stale on agent edits — refetching the query updates the prop, yet the local copy never re-adopts it. This is a recurring bug.
|
||||
|
||||
**Never do this** for a value the agent can mutate:
|
||||
|
||||
```ts
|
||||
// BUG: `title` is captured once and never re-reads the prop.
|
||||
const [title, setTitle] = useState(props.title);
|
||||
```
|
||||
|
||||
When the agent renames the record, the query refetches, `props.title` updates, but the input still shows the stale value until the component remounts.
|
||||
|
||||
**Derived-state surfaces (form fields, inline editors, popovers): use `useReconciledState`.** It re-adopts the authoritative external value when it changes, except while the user is actively editing that field — so agent mutations show up live without clobbering in-progress typing:
|
||||
|
||||
```ts
|
||||
import { useReconciledState } from "@agent-native/core/client/hooks";
|
||||
|
||||
// `active` = true while the user is editing this field (focused / dirty).
|
||||
const [title, setTitle] = useReconciledState(props.title, { active: isEditing });
|
||||
```
|
||||
|
||||
**Collaborative rich-text editors are different** — they don't copy a value into `useState`. They reconcile authoritative SQL content into a shared Y.Doc under an `updatedAt` gate with lead-client election. See `real-time-collab` → "Agent edits as a real-time peer editor". Don't reach for `useReconciledState` for a Yjs-backed editor.
|
||||
|
||||
| Surface | Keep it fresh with |
|
||||
| ------- | ------------------ |
|
||||
| React Query reads | `useChangeVersion` / `useActionQuery` (above) |
|
||||
| Local edit state copied from a server value (inputs, popovers, inline editors) | `useReconciledState(externalValue, { active })` |
|
||||
| Collaborative rich-text editor (Yjs) | `updatedAt`-gated reconcile + `isReconcileLeadClient` — see `real-time-collab` |
|
||||
|
||||
## Granular server-side merge for non-body fields
|
||||
|
||||
For structured documents (slide decks, form builders, design files) where the
|
||||
Yjs body collab would cause LWW conflicts at the container level, pair the
|
||||
change-sync `updatedAt` bump with a **granular server-side merge action** that
|
||||
accepts targeted per-item operations (add/patch/delete/reorder). Concurrent
|
||||
edits to different items both survive at the action level; the `collab` source
|
||||
version bump then propagates the merged state to all open clients. See
|
||||
`real-time-collab` for the pattern and examples.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **storing-data** — Application-state and settings are data stores that sync through change events
|
||||
- **context-awareness** — Navigation state writes use jitter prevention to avoid overwriting active edits
|
||||
- **actions** — Mutating actions trigger change events
|
||||
- **client-methods** — Route details belong in helpers/hooks, not components
|
||||
- **self-modifying-code** — Agent code edits trigger change events; rapid edits can cause event storms
|
||||
- **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump; also the granular server-side merge pattern for structured data
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
name: security
|
||||
description: >-
|
||||
Secure coding practices for agent-native apps: input validation, SQL
|
||||
injection, XSS, secrets, data scoping, and auth. Use when writing any action,
|
||||
route, or component that touches user data or external input.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
## Rule
|
||||
|
||||
Use the framework's security primitives everywhere. Never bypass them.
|
||||
|
||||
## Absolute Secrets Rule
|
||||
|
||||
Never hardcode secret values or real private data. This applies to source code,
|
||||
docs, tests, fixtures, generated prompts, screenshots, seed data, and extension
|
||||
HTML just as much as production code.
|
||||
|
||||
Do not paste or invent real-looking API keys, bearer tokens, OAuth refresh
|
||||
tokens, webhook URLs, signing secrets, private Builder/internal data, or customer
|
||||
data into the repo. Examples must use obvious placeholders such as
|
||||
`<OPENAI_API_KEY>`, `${keys.SLACK_WEBHOOK}`, `sk-test-example`, or
|
||||
`example.customer@example.com`. Test literals should be clearly fake and must
|
||||
not match real provider token formats when an `example` token will do.
|
||||
|
||||
Credential values enter the system only through approved runtime channels:
|
||||
deployment env vars for deploy-level secrets, the encrypted `app_secrets` vault
|
||||
or `saveCredential` / `resolveCredential` for user/org/workspace API keys, and
|
||||
`oauth_tokens` for OAuth. Code and instructions may name the credential key
|
||||
(`OPENAI_API_KEY`), but must never contain the credential value.
|
||||
|
||||
## Input Validation
|
||||
|
||||
Use `defineAction` with a Zod `schema:` for every action. The framework validates input automatically and returns clear 400 errors for HTTP callers and structured error results for agent tool calls.
|
||||
|
||||
```ts
|
||||
export default defineAction({
|
||||
schema: z.object({
|
||||
email: z.string().email(),
|
||||
role: z.enum(["admin", "member"]),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(25),
|
||||
}),
|
||||
run: async (args) => { /* args is fully typed and validated */ },
|
||||
});
|
||||
```
|
||||
|
||||
The legacy `parameters:` field (plain JSON Schema) has no runtime validation — do not use it for new code.
|
||||
|
||||
## Large Payloads
|
||||
|
||||
Do not accept or persist unbounded base64/file blobs through actions, SQL writes,
|
||||
or `application_state`. Route uploads through the file-upload provider and store
|
||||
references. This prevents database bloat, slow hot paths, and accidental secret
|
||||
or customer-data embedding inside binary payloads.
|
||||
|
||||
## SQL Injection
|
||||
|
||||
Never concatenate user input into SQL strings. Use Drizzle ORM's query builder (always safe) or parameterized queries:
|
||||
|
||||
```ts
|
||||
// Safe — Drizzle ORM
|
||||
await db.select().from(users).where(eq(users.email, args.email));
|
||||
|
||||
// Safe — parameterized raw SQL
|
||||
await client.execute({ sql: "SELECT * FROM users WHERE id = ?", args: [id] });
|
||||
|
||||
// NEVER do this
|
||||
await client.execute(`SELECT * FROM users WHERE id = '${id}'`);
|
||||
```
|
||||
|
||||
## XSS
|
||||
|
||||
- React auto-escapes JSX content — trust it.
|
||||
- Never use `dangerouslySetInnerHTML`, `innerHTML`, `eval()`, or `document.write()` with user-controlled content.
|
||||
- For rich text editing, use TipTap (framework dependency).
|
||||
- For rendering markdown, use `react-markdown`.
|
||||
|
||||
## SSRF
|
||||
|
||||
Any server-side `fetch` of a user- or agent-controlled URL must go through the framework SSRF guard — a bare `fetch()` can be steered at cloud metadata (`169.254.169.254`), `localhost`, or internal services.
|
||||
|
||||
```ts
|
||||
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
||||
// Blocks private/internal targets, re-checks the resolved IP at connect time
|
||||
// (DNS rebinding), and re-validates every redirect hop.
|
||||
const res = await ssrfSafeFetch(userProvidedUrl, {}, { maxRedirects: 3 });
|
||||
```
|
||||
|
||||
For a pre-flight-only check (e.g. before a streaming or one-shot fetch), use `isBlockedExtensionUrlWithDns(url)` plus `createSsrfSafeDispatcher()` from the same module, and set `redirect: "manual"`. Never let the default `fetch` follow redirects for an untrusted URL — a public URL can 30x into the private network.
|
||||
|
||||
## Secrets
|
||||
|
||||
- OAuth tokens go in the `oauth_tokens` store via `saveOAuthTokens()`.
|
||||
- Per-user / per-org API keys go through `saveCredential` / `resolveCredential` (`@agent-native/core/credentials`) or the `app_secrets` vault. Both encrypt values at rest with AES-256-GCM (keyed by `SECRETS_ENCRYPTION_KEY`, falling back to `BETTER_AUTH_SECRET`; production refuses to start without one).
|
||||
- Never hand-roll secrets into `settings`, `application_state`, source code, or action responses sent to the client. The credential / vault APIs above are the only sanctioned stores.
|
||||
- Never commit real keys, tokens, webhook URLs, signing secrets, or private
|
||||
Builder/customer data in examples or fixtures. Use placeholders that cannot be
|
||||
mistaken for working credentials.
|
||||
|
||||
## User Credentials Are Per-User Data — Never `process.env`
|
||||
|
||||
User credentials (API keys, third-party tokens) are per-user (or per-org) data. They MUST live in SQL, scoped per-user (`u:<email>:credential:KEY`) or per-org (`o:<orgId>:credential:KEY`). Always read with the request context:
|
||||
|
||||
```ts
|
||||
import { resolveCredential } from "@agent-native/core/credentials";
|
||||
const apiKey = await resolveCredential("OPENAI_API_KEY", { userEmail, orgId });
|
||||
```
|
||||
|
||||
Values are encrypted at rest (AES-256-GCM, shared `secrets/crypto.ts`): `saveCredential` encrypts on write and `resolveCredential` decrypts on read, with a transparent fallback for legacy plaintext rows. The agent's raw `db-query` / `db-exec` tools also cannot read credential rows — they are excluded from the scoped `settings` view. To encrypt pre-existing rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive; needs the same `SECRETS_ENCRYPTION_KEY` / `BETTER_AUTH_SECRET` as the app).
|
||||
|
||||
On 2026-04-29 the previous one-arg `resolveCredential(key)` form fell back to `process.env[key]` and an unscoped global `settings` row, so every signed-in user inherited the deployment's credentials. Two guards now block this in CI (`pnpm prep`):
|
||||
|
||||
- `scripts/guard-no-env-credentials.mjs` — bans `process.env.<KEY>` reads in `packages/core/src/credentials/`, `secrets/`, `vault/`, and `templates/*/server/{lib,routes/api}/credential*` paths, except for an explicit allowlist of deploy-level vars (`DATABASE_URL`, `BETTER_AUTH_SECRET`, `NETLIFY_*`, etc.). Per-line opt-out: `// guard:allow-env-credential — <reason>`.
|
||||
- `scripts/guard-no-unscoped-credentials.mjs` — bans one-arg calls to `resolveCredential` / `hasCredential` / `saveCredential` / `deleteCredential`. Per-line opt-out: `// guard:allow-unscoped-credential — <reason>`.
|
||||
|
||||
If a deploy-level value genuinely needs an env var (CI-set token, host secret), it's not a user credential — keep it out of the credentials/ secrets/ vault/ paths and the env-credentials guard won't see it.
|
||||
|
||||
## Guards
|
||||
|
||||
Two more CI guards (also wired into `pnpm prep`) target the 2026-04 cross-tenant leak class — request-state escaping into shared process state, and dev-mode sentinel identities used as production fallbacks.
|
||||
|
||||
- `scripts/guard-no-env-mutation.mjs` — bans `process.env.<KEY> = …` (and bracket / compound forms) anywhere in production code. On serverless, every warm container handles many concurrent requests in one Node process, so `process.env` mutation leaks across in-flight requests (the "restore" line at the end of a handler races and never helps — most recently the Zoom webhook). Use `runWithRequestContext({ userEmail, orgId, timezone }, fn)` from `@agent-native/core/server` instead — it's AsyncLocalStorage-backed and per-request safe. Allowlisted paths: `scripts/`, `*.spec.ts` / `*.test.ts`, `packages/core/src/dev**`, `templates/*/test/`, anything under `/cli/` or `/scaffold/`. Per-line opt-out: `process.env.X = y // guard:allow-env-mutation — <reason>`.
|
||||
- `scripts/guard-no-localhost-fallback.mjs` — bans the literal `"local@localhost"` / `'local@localhost'` / `` `local@localhost` `` in production code. The bug class: `getRequestUserEmail() ?? "local@localhost"` silently pools every unauthenticated request into a single shared tenant, leaking credentials, tools, and `application_state` rows between accounts. The right behavior is to throw / 401 when there's no session. Allowlisted paths: the dev-mode auth shim (`packages/core/src/server/auth.ts`), `packages/core/src/dev**`, tests, `scripts/`, `seed/` / `seeds/`, plus a few framework helpers that intentionally inspect or migrate the dev identity. SQL DDL `DEFAULT 'local@localhost'` and the Drizzle helper `.default('local@localhost')` are skipped per-line — schema column defaults are intentional dev fixtures, not the dangerous fallback pattern. Per-line opt-out: `email ?? "local@localhost" // guard:allow-localhost-fallback — <reason>`.
|
||||
|
||||
## Auth
|
||||
|
||||
- All actions are protected by the auth guard automatically.
|
||||
- Prefer actions for normal app data. Do not hand-write `/api/*` routes for
|
||||
CRUD, data queries, or action re-exports just to add auth; action endpoints
|
||||
already get auth and request context.
|
||||
- If you must create custom `/api/` routes, always call `getSession(event)` and reject requests without a session:
|
||||
|
||||
```ts
|
||||
import { getSession } from "@agent-native/core/server";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getSession(event);
|
||||
if (!session) throw createError({ statusCode: 401 });
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
- Never create unprotected routes that modify data.
|
||||
|
||||
**Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
|
||||
The rule above is for routes that read or mutate user data. The SSR page
|
||||
render and React Router `.data` route are different: they serve one
|
||||
impersonal, public-cacheable shell to every visitor by design, and loaders on
|
||||
that path render no user data. Do not "fix" this by adding `getSession`,
|
||||
`private`, or `no-store` to it — that regresses the CDN cache contract for the
|
||||
whole site. Data scoping lives in actions and API routes; the client gates
|
||||
private UI after the shell loads. See the `authentication` skill and
|
||||
`guard:ssr-cache-shell` / `ssr-handler.spec.ts`.
|
||||
|
||||
## Human-in-the-Loop Approval for High-Consequence Actions
|
||||
|
||||
For a small set of outward-facing, hard-to-undo operations — sending an email, charging a card, deleting an account, posting publicly — auth and access control are necessary but not sufficient: you also do not want the **agent** to perform them autonomously. Set `needsApproval` on the `defineAction` so the agent cannot run the action without a human approving the specific call.
|
||||
|
||||
```ts
|
||||
export default defineAction({
|
||||
description: "Send an email via Gmail.",
|
||||
schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
|
||||
needsApproval: true, // or (args, ctx) => boolean | Promise<boolean>
|
||||
run: async (args) => {
|
||||
/* ...actually send... */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When the gate is truthy and the call is not yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's stable `approvalKey`; only then does the action run. A predicate gates conditionally (e.g. only external recipients) and **fails closed** — a throw is treated as "approval required".
|
||||
|
||||
Rules:
|
||||
|
||||
- Reach for `needsApproval` only for genuinely high-consequence operations. The default is off, and the framework intentionally keeps approvals rare — over-gating turns the agent into a click-through wizard. The canonical (and intentionally lone) framework example is Mail's `send-email`.
|
||||
- `needsApproval` is **not** a substitute for `accessFilter` / `assertAccess` or for hiding sensitive operations from the model with `agentTool: false` / `toolCallable: false`. It is the layer for "a human must explicitly bless this specific outward-facing call," not for scoping data. See the `actions` skill for the full surface.
|
||||
|
||||
## Custom HTTP Routes Must Apply Access Control Themselves
|
||||
|
||||
This is the single most-failed rule in the codebase. Auto-mounted action routes (`/_agent-native/actions/...`) get a request context wired up automatically. **Hand-written `/api/*` Nitro routes do not.** If your handler queries an ownable resource (any table with `...ownableColumns()`), you MUST:
|
||||
|
||||
1. Read the session: `const session = await getSession(event).catch(() => null)`.
|
||||
2. Run the work inside `runWithRequestContext({ userEmail: session?.email, orgId: session?.orgId }, fn)` from `@agent-native/core/server`.
|
||||
3. Inside `fn`, query through one of:
|
||||
- `accessFilter(table, sharesTable)` in the WHERE clause for list/read-many.
|
||||
- `resolveAccess("<type>", id)` for read-by-id (returns null if no access — return 404, not 403, so existence isn't leaked).
|
||||
- `assertAccess("<type>", id, "viewer"|"editor"|"admin")` for write/delete-by-id.
|
||||
|
||||
```ts
|
||||
// Bad — Brent's signup leaked every other user's decks because of this exact shape.
|
||||
export default defineEventHandler(async () => {
|
||||
const db = getDb();
|
||||
return db.select().from(schema.decks); // no access filter!
|
||||
});
|
||||
|
||||
// Good
|
||||
import { getSession, runWithRequestContext } from "@agent-native/core/server";
|
||||
import { accessFilter } from "@agent-native/core/sharing";
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getSession(event).catch(() => null);
|
||||
return runWithRequestContext(
|
||||
{ userEmail: session?.email, orgId: session?.orgId },
|
||||
async () => {
|
||||
const db = getDb();
|
||||
return db
|
||||
.select()
|
||||
.from(schema.decks)
|
||||
.where(accessFilter(schema.decks, schema.deckShares));
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
`scripts/guard-no-unscoped-queries.mjs` runs in `pnpm prep` and fails the build if any file in `templates/*/server/`, `templates/*/actions/`, or `packages/*/src/` queries an ownable table without one of the access helpers. Last-resort opt-out is the marker comment `// guard:allow-unscoped — <reason>` — only use it for cases like the sharing primitives themselves or share-token-public viewer endpoints, and always include a reviewer-readable reason.
|
||||
|
||||
## Data Scoping
|
||||
|
||||
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views. This is enforced at the SQL level — the agent cannot bypass it.
|
||||
|
||||
The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path) reject schema-qualified table references like `public.<table>` or `main.<table>` — a qualified name resolves to the base table and would skip the temp view. Use bare table names; scoping is applied automatically.
|
||||
|
||||
### Per-User Scoping (`owner_email`)
|
||||
|
||||
Every template table with user data **must** have an `owner_email` text column:
|
||||
|
||||
1. Framework detects `owner_email` via schema introspection
|
||||
2. Creates temp views `WHERE owner_email = <current user>` before each query
|
||||
3. Auto-injects `owner_email` into INSERT statements
|
||||
|
||||
The current user is resolved from `AGENT_USER_EMAIL` (set automatically from the session).
|
||||
|
||||
### Per-Org Scoping (`org_id`)
|
||||
|
||||
For multi-org apps, tables also need `org_id`:
|
||||
|
||||
1. `WHERE org_id = <current org>` is added (in addition to `owner_email` if present)
|
||||
2. `org_id` is auto-injected into INSERT statements
|
||||
|
||||
Enable org scoping in the agent-chat plugin:
|
||||
|
||||
```ts
|
||||
createAgentChatPlugin({
|
||||
resolveOrgId: async (event) => {
|
||||
const ctx = await getOrgContext(event);
|
||||
return ctx.orgId;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Column Conventions
|
||||
|
||||
| Column | Purpose | Required |
|
||||
| ------------- | ----------------------- | ------------------------------- |
|
||||
| `owner_email` | Per-user data isolation | Yes, for all user-facing tables |
|
||||
| `org_id` | Per-org data isolation | Yes, for multi-org apps |
|
||||
|
||||
Run `pnpm action db-check-scoping` to verify. Use `--require-org` for multi-org apps.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] New action uses `defineAction` with a Zod `schema:`
|
||||
- [ ] No SQL string concatenation with user input
|
||||
- [ ] No `dangerouslySetInnerHTML` with user content
|
||||
- [ ] Server-side fetches of user/agent URLs use `ssrfSafeFetch`, not bare `fetch`
|
||||
- [ ] Secrets stored via `saveCredential` / the vault (encrypted), never raw in `settings` or responses
|
||||
- [ ] No hardcoded API keys, tokens, webhook URLs, signing secrets, real
|
||||
credential-looking strings, private Builder/internal data, or customer data
|
||||
- [ ] New env vars in `.env` only, not committed
|
||||
- [ ] New user-data tables have `owner_email` column
|
||||
- [ ] Custom routes call `getSession` and reject unauthenticated requests
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `storing-data` — SQL patterns and the agent's db tools
|
||||
- `actions` — `defineAction` with Zod schema validation
|
||||
- `authentication` — Auth modes, sessions, and org context
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: self-modifying-code
|
||||
description: >-
|
||||
How the agent can modify the app's own source code. Use when the agent needs
|
||||
to edit components, routes, styles, or scripts, when designing UI for agent
|
||||
editability, or when deciding what the agent should and shouldn't modify.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Self-Modifying Code
|
||||
|
||||
## Rule
|
||||
|
||||
The agent can edit the app's own source code — components, routes, styles, scripts. This is a feature, not a bug. Design your app expecting this.
|
||||
|
||||
## Why
|
||||
|
||||
An agent-native app isn't just an app the agent can _use_ — it's an app the agent can _change_. The agent can fix bugs, add features, adjust styles, and restructure code. This makes the agent a true collaborator, not just an operator.
|
||||
|
||||
## Modification Taxonomy
|
||||
|
||||
Not all modifications are equal. Use this to decide what level of care is needed:
|
||||
|
||||
| Tier | What | Examples | After modifying |
|
||||
| ------------- | --------------------- | ------------------------------------------------ | --------------------------------- |
|
||||
| 1: Data | Files in `data/` | JSON state, generated content, markdown | Nothing — these are routine |
|
||||
| 2: Source | App code | Components, routes, styles, scripts | Run `pnpm typecheck && pnpm lint` |
|
||||
| 3: Config | Project config | `package.json`, `tsconfig.json`, `vite.config.*` | Ask for explicit approval first |
|
||||
| 4: Off limits | Secrets and framework | `.env`, `@agent-native/*` packages & overrides | Never modify these |
|
||||
|
||||
Tier 4 includes **all** of the following — not only editing package source:
|
||||
|
||||
- Files under `node_modules/@agent-native/*` (core, dispatch, scheduling, …)
|
||||
- `pnpm.overrides`, `overrides`, `resolutions`, or `patchedDependencies` that
|
||||
target any `@agent-native/*` package
|
||||
- Local package patches or invented "dispatch/core behavior" shims meant to
|
||||
paper over a version skew or failed upgrade
|
||||
|
||||
This does not prohibit intentional app-owned UI customization. When public
|
||||
props and composition are insufficient, the `customizing-agent-native` skill
|
||||
uses `agent-native eject` to transfer the smallest supported unit from the
|
||||
installed package into the app. The ejected unit must keep public runtime
|
||||
contracts and must not replace Core auth, DB, actions, agent execution, or
|
||||
transport behavior. Manual copying is only the fallback described by an unknown
|
||||
third-party package's add-style blueprint.
|
||||
|
||||
When an older branch needs current packages, use **`agent-native upgrade`**
|
||||
(see the `upgrade-agent-native` skill). If upgrade or typecheck fails, fix
|
||||
**app** code or stop and ask — do not patch the framework.
|
||||
|
||||
## Git Checkpoint Pattern
|
||||
|
||||
Before modifying source code (Tier 2+), create a rollback point:
|
||||
|
||||
1. Commit or stash current state
|
||||
2. Make the edit
|
||||
3. Run `pnpm typecheck && pnpm lint`
|
||||
4. If verification fails → revert with `git checkout -- <file>`
|
||||
5. If verification passes → continue
|
||||
|
||||
This ensures the agent can experiment without breaking the app.
|
||||
|
||||
## Designing for Agent Editability
|
||||
|
||||
Make your app easy for the agent to understand and modify:
|
||||
|
||||
**Expose UI state via `data-*` attributes** so the agent knows what's selected:
|
||||
|
||||
```ts
|
||||
const el = document.documentElement;
|
||||
el.dataset.currentView = view;
|
||||
el.dataset.selectedId = selectedItem?.id || "";
|
||||
```
|
||||
|
||||
**Expose richer context via `window.__appState`** for complex state:
|
||||
|
||||
```ts
|
||||
(window as any).__appState = {
|
||||
selectedId: id,
|
||||
currentLayout: layout,
|
||||
itemCount: items.length,
|
||||
};
|
||||
```
|
||||
|
||||
**Use configuration-driven rendering** — Extract visual decisions (colors, layouts, sizes) into JSON config files in `data/`. The agent can modify the config (Tier 1) instead of the component source (Tier 2).
|
||||
|
||||
**Keep localized copy in catalogs** — When editing visible UI copy, labels,
|
||||
toasts, empty states, prompts, or formatting, read `internationalization` and
|
||||
update `app/i18n/en-US.ts` plus existing locale catalogs instead of leaving new
|
||||
inline strings in components.
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't modify `.env` files or files containing secrets
|
||||
- Don't modify `@agent-native/core`, `@agent-native/dispatch`, or other
|
||||
`@agent-native/*` package internals (including under `node_modules`)
|
||||
- Don't confuse readable package source with app-owned code: use
|
||||
`customizing-agent-native` and `agent-native eject` for supported ownership
|
||||
transfer
|
||||
- Don't add `pnpm.overrides` / `patchedDependencies` / `resolutions` for
|
||||
`@agent-native/*` to "make the app run" after a version bump
|
||||
- Don't invent local dispatch/core behavior overrides when upgrade fails —
|
||||
run `npx @agent-native/core@latest upgrade`, then fix app-level errors only
|
||||
- Don't modify `.agents/skills/` or `AGENTS.md` unless explicitly requested
|
||||
- Don't skip the typecheck/lint step after editing source code
|
||||
- Don't make source changes without a git checkpoint to roll back to
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **upgrade-agent-native** — supported path to bring an older app/workspace current
|
||||
- **customizing-agent-native** — configure, compose, or eject installed features safely
|
||||
- **storing-data** — Tier 1 modifications (data files) are the safest and most common
|
||||
- **actions** — The agent can create or modify actions to add new capabilities
|
||||
- **delegate-to-agent** — Self-modification requests come through the agent chat
|
||||
- **real-time-sync** — Database writes trigger change events to update the UI
|
||||
- **internationalization** — UI copy, language catalogs, locale switching, and RTL-safe edits
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: shadcn-ui
|
||||
description: >-
|
||||
Use when adding, updating, debugging, styling, or composing shadcn/ui
|
||||
components, forms, dialogs, menus, charts, sidebars, themes, registries, or
|
||||
any project with a components.json file.
|
||||
scope: dev
|
||||
source: https://ui.shadcn.com/docs/skills
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# shadcn/ui
|
||||
|
||||
This skill keeps shadcn/ui work project-aware. Components are source files in the app, so always inspect the local project before adding, importing, or rewriting them.
|
||||
|
||||
## First Steps
|
||||
|
||||
1. Work from the app root that owns `components.json`.
|
||||
2. In an Agent Native app, inspect `app/design-system.ts` and
|
||||
`ToolkitProvider` before choosing a primitive. A registered company design
|
||||
system takes precedence over the default shadcn adapter.
|
||||
3. Run `pnpm dlx shadcn@latest info --json` when you need current project context: framework, Tailwind version, aliases, icon library, installed components, and resolved paths.
|
||||
4. Use the actual aliases from `components.json` or `shadcn info`; do not assume `@/components/ui` if the project says otherwise.
|
||||
5. Check `app/components/ui/` or the resolved `ui` path before importing a component.
|
||||
6. For unfamiliar components, run `pnpm dlx shadcn@latest docs <component>` and read the returned docs or examples before coding.
|
||||
|
||||
## Agent Native Adapter Rule
|
||||
|
||||
Pages, routes, and domain components import controls through the app's local UI
|
||||
adapter layer. Never import `@agent-native/toolkit/ui/*` directly in app product
|
||||
code. Direct imports bypass `app/design-system.ts` and make Toolkit/Core
|
||||
surfaces use different controls from the app.
|
||||
|
||||
When shadcn is the app's adapter, add or update the local primitive and keep
|
||||
product code on that local import. When a company design system is registered,
|
||||
adapt its components to the semantic contracts from
|
||||
`@agent-native/toolkit/design-system`; do not recreate a parallel shadcn
|
||||
surface. The semantic API is styling-runtime agnostic, so do not require
|
||||
Tailwind, CVA, or `className` in customer adapters.
|
||||
|
||||
For shared Toolkit features, customize presentation through semantic
|
||||
components, a feature-level controller, and product-level render slots. Keep
|
||||
the same controller for default and custom views. Use the conformance kit for
|
||||
behavior components whose focus, portal, keyboard, dismissal, or stacking
|
||||
behavior comes from the company design system.
|
||||
|
||||
## Adding Or Updating Components
|
||||
|
||||
- Add missing primitives with `pnpm dlx shadcn@latest add <component>` from the app root.
|
||||
- Before overwriting an existing component, use `pnpm dlx shadcn@latest add <component> --dry-run` and `--diff` to inspect the change.
|
||||
- After adding registry code, read the generated files. Fix import aliases, icon imports, missing subcomponents, and composition issues before using the component.
|
||||
- Do not fetch raw component files manually from GitHub when the shadcn CLI can resolve the registry item.
|
||||
- If a user asks to add a third-party block but does not name a registry, ask which registry to use instead of guessing.
|
||||
|
||||
## Component Composition
|
||||
|
||||
- Use existing primitives before custom markup: `Alert` for callouts, `Badge` for small status labels, `Separator` for dividers, `Skeleton` for placeholders, `Table` for tabular data, and `Card` for framed content.
|
||||
- Use full card anatomy when appropriate: `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, and `CardFooter`.
|
||||
- Dialog, Sheet, Drawer, and AlertDialog content must include an accessible title. Use visually hidden titles only when the visible UI already communicates the title.
|
||||
- Put items inside their group components: `SelectItem` in `SelectGroup`, `DropdownMenuItem` in `DropdownMenuGroup`, `CommandItem` in `CommandGroup`, and equivalent menu groups.
|
||||
- `TabsTrigger` belongs inside `TabsList`.
|
||||
- `Avatar` always needs `AvatarFallback`.
|
||||
- Buttons do not have magic loading props. Compose loading with `disabled`, `Spinner`, and clear text.
|
||||
|
||||
## Forms And Inputs
|
||||
|
||||
- Use the app's shadcn form primitives instead of raw div stacks.
|
||||
- If `Field`, `FieldGroup`, `FieldSet`, or `InputGroup` are installed or worth adding, use them for form layout, grouped fields, and input add-ons.
|
||||
- Do not place buttons inside inputs with absolute positioning. Use `InputGroup` and `InputGroupAddon` when available.
|
||||
- Use `ToggleGroup` for small option sets, `RadioGroup` for one-of-many choices, `Checkbox` for multi-select, `Switch` for settings toggles, `Select` or `Combobox` for predefined choices, and `Slider` or numeric input for numeric values.
|
||||
- Validation must be accessible: pair visual invalid states with `aria-invalid`, and connect descriptions/errors to controls.
|
||||
|
||||
## Styling And Theming
|
||||
|
||||
- Use semantic tokens (`bg-background`, `text-foreground`, `text-muted-foreground`, `bg-primary`, `border-border`, `text-destructive`) instead of raw colors for reusable app UI.
|
||||
- Prefer built-in variants and sizes before custom classes.
|
||||
- Use `className` mostly for layout and spacing; avoid overriding component colors and typography unless the component is intentionally being extended.
|
||||
- Use `gap-*` instead of `space-x-*` / `space-y-*`.
|
||||
- Use `size-*` when width and height are equal.
|
||||
- Use `truncate` for single-line clipping.
|
||||
- Use `cn()` for conditional classes.
|
||||
- Do not add manual `z-index` to overlay primitives unless you are fixing a verified stacking bug.
|
||||
- Add custom colors as CSS variables in the existing Tailwind CSS file reported by shadcn info. For Tailwind v4, register variables with `@theme inline`.
|
||||
|
||||
## Transitions And Motion
|
||||
|
||||
shadcn's built-in component animations are the right level of polish — keep them. The goal is a snappy, clean UI, not a motionless one. Match shadcn's motion vocabulary; don't strip it and don't pile on decorative custom animation.
|
||||
|
||||
- **Never remove or override a shadcn component's default animation.** `data-[state=open]:animate-in`, `data-[state=closed]:animate-out`, `fade-in/out`, `zoom-in/out`, `slide-in-from-*`, accordion height, the `tailwindcss-animate` utilities — these ship for a reason. Leave them as-is.
|
||||
- **Custom transitions are fine when they communicate a state change and match shadcn's feel.** Reuse the same vocabulary: short durations (~120–200ms), `ease-out`, opacity/transform only, gated on `data-[state=...]`. Examples that are good and welcome:
|
||||
- A portaled custom popover/tooltip/sheet that fades + scales/slides in on `data-[state=delayed-open]` / `data-[state=closed]`, mirroring Radix's own content animation.
|
||||
- A list row or toast that fades/slides in on mount and out on dismiss.
|
||||
- A chevron/caret `rotate` on expand, a subtle `opacity`/`color` hover on an icon button, skeleton shimmer, a progress/height transition on a collapsible.
|
||||
- Continuous, product-defining motion where it _is_ the experience (e.g. a multi-stage booking flow's stage transitions) — fine, and framer-motion is acceptable there.
|
||||
- **Avoid decorative, attention-seeking, or slow motion:** hand-rolled `duration-700` hero fade-ins, parallax, bouncing/spring entrances on ordinary content, animated gradients, staggered cascades on long lists, anything that delays the user seeing or acting on content. If an animation makes the UI feel slower, cut it.
|
||||
- Rule of thumb: if the motion clarifies what just changed and is over in well under a quarter-second, it's polish; if it's there to look impressive, it's bloat.
|
||||
|
||||
## Icons
|
||||
|
||||
- Agent-native apps use `@tabler/icons-react`. Do not add `lucide-react` because a registry example used it.
|
||||
- If registry code imports a different icon package, replace those imports with Tabler equivalents before finishing.
|
||||
- Let shadcn components size icons through their CSS. Avoid manual icon sizing inside buttons, menus, alerts, and sidebars unless the local component API requires it.
|
||||
|
||||
## Base-Specific APIs
|
||||
|
||||
Check the project context before using trigger composition APIs:
|
||||
|
||||
- Radix-based components use `asChild` for custom triggers.
|
||||
- Base UI components may use `render` and sometimes `nativeButton={false}`.
|
||||
|
||||
Do not wrap triggers in extra divs just to place a Button or Link inside them.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **frontend-design** — Product UX, visual direction, responsive polish, and verification
|
||||
- **customizing-agent-native** — Registered design systems, controllers, slots, conformance, and ejection
|
||||
- **actions** — Data fetching and mutation patterns for agent-native apps
|
||||
- **security** — User data, forms, external input, and action safety
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: upgrade-agent-native
|
||||
description: >-
|
||||
Bring an older Agent Native app or workspace current. Use when updating
|
||||
@agent-native/core, fixing a broken upgrade, or when tempted to patch or
|
||||
override core/dispatch packages to make an old branch run.
|
||||
scope: dev
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Upgrade Agent Native
|
||||
|
||||
## Rule
|
||||
|
||||
When an older Agent Native app/branch needs to run on current packages, use
|
||||
`agent-native upgrade`. Never "fix" upgrade breakage with
|
||||
`pnpm.overrides`, `patchedDependencies`, `resolutions`, local patches, or
|
||||
edits under `node_modules/@agent-native/*` — especially not against
|
||||
`@agent-native/core` or `@agent-native/dispatch`.
|
||||
|
||||
## Why
|
||||
|
||||
Agents often respond to a failed core bump by inventing framework patches and
|
||||
dispatch behavior overrides. That hides the real app-level break, drifts from
|
||||
upstream, and makes the next upgrade worse. The supported path is bump →
|
||||
install → refresh scaffold skills → verify, then fix **app** code only.
|
||||
|
||||
## How
|
||||
|
||||
1. **Preview migration codemods first**
|
||||
|
||||
```bash
|
||||
npx @agent-native/core@latest upgrade --codemods
|
||||
```
|
||||
|
||||
Codemods are preview-by-default: read the diff before applying it. Do not
|
||||
manually edit imports before running this command; the migration manifest is
|
||||
the source of truth for renamed specifiers and symbols.
|
||||
|
||||
2. **Apply the reviewed codemods, then run the upgrade**
|
||||
|
||||
```bash
|
||||
npx @agent-native/core@latest upgrade --codemods --yes
|
||||
npx @agent-native/core@latest upgrade
|
||||
```
|
||||
|
||||
Or from an already-installed CLI: `pnpm exec agent-native upgrade` /
|
||||
`agent-native upgrade`.
|
||||
|
||||
What it does:
|
||||
|
||||
- Blocks (unless `--force`) when `@agent-native/*` overrides/patches exist
|
||||
- Rewrites non-local `@agent-native/*` dependency pins to `latest`
|
||||
- Runs the package manager install
|
||||
- Runs `skills update scaffold --project`
|
||||
- Runs `typecheck` when the project has that script
|
||||
|
||||
3. **Pull upstream template changes (optional, separate from the bump)**
|
||||
|
||||
`agent-native upgrade` moves package versions. It never touches files that
|
||||
were copied out of a template at scaffold time, so template fixes and
|
||||
improvements do not arrive with a bump.
|
||||
|
||||
```bash
|
||||
agent-native template status # recorded ref vs latest, drift counts
|
||||
agent-native template diff # what upstream changed, read-only
|
||||
agent-native template sync # 3-way merge it into the app
|
||||
```
|
||||
|
||||
`sync` defaults to the ref matching the installed `@agent-native/core`, so
|
||||
run it after `upgrade`. It merges per file against a pristine baseline
|
||||
stored in `refs/agent-native/template-baseline/<app-path>`; files upstream
|
||||
did not touch are left alone, and real collisions get conflict markers.
|
||||
After resolving markers, run `agent-native template accept` — the baseline
|
||||
deliberately does not advance past an unresolved merge.
|
||||
|
||||
Apps scaffolded before provenance existed have no baseline. Create one
|
||||
with `agent-native template baseline` before the first sync.
|
||||
|
||||
4. **If upgrade or typecheck fails**
|
||||
|
||||
- Read the concrete error
|
||||
- Fix **app** source, actions, config, or env — not framework packages
|
||||
- Re-run `agent-native upgrade` or `pnpm typecheck`
|
||||
- Stop and ask the user if you cannot fix the app-level error
|
||||
|
||||
Intentional app-level UI customization is a separate workflow. Read
|
||||
`customizing-agent-native` when the product needs to own a selectively
|
||||
copied component; do not use that path to reproduce framework runtime
|
||||
behavior or hide version skew.
|
||||
|
||||
5. **Dry-run / partial runs**
|
||||
|
||||
```bash
|
||||
agent-native upgrade --dry-run
|
||||
agent-native upgrade --skip-verify
|
||||
agent-native upgrade --skip-install # package.json bumps only
|
||||
agent-native doctor --only migration-manifest
|
||||
```
|
||||
|
||||
`migration-manifest` has no opt-out. Run it in CI before upgrading to find
|
||||
imports that will break, then use `npx @agent-native/core@latest upgrade --codemods`
|
||||
to preview the supported rewrite.
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't add `pnpm.overrides`, `overrides`, `resolutions`, or
|
||||
`patchedDependencies` for any `@agent-native/*` package
|
||||
- Don't edit `node_modules/@agent-native/core` or
|
||||
`node_modules/@agent-native/dispatch`
|
||||
- Don't invent local "dispatch behavior" shims to paper over version skew
|
||||
- Don't keep iterating with more framework patches after a failed install
|
||||
- Don't skip `skills update scaffold --project` after a core bump (the
|
||||
upgrade command does this for you)
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **self-modifying-code** — Tier 4: framework packages are off limits
|
||||
- **agent-native-docs** — version-matched docs after the bump
|
||||
- **customizing-agent-native** — intentional app-owned UI copies, not upgrade patches
|
||||
- **portability** — keep app code provider-agnostic across upgrades
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -0,0 +1,5 @@
|
||||
# Custom health check message
|
||||
PING_MESSAGE=pong
|
||||
|
||||
# Skip login/signup (local dev / preview only)
|
||||
# AUTH_DISABLED=true
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# React Router generated types
|
||||
.react-router/
|
||||
.generated/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Data
|
||||
data/*.db
|
||||
data/*.db-shm
|
||||
data/*.db-wal
|
||||
data/uploads/
|
||||
data/settings.json
|
||||
data/.sessions.json
|
||||
|
||||
# Learnings (personal preferences and memory — use learnings.defaults.md for tracked defaults)
|
||||
learnings.md
|
||||
|
||||
.vercel/
|
||||
.agent-native/
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "all",
|
||||
"proseWrap": "preserve",
|
||||
"sortImports": true
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Chat — Agent Guide
|
||||
|
||||
Chat is the minimal chat-first agent-native app template. Keep chat as the
|
||||
primary surface, add actions for real capabilities, and add screens only when a
|
||||
workflow needs durable UI around the conversation.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Store large file/blob payloads in configured file/blob storage, not SQL: no
|
||||
base64, `data:` URLs, images, video/audio, PDFs, ZIPs, screenshots,
|
||||
thumbnails, or replay chunks in app tables, `application_state`, `settings`,
|
||||
or `resources`; persist URLs, ids, or handles instead.
|
||||
- Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
|
||||
- Follow the root framework contract: data in SQL, actions first, application
|
||||
state for navigation/selection, and shared agent chat for AI work.
|
||||
- Use actions for app operations and keep frontend/API parity.
|
||||
- Treat the chat as the default UI. When the user asks for a capability, prefer
|
||||
adding or improving the action surface first, then add a page, table, form, or
|
||||
widget only when the user needs to inspect, compare, approve, or share durable
|
||||
objects.
|
||||
- If the user wants to plug in their own agent backend, keep the app shell and
|
||||
thread UI intact and adapt the chat through the framework's `AgentChatRuntime`
|
||||
connector helpers instead of forking the transcript/composer UI.
|
||||
- Keep the action surface small and orthogonal: every action is a tool in the
|
||||
model's context window, so prefer one CRUD-style `update` (patch of fields)
|
||||
over many per-field actions, reach for an existing generic query / escape
|
||||
hatch (`provider-api-*`, dev `db-query`) before minting a new read action,
|
||||
mark UI-only or programmatic actions `agentTool: false` to hide them from the
|
||||
model (distinct from `toolCallable: false`, which only gates the extension
|
||||
iframe), and delete or hide actions the UI no longer uses. See the `actions`
|
||||
skill.
|
||||
- Keep database code provider-agnostic and additive.
|
||||
- Use `view-screen` or application state when the active page/selection is
|
||||
unclear.
|
||||
- For new features, update UI, actions, skills/instructions, and application
|
||||
state when applicable.
|
||||
|
||||
## Application State
|
||||
|
||||
- `navigation` should describe the current view and selected entity ids. The
|
||||
default chat view is `chat` at `/`.
|
||||
- `navigate` may be used to move the UI when the app supports it.
|
||||
- `view-screen` is the first tool to call when the user's visible context
|
||||
matters.
|
||||
|
||||
## Framework Docs Lookup
|
||||
|
||||
- Before implementing or explaining non-trivial Agent Native behavior, use the
|
||||
`agent-native-docs` skill and the built-in `docs-search` action/tool to read
|
||||
the version-matched framework docs bundled with `@agent-native/core`.
|
||||
- Use the built-in `source-search` action/tool, or search
|
||||
`node_modules/@agent-native/core/corpus`, when you need current core or
|
||||
first-party template implementation examples.
|
||||
- Prefer those installed docs over memory or public docs when package APIs,
|
||||
generated-app conventions, workspaces, actions, or agent surfaces are involved.
|
||||
- Before building common workspace or agent UI, read `agent-native-toolkit` to
|
||||
inventory existing public kits and installed package seams.
|
||||
- Read `customizing-agent-native` before overriding the chat shell or shared UI.
|
||||
Keep Core thread/runtime behavior and use the supported ladder: configure →
|
||||
compose → eject the smallest presentation unit → propose a shared seam.
|
||||
Preview before `--apply` and commit `agent-native.ejections.json`.
|
||||
|
||||
## Skills
|
||||
|
||||
Read the relevant root skill before implementation: `adding-a-feature`,
|
||||
`actions`, `agent-native-docs`, `agent-native-toolkit`,
|
||||
`customizing-agent-native`, `storing-data`,
|
||||
`real-time-sync`, `security`, `delegate-to-agent`, `frontend-design`, `shadcn-ui`, and
|
||||
`self-modifying-code`.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
All notable user-facing changes to Chat are documented here. Open it any
|
||||
time from the command menu (Cmd+K → "What's new").
|
||||
|
||||
## 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- See what's new right inside Chat — a changelog now lives in the command menu (Cmd+K).
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
# app — Development Guide
|
||||
|
||||
This guide is for development-mode agents editing this app's source code. For app operations and tools, see AGENTS.md.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** @agent-native/core + React Router v8 (framework mode)
|
||||
- **Frontend:** React 19, Vite, TailwindCSS, shadcn/ui
|
||||
- **Routing:** File-based via `flatRoutes()` — SSR shell + client rendering
|
||||
- **Backend:** Nitro (via @agent-native/core) — file-based API routing, server plugins, deploy-anywhere presets
|
||||
- **State:** SQL-backed (SSE for real-time updates)
|
||||
|
||||
## Commands
|
||||
|
||||
- **Dev:** `pnpm dev` (Vite dev server with both React Router + Nitro plugins)
|
||||
- **Build:** `pnpm build` (React Router build — client + SSR + Nitro server)
|
||||
- **Start:** `node .output/server/index.mjs` (production)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
app/ # React frontend
|
||||
root.tsx # HTML shell + global providers
|
||||
entry.client.tsx # Client hydration entry
|
||||
routes.ts # Route config — flatRoutes()
|
||||
routes/ # File-based page routes (auto-discovered)
|
||||
_index.tsx # / (chat page)
|
||||
components/ # UI components
|
||||
hooks/ # React hooks
|
||||
lib/ # Utilities (cn, etc)
|
||||
|
||||
server/ # Nitro API server
|
||||
routes/
|
||||
api/ # Route-only endpoints (uploads, webhooks, OAuth, streaming)
|
||||
[...page].get.ts # SSR catch-all (delegates to React Router)
|
||||
plugins/ # Server plugins (startup logic)
|
||||
lib/ # Shared server modules
|
||||
|
||||
shared/ # Isomorphic code (imported by both client & server)
|
||||
|
||||
actions/ # Shared app operations (defineAction; UI uses action hooks)
|
||||
run.ts # Script dispatcher
|
||||
*.ts # Individual actions (pnpm action <name>)
|
||||
|
||||
data/ # Local development database fallback
|
||||
|
||||
react-router.config.ts # React Router framework config
|
||||
.agents/skills/ # Agent skills — detailed guidance for each rule
|
||||
```
|
||||
|
||||
## Framework Basics
|
||||
|
||||
**SSR-first framework, CSR-by-default content:** This app uses React Router v8 framework mode with `ssr: true`. But virtually every route renders only an SSR shell (loading spinner + meta tags). Normal app data fetching happens on the client via action hooks. Server-side data fetching is the exception — only used for public pages that need SEO/OG tags.
|
||||
|
||||
## Chat-First Shape
|
||||
|
||||
The `/` route is the app's primary chat surface. Keep it on
|
||||
`AgentChatSurface` unless you are intentionally replacing the whole chat
|
||||
experience. The left sidebar owns the durable thread list through
|
||||
`useChatThreads`; app-specific screens can be added alongside it as nav items
|
||||
when they become useful.
|
||||
|
||||
For a headless app that later needs UI, this template is the intended landing
|
||||
zone: bring the existing actions over, keep their names stable, and let the chat
|
||||
call them before adding extra screens. For a custom agent backend, keep the app
|
||||
shell and swap the chat runtime with an `AgentChatRuntime` connector instead of
|
||||
rewriting the composer/transcript UI.
|
||||
|
||||
## Adding a Page
|
||||
|
||||
Create a file in `app/routes/`. The filename determines the URL path:
|
||||
|
||||
```
|
||||
app/routes/_index.tsx → /
|
||||
app/routes/settings.tsx → /settings
|
||||
app/routes/inbox.tsx → /inbox
|
||||
app/routes/inbox.$threadId.tsx → /inbox/:threadId
|
||||
app/routes/$id.tsx → /:id (dynamic param)
|
||||
```
|
||||
|
||||
Each route file exports a default component, optional `meta()`, and optional `HydrateFallback()`:
|
||||
|
||||
```tsx
|
||||
import MyPage from "@/pages/MyPage";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "My Page" }];
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyPageRoute() {
|
||||
return <MyPage />;
|
||||
}
|
||||
```
|
||||
|
||||
**Do NOT fetch data server-side** in route loaders unless the page genuinely needs SEO/OG content. The standard pattern is: SSR renders the shell, client hydrates, and React reads/writes normal app data through actions with `useActionQuery` / `useActionMutation`.
|
||||
|
||||
## Adding App Data
|
||||
|
||||
Normal app data starts as an action, not a custom route. Add `actions/<verb>-<resource>.ts` with `defineAction`, mark reads with `http: { method: "GET" }`, and call reads/writes from React with `useActionQuery` / `useActionMutation` from `@agent-native/core/client`. This keeps the UI and agent on one contract and lets mutating actions refresh action-backed queries automatically.
|
||||
|
||||
## Adding a Route-Only Endpoint
|
||||
|
||||
Use `server/routes/api/` only for protocols that cannot be modeled as JSON actions: multipart uploads, streaming/SSE/WebSocket, webhooks, OAuth callbacks/redirects, public SEO/OG endpoints, or binary/static asset serving. Do not add `/api/*` routes for normal CRUD, data queries, or pass-through wrappers around actions; the action endpoint already exists at `/_agent-native/actions/:name`.
|
||||
|
||||
Each route-only endpoint still exports a default `defineEventHandler`, but keep shared app logic in actions or server libraries so agent and UI behavior do not fork.
|
||||
|
||||
## Adding a Server Plugin
|
||||
|
||||
Startup logic (auth, SSE, etc.) lives in `server/plugins/`. Use `defineNitroPlugin` from core:
|
||||
|
||||
```ts
|
||||
import { defineNitroPlugin } from "@agent-native/core";
|
||||
|
||||
export default defineNitroPlugin(async (nitroApp) => {
|
||||
// Runs once at server startup
|
||||
});
|
||||
```
|
||||
|
||||
## Key Imports from `@agent-native/core`
|
||||
|
||||
| Import | Purpose |
|
||||
| -------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `defineNitroPlugin` | Define a server plugin (re-exported from Nitro) |
|
||||
| `createDefaultSSEHandler` | Create SSE endpoint for DB change events (server) |
|
||||
| `readAppState`, `writeAppState` | Read/write application state (from `@agent-native/core/application-state`) |
|
||||
| `readSetting`, `writeSetting` | Read/write settings (from `@agent-native/core/settings`) |
|
||||
| `defineEventHandler`, `readBody`, `getQuery` | H3 route handler utilities (re-exported) |
|
||||
| `sendToAgentChat` | Send messages to agent from UI (client-side) |
|
||||
| `agentChat` | Send messages to agent from scripts (server-side) |
|
||||
|
||||
## Adding an Action
|
||||
|
||||
Create `actions/<verb>-<resource>.ts` with `defineAction`. Run with `pnpm action <name> --id value`; React callers should use `useActionQuery` for GET actions and `useActionMutation` for mutating actions, not a matching `/api/*` wrapper.
|
||||
|
||||
**Sending to agent chat from UI:**
|
||||
|
||||
```ts
|
||||
import { sendToAgentChat } from "@agent-native/core/client";
|
||||
sendToAgentChat({
|
||||
message: "Generate something",
|
||||
context: "...",
|
||||
submit: true,
|
||||
});
|
||||
```
|
||||
|
||||
**Sending to agent chat from scripts:**
|
||||
|
||||
```ts
|
||||
import { agentChat } from "@agent-native/core";
|
||||
agentChat.submit("Generate something");
|
||||
```
|
||||
|
||||
## Database & Environment Variables
|
||||
|
||||
Local development defaults to a SQLite file at `data/app.db`. That local file is for development; containers, previews, and serverless deploys can reset their filesystem. For production/cloud deployment, set `DATABASE_URL` to point to a persistent SQL database. Turso is optional, not required; common choices include Neon, Supabase, Turso/libSQL, plain Postgres, durable SQLite, D1 bindings, and Builder.io-managed environments when available.
|
||||
|
||||
Real credential values belong only in local `.env` files, deployment configuration, or registered secrets/settings UI. Never commit, document, log, return, paste, or include real keys, tokens, webhook URLs, signing secrets, or private data in examples; use empty values or obvious placeholders.
|
||||
|
||||
When adding app data, define tables with `@agent-native/core/db/schema` helpers and use Drizzle's query builder for reads/writes. Do not import dialect-specific schema helpers from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core`, and do not write raw SQL in normal actions or handlers when Drizzle can express the query. Raw SQL belongs in additive migrations, health checks, or carefully scoped maintenance.
|
||||
|
||||
| Variable | Required | Description |
|
||||
| --------------------- | ------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `DATABASE_URL` | Production yes, local dev no | Persistent SQL connection string (local dev default: `file:./data/app.db`) |
|
||||
| `DATABASE_AUTH_TOKEN` | Only when the provider needs it | Auth token for providers such as Turso/libSQL |
|
||||
| `AUTH_DISABLED` | Optional | Set to `true` or `1` to skip login/signup (local dev/preview only) |
|
||||
|
||||
## Extensions (Framework Feature)
|
||||
|
||||
The framework provides **Extensions** — mini sandboxed Alpine.js apps that run inside iframes. Extensions let users (or the agent) create interactive widgets, dashboards, and utilities without modifying the app's source code. They appear in the sidebar under an "Extensions" section. (Distinct from LLM tools — the function-calling primitives the agent invokes.)
|
||||
|
||||
- **Creating extensions**: Via the sidebar "+" button, agent chat, or `POST /_agent-native/extensions`
|
||||
- **API calls**: Extensions use `extensionFetch()` (legacy alias `toolFetch`) which proxies requests through the server with `${keys.NAME}` secret injection
|
||||
- **Styling**: Extensions inherit the main app's Tailwind v4 theme automatically
|
||||
- **Sharing**: Private by default, shareable with org or specific users (same model as other ownable resources)
|
||||
- **Security**: Iframe sandbox + CSP + SSRF protection on the proxy
|
||||
|
||||
See the `extensions` skill in `.agents/skills/extensions/SKILL.md` for full implementation details.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Chat
|
||||
|
||||
The minimal agent-native starter app — a clean, ChatGPT-style shell with chat at
|
||||
the center, durable threads, standard app navigation, auth, live sync, and
|
||||
actions. Start here when you want a real browser app to build on without
|
||||
committing to a domain template.
|
||||
|
||||
**Live app: [chat.agent-native.com](https://chat.agent-native.com)**
|
||||
|
||||
Chat is the basic agent-native app starting point. It gives you the app-agent
|
||||
loop wired end to end and one example action, so you can add your own UI, data,
|
||||
and actions on top.
|
||||
|
||||
## Features
|
||||
|
||||
- ChatGPT-style shell with a threads list and durable chat history.
|
||||
- Auth, live sync, and application state wired out of the box.
|
||||
- The action surface the agent and UI share, plus one example action to copy.
|
||||
- A minimal, brandable base for any domain app.
|
||||
|
||||
## Develop locally
|
||||
|
||||
Scaffold your own copy and run it:
|
||||
|
||||
```bash
|
||||
npx @agent-native/core@latest create my-app --standalone --template chat
|
||||
cd my-app
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Full docs: [agent-native.com/docs/template-chat](https://agent-native.com/docs/template-chat).
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
import { z } from "zod";
|
||||
|
||||
export default defineAction({
|
||||
description: "Return a friendly greeting.",
|
||||
schema: z.object({
|
||||
name: z.string().default("world").describe("Name to greet"),
|
||||
}),
|
||||
http: { method: "GET" },
|
||||
run: async ({ name }) => {
|
||||
return { message: `Hello, ${name}!` };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Navigate the UI to a view.
|
||||
*
|
||||
* Writes a navigate command to application state which the UI reads and auto-deletes.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm action navigate --view=chat
|
||||
* pnpm action navigate --path=/some/route
|
||||
*
|
||||
* Options:
|
||||
* --view View name to navigate to
|
||||
* --path URL path to navigate to
|
||||
* --threadId Chat thread ID to open on the chat route
|
||||
*/
|
||||
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
import { writeAppState } from "@agent-native/core/application-state";
|
||||
import { z } from "zod";
|
||||
|
||||
export default defineAction({
|
||||
description:
|
||||
"Navigate the UI to a specific view or path. Writes a navigate command to application state which the UI reads and auto-deletes.",
|
||||
schema: z.object({
|
||||
view: z.string().optional().describe("View name to navigate to"),
|
||||
path: z.string().optional().describe("URL path to navigate to"),
|
||||
threadId: z.string().optional().describe("Chat thread ID to open"),
|
||||
}),
|
||||
http: false,
|
||||
run: async (args) => {
|
||||
if (!args.view && !args.path) {
|
||||
throw new Error("At least --view or --path is required.");
|
||||
}
|
||||
const nav: Record<string, string> = {};
|
||||
if (args.view) nav.view = args.view;
|
||||
if (args.path) nav.path = args.path;
|
||||
if (args.threadId) nav.threadId = args.threadId;
|
||||
nav._writeId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
await writeAppState("navigate", nav);
|
||||
return `Navigating to ${args.view || args.path}`;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
import { runScript } from "@agent-native/core/scripts";
|
||||
runScript();
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* See what the user is currently looking at on screen.
|
||||
*
|
||||
* Reads and returns the current navigation state from application state.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm action view-screen
|
||||
*/
|
||||
|
||||
import { defineAction } from "@agent-native/core/action";
|
||||
import { readAppState } from "@agent-native/core/application-state";
|
||||
import { z } from "zod";
|
||||
|
||||
export default defineAction({
|
||||
description:
|
||||
"See what the user is currently looking at on screen. Returns the current navigation state for the chat-first app. Always call this first before taking any action.",
|
||||
schema: z.object({}),
|
||||
http: false,
|
||||
readOnly: true,
|
||||
run: async () => {
|
||||
const navigation = await readAppState("navigation");
|
||||
|
||||
const screen: Record<string, unknown> = {};
|
||||
if (navigation) screen.navigation = navigation;
|
||||
|
||||
if (Object.keys(screen).length === 0) {
|
||||
return "No application state found. Is the app running?";
|
||||
}
|
||||
return screen;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { AgentToggleButton } from "@agent-native/core/client/agent-chat";
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import {
|
||||
useHeaderTitle,
|
||||
useHeaderActions,
|
||||
} from "@agent-native/toolkit/app-shell";
|
||||
import { IconMenu2 } from "@tabler/icons-react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
const pageTitleKeys: Record<string, string> = {
|
||||
"/": "navigation.chat",
|
||||
"/observability": "navigation.observability",
|
||||
"/agent": "settings.agentTitle",
|
||||
"/settings": "navigation.settings",
|
||||
};
|
||||
|
||||
function resolveTitle(pathname: string, t: (key: string) => string): string {
|
||||
if (pageTitleKeys[pathname]) return t(pageTitleKeys[pathname]);
|
||||
if (pathname.startsWith("/extensions")) return t("navigation.extensions");
|
||||
return APP_TITLE;
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
onOpenMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function Header({ onOpenMobileSidebar }: HeaderProps) {
|
||||
const location = useLocation();
|
||||
const t = useT();
|
||||
const title = useHeaderTitle();
|
||||
const actions = useHeaderActions();
|
||||
|
||||
return (
|
||||
<header className="flex h-12 items-center gap-3 border-b border-border bg-background px-4 lg:px-6 shrink-0">
|
||||
{onOpenMobileSidebar && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenMobileSidebar}
|
||||
aria-label={t("navigation.openNavigation")}
|
||||
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent md:hidden"
|
||||
>
|
||||
<IconMenu2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{title ?? (
|
||||
<h1 className="text-lg font-semibold tracking-tight truncate">
|
||||
{resolveTitle(location.pathname, t)}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{actions}
|
||||
<AgentToggleButton />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
AgentSidebar,
|
||||
focusAgentChat,
|
||||
isAgentChatHomeHandoffActive,
|
||||
navigateWithAgentChatViewTransition,
|
||||
useAgentChatHomeHandoff,
|
||||
useAgentChatHomeHandoffLinks,
|
||||
} from "@agent-native/core/client/agent-chat";
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell";
|
||||
import { IconMenu2 } from "@tabler/icons-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
import { TAB_ID } from "@/lib/tab-id";
|
||||
|
||||
import { Header } from "./Header";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SIDEBAR_COLLAPSE_KEY = "chat.sidebar.collapsed";
|
||||
|
||||
/**
|
||||
* Routes whose page renders its own toolbar. Layout still wraps these with the
|
||||
* left Sidebar and agent surfaces but skips the global Header so they don't
|
||||
* double-stack chrome.
|
||||
*/
|
||||
function routeOwnsToolbar(pathname: string): boolean {
|
||||
return (
|
||||
pathname === "/" ||
|
||||
pathname.startsWith("/chat/") ||
|
||||
pathname === "/database" ||
|
||||
pathname.startsWith("/extensions")
|
||||
);
|
||||
}
|
||||
|
||||
export function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const t = useT();
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||
const isChatRoute =
|
||||
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
||||
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
||||
storageKey: "chat",
|
||||
activePath: location.pathname,
|
||||
enabled: !isChatRoute,
|
||||
});
|
||||
const chatHomeHandoffPending = isAgentChatHomeHandoffActive("chat");
|
||||
useAgentChatHomeHandoffLinks({
|
||||
storageKey: "chat",
|
||||
isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
|
||||
requireActiveHandoff: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMobileSidebarOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const closeMobileSidebar = () => setMobileSidebarOpen(false);
|
||||
window.addEventListener("agent-chat:open-thread", closeMobileSidebar);
|
||||
return () => {
|
||||
window.removeEventListener("agent-chat:open-thread", closeMobileSidebar);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = window.localStorage.getItem(SIDEBAR_COLLAPSE_KEY);
|
||||
if (stored !== null) setSidebarCollapsed(stored === "1");
|
||||
} catch {
|
||||
// Ignore storage access errors; the default collapsed state still works.
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
SIDEBAR_COLLAPSE_KEY,
|
||||
sidebarCollapsed ? "1" : "0",
|
||||
);
|
||||
} catch {
|
||||
// Ignore storage access errors.
|
||||
}
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
const ownsToolbar = routeOwnsToolbar(location.pathname);
|
||||
function openAskAgentFullscreen() {
|
||||
focusAgentChat();
|
||||
navigateWithAgentChatViewTransition(navigate, "/");
|
||||
}
|
||||
|
||||
const contentFrame = (
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{isChatRoute ? (
|
||||
<div className="flex h-12 shrink-0 items-center gap-3 border-b border-border bg-card px-3 md:hidden">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setMobileSidebarOpen(true)}
|
||||
aria-label={t("navigation.openNavigation")}
|
||||
>
|
||||
<IconMenu2 className="size-4" />
|
||||
</Button>
|
||||
<span className="truncate text-sm font-semibold">{APP_TITLE}</span>
|
||||
</div>
|
||||
) : ownsToolbar ? (
|
||||
<div className="flex h-12 shrink-0 items-center border-b border-border px-4 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileSidebarOpen(true)}
|
||||
aria-label={t("navigation.openNavigation")}
|
||||
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<IconMenu2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Header onOpenMobileSidebar={() => setMobileSidebarOpen(true)} />
|
||||
)}
|
||||
<main className="agent-native-app-main min-w-0 flex-1 overflow-y-auto overscroll-contain">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<HeaderActionsProvider>
|
||||
<div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<div className="agent-layout-left-drawer hidden md:block">
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onCollapsedChange={setSidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
||||
<SheetContent side="left" className="p-0 w-[260px]">
|
||||
<SheetTitle className="sr-only">
|
||||
{t("navigation.navigation")}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
{t("navigation.navigationDescription")}
|
||||
</SheetDescription>
|
||||
<Sidebar collapsed={false} collapsible={false} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{isChatRoute ? (
|
||||
<div className="agent-layout-main-surface flex min-w-0 flex-1 overflow-hidden">
|
||||
{contentFrame}
|
||||
</div>
|
||||
) : (
|
||||
<AgentSidebar
|
||||
position="right"
|
||||
chatViewTransition
|
||||
chatViewTransitionHandoff={chatHomeHandoffPending}
|
||||
storageKey="chat"
|
||||
browserTabId={TAB_ID}
|
||||
openOnChatRunning={chatHomeHandoffActive}
|
||||
onFullscreenRequest={openAskAgentFullscreen}
|
||||
emptyStateText={t("chat.inspectEmptyState")}
|
||||
agentPageHref="/agent"
|
||||
suggestions={[
|
||||
t("chat.inspectSuggestionCapabilities"),
|
||||
t("chat.inspectSuggestionHello"),
|
||||
t("chat.inspectSuggestionAction"),
|
||||
]}
|
||||
>
|
||||
{contentFrame}
|
||||
</AgentSidebar>
|
||||
)}
|
||||
</div>
|
||||
</HeaderActionsProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
import {
|
||||
navigateWithAgentChatViewTransition,
|
||||
useChatThreads,
|
||||
type ChatThreadSummary,
|
||||
} from "@agent-native/core/client/agent-chat";
|
||||
import { appPath } from "@agent-native/core/client/api-path";
|
||||
import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
|
||||
import { openCommandMenu } from "@agent-native/core/client/navigation";
|
||||
import { OrgSwitcher } from "@agent-native/core/client/org";
|
||||
import { FeedbackButton } from "@agent-native/core/client/ui";
|
||||
import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
|
||||
import {
|
||||
ChatHistoryRail,
|
||||
type ChatHistoryItem,
|
||||
} from "@agent-native/toolkit/chat-history";
|
||||
import {
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
IconMessageCircle,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
icon: IconMessageCircle,
|
||||
labelKey: "navigation.chat",
|
||||
href: "/",
|
||||
view: "chat",
|
||||
},
|
||||
];
|
||||
|
||||
const bottomNavItems = [
|
||||
{
|
||||
icon: IconSettings,
|
||||
labelKey: "navigation.settings",
|
||||
href: "/settings",
|
||||
view: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
const CHAT_STORAGE_KEY = "chat";
|
||||
const CHAT_ACTIVE_THREAD_KEY = `agent-chat-active-thread:${CHAT_STORAGE_KEY}`;
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed?: boolean;
|
||||
collapsible?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
function formatThreadAge(updatedAt: number) {
|
||||
const diffMs = Math.max(0, Date.now() - updatedAt);
|
||||
const minutes = Math.floor(diffMs / 60_000);
|
||||
if (minutes < 1) return "now";
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 7) return `${days}d`;
|
||||
return new Date(updatedAt).toLocaleDateString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function threadTitle(thread: ChatThreadSummary) {
|
||||
return thread.title || thread.preview || "Untitled chat";
|
||||
}
|
||||
|
||||
function threadUpdatedAt(thread: ChatThreadSummary) {
|
||||
return Number.isFinite(thread.updatedAt)
|
||||
? thread.updatedAt
|
||||
: Number.isFinite(thread.createdAt)
|
||||
? thread.createdAt
|
||||
: 0;
|
||||
}
|
||||
|
||||
function compareThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
|
||||
const aPinned = a.pinnedAt ?? 0;
|
||||
const bPinned = b.pinnedAt ?? 0;
|
||||
if (aPinned || bPinned) return bPinned - aPinned;
|
||||
return threadUpdatedAt(b) - threadUpdatedAt(a);
|
||||
}
|
||||
|
||||
function persistedActiveThreadId() {
|
||||
try {
|
||||
return localStorage.getItem(CHAT_ACTIVE_THREAD_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persistActiveThreadId(threadId: string) {
|
||||
try {
|
||||
localStorage.setItem(CHAT_ACTIVE_THREAD_KEY, threadId);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function threadIdFromPath(pathname: string) {
|
||||
const match = pathname.match(/^\/chat\/([^/]+)/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const value = decodeURIComponent(match[1]).trim();
|
||||
return value || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function chatThreadPath(threadId: string) {
|
||||
return `/chat/${encodeURIComponent(threadId)}`;
|
||||
}
|
||||
|
||||
function ChatThreadsSection({ open }: { open: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const t = useT();
|
||||
const {
|
||||
threads,
|
||||
activeThreadId,
|
||||
createThread,
|
||||
switchThread,
|
||||
pinThread,
|
||||
archiveThread,
|
||||
renameThread,
|
||||
refreshThreads,
|
||||
} = useChatThreads(undefined, CHAT_STORAGE_KEY, undefined, {
|
||||
autoCreate: false,
|
||||
restoreActiveThread: false,
|
||||
});
|
||||
|
||||
const visibleThreads = useMemo(
|
||||
() =>
|
||||
threads
|
||||
.filter((thread) => thread.messageCount > 0 && !thread.archivedAt)
|
||||
.sort(compareThreads)
|
||||
.slice(0, 15),
|
||||
[threads],
|
||||
);
|
||||
const displayedActiveThreadId =
|
||||
threadIdFromPath(location.pathname) ??
|
||||
(location.pathname === "/" ? null : activeThreadId);
|
||||
const chatItems = useMemo<ChatHistoryItem[]>(
|
||||
() =>
|
||||
visibleThreads.map((thread) => ({
|
||||
id: thread.id,
|
||||
title: threadTitle(thread),
|
||||
titleText: threadTitle(thread),
|
||||
timestamp:
|
||||
thread.id === displayedActiveThreadId
|
||||
? undefined
|
||||
: formatThreadAge(threadUpdatedAt(thread)),
|
||||
pinned: Boolean(thread.pinnedAt),
|
||||
})),
|
||||
[displayedActiveThreadId, visibleThreads],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => refreshThreads();
|
||||
const handleRunning = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail as
|
||||
| { isRunning?: unknown }
|
||||
| undefined;
|
||||
if (typeof detail?.isRunning === "boolean") refreshThreads();
|
||||
};
|
||||
|
||||
window.addEventListener("agent-chat:threads-updated", refresh);
|
||||
window.addEventListener("agentNative.chatRunning", handleRunning);
|
||||
window.addEventListener("focus", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("agent-chat:threads-updated", refresh);
|
||||
window.removeEventListener("agentNative.chatRunning", handleRunning);
|
||||
window.removeEventListener("focus", refresh);
|
||||
};
|
||||
}, [refreshThreads]);
|
||||
|
||||
function openThread(threadId: string, options?: { isNew?: boolean }) {
|
||||
switchThread(threadId);
|
||||
persistActiveThreadId(threadId);
|
||||
navigateWithAgentChatViewTransition(
|
||||
navigate,
|
||||
options?.isNew ? "/" : chatThreadPath(threadId),
|
||||
);
|
||||
window.requestAnimationFrame(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("agent-chat:open-thread", {
|
||||
detail: { threadId, newThread: options?.isNew === true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleNewChat() {
|
||||
const threadId = await createThread();
|
||||
if (threadId) openThread(threadId, { isNew: true });
|
||||
}
|
||||
|
||||
async function handleArchiveThread(threadId: string) {
|
||||
const wasActive =
|
||||
threadId === activeThreadId || threadId === persistedActiveThreadId();
|
||||
const archived = await archiveThread(threadId);
|
||||
if (!archived) {
|
||||
toast.error(t("chat.archiveFailed"));
|
||||
return;
|
||||
}
|
||||
if (wasActive) {
|
||||
await handleNewChat();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRenameThread(threadId: string, title: string) {
|
||||
void renameThread(threadId, title).then((renamed) => {
|
||||
if (!renamed) toast.error(t("chat.renameFailed"));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="an-chat-history-rail__collapse"
|
||||
data-state={open ? "open" : "closed"}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div className="ms-4">
|
||||
<ChatHistoryRail
|
||||
items={chatItems}
|
||||
activeId={displayedActiveThreadId}
|
||||
onSelect={(threadId) => openThread(threadId)}
|
||||
onNewChat={() => void handleNewChat()}
|
||||
railLabels={{
|
||||
newChat: t("chat.newChat"),
|
||||
showMore: t("chat.chats"),
|
||||
showLess: t("chat.chats"),
|
||||
}}
|
||||
renameMaxLength={160}
|
||||
onTogglePin={(threadId) => {
|
||||
const thread = visibleThreads.find((item) => item.id === threadId);
|
||||
if (thread) void pinThread(threadId, !thread.pinnedAt);
|
||||
}}
|
||||
onRename={handleRenameThread}
|
||||
onDelete={(threadId) => void handleArchiveThread(threadId)}
|
||||
labels={{
|
||||
options: (item) =>
|
||||
t("chat.optionsFor", { title: item.titleText ?? "" }),
|
||||
renameInput: (item) =>
|
||||
t("chat.renameThread", { title: item.titleText ?? "" }),
|
||||
rename: t("chat.renameChat"),
|
||||
pin: t("chat.pinChat"),
|
||||
unpin: t("chat.unpinChat"),
|
||||
delete: t("chat.archiveChat"),
|
||||
}}
|
||||
className="min-w-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
collapsed = false,
|
||||
collapsible = true,
|
||||
onCollapsedChange,
|
||||
}: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const t = useT();
|
||||
const isChatRoute =
|
||||
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
||||
const ToggleIcon = collapsed
|
||||
? IconLayoutSidebarLeftExpand
|
||||
: IconLayoutSidebarLeftCollapse;
|
||||
const navClass = ({ isActive }: { isActive: boolean }) =>
|
||||
cn(
|
||||
"flex items-center text-sm transition-colors",
|
||||
collapsed
|
||||
? "relative h-10 w-full justify-center rounded-none px-0"
|
||||
: "h-9 rounded-md gap-3 px-3",
|
||||
isActive
|
||||
? collapsed
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||
: "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||
: collapsed
|
||||
? "text-sidebar-foreground/70 hover:bg-sidebar-accent/55 hover:text-sidebar-accent-foreground"
|
||||
: "text-sidebar-foreground hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground",
|
||||
);
|
||||
const collapseButton = collapsible ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCollapsedChange?.(!collapsed)}
|
||||
className={cn(
|
||||
"flex shrink-0 items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
collapsed ? "size-8" : "size-7",
|
||||
)}
|
||||
aria-label={
|
||||
collapsed
|
||||
? t("navigation.expandSidebar")
|
||||
: t("navigation.collapseSidebar")
|
||||
}
|
||||
>
|
||||
<ToggleIcon className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{collapsed
|
||||
? t("navigation.expandSidebar")
|
||||
: t("navigation.collapseSidebar")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
const searchButton = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCommandMenu}
|
||||
aria-label={t("root.commandSearch")}
|
||||
className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<IconSearch className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{t("root.commandSearch")}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
const translateButton = (
|
||||
<LanguagePicker variant="ghost-icon" label={t("settings.languageLabel")} />
|
||||
);
|
||||
const feedbackButton = (
|
||||
<FeedbackButton
|
||||
variant={collapsed ? "icon" : "sidebar"}
|
||||
side="right"
|
||||
className={collapsed ? "h-8 w-8" : "min-w-0"}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
data-collapsed={collapsed ? "true" : "false"}
|
||||
className={cn(
|
||||
"flex h-full min-w-0 shrink-0 flex-col overflow-hidden border-e border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 ease-out",
|
||||
collapsed ? "w-12" : "w-60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex shrink-0 items-center border-b border-sidebar-border",
|
||||
collapsed ? "h-12 justify-center px-0" : "h-14 px-3",
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
className={cn(
|
||||
"flex min-w-0 items-center rounded outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
collapsed ? "size-7 justify-center" : "flex-1 gap-3",
|
||||
)}
|
||||
aria-label={collapsed ? APP_TITLE : undefined}
|
||||
>
|
||||
<img
|
||||
src={appPath("/agent-native-icon-light.svg")}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="block h-4 w-auto shrink-0 dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={appPath("/agent-native-icon-dark.svg")}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="hidden h-4 w-auto shrink-0 dark:block"
|
||||
/>
|
||||
<div className={cn("min-w-0", collapsed && "sr-only")}>
|
||||
<p className="truncate text-sm font-semibold text-sidebar-accent-foreground">
|
||||
{APP_TITLE}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto",
|
||||
collapsed ? "px-0 py-2" : "px-2 py-3",
|
||||
)}
|
||||
>
|
||||
<div className={cn("grid", collapsed ? "gap-0" : "gap-1")}>
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive =
|
||||
item.href === "/"
|
||||
? isChatRoute
|
||||
: location.pathname.startsWith(item.href);
|
||||
const link = (
|
||||
<Link
|
||||
to={item.href}
|
||||
onClick={(event) => {
|
||||
if (
|
||||
item.href === "/" &&
|
||||
!isChatRoute &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigateWithAgentChatViewTransition(navigate, "/");
|
||||
}
|
||||
}}
|
||||
className={navClass({ isActive })}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-label={collapsed ? t(item.labelKey) : undefined}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className={collapsed ? "sr-only" : "truncate"}>
|
||||
{t(item.labelKey)}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
return (
|
||||
<div key={item.href}>
|
||||
{collapsed ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{link}</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{t(item.labelKey)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
link
|
||||
)}
|
||||
{!collapsed && item.view === "chat" ? (
|
||||
<ChatThreadsSection open={isChatRoute} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className={cn("mt-auto shrink-0", collapsed && "py-2")}>
|
||||
<nav
|
||||
className={cn(
|
||||
"grid",
|
||||
collapsed ? "gap-0 px-1 py-1" : "gap-1 px-2 py-1",
|
||||
)}
|
||||
>
|
||||
{bottomNavItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location.pathname.startsWith(item.href);
|
||||
const link = (
|
||||
<Link
|
||||
to={item.href}
|
||||
className={navClass({ isActive })}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-label={collapsed ? t(item.labelKey) : undefined}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className={collapsed ? "sr-only" : "truncate"}>
|
||||
{t(item.labelKey)}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
return collapsed ? (
|
||||
<Tooltip key={item.href}>
|
||||
<TooltipTrigger asChild>{link}</TooltipTrigger>
|
||||
<TooltipContent side="right">{t(item.labelKey)}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div key={item.href}>{link}</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className={cn(collapsed ? "px-1 py-1" : "px-3 py-2")}>
|
||||
<OrgSwitcher
|
||||
reserveSpace
|
||||
className={
|
||||
collapsed
|
||||
? "h-8 justify-center px-0 [&>span]:sr-only [&>svg:last-child]:hidden"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarFooterActions
|
||||
collapsed={collapsed}
|
||||
feedback={feedbackButton}
|
||||
translate={translateButton}
|
||||
search={searchButton}
|
||||
collapse={collapseButton}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/button";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/card";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/dropdown-menu";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/input";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/label";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/sheet";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ToolkitProvider, type ToolkitComponents } from "@agent-native/toolkit";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { designSystem } from "@/design-system";
|
||||
|
||||
const components: ToolkitComponents = {
|
||||
Button: Button as ToolkitComponents["Button"],
|
||||
};
|
||||
|
||||
export function AppToolkitProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ToolkitProvider components={components} designSystem={designSystem}>
|
||||
{children}
|
||||
</ToolkitProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@agent-native/toolkit/ui/tooltip";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineDesignSystem } from "@agent-native/toolkit/design-system";
|
||||
|
||||
export const designSystem = defineDesignSystem({});
|
||||
@@ -0,0 +1,19 @@
|
||||
// Import only the tiny URL-helper module, not the full client barrel.
|
||||
// This keeps the ~650-700 KB gzip chat stack off the static import closure
|
||||
// of the client entry point so it can't block the first page parse.
|
||||
import { appBasePath } from "@agent-native/core/client/api-path";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
|
||||
const basePath = appBasePath();
|
||||
const pathname = window.location.pathname;
|
||||
const routerBasePath =
|
||||
basePath && (pathname === basePath || pathname.startsWith(`${basePath}/`))
|
||||
? basePath
|
||||
: "";
|
||||
const context = (
|
||||
window as Window & { __reactRouterContext?: { basename?: string } }
|
||||
).__reactRouterContext;
|
||||
if (context) context.basename = routerBasePath;
|
||||
|
||||
hydrateRoot(document, <HydratedRouter />);
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
createDocumentRequestHandler,
|
||||
streamTimeout,
|
||||
} from "@agent-native/core/server/entry-server";
|
||||
import { ServerRouter } from "react-router";
|
||||
|
||||
const handleDocumentRequest = createDocumentRequestHandler(ServerRouter);
|
||||
|
||||
export { streamTimeout };
|
||||
export default handleDocumentRequest;
|
||||
@@ -0,0 +1,93 @@
|
||||
/* Self-hosted via @fontsource-variable/inter — no render-blocking Google Fonts request */
|
||||
@import "@fontsource-variable/inter";
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "@agent-native/core/styles/agent-native.css";
|
||||
@import "@agent-native/toolkit/styles.css";
|
||||
|
||||
@source "./**/*.{ts,tsx}";
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 10%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 10%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 10%;
|
||||
--primary: 0 0% 15%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 0 0% 95%;
|
||||
--secondary-foreground: 0 0% 15%;
|
||||
--muted: 0 0% 95%;
|
||||
--muted-foreground: 0 0% 45%;
|
||||
--accent: 0 0% 95%;
|
||||
--accent-foreground: 0 0% 15%;
|
||||
--destructive: 0 91% 71%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 90%;
|
||||
--input: 0 0% 90%;
|
||||
--ring: 0 0% 40%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 97%;
|
||||
--sidebar-foreground: 0 0% 45%;
|
||||
--sidebar-primary: 0 0% 15%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 95%;
|
||||
--sidebar-accent-foreground: 0 0% 15%;
|
||||
--sidebar-border: 0 0% 90%;
|
||||
--sidebar-ring: 0 0% 40%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 13%;
|
||||
--foreground: 0 0% 90%;
|
||||
--card: 0 0% 15%;
|
||||
--card-foreground: 0 0% 90%;
|
||||
--popover: 0 0% 15%;
|
||||
--popover-foreground: 0 0% 90%;
|
||||
--primary: 0 0% 75%;
|
||||
--primary-foreground: 0 0% 10%;
|
||||
--secondary: 0 0% 18%;
|
||||
--secondary-foreground: 0 0% 90%;
|
||||
--muted: 0 0% 16%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
--accent: 0 0% 18%;
|
||||
--accent-foreground: 0 0% 90%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 24%;
|
||||
--input: 0 0% 24%;
|
||||
--ring: 0 0% 60%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 10%;
|
||||
--sidebar-foreground: 0 0% 60%;
|
||||
--sidebar-primary: 0 0% 75%;
|
||||
--sidebar-primary-foreground: 0 0% 10%;
|
||||
--sidebar-accent: 0 0% 16%;
|
||||
--sidebar-accent-foreground: 0 0% 90%;
|
||||
--sidebar-border: 0 0% 20%;
|
||||
--sidebar-ring: 0 0% 60%;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: "Inter Variable", "Inter", sans-serif;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground) / 0.4);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { appBasePath, appPath } from "@agent-native/core/client/api-path";
|
||||
import { useAgentRouteState } from "@agent-native/core/client/navigation";
|
||||
|
||||
import { TAB_ID } from "@/lib/tab-id";
|
||||
|
||||
export interface NavigationState {
|
||||
view: string;
|
||||
path?: string;
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
export function useNavigationState() {
|
||||
useAgentRouteState<NavigationState>({
|
||||
browserTabId: TAB_ID,
|
||||
requestSource: TAB_ID,
|
||||
getNavigationState: ({ pathname }) => {
|
||||
const threadId = threadIdFromPath(pathname);
|
||||
return {
|
||||
view: viewForPath(pathname),
|
||||
path: appPath(pathname),
|
||||
...(threadId ? { threadId } : {}),
|
||||
};
|
||||
},
|
||||
getCommandPath: (command) =>
|
||||
routerPath(command.path || pathForCommand(command)),
|
||||
});
|
||||
}
|
||||
|
||||
function threadIdFromPath(pathname: string): string | null {
|
||||
const match = pathname.match(/^\/chat\/([^/]+)/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const value = decodeURIComponent(match[1]).trim();
|
||||
return value || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function viewForPath(pathname: string): string {
|
||||
if (isChatPath(pathname)) return "chat";
|
||||
if (pathname.startsWith("/database")) return "database";
|
||||
if (pathname.startsWith("/extensions")) return "extensions";
|
||||
if (pathname.startsWith("/observability")) return "observability";
|
||||
if (pathname.startsWith("/agent")) return "agent";
|
||||
if (pathname.startsWith("/team")) return "settings";
|
||||
return "chat";
|
||||
}
|
||||
|
||||
function pathForView(view?: string): string {
|
||||
switch (view) {
|
||||
case "chat":
|
||||
case "home":
|
||||
case "ask":
|
||||
return "/";
|
||||
case "database":
|
||||
return "/database";
|
||||
case "extensions":
|
||||
return "/extensions";
|
||||
case "observability":
|
||||
return "/observability";
|
||||
case "agent":
|
||||
return "/agent";
|
||||
case "settings":
|
||||
return "/settings";
|
||||
case "team":
|
||||
return "/settings#organization";
|
||||
default:
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
function pathForCommand(command: any): string {
|
||||
const path = pathForView(command?.view);
|
||||
if (path !== "/") return path;
|
||||
const threadId =
|
||||
typeof command?.threadId === "string" ? command.threadId.trim() : "";
|
||||
return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
|
||||
}
|
||||
|
||||
function routerPath(path: string): string {
|
||||
const basePath = appBasePath();
|
||||
if (!basePath) return path;
|
||||
if (path === basePath) return "/";
|
||||
if (path.startsWith(`${basePath}/`)) {
|
||||
return path.slice(basePath.length) || "/";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function isChatPath(pathname: string): boolean {
|
||||
return pathname === "/" || pathname.startsWith("/chat/");
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
import { type LocaleCode } from "@agent-native/core/client/i18n";
|
||||
|
||||
import zhTW from "./i18n/zh-TW";
|
||||
|
||||
const enUS = {
|
||||
root: {
|
||||
commandActions: "Actions",
|
||||
commandSearch: "Search",
|
||||
commandAppearance: "Appearance",
|
||||
toggleTheme: "Toggle theme",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
observability: "Observability",
|
||||
database: "Database",
|
||||
team: "Team",
|
||||
settings: "Settings",
|
||||
extensions: "Extensions",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "App navigation links",
|
||||
openNavigation: "Open navigation",
|
||||
expandSidebar: "Expand sidebar",
|
||||
collapseSidebar: "Collapse sidebar",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "Agent Observability",
|
||||
observabilityPageTitle: "Observability",
|
||||
databaseTitle: "Database",
|
||||
teamTitle: "Team",
|
||||
teamCreateOrgDescription:
|
||||
"Set up a team to share this app with your colleagues.",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "Ask anything, then customize the app when you need more.",
|
||||
composerPlaceholder: "Message the agent...",
|
||||
heroTitle: "How can I help?",
|
||||
heroDescription:
|
||||
"Chat about anything. Add actions, components, pages, jobs, or your own agent backend when you want this app to do more.",
|
||||
suggestionCapabilities: "What can you do?",
|
||||
suggestionCustomize: "Help me customize this chat app",
|
||||
suggestionActions: "Show me the actions and pages I can add",
|
||||
inspectEmptyState: "Ask the agent to inspect or change this app.",
|
||||
inspectSuggestionCapabilities: "What can you do here?",
|
||||
inspectSuggestionHello: "Call hello for Builder",
|
||||
inspectSuggestionAction: "Add a new action and show it in the UI",
|
||||
chats: "Chats",
|
||||
newChat: "New chat",
|
||||
renameChat: "Rename chat",
|
||||
pinChat: "Pin chat",
|
||||
unpinChat: "Unpin chat",
|
||||
archiveChat: "Archive chat",
|
||||
archiveFailed: "Could not archive chat.",
|
||||
renameFailed: "Could not rename chat.",
|
||||
renameThread: "Rename {{title}}",
|
||||
optionsFor: "Chat options for {{title}}",
|
||||
},
|
||||
};
|
||||
|
||||
type Messages = typeof enUS;
|
||||
type PartialMessages = { [K in keyof Messages]?: Partial<Messages[K]> };
|
||||
|
||||
function mergeMessages(overrides: PartialMessages): Messages {
|
||||
return {
|
||||
root: { ...enUS.root, ...overrides.root },
|
||||
navigation: { ...enUS.navigation, ...overrides.navigation },
|
||||
pages: { ...enUS.pages, ...overrides.pages },
|
||||
chat: { ...enUS.chat, ...overrides.chat },
|
||||
};
|
||||
}
|
||||
|
||||
export const messagesByLocale = {
|
||||
"en-US": enUS,
|
||||
"zh-TW": mergeMessages(zhTW),
|
||||
"zh-CN": mergeMessages({
|
||||
root: {
|
||||
commandActions: "操作",
|
||||
commandSearch: "搜索",
|
||||
commandAppearance: "外观",
|
||||
toggleTheme: "切换主题",
|
||||
},
|
||||
navigation: {
|
||||
chat: "聊天",
|
||||
observability: "可观测性",
|
||||
database: "数据库",
|
||||
team: "团队",
|
||||
settings: "设置",
|
||||
extensions: "扩展",
|
||||
navigation: "导航",
|
||||
navigationDescription: "应用导航链接",
|
||||
openNavigation: "打开导航",
|
||||
expandSidebar: "展开侧边栏",
|
||||
collapseSidebar: "收起侧边栏",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "代理可观测性",
|
||||
observabilityPageTitle: "可观测性",
|
||||
databaseTitle: "数据库",
|
||||
teamTitle: "团队",
|
||||
teamCreateOrgDescription: "设置团队,与同事共享此应用。",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "随便提问,需要更多功能时再自定义应用。",
|
||||
composerPlaceholder: "给代理发消息...",
|
||||
heroTitle: "我能帮你什么?",
|
||||
heroDescription:
|
||||
"可以聊任何内容。需要此应用做更多事时,可添加操作、组件、页面、任务或自己的代理后端。",
|
||||
suggestionCapabilities: "你能做什么?",
|
||||
suggestionCustomize: "帮我自定义这个聊天应用",
|
||||
suggestionActions: "展示我可以添加的操作和页面",
|
||||
inspectEmptyState: "让代理检查或修改此应用。",
|
||||
inspectSuggestionCapabilities: "你在这里能做什么?",
|
||||
inspectSuggestionHello: "为 Builder 调用 hello",
|
||||
inspectSuggestionAction: "添加新操作并显示在 UI 中",
|
||||
chats: "聊天",
|
||||
newChat: "新聊天",
|
||||
renameChat: "重命名聊天",
|
||||
pinChat: "置顶聊天",
|
||||
unpinChat: "取消置顶聊天",
|
||||
archiveChat: "归档聊天",
|
||||
archiveFailed: "无法归档聊天。",
|
||||
renameFailed: "无法重命名聊天。",
|
||||
renameThread: "重命名 {{title}}",
|
||||
optionsFor: "{{title}} 的聊天选项",
|
||||
},
|
||||
}),
|
||||
"es-ES": mergeMessages({
|
||||
root: {
|
||||
commandActions: "Acciones",
|
||||
commandSearch: "Buscar",
|
||||
commandAppearance: "Apariencia",
|
||||
toggleTheme: "Cambiar tema",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
observability: "Observabilidad",
|
||||
database: "Base de datos",
|
||||
team: "Equipo",
|
||||
settings: "Ajustes",
|
||||
extensions: "Extensiones",
|
||||
navigation: "Navegación",
|
||||
navigationDescription: "Enlaces de navegación de la app",
|
||||
openNavigation: "Abrir navegación",
|
||||
expandSidebar: "Expandir barra lateral",
|
||||
collapseSidebar: "Contraer barra lateral",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "Observabilidad del agente",
|
||||
observabilityPageTitle: "Observabilidad",
|
||||
databaseTitle: "Base de datos",
|
||||
teamTitle: "Equipo",
|
||||
teamCreateOrgDescription:
|
||||
"Configura un equipo para compartir esta app con tus compañeros.",
|
||||
},
|
||||
chat: {
|
||||
emptyState:
|
||||
"Pregunta cualquier cosa y personaliza la app cuando necesites más.",
|
||||
composerPlaceholder: "Mensaje al agente...",
|
||||
heroTitle: "¿En qué puedo ayudar?",
|
||||
heroDescription:
|
||||
"Chatea sobre cualquier cosa. Añade acciones, componentes, páginas, trabajos o tu propio backend de agente cuando quieras más.",
|
||||
suggestionCapabilities: "¿Qué puedes hacer?",
|
||||
suggestionCustomize: "Ayúdame a personalizar esta app de chat",
|
||||
suggestionActions: "Muéstrame las acciones y páginas que puedo añadir",
|
||||
inspectEmptyState: "Pide al agente que inspeccione o cambie esta app.",
|
||||
inspectSuggestionCapabilities: "¿Qué puedes hacer aquí?",
|
||||
inspectSuggestionHello: "Llama a hello para Builder",
|
||||
inspectSuggestionAction: "Añade una acción nueva y muéstrala en la UI",
|
||||
chats: "Chats",
|
||||
newChat: "Nuevo chat",
|
||||
renameChat: "Renombrar chat",
|
||||
pinChat: "Fijar chat",
|
||||
unpinChat: "Desfijar chat",
|
||||
archiveChat: "Archivar chat",
|
||||
archiveFailed: "No se pudo archivar el chat.",
|
||||
renameFailed: "No se pudo renombrar el chat.",
|
||||
renameThread: "Renombrar {{title}}",
|
||||
optionsFor: "Opciones de chat para {{title}}",
|
||||
},
|
||||
}),
|
||||
"fr-FR": mergeMessages({
|
||||
root: {
|
||||
commandActions: "Actions",
|
||||
commandSearch: "Rechercher",
|
||||
commandAppearance: "Apparence",
|
||||
toggleTheme: "Changer de thème",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
observability: "Observabilité",
|
||||
database: "Base de données",
|
||||
team: "Équipe",
|
||||
settings: "Paramètres",
|
||||
extensions: "Extensions",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "Liens de navigation de l'app",
|
||||
openNavigation: "Ouvrir la navigation",
|
||||
expandSidebar: "Développer la barre latérale",
|
||||
collapseSidebar: "Réduire la barre latérale",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "Observabilité de l’agent",
|
||||
observabilityPageTitle: "Observabilité",
|
||||
databaseTitle: "Base de données",
|
||||
teamTitle: "Équipe",
|
||||
teamCreateOrgDescription:
|
||||
"Configurez une équipe pour partager cette app avec vos collègues.",
|
||||
},
|
||||
chat: {
|
||||
emptyState:
|
||||
"Demandez n'importe quoi, puis personnalisez l'app au besoin.",
|
||||
composerPlaceholder: "Message à l'agent...",
|
||||
heroTitle: "Comment puis-je aider ?",
|
||||
heroDescription:
|
||||
"Discutez de tout. Ajoutez actions, composants, pages, jobs ou votre propre backend d'agent lorsque vous voulez aller plus loin.",
|
||||
suggestionCapabilities: "Que peux-tu faire ?",
|
||||
suggestionCustomize: "Aide-moi à personnaliser cette app de chat",
|
||||
suggestionActions: "Montre-moi les actions et pages que je peux ajouter",
|
||||
inspectEmptyState:
|
||||
"Demandez à l'agent d'inspecter ou modifier cette app.",
|
||||
inspectSuggestionCapabilities: "Que peux-tu faire ici ?",
|
||||
inspectSuggestionHello: "Appelle hello pour Builder",
|
||||
inspectSuggestionAction: "Ajoute une action et montre-la dans l'UI",
|
||||
chats: "Chats",
|
||||
newChat: "Nouveau chat",
|
||||
renameChat: "Renommer le chat",
|
||||
pinChat: "Épingler le chat",
|
||||
unpinChat: "Désépingler le chat",
|
||||
archiveChat: "Archiver le chat",
|
||||
archiveFailed: "Impossible d'archiver le chat.",
|
||||
renameFailed: "Impossible de renommer le chat.",
|
||||
renameThread: "Renommer {{title}}",
|
||||
optionsFor: "Options du chat pour {{title}}",
|
||||
},
|
||||
}),
|
||||
"de-DE": mergeMessages({
|
||||
root: {
|
||||
commandActions: "Aktionen",
|
||||
commandSearch: "Suchen",
|
||||
commandAppearance: "Darstellung",
|
||||
toggleTheme: "Theme wechseln",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
observability: "Observability",
|
||||
database: "Datenbank",
|
||||
team: "Team",
|
||||
settings: "Einstellungen",
|
||||
extensions: "Erweiterungen",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "App-Navigationslinks",
|
||||
openNavigation: "Navigation öffnen",
|
||||
expandSidebar: "Seitenleiste erweitern",
|
||||
collapseSidebar: "Seitenleiste einklappen",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "Agent-Beobachtbarkeit",
|
||||
observabilityPageTitle: "Beobachtbarkeit",
|
||||
databaseTitle: "Datenbank",
|
||||
teamTitle: "Team",
|
||||
teamCreateOrgDescription:
|
||||
"Richte ein Team ein, um diese App mit deinen Kollegen zu teilen.",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "Frag alles und passe die App an, wenn du mehr brauchst.",
|
||||
composerPlaceholder: "Nachricht an den Agenten...",
|
||||
heroTitle: "Wie kann ich helfen?",
|
||||
heroDescription:
|
||||
"Chatte über alles. Füge Aktionen, Komponenten, Seiten, Jobs oder dein eigenes Agent-Backend hinzu, wenn die App mehr tun soll.",
|
||||
suggestionCapabilities: "Was kannst du tun?",
|
||||
suggestionCustomize: "Hilf mir, diese Chat-App anzupassen",
|
||||
suggestionActions:
|
||||
"Zeige mir Aktionen und Seiten, die ich hinzufügen kann",
|
||||
inspectEmptyState:
|
||||
"Bitte den Agenten, diese App zu prüfen oder zu ändern.",
|
||||
inspectSuggestionCapabilities: "Was kannst du hier tun?",
|
||||
inspectSuggestionHello: "Rufe hello für Builder auf",
|
||||
inspectSuggestionAction:
|
||||
"Füge eine neue Aktion hinzu und zeige sie in der UI",
|
||||
chats: "Chats",
|
||||
newChat: "Neuer Chat",
|
||||
renameChat: "Chat umbenennen",
|
||||
pinChat: "Chat anheften",
|
||||
unpinChat: "Chat lösen",
|
||||
archiveChat: "Chat archivieren",
|
||||
archiveFailed: "Chat konnte nicht archiviert werden.",
|
||||
renameFailed: "Chat konnte nicht umbenannt werden.",
|
||||
renameThread: "{{title}} umbenennen",
|
||||
optionsFor: "Chatoptionen für {{title}}",
|
||||
},
|
||||
}),
|
||||
"ja-JP": mergeMessages({
|
||||
root: {
|
||||
commandActions: "操作",
|
||||
commandSearch: "検索",
|
||||
commandAppearance: "外観",
|
||||
toggleTheme: "テーマを切り替え",
|
||||
},
|
||||
navigation: {
|
||||
chat: "チャット",
|
||||
observability: "可観測性",
|
||||
database: "データベース",
|
||||
team: "チーム",
|
||||
settings: "設定",
|
||||
extensions: "拡張機能",
|
||||
navigation: "ナビゲーション",
|
||||
navigationDescription: "アプリのナビゲーションリンク",
|
||||
openNavigation: "ナビゲーションを開く",
|
||||
expandSidebar: "サイドバーを展開",
|
||||
collapseSidebar: "サイドバーを折りたたむ",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "エージェントの可観測性",
|
||||
observabilityPageTitle: "可観測性",
|
||||
databaseTitle: "データベース",
|
||||
teamTitle: "チーム",
|
||||
teamCreateOrgDescription:
|
||||
"同僚とこのアプリを共有するためのチームを設定します。",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "何でも質問し、必要になったらアプリをカスタマイズできます。",
|
||||
composerPlaceholder: "エージェントにメッセージ...",
|
||||
heroTitle: "どうお手伝いできますか?",
|
||||
heroDescription:
|
||||
"何でもチャットできます。必要に応じてアクション、コンポーネント、ページ、ジョブ、独自のエージェントバックエンドを追加できます。",
|
||||
suggestionCapabilities: "何ができますか?",
|
||||
suggestionCustomize: "このチャットアプリをカスタマイズして",
|
||||
suggestionActions: "追加できるアクションとページを見せて",
|
||||
inspectEmptyState: "エージェントにこのアプリの確認や変更を依頼します。",
|
||||
inspectSuggestionCapabilities: "ここで何ができますか?",
|
||||
inspectSuggestionHello: "Builder に hello を呼び出す",
|
||||
inspectSuggestionAction: "新しいアクションを追加して UI に表示",
|
||||
chats: "チャット",
|
||||
newChat: "新しいチャット",
|
||||
renameChat: "チャット名を変更",
|
||||
pinChat: "チャットを固定",
|
||||
unpinChat: "固定を解除",
|
||||
archiveChat: "チャットをアーカイブ",
|
||||
archiveFailed: "チャットをアーカイブできませんでした。",
|
||||
renameFailed: "チャット名を変更できませんでした。",
|
||||
renameThread: "{{title}} の名前を変更",
|
||||
optionsFor: "{{title}} のチャットオプション",
|
||||
},
|
||||
}),
|
||||
"ko-KR": mergeMessages({
|
||||
root: {
|
||||
commandActions: "작업",
|
||||
commandSearch: "검색",
|
||||
commandAppearance: "모양",
|
||||
toggleTheme: "테마 전환",
|
||||
},
|
||||
navigation: {
|
||||
chat: "채팅",
|
||||
observability: "관측성",
|
||||
database: "데이터베이스",
|
||||
team: "팀",
|
||||
settings: "설정",
|
||||
extensions: "확장",
|
||||
navigation: "탐색",
|
||||
navigationDescription: "앱 탐색 링크",
|
||||
openNavigation: "탐색 열기",
|
||||
expandSidebar: "사이드바 펼치기",
|
||||
collapseSidebar: "사이드바 접기",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "에이전트 관찰 가능성",
|
||||
observabilityPageTitle: "관찰 가능성",
|
||||
databaseTitle: "데이터베이스",
|
||||
teamTitle: "팀",
|
||||
teamCreateOrgDescription: "동료와 이 앱을 공유할 팀을 설정하세요.",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "무엇이든 물어보고, 더 필요할 때 앱을 사용자 지정하세요.",
|
||||
composerPlaceholder: "에이전트에게 메시지...",
|
||||
heroTitle: "무엇을 도와드릴까요?",
|
||||
heroDescription:
|
||||
"무엇이든 채팅하세요. 앱이 더 많은 일을 하게 하고 싶을 때 작업, 컴포넌트, 페이지, 작업 또는 자체 에이전트 백엔드를 추가하세요.",
|
||||
suggestionCapabilities: "무엇을 할 수 있나요?",
|
||||
suggestionCustomize: "이 채팅 앱을 사용자 지정하게 도와줘",
|
||||
suggestionActions: "추가할 수 있는 작업과 페이지를 보여줘",
|
||||
inspectEmptyState:
|
||||
"에이전트에게 이 앱을 검사하거나 변경하도록 요청하세요.",
|
||||
inspectSuggestionCapabilities: "여기서 무엇을 할 수 있나요?",
|
||||
inspectSuggestionHello: "Builder용 hello 호출",
|
||||
inspectSuggestionAction: "새 작업을 추가하고 UI에 보여줘",
|
||||
chats: "채팅",
|
||||
newChat: "새 채팅",
|
||||
renameChat: "채팅 이름 변경",
|
||||
pinChat: "채팅 고정",
|
||||
unpinChat: "채팅 고정 해제",
|
||||
archiveChat: "채팅 보관",
|
||||
archiveFailed: "채팅을 보관할 수 없습니다.",
|
||||
renameFailed: "채팅 이름을 변경할 수 없습니다.",
|
||||
renameThread: "{{title}} 이름 변경",
|
||||
optionsFor: "{{title}} 채팅 옵션",
|
||||
},
|
||||
}),
|
||||
"pt-BR": mergeMessages({
|
||||
root: {
|
||||
commandActions: "Ações",
|
||||
commandSearch: "Buscar",
|
||||
commandAppearance: "Aparência",
|
||||
toggleTheme: "Alternar tema",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
observability: "Observabilidade",
|
||||
database: "Banco de dados",
|
||||
team: "Equipe",
|
||||
settings: "Configurações",
|
||||
extensions: "Extensões",
|
||||
navigation: "Navegação",
|
||||
navigationDescription: "Links de navegação do app",
|
||||
openNavigation: "Abrir navegação",
|
||||
expandSidebar: "Expandir barra lateral",
|
||||
collapseSidebar: "Recolher barra lateral",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "Observabilidade do agente",
|
||||
observabilityPageTitle: "Observabilidade",
|
||||
databaseTitle: "Banco de dados",
|
||||
teamTitle: "Equipe",
|
||||
teamCreateOrgDescription:
|
||||
"Configure uma equipe para compartilhar este app com seus colegas.",
|
||||
},
|
||||
chat: {
|
||||
emptyState:
|
||||
"Pergunte qualquer coisa e personalize o app quando precisar.",
|
||||
composerPlaceholder: "Mensagem para o agente...",
|
||||
heroTitle: "Como posso ajudar?",
|
||||
heroDescription:
|
||||
"Converse sobre qualquer coisa. Adicione ações, componentes, páginas, jobs ou seu próprio backend de agente quando quiser que o app faça mais.",
|
||||
suggestionCapabilities: "O que você pode fazer?",
|
||||
suggestionCustomize: "Ajude-me a personalizar este app de chat",
|
||||
suggestionActions: "Mostre as ações e páginas que posso adicionar",
|
||||
inspectEmptyState: "Peça ao agente para inspecionar ou alterar este app.",
|
||||
inspectSuggestionCapabilities: "O que você pode fazer aqui?",
|
||||
inspectSuggestionHello: "Chame hello para Builder",
|
||||
inspectSuggestionAction: "Adicione uma nova ação e mostre na UI",
|
||||
chats: "Chats",
|
||||
newChat: "Novo chat",
|
||||
renameChat: "Renomear chat",
|
||||
pinChat: "Fixar chat",
|
||||
unpinChat: "Desafixar chat",
|
||||
archiveChat: "Arquivar chat",
|
||||
archiveFailed: "Não foi possível arquivar o chat.",
|
||||
renameFailed: "Não foi possível renomear o chat.",
|
||||
renameThread: "Renomear {{title}}",
|
||||
optionsFor: "Opções de chat para {{title}}",
|
||||
},
|
||||
}),
|
||||
"hi-IN": mergeMessages({
|
||||
root: {
|
||||
commandActions: "क्रियाएं",
|
||||
commandSearch: "खोजें",
|
||||
commandAppearance: "रूप",
|
||||
toggleTheme: "थीम बदलें",
|
||||
},
|
||||
navigation: {
|
||||
chat: "चैट",
|
||||
observability: "ऑब्ज़र्वेबिलिटी",
|
||||
database: "डेटाबेस",
|
||||
team: "टीम",
|
||||
settings: "सेटिंग्स",
|
||||
extensions: "एक्सटेंशन",
|
||||
navigation: "नेविगेशन",
|
||||
navigationDescription: "ऐप नेविगेशन लिंक",
|
||||
openNavigation: "नेविगेशन खोलें",
|
||||
expandSidebar: "साइडबार फैलाएं",
|
||||
collapseSidebar: "साइडबार समेटें",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "एजेंट ऑब्जर्वेबिलिटी",
|
||||
observabilityPageTitle: "ऑब्जर्वेबिलिटी",
|
||||
databaseTitle: "डेटाबेस",
|
||||
teamTitle: "टीम",
|
||||
teamCreateOrgDescription:
|
||||
"इस ऐप को अपने सहयोगियों के साथ साझा करने के लिए टीम सेट करें।",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "कुछ भी पूछें, फिर जरूरत होने पर ऐप कस्टमाइज़ करें।",
|
||||
composerPlaceholder: "एजेंट को संदेश...",
|
||||
heroTitle: "मैं कैसे मदद कर सकता हूं?",
|
||||
heroDescription:
|
||||
"किसी भी चीज़ पर चैट करें। जब ऐप से और काम कराना हो तो actions, components, pages, jobs या अपना agent backend जोड़ें।",
|
||||
suggestionCapabilities: "आप क्या कर सकते हैं?",
|
||||
suggestionCustomize: "इस chat app को कस्टमाइज़ करने में मदद करें",
|
||||
suggestionActions: "जो actions और pages जोड़ सकता हूं वे दिखाएं",
|
||||
inspectEmptyState: "एजेंट से इस ऐप को inspect या change करने को कहें।",
|
||||
inspectSuggestionCapabilities: "आप यहां क्या कर सकते हैं?",
|
||||
inspectSuggestionHello: "Builder के लिए hello call करें",
|
||||
inspectSuggestionAction: "नई action जोड़ें और UI में दिखाएं",
|
||||
chats: "चैट",
|
||||
newChat: "नई चैट",
|
||||
renameChat: "चैट का नाम बदलें",
|
||||
pinChat: "चैट पिन करें",
|
||||
unpinChat: "चैट अनपिन करें",
|
||||
archiveChat: "चैट आर्काइव करें",
|
||||
archiveFailed: "चैट आर्काइव नहीं कर सके।",
|
||||
renameFailed: "चैट का नाम नहीं बदल सके।",
|
||||
renameThread: "{{title}} का नाम बदलें",
|
||||
optionsFor: "{{title}} के लिए चैट विकल्प",
|
||||
},
|
||||
}),
|
||||
"ar-SA": mergeMessages({
|
||||
root: {
|
||||
commandActions: "الإجراءات",
|
||||
commandSearch: "بحث",
|
||||
commandAppearance: "المظهر",
|
||||
toggleTheme: "تبديل السمة",
|
||||
},
|
||||
navigation: {
|
||||
chat: "المحادثة",
|
||||
observability: "المراقبة",
|
||||
database: "قاعدة البيانات",
|
||||
team: "الفريق",
|
||||
settings: "الإعدادات",
|
||||
extensions: "الإضافات",
|
||||
navigation: "التنقل",
|
||||
navigationDescription: "روابط تنقل التطبيق",
|
||||
openNavigation: "فتح التنقل",
|
||||
expandSidebar: "توسيع الشريط الجانبي",
|
||||
collapseSidebar: "طي الشريط الجانبي",
|
||||
},
|
||||
pages: {
|
||||
observabilityTitle: "قابلية ملاحظة الوكيل",
|
||||
observabilityPageTitle: "قابلية الملاحظة",
|
||||
databaseTitle: "قاعدة البيانات",
|
||||
teamTitle: "الفريق",
|
||||
teamCreateOrgDescription: "أعد فريقا لمشاركة هذا التطبيق مع زملائك.",
|
||||
},
|
||||
chat: {
|
||||
emptyState: "اسأل أي شيء ثم خصص التطبيق عندما تحتاج المزيد.",
|
||||
composerPlaceholder: "راسل الوكيل...",
|
||||
heroTitle: "كيف يمكنني المساعدة؟",
|
||||
heroDescription:
|
||||
"تحدث عن أي شيء. أضف إجراءات أو مكونات أو صفحات أو مهام أو خلفية وكيل خاصة عندما تريد أن يفعل التطبيق المزيد.",
|
||||
suggestionCapabilities: "ماذا يمكنك أن تفعل؟",
|
||||
suggestionCustomize: "ساعدني في تخصيص تطبيق الدردشة هذا",
|
||||
suggestionActions: "أرني الإجراءات والصفحات التي يمكنني إضافتها",
|
||||
inspectEmptyState: "اطلب من الوكيل فحص هذا التطبيق أو تغييره.",
|
||||
inspectSuggestionCapabilities: "ماذا يمكنك أن تفعل هنا؟",
|
||||
inspectSuggestionHello: "استدع hello لـ Builder",
|
||||
inspectSuggestionAction: "أضف إجراءً جديدًا واعرضه في الواجهة",
|
||||
chats: "المحادثات",
|
||||
newChat: "محادثة جديدة",
|
||||
renameChat: "إعادة تسمية المحادثة",
|
||||
pinChat: "تثبيت المحادثة",
|
||||
unpinChat: "إلغاء تثبيت المحادثة",
|
||||
archiveChat: "أرشفة المحادثة",
|
||||
archiveFailed: "تعذرت أرشفة المحادثة.",
|
||||
renameFailed: "تعذرت إعادة تسمية المحادثة.",
|
||||
renameThread: "إعادة تسمية {{title}}",
|
||||
optionsFor: "خيارات المحادثة لـ {{title}}",
|
||||
},
|
||||
}),
|
||||
} satisfies Record<LocaleCode, Messages>;
|
||||
@@ -0,0 +1,68 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "الإعدادات",
|
||||
description: "تفضيلات اللغة ومساحة العمل لهذا التطبيق.",
|
||||
languageTitle: "اللغة",
|
||||
languageDescription: "اختر لغة الواجهة. يتم حفظ هذا التفضيل في حسابك.",
|
||||
languageLabel: "لغة الواجهة",
|
||||
workspaceTitle: "مساحة العمل",
|
||||
workspaceDescription:
|
||||
"إدارة أعضاء الفريق ووصول المؤسسة وتفضيلات مساحة العمل المشتركة.",
|
||||
openTeamSettings: "فتح إعدادات الفريق",
|
||||
openResourceSettings: "فتح إعدادات الموارد",
|
||||
agentTitle: "إدارة الوكيل",
|
||||
agentDescription:
|
||||
"أدر نموذج الوكيل ومفاتيح API والأتمتة والصوت وعناصر التحكم الأخرى.",
|
||||
openAgentSettings: "إدارة الوكيل",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "أرشفة المحادثة",
|
||||
archiveFailed: "فشلت الأرشفة",
|
||||
chats: "المحادثات",
|
||||
composerPlaceholder: "اسأل الوكيل...",
|
||||
emptyState: "اسألني أي شيء",
|
||||
heroDescription: "اطلب من الوكيل فحص هذا التطبيق أو شرحه أو تغييره.",
|
||||
heroTitle: "كيف يمكنني المساعدة؟",
|
||||
inspectEmptyState: "اسألني أي شيء عن هذا التطبيق",
|
||||
inspectSuggestionAction: "إظهار الإجراءات المتاحة",
|
||||
inspectSuggestionCapabilities: "ما الذي يمكن لهذا التطبيق فعله؟",
|
||||
inspectSuggestionHello: "ساعدني على البدء",
|
||||
newChat: "محادثة جديدة",
|
||||
optionsFor: "خيارات لـ",
|
||||
pinChat: "تثبيت المحادثة",
|
||||
renameChat: "إعادة تسمية المحادثة",
|
||||
renameFailed: "فشلت إعادة التسمية",
|
||||
renameThread: "إعادة تسمية السلسلة",
|
||||
suggestionActions: "أرني الإجراءات المتاحة",
|
||||
suggestionCapabilities: "ما الذي يمكن لهذا التطبيق فعله؟",
|
||||
suggestionCustomize: "ساعدني في تخصيص هذا التطبيق",
|
||||
unpinChat: "إلغاء تثبيت المحادثة",
|
||||
},
|
||||
navigation: {
|
||||
chat: "المحادثة",
|
||||
collapseSidebar: "طي الشريط الجانبي",
|
||||
database: "قاعدة البيانات",
|
||||
expandSidebar: "توسيع الشريط الجانبي",
|
||||
extensions: "الإضافات",
|
||||
navigation: "التنقل",
|
||||
navigationDescription: "التنقل الرئيسي",
|
||||
observability: "قابلية المراقبة",
|
||||
openNavigation: "فتح التنقل",
|
||||
settings: "الإعدادات",
|
||||
team: "الفريق",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "قاعدة البيانات",
|
||||
observabilityPageTitle: "قابلية ملاحظة الوكيل",
|
||||
teamTitle: "الفريق",
|
||||
teamCreateOrgDescription: "أنشئ مؤسسة لدعوة الزملاء ومشاركة هذا التطبيق.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "الإجراءات",
|
||||
commandAppearance: "المظهر",
|
||||
commandSearch: "بحث",
|
||||
toggleTheme: "تبديل السمة",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,71 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "Einstellungen",
|
||||
description: "Sprach- und Arbeitsbereichseinstellungen für diese App.",
|
||||
languageTitle: "Sprache",
|
||||
languageDescription:
|
||||
"Wähle die Sprache der Oberfläche. Diese Einstellung wird in deinem Konto gespeichert.",
|
||||
languageLabel: "Oberflächensprache",
|
||||
workspaceTitle: "Arbeitsbereich",
|
||||
workspaceDescription:
|
||||
"Verwalte Teammitglieder, Organisationszugriff und gemeinsame Arbeitsbereichseinstellungen.",
|
||||
openTeamSettings: "Teameinstellungen öffnen",
|
||||
openResourceSettings: "Ressourceneinstellungen öffnen",
|
||||
agentTitle: "Agent verwalten",
|
||||
agentDescription:
|
||||
"Verwalte das Modell, die API-Schlüssel, Automatisierungen, Sprache und weitere Steuerungen des Agents.",
|
||||
openAgentSettings: "Agent verwalten",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "Chat archivieren",
|
||||
archiveFailed: "Archivieren fehlgeschlagen",
|
||||
chats: "Chats",
|
||||
composerPlaceholder: "Frag den Agenten...",
|
||||
emptyState: "Frag mich alles",
|
||||
heroDescription:
|
||||
"Bitte den Agenten, diese App zu prüfen, zu erklären oder zu ändern.",
|
||||
heroTitle: "Wie kann ich helfen?",
|
||||
inspectEmptyState: "Frag mich alles über diese App",
|
||||
inspectSuggestionAction: "Verfügbare Aktionen anzeigen",
|
||||
inspectSuggestionCapabilities: "Was kann diese App?",
|
||||
inspectSuggestionHello: "Hilf mir beim Einstieg",
|
||||
newChat: "Neuer Chat",
|
||||
optionsFor: "Optionen für",
|
||||
pinChat: "Chat anheften",
|
||||
renameChat: "Chat umbenennen",
|
||||
renameFailed: "Umbenennen fehlgeschlagen",
|
||||
renameThread: "Thread umbenennen",
|
||||
suggestionActions: "Zeig mir die verfügbaren Aktionen",
|
||||
suggestionCapabilities: "Was kann diese App?",
|
||||
suggestionCustomize: "Hilf mir, diese App anzupassen",
|
||||
unpinChat: "Chat lösen",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
collapseSidebar: "Seitenleiste einklappen",
|
||||
database: "Datenbank",
|
||||
expandSidebar: "Seitenleiste ausklappen",
|
||||
extensions: "Erweiterungen",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "Hauptnavigation",
|
||||
observability: "Beobachtbarkeit",
|
||||
openNavigation: "Navigation öffnen",
|
||||
settings: "Einstellungen",
|
||||
team: "Team",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "Datenbank",
|
||||
observabilityPageTitle: "Agent-Beobachtbarkeit",
|
||||
teamTitle: "Team",
|
||||
teamCreateOrgDescription:
|
||||
"Erstelle eine Organisation, um Teammitglieder einzuladen und diese App zu teilen.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "Aktionen",
|
||||
commandAppearance: "Darstellung",
|
||||
commandSearch: "Suchen",
|
||||
toggleTheme: "Design wechseln",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,70 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "Settings",
|
||||
description: "Language and workspace preferences for this app.",
|
||||
languageTitle: "Language",
|
||||
languageDescription:
|
||||
"Choose the interface language. This preference is saved for your account.",
|
||||
languageLabel: "Interface language",
|
||||
workspaceTitle: "Workspace",
|
||||
workspaceDescription:
|
||||
"Manage team members, organization access, and shared workspace preferences.",
|
||||
openTeamSettings: "Open team settings",
|
||||
openResourceSettings: "Open resource settings",
|
||||
agentTitle: "Manage agent",
|
||||
agentDescription:
|
||||
"Manage the agent's model, API keys, automations, voice, and other controls.",
|
||||
openAgentSettings: "Manage agent",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "Archive Chat",
|
||||
archiveFailed: "Archive Failed",
|
||||
chats: "Chats",
|
||||
composerPlaceholder: "Ask the agent...",
|
||||
emptyState: "Ask me anything",
|
||||
heroDescription: "Ask the agent to inspect, explain, or change this app.",
|
||||
heroTitle: "How can I help?",
|
||||
inspectEmptyState: "Ask me anything about this app",
|
||||
inspectSuggestionAction: "Show available actions",
|
||||
inspectSuggestionCapabilities: "What can this app do?",
|
||||
inspectSuggestionHello: "Help me get started",
|
||||
newChat: "New Chat",
|
||||
optionsFor: "Options For",
|
||||
pinChat: "Pin Chat",
|
||||
renameChat: "Rename Chat",
|
||||
renameFailed: "Rename Failed",
|
||||
renameThread: "Rename Thread",
|
||||
suggestionActions: "Show me the available actions",
|
||||
suggestionCapabilities: "What can this app do?",
|
||||
suggestionCustomize: "Help me customize this app",
|
||||
unpinChat: "Unpin Chat",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
collapseSidebar: "Collapse Sidebar",
|
||||
database: "Database",
|
||||
expandSidebar: "Expand Sidebar",
|
||||
extensions: "Extensions",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "Main navigation",
|
||||
observability: "Observability",
|
||||
openNavigation: "Open navigation",
|
||||
settings: "Settings",
|
||||
team: "Team",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "Database",
|
||||
observabilityPageTitle: "Agent Observability",
|
||||
teamTitle: "Team",
|
||||
teamCreateOrgDescription:
|
||||
"Create an organization to invite teammates and share this app.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "Actions",
|
||||
commandAppearance: "Appearance",
|
||||
commandSearch: "Search",
|
||||
toggleTheme: "Toggle theme",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,71 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "Ajustes",
|
||||
description: "Preferencias de idioma y espacio de trabajo para esta app.",
|
||||
languageTitle: "Idioma",
|
||||
languageDescription:
|
||||
"Elige el idioma de la interfaz. Esta preferencia se guarda en tu cuenta.",
|
||||
languageLabel: "Idioma de la interfaz",
|
||||
workspaceTitle: "Espacio de trabajo",
|
||||
workspaceDescription:
|
||||
"Gestiona miembros del equipo, acceso de la organización y preferencias compartidas.",
|
||||
openTeamSettings: "Abrir ajustes del equipo",
|
||||
openResourceSettings: "Abrir ajustes de recursos",
|
||||
agentTitle: "Gestionar agente",
|
||||
agentDescription:
|
||||
"Gestiona el modelo del agente, claves API, automatizaciones, voz y otros controles.",
|
||||
openAgentSettings: "Gestionar agente",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "Archivar chat",
|
||||
archiveFailed: "No se pudo archivar",
|
||||
chats: "Chats",
|
||||
composerPlaceholder: "Pregunta al agente...",
|
||||
emptyState: "Pregúntame lo que quieras",
|
||||
heroDescription:
|
||||
"Pide al agente que inspeccione, explique o cambie esta app.",
|
||||
heroTitle: "¿Cómo puedo ayudarte?",
|
||||
inspectEmptyState: "Pregúntame cualquier cosa sobre esta app",
|
||||
inspectSuggestionAction: "Mostrar acciones disponibles",
|
||||
inspectSuggestionCapabilities: "¿Qué puede hacer esta app?",
|
||||
inspectSuggestionHello: "Ayúdame a empezar",
|
||||
newChat: "Nuevo chat",
|
||||
optionsFor: "Opciones para",
|
||||
pinChat: "Fijar chat",
|
||||
renameChat: "Renombrar chat",
|
||||
renameFailed: "No se pudo renombrar",
|
||||
renameThread: "Renombrar hilo",
|
||||
suggestionActions: "Muéstrame las acciones disponibles",
|
||||
suggestionCapabilities: "¿Qué puede hacer esta app?",
|
||||
suggestionCustomize: "Ayúdame a personalizar esta app",
|
||||
unpinChat: "Desfijar chat",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
collapseSidebar: "Contraer barra lateral",
|
||||
database: "Base de datos",
|
||||
expandSidebar: "Expandir barra lateral",
|
||||
extensions: "Extensiones",
|
||||
navigation: "Navegación",
|
||||
navigationDescription: "Navegación principal",
|
||||
observability: "Observabilidad",
|
||||
openNavigation: "Abrir navegación",
|
||||
settings: "Ajustes",
|
||||
team: "Equipo",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "Base de datos",
|
||||
observabilityPageTitle: "Observabilidad del agente",
|
||||
teamTitle: "Equipo",
|
||||
teamCreateOrgDescription:
|
||||
"Crea una organización para invitar compañeros y compartir esta app.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "Acciones",
|
||||
commandAppearance: "Apariencia",
|
||||
commandSearch: "Buscar",
|
||||
toggleTheme: "Cambiar tema",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,71 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "Paramètres",
|
||||
description: "Préférences de langue et d’espace de travail pour cette app.",
|
||||
languageTitle: "Langue",
|
||||
languageDescription:
|
||||
"Choisissez la langue de l’interface. Cette préférence est enregistrée dans votre compte.",
|
||||
languageLabel: "Langue de l’interface",
|
||||
workspaceTitle: "Espace de travail",
|
||||
workspaceDescription:
|
||||
"Gérez les membres, l’accès de l’organisation et les préférences partagées.",
|
||||
openTeamSettings: "Ouvrir les paramètres d’équipe",
|
||||
openResourceSettings: "Ouvrir les paramètres des ressources",
|
||||
agentTitle: "Gérer l’agent",
|
||||
agentDescription:
|
||||
"Gérez le modèle de l’agent, les clés API, les automatisations, la voix et les autres contrôles.",
|
||||
openAgentSettings: "Gérer l’agent",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "Archiver le chat",
|
||||
archiveFailed: "Échec de l’archivage",
|
||||
chats: "Discussions",
|
||||
composerPlaceholder: "Demandez à l’agent...",
|
||||
emptyState: "Posez-moi une question",
|
||||
heroDescription:
|
||||
"Demandez à l’agent d’inspecter, d’expliquer ou de modifier cette app.",
|
||||
heroTitle: "Comment puis-je aider ?",
|
||||
inspectEmptyState: "Posez-moi une question sur cette app",
|
||||
inspectSuggestionAction: "Afficher les actions disponibles",
|
||||
inspectSuggestionCapabilities: "Que peut faire cette app ?",
|
||||
inspectSuggestionHello: "Aidez-moi à démarrer",
|
||||
newChat: "Nouveau chat",
|
||||
optionsFor: "Options pour",
|
||||
pinChat: "Épingler le chat",
|
||||
renameChat: "Renommer le chat",
|
||||
renameFailed: "Échec du renommage",
|
||||
renameThread: "Renommer le fil",
|
||||
suggestionActions: "Montrez-moi les actions disponibles",
|
||||
suggestionCapabilities: "Que peut faire cette app ?",
|
||||
suggestionCustomize: "Aidez-moi à personnaliser cette app",
|
||||
unpinChat: "Désépingler le chat",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
collapseSidebar: "Réduire la barre latérale",
|
||||
database: "Base de données",
|
||||
expandSidebar: "Développer la barre latérale",
|
||||
extensions: "Rallonges",
|
||||
navigation: "Navigation",
|
||||
navigationDescription: "Navigation principale",
|
||||
observability: "Observabilité",
|
||||
openNavigation: "Ouvrir la navigation",
|
||||
settings: "Paramètres",
|
||||
team: "Équipe",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "Base de données",
|
||||
observabilityPageTitle: "Observabilité de l'agent",
|
||||
teamTitle: "Équipe",
|
||||
teamCreateOrgDescription:
|
||||
"Créez une organisation pour inviter des coéquipiers et partager cette app.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "Opérations",
|
||||
commandAppearance: "Apparence",
|
||||
commandSearch: "Rechercher",
|
||||
toggleTheme: "Changer de thème",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,69 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "सेटिंग्स",
|
||||
description: "इस ऐप के लिए भाषा और कार्यस्थान प्राथमिकताएं।",
|
||||
languageTitle: "भाषा",
|
||||
languageDescription: "इंटरफ़ेस भाषा चुनें। यह पसंद आपके खाते में सहेजी जाती है।",
|
||||
languageLabel: "इंटरफ़ेस भाषा",
|
||||
workspaceTitle: "कार्यस्थान",
|
||||
workspaceDescription:
|
||||
"टीम सदस्यों, संगठन पहुंच और साझा कार्यस्थान प्राथमिकताओं को प्रबंधित करें।",
|
||||
openTeamSettings: "टीम सेटिंग्स खोलें",
|
||||
openResourceSettings: "संसाधन सेटिंग्स खोलें",
|
||||
agentTitle: "एजेंट प्रबंधित करें",
|
||||
agentDescription:
|
||||
"एजेंट के मॉडल, API कुंजियों, ऑटोमेशन, आवाज़ और अन्य नियंत्रणों को प्रबंधित करें।",
|
||||
openAgentSettings: "एजेंट प्रबंधित करें",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "चैट आर्काइव करें",
|
||||
archiveFailed: "आर्काइव विफल",
|
||||
chats: "चैट",
|
||||
composerPlaceholder: "एजेंट से पूछें...",
|
||||
emptyState: "मुझसे कुछ भी पूछें",
|
||||
heroDescription: "एजेंट से इस ऐप की जांच, व्याख्या या बदलाव करने को कहें।",
|
||||
heroTitle: "मैं कैसे मदद कर सकता हूं?",
|
||||
inspectEmptyState: "इस ऐप के बारे में मुझसे कुछ भी पूछें",
|
||||
inspectSuggestionAction: "उपलब्ध क्रियाएं दिखाएं",
|
||||
inspectSuggestionCapabilities: "यह ऐप क्या कर सकता है?",
|
||||
inspectSuggestionHello: "शुरू करने में मेरी मदद करें",
|
||||
newChat: "नई चैट",
|
||||
optionsFor: "इसके लिए विकल्प",
|
||||
pinChat: "चैट पिन करें",
|
||||
renameChat: "चैट का नाम बदलें",
|
||||
renameFailed: "नाम बदलना विफल",
|
||||
renameThread: "थ्रेड का नाम बदलें",
|
||||
suggestionActions: "मुझे उपलब्ध क्रियाएं दिखाएं",
|
||||
suggestionCapabilities: "यह ऐप क्या कर सकता है?",
|
||||
suggestionCustomize: "इस ऐप को कस्टमाइज करने में मेरी मदद करें",
|
||||
unpinChat: "चैट अनपिन करें",
|
||||
},
|
||||
navigation: {
|
||||
chat: "चैट",
|
||||
collapseSidebar: "साइडबार संक्षिप्त करें",
|
||||
database: "डेटाबेस",
|
||||
expandSidebar: "साइडबार विस्तृत करें",
|
||||
extensions: "एक्सटेंशन",
|
||||
navigation: "नेविगेशन",
|
||||
navigationDescription: "मुख्य नेविगेशन",
|
||||
observability: "अवलोकनक्षमता",
|
||||
openNavigation: "नेविगेशन खोलें",
|
||||
settings: "सेटिंग्स",
|
||||
team: "टीम",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "डेटाबेस",
|
||||
observabilityPageTitle: "एजेंट ऑब्ज़र्वेबिलिटी",
|
||||
teamTitle: "टीम",
|
||||
teamCreateOrgDescription:
|
||||
"टीम के सदस्यों को आमंत्रित करने और यह ऐप साझा करने के लिए संगठन बनाएं।",
|
||||
},
|
||||
root: {
|
||||
commandActions: "कार्रवाइयाँ",
|
||||
commandAppearance: "दिखावट",
|
||||
commandSearch: "खोजें",
|
||||
toggleTheme: "थीम बदलें",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { type AgentNativeI18nCatalog } from "@agent-native/core/client/i18n";
|
||||
|
||||
import enUS from "./en-US";
|
||||
|
||||
export const i18nCatalog = {
|
||||
sourceLocale: "en-US",
|
||||
messages: enUS,
|
||||
loadMessages: async (locale) => {
|
||||
switch (locale) {
|
||||
case "zh-CN":
|
||||
return (await import("./zh-CN")).default;
|
||||
case "zh-TW":
|
||||
return (await import("./zh-TW")).default;
|
||||
case "es-ES":
|
||||
return (await import("./es-ES")).default;
|
||||
case "fr-FR":
|
||||
return (await import("./fr-FR")).default;
|
||||
case "de-DE":
|
||||
return (await import("./de-DE")).default;
|
||||
case "ja-JP":
|
||||
return (await import("./ja-JP")).default;
|
||||
case "ko-KR":
|
||||
return (await import("./ko-KR")).default;
|
||||
case "pt-BR":
|
||||
return (await import("./pt-BR")).default;
|
||||
case "hi-IN":
|
||||
return (await import("./hi-IN")).default;
|
||||
case "ar-SA":
|
||||
return (await import("./ar-SA")).default;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
} satisfies AgentNativeI18nCatalog;
|
||||
@@ -0,0 +1,70 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "設定",
|
||||
description: "このアプリの言語とワークスペース設定。",
|
||||
languageTitle: "言語",
|
||||
languageDescription:
|
||||
"インターフェース言語を選択します。この設定はアカウントに保存されます。",
|
||||
languageLabel: "インターフェース言語",
|
||||
workspaceTitle: "ワークスペース",
|
||||
workspaceDescription:
|
||||
"チームメンバー、組織アクセス、共有ワークスペース設定を管理します。",
|
||||
openTeamSettings: "チーム設定を開く",
|
||||
openResourceSettings: "リソース設定を開く",
|
||||
agentTitle: "エージェントを管理",
|
||||
agentDescription:
|
||||
"エージェントのモデル、API キー、自動化、音声などを管理します。",
|
||||
openAgentSettings: "エージェントを管理",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "チャットをアーカイブ",
|
||||
archiveFailed: "アーカイブに失敗しました",
|
||||
chats: "チャット",
|
||||
composerPlaceholder: "エージェントに質問...",
|
||||
emptyState: "何でも聞いてください",
|
||||
heroDescription: "このアプリの確認、説明、変更をエージェントに依頼します。",
|
||||
heroTitle: "どのようにお手伝いできますか?",
|
||||
inspectEmptyState: "このアプリについて何でも聞いてください",
|
||||
inspectSuggestionAction: "利用可能なアクションを表示",
|
||||
inspectSuggestionCapabilities: "このアプリで何ができますか?",
|
||||
inspectSuggestionHello: "使い始めを手伝って",
|
||||
newChat: "新しいチャット",
|
||||
optionsFor: "オプション:",
|
||||
pinChat: "チャットをピン留め",
|
||||
renameChat: "チャット名を変更",
|
||||
renameFailed: "名前の変更に失敗しました",
|
||||
renameThread: "スレッド名を変更",
|
||||
suggestionActions: "利用可能なアクションを表示して",
|
||||
suggestionCapabilities: "このアプリで何ができますか?",
|
||||
suggestionCustomize: "このアプリのカスタマイズを手伝って",
|
||||
unpinChat: "チャットのピン留めを解除",
|
||||
},
|
||||
navigation: {
|
||||
chat: "チャット",
|
||||
collapseSidebar: "サイドバーを折りたたむ",
|
||||
database: "データベース",
|
||||
expandSidebar: "サイドバーを展開",
|
||||
extensions: "拡張機能",
|
||||
navigation: "ナビゲーション",
|
||||
navigationDescription: "メインナビゲーション",
|
||||
observability: "可観測性",
|
||||
openNavigation: "ナビゲーションを開く",
|
||||
settings: "設定",
|
||||
team: "チーム",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "データベース",
|
||||
observabilityPageTitle: "エージェント可観測性",
|
||||
teamTitle: "チーム",
|
||||
teamCreateOrgDescription:
|
||||
"チームメイトを招待してこのアプリを共有するための組織を作成します。",
|
||||
},
|
||||
root: {
|
||||
commandActions: "操作",
|
||||
commandAppearance: "外観",
|
||||
commandSearch: "検索",
|
||||
toggleTheme: "テーマを切り替え",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,70 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "설정",
|
||||
description: "이 앱의 언어 및 워크스페이스 환경설정입니다.",
|
||||
languageTitle: "언어",
|
||||
languageDescription:
|
||||
"인터페이스 언어를 선택하세요. 이 기본 설정은 계정에 저장됩니다.",
|
||||
languageLabel: "인터페이스 언어",
|
||||
workspaceTitle: "워크스페이스",
|
||||
workspaceDescription:
|
||||
"팀원, 조직 접근 권한, 공유 워크스페이스 환경설정을 관리합니다.",
|
||||
openTeamSettings: "팀 설정 열기",
|
||||
openResourceSettings: "리소스 설정 열기",
|
||||
agentTitle: "에이전트 관리",
|
||||
agentDescription:
|
||||
"에이전트의 모델, API 키, 자동화, 음성 및 기타 제어를 관리합니다.",
|
||||
openAgentSettings: "에이전트 관리",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "채팅 보관",
|
||||
archiveFailed: "보관 실패",
|
||||
chats: "채팅",
|
||||
composerPlaceholder: "에이전트에게 물어보세요...",
|
||||
emptyState: "무엇이든 물어보세요",
|
||||
heroDescription:
|
||||
"에이전트에게 이 앱을 살펴보고 설명하거나 변경하도록 요청하세요.",
|
||||
heroTitle: "무엇을 도와드릴까요?",
|
||||
inspectEmptyState: "이 앱에 대해 무엇이든 물어보세요",
|
||||
inspectSuggestionAction: "사용 가능한 작업 보기",
|
||||
inspectSuggestionCapabilities: "이 앱은 무엇을 할 수 있나요?",
|
||||
inspectSuggestionHello: "시작할 수 있게 도와주세요",
|
||||
newChat: "새 채팅",
|
||||
optionsFor: "옵션 대상",
|
||||
pinChat: "채팅 고정",
|
||||
renameChat: "채팅 이름 바꾸기",
|
||||
renameFailed: "이름 변경 실패",
|
||||
renameThread: "스레드 이름 바꾸기",
|
||||
suggestionActions: "사용 가능한 작업을 보여줘",
|
||||
suggestionCapabilities: "이 앱은 무엇을 할 수 있나요?",
|
||||
suggestionCustomize: "이 앱을 맞춤 설정하도록 도와줘",
|
||||
unpinChat: "채팅 고정 해제",
|
||||
},
|
||||
navigation: {
|
||||
chat: "채팅",
|
||||
collapseSidebar: "사이드바 접기",
|
||||
database: "데이터베이스",
|
||||
expandSidebar: "사이드바 펼치기",
|
||||
extensions: "확장 프로그램",
|
||||
navigation: "탐색",
|
||||
navigationDescription: "기본 탐색",
|
||||
observability: "관찰성",
|
||||
openNavigation: "탐색 열기",
|
||||
settings: "설정",
|
||||
team: "팀",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "데이터베이스",
|
||||
observabilityPageTitle: "에이전트 관찰성",
|
||||
teamTitle: "팀",
|
||||
teamCreateOrgDescription: "팀원을 초대하고 이 앱을 공유할 조직을 만드세요.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "작업",
|
||||
commandAppearance: "화면 표시",
|
||||
commandSearch: "검색",
|
||||
toggleTheme: "테마 전환",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,71 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "Configurações",
|
||||
description: "Preferências de idioma e espaço de trabalho deste app.",
|
||||
languageTitle: "Idioma",
|
||||
languageDescription:
|
||||
"Escolha o idioma da interface. Essa preferência é salva na sua conta.",
|
||||
languageLabel: "Idioma da interface",
|
||||
workspaceTitle: "Espaço de trabalho",
|
||||
workspaceDescription:
|
||||
"Gerencie membros da equipe, acesso da organização e preferências compartilhadas.",
|
||||
openTeamSettings: "Abrir configurações da equipe",
|
||||
openResourceSettings: "Abrir configurações de recursos",
|
||||
agentTitle: "Gerenciar agente",
|
||||
agentDescription:
|
||||
"Gerencie o modelo do agente, chaves de API, automações, voz e outros controles.",
|
||||
openAgentSettings: "Gerenciar agente",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "Arquivar chat",
|
||||
archiveFailed: "Falha ao arquivar",
|
||||
chats: "Chats",
|
||||
composerPlaceholder: "Pergunte ao agente...",
|
||||
emptyState: "Pergunte-me qualquer coisa",
|
||||
heroDescription:
|
||||
"Peça ao agente para inspecionar, explicar ou alterar este app.",
|
||||
heroTitle: "Como posso ajudar?",
|
||||
inspectEmptyState: "Pergunte-me qualquer coisa sobre este app",
|
||||
inspectSuggestionAction: "Mostrar ações disponíveis",
|
||||
inspectSuggestionCapabilities: "O que este app pode fazer?",
|
||||
inspectSuggestionHello: "Ajude-me a começar",
|
||||
newChat: "Novo chat",
|
||||
optionsFor: "Opções para",
|
||||
pinChat: "Fixar chat",
|
||||
renameChat: "Renomear chat",
|
||||
renameFailed: "Falha ao renomear",
|
||||
renameThread: "Renomear conversa",
|
||||
suggestionActions: "Mostre-me as ações disponíveis",
|
||||
suggestionCapabilities: "O que este app pode fazer?",
|
||||
suggestionCustomize: "Ajude-me a personalizar este app",
|
||||
unpinChat: "Desafixar chat",
|
||||
},
|
||||
navigation: {
|
||||
chat: "Chat",
|
||||
collapseSidebar: "Recolher barra lateral",
|
||||
database: "Banco de dados",
|
||||
expandSidebar: "Expandir barra lateral",
|
||||
extensions: "Extensões",
|
||||
navigation: "Navegação",
|
||||
navigationDescription: "Navegação principal",
|
||||
observability: "Observabilidade",
|
||||
openNavigation: "Abrir navegação",
|
||||
settings: "Configurações",
|
||||
team: "Equipe",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "Banco de dados",
|
||||
observabilityPageTitle: "Observabilidade do agente",
|
||||
teamTitle: "Equipe",
|
||||
teamCreateOrgDescription:
|
||||
"Crie uma organização para convidar colegas e compartilhar este app.",
|
||||
},
|
||||
root: {
|
||||
commandActions: "Ações",
|
||||
commandAppearance: "Aparência",
|
||||
commandSearch: "Pesquisar",
|
||||
toggleTheme: "Alternar tema",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,66 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "设置",
|
||||
description: "此应用的语言和工作区偏好设置。",
|
||||
languageTitle: "语言",
|
||||
languageDescription: "选择界面语言。此偏好会保存到你的账户。",
|
||||
languageLabel: "界面语言",
|
||||
workspaceTitle: "工作区",
|
||||
workspaceDescription: "管理团队成员、组织访问权限和共享工作区偏好。",
|
||||
openTeamSettings: "打开团队设置",
|
||||
openResourceSettings: "打开资源设置",
|
||||
agentTitle: "管理代理",
|
||||
agentDescription: "管理代理的模型、API 密钥、自动化、语音和其他控制项。",
|
||||
openAgentSettings: "管理代理",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "归档聊天",
|
||||
archiveFailed: "归档失败",
|
||||
chats: "聊天",
|
||||
composerPlaceholder: "询问代理...",
|
||||
emptyState: "问我任何问题",
|
||||
heroDescription: "让代理检查、解释或更改此应用。",
|
||||
heroTitle: "我能帮你什么?",
|
||||
inspectEmptyState: "询问我有关此应用的任何问题",
|
||||
inspectSuggestionAction: "显示可用操作",
|
||||
inspectSuggestionCapabilities: "这个应用能做什么?",
|
||||
inspectSuggestionHello: "帮我开始使用",
|
||||
newChat: "新建聊天",
|
||||
optionsFor: "选项:",
|
||||
pinChat: "置顶聊天",
|
||||
renameChat: "重命名聊天",
|
||||
renameFailed: "重命名失败",
|
||||
renameThread: "重命名对话",
|
||||
suggestionActions: "显示可用操作",
|
||||
suggestionCapabilities: "这个应用能做什么?",
|
||||
suggestionCustomize: "帮我自定义此应用",
|
||||
unpinChat: "取消置顶聊天",
|
||||
},
|
||||
navigation: {
|
||||
chat: "聊天",
|
||||
collapseSidebar: "收起侧边栏",
|
||||
database: "数据库",
|
||||
expandSidebar: "展开侧边栏",
|
||||
extensions: "扩展",
|
||||
navigation: "导航",
|
||||
navigationDescription: "主导航",
|
||||
observability: "可观测性",
|
||||
openNavigation: "打开导航",
|
||||
settings: "设置",
|
||||
team: "团队",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "数据库",
|
||||
observabilityPageTitle: "代理可观测性",
|
||||
teamTitle: "团队",
|
||||
teamCreateOrgDescription: "创建组织以邀请队友并共享此应用。",
|
||||
},
|
||||
root: {
|
||||
commandActions: "操作",
|
||||
commandAppearance: "外观",
|
||||
commandSearch: "搜索",
|
||||
toggleTheme: "切换主题",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,66 @@
|
||||
const messages = {
|
||||
settings: {
|
||||
title: "設定",
|
||||
description: "此應用的語言和工作區偏好設定。",
|
||||
languageTitle: "語言",
|
||||
languageDescription: "選取介面語言。此偏好會儲存到你的帳戶。",
|
||||
languageLabel: "介面語言",
|
||||
workspaceTitle: "工作區",
|
||||
workspaceDescription: "管理團隊成員、組織存取權限和共用工作區偏好。",
|
||||
openTeamSettings: "開啟團隊設定",
|
||||
openResourceSettings: "開啟資源設定",
|
||||
agentTitle: "管理代理",
|
||||
agentDescription: "管理代理的模型、API 金鑰、自動化、語音和其他控制項。",
|
||||
openAgentSettings: "管理代理",
|
||||
},
|
||||
chat: {
|
||||
archiveChat: "封存聊天",
|
||||
archiveFailed: "封存失敗",
|
||||
chats: "聊天",
|
||||
composerPlaceholder: "詢問代理...",
|
||||
emptyState: "問我任何問題",
|
||||
heroDescription: "讓代理檢查、解釋或更改此應用。",
|
||||
heroTitle: "我能幫你什麼?",
|
||||
inspectEmptyState: "詢問我有關此應用的任何問題",
|
||||
inspectSuggestionAction: "顯示可用操作",
|
||||
inspectSuggestionCapabilities: "這個應用能做什麼?",
|
||||
inspectSuggestionHello: "幫我開始使用",
|
||||
newChat: "新建聊天",
|
||||
optionsFor: "選項:",
|
||||
pinChat: "置頂聊天",
|
||||
renameChat: "重新命名聊天",
|
||||
renameFailed: "重新命名失敗",
|
||||
renameThread: "重新命名對話",
|
||||
suggestionActions: "顯示可用操作",
|
||||
suggestionCapabilities: "這個應用能做什麼?",
|
||||
suggestionCustomize: "幫我自訂此應用",
|
||||
unpinChat: "取消置頂聊天",
|
||||
},
|
||||
navigation: {
|
||||
chat: "聊天",
|
||||
collapseSidebar: "收起側邊欄",
|
||||
database: "資料庫",
|
||||
expandSidebar: "展開側邊欄",
|
||||
extensions: "擴充功能",
|
||||
navigation: "導覽",
|
||||
navigationDescription: "主導覽",
|
||||
observability: "可觀測性",
|
||||
openNavigation: "開啟導覽",
|
||||
settings: "設定",
|
||||
team: "團隊",
|
||||
},
|
||||
pages: {
|
||||
databaseTitle: "資料庫",
|
||||
observabilityPageTitle: "代理可觀測性",
|
||||
teamTitle: "團隊",
|
||||
teamCreateOrgDescription: "建立組織以邀請隊友並共用此應用。",
|
||||
},
|
||||
root: {
|
||||
commandActions: "操作",
|
||||
commandAppearance: "外觀",
|
||||
commandSearch: "搜尋",
|
||||
toggleTheme: "切換主題",
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export type AgentPageProps = {
|
||||
appName?: string;
|
||||
};
|
||||
|
||||
type AgentClientModule = {
|
||||
AgentChatSurface: ComponentType<{
|
||||
mode?: "panel" | "page";
|
||||
className?: string;
|
||||
showHeader?: boolean;
|
||||
showTabBar?: boolean;
|
||||
}>;
|
||||
AgentTabsPage?: ComponentType<AgentPageProps>;
|
||||
};
|
||||
|
||||
const legacyAgentPages = new WeakMap<
|
||||
AgentClientModule,
|
||||
ComponentType<AgentPageProps>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Keep the chat scaffold runnable when its template and core package are
|
||||
* briefly out of sync during a release. Older core versions do not export
|
||||
* AgentTabsPage, but they do expose the page-level chat surface.
|
||||
*/
|
||||
export function resolveAgentPageComponent(
|
||||
client: AgentClientModule,
|
||||
): ComponentType<AgentPageProps> {
|
||||
if (typeof client.AgentTabsPage === "function") {
|
||||
return client.AgentTabsPage;
|
||||
}
|
||||
|
||||
const existing = legacyAgentPages.get(client);
|
||||
if (existing) return existing;
|
||||
|
||||
const legacyAgentPage = function LegacyAgentPage() {
|
||||
return (
|
||||
<client.AgentChatSurface
|
||||
mode="page"
|
||||
className="h-full"
|
||||
showHeader={false}
|
||||
showTabBar={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
legacyAgentPages.set(client, legacyAgentPage);
|
||||
return legacyAgentPage;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const rawAppName = "app";
|
||||
const rawAppTitle = "App";
|
||||
|
||||
const APP_NAME_PLACEHOLDER = "{" + "{APP_NAME}}";
|
||||
const APP_TITLE_PLACEHOLDER = "{" + "{APP_TITLE}}";
|
||||
|
||||
export const APP_NAME =
|
||||
rawAppName === APP_NAME_PLACEHOLDER ? "chat" : rawAppName;
|
||||
|
||||
export const APP_TITLE =
|
||||
rawAppTitle === APP_TITLE_PLACEHOLDER ? "Chat" : rawAppTitle;
|
||||
@@ -0,0 +1 @@
|
||||
export const TAB_ID = Math.random().toString(36).slice(2, 10);
|
||||
@@ -0,0 +1 @@
|
||||
export { cn } from "@agent-native/toolkit/utils";
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import { configureTracking } from "@agent-native/core/client/analytics";
|
||||
import { appPath } from "@agent-native/core/client/api-path";
|
||||
import { useDbSync } from "@agent-native/core/client/hooks";
|
||||
import {
|
||||
AppProviders,
|
||||
createAgentNativeQueryClient,
|
||||
} from "@agent-native/core/client/hooks";
|
||||
import { getLocaleInitScript, useT } from "@agent-native/core/client/i18n";
|
||||
import {
|
||||
CommandMenu,
|
||||
useCommandMenuShortcut,
|
||||
} from "@agent-native/core/client/navigation";
|
||||
import { getThemeInitScript } from "@agent-native/core/client/ui";
|
||||
import { IconHierarchy2, IconSun, IconMoon } from "@tabler/icons-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useNavigate,
|
||||
} from "react-router";
|
||||
import type { LinksFunction } from "react-router";
|
||||
|
||||
import { Layout as AppLayout } from "@/components/layout/Layout";
|
||||
import { AppToolkitProvider } from "@/components/ui/toolkit-provider";
|
||||
import { useNavigationState } from "@/hooks/use-navigation-state";
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
import { TAB_ID } from "@/lib/tab-id";
|
||||
|
||||
import changelog from "../CHANGELOG.md?raw";
|
||||
import { i18nCatalog } from "./i18n";
|
||||
|
||||
import stylesheet from "./global.css?url";
|
||||
|
||||
configureTracking({
|
||||
getDefaultProps: (_name, properties) => ({
|
||||
...properties,
|
||||
app: "app",
|
||||
template: "chat",
|
||||
}),
|
||||
});
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: "stylesheet", href: stylesheet },
|
||||
];
|
||||
|
||||
const THEME_INIT_SCRIPT = getThemeInitScript();
|
||||
const LOCALE_INIT_SCRIPT = getLocaleInitScript();
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
/>
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }}
|
||||
/>
|
||||
<script
|
||||
data-agent-native-locale-init
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{ __html: LOCALE_INIT_SCRIPT }}
|
||||
/>
|
||||
<link rel="manifest" href={appPath("/manifest.json")} />
|
||||
<meta name="theme-color" content="#18181B" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content={APP_TITLE} />
|
||||
<link rel="icon" type="image/svg+xml" href={appPath("/favicon.svg")} />
|
||||
<link rel="apple-touch-icon" href={appPath("/icon-180.svg")} />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function DbSyncSetup() {
|
||||
const qc = useQueryClient();
|
||||
useNavigationState();
|
||||
useDbSync({
|
||||
queryClient: qc,
|
||||
ignoreSource: TAB_ID,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
function ThemeToggleItem() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const t = useT();
|
||||
const isDark = resolvedTheme === "dark";
|
||||
return (
|
||||
<CommandMenu.Item
|
||||
onSelect={() => setTheme(isDark ? "light" : "dark")}
|
||||
keywords={["theme", "dark", "light", "mode"]}
|
||||
>
|
||||
{isDark ? <IconSun size={16} /> : <IconMoon size={16} />}
|
||||
{t("root.toggleTheme")}
|
||||
</CommandMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const [cmdkOpen, setCmdkOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const t = useT();
|
||||
useCommandMenuShortcut(useCallback(() => setCmdkOpen(true), []));
|
||||
return (
|
||||
<>
|
||||
<CommandMenu
|
||||
open={cmdkOpen}
|
||||
onOpenChange={setCmdkOpen}
|
||||
changelog={changelog}
|
||||
changelogKey="chat"
|
||||
>
|
||||
<CommandMenu.Group heading={t("root.commandActions")}>
|
||||
<CommandMenu.Item onSelect={() => {}}>
|
||||
{t("root.commandSearch")}
|
||||
</CommandMenu.Item>
|
||||
<CommandMenu.Item
|
||||
onSelect={() => navigate("/agent")}
|
||||
keywords={[
|
||||
"agent",
|
||||
"context",
|
||||
"files",
|
||||
"connections",
|
||||
"jobs",
|
||||
"access",
|
||||
]}
|
||||
>
|
||||
<IconHierarchy2 size={16} />
|
||||
{t("settings.openAgentSettings")}
|
||||
</CommandMenu.Item>
|
||||
</CommandMenu.Group>
|
||||
<CommandMenu.Group heading={t("root.commandAppearance")}>
|
||||
<ThemeToggleItem />
|
||||
</CommandMenu.Group>
|
||||
</CommandMenu>
|
||||
<AppLayout>
|
||||
<Outlet />
|
||||
</AppLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Root() {
|
||||
const [queryClient] = useState(() => createAgentNativeQueryClient());
|
||||
return (
|
||||
<AppToolkitProvider>
|
||||
<AppProviders queryClient={queryClient} i18n={{ catalog: i18nCatalog }}>
|
||||
<DbSyncSetup />
|
||||
<AppContent />
|
||||
</AppProviders>
|
||||
</AppToolkitProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { ErrorBoundary } from "@agent-native/core/client/ui";
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
AgentChatSurface,
|
||||
markAgentChatHomeHandoff,
|
||||
} from "@agent-native/core/client/agent-chat";
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
import { TAB_ID } from "@/lib/tab-id";
|
||||
|
||||
const SEO_TITLE = `${APP_TITLE} - Open Source AI app starter with actions`;
|
||||
const SEO_DESCRIPTION =
|
||||
"Open Source starter for agent-native apps with durable chat, shared actions, UI state, tools, and a backend your agent can extend.";
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{ title: SEO_TITLE },
|
||||
{
|
||||
name: "description",
|
||||
content: SEO_DESCRIPTION,
|
||||
},
|
||||
{ property: "og:title", content: SEO_TITLE },
|
||||
{ property: "og:description", content: SEO_DESCRIPTION },
|
||||
{ name: "twitter:card", content: "summary" },
|
||||
{ name: "twitter:title", content: SEO_TITLE },
|
||||
{ name: "twitter:description", content: SEO_DESCRIPTION },
|
||||
];
|
||||
}
|
||||
|
||||
function chatThreadPath(threadId: string | null) {
|
||||
return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
|
||||
}
|
||||
|
||||
export default function ChatRoute() {
|
||||
const { threadId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const t = useT();
|
||||
const threadUrlSync = threadId
|
||||
? {
|
||||
routeThreadId: threadId,
|
||||
getPath: chatThreadPath,
|
||||
navigate,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
function handleChatRunning(event: Event) {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail?.isRunning === true) markAgentChatHomeHandoff("chat");
|
||||
}
|
||||
|
||||
window.addEventListener("agentNative.chatRunning", handleChatRunning);
|
||||
return () =>
|
||||
window.removeEventListener("agentNative.chatRunning", handleChatRunning);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background">
|
||||
<AgentChatSurface
|
||||
mode="page"
|
||||
chatViewTransition
|
||||
className="h-full"
|
||||
defaultMode="chat"
|
||||
storageKey="chat"
|
||||
threadUrlSync={threadUrlSync}
|
||||
browserTabId={TAB_ID}
|
||||
showHeader={false}
|
||||
showTabBar={false}
|
||||
dynamicSuggestions={false}
|
||||
suggestions={[
|
||||
t("chat.suggestionCapabilities"),
|
||||
t("chat.suggestionCustomize"),
|
||||
t("chat.suggestionActions"),
|
||||
]}
|
||||
emptyStateText={t("chat.emptyState")}
|
||||
emptyStateDisplay="hidden"
|
||||
centerComposerWhenEmpty
|
||||
composerLayoutVariant="hero"
|
||||
composerPlaceholder={t("chat.composerPlaceholder")}
|
||||
composerSlot={
|
||||
<div className="mx-auto mb-5 max-w-xl px-4 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-normal text-foreground sm:text-3xl">
|
||||
{t("chat.heroTitle")}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
{t("chat.heroDescription")}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
AgentChatSurface,
|
||||
AgentTabsPage,
|
||||
} from "@agent-native/core/client/agent-chat";
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
|
||||
|
||||
import { resolveAgentPageComponent } from "@/lib/agent-page";
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: `Agent - ${APP_TITLE}` }];
|
||||
}
|
||||
|
||||
export default function AgentRoute() {
|
||||
const t = useT();
|
||||
useSetPageTitle(t("settings.agentTitle"));
|
||||
|
||||
const AgentPage = resolveAgentPageComponent({
|
||||
AgentChatSurface,
|
||||
AgentTabsPage,
|
||||
});
|
||||
return <AgentPage appName={APP_TITLE} />;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default, meta } from "./_index";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { DbAdminPage } from "@agent-native/core/client/db-admin";
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "Database" }];
|
||||
}
|
||||
|
||||
export default function DatabasePage() {
|
||||
const t = useT();
|
||||
useSetPageTitle(t("pages.databaseTitle"));
|
||||
return (
|
||||
<div className="h-full">
|
||||
<DbAdminPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./extensions.$id";
|
||||
export { default } from "./extensions.$id";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ExtensionViewerPage } from "@agent-native/core/client/extensions";
|
||||
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: `Extension — ${APP_TITLE}` }];
|
||||
}
|
||||
|
||||
export default function ExtensionViewerRoute() {
|
||||
return <ExtensionViewerPage />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Navigate } from "react-router";
|
||||
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: `Extensions — ${APP_TITLE}` }];
|
||||
}
|
||||
|
||||
export default function ExtensionsRoute() {
|
||||
return <Navigate to="/settings#extensions" replace />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function ExtensionsLayout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useT } from "@agent-native/core/client/i18n";
|
||||
import { ObservabilityDashboard } from "@agent-native/core/client/observability";
|
||||
import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
|
||||
|
||||
import enUS from "@/i18n/en-US";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: enUS.pages.observabilityPageTitle }];
|
||||
}
|
||||
|
||||
export default function ObservabilityPage() {
|
||||
const t = useT();
|
||||
useSetPageTitle(t("pages.observabilityPageTitle"));
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ObservabilityDashboard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ChangelogSettingsCard } from "@agent-native/core/client/changelog";
|
||||
import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
|
||||
import { TeamPage } from "@agent-native/core/client/org";
|
||||
import {
|
||||
AccountSettingsCard,
|
||||
SettingsTabsPage,
|
||||
useAgentSettingsTabs,
|
||||
type SettingsSearchEntry,
|
||||
} from "@agent-native/core/client/settings";
|
||||
import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
import changelog from "../../CHANGELOG.md?raw";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: `Settings - ${APP_TITLE}` }];
|
||||
}
|
||||
|
||||
export default function SettingsRoute() {
|
||||
const t = useT();
|
||||
const agentSettingsTabs = useAgentSettingsTabs();
|
||||
useSetPageTitle(t("settings.title"));
|
||||
|
||||
const generalSearchEntries = useMemo<SettingsSearchEntry[]>(
|
||||
() => [
|
||||
{
|
||||
id: "chat-language",
|
||||
label: t("settings.languageTitle"),
|
||||
keywords: "language locale translation i18n",
|
||||
hash: "language",
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsTabsPage
|
||||
account={<AccountSettingsCard />}
|
||||
teamLabel={t("navigation.team")}
|
||||
extraTabs={agentSettingsTabs}
|
||||
generalSearchEntries={generalSearchEntries}
|
||||
general={
|
||||
<div className="mx-auto w-full max-w-2xl space-y-6">
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{t("settings.description")}
|
||||
</p>
|
||||
|
||||
<Card id="language" className="scroll-mt-16">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{t("settings.languageTitle")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("settings.languageDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="max-w-xs space-y-1.5">
|
||||
<Label>{t("settings.languageLabel")}</Label>
|
||||
<LanguagePicker label={t("settings.languageLabel")} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
team={
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<TeamPage
|
||||
showTitle={false}
|
||||
createOrgDescription={t("pages.teamCreateOrgDescription")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
whatsNew={
|
||||
<div className="mx-auto w-full max-w-2xl">
|
||||
<ChangelogSettingsCard markdown={changelog} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Navigate } from "react-router";
|
||||
|
||||
import { APP_TITLE } from "@/lib/app-config";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: `Team — ${APP_TITLE}` }];
|
||||
}
|
||||
|
||||
export default function TeamRoute() {
|
||||
return <Navigate to="/settings#organization" replace />;
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "react-dom/server.browser" {
|
||||
export * from "react-dom/server";
|
||||
export { default } from "react-dom/server";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: added
|
||||
date: 2026-06-24
|
||||
---
|
||||
|
||||
A new Settings page gives quick access to language, workspace, and agent preferences.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: added
|
||||
date: 2026-06-24
|
||||
---
|
||||
|
||||
Added a language picker and localized app chrome for supported languages.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fixed
|
||||
date: 2026-06-27
|
||||
---
|
||||
|
||||
Traditional Chinese copy now uses Taiwan terminology and clearer technical wording.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-06-28
|
||||
---
|
||||
|
||||
Left sidebar collapse motion and footer controls now feel smoother and use less divider chrome.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-06-29
|
||||
---
|
||||
|
||||
Chat layouts adapt when the agent sidebar is open.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: improved
|
||||
---
|
||||
|
||||
Settings are cleaner and searchable, with a consistent navigation that jumps straight to any setting.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-10
|
||||
---
|
||||
|
||||
Chat now makes AI connection setup clear without shifting the composer.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: added
|
||||
date: 2026-07-13
|
||||
---
|
||||
|
||||
A full Agent page now brings context, files, connections, jobs, and external access together
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fixed
|
||||
date: 2026-07-14
|
||||
---
|
||||
|
||||
Chat opens reliably on hosted deployments instead of failing during startup
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fixed
|
||||
date: 2026-07-14
|
||||
---
|
||||
|
||||
Fixed chat template startup with older core versions
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: removed
|
||||
date: 2026-07-15
|
||||
---
|
||||
|
||||
The main navigation is now focused on Chat, Agent, and essential workspace settings.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fixed
|
||||
date: 2026-07-17
|
||||
---
|
||||
|
||||
The agent chat sidebar stays closed until you open it or start a chat handoff.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-22
|
||||
---
|
||||
|
||||
The sidebar footer now keeps Feedback and the collapse control on one compact row.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fixed
|
||||
date: 2026-07-22
|
||||
---
|
||||
|
||||
Full-page chat keeps the active conversation when moving to and from the sidebar.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-22
|
||||
---
|
||||
|
||||
Manage agent navigation now uses the connected-nodes icon.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-22
|
||||
---
|
||||
|
||||
Recent chats now expand from a compact five-item list with New chat anchored at the bottom.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-23
|
||||
---
|
||||
|
||||
Full-page chat is better centered, with quieter chat history and a left-aligned New chat action.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-24
|
||||
---
|
||||
|
||||
Secondary controls and dashboard surfaces now use quieter borderless styling.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-24
|
||||
---
|
||||
|
||||
Sidebar utility controls now follow a consistent footer order.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: improved
|
||||
date: 2026-07-25
|
||||
---
|
||||
|
||||
Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/global.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "patterns": ["data/**/*.json"] }
|
||||
@@ -0,0 +1,5 @@
|
||||
# Learnings
|
||||
|
||||
<!-- This file is your app's memory. The AI reads it at the start of every conversation and updates it when it learns something new. -->
|
||||
<!-- Your personal learnings.md is gitignored so preferences and private info stay local. -->
|
||||
<!-- This defaults file is what new checkouts start with. -->
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user