Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@

An open-source, in-app inspector for [eve](https://github.com/vercel/eve) agents. It turns an agent's event stream into a live visual trace of conversations, reasoning, tool calls, token usage, and timing.

The inspector runs as a floating panel inside your app, making it easier to understand agent behavior without switching to a separate dashboard.
The inspector runs in a side panel that makes room for itself beside your app, making it easier to understand agent behavior without switching to a separate dashboard.

## Features

- Inspect turns, reasoning, tool inputs and outputs, errors, and usage as they happen.
- Resize the side panel while the normal page layout makes room for it.
- Use the full-screen panel on narrow mobile viewports.
- Drop it into any browser app with a small framework-agnostic API or the React provider.
- Keep application styles isolated with a shadow root.
- Debug locally without sending data anywhere. The package makes no network requests.

### Fixed host elements

The panel resizes normal and sticky page layouts without changing their positioning contexts. Browser-fixed elements remain anchored to the viewport by design. If your app has a control fixed to the right edge, offset it with the panel width exposed by the devtools:

```css
.fixed-control {
right: var(--eve-devtools-panel-width, 0px);
}
```

The property returns to its previous value when the panel is collapsed or the devtools are unmounted.

## Install

```sh
Expand Down Expand Up @@ -67,10 +81,40 @@ Issues and pull requests are welcome. To run the project locally:
bun install
bun run dev # demo page with a minimal eve agent traced live
bun run check # lint, format, and types
bun run test # behavior tests
```

The demo lives in `demo/` and calls its model through the Vercel AI Gateway. Set `AI_GATEWAY_API_KEY` in `demo/.env.local` before starting it.

## Releasing

Publish releases manually from a clean, up-to-date `main` branch:

```sh
git checkout main
git pull --ff-only origin main

npm login # only needed when not already authenticated
bun pm whoami
bun pm pkg set version=0.0.4

bun run check
bun run test
bun run build
(cd demo && bun run build)
bun publish --dry-run

git add package.json
git commit -m "Release v0.0.4"
git tag v0.0.4
git push origin main
git push origin v0.0.4

bun publish --access public
```

After publishing, verify the registry entry with `bun pm view eve-devtools@0.0.4`.

## License

MIT
18 changes: 18 additions & 0 deletions bun.lock

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

56 changes: 53 additions & 3 deletions demo/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Client } from "eve/client";
import {
Client,
type InputOption,
type InputRequest,
type SendTurnInput,
} from "eve/client";
import { mount } from "../../index";

const devtools = mount();
Expand All @@ -20,12 +25,54 @@ const addMessage = (role: "user" | "assistant", text: string) => {
return message;
};

const send = async (text: string) => {
const createOptionButton = (request: InputRequest, option: InputOption) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = option.label;
button.addEventListener("click", () => {
const siblings = button.parentElement?.querySelectorAll("button") ?? [];
for (const sibling of siblings) {
sibling.disabled = true;
}
const response = { requestId: request.requestId, optionId: option.id };
devtools.onEvent({
type: "client.input.responded",
data: { responses: [response] },
});
void send(option.label, {
inputResponses: [response],
});
});
return button;
};

const renderInputRequests = (
reply: HTMLParagraphElement,
requests: readonly InputRequest[],
) => {
reply.replaceChildren();
reply.classList.add("question");
for (const request of requests) {
const prompt = document.createElement("span");
prompt.textContent = request.prompt;
reply.append(prompt);
if (request.options !== undefined && request.options.length > 0) {
const options = document.createElement("span");
options.className = "options";
options.append(
...request.options.map((option) => createOptionButton(request, option)),
);
reply.append(options);
}
}
};

