Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7912662
feat(appkit): reference agent-app, dev-playground chat UI, docs, and …
MarioCadenas Apr 21, 2026
9fc631c
fix(appkit): align chat clients + template with renamed 'agents' plugin
MarioCadenas Apr 22, 2026
4d2a60b
chore(appkit): pin @langchain/core and @types/js-yaml devDependency v…
MarioCadenas Apr 23, 2026
7c05c46
docs(agents): folder layout on disk, migrate samples, sync API refs
MarioCadenas Apr 23, 2026
46137c1
docs(appkit): regenerate typedoc API reference for folder-agents loader
MarioCadenas Apr 23, 2026
183f62e
feat(dev-playground): port Smart Dashboard as /smart-dashboard route;…
MarioCadenas Apr 24, 2026
4513cb0
feat(dev-playground): stage 2-4 of smart-dashboard demo
MarioCadenas Apr 24, 2026
07f57b6
feat(appkit): sub-agent approval gate + save view to volume + saved v…
MarioCadenas Apr 24, 2026
4f0db5e
fix(playground): treat missing saved-views dir as empty list, not 500
MarioCadenas Apr 24, 2026
f290120
fix(appkit): forward all sub-agent events except metadata
MarioCadenas Apr 24, 2026
99b91b4
fix(playground): use html2canvas-pro to support oklch() colors
MarioCadenas Apr 24, 2026
3295cf4
fix(playground): unwrap DownloadResponse when serving saved-view PNGs
MarioCadenas Apr 24, 2026
c7ff49f
fix(playground): apply saved view directly from metadata on thumbnail…
MarioCadenas Apr 24, 2026
0269dd8
docs(appkit): regenerate typedoc for tool annotations
MarioCadenas Apr 24, 2026
46909a9
feat(playground): revamp smart dashboard with denser charts and actio…
MarioCadenas Apr 24, 2026
2f55d13
feat(playground): hamburger nav with shared catalog and redesigned home
MarioCadenas Apr 24, 2026
398b881
feat(playground): tiered approval card — writes vs updates vs destruc…
MarioCadenas Apr 24, 2026
682bb6d
fix(playground): pin agent-feed card tints to sRGB hex
MarioCadenas Apr 24, 2026
d8fc551
fix(playground): gate Tailwind dark: variant on the theme class
MarioCadenas Apr 24, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.

# Changelog

## Unreleased

### appkit

* **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.24.0](https://github.com/databricks/appkit/compare/v0.23.0...v0.24.0) (2026-04-20)

* add AST extraction to serving type generator and move types to shared/ ([#279](https://github.com/databricks/appkit/issues/279)) ([422afb3](https://github.com/databricks/appkit/commit/422afb38aa73f8adb94e091225dc3381bd92cfcd))
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
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