@hachej/boring-agent 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,82 +1,282 @@
1
- # @boring/agent
1
+ # @hachej/boring-agent
2
2
 
3
- Agent runtime and chat UI for boring-ui apps.
3
+ <div align="center">
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![npm](https://img.shields.io/npm/v/@hachej/boring-agent.svg)](https://www.npmjs.com/package/@hachej/boring-agent)
7
+
8
+ </div>
9
+
10
+ A pane-embeddable coding agent with three execution modes behind one interface. Ships as a standalone CLI (`npx @hachej/boring-agent`) and composes into any app shell.
4
11
 
5
12
  ```bash
6
- pnpm add @boring/agent
13
+ npx @hachej/boring-agent
7
14
  ```
8
15
 
9
16
  ---
10
17
 
11
- ## What it provides
18
+ ## TL;DR
19
+
20
+ **The Problem**: You want a coding agent in the browser — but also as a CLI — and you need it to run your code somewhere safe (or not). Existing solutions force you into one deployment model and one UI.
21
+
22
+ **The Solution**: One package that ships a full LLM agent loop, tool catalog, and chat UI — with swappable execution backends. Same agent, same tools, same UI. Three modes. Zero config to start.
23
+
24
+ ### Why Use @hachej/boring-agent?
25
+
26
+ | Feature | What It Does |
27
+ |---------|--------------|
28
+ | **Three execution modes** | `direct` (no isolation, macOS dev) / `local` (bwrap sandbox) / `vercel-sandbox` (Firecracker microVM) |
29
+ | **CLI + embeddable** | `npx @hachej/boring-agent` works standalone; `<ChatPanel />` composes into any layout |
30
+ | **7 standard tools** | `bash`, `read`, `write`, `edit`, `find`, `grep`, `ls` — ported from pi-coding-agent |
31
+ | **Workspace-agnostic FS** | `Workspace` interface — agent tools and HTTP routes share the same filesystem view |
32
+ | **Session management** | List, create, switch, delete sessions with streamed history hydration |
33
+ | **UI bridge** | Agent opens files, panels, and surfaces in the workbench via typed commands |
34
+ | **Model picker + thinking toggle** | Inline in the composer — switch models and reasoning depth per message |
35
+
36
+ ### Quick Example
37
+
38
+ ```bash
39
+ # Start the agent in your current directory — zero setup
40
+ npx @hachej/boring-agent
41
+
42
+ # Or run with a specific workspace root
43
+ BORING_AGENT_WORKSPACE_ROOT=/path/to/project npx @hachej/boring-agent
44
+
45
+ # Set the API key
46
+ ANTHROPIC_API_KEY=sk-ant-... npx @hachej/boring-agent
47
+
48
+ # Run in local sandbox mode (Linux + bubblewrap)
49
+ BORING_AGENT_MODE=local npx @hachej/boring-agent
50
+ ```
51
+
52
+ In the browser chat, try:
53
+ ```
54
+ read the README and summarize it
55
+ find all TypeScript files that import "react"
56
+ write a test for src/utils.ts
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Architecture
62
+
63
+ ```
64
+ ┌─────────────────────────────────────┐
65
+ │ Chat UI (browser) │
66
+ │ Composer · Messages · SessionBar │
67
+ └──────────────────┬──────────────────┘
68
+ │ UIMessage stream (SSE)
69
+ ┌──────────────────▼──────────────────┐
70
+ │ Agent Harness │
71
+ │ (pi-coding-agent loop) │
72
+ └──────────────────┬──────────────────┘
73
+ │ AgentTool[]
74
+ ┌──────────────────▼──────────────────┐
75
+ │ Tool Catalog │
76
+ │ bash · read · write · edit │
77
+ │ find · grep · ls │
78
+ └──────┬──────────────┬───────────────┘
79
+ │ │
80
+ ┌──────▼─────┐ ┌──────▼─────┐
81
+ │ Workspace │ │ Sandbox │
82
+ │ (fs ops) │ │ (exec) │
83
+ │ read/write │ │ bwrap │
84
+ │ readdir │ │ vercel │
85
+ └────────────┘ └────────────┘
86
+ ```
87
+
88
+ **Two layers, clear boundary:**
89
+
90
+ - **Layer 1 (Core runtime):** `AgentHarness` · `Catalog` · `Workspace` · `Sandbox` — interfaces locked; adapters swap per mode.
91
+ - **Layer 2 (Integration):** `SessionStore` · `UiBridge` · `Provisioning` — replaceable plumbing, independent evolution.
92
+
93
+ ### Execution Modes
94
+
95
+ | Mode | Workspace | Sandbox | Isolation | Use Case |
96
+ |------|-----------|---------|-----------|----------|
97
+ | `direct` | `NodeWorkspace` | `DirectSandbox` | None | macOS/Windows dev, quick tests |
98
+ | `local` | `NodeWorkspace` | `BwrapSandbox` | bwrap process jail | Linux deployments, safer default |
99
+ | `vercel-sandbox` | `VercelSandboxWorkspace` | `VercelSandboxExec` | Firecracker microVM | Multi-tenant, remote execution |
12
100
 
13
- - **Agent runtime** LLM conversation loop with streaming, tool calling, and three execution modes
14
- - **Tool catalog** — `bash`, `read`, `write`, `edit`, `grep`, `find`, `ls` and more
15
- - **Chat UI** — embeddable `ChatPanel` React component
101
+ **Pairing invariant:** Workspace + Sandbox must target the same filesystem substrate. The adapter factory enforces this at construction — mismatched pairs are impossible.
16
102
 
17
103
  ---
18
104
 
19
- ## Execution modes
105
+ ## Installation
106
+
107
+ ```bash
108
+ # npm
109
+ npm install @hachej/boring-agent
110
+
111
+ # pnpm
112
+ pnpm add @hachej/boring-agent
113
+
114
+ # standalone (no install needed)
115
+ npx @hachej/boring-agent
116
+ ```
20
117
 
21
- | Mode | Isolation | Typical use |
22
- |---|---|---|
23
- | `direct` | None | Local dev |
24
- | `local` | `bwrap` process isolation | Safer Linux deployments |
25
- | `vercel-sandbox` | Firecracker microVM | Multi-tenant / remote |
118
+ ### From Source
119
+
120
+ ```bash
121
+ git clone https://github.com/hachej/boring-ui.git
122
+ cd boring-ui
123
+ pnpm install
124
+ pnpm --filter @hachej/boring-agent build
125
+ ```
26
126
 
27
127
  ---
28
128
 
29
- ## Quickstart
129
+ ## Quick Start
130
+
131
+ ### 1. As a CLI
132
+
133
+ ```bash
134
+ # Set your API key
135
+ export ANTHROPIC_API_KEY=sk-ant-...
136
+
137
+ # Run in your project directory
138
+ cd /path/to/project
139
+ npx @hachej/boring-agent
140
+ ```
141
+
142
+ Opens `http://localhost:5200` with a full agent workspace pointed at your cwd.
143
+
144
+ ### 2. Embedded in an App
145
+
146
+ **Server:**
147
+
148
+ ```ts
149
+ import { createAgentApp } from "@hachej/boring-agent/server"
150
+
151
+ const app = await createAgentApp({
152
+ mode: "local", // "direct" | "local" | "vercel-sandbox"
153
+ workspaceRoot: process.cwd(),
154
+ apiBaseUrl: "http://localhost:3000",
155
+ })
156
+ await app.listen({ port: 3001 })
157
+ ```
158
+
159
+ **Frontend:**
30
160
 
31
161
  ```tsx
32
- import { ChatPanel } from "@boring/agent"
33
- import { WorkspaceProvider, IdeLayout } from "@boring/workspace"
162
+ import { ChatPanel, useAgentChat } from "@hachej/boring-agent"
163
+ import "@hachej/boring-agent/front/styles.css"
164
+
165
+ function App() {
166
+ return <ChatPanel apiBaseUrl="http://localhost:3000" />
167
+ }
168
+ ```
169
+
170
+ ### 3. Composed with Workspace
34
171
 
35
- export function App() {
172
+ ```tsx
173
+ import { WorkspaceProvider, IdeLayout } from "@hachej/boring-workspace"
174
+ import { ChatPanel } from "@hachej/boring-agent"
175
+
176
+ function App() {
36
177
  return (
37
- <WorkspaceProvider chatPanel={ChatPanel}>
178
+ <WorkspaceProvider chatPanel={ChatPanel} workspaceId="proj-1">
38
179
  <IdeLayout />
39
180
  </WorkspaceProvider>
40
181
  )
41
182
  }
42
183
  ```
43
184
 
44
- Server:
185
+ ---
45
186
 
46
- ```ts
47
- import { createAgentApp } from "@boring/agent/server"
187
+ ## Package Surfaces
48
188
 
49
- const app = await createAgentApp({ mode: "local", workspaceRoot: process.cwd() })
50
- await app.listen({ port: 3001 })
51
- ```
189
+ | Import | Environment | What You Get |
190
+ |--------|-------------|--------------|
191
+ | `@hachej/boring-agent` | Browser | `ChatPanel`, `SessionToolbar`, primitives, hooks, `theme.css` |
192
+ | `@hachej/boring-agent/server` | Node | `createAgentApp`, routes, harness, sandbox, workspace adapters |
193
+ | `@hachej/boring-agent/front` | Browser | Frontend-specific (same as top-level, explicit subpath) |
194
+ | `@hachej/boring-agent/shared` | Any | `AgentHarness`, `Workspace`, `Sandbox`, `AgentTool`, `SessionStore` interfaces |
195
+ | `@hachej/boring-agent/front/styles.css` | Browser | CSS custom properties for theming |
196
+ | `@hachej/boring-agent/eval` | Node | Evaluation toolkit for agent behavior |
52
197
 
53
198
  ---
54
199
 
55
- ## Model config
200
+ ## Configuration
56
201
 
57
- ```bash
58
- ANTHROPIC_API_KEY=sk-ant-...
59
- BORING_AGENT_DEFAULT_MODEL_PROVIDER=anthropic
60
- BORING_AGENT_DEFAULT_MODEL_ID=claude-sonnet-4-6
202
+ ### Environment Variables
203
+
204
+ | Variable | Required | Default | Description |
205
+ |----------|----------|---------|-------------|
206
+ | `ANTHROPIC_API_KEY` | Yes | — | Anthropic API key for Claude |
207
+ | `BORING_AGENT_MODE` | No | `direct` | `direct`, `local`, or `vercel-sandbox` |
208
+ | `BORING_AGENT_WORKSPACE_ROOT` | No | `.` | Root directory for workspace |
209
+ | `BORING_AGENT_DEFAULT_MODEL_PROVIDER` | No | `anthropic` | Default model provider |
210
+ | `BORING_AGENT_DEFAULT_MODEL_ID` | No | `claude-sonnet-4-6` | Default model ID |
211
+ | `VERCEL_OIDC_TOKEN` | Remote only | — | Required for `vercel-sandbox` mode |
212
+ | `PORT` | No | `5200` | Server port |
213
+ | `HOST` | No | `localhost` | Server host |
214
+
215
+ ### Config File
216
+
217
+ `boring.app.toml` (optional, for embedded mode):
218
+
219
+ ```toml
220
+ [runtime]
221
+ mode = "local" # direct | local | vercel-sandbox
222
+
223
+ [model]
224
+ default_provider = "anthropic"
225
+ default_id = "claude-sonnet-4-6"
61
226
  ```
62
227
 
63
228
  ---
64
229
 
65
- ## Package surfaces
230
+ ## Troubleshooting
66
231
 
67
- ```ts
68
- import { ChatPanel } from "@boring/agent" // React chat UI
69
- import { ... } from "@boring/agent/server" // Node/server entry
70
- import { ... } from "@boring/agent/front" // Frontend-only
71
- import { ... } from "@boring/agent/shared" // Platform-agnostic contracts
72
- ```
232
+ | Error | Cause | Fix |
233
+ |-------|-------|-----|
234
+ | `ANTHROPIC_API_KEY not set` | Missing API key | `export ANTHROPIC_API_KEY=sk-ant-...` |
235
+ | `bwrap not found` (local mode) | bubblewrap not installed | `sudo apt install bubblewrap` (Debian/Ubuntu) |
236
+ | `port already in use` | Port 5200 occupied | `PORT=5201 npx @hachej/boring-agent` |
237
+ | `workspace root not found` | Invalid `BORING_AGENT_WORKSPACE_ROOT` | Point to an existing directory |
238
+ | `Vercel sandbox auth failed` (remote mode) | Missing/invalid OIDC token | Set `VERCEL_OIDC_TOKEN` |
239
+ | `model provider not supported` | Unknown provider in config | Use `anthropic` (only supported provider in v1) |
240
+
241
+ ---
242
+
243
+ ## Limitations
244
+
245
+ - **Single model provider**: Only Anthropic (Claude) is supported in v1. The harness interface is designed to accept others, but only `anthropic` is wired.
246
+ - **No multi-user auth**: The agent is single-workspace-per-instance. Multi-user auth, billing, and workspace CRUD belong to `@hachej/boring-core`.
247
+ - **No git UI**: The agent runs git via `bash`, but there's no status bar, diff pane, or branch picker. When git UI lands, thin routes will be added.
248
+ - **Plugin loading is local-only**: Pi plugins load in the backend Node process. They're disabled in `vercel-sandbox` mode for security.
249
+ - **No browser-agent mode yet**: The `AgentHarness` interface has a `placement: "browser"` option, but no browser harness is implemented.
250
+ - **No MCP tool integration**: Not in scope for v1.
251
+
252
+ ---
253
+
254
+ ## FAQ
255
+
256
+ **Q: What's the difference between `direct` and `local` mode?**
257
+ A: `direct` runs bash commands with no sandbox — the agent has full access to your machine. `local` wraps commands in bubblewrap (`bwrap`), which provides filesystem and process isolation on Linux. Use `direct` for macOS dev; use `local` on Linux servers.
258
+
259
+ **Q: Can I use OpenAI or other model providers?**
260
+ A: Not in v1. Only Anthropic's Claude is wired up. The harness interface is provider-agnostic — community PRs for other providers are welcome.
261
+
262
+ **Q: How do sessions persist?**
263
+ A: Via pi-coding-agent's JSONL session files under `${workdir}/.pi/sessions/`. The `PiSessionStore` reads and manages lifecycle. SQLite and IndexedDB implementations are planned.
264
+
265
+ **Q: Can I add custom tools to the agent?**
266
+ A: Yes. Use the `CatalogDeps` pattern to build tools that bind to `Workspace` and `Sandbox`. For pi-native tools, register them via pi's extension system (`pi.extensions` in config). In `vercel-sandbox` mode, extensions are disabled.
267
+
268
+ **Q: What's the UI bridge for?**
269
+ A: It lets the agent programmatically open files, panels, and surfaces in the workbench. The agent calls `exec_ui({ kind: "openFile", params: { path: "src/index.ts" } })` and the panel opens. It's a typed pubsub bus between backend and frontend.
270
+
271
+ **Q: How does stream resumption work?**
272
+ A: The server wraps the harness's event stream in a per-turn ring buffer. Disconnected clients reconnect via `GET /api/v1/agent/chat/:sessionId/:turnId?cursor=<n>`. If the turn completed, it replays from `SessionStore`.
273
+
274
+ ---
275
+
276
+ *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
73
277
 
74
278
  ---
75
279
 
76
- ## Part of [boring-ui](https://github.com/hachej/boring-ui)
280
+ ## License
77
281
 
78
- | Package | Role |
79
- |---|---|
80
- | `@boring/core` | DB, auth, app factory |
81
- | `@boring/workspace` | Plugin system, layouts |
82
- | `@boring/agent` | Agent runtime + tools |
282
+ MIT
@@ -0,0 +1,6 @@
1
+ import {
2
+ DebugDrawer
3
+ } from "./chunk-MMJA3QON.js";
4
+ export {
5
+ DebugDrawer
6
+ };
@@ -0,0 +1,38 @@
1
+ // src/shared/tool-ui.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ function isToolUiMetadata(value) {
6
+ if (!isRecord(value)) return false;
7
+ return (value.rendererId === void 0 || typeof value.rendererId === "string") && (value.displayGroup === void 0 || typeof value.displayGroup === "string") && (value.icon === void 0 || typeof value.icon === "string");
8
+ }
9
+ function extractToolUiMetadata(output) {
10
+ if (!isRecord(output)) return void 0;
11
+ const details = output.details;
12
+ if (!isRecord(details)) return void 0;
13
+ if (isToolUiMetadata(details.ui)) return details.ui;
14
+ if (typeof details.uiKind === "string") {
15
+ return {
16
+ rendererId: details.uiKind,
17
+ details
18
+ };
19
+ }
20
+ return void 0;
21
+ }
22
+
23
+ // src/shared/capabilities.ts
24
+ var DEFAULT_AGENT_RUNTIME_CAPABILITIES = {
25
+ nativeFollowUp: false,
26
+ aiSdkOwnsHistory: true
27
+ };
28
+ var PI_AGENT_RUNTIME_CAPABILITIES = {
29
+ nativeFollowUp: true,
30
+ aiSdkOwnsHistory: false
31
+ };
32
+
33
+ export {
34
+ isToolUiMetadata,
35
+ extractToolUiMetadata,
36
+ DEFAULT_AGENT_RUNTIME_CAPABILITIES,
37
+ PI_AGENT_RUNTIME_CAPABILITIES
38
+ };
@@ -0,0 +1,295 @@
1
+ // src/front/DebugDrawer.tsx
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ // src/front/lib/index.ts
5
+ import { clsx } from "clsx";
6
+ import { twMerge } from "tailwind-merge";
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+
11
+ // src/front/DebugDrawer.tsx
12
+ import { Button, IconButton, Tabs, TabsContent, TabsList, TabsTrigger } from "@hachej/boring-ui-kit";
13
+ import {
14
+ CheckIcon,
15
+ ChevronDownIcon,
16
+ ChevronRightIcon,
17
+ CopyIcon,
18
+ RefreshCwIcon
19
+ } from "lucide-react";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ function CopyButton({ value, label }) {
22
+ const [copied, setCopied] = useState(false);
23
+ const onCopy = useCallback(() => {
24
+ const writeText = navigator.clipboard?.writeText?.bind(navigator.clipboard);
25
+ if (!writeText) return;
26
+ void writeText(value).then(() => {
27
+ setCopied(true);
28
+ window.setTimeout(() => setCopied(false), 1200);
29
+ });
30
+ }, [value]);
31
+ return /* @__PURE__ */ jsx(
32
+ IconButton,
33
+ {
34
+ type: "button",
35
+ variant: "ghost",
36
+ size: "icon-xs",
37
+ onClick: onCopy,
38
+ className: "text-muted-foreground/60",
39
+ "aria-label": label,
40
+ title: label,
41
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(CopyIcon, { className: "h-3 w-3" })
42
+ }
43
+ );
44
+ }
45
+ function DebugValue({ label, value }) {
46
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border/40 bg-muted/20 p-2", children: [
47
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
48
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground/80", children: label }),
49
+ /* @__PURE__ */ jsx(CopyButton, { value, label: `Copy ${label}` })
50
+ ] }),
51
+ /* @__PURE__ */ jsx("code", { className: "block break-all font-mono text-[11px] leading-relaxed text-foreground dark:text-zinc-100", children: value })
52
+ ] });
53
+ }
54
+ function SessionTab({ sessionId }) {
55
+ const resumeCommand = `pi --session ${sessionId}`;
56
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 overflow-auto p-3 text-[11px] text-muted-foreground dark:text-zinc-300", children: [
57
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground dark:text-zinc-300", children: "This web chat is backed by a pi session. Use the id below from the workspace root to resume the same conversation in a terminal." }),
58
+ /* @__PURE__ */ jsx(DebugValue, { label: "Pi session id", value: sessionId }),
59
+ /* @__PURE__ */ jsx(DebugValue, { label: "Resume command", value: resumeCommand }),
60
+ /* @__PURE__ */ jsxs("p", { className: "rounded-md border border-border/30 bg-muted/10 p-2 text-[10px] leading-relaxed text-muted-foreground/75 dark:text-zinc-300", children: [
61
+ "Tip: ",
62
+ /* @__PURE__ */ jsx("code", { className: "font-mono text-foreground/80", children: "pi --continue" }),
63
+ " ",
64
+ "opens the most recent session for the current working directory. The explicit command above targets this session directly."
65
+ ] })
66
+ ] });
67
+ }
68
+ var RETRY_DELAY_MS = 2500;
69
+ var MAX_RETRIES = 20;
70
+ function SystemPromptTab({
71
+ sessionId,
72
+ requestHeaders
73
+ }) {
74
+ const [state, setState] = useState({ kind: "loading" });
75
+ const [retryKey, setRetryKey] = useState(0);
76
+ const retryCount = useRef(0);
77
+ const refresh = useCallback(() => {
78
+ retryCount.current = 0;
79
+ setRetryKey((k) => k + 1);
80
+ }, []);
81
+ useEffect(() => {
82
+ let aborted = false;
83
+ let retryTimer = null;
84
+ setState({ kind: "loading" });
85
+ const opts = requestHeaders ? { headers: requestHeaders } : void 0;
86
+ fetch(`/api/v1/agent/sessions/${encodeURIComponent(sessionId)}/system-prompt`, opts).then(async (res) => {
87
+ if (aborted) return;
88
+ if (res.ok) {
89
+ const payload2 = await res.json();
90
+ if (typeof payload2.systemPrompt === "string") {
91
+ retryCount.current = 0;
92
+ setState({ kind: "ok", text: payload2.systemPrompt });
93
+ return;
94
+ }
95
+ }
96
+ const payload = await res.json().catch(() => null);
97
+ const reason = payload?.error?.message ?? `HTTP ${res.status}`;
98
+ if (res.status === 404 && retryCount.current < MAX_RETRIES) {
99
+ setState({ kind: "empty", reason });
100
+ retryTimer = setTimeout(() => {
101
+ if (!aborted) {
102
+ retryCount.current++;
103
+ setRetryKey((k) => k + 1);
104
+ }
105
+ }, RETRY_DELAY_MS);
106
+ } else {
107
+ setState(res.status === 404 ? { kind: "empty", reason } : { kind: "error", reason });
108
+ }
109
+ }).catch((err) => {
110
+ if (!aborted) setState({ kind: "error", reason: err instanceof Error ? err.message : String(err) });
111
+ });
112
+ return () => {
113
+ aborted = true;
114
+ if (retryTimer) clearTimeout(retryTimer);
115
+ };
116
+ }, [sessionId, requestHeaders, retryKey]);
117
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
118
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 flex items-center justify-between px-3 pt-2 pb-1 border-b border-border/40", children: [
119
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/80 font-mono", children: state.kind === "ok" ? `${state.text.length.toLocaleString()} chars` : state.kind === "loading" ? "loading\u2026" : state.kind === "empty" ? `waiting for session \xB7 retry ${retryCount.current}/${MAX_RETRIES}` : "error" }),
120
+ state.kind !== "loading" && /* @__PURE__ */ jsx(
121
+ IconButton,
122
+ {
123
+ type: "button",
124
+ variant: "ghost",
125
+ size: "icon-xs",
126
+ onClick: refresh,
127
+ className: "text-muted-foreground/60",
128
+ "aria-label": "Refresh system prompt",
129
+ children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: "h-3 w-3" })
130
+ }
131
+ )
132
+ ] }),
133
+ (state.kind === "loading" || state.kind === "empty") && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground p-3", children: state.kind === "loading" ? "Loading\u2026" : state.reason }),
134
+ state.kind === "error" && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive p-3", children: state.reason }),
135
+ state.kind === "ok" && /* @__PURE__ */ jsx("pre", { className: "flex-1 overflow-auto px-3 py-2 font-mono text-[11px] leading-relaxed text-foreground whitespace-pre-wrap break-words", children: state.text })
136
+ ] });
137
+ }
138
+ function partSummary(part) {
139
+ const p = part;
140
+ const type = p?.type ?? "?";
141
+ if (type === "text") {
142
+ const text = String(p.text ?? "");
143
+ return text.slice(0, 80) + (text.length > 80 ? "\u2026" : "");
144
+ }
145
+ if (type === "tool-invocation") {
146
+ const inv = p.toolInvocation;
147
+ const state = String(inv?.state ?? "");
148
+ return `${inv?.toolName ?? "?"}() \xB7 ${state}`;
149
+ }
150
+ if (type === "reasoning") {
151
+ const text = String(p.text ?? p.reasoning ?? "");
152
+ return text.slice(0, 80) + (text.length > 80 ? "\u2026" : "");
153
+ }
154
+ return type;
155
+ }
156
+ function flattenMessages(messages) {
157
+ const out = [];
158
+ for (let mi = 0; mi < messages.length; mi++) {
159
+ const msg = messages[mi];
160
+ const msgAny = msg;
161
+ const time = msgAny.createdAt ? new Date(msgAny.createdAt).toISOString().slice(11, 23) : null;
162
+ for (let pi = 0; pi < msg.parts.length; pi++) {
163
+ const part = msg.parts[pi];
164
+ out.push({
165
+ msgId: msg.id,
166
+ msgIndex: mi,
167
+ role: msg.role,
168
+ time,
169
+ partIndex: pi,
170
+ partType: part?.type ?? "unknown",
171
+ part
172
+ });
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+ function MessagesTab({ messages }) {
178
+ const parts = flattenMessages(messages);
179
+ const [expanded, setExpanded] = useState(null);
180
+ const bottomRef = useRef(null);
181
+ const prevCount = useRef(parts.length);
182
+ useEffect(() => {
183
+ if (parts.length !== prevCount.current) {
184
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
185
+ prevCount.current = parts.length;
186
+ }
187
+ }, [parts.length]);
188
+ if (parts.length === 0) {
189
+ return /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground p-3", children: "No messages yet." });
190
+ }
191
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
192
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 px-3 pt-2 pb-1 text-[10px] text-muted-foreground/80 font-mono border-b border-border/40", children: [
193
+ messages.length,
194
+ " msg \xB7 ",
195
+ parts.length,
196
+ " parts"
197
+ ] }),
198
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto", children: [
199
+ parts.map((fp) => {
200
+ const key = `${fp.msgId}:${fp.partIndex}`;
201
+ const open = expanded === key;
202
+ return /* @__PURE__ */ jsxs("div", { className: "border-b border-border/30 last:border-0", children: [
203
+ /* @__PURE__ */ jsxs(
204
+ Button,
205
+ {
206
+ type: "button",
207
+ variant: "ghost",
208
+ onClick: () => setExpanded(open ? null : key),
209
+ className: "h-auto w-full justify-start gap-1.5 rounded-none px-2 py-1.5 text-left",
210
+ children: [
211
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 shrink-0 text-muted-foreground/40", children: open ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { className: "h-3 w-3" }) }),
212
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] shrink-0 w-20", children: fp.time ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: fp.time }) : /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/30", children: [
213
+ "m",
214
+ fp.msgIndex
215
+ ] }) }),
216
+ /* @__PURE__ */ jsx("span", { className: cn(
217
+ "shrink-0 font-mono text-[10px] w-14",
218
+ fp.role === "user" ? "text-accent" : "text-muted-foreground"
219
+ ), children: fp.role }),
220
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-mono text-[10px] text-muted-foreground/60 w-24", children: fp.partType }),
221
+ /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 font-mono text-[11px] text-foreground truncate", children: partSummary(fp.part) })
222
+ ]
223
+ }
224
+ ),
225
+ open && /* @__PURE__ */ jsx("pre", { className: "px-3 pb-2 font-mono text-[10px] leading-relaxed text-foreground/80 whitespace-pre-wrap break-words bg-muted/20", children: JSON.stringify(fp.part, null, 2) })
226
+ ] }, key);
227
+ }),
228
+ /* @__PURE__ */ jsx("div", { ref: bottomRef })
229
+ ] })
230
+ ] });
231
+ }
232
+ var TABS = [
233
+ { id: "session", label: "Session" },
234
+ { id: "prompt", label: "System prompt" },
235
+ { id: "messages", label: "Messages" }
236
+ ];
237
+ var MIN_WIDTH = 280;
238
+ var MAX_WIDTH = 800;
239
+ function DebugDrawer({ sessionId, messages, requestHeaders, width, onWidthChange }) {
240
+ const [tab, setTab] = useState("session");
241
+ const onDragStart = useCallback((e) => {
242
+ e.preventDefault();
243
+ const startX = e.clientX;
244
+ const startW = width;
245
+ const onMove = (ev) => {
246
+ const delta = startX - ev.clientX;
247
+ onWidthChange(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startW + delta)));
248
+ };
249
+ const onUp = () => {
250
+ window.removeEventListener("mousemove", onMove);
251
+ window.removeEventListener("mouseup", onUp);
252
+ };
253
+ window.addEventListener("mousemove", onMove);
254
+ window.addEventListener("mouseup", onUp);
255
+ }, [width, onWidthChange]);
256
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
257
+ /* @__PURE__ */ jsx(
258
+ "div",
259
+ {
260
+ onMouseDown: onDragStart,
261
+ className: "w-1 shrink-0 cursor-col-resize hover:bg-accent/40 active:bg-accent/60 transition-colors",
262
+ "aria-hidden": true
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsx(
266
+ "aside",
267
+ {
268
+ style: { width },
269
+ className: cn(
270
+ "flex h-full shrink-0 flex-col border-l border-border/60",
271
+ "bg-[oklch(from_var(--background)_calc(l-0.01)_c_h)]"
272
+ ),
273
+ children: /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (next) => setTab(next), className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
274
+ /* @__PURE__ */ jsx("header", { className: "flex shrink-0 items-center gap-0 border-b border-border/60 px-1", children: /* @__PURE__ */ jsx(TabsList, { variant: "line", className: "h-auto gap-0 p-0 w-full", children: TABS.map(({ id, label }) => /* @__PURE__ */ jsx(
275
+ TabsTrigger,
276
+ {
277
+ value: id,
278
+ className: "h-8 flex-none px-3 py-2 text-[11px] font-medium data-[state=active]:after:bg-[color:var(--accent)]",
279
+ children: label
280
+ },
281
+ id
282
+ )) }) }),
283
+ /* @__PURE__ */ jsx(TabsContent, { value: "session", forceMount: true, className: "flex flex-col flex-1 min-h-0 overflow-hidden data-[state=inactive]:hidden", children: /* @__PURE__ */ jsx(SessionTab, { sessionId }) }),
284
+ /* @__PURE__ */ jsx(TabsContent, { value: "prompt", forceMount: true, className: "flex flex-col flex-1 min-h-0 overflow-hidden data-[state=inactive]:hidden", children: /* @__PURE__ */ jsx(SystemPromptTab, { sessionId, requestHeaders }) }),
285
+ /* @__PURE__ */ jsx(TabsContent, { value: "messages", forceMount: true, className: "flex flex-col flex-1 min-h-0 overflow-hidden data-[state=inactive]:hidden", children: /* @__PURE__ */ jsx(MessagesTab, { messages }) })
286
+ ] })
287
+ }
288
+ )
289
+ ] });
290
+ }
291
+
292
+ export {
293
+ cn,
294
+ DebugDrawer
295
+ };