const send = async (text: string, turnInput: SendTurnInput = text) => {
input.disabled = true;
addMessage("user", text);
const reply = addMessage("assistant", "…");
try {
const response = await session.send(text);
const response = await session.send(turnInput);
for await (const event of response) {
devtools.onEvent(event);
if (event.type === "message.appended") {
Expand All @@ -34,6 +81,9 @@ const send = async (text: string) => {
if (event.type === "message.completed" && event.data.message !== null) {
reply.textContent = event.data.message;
}
if (event.type === "input.requested") {
renderInputRequests(reply, event.data.requests);
}
messages.scrollTop = messages.scrollHeight;
}
} finally {
Expand Down
31 changes: 31 additions & 0 deletions demo/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,37 @@ main p {
align-self: flex-start;
}

.question {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.options {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}

.options button {
padding: 0.25rem 0.625rem;
border: 1px solid #404040;
border-radius: 1rem;
background: #232323;
color: inherit;
font: inherit;
cursor: pointer;
}

.options button:hover {
background: #2c2c2c;
}

.options button:disabled {
opacity: 0.5;
cursor: default;
}

form {
width: 100%;
max-width: 640px;
Expand Down
1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"devDependencies": {
"@tailwindcss/vite": "^4.3.2",
"eve": "0.22.3",
"microsandbox": "^0.6.6",
"tailwindcss": "^4.3.2",
"vite": "^8.1.3",
"zod": "^4.4.3"
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"build": "vite build",
"prepack": "bun run build",
"check": "biome check . && tsc --noEmit",
"fix": "biome check --write ."
"fix": "biome check --write .",
"test": "bun test"
},
"peerDependencies": {
"react": ">=17"
Expand All @@ -47,6 +48,7 @@
"devDependencies": {
"@biomejs/biome": "^2.5.2",
"@tailwindcss/vite": "^4.3.2",
"@types/bun": "^1.3.14",
"@types/react": "^19.2.17",
"cnfast": "^0.0.8",
"lucide-preact": "^1.23.0",
Expand Down
13 changes: 13 additions & 0 deletions src/components/disclosure-chevron.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { cn } from "cnfast";
import { ChevronLeft } from "lucide-preact";

export function DisclosureChevron({ className }: { className?: string }) {
return (
<ChevronLeft
class={cn(
"disclosure-chevron size-3.5 text-neutral-500 transition-transform",
className,
)}
/>
);
}
6 changes: 4 additions & 2 deletions src/components/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export function Entry({
children: ComponentChildren;
}) {
return (
<div class="flex gap-2.5 py-1">
<span class="mt-0.5 shrink-0 text-neutral-400">{icon}</span>
<div class="flex gap-2 py-1">
<span class="flex h-[1lh] w-3.5 shrink-0 items-center justify-center text-neutral-400 [&>svg]:size-3.5">
{icon}
</span>
<div class="min-w-0 flex-1">{children}</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/json-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { stringifyJson } from "@/trace/format";

export function JsonView({ value }: { value: unknown }) {
return (
<pre class="scroll overflow-x-auto rounded-md border border-line-1 bg-background p-2 font-mono text-xs leading-5 text-foreground/70">
<pre class="scroll max-h-[calc(10lh+1rem+2px)] overflow-auto rounded-md border border-line-1 bg-background p-2 font-mono text-xs leading-5 text-foreground/70">
{stringifyJson(value)}
</pre>
);
Expand Down
36 changes: 33 additions & 3 deletions src/components/metric.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
import { cn } from "cnfast";
import type { ComponentChildren } from "preact";

// An icon paired with a compact value, e.g. a token count or a duration.
type MetricTone = "neutral" | "input" | "output";

const toneClass: Record<MetricTone, string> = {
neutral: "text-neutral-300",
input: "text-metric-input",
output: "text-metric-output",
};

const gapClass: Record<MetricTone, string> = {
neutral: "gap-[3px]",
input: "gap-0.5",
output: "gap-0.5",
};

// A compact labeled value, optionally paired with an icon.
export function Metric({
icon,
label,
tabularNumbers = false,
tone = "neutral",
children,
}: {
icon: ComponentChildren;
icon?: ComponentChildren;
label: string;
tabularNumbers?: boolean;
tone?: MetricTone;
children: ComponentChildren;
}) {
return (
<span class="flex items-center gap-1 text-xs tabular-nums text-neutral-400">
<span
title={label}
class={cn(
"flex shrink-0 items-center text-xs leading-none [&>svg]:size-3.5",
gapClass[tone],
tabularNumbers && "tabular-nums",
toneClass[tone],
)}
>
<span class="sr-only">{label}: </span>
{icon}
{children}
</span>
Expand Down
Loading
Loading