Skip to content

Commit cba352e

Browse files
authored
fix(calendar): PR #154 review fixes — delete safety, full undo, bounded stack (#155)
* fix(calendar): address PR #154 review feedback - Remove dangerous Delete-key-to-discard in QuickEditInput (was invisible when text was present, Enter would silently delete the event) - Delete undo now preserves full event snapshot (attendees, recurrence, conferenceData, etc.) instead of just 7 basic fields - Cap undo stack at 20 entries to prevent unbounded growth * fix: calendar quick-edit cleanup, agent panel & content editor improvements * feat(calendar): extract QuickEditPopover component + compose toolbar improvements * fix: AgentPanel loading stuck on error, agents saved as shared scope - Fix setLoading(false) never called on non-OK response (try→finally) - Save agents with shared:true so they're discoverable for @-mentions in production - Include concurrent mail template changes * fix: stop mail nitro task spam + integrations plugin crash on auto-mount - Remove Nitro scheduled tasks from mail vite.config (the setInterval-based scheduler in server plugins already handles job processing; Nitro tasks don't resolve in dev mode and spam ERR_MODULE_NOT_FOUND every 30s) - Fix integrations plugin crash when h3App is undefined (happens during auto-mount via framework request handler's fake nitroApp shim) * fix: 9 bugs + add slash commands and DEVELOPMENT.md Bugs fixed: - Calendar: event creation now shows full detail popover with editable title that auto-saves on dismiss, works in both popover and sidebar mode - Analytics: remove catch-all redirect that masked SSR errors and caused dashboard flash-then-redirect - Forms: fix open-in-new-tab by returning Response object instead of using H3 headers (Vite 8 SSR compat); remove broken publicFormSSR plugin - Integrations: refactor plugin to use getH3App() so routes actually mount - @-tagging: preserve query strings in framework request handler so mention search and resource scope params work - Files sidebar: same query string fix enables proper resource fetching - Backdrop filter: add -webkit- prefix and cssTarget for Safari compat - Cloudflare cold starts: add Smart Placement to all wrangler configs - Recruiting: add OrgSwitcher component to sidebar for multi-tenancy Features: - Add /clear, /new, /history, /help slash commands to agent chat - Add DEVELOPMENT.md with setup, workspace structure, and env var docs * fix: address PR review feedback — blur listener leak, toolbar dismiss, mail jobs - ComposeBubbleToolbar: use named handleBlur ref so editor.off actually removes the listener; add setTimeout check so link/AI inputs don't dismiss the toolbar when autoFocus fires - Add mail-jobs server plugin with setInterval scheduler for processJobs (snooze, scheduled-send) and processAutomations, replacing the Nitro tasks that were removed in f3df24c * fix: update mail-jobs plugin imports after tasks/ dir removed in main * ci: retrigger Cloudflare builds * fix: remove Smart Placement from slides worker (incompatible with CF config) * fix: createRequire patch regex fails on minified $ in variable names The CF Workers deploy patch for createRequire used (\w+) to capture the aliased variable name, but minifiers like Rolldown produce names like L$n where $ is valid in JS identifiers but not matched by \w. Changed to ([\w$]+) so the patch catches these and stubs them out.
1 parent f7c9210 commit cba352e

44 files changed

Lines changed: 1736 additions & 994 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/scheduled_tasks.lock

Lines changed: 0 additions & 1 deletion
This file was deleted.

DEVELOPMENT.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Development Guide
2+
3+
## Prerequisites
4+
5+
- **Node.js** >= 22 (v24+ recommended)
6+
- **pnpm** >= 10 (`corepack enable` to use the version pinned in templates)
7+
8+
## Getting Started
9+
10+
```bash
11+
git clone https://github.com/BuilderIO/agent-native.git
12+
cd agent-native/framework
13+
pnpm install
14+
```
15+
16+
The `postinstall` script automatically builds `@agent-native/core` and `@agent-native/pinpoint`, which other packages depend on.
17+
18+
## Development
19+
20+
### Run all template apps
21+
22+
```bash
23+
pnpm run dev:all
24+
```
25+
26+
This builds core first, then starts every template app in parallel on sequential ports.
27+
28+
### Run a single package or template
29+
30+
```bash
31+
pnpm --filter mail dev # run the mail template
32+
pnpm --filter calendar dev # run the calendar template
33+
pnpm --filter @agent-native/core dev # watch-build core
34+
pnpm --filter @agent-native/docs dev # run the docs site
35+
```
36+
37+
### Electron desktop app
38+
39+
```bash
40+
pnpm run dev:electron # run the desktop app
41+
pnpm run dev:electron:apps # run with template apps
42+
```
43+
44+
## Workspace Structure
45+
46+
This is a pnpm monorepo. Workspaces are defined in `pnpm-workspace.yaml`.
47+
48+
### Packages (`packages/`)
49+
50+
| Package | Description |
51+
| ------------------- | ---------------------------------------------------------------------------------------------- |
52+
| `core` | Core framework library (`@agent-native/core`) -- CLI, server plugins, agent tools, Vite plugin |
53+
| `desktop-app` | Electron desktop app |
54+
| `mobile-app` | Mobile app |
55+
| `docs` | Documentation site |
56+
| `pinpoint` | Pinpoint package |
57+
| `shared-app-config` | Shared app configuration |
58+
59+
### Templates (`templates/`)
60+
61+
Production-ready template apps that demonstrate the framework. Each template is a standalone app with its own `package.json`, Drizzle schema, actions, and UI.
62+
63+
Templates: `analytics`, `calendar`, `calorie-tracker`, `content`, `forms`, `issues`, `mail`, `recruiting`, `slides`, `starter`, `videos`
64+
65+
Each template uses the same scripts:
66+
67+
```bash
68+
pnpm dev # start dev server (via agent-native dev)
69+
pnpm build # production build
70+
pnpm action <name> # run an agent action
71+
pnpm typecheck # type-check
72+
```
73+
74+
## Environment Variables
75+
76+
Templates read from `.env` in their own directory. Key variables:
77+
78+
| Variable | Purpose |
79+
| ---------------------- | ------------------------------------------------------------- |
80+
| `DATABASE_URL` | Database connection string (see below) |
81+
| `ANTHROPIC_API_KEY` | API key for Claude (required for agent chat) |
82+
| `ACCESS_TOKEN` | Enables auth in production mode; without it, auth is bypassed |
83+
| `GOOGLE_CLIENT_ID` | Google OAuth client ID (for Gmail, Calendar integrations) |
84+
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
85+
86+
### Database options
87+
88+
Set `DATABASE_URL` to connect to your database. When unset, defaults to a local SQLite file at `data/app.db`.
89+
90+
| Provider | Example `DATABASE_URL` |
91+
| ---------------- | ---------------------------------------------------------- |
92+
| SQLite (default) | _(unset, or `file:./data/app.db`)_ |
93+
| Neon Postgres | `postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/db` |
94+
| Supabase | `postgresql://user:pass@db.xxx.supabase.co:5432/postgres` |
95+
| Turso (libSQL) | `libsql://your-db.turso.io?authToken=...` |
96+
| Plain Postgres | `postgresql://user:pass@localhost:5432/mydb` |
97+
98+
All SQL must be dialect-agnostic -- never assume SQLite.
99+
100+
## Key Commands
101+
102+
Run these from the repo root:
103+
104+
| Command | Description |
105+
| -------------------- | ------------------------------------------------------- |
106+
| `pnpm run prep` | Format + typecheck + test in parallel (run before push) |
107+
| `pnpm run fmt` | Format all files with Prettier |
108+
| `pnpm run fmt:check` | Check formatting without writing |
109+
| `pnpm run typecheck` | Type-check all packages and templates |
110+
| `pnpm test` | Run tests (core + docs) |
111+
| `pnpm run lint` | Format check + typecheck |
112+
113+
## Building
114+
115+
```bash
116+
pnpm run build # build all packages and templates
117+
```
118+
119+
Individual packages:
120+
121+
```bash
122+
pnpm --filter @agent-native/core build
123+
pnpm --filter mail build
124+
```

packages/core/src/agent/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface MentionProvider {
4646
icon?: string;
4747
search: (
4848
query: string,
49+
/** The H3 event for the current request — use to make internal API calls */
50+
event?: any,
4951
) => MentionProviderItem[] | Promise<MentionProviderItem[]>;
5052
}
5153

packages/core/src/client/AgentPanel.tsx

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import {
4747
IconClockHour3,
4848
IconDotsVertical,
4949
IconHistory,
50+
IconTrash,
51+
IconPlugConnected,
5052
} from "@tabler/icons-react";
5153
import {
5254
MultiTabAssistantChat,
@@ -343,6 +345,9 @@ function AgentSettingsPopover({
343345
<IntegrationsPanel />
344346
</Suspense>
345347
</div>
348+
<div className="border-t border-border pt-3 mt-3">
349+
<AgentsSection />
350+
</div>
346351
</div>
347352
</div>,
348353
document.body,
@@ -351,6 +356,219 @@ function AgentSettingsPopover({
351356
);
352357
}
353358

359+
// ─── Agents Management Section ───────────────────────────────────────────────
360+
361+
function AgentsSection() {
362+
const [showAdd, setShowAdd] = useState(false);
363+
const [name, setName] = useState("");
364+
const [url, setUrl] = useState("");
365+
const [description, setDescription] = useState("");
366+
const nameRef = useRef<HTMLInputElement>(null);
367+
368+
// Fetch agents from resources
369+
const [agents, setAgents] = useState<
370+
{
371+
id: string;
372+
path: string;
373+
name: string;
374+
url: string;
375+
description?: string;
376+
}[]
377+
>([]);
378+
const [loading, setLoading] = useState(true);
379+
380+
const fetchAgents = useCallback(async () => {
381+
try {
382+
const res = await fetch("/_agent-native/resources?scope=all");
383+
if (!res.ok) return;
384+
const data = await res.json();
385+
const agentResources = (data.resources ?? []).filter(
386+
(r: { path: string }) =>
387+
r.path.startsWith("agents/") && r.path.endsWith(".json"),
388+
);
389+
const parsed = await Promise.all(
390+
agentResources.map(async (r: { id: string; path: string }) => {
391+
try {
392+
const detail = await fetch(`/_agent-native/resources/${r.id}`);
393+
if (!detail.ok) return null;
394+
const d = await detail.json();
395+
const config = JSON.parse(d.content);
396+
return {
397+
id: r.id,
398+
path: r.path,
399+
name: config.name,
400+
url: config.url,
401+
description: config.description,
402+
};
403+
} catch {
404+
return null;
405+
}
406+
}),
407+
);
408+
setAgents(parsed.filter(Boolean));
409+
} finally {
410+
setLoading(false);
411+
}
412+
}, []);
413+
414+
useEffect(() => {
415+
fetchAgents();
416+
}, [fetchAgents]);
417+
418+
useEffect(() => {
419+
if (showAdd) {
420+
setName("");
421+
setUrl("");
422+
setDescription("");
423+
const t = setTimeout(() => nameRef.current?.focus(), 50);
424+
return () => clearTimeout(t);
425+
}
426+
}, [showAdd]);
427+
428+
const handleAdd = async () => {
429+
const trimmedName = name.trim();
430+
const trimmedUrl = url.trim();
431+
if (!trimmedName || !trimmedUrl) return;
432+
433+
const id = trimmedName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
434+
const agentJson = JSON.stringify(
435+
{
436+
id,
437+
name: trimmedName,
438+
description: description.trim() || undefined,
439+
url: trimmedUrl,
440+
color: "#6B7280",
441+
},
442+
null,
443+
2,
444+
);
445+
446+
try {
447+
const res = await fetch("/_agent-native/resources", {
448+
method: "POST",
449+
headers: { "Content-Type": "application/json" },
450+
body: JSON.stringify({
451+
path: `agents/${id}.json`,
452+
content: agentJson,
453+
shared: true,
454+
}),
455+
});
456+
if (res.ok) {
457+
setShowAdd(false);
458+
fetchAgents();
459+
}
460+
} catch {}
461+
};
462+
463+
const handleDelete = async (agentId: string) => {
464+
try {
465+
const res = await fetch(`/_agent-native/resources/${agentId}`, {
466+
method: "DELETE",
467+
});
468+
if (res.ok) fetchAgents();
469+
} catch {}
470+
};
471+
472+
return (
473+
<div>
474+
<div className="flex items-center justify-between mb-2">
475+
<label className="text-[11px] font-medium text-muted-foreground">
476+
Agents
477+
</label>
478+
<button
479+
onClick={() => setShowAdd(!showAdd)}
480+
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50"
481+
title="Add agent"
482+
>
483+
{showAdd ? <IconX size={12} /> : <IconPlus size={12} />}
484+
</button>
485+
</div>
486+
487+
{showAdd && (
488+
<div className="mb-2 flex flex-col gap-1.5 rounded-md border border-border bg-background p-2">
489+
<input
490+
ref={nameRef}
491+
value={name}
492+
onChange={(e) => setName(e.target.value)}
493+
onKeyDown={(e) => {
494+
if (e.key === "Enter") handleAdd();
495+
if (e.key === "Escape") setShowAdd(false);
496+
}}
497+
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
498+
placeholder="Name"
499+
/>
500+
<input
501+
value={url}
502+
onChange={(e) => setUrl(e.target.value)}
503+
onKeyDown={(e) => {
504+
if (e.key === "Enter") handleAdd();
505+
if (e.key === "Escape") setShowAdd(false);
506+
}}
507+
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
508+
placeholder="URL"
509+
/>
510+
<input
511+
value={description}
512+
onChange={(e) => setDescription(e.target.value)}
513+
onKeyDown={(e) => {
514+
if (e.key === "Enter") handleAdd();
515+
if (e.key === "Escape") setShowAdd(false);
516+
}}
517+
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
518+
placeholder="Description (optional)"
519+
/>
520+
<div className="flex justify-end">
521+
<button
522+
onClick={handleAdd}
523+
disabled={!name.trim() || !url.trim()}
524+
className="rounded bg-accent px-2.5 py-1 text-[11px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40 disabled:pointer-events-none"
525+
>
526+
Add
527+
</button>
528+
</div>
529+
</div>
530+
)}
531+
532+
{loading ? (
533+
<p className="text-[11px] text-muted-foreground/50">Loading...</p>
534+
) : agents.length === 0 && !showAdd ? (
535+
<p className="text-[11px] text-muted-foreground/50">
536+
No agents configured. Add one to @-mention and communicate via A2A.
537+
</p>
538+
) : (
539+
<div className="flex flex-col gap-1">
540+
{agents.map((agent) => (
541+
<div
542+
key={agent.id}
543+
className="group flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent/30"
544+
>
545+
<IconPlugConnected
546+
size={13}
547+
className="shrink-0 text-muted-foreground"
548+
/>
549+
<div className="flex-1 min-w-0">
550+
<div className="text-[12px] font-medium text-foreground truncate">
551+
{agent.name}
552+
</div>
553+
<div className="text-[10px] text-muted-foreground/60 truncate">
554+
{agent.url}
555+
</div>
556+
</div>
557+
<button
558+
onClick={() => handleDelete(agent.id)}
559+
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground/40 hover:text-red-400 opacity-0 group-hover:opacity-100"
560+
title="Remove agent"
561+
>
562+
<IconTrash size={12} />
563+
</button>
564+
</div>
565+
))}
566+
</div>
567+
)}
568+
</div>
569+
);
570+
}
571+
354572
// ─── AgentPanel ─────────────────────────────────────────────────────────────
355573

356574
export interface AgentPanelProps extends Omit<

0 commit comments

Comments
 (0)