pi-web 0.4.1 → 0.4.3

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.
@@ -1,24 +1,24 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn } from 'node:child_process';
2
2
  export class RpcSession {
3
3
  proc;
4
- buffer = "";
4
+ buffer = '';
5
5
  killed = false;
6
6
  opts;
7
7
  constructor(opts) {
8
8
  this.opts = opts;
9
9
  const parts = opts.piCmd.split(/\s+/);
10
10
  const cmd = parts[0];
11
- const args = [...parts.slice(1), "--mode", "rpc"];
11
+ const args = [...parts.slice(1), '--mode', 'rpc'];
12
12
  this.proc = spawn(cmd, args, {
13
13
  cwd: opts.cwd,
14
- stdio: ["pipe", "pipe", "pipe"],
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
15
  env: { ...process.env },
16
16
  });
17
- this.proc.stdout.setEncoding("utf-8");
18
- this.proc.stderr.setEncoding("utf-8");
19
- this.proc.stdout.on("data", (chunk) => {
17
+ this.proc.stdout.setEncoding('utf-8');
18
+ this.proc.stderr.setEncoding('utf-8');
19
+ this.proc.stdout.on('data', (chunk) => {
20
20
  this.buffer += chunk;
21
- let idx = this.buffer.indexOf("\n");
21
+ let idx = this.buffer.indexOf('\n');
22
22
  while (idx >= 0) {
23
23
  const line = this.buffer.slice(0, idx).trim();
24
24
  this.buffer = this.buffer.slice(idx + 1);
@@ -28,35 +28,37 @@ export class RpcSession {
28
28
  }
29
29
  catch { }
30
30
  }
31
- idx = this.buffer.indexOf("\n");
31
+ idx = this.buffer.indexOf('\n');
32
32
  }
33
33
  });
34
- this.proc.stderr.on("data", (chunk) => {
34
+ this.proc.stderr.on('data', (chunk) => {
35
35
  const msg = chunk.trim();
36
36
  if (msg)
37
37
  this.opts.onError(msg);
38
38
  });
39
- this.proc.on("error", (err) => this.opts.onError(err.message));
40
- this.proc.on("exit", (code) => this.opts.onExit(code));
39
+ this.proc.on('error', (err) => this.opts.onError(err.message));
40
+ this.proc.on('exit', (code) => this.opts.onExit(code));
41
41
  if (opts.sessionFile) {
42
- this.send({ type: "switch_session", sessionPath: opts.sessionFile });
42
+ this.send({ type: 'switch_session', sessionPath: opts.sessionFile });
43
43
  }
44
44
  }
45
45
  send(command) {
46
46
  if (this.killed || !this.proc.stdin.writable)
47
47
  return;
48
- this.proc.stdin.write(JSON.stringify(command) + "\n", "utf-8");
48
+ this.proc.stdin.write(JSON.stringify(command) + '\n', 'utf-8');
49
49
  }
50
50
  kill() {
51
51
  if (this.killed)
52
52
  return;
53
53
  this.killed = true;
54
54
  try {
55
- this.proc.kill("SIGTERM");
56
- setTimeout(() => { try {
57
- this.proc.kill("SIGKILL");
58
- }
59
- catch { } }, 2000);
55
+ this.proc.kill('SIGTERM');
56
+ setTimeout(() => {
57
+ try {
58
+ this.proc.kill('SIGKILL');
59
+ }
60
+ catch { }
61
+ }, 2000);
60
62
  }
61
63
  catch { }
62
64
  }
@@ -1,12 +1,12 @@
1
- import { createServer } from "node:http";
2
- import { createReadStream, existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
3
- import { dirname, extname, join, normalize } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { WebSocketServer, WebSocket } from "ws";
6
- import { RpcSession } from "./rpc.js";
7
- import { listSessions, readSessionMessages } from "./sessions.js";
1
+ import { createServer } from 'node:http';
2
+ import { createReadStream, existsSync, readFileSync, statSync, unlinkSync } from 'node:fs';
3
+ import { dirname, extname, join, normalize } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ import { RpcSession } from './rpc.js';
7
+ import { listSessions, readSessionMessages, getSessionFilePath } from './sessions.js';
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
- if (process.argv.includes("--help") || process.argv.includes("-h")) {
9
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
10
10
  console.log(`
11
11
  pi-web - Web UI for the pi coding agent
12
12
 
@@ -25,118 +25,123 @@ function getArg(name) {
25
25
  if (pair)
26
26
  return pair.slice(flag.length);
27
27
  const idx = process.argv.indexOf(`--${name}`);
28
- if (idx !== -1 && process.argv[idx + 1] && !process.argv[idx + 1].startsWith("--"))
28
+ if (idx !== -1 && process.argv[idx + 1] && !process.argv[idx + 1].startsWith('--'))
29
29
  return process.argv[idx + 1];
30
30
  return undefined;
31
31
  }
32
- const PORT = parseInt(getArg("port") || process.env.PORT || "8192", 10);
33
- const HOST = getArg("host") || process.env.HOST || "127.0.0.1";
34
- const PI_CMD = process.env.PI_CMD || "npx -y @mariozechner/pi-coding-agent@latest";
35
- const isDev = process.argv.includes("--watch") || process.env.NODE_ENV === "development";
36
- const distDirCandidates = [join(__dirname, "dist"), join(__dirname, "..", "..", "dist")];
37
- const distDir = distDirCandidates.find((candidate) => existsSync(join(candidate, "index.html"))) ?? distDirCandidates[0];
38
- const htmlPath = join(distDir, "index.html");
39
- const htmlCache = isDev || !existsSync(htmlPath) ? null : readFileSync(htmlPath, "utf-8");
32
+ const PORT = parseInt(getArg('port') || process.env.PORT || '8192', 10);
33
+ const HOST = getArg('host') || process.env.HOST || '127.0.0.1';
34
+ const PI_CMD = process.env.PI_CMD || 'npx -y @mariozechner/pi-coding-agent@latest';
35
+ const isDev = process.argv.includes('--watch') || process.env.NODE_ENV === 'development';
36
+ const distDirCandidates = [join(__dirname, 'dist'), join(__dirname, '..', '..', 'dist')];
37
+ const distDir = distDirCandidates.find((candidate) => existsSync(join(candidate, 'index.html'))) ??
38
+ distDirCandidates[0];
39
+ const htmlPath = join(distDir, 'index.html');
40
+ const htmlCache = isDev || !existsSync(htmlPath) ? null : readFileSync(htmlPath, 'utf-8');
40
41
  const contentTypes = {
41
- ".html": "text/html",
42
- ".js": "text/javascript",
43
- ".mjs": "text/javascript",
44
- ".css": "text/css",
45
- ".json": "application/json",
46
- ".svg": "image/svg+xml",
47
- ".png": "image/png",
48
- ".jpg": "image/jpeg",
49
- ".jpeg": "image/jpeg",
50
- ".ico": "image/x-icon",
42
+ '.html': 'text/html',
43
+ '.js': 'text/javascript',
44
+ '.mjs': 'text/javascript',
45
+ '.css': 'text/css',
46
+ '.json': 'application/json',
47
+ '.svg': 'image/svg+xml',
48
+ '.png': 'image/png',
49
+ '.jpg': 'image/jpeg',
50
+ '.jpeg': 'image/jpeg',
51
+ '.ico': 'image/x-icon',
51
52
  };
52
53
  function serveFile(filePath, res) {
53
54
  const ext = extname(filePath).toLowerCase();
54
- res.writeHead(200, { "Content-Type": contentTypes[ext] || "application/octet-stream" });
55
+ res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
55
56
  createReadStream(filePath).pipe(res);
56
57
  }
57
58
  const server = createServer((req, res) => {
58
- if (req.url === "/" || req.url === "/index.html") {
59
+ if (req.url === '/' || req.url === '/index.html') {
59
60
  if (!existsSync(htmlPath)) {
60
- res.writeHead(503, { "Content-Type": "text/plain" });
61
- res.end("frontend not built. run: npm run build");
61
+ res.writeHead(503, { 'Content-Type': 'text/plain' });
62
+ res.end('frontend not built. run: npm run build');
62
63
  return;
63
64
  }
64
- res.writeHead(200, { "Content-Type": "text/html" });
65
- res.end(htmlCache ?? readFileSync(htmlPath, "utf-8"));
65
+ res.writeHead(200, { 'Content-Type': 'text/html' });
66
+ res.end(htmlCache ?? readFileSync(htmlPath, 'utf-8'));
66
67
  return;
67
68
  }
68
- if (req.url === "/api/sessions") {
69
+ if (req.url === '/api/sessions') {
69
70
  const url = new URL(req.url, `http://${req.headers.host}`);
70
- const cwd = url.searchParams.get("cwd") || undefined;
71
+ const cwd = url.searchParams.get('cwd') || undefined;
71
72
  listSessions({ cwd, limit: 50 })
72
73
  .then((data) => {
73
- res.writeHead(200, { "Content-Type": "application/json" });
74
+ res.writeHead(200, { 'Content-Type': 'application/json' });
74
75
  res.end(JSON.stringify(data));
75
76
  })
76
77
  .catch((err) => {
77
- res.writeHead(500, { "Content-Type": "application/json" });
78
+ res.writeHead(500, { 'Content-Type': 'application/json' });
78
79
  res.end(JSON.stringify({ error: String(err) }));
79
80
  });
80
81
  return;
81
82
  }
82
- if (req.url?.startsWith("/api/session?")) {
83
+ if (req.url?.startsWith('/api/session?')) {
83
84
  const url = new URL(req.url, `http://${req.headers.host}`);
84
- const file = url.searchParams.get("file");
85
- if (!file) {
86
- res.writeHead(400, { "Content-Type": "application/json" });
87
- res.end(JSON.stringify({ error: "file parameter required" }));
85
+ const cwd = url.searchParams.get('cwd');
86
+ const filename = url.searchParams.get('filename');
87
+ if (!cwd || !filename) {
88
+ res.writeHead(400, { 'Content-Type': 'application/json' });
89
+ res.end(JSON.stringify({ error: 'cwd and filename parameters required' }));
88
90
  return;
89
91
  }
90
- if (req.method === "DELETE") {
92
+ const file = getSessionFilePath(cwd, filename);
93
+ if (req.method === 'DELETE') {
91
94
  try {
92
95
  unlinkSync(file);
93
- res.writeHead(200, { "Content-Type": "application/json" });
96
+ res.writeHead(200, { 'Content-Type': 'application/json' });
94
97
  res.end(JSON.stringify({ ok: true }));
95
98
  }
96
99
  catch (err) {
97
- res.writeHead(500, { "Content-Type": "application/json" });
100
+ res.writeHead(500, { 'Content-Type': 'application/json' });
98
101
  res.end(JSON.stringify({ error: String(err) }));
99
102
  }
100
103
  return;
101
104
  }
102
105
  readSessionMessages(file)
103
106
  .then((messages) => {
104
- res.writeHead(200, { "Content-Type": "application/json" });
107
+ res.writeHead(200, { 'Content-Type': 'application/json' });
105
108
  res.end(JSON.stringify(messages));
106
109
  })
107
110
  .catch((err) => {
108
- res.writeHead(500, { "Content-Type": "application/json" });
111
+ res.writeHead(500, { 'Content-Type': 'application/json' });
109
112
  res.end(JSON.stringify({ error: String(err) }));
110
113
  });
111
114
  return;
112
115
  }
113
116
  if (req.url) {
114
117
  const url = new URL(req.url, `http://${req.headers.host}`);
115
- const safePath = normalize(url.pathname).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
118
+ const safePath = normalize(url.pathname)
119
+ .replace(/^(\.\.[/\\])+/, '')
120
+ .replace(/^[/\\]+/, '');
116
121
  const filePath = join(distDir, safePath);
117
122
  if (filePath.startsWith(distDir) && existsSync(filePath) && statSync(filePath).isFile()) {
118
123
  serveFile(filePath, res);
119
124
  return;
120
125
  }
121
- const acceptsHtml = (req.headers.accept ?? "").includes("text/html");
122
- if (req.method === "GET" && !url.pathname.startsWith("/api/") && acceptsHtml) {
126
+ const acceptsHtml = (req.headers.accept ?? '').includes('text/html');
127
+ if (req.method === 'GET' && !url.pathname.startsWith('/api/') && acceptsHtml) {
123
128
  if (!existsSync(htmlPath)) {
124
- res.writeHead(503, { "Content-Type": "text/plain" });
125
- res.end("frontend not built. run: npm run build");
129
+ res.writeHead(503, { 'Content-Type': 'text/plain' });
130
+ res.end('frontend not built. run: npm run build');
126
131
  return;
127
132
  }
128
- res.writeHead(200, { "Content-Type": "text/html" });
129
- res.end(htmlCache ?? readFileSync(htmlPath, "utf-8"));
133
+ res.writeHead(200, { 'Content-Type': 'text/html' });
134
+ res.end(htmlCache ?? readFileSync(htmlPath, 'utf-8'));
130
135
  return;
131
136
  }
132
137
  }
133
- res.writeHead(404, { "Content-Type": "text/plain" });
134
- res.end("not found");
138
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
139
+ res.end('not found');
135
140
  });
136
141
  const wss = new WebSocketServer({ server });
137
142
  const rpcSessions = new Map();
138
- wss.on("connection", (ws) => {
139
- ws.on("message", (raw) => {
143
+ wss.on('connection', (ws) => {
144
+ ws.on('message', (raw) => {
140
145
  let msg;
141
146
  try {
142
147
  msg = JSON.parse(String(raw));
@@ -144,43 +149,45 @@ wss.on("connection", (ws) => {
144
149
  catch {
145
150
  return;
146
151
  }
147
- if (msg.type === "start_session") {
152
+ if (msg.type === 'start_session') {
148
153
  rpcSessions.get(ws)?.kill();
149
154
  const rpc = new RpcSession({
150
155
  piCmd: PI_CMD,
151
- cwd: msg.cwd || process.env.HOME || "/",
152
- sessionFile: msg.sessionFile || undefined,
156
+ cwd: msg.cwd || process.env.HOME || '/',
157
+ sessionFile: msg.sessionFile
158
+ ? getSessionFilePath(msg.cwd || process.env.HOME || '/', msg.sessionFile)
159
+ : undefined,
153
160
  onEvent: (event) => {
154
161
  if (ws.readyState === WebSocket.OPEN)
155
- ws.send(JSON.stringify({ type: "rpc_event", event }));
162
+ ws.send(JSON.stringify({ type: 'rpc_event', event }));
156
163
  },
157
164
  onError: (error) => {
158
165
  if (ws.readyState === WebSocket.OPEN)
159
- ws.send(JSON.stringify({ type: "error", message: error }));
166
+ ws.send(JSON.stringify({ type: 'error', message: error }));
160
167
  },
161
168
  onExit: (code) => {
162
169
  rpcSessions.delete(ws);
163
170
  if (ws.readyState === WebSocket.OPEN)
164
- ws.send(JSON.stringify({ type: "session_ended", code }));
171
+ ws.send(JSON.stringify({ type: 'session_ended', code }));
165
172
  },
166
173
  });
167
174
  rpcSessions.set(ws, rpc);
168
175
  return;
169
176
  }
170
- if (msg.type === "rpc_command") {
177
+ if (msg.type === 'rpc_command') {
171
178
  const rpc = rpcSessions.get(ws);
172
179
  if (!rpc) {
173
- ws.send(JSON.stringify({ type: "error", message: "no active session" }));
180
+ ws.send(JSON.stringify({ type: 'error', message: 'no active session' }));
174
181
  return;
175
182
  }
176
183
  rpc.send(msg.command);
177
184
  return;
178
185
  }
179
- if (msg.type === "ping") {
180
- ws.send(JSON.stringify({ type: "pong" }));
186
+ if (msg.type === 'ping') {
187
+ ws.send(JSON.stringify({ type: 'pong' }));
181
188
  }
182
189
  });
183
- ws.on("close", () => {
190
+ ws.on('close', () => {
184
191
  rpcSessions.get(ws)?.kill();
185
192
  rpcSessions.delete(ws);
186
193
  });
@@ -1,11 +1,14 @@
1
- import { readdir } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- import { createReadStream } from "node:fs";
5
- import { createInterface } from "node:readline";
6
- const SESSION_DIR = join(homedir(), ".pi", "agent", "sessions");
1
+ import { readdir } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { createReadStream } from 'node:fs';
5
+ import { createInterface } from 'node:readline';
6
+ const SESSION_DIR = join(homedir(), '.pi', 'agent', 'sessions');
7
7
  function cwdToSessionDir(cwd) {
8
- return "-" + cwd.replace(/\//g, "-") + "-";
8
+ return '-' + cwd.replace(/\//g, '-') + '--';
9
+ }
10
+ export function getSessionFilePath(cwd, filename) {
11
+ return join(SESSION_DIR, cwdToSessionDir(cwd), filename);
9
12
  }
10
13
  export async function listSessions(opts) {
11
14
  const { cwd, limit = 30 } = opts;
@@ -19,7 +22,7 @@ export async function listSessions(opts) {
19
22
  const dirPath = join(SESSION_DIR, dir.name);
20
23
  let files;
21
24
  try {
22
- files = (await readdir(dirPath)).filter((f) => f.endsWith(".jsonl"));
25
+ files = (await readdir(dirPath)).filter((f) => f.endsWith('.jsonl'));
23
26
  }
24
27
  catch {
25
28
  continue;
@@ -45,7 +48,7 @@ export async function listSessions(opts) {
45
48
  return results.slice(0, limit);
46
49
  }
47
50
  export async function readSessionMessages(filePath) {
48
- const stream = createReadStream(filePath, { encoding: "utf8" });
51
+ const stream = createReadStream(filePath, { encoding: 'utf8' });
49
52
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
50
53
  const messages = [];
51
54
  const toolResults = new Map();
@@ -56,20 +59,20 @@ export async function readSessionMessages(filePath) {
56
59
  continue;
57
60
  try {
58
61
  const entry = JSON.parse(trimmed);
59
- if (entry.type !== "message" || !entry.message)
62
+ if (entry.type !== 'message' || !entry.message)
60
63
  continue;
61
64
  const msg = entry.message;
62
65
  const role = msg.role;
63
- if (!role || role === "system")
66
+ if (!role || role === 'system')
64
67
  continue;
65
- if (role === "toolResult" || role === "tool_result") {
68
+ if (role === 'toolResult' || role === 'tool_result') {
66
69
  const id = msg.toolCallId;
67
- const text = msg.content?.[0]?.text ?? "";
70
+ const text = msg.content?.[0]?.text ?? '';
68
71
  if (id)
69
72
  toolResults.set(id, text);
70
73
  continue;
71
74
  }
72
- if (role === "tool")
75
+ if (role === 'tool')
73
76
  continue;
74
77
  messages.push({
75
78
  id: entry.id || crypto.randomUUID(),
@@ -92,7 +95,7 @@ export async function readSessionMessages(filePath) {
92
95
  if (!Array.isArray(msg.content))
93
96
  continue;
94
97
  for (const block of msg.content) {
95
- if (block.type === "toolCall" && block.id && toolResults.has(block.id)) {
98
+ if (block.type === 'toolCall' && block.id && toolResults.has(block.id)) {
96
99
  block.result = toolResults.get(block.id);
97
100
  }
98
101
  }
@@ -100,7 +103,7 @@ export async function readSessionMessages(filePath) {
100
103
  return messages;
101
104
  }
102
105
  async function readSessionHeader(filePath) {
103
- const stream = createReadStream(filePath, { encoding: "utf8" });
106
+ const stream = createReadStream(filePath, { encoding: 'utf8' });
104
107
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
105
108
  let header = null;
106
109
  let firstPrompt;
@@ -113,7 +116,7 @@ async function readSessionHeader(filePath) {
113
116
  if (!header) {
114
117
  try {
115
118
  const parsed = JSON.parse(trimmed);
116
- if (parsed.type === "session") {
119
+ if (parsed.type === 'session') {
117
120
  header = parsed;
118
121
  continue;
119
122
  }
@@ -125,13 +128,13 @@ async function readSessionHeader(filePath) {
125
128
  if (!firstPrompt && trimmed.includes('"role":"user"')) {
126
129
  try {
127
130
  const msg = JSON.parse(trimmed);
128
- if (msg.message?.role === "user") {
131
+ if (msg.message?.role === 'user') {
129
132
  const content = msg.message.content;
130
- if (typeof content === "string") {
133
+ if (typeof content === 'string') {
131
134
  firstPrompt = content.slice(0, 120);
132
135
  }
133
136
  else if (Array.isArray(content)) {
134
- const text = content.find((c) => c.type === "text");
137
+ const text = content.find((c) => c.type === 'text');
135
138
  if (text?.text)
136
139
  firstPrompt = text.text.slice(0, 120);
137
140
  }
@@ -150,9 +153,9 @@ async function readSessionHeader(filePath) {
150
153
  return null;
151
154
  return {
152
155
  id: header.id,
153
- file: filePath,
154
- cwd: header.cwd || "",
155
- timestamp: header.timestamp || "",
156
+ file: basename(filePath),
157
+ cwd: header.cwd || '',
158
+ timestamp: header.timestamp || '',
156
159
  firstPrompt,
157
160
  messageCount,
158
161
  };
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap";@layer components;@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-semibold:600;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-pi-accent:#5a8080;--color-pi-border-muted:#b0b0b0;--color-pi-success:#588458;--color-pi-error:#a55;--color-pi-warning:#9a7326;--color-pi-muted:#6c6c6c;--color-pi-dim:#767676;--color-pi-user-bg:#e8e8e8;--color-pi-tool-pending:#e8e8f0;--color-pi-tool-success:#e8f0e8;--color-pi-tool-error:#f0e8e8;--color-pi-md-heading:#9a7326;--color-pi-md-link:#547da7;--color-pi-md-code:#5a8080;--color-pi-md-code-block:#588458;--color-pi-tool-output:#6c6c6c;--color-pi-page-bg:#f8f8f8;--color-pi-card-bg:#fff}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.relative{position:relative}.start{inset-inline-start:var(--spacing)}.-top-1\.5{top:calc(var(--spacing) * -1.5)}.top-3{top:calc(var(--spacing) * 3)}.-right-1\.5{right:calc(var(--spacing) * -1.5)}.right-3{right:calc(var(--spacing) * 3)}.mx-auto{margin-inline:auto}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline-flex{display:inline-flex}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-10{height:calc(var(--spacing) * 10)}.h-\[42px\]{height:42px}.h-full{height:100%}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-\[200px\]{max-height:200px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-\[42px\]{min-height:42px}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-10{width:calc(var(--spacing) * 10)}.w-\[42px\]{width:42px}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.rotate-180{rotate:180deg}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-pi-border-muted{border-color:var(--color-pi-border-muted)}.border-pi-border-muted\/40{border-color:#b0b0b066}@supports (color:color-mix(in lab,red,red)){.border-pi-border-muted\/40{border-color:color-mix(in oklab,var(--color-pi-border-muted) 40%,transparent)}}.border-pi-error{border-color:var(--color-pi-error)}.border-pi-success{border-color:var(--color-pi-success)}.border-pi-warning{border-color:var(--color-pi-warning)}.bg-pi-accent{background-color:var(--color-pi-accent)}.bg-pi-card-bg{background-color:var(--color-pi-card-bg)}.bg-pi-error{background-color:var(--color-pi-error)}.bg-pi-page-bg{background-color:var(--color-pi-page-bg)}.bg-pi-tool-error{background-color:var(--color-pi-tool-error)}.bg-pi-tool-pending{background-color:var(--color-pi-tool-pending)}.bg-pi-tool-success{background-color:var(--color-pi-tool-success)}.bg-pi-user-bg{background-color:var(--color-pi-user-bg)}.bg-pi-warning{background-color:var(--color-pi-warning)}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.\!text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-pi-accent{color:var(--color-pi-accent)}.text-pi-dim{color:var(--color-pi-dim)}.text-pi-error{color:var(--color-pi-error)}.text-pi-md-code{color:var(--color-pi-md-code)}.text-pi-md-code-block{color:var(--color-pi-md-code-block)}.text-pi-md-heading{color:var(--color-pi-md-heading)}.text-pi-md-link{color:var(--color-pi-md-link)}.text-pi-muted{color:var(--color-pi-muted)}.text-pi-success{color:var(--color-pi-success)}.text-pi-tool-output{color:var(--color-pi-tool-output)}.text-pi-warning{color:var(--color-pi-warning)}.text-white{color:var(--color-white)}.italic{font-style:italic}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.outline-none{--tw-outline-style:none;outline-style:none}@media(hover:hover){.group-hover\:inline-flex:is(:where(.group):hover *){display:inline-flex}.hover\:bg-pi-tool-error:hover{background-color:var(--color-pi-tool-error)}.hover\:bg-pi-user-bg:hover{background-color:var(--color-pi-user-bg)}.hover\:text-pi-accent:hover{color:var(--color-pi-accent)}.hover\:text-pi-error:hover{color:var(--color-pi-error)}.hover\:opacity-85:hover{opacity:.85}.hover\:opacity-90:hover{opacity:.9}.hover\:brightness-95:hover{--tw-brightness:brightness(95%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus\:border-pi-accent:focus{border-color:var(--color-pi-accent)}.disabled\:cursor-default:disabled{cursor:default}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:48rem){.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:\!text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}}html,body,#root{height:100dvh;overflow:hidden}*,:before,:after{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important;font-size:14px!important}@supports (-webkit-touch-callout:none){.prompt-input{font-size:16px!important}}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.4}}.animate-pulse-dot{animation:1s infinite pulse-dot}.select-fit-content{field-sizing:content}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}