Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3784c43
feat(appkit): reference agent-app, dev-playground chat UI, docs, and …
MarioCadenas Apr 21, 2026
aae324f
fix(appkit): align chat clients + template with renamed 'agents' plugin
MarioCadenas Apr 22, 2026
41404e3
docs(agents): folder layout on disk, migrate samples, sync API refs
MarioCadenas Apr 23, 2026
51c7bfa
docs(appkit): regenerate typedoc API reference for folder-agents loader
MarioCadenas Apr 23, 2026
2dfa041
feat(dev-playground): port Smart Dashboard as /smart-dashboard route;…
MarioCadenas Apr 24, 2026
a371f8a
feat(dev-playground): stage 2-4 of smart-dashboard demo
MarioCadenas Apr 24, 2026
1f64b8f
feat(appkit): sub-agent approval gate + save view to volume + saved v…
MarioCadenas May 4, 2026
c5f85c4
fix(playground): treat missing saved-views dir as empty list, not 500
MarioCadenas Apr 24, 2026
1e29c53
fix(appkit): forward all sub-agent events except metadata
MarioCadenas May 4, 2026
755cfbf
fix(playground): use html2canvas-pro to support oklch() colors
MarioCadenas Apr 24, 2026
2352683
fix(playground): unwrap DownloadResponse when serving saved-view PNGs
MarioCadenas Apr 24, 2026
9cc3e2c
fix(playground): apply saved view directly from metadata on thumbnail…
MarioCadenas Apr 24, 2026
d91f9c3
docs(appkit): regenerate typedoc for tool annotations
MarioCadenas Apr 24, 2026
906be67
feat(playground): revamp smart dashboard with denser charts and actio…
MarioCadenas Apr 24, 2026
e4fda01
feat(playground): hamburger nav with shared catalog and redesigned home
MarioCadenas Apr 24, 2026
56584d2
feat(playground): tiered approval card — writes vs updates vs destruc…
MarioCadenas Apr 24, 2026
8b43c4f
fix(playground): pin agent-feed card tints to sRGB hex
MarioCadenas Apr 24, 2026
8d0af9f
fix(playground): gate Tailwind dark: variant on the theme class
MarioCadenas Apr 24, 2026
098ccc6
fix(playground): stop streaming chat bubbles from pulsing
MarioCadenas Apr 27, 2026
8be75bb
chore(playground): migrate dev-playground server to onPluginsReady
MarioCadenas Apr 27, 2026
1051359
feat(template): scaffold a working starter agent
MarioCadenas Apr 29, 2026
463161e
fix(playground, template): import agents from @databricks/appkit/beta
MarioCadenas May 4, 2026
76e7f67
docs: beta agents banner, template stability, and unified typedoc entry
MarioCadenas May 4, 2026
d95be98
chore: remove plans scratch docs from agents stack branch
MarioCadenas May 4, 2026
38562da
chore(appkit): regenerate typedoc and sync lockfile after rebase
MarioCadenas May 7, 2026
133c185
chore(appkit): regenerate typedoc after rebase onto v5
MarioCadenas May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CHANGELOG.md
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, this file shouldn't be modified, right? Because there's no breaking change if we haven't released agent plugin yet?

(BTW, why do we have so many "Changelog" headers? 😄 something is wrong with our changelog generation, I guess)

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@

All notable changes to this project will be documented in this file.

# Changelog
## Unreleased

# Changelog
### appkit

# Changelog
* **appkit:** **Breaking change:** markdown agents must live under `config/agents/<id>/agent.md`. Top-level `config/agents/*.md` is no longer discovered; migrate each file to `<stem>/agent.md`. The reserved folder `config/agents/skills` is ignored until per-agent skills ship.

## [0.25.1](https://github.com/databricks/appkit/compare/v0.25.0...v0.25.1) (2026-04-27)

### appkit

* **appkit:** check isRetryable before retrying in interceptor ([#276](https://github.com/databricks/appkit/issues/276)) ([1c994a6](https://github.com/databricks/appkit/commit/1c994a6d99f397b56e90f1b53df06a61f02b9e82))


## [0.25.0](https://github.com/databricks/appkit/compare/v0.24.0...v0.25.0) (2026-04-23)

### files

* **files:** per-volume in-app policy enforcement ([#197](https://github.com/databricks/appkit/issues/197)) ([f54dca5](https://github.com/databricks/appkit/commit/f54dca5da5af5368c7bcb18745715b54a99d47e9))

# Changelog

Expand Down
64 changes: 64 additions & 0 deletions apps/dev-playground/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion apps/dev-playground/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"@tanstack/router-plugin": "1.133.22",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"html2canvas": "1.4.1",
"html2canvas-pro": "2.0.2",
"lucide-react": "0.546.0",
"react": "19.2.0",
"react-dom": "19.2.0",
Expand All @@ -30,6 +32,7 @@
},
"devDependencies": {
"@eslint/js": "9.36.0",
"@tailwindcss/postcss": "4.1.17",
"@tanstack/router-cli": "1.133.20",
"@types/node": "24.6.0",
"@types/react": "19.2.2",
Expand All @@ -43,7 +46,6 @@
"postcss": "8.5.6",
"shiki": "3.15.0",
"tailwindcss": "4.1.17",
"@tailwindcss/postcss": "4.1.17",
"typescript": "5.9.3",
"typescript-eslint": "8.45.0",
"vite": "npm:rolldown-vite@7.1.14"
Expand Down
Copy link
Copy Markdown
Member

@pkosiec pkosiec May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot of the code for the dev playground.

Honestly, I'm not sure if that fits in the dev playground - maybe we should move it somewhere? E.g. Move it as a separate template in app-templates?

IMO this is too good to keep it in the dev playground.
Just saying, it's not a blocker but I think it has a huge potential 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, as a second thought - the "Smart dashboard" example is kind of reimplementing Metric Views? Correct me if I'm wrong 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic reviews (aggregated report):

Code Review — agent/v2/6-apps-docs

Context

Branch agent/v2/6-apps-docs (based on agent/v2/5-fromplugin-runagent) adds ~22K lines implementing the full agents system: core agent runtime, agents plugin, MCP client, plugin context mediator, tool primitives, ToolProvider surfaces on all core plugins, smart dashboard demo, template updates, and docs. This review covers both correctness bugs and developer experience issues.


What works well

  • Progressive disclosure — 5 levels (markdown drop-in → frontmatter scoping → code agents → sub-agents → standalone) is a strong pedagogical ladder
  • tool() ergonomics — Zod schema drives type inference + JSON Schema generation + runtime validation with LLM-friendly errors
  • fromPlugin() pattern — elegant lazy references; no instance coupling, spread-friendly, clear error messages with Available: [...] listing
  • Safety defaultsautoInheritTools: false, approval gates, resource limits, MCP host policy, SQL classifier. Two-key operation for auto-inherit is well-designed
  • docs/docs/plugins/agents.md — comprehensive, well-structured, covers the full lifecycle with runnable examples at each level

P0 — Must fix

1. Doc/behavior contradiction on auto-inherit

File: docs/docs/plugins/agents.md:57-58 vs :233

Level 1 documentation says:

"4. Auto-inherits every registered ToolProvider plugin's tools (analytics.*, files.*, ...)"

But the Configuration Reference says:

"autoInheritTools defaults to { file: false, code: false } — no tools spread into any agent unless the developer explicitly opts in."

These directly contradict each other. A developer following Level 1 will expect their markdown agent to see all plugin tools out of the box, but it won't — they get zero tools. This is the single most confusing thing in the agents DX.

Fix: Either (a) add agents({ autoInheritTools: { file: true } }) to the Level 1 example, or (b) rewrite the Level 1 narrative to say the agent has no tools yet and point to Level 2.


P1 — Should fix before merge

2. MCP callTool corrupts results when text is undefined

File: packages/appkit/src/connectors/mcp/client.ts:264-275

McpToolCallResult.content[].text is typed text?: string (line 58), but callTool maps c.text without filtering undefined:

.filter((c) => c.type === "text")
.map((c) => c.text)       // c.text can be undefined
.join("\n");               // produces "undefined" in output

Fix: Change both occurrences (error path line 267 and success path line 274) to filter:

.filter((c): c is { type: string; text: string } => c.type === "text" && typeof c.text === "string")
.map((c) => c.text)

3. MCP sendNotification ignores HTTP error status

File: packages/appkit/src/connectors/mcp/client.ts:362-389

The notifications/initialized fetch doesn't check response.ok. A 4xx/5xx response is silently ignored, making connect() appear successful when the server may not have registered the client.

Fix: Warn (don't throw — MCP spec says notifications are fire-and-forget):

if (!response.ok) {
  logger.warn("MCP notification %s failed: %s %s", method, response.status, response.statusText);
}

4. MCP SSE response body read has no size limit

File: packages/appkit/src/connectors/mcp/client.ts:339-340

response.text() reads the entire SSE body into memory. A malicious or misconfigured MCP server could send unbounded data.

Fix: Read incrementally with a size cap (e.g., 10 MB), or keep response.text() but add a Content-Length check if the header is present and document the 30s AbortSignal.timeout as the backstop.

5. reload() races with in-flight streams

File: packages/appkit/src/plugins/agents/agents.ts:161-168

reload() calls mcpClient.close() and sets it to null. In-flight streams captured this.mcpClient at _streamAgent call time (line 802). The old client is now closed, so sendRpc throws "MCP client is closed" mid-stream.

Fix: Don't close the old client synchronously — let in-flight streams drain. Or simpler: don't close it at all (it has no keep-alive connections; GC collects after in-flight refs drop).

6. tool() return type lies about string-only

Files: packages/appkit/src/core/agent/tools/tool.ts:19, function-tool.ts:19

ToolConfig.execute is typed as (args) => Promise<string> | string, and FunctionTool.execute matches. But the template's helper.ts:29-30 returns objects:

execute: () => ({ now: new Date().toISOString() }),

This works at runtime (the result gets serialized downstream) but the type is wrong. Developers naturally want to return structured objects from tools.

Fix: Widen ToolConfig.execute and FunctionTool.execute return types to unknown | Promise<unknown> (matching defineTool's handler signature), with serialization handled by the runner.

7. SSE parsing boilerplate in template AgentChat

File: template/client/src/pages/agents/AgentChat.tsx:104-125

~20 lines of manual SSE frame splitting, data-line extraction, and JSON parsing. Every developer who scaffolds an app will copy this verbatim. The comment at line 34 acknowledges the gap. Consider shipping a thin parseSSEStream(reader) async generator in @databricks/appkit-ui so the template doesn't teach low-level SSE plumbing as the canonical pattern.


P2 — Fix if straightforward

8. Files and Genie plugin tools don't forward signal

Files:

  • packages/appkit/src/plugins/files/plugin.ts:1059-1123
  • packages/appkit/src/plugins/genie/genie.ts:63-99

All tool handlers in _defineVolumeTools and _defineSpaceTools ignore the signal parameter from ToolEntry.handler. The analytics and lakebase plugins correctly forward it.

Fix: Add signal parameter to each handler. For genie sendMessage, pass it to abort the async iteration.

9. MCP JSON-RPC response shape not validated

File: packages/appkit/src/connectors/mcp/client.ts:349-351

as JsonRpcResponse is an unsafe cast. A malformed server response could pass through without json.error, json.result, or matching json.id.

Fix: Add minimal validation:

if (typeof json !== "object" || json === null || json.jsonrpc !== "2.0") {
  throw new Error(`MCP response for ${method} is not valid JSON-RPC 2.0`);
}

10. Hardcoded 30s tool execution timeout

File: packages/appkit/src/core/plugin-context.ts:187

const timeout = 30_000; is not configurable. Complex SQL queries or batch operations may need more.

Fix: Accept an optional timeoutMs in the executeTool signature with 30_000 as default.

11. Missing approval timeout validation

File: packages/appkit/src/plugins/agents/agents.ts:124

cfg.timeoutMs from user config is not validated. A negative or zero value causes immediate denial.

Fix: timeoutMs: Math.max(cfg.timeoutMs ?? 60_000, 1_000)

12. Inconsistent tool annotation fields across plugins

Files: lakebase.ts:181-185, analytics.ts:290, files/plugin.ts:1067,1108,1119, genie.ts:76,96

The ToolAnnotations type defines a preferred effect enum ("read" | "write" | "update" | "destructive") but no plugin uses it — all use the deprecated readOnly/destructive booleans. The lakebase plugin sets both readOnly AND destructive (inverse of each other) which is redundant.

Fix: Standardize on the effect field across all plugins. Remove redundant pairs in lakebase.

13. Stale comments reference destructive: true instead of effect

Files: shared/src/agent.ts:126, core/agent/types.ts:149, docs/docs/plugins/agents.md:334

These all reference the legacy boolean form. Since effect is now preferred and destructive is @deprecated, docs and comments should lead with effect.

14. createAgent({ name }) precedence unclear

File: packages/appkit/src/core/agent/types.ts:77-78

AgentDefinition.name is optional ("Filled in from the enclosing key"), but the template explicitly sets it. Does explicit name override the key? What if they differ? The template should either omit name or the docs should explain precedence.

15. tool() vs defineTool() callback naming inconsistency

Files: tool.ts, define-tool.ts

Two tool-creation functions for different audiences (agent authors vs plugin authors) use different callback names: execute vs handler. A developer switching from agent-side to plugin-side tool authoring will wonder why the API changed.


P3 — Low priority

16. Sub-agent input extraction is lenient

File: packages/appkit/src/plugins/agents/agents.ts:975-980

Falls back to JSON.stringify(args) when args.input isn't a string. An object like { input: null } serializes the entire args object.

17. AbortSignal.any() requires Node 20+

File: packages/appkit/src/connectors/mcp/client.ts:327

No polyfill or feature detection. Document Node 20+ as minimum requirement or add a fallback.

18. MCP connectAll doesn't surface partial failures clearly

File: packages/appkit/src/connectors/mcp/client.ts:103-116

Failed connections are logged but not thrown. Callers can't distinguish "all connected" from "partially connected".

19. tool() description silently falls back to name

File: tool.ts:40description: config.description ?? config.name

If a developer omits description, the LLM sees the tool name as the description (e.g., "get_weather"). Consider logging a warning or making description required.

20. Playground server is too dense for a reference

File: apps/dev-playground/server/index.ts — 750 lines

Mixes agent setup, file policy harness, saved-views CRUD, and OBO/SP smoke tests. Consider extracting non-agent concerns into separate files.

21. No shared SSE/stream hook in appkit-ui

The playground has custom hooks (use-agent-stream.ts, use-action-dispatcher.ts) that demonstrate SSE consumption, but these are local — not reusable. The template reimplements the same SSE parsing inline, creating two divergent implementations.


Not a bug (reviewer false positives)

  • Budget consumed before approval: Intentional security design ("Counted pre-dispatch so a prompt-injected agent cannot drain the budget silently via denied calls")
  • EventChannel push/close race: Node.js is single-threaded; these can't execute concurrently
  • Stream cleanup ordering: activeStreams.delete() in driver's finally runs before the generator drains, but approval gate entries are already resolved. The 404 to late approvals is correct behavior
  • OTel context leak in asUser() dev proxy: Proxy objects and closures are lightweight; GC handles them fine

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CheckCircle2Icon } from "lucide-react";
import { useEffect, useState } from "react";

interface ActionToastProps {
/**
* Latest dispatcher-surfaced action summary. Each new value bumps a
* render key so the toast re-animates even if the same message arrives
* twice (e.g. two identical filter calls in a row).
*/
message: string | null;
durationMs?: number;
}

/**
* Non-intrusive bottom-left toast that confirms every agent-driven UI
* action. Silent success was the worst failure mode before: an action
* silently not-applied looked identical to one that worked but didn't
* show its effect.
*/
export function ActionToast({ message, durationMs = 2800 }: ActionToastProps) {
const [visible, setVisible] = useState<{ key: number; text: string } | null>(
null,
);

useEffect(() => {
if (!message) return;
const key = Date.now();
setVisible({ key, text: message });
const t = setTimeout(() => {
setVisible((v) => (v?.key === key ? null : v));
}, durationMs);
return () => {
clearTimeout(t);
};
}, [message, durationMs]);

if (!visible) return null;

return (
<div
key={visible.key}
className="fixed bottom-20 left-4 z-30 rounded-full bg-card border border-border shadow-lg px-3 py-1.5 flex items-center gap-2 animate-in fade-in slide-in-from-bottom-2 duration-200"
>
<CheckCircle2Icon className="h-3.5 w-3.5 text-green-500 shrink-0" />
<span className="text-xs text-foreground">{visible.text}</span>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
AlertTriangleIcon,
ArrowRightIcon,
CalendarIcon,
CrosshairIcon,
DollarSignIcon,
HighlighterIcon,
LightbulbIcon,
MapPinIcon,
MessageSquareIcon,
} from "lucide-react";
import type { FeedAction } from "../lib/feed-actions";

type Variant = "insight" | "anomaly";
type Severity = "low" | "medium" | "high";

interface ActionableCardProps {
variant: Variant;
severity?: Severity;
title: string;
description: string;
actions: FeedAction[];
/** Fired for non-ask actions. Route applies them to dashboard state. */
onAction: (action: FeedAction) => void;
/** Fired for `ask` actions. Route forwards the prompt to the chat drawer. */
onAsk: (prompt: string) => void;
}

// Backgrounds are written as arbitrary 8-digit hex (e.g. `bg-[#eff6ff80]`)
// instead of Tailwind's `/N` alpha shorthand. Rationale: `bg-blue-50/50`
// compiles in Tailwind v4 to a pair — an sRGB hex fallback and a
// `@supports (color-mix)` override that re-mixes in oklab over the oklch
// palette token. Browsers that support `color-mix` (recent Chrome/Arc) take
// the oklab path; older embedded Chromiums (e.g. Cursor's built-in browser
// at the time of writing) fall through to the sRGB hex. Because oklab and
// sRGB interpolation produce visibly different tints — especially against
// the dark `--card` token — the same card ends up looking different in each
// browser. Pinning the colour to a literal hex (no `/N`, no @supports
// override) keeps all browsers on the same sRGB path and therefore the same
// visual result.
const INSIGHT_STYLES = {
border: "border-blue-200 dark:border-blue-900",
bg: "bg-[#eff6ff80] dark:bg-[#1624564d]",
icon: "text-blue-500",
};

const ANOMALY_STYLES: Record<
Severity,
{ border: string; bg: string; icon: string; badge: string }
> = {
low: {
border: "border-yellow-200 dark:border-yellow-900",
bg: "bg-[#fefce880] dark:bg-[#4320044d]",
icon: "text-yellow-500",
badge:
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-400",
},
medium: {
border: "border-orange-200 dark:border-orange-900",
bg: "bg-[#fff7ed80] dark:bg-[#4413064d]",
icon: "text-orange-500",
badge:
"bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-400",
},
high: {
border: "border-red-200 dark:border-red-900",
bg: "bg-[#fef2f280] dark:bg-[#4608094d]",
icon: "text-red-500",
badge: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400",
},
};

function iconForAction(kind: FeedAction["kind"]): React.ReactNode {
const cls = "h-3 w-3";
switch (kind) {
case "filter_date":
return <CalendarIcon className={cls} />;
case "filter_zip":
return <MapPinIcon className={cls} />;
case "filter_fare":
return <DollarSignIcon className={cls} />;
case "highlight_period":
return <HighlighterIcon className={cls} />;
case "highlight_zone":
return <MapPinIcon className={cls} />;
case "focus_chart":
return <CrosshairIcon className={cls} />;
case "ask":
return <MessageSquareIcon className={cls} />;
}
}

/**
* Action chip for a single feed suggestion. The chip's visual weight depends
* on its kind: structural mutations (filter/highlight/focus) use the primary
* tint, `ask` uses a neutral outline so the user can tell "this opens the
* chat" from "this changes the dashboard" without reading the label.
*/
function ActionChip({
action,
onAction,
onAsk,
}: {
action: FeedAction;
onAction: (a: FeedAction) => void;
onAsk: (prompt: string) => void;
}) {
const isAsk = action.kind === "ask";
const isHighlight =
action.kind === "highlight_period" || action.kind === "highlight_zone";

return (
<button
type="button"
onClick={() => {
if (isAsk) onAsk(action.prompt);
else onAction(action);
}}
className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded-md transition-colors ${
isAsk
? "border border-border bg-background text-foreground/80 hover:bg-muted hover:text-foreground"
: isHighlight
? "bg-amber-100 text-amber-800 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60"
: "bg-primary/10 text-primary hover:bg-primary/20"
}`}
>
{iconForAction(action.kind)}
<span>{action.label}</span>
{isAsk && <ArrowRightIcon className="h-3 w-3 opacity-70" />}
</button>
);
}

export function ActionableCard({
variant,
severity,
title,
description,
actions,
onAction,
onAsk,
}: ActionableCardProps) {
const isAnomaly = variant === "anomaly";
const styles = isAnomaly
? ANOMALY_STYLES[severity ?? "low"]
: { ...INSIGHT_STYLES, badge: "" };

return (
<div className={`rounded-lg border ${styles.border} ${styles.bg} p-3`}>
<div className="flex items-start gap-2 mb-2">
{isAnomaly ? (
<AlertTriangleIcon
className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`}
/>
) : (
<LightbulbIcon className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`} />
)}
<div className="min-w-0 flex-1">
<div className="flex items-start gap-2">
<p className="text-sm font-medium text-foreground leading-tight flex-1">
{title}
</p>
{isAnomaly && severity && (
<span
className={`text-[10px] font-medium px-1.5 py-0.5 rounded shrink-0 ${styles.badge}`}
>
{severity}
</span>
)}
</div>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
{description}
</p>
</div>
</div>

{actions.length > 0 && (
<div className="flex flex-wrap gap-1.5 pl-6">
{actions.map((action, i) => (
<ActionChip
key={`${action.kind}-${i}-${action.label}`}
action={action}
onAction={onAction}
onAsk={onAsk}
/>
))}
</div>
)}
</div>
);
}
Loading