@poncho-ai/cli 0.14.1 → 0.16.0

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.
@@ -5,7 +5,7 @@ import { existsSync, watch as fsWatch } from "fs";
5
5
  import {
6
6
  createServer
7
7
  } from "http";
8
- import { basename as basename2, dirname as dirname3, normalize, relative, resolve as resolve3 } from "path";
8
+ import { basename as basename2, dirname as dirname4, normalize, relative, resolve as resolve3 } from "path";
9
9
  import { createRequire as createRequire2 } from "module";
10
10
  import { fileURLToPath } from "url";
11
11
  import {
@@ -33,243 +33,12 @@ import dotenv from "dotenv";
33
33
  import YAML from "yaml";
34
34
 
35
35
  // src/web-ui.ts
36
- import { createHash, randomUUID, timingSafeEqual } from "crypto";
37
- import { mkdir, readFile, writeFile } from "fs/promises";
38
36
  import { readFileSync } from "fs";
39
- import { basename, dirname, resolve, join } from "path";
40
- import { homedir } from "os";
37
+ import { dirname as dirname2, join } from "path";
41
38
  import { createRequire } from "module";
42
- var require2 = createRequire(import.meta.url);
43
- var markedPackagePath = require2.resolve("marked");
44
- var markedDir = dirname(markedPackagePath);
45
- var markedSource = readFileSync(join(markedDir, "marked.umd.js"), "utf-8");
46
- var DEFAULT_OWNER = "local-owner";
47
- var SessionStore = class {
48
- sessions = /* @__PURE__ */ new Map();
49
- ttlMs;
50
- constructor(ttlMs = 1e3 * 60 * 60 * 8) {
51
- this.ttlMs = ttlMs;
52
- }
53
- create(ownerId = DEFAULT_OWNER) {
54
- const now = Date.now();
55
- const session = {
56
- sessionId: randomUUID(),
57
- ownerId,
58
- csrfToken: randomUUID(),
59
- createdAt: now,
60
- expiresAt: now + this.ttlMs,
61
- lastSeenAt: now
62
- };
63
- this.sessions.set(session.sessionId, session);
64
- return session;
65
- }
66
- get(sessionId) {
67
- const session = this.sessions.get(sessionId);
68
- if (!session) {
69
- return void 0;
70
- }
71
- if (Date.now() > session.expiresAt) {
72
- this.sessions.delete(sessionId);
73
- return void 0;
74
- }
75
- session.lastSeenAt = Date.now();
76
- return session;
77
- }
78
- delete(sessionId) {
79
- this.sessions.delete(sessionId);
80
- }
81
- };
82
- var LoginRateLimiter = class {
83
- constructor(maxAttempts = 5, windowMs = 1e3 * 60 * 5, lockoutMs = 1e3 * 60 * 10) {
84
- this.maxAttempts = maxAttempts;
85
- this.windowMs = windowMs;
86
- this.lockoutMs = lockoutMs;
87
- }
88
- attempts = /* @__PURE__ */ new Map();
89
- canAttempt(key) {
90
- const current = this.attempts.get(key);
91
- if (!current) {
92
- return { allowed: true };
93
- }
94
- if (current.lockedUntil && Date.now() < current.lockedUntil) {
95
- return {
96
- allowed: false,
97
- retryAfterSeconds: Math.ceil((current.lockedUntil - Date.now()) / 1e3)
98
- };
99
- }
100
- return { allowed: true };
101
- }
102
- registerFailure(key) {
103
- const now = Date.now();
104
- const current = this.attempts.get(key);
105
- if (!current || now - current.firstFailureAt > this.windowMs) {
106
- this.attempts.set(key, { count: 1, firstFailureAt: now });
107
- return { locked: false };
108
- }
109
- const count = current.count + 1;
110
- const next = {
111
- ...current,
112
- count
113
- };
114
- if (count >= this.maxAttempts) {
115
- next.lockedUntil = now + this.lockoutMs;
116
- this.attempts.set(key, next);
117
- return { locked: true, retryAfterSeconds: Math.ceil(this.lockoutMs / 1e3) };
118
- }
119
- this.attempts.set(key, next);
120
- return { locked: false };
121
- }
122
- registerSuccess(key) {
123
- this.attempts.delete(key);
124
- }
125
- };
126
- var parseCookies = (request) => {
127
- const cookieHeader = request.headers.cookie ?? "";
128
- const pairs = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean);
129
- const cookies = {};
130
- for (const pair of pairs) {
131
- const index = pair.indexOf("=");
132
- if (index <= 0) {
133
- continue;
134
- }
135
- const key = pair.slice(0, index);
136
- const value = pair.slice(index + 1);
137
- try {
138
- cookies[key] = decodeURIComponent(value);
139
- } catch {
140
- cookies[key] = value;
141
- }
142
- }
143
- return cookies;
144
- };
145
- var setCookie = (response, name, value, options) => {
146
- const segments = [`${name}=${encodeURIComponent(value)}`];
147
- segments.push(`Path=${options.path ?? "/"}`);
148
- if (typeof options.maxAge === "number") {
149
- segments.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
150
- }
151
- if (options.httpOnly) {
152
- segments.push("HttpOnly");
153
- }
154
- if (options.secure) {
155
- segments.push("Secure");
156
- }
157
- if (options.sameSite) {
158
- segments.push(`SameSite=${options.sameSite}`);
159
- }
160
- const previous = response.getHeader("Set-Cookie");
161
- const serialized = segments.join("; ");
162
- if (!previous) {
163
- response.setHeader("Set-Cookie", serialized);
164
- return;
165
- }
166
- if (Array.isArray(previous)) {
167
- response.setHeader("Set-Cookie", [...previous, serialized]);
168
- return;
169
- }
170
- response.setHeader("Set-Cookie", [String(previous), serialized]);
171
- };
172
- var verifyPassphrase = (provided, expected) => {
173
- const providedBuffer = Buffer.from(provided);
174
- const expectedBuffer = Buffer.from(expected);
175
- if (providedBuffer.length !== expectedBuffer.length) {
176
- const zero = Buffer.alloc(expectedBuffer.length);
177
- return timingSafeEqual(expectedBuffer, zero) && false;
178
- }
179
- return timingSafeEqual(providedBuffer, expectedBuffer);
180
- };
181
- var getRequestIp = (request) => {
182
- return request.socket.remoteAddress ?? "unknown";
183
- };
184
- var inferConversationTitle = (text) => {
185
- const normalized = text.trim().replace(/\s+/g, " ");
186
- if (!normalized) {
187
- return "New conversation";
188
- }
189
- return normalized.length <= 48 ? normalized : `${normalized.slice(0, 48)}...`;
190
- };
191
- var renderManifest = (options) => {
192
- const name = options?.agentName ?? "Agent";
193
- return JSON.stringify({
194
- name,
195
- short_name: name,
196
- description: `${name} \u2014 AI agent powered by Poncho`,
197
- start_url: "/",
198
- display: "standalone",
199
- background_color: "#000000",
200
- theme_color: "#000000",
201
- icons: [
202
- { src: "/icon.svg", sizes: "any", type: "image/svg+xml" },
203
- { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
204
- { src: "/icon-512.png", sizes: "512x512", type: "image/png" }
205
- ]
206
- });
207
- };
208
- var renderIconSvg = (options) => {
209
- const letter = (options?.agentName ?? "A").charAt(0).toUpperCase();
210
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
211
- <rect width="512" height="512" rx="96" fill="#000"/>
212
- <text x="256" y="256" dy=".35em" text-anchor="middle"
213
- font-family="-apple-system,BlinkMacSystemFont,sans-serif"
214
- font-size="280" font-weight="700" fill="#fff">${letter}</text>
215
- </svg>`;
216
- };
217
- var renderServiceWorker = () => `
218
- const CACHE_NAME = "poncho-shell-v1";
219
- const SHELL_URLS = ["/"];
220
39
 
221
- self.addEventListener("install", (event) => {
222
- event.waitUntil(
223
- caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
224
- );
225
- self.skipWaiting();
226
- });
227
-
228
- self.addEventListener("activate", (event) => {
229
- event.waitUntil(
230
- caches.keys().then((keys) =>
231
- Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
232
- )
233
- );
234
- self.clients.claim();
235
- });
236
-
237
- self.addEventListener("fetch", (event) => {
238
- const url = new URL(event.request.url);
239
- // Only cache GET requests for the app shell; let API calls pass through
240
- if (event.request.method !== "GET" || url.pathname.startsWith("/api/")) {
241
- return;
242
- }
243
- event.respondWith(
244
- fetch(event.request)
245
- .then((response) => {
246
- const clone = response.clone();
247
- caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
248
- return response;
249
- })
250
- .catch(() => caches.match(event.request))
251
- );
252
- });
253
- `;
254
- var renderWebUiHtml = (options) => {
255
- const agentInitial = (options?.agentName ?? "A").charAt(0).toUpperCase();
256
- const agentName = options?.agentName ?? "Agent";
257
- return `<!doctype html>
258
- <html lang="en">
259
- <head>
260
- <meta charset="utf-8">
261
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
262
- <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
263
- <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
264
- <meta name="apple-mobile-web-app-capable" content="yes">
265
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
266
- <meta name="apple-mobile-web-app-title" content="${agentName}">
267
- <link rel="manifest" href="/manifest.json">
268
- <link rel="icon" href="/icon.svg" type="image/svg+xml">
269
- <link rel="apple-touch-icon" href="/icon-192.png">
270
- <title>${agentName}</title>
271
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
272
- <style>
40
+ // src/web-ui-styles.ts
41
+ var WEB_UI_STYLES = `
273
42
  :root {
274
43
  color-scheme: light dark;
275
44
 
@@ -675,6 +444,8 @@ var renderWebUiHtml = (options) => {
675
444
 
676
445
  /* Main */
677
446
  .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: var(--bg); overflow: hidden; }
447
+ .main-body { flex: 1; display: flex; min-height: 0; overflow: hidden; }
448
+ .main-chat { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
678
449
  .topbar {
679
450
  height: calc(52px + env(safe-area-inset-top, 0px));
680
451
  padding-top: env(safe-area-inset-top, 0px);
@@ -695,6 +466,22 @@ var renderWebUiHtml = (options) => {
695
466
  white-space: nowrap;
696
467
  letter-spacing: -0.01em;
697
468
  padding: 0 50px;
469
+ cursor: default;
470
+ }
471
+ .topbar-title-input {
472
+ font: inherit;
473
+ font-weight: inherit;
474
+ letter-spacing: inherit;
475
+ color: inherit;
476
+ background: var(--bg-2);
477
+ border: none;
478
+ border-radius: 4px;
479
+ padding: 2px 6px;
480
+ margin: -3px 0;
481
+ max-width: 100%;
482
+ outline: none;
483
+ box-sizing: border-box;
484
+ text-align: center;
698
485
  }
699
486
  .sidebar-toggle {
700
487
  display: none;
@@ -937,6 +724,18 @@ var renderWebUiHtml = (options) => {
937
724
  padding: 4px 7px;
938
725
  color: var(--fg-tool-item);
939
726
  }
727
+ .tool-images {
728
+ padding: 10px 12px 4px;
729
+ display: flex;
730
+ flex-wrap: wrap;
731
+ gap: 8px;
732
+ }
733
+ .tool-screenshot {
734
+ max-width: 100%;
735
+ border-radius: 6px;
736
+ border: 1px solid var(--border-2);
737
+ cursor: pointer;
738
+ }
940
739
  .approval-requests {
941
740
  border-top: 1px solid var(--border-2);
942
741
  padding: 10px 12px 12px;
@@ -1442,13 +1241,133 @@ var renderWebUiHtml = (options) => {
1442
1241
  pointer-events: none;
1443
1242
  will-change: opacity;
1444
1243
  }
1445
- .sidebar-backdrop:not(.dragging) { transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
1446
- .sidebar-backdrop.dragging { transition: none; }
1447
- .shell.sidebar-open .sidebar-backdrop { opacity: 1; pointer-events: auto; }
1448
- .messages { padding: 16px; }
1449
- .composer { padding: 8px 16px 16px; }
1450
- /* Always show delete button on mobile (no hover) */
1451
- .conversation-item .delete-btn { opacity: 1; }
1244
+ .sidebar-backdrop:not(.dragging) { transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
1245
+ .sidebar-backdrop.dragging { transition: none; }
1246
+ .shell.sidebar-open .sidebar-backdrop { opacity: 1; pointer-events: auto; }
1247
+ .messages { padding: 16px; }
1248
+ .composer { padding: 8px 16px 16px; }
1249
+ /* Always show delete button on mobile (no hover) */
1250
+ .conversation-item .delete-btn { opacity: 1; }
1251
+ }
1252
+
1253
+ /* Browser viewport panel */
1254
+ .browser-panel-resize {
1255
+ width: 1px;
1256
+ cursor: col-resize;
1257
+ background: var(--border-1);
1258
+ flex-shrink: 0;
1259
+ position: relative;
1260
+ z-index: 10;
1261
+ }
1262
+ .browser-panel-resize::after {
1263
+ content: "";
1264
+ position: absolute;
1265
+ inset: 0 -3px;
1266
+ }
1267
+ .browser-panel-resize:hover,
1268
+ .browser-panel-resize.dragging {
1269
+ background: var(--fg-5);
1270
+ }
1271
+ .browser-panel {
1272
+ flex: 2 1 0%;
1273
+ min-width: 280px;
1274
+ background: var(--bg);
1275
+ display: flex;
1276
+ flex-direction: column;
1277
+ overflow: hidden;
1278
+ }
1279
+ .main-chat.has-browser {
1280
+ flex: 1 1 0%;
1281
+ min-width: 280px;
1282
+ }
1283
+ .browser-panel-header {
1284
+ display: flex;
1285
+ align-items: center;
1286
+ gap: 8px;
1287
+ padding: 8px 12px;
1288
+ border-bottom: 1px solid var(--border);
1289
+ min-height: 40px;
1290
+ }
1291
+ .browser-panel-title {
1292
+ font-size: 12px;
1293
+ font-weight: 600;
1294
+ text-transform: uppercase;
1295
+ letter-spacing: 0.06em;
1296
+ color: var(--fg-tool);
1297
+ white-space: nowrap;
1298
+ }
1299
+ .browser-nav-btn {
1300
+ background: none;
1301
+ border: none;
1302
+ color: var(--fg-3);
1303
+ cursor: pointer;
1304
+ padding: 4px;
1305
+ border-radius: 4px;
1306
+ display: flex;
1307
+ align-items: center;
1308
+ justify-content: center;
1309
+ transition: color 0.15s, background 0.15s;
1310
+ flex-shrink: 0;
1311
+ }
1312
+ .browser-nav-btn:hover:not(:disabled) { color: var(--fg); background: var(--bg-bubble-user); }
1313
+ .browser-nav-btn:disabled { opacity: 0.3; cursor: default; }
1314
+ .browser-panel-url {
1315
+ flex: 1;
1316
+ font-size: 11px;
1317
+ color: var(--fg-3);
1318
+ overflow: hidden;
1319
+ text-overflow: ellipsis;
1320
+ white-space: nowrap;
1321
+ }
1322
+ .browser-panel-close {
1323
+ background: none;
1324
+ border: none;
1325
+ color: var(--fg-3);
1326
+ font-size: 18px;
1327
+ cursor: pointer;
1328
+ padding: 0 4px;
1329
+ line-height: 1;
1330
+ }
1331
+ .browser-panel-close:hover {
1332
+ color: var(--fg);
1333
+ }
1334
+ .browser-panel-viewport {
1335
+ flex: 1;
1336
+ position: relative;
1337
+ overflow: auto;
1338
+ display: flex;
1339
+ align-items: flex-start;
1340
+ justify-content: center;
1341
+ padding: 8px;
1342
+ }
1343
+ .browser-panel-viewport img {
1344
+ max-width: 100%;
1345
+ border-radius: 4px;
1346
+ display: block;
1347
+ outline: none;
1348
+ }
1349
+ .browser-panel-viewport img:focus {
1350
+ box-shadow: 0 0 0 2px var(--accent);
1351
+ }
1352
+ .browser-panel-placeholder {
1353
+ position: absolute;
1354
+ inset: 0;
1355
+ display: flex;
1356
+ align-items: center;
1357
+ justify-content: center;
1358
+ color: var(--fg-3);
1359
+ font-size: 13px;
1360
+ }
1361
+ @media (max-width: 768px) {
1362
+ .browser-panel {
1363
+ position: fixed;
1364
+ inset: 0;
1365
+ width: 100% !important;
1366
+ flex: none !important;
1367
+ z-index: 200;
1368
+ }
1369
+ .browser-panel-resize { display: none !important; }
1370
+ .main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; }
1452
1371
  }
1453
1372
 
1454
1373
  /* Reduced motion */
@@ -1458,77 +1377,12 @@ var renderWebUiHtml = (options) => {
1458
1377
  transition-duration: 0.01ms !important;
1459
1378
  }
1460
1379
  }
1461
- </style>
1462
- </head>
1463
- <body data-agent-initial="${agentInitial}" data-agent-name="${agentName}">
1464
- <div class="edge-blocker-right"></div>
1465
- <div id="auth" class="auth hidden">
1466
- <form id="login-form" class="auth-card">
1467
- <div class="auth-shell">
1468
- <input id="passphrase" class="auth-input" type="password" placeholder="Passphrase" required autofocus>
1469
- <button class="auth-submit" type="submit">
1470
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 8h8M9 5l3 3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1471
- </button>
1472
- </div>
1473
- <div id="login-error" class="error"></div>
1474
- </form>
1475
- </div>
1476
-
1477
- <div id="app" class="shell hidden">
1478
- <aside class="sidebar">
1479
- <button id="new-chat" class="new-chat-btn">
1480
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
1481
- </button>
1482
- <div id="conversation-list" class="conversation-list"></div>
1483
- <div class="sidebar-footer">
1484
- <button id="logout" class="logout-btn">Log out</button>
1485
- </div>
1486
- </aside>
1487
- <div id="sidebar-backdrop" class="sidebar-backdrop"></div>
1488
- <main class="main">
1489
- <div class="topbar">
1490
- <button id="sidebar-toggle" class="sidebar-toggle">&#9776;</button>
1491
- <div id="chat-title" class="topbar-title"></div>
1492
- <button id="topbar-new-chat" class="topbar-new-chat">
1493
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
1494
- </button>
1495
- <a class="poncho-badge" href="https://github.com/cesr/poncho-ai" target="_blank" rel="noopener noreferrer"><img class="poncho-badge-avatar" src="data:image/jpeg;base64,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" alt="" aria-hidden="true">Built with Poncho</a>
1496
- </div>
1497
- <div id="messages" class="messages">
1498
- <div class="empty-state">
1499
- <div class="assistant-avatar">${agentInitial}</div>
1500
- <div class="empty-state-text">How can I help you today?</div>
1501
- </div>
1502
- </div>
1503
- <form id="composer" class="composer">
1504
- <div class="composer-inner">
1505
- <div id="attachment-preview" class="attachment-preview" style="display:none"></div>
1506
- <div class="composer-shell">
1507
- <button id="attach-btn" class="attach-btn" type="button" title="Attach files">
1508
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
1509
- </button>
1510
- <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1511
- <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1512
- <div class="send-btn-wrapper" id="send-btn-wrapper">
1513
- <svg class="context-ring" viewBox="0 0 36 36">
1514
- <circle class="context-ring-fill" id="context-ring-fill" cx="18" cy="18" r="14.5" />
1515
- </svg>
1516
- <button id="send" class="send-btn" type="submit">
1517
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1518
- </button>
1519
- <div class="context-tooltip" id="context-tooltip"></div>
1520
- </div>
1521
- </div>
1522
- </div>
1523
- </form>
1524
- </main>
1525
- </div>
1526
- <div id="drag-overlay" class="drag-overlay"><div class="drag-overlay-inner">Drop files to attach</div></div>
1527
- <div id="lightbox" class="lightbox" style="display:none"><img /></div>
1380
+ `;
1528
1381
 
1529
- <script>
1382
+ // src/web-ui-client.ts
1383
+ var getWebUiClientScript = (markedSource2) => `
1530
1384
  // Marked library (inlined)
1531
- ${markedSource}
1385
+ ${markedSource2}
1532
1386
 
1533
1387
  // Configure marked for GitHub Flavored Markdown (tables, etc.)
1534
1388
  marked.setOptions({
@@ -1581,6 +1435,14 @@ var renderWebUiHtml = (options) => {
1581
1435
  contextRingFill: $("context-ring-fill"),
1582
1436
  contextTooltip: $("context-tooltip"),
1583
1437
  sendBtnWrapper: $("send-btn-wrapper"),
1438
+ browserPanel: $("browser-panel"),
1439
+ browserPanelResize: $("browser-panel-resize"),
1440
+ browserPanelFrame: $("browser-panel-frame"),
1441
+ browserPanelUrl: $("browser-panel-url"),
1442
+ browserPanelPlaceholder: $("browser-panel-placeholder"),
1443
+ browserPanelClose: $("browser-panel-close"),
1444
+ browserNavBack: $("browser-nav-back"),
1445
+ browserNavForward: $("browser-nav-forward"),
1584
1446
  };
1585
1447
  const sendIconMarkup =
1586
1448
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
@@ -1750,7 +1612,19 @@ var renderWebUiHtml = (options) => {
1750
1612
  );
1751
1613
  };
1752
1614
 
1753
- const renderToolActivity = (items, approvalRequests = []) => {
1615
+ const extractToolImages = (output) => {
1616
+ const images = [];
1617
+ if (!output || typeof output !== "object") return images;
1618
+ const check = (val) => {
1619
+ if (val && typeof val === "object" && val.type === "file" && val.data && typeof val.mediaType === "string" && val.mediaType.startsWith("image/")) {
1620
+ images.push(val);
1621
+ }
1622
+ };
1623
+ if (Array.isArray(output)) { output.forEach(check); } else { Object.values(output).forEach(check); }
1624
+ return images;
1625
+ };
1626
+
1627
+ const renderToolActivity = (items, approvalRequests = [], toolImages = []) => {
1754
1628
  const hasItems = Array.isArray(items) && items.length > 0;
1755
1629
  const hasApprovals = Array.isArray(approvalRequests) && approvalRequests.length > 0;
1756
1630
  if (!hasItems && !hasApprovals) {
@@ -1774,9 +1648,16 @@ var renderWebUiHtml = (options) => {
1774
1648
  "</details>"
1775
1649
  )
1776
1650
  : "";
1651
+ const hasImages = Array.isArray(toolImages) && toolImages.length > 0;
1652
+ const imagesHtml = hasImages
1653
+ ? '<div class="tool-images">' + toolImages.map((img) =>
1654
+ '<img class="tool-screenshot" src="data:' + escapeHtml(img.mediaType) + ';base64,' + img.data + '" alt="' + escapeHtml(img.filename || "screenshot") + '" />'
1655
+ ).join("") + "</div>"
1656
+ : "";
1777
1657
  const cls = "tool-activity" + (hasApprovals ? " has-approvals" : "");
1778
1658
  return (
1779
1659
  '<div class="' + cls + '">' +
1660
+ imagesHtml +
1780
1661
  disclosure +
1781
1662
  renderApprovalRequests(approvalRequests) +
1782
1663
  "</div>"
@@ -2119,9 +2000,13 @@ var renderWebUiHtml = (options) => {
2119
2000
  sectionIdx === lastToolsSectionIndex
2120
2001
  ? pendingApprovals
2121
2002
  : [];
2003
+ const sectionImages =
2004
+ sectionIdx === lastToolsSectionIndex
2005
+ ? (m._toolImages || [])
2006
+ : [];
2122
2007
  content.insertAdjacentHTML(
2123
2008
  "beforeend",
2124
- renderToolActivity(section.content, sectionApprovals),
2009
+ renderToolActivity(section.content, sectionApprovals, sectionImages),
2125
2010
  );
2126
2011
  }
2127
2012
  });
@@ -2129,7 +2014,7 @@ var renderWebUiHtml = (options) => {
2129
2014
  if (isStreaming && i === messages.length - 1 && m._currentTools && m._currentTools.length > 0) {
2130
2015
  content.insertAdjacentHTML(
2131
2016
  "beforeend",
2132
- renderToolActivity(m._currentTools, m._pendingApprovals || []),
2017
+ renderToolActivity(m._currentTools, m._pendingApprovals || [], m._toolImages || []),
2133
2018
  );
2134
2019
  }
2135
2020
  // When reloading with unresolved approvals, show them even when not streaming
@@ -2230,6 +2115,7 @@ var renderWebUiHtml = (options) => {
2230
2115
  };
2231
2116
 
2232
2117
  const loadConversation = async (conversationId) => {
2118
+ if (window._resetBrowserPanel) window._resetBrowserPanel();
2233
2119
  const payload = await api("/api/conversations/" + encodeURIComponent(conversationId));
2234
2120
  elements.chatTitle.textContent = payload.conversation.title;
2235
2121
  state.activeMessages = hydratePendingApprovals(
@@ -2252,6 +2138,67 @@ var renderWebUiHtml = (options) => {
2252
2138
  }
2253
2139
  };
2254
2140
 
2141
+ const renameConversation = async (conversationId, title) => {
2142
+ const payload = await api("/api/conversations/" + encodeURIComponent(conversationId), {
2143
+ method: "PATCH",
2144
+ body: JSON.stringify({ title }),
2145
+ });
2146
+ elements.chatTitle.textContent = payload.conversation.title;
2147
+ const entry = state.conversations.find(c => c.conversationId === conversationId);
2148
+ if (entry) entry.title = payload.conversation.title;
2149
+ renderConversationList();
2150
+ };
2151
+
2152
+ const beginTitleEdit = () => {
2153
+ if (!state.activeConversationId) return;
2154
+ if (elements.chatTitle.querySelector("input")) return;
2155
+
2156
+ const current = elements.chatTitle.textContent || "";
2157
+ elements.chatTitle.textContent = "";
2158
+
2159
+ const input = document.createElement("input");
2160
+ input.type = "text";
2161
+ input.className = "topbar-title-input";
2162
+ input.value = current;
2163
+
2164
+ const sizer = document.createElement("span");
2165
+ sizer.style.cssText = "position:absolute;visibility:hidden;white-space:pre;font:inherit;font-weight:inherit;letter-spacing:inherit;padding:0 6px;";
2166
+ const autoSize = () => {
2167
+ sizer.textContent = input.value || " ";
2168
+ elements.chatTitle.appendChild(sizer);
2169
+ input.style.width = sizer.offsetWidth + 12 + "px";
2170
+ sizer.remove();
2171
+ };
2172
+
2173
+ elements.chatTitle.appendChild(input);
2174
+ autoSize();
2175
+ input.focus();
2176
+ input.select();
2177
+ input.addEventListener("input", autoSize);
2178
+
2179
+ const commit = async () => {
2180
+ const newTitle = input.value.trim();
2181
+ if (input._committed) return;
2182
+ input._committed = true;
2183
+
2184
+ if (newTitle && newTitle !== current) {
2185
+ try {
2186
+ await renameConversation(state.activeConversationId, newTitle);
2187
+ } catch {
2188
+ elements.chatTitle.textContent = current;
2189
+ }
2190
+ } else {
2191
+ elements.chatTitle.textContent = current;
2192
+ }
2193
+ };
2194
+
2195
+ input.addEventListener("blur", commit);
2196
+ input.addEventListener("keydown", (e) => {
2197
+ if (e.key === "Enter") { e.preventDefault(); input.blur(); }
2198
+ if (e.key === "Escape") { input.value = current; input.blur(); }
2199
+ });
2200
+ };
2201
+
2255
2202
  const streamConversationEvents = (conversationId, options) => {
2256
2203
  const liveOnly = options && options.liveOnly;
2257
2204
  return new Promise((resolve) => {
@@ -2271,6 +2218,7 @@ var renderWebUiHtml = (options) => {
2271
2218
  _sections: [],
2272
2219
  _currentText: "",
2273
2220
  _currentTools: [],
2221
+ _toolImages: [],
2274
2222
  _pendingApprovals: [],
2275
2223
  _activeActivities: [],
2276
2224
  metadata: { toolActivity: [] },
@@ -2418,6 +2366,9 @@ var renderWebUiHtml = (options) => {
2418
2366
  assistantMessage.metadata.toolActivity.push(toolText);
2419
2367
  renderIfActiveConversation(true);
2420
2368
  }
2369
+ if (eventName === "browser:status" && payload.active) {
2370
+ if (window._connectBrowserStream) window._connectBrowserStream();
2371
+ }
2421
2372
  if (eventName === "tool:approval:required") {
2422
2373
  const toolName = payload.tool || "tool";
2423
2374
  const activeActivity = removeActiveActivityForTool(
@@ -2569,6 +2520,7 @@ var renderWebUiHtml = (options) => {
2569
2520
  };
2570
2521
 
2571
2522
  const createConversation = async (title, options = {}) => {
2523
+ if (window._resetBrowserPanel) window._resetBrowserPanel();
2572
2524
  const shouldLoadConversation = options.loadConversation !== false;
2573
2525
  const payload = await api("/api/conversations", {
2574
2526
  method: "POST",
@@ -2896,9 +2848,10 @@ var renderWebUiHtml = (options) => {
2896
2848
  let assistantMessage = {
2897
2849
  role: "assistant",
2898
2850
  content: "",
2899
- _sections: [], // Array of {type: 'text'|'tools', content: string|array}
2851
+ _sections: [],
2900
2852
  _currentText: "",
2901
2853
  _currentTools: [],
2854
+ _toolImages: [],
2902
2855
  _activeActivities: [],
2903
2856
  _pendingApprovals: [],
2904
2857
  metadata: { toolActivity: [] }
@@ -3074,6 +3027,9 @@ var renderWebUiHtml = (options) => {
3074
3027
  assistantMessage.metadata.toolActivity.push(toolText);
3075
3028
  renderIfActiveConversation(true);
3076
3029
  }
3030
+ if (eventName === "browser:status" && payload.active) {
3031
+ if (window._connectBrowserStream) window._connectBrowserStream();
3032
+ }
3077
3033
  if (eventName === "tool:approval:required") {
3078
3034
  const toolName = payload.tool || "tool";
3079
3035
  const activeActivity = removeActiveActivityForTool(
@@ -3266,6 +3222,7 @@ var renderWebUiHtml = (options) => {
3266
3222
  });
3267
3223
 
3268
3224
  const startNewChat = () => {
3225
+ if (window._resetBrowserPanel) window._resetBrowserPanel();
3269
3226
  state.activeConversationId = null;
3270
3227
  state.activeMessages = [];
3271
3228
  state.confirmDeleteId = null;
@@ -3285,6 +3242,8 @@ var renderWebUiHtml = (options) => {
3285
3242
  elements.newChat.addEventListener("click", startNewChat);
3286
3243
  elements.topbarNewChat.addEventListener("click", startNewChat);
3287
3244
 
3245
+ elements.chatTitle.addEventListener("dblclick", beginTitleEdit);
3246
+
3288
3247
  elements.prompt.addEventListener("input", () => {
3289
3248
  autoResizePrompt();
3290
3249
  });
@@ -3406,11 +3365,13 @@ var renderWebUiHtml = (options) => {
3406
3365
  if (e.key === "Escape" && elements.lightbox.style.display !== "none") closeLightbox();
3407
3366
  });
3408
3367
 
3409
- // Lightbox from message images
3368
+ // Lightbox from message images and tool screenshots
3410
3369
  elements.messages.addEventListener("click", (e) => {
3411
3370
  const img = e.target;
3412
- if (!(img instanceof HTMLImageElement) || !img.closest(".user-file-attachments")) return;
3413
- openLightbox(img.src);
3371
+ if (!(img instanceof HTMLImageElement)) return;
3372
+ if (img.closest(".user-file-attachments") || img.classList.contains("tool-screenshot")) {
3373
+ openLightbox(img.src);
3374
+ }
3414
3375
  });
3415
3376
 
3416
3377
  // Lightbox from attachment preview chips
@@ -3757,27 +3718,603 @@ var renderWebUiHtml = (options) => {
3757
3718
  document.addEventListener("touchcancel", onTouchEnd, { passive: true });
3758
3719
  })();
3759
3720
 
3760
- // Prevent Safari back/forward navigation by manipulating history
3761
- // This doesn't stop the gesture animation but prevents actual navigation
3762
- if (window.navigator.standalone || window.matchMedia("(display-mode: standalone)").matches) {
3763
- history.pushState(null, "", location.href);
3764
- window.addEventListener("popstate", function() {
3765
- history.pushState(null, "", location.href);
3766
- });
3767
- }
3768
-
3769
- // Right edge blocker - intercept touch events to prevent forward navigation
3770
- var rightBlocker = document.querySelector(".edge-blocker-right");
3771
- if (rightBlocker) {
3772
- rightBlocker.addEventListener("touchstart", function(e) {
3773
- e.preventDefault();
3774
- }, { passive: false });
3775
- rightBlocker.addEventListener("touchmove", function(e) {
3776
- e.preventDefault();
3777
- }, { passive: false });
3778
- }
3779
- })();
3721
+ // Prevent Safari back/forward navigation by manipulating history
3722
+ // This doesn't stop the gesture animation but prevents actual navigation
3723
+ if (window.navigator.standalone || window.matchMedia("(display-mode: standalone)").matches) {
3724
+ history.pushState(null, "", location.href);
3725
+ window.addEventListener("popstate", function() {
3726
+ history.pushState(null, "", location.href);
3727
+ });
3728
+ }
3729
+
3730
+ // Right edge blocker - intercept touch events to prevent forward navigation
3731
+ var rightBlocker = document.querySelector(".edge-blocker-right");
3732
+ if (rightBlocker) {
3733
+ rightBlocker.addEventListener("touchstart", function(e) {
3734
+ e.preventDefault();
3735
+ }, { passive: false });
3736
+ rightBlocker.addEventListener("touchmove", function(e) {
3737
+ e.preventDefault();
3738
+ }, { passive: false });
3739
+ }
3740
+
3741
+ // Browser viewport panel
3742
+ (function initBrowserPanel() {
3743
+ var panel = elements.browserPanel;
3744
+ var frameImg = elements.browserPanelFrame;
3745
+ var urlLabel = elements.browserPanelUrl;
3746
+ var placeholder = elements.browserPanelPlaceholder;
3747
+ var closeBtn = elements.browserPanelClose;
3748
+ if (!panel || !frameImg) return;
3749
+
3750
+ var resizeHandle = elements.browserPanelResize;
3751
+ var mainEl = document.querySelector(".main-chat");
3752
+ var abortController = null;
3753
+ var panelHiddenByUser = false;
3754
+
3755
+ var showPanel = function(show) {
3756
+ var visible = show && !panelHiddenByUser;
3757
+ panel.style.display = visible ? "flex" : "none";
3758
+ if (resizeHandle) resizeHandle.style.display = visible ? "block" : "none";
3759
+ if (mainEl) {
3760
+ if (visible) mainEl.classList.add("has-browser");
3761
+ else mainEl.classList.remove("has-browser");
3762
+ }
3763
+ };
3764
+
3765
+
3766
+ closeBtn && closeBtn.addEventListener("click", function() {
3767
+ panelHiddenByUser = true;
3768
+ showPanel(false);
3769
+ });
3770
+
3771
+ var navBack = elements.browserNavBack;
3772
+ var navFwd = elements.browserNavForward;
3773
+ var sendBrowserNav = function(action) {
3774
+ var headers = { "Content-Type": "application/json" };
3775
+ if (state.csrfToken) headers["x-csrf-token"] = state.csrfToken;
3776
+ if (state.authToken) headers["authorization"] = "Bearer " + state.authToken;
3777
+ fetch("/api/browser/navigate", {
3778
+ method: "POST",
3779
+ headers: headers,
3780
+ body: JSON.stringify({ action: action, conversationId: state.activeConversationId }),
3781
+ });
3782
+ };
3783
+ navBack && navBack.addEventListener("click", function() { sendBrowserNav("back"); });
3784
+ navFwd && navFwd.addEventListener("click", function() { sendBrowserNav("forward"); });
3785
+
3786
+ window._resetBrowserPanel = function() {
3787
+ if (abortController) { abortController.abort(); abortController = null; }
3788
+ streamConversationId = null;
3789
+ panelHiddenByUser = false;
3790
+ showPanel(false);
3791
+ frameImg.style.display = "none";
3792
+ if (placeholder) {
3793
+ placeholder.textContent = "No active browser session";
3794
+ placeholder.style.display = "flex";
3795
+ }
3796
+ if (urlLabel) urlLabel.textContent = "";
3797
+ if (navBack) navBack.disabled = true;
3798
+ if (navFwd) navFwd.disabled = true;
3799
+ var headers = {};
3800
+ if (state.csrfToken) headers["x-csrf-token"] = state.csrfToken;
3801
+ if (state.authToken) headers["authorization"] = "Bearer " + state.authToken;
3802
+ var cid = state.activeConversationId;
3803
+ if (!cid) return;
3804
+ fetch("/api/browser/status?conversationId=" + encodeURIComponent(cid), { headers: headers }).then(function(r) {
3805
+ return r.json();
3806
+ }).then(function(s) {
3807
+ if (s && s.active && cid === state.activeConversationId) {
3808
+ if (urlLabel && s.url) urlLabel.textContent = s.url;
3809
+ if (navBack) navBack.disabled = false;
3810
+ if (navFwd) navFwd.disabled = false;
3811
+ connectBrowserStream();
3812
+ showPanel(true);
3813
+ }
3814
+ }).catch(function() {});
3815
+ };
3816
+
3817
+ // Drag-to-resize between conversation and browser panel
3818
+ if (resizeHandle && mainEl) {
3819
+ var dragging = false;
3820
+ resizeHandle.addEventListener("mousedown", function(e) {
3821
+ e.preventDefault();
3822
+ dragging = true;
3823
+ resizeHandle.classList.add("dragging");
3824
+ document.body.style.cursor = "col-resize";
3825
+ document.body.style.userSelect = "none";
3826
+ });
3827
+ document.addEventListener("mousemove", function(e) {
3828
+ if (!dragging) return;
3829
+ var body = mainEl.parentElement;
3830
+ if (!body) return;
3831
+ var bodyRect = body.getBoundingClientRect();
3832
+ var available = bodyRect.width - 1;
3833
+ var chatW = e.clientX - bodyRect.left;
3834
+ chatW = Math.max(280, Math.min(chatW, available - 280));
3835
+ var browserW = available - chatW;
3836
+ mainEl.style.flex = "0 0 " + chatW + "px";
3837
+ panel.style.flex = "0 0 " + browserW + "px";
3838
+ });
3839
+ document.addEventListener("mouseup", function() {
3840
+ if (!dragging) return;
3841
+ dragging = false;
3842
+ resizeHandle.classList.remove("dragging");
3843
+ document.body.style.cursor = "";
3844
+ document.body.style.userSelect = "";
3845
+ });
3846
+ }
3847
+
3848
+ // --- Browser viewport interaction ---
3849
+ var browserViewportW = 1280;
3850
+ var browserViewportH = 720;
3851
+ var sendBrowserInput = function(kind, event) {
3852
+ var headers = { "Content-Type": "application/json" };
3853
+ if (state.csrfToken) headers["x-csrf-token"] = state.csrfToken;
3854
+ if (state.authToken) headers["authorization"] = "Bearer " + state.authToken;
3855
+ fetch("/api/browser/input", {
3856
+ method: "POST",
3857
+ headers: headers,
3858
+ body: JSON.stringify({ kind: kind, event: event, conversationId: state.activeConversationId }),
3859
+ }).catch(function() {});
3860
+ };
3861
+ var toViewportCoords = function(e) {
3862
+ var rect = frameImg.getBoundingClientRect();
3863
+ var scaleX = browserViewportW / rect.width;
3864
+ var scaleY = browserViewportH / rect.height;
3865
+ return { x: Math.round((e.clientX - rect.left) * scaleX), y: Math.round((e.clientY - rect.top) * scaleY) };
3866
+ };
3867
+
3868
+ frameImg.style.cursor = "default";
3869
+ frameImg.setAttribute("tabindex", "0");
3870
+
3871
+ frameImg.addEventListener("click", function(e) {
3872
+ var coords = toViewportCoords(e);
3873
+ sendBrowserInput("mouse", { type: "mousePressed", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
3874
+ setTimeout(function() {
3875
+ sendBrowserInput("mouse", { type: "mouseReleased", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
3876
+ }, 50);
3877
+ frameImg.focus();
3878
+ });
3879
+
3880
+ frameImg.addEventListener("wheel", function(e) {
3881
+ e.preventDefault();
3882
+ var coords = toViewportCoords(e);
3883
+ sendBrowserInput("scroll", { deltaX: e.deltaX, deltaY: e.deltaY, x: coords.x, y: coords.y });
3884
+ }, { passive: false });
3885
+
3886
+ frameImg.addEventListener("keydown", function(e) {
3887
+ e.preventDefault();
3888
+ e.stopPropagation();
3889
+ if ((e.metaKey || e.ctrlKey) && e.key === "v") {
3890
+ navigator.clipboard.readText().then(function(clip) {
3891
+ if (clip) sendBrowserInput("paste", { text: clip });
3892
+ }).catch(function() {});
3893
+ return;
3894
+ }
3895
+ var text = e.key.length === 1 ? e.key : undefined;
3896
+ sendBrowserInput("keyboard", { type: "keyDown", key: e.key, code: e.code, text: text, keyCode: e.keyCode });
3897
+ });
3898
+ frameImg.addEventListener("keyup", function(e) {
3899
+ e.preventDefault();
3900
+ e.stopPropagation();
3901
+ sendBrowserInput("keyboard", { type: "keyUp", key: e.key, code: e.code, keyCode: e.keyCode });
3902
+ });
3903
+
3904
+ var streamConversationId = null;
3905
+
3906
+ var connectBrowserStream = function() {
3907
+ var cid = state.activeConversationId;
3908
+ if (!cid) return;
3909
+ if (streamConversationId === cid && abortController) return;
3910
+ if (abortController) abortController.abort();
3911
+ streamConversationId = cid;
3912
+ abortController = new AbortController();
3913
+ var headers = {};
3914
+ if (state.csrfToken) headers["x-csrf-token"] = state.csrfToken;
3915
+ if (state.authToken) headers["authorization"] = "Bearer " + state.authToken;
3916
+ fetch("/api/browser/stream?conversationId=" + encodeURIComponent(cid), { headers: headers, signal: abortController.signal })
3917
+ .then(function(res) {
3918
+ if (!res.ok || !res.body) return;
3919
+ var reader = res.body.getReader();
3920
+ var decoder = new TextDecoder();
3921
+ var buf = "";
3922
+ var sseEvent = "";
3923
+ var sseData = "";
3924
+ var pump = function() {
3925
+ reader.read().then(function(result) {
3926
+ if (result.done) return;
3927
+ buf += decoder.decode(result.value, { stream: true });
3928
+ var lines = buf.split("\\n");
3929
+ buf = lines.pop() || "";
3930
+ var eventName = sseEvent;
3931
+ var data = sseData;
3932
+ for (var i = 0; i < lines.length; i++) {
3933
+ var line = lines[i];
3934
+ if (line.startsWith("event: ")) {
3935
+ eventName = line.slice(7).trim();
3936
+ } else if (line.startsWith("data: ")) {
3937
+ data += line.slice(6);
3938
+ } else if (line === "" && eventName && data) {
3939
+ try {
3940
+ var payload = JSON.parse(data);
3941
+ if (streamConversationId !== state.activeConversationId) { eventName = ""; data = ""; continue; }
3942
+ if (eventName === "browser:frame") {
3943
+ frameImg.src = "data:image/jpeg;base64," + payload.data;
3944
+ frameImg.style.display = "block";
3945
+ if (payload.width) browserViewportW = payload.width;
3946
+ if (payload.height) browserViewportH = payload.height;
3947
+ if (placeholder) placeholder.style.display = "none";
3948
+ showPanel(true);
3949
+ void panel.offsetHeight;
3950
+ }
3951
+ if (eventName === "browser:status") {
3952
+ if (payload.url && urlLabel) urlLabel.textContent = payload.url;
3953
+ if (navBack) navBack.disabled = !payload.active;
3954
+ if (navFwd) navFwd.disabled = !payload.active;
3955
+ if (payload.active) {
3956
+ showPanel(true);
3957
+ } else {
3958
+ if (abortController) { abortController.abort(); abortController = null; }
3959
+ streamConversationId = null;
3960
+ frameImg.style.display = "none";
3961
+ if (placeholder) {
3962
+ placeholder.textContent = "Browser closed";
3963
+ placeholder.style.display = "flex";
3964
+ }
3965
+ showPanel(false);
3966
+ }
3967
+ }
3968
+ } catch(e) {}
3969
+ eventName = "";
3970
+ data = "";
3971
+ }
3972
+ }
3973
+ sseEvent = eventName;
3974
+ sseData = data;
3975
+ pump();
3976
+ }).catch(function() {});
3977
+ };
3978
+ pump();
3979
+ })
3980
+ .catch(function() {});
3981
+ };
3982
+
3983
+ window._connectBrowserStream = connectBrowserStream;
3984
+ })();
3985
+ })();
3986
+
3987
+ `;
3988
+
3989
+ // src/web-ui-store.ts
3990
+ import { createHash, randomUUID, timingSafeEqual } from "crypto";
3991
+ import { mkdir, readFile, writeFile } from "fs/promises";
3992
+ import { basename, dirname, resolve } from "path";
3993
+ import { homedir } from "os";
3994
+ var DEFAULT_OWNER = "local-owner";
3995
+ var SessionStore = class {
3996
+ sessions = /* @__PURE__ */ new Map();
3997
+ ttlMs;
3998
+ constructor(ttlMs = 1e3 * 60 * 60 * 8) {
3999
+ this.ttlMs = ttlMs;
4000
+ }
4001
+ create(ownerId = DEFAULT_OWNER) {
4002
+ const now = Date.now();
4003
+ const session = {
4004
+ sessionId: randomUUID(),
4005
+ ownerId,
4006
+ csrfToken: randomUUID(),
4007
+ createdAt: now,
4008
+ expiresAt: now + this.ttlMs,
4009
+ lastSeenAt: now
4010
+ };
4011
+ this.sessions.set(session.sessionId, session);
4012
+ return session;
4013
+ }
4014
+ get(sessionId) {
4015
+ const session = this.sessions.get(sessionId);
4016
+ if (!session) {
4017
+ return void 0;
4018
+ }
4019
+ if (Date.now() > session.expiresAt) {
4020
+ this.sessions.delete(sessionId);
4021
+ return void 0;
4022
+ }
4023
+ session.lastSeenAt = Date.now();
4024
+ return session;
4025
+ }
4026
+ delete(sessionId) {
4027
+ this.sessions.delete(sessionId);
4028
+ }
4029
+ };
4030
+ var LoginRateLimiter = class {
4031
+ constructor(maxAttempts = 5, windowMs = 1e3 * 60 * 5, lockoutMs = 1e3 * 60 * 10) {
4032
+ this.maxAttempts = maxAttempts;
4033
+ this.windowMs = windowMs;
4034
+ this.lockoutMs = lockoutMs;
4035
+ }
4036
+ attempts = /* @__PURE__ */ new Map();
4037
+ canAttempt(key) {
4038
+ const current = this.attempts.get(key);
4039
+ if (!current) {
4040
+ return { allowed: true };
4041
+ }
4042
+ if (current.lockedUntil && Date.now() < current.lockedUntil) {
4043
+ return {
4044
+ allowed: false,
4045
+ retryAfterSeconds: Math.ceil((current.lockedUntil - Date.now()) / 1e3)
4046
+ };
4047
+ }
4048
+ return { allowed: true };
4049
+ }
4050
+ registerFailure(key) {
4051
+ const now = Date.now();
4052
+ const current = this.attempts.get(key);
4053
+ if (!current || now - current.firstFailureAt > this.windowMs) {
4054
+ this.attempts.set(key, { count: 1, firstFailureAt: now });
4055
+ return { locked: false };
4056
+ }
4057
+ const count = current.count + 1;
4058
+ const next = {
4059
+ ...current,
4060
+ count
4061
+ };
4062
+ if (count >= this.maxAttempts) {
4063
+ next.lockedUntil = now + this.lockoutMs;
4064
+ this.attempts.set(key, next);
4065
+ return { locked: true, retryAfterSeconds: Math.ceil(this.lockoutMs / 1e3) };
4066
+ }
4067
+ this.attempts.set(key, next);
4068
+ return { locked: false };
4069
+ }
4070
+ registerSuccess(key) {
4071
+ this.attempts.delete(key);
4072
+ }
4073
+ };
4074
+ var parseCookies = (request) => {
4075
+ const cookieHeader = request.headers.cookie ?? "";
4076
+ const pairs = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean);
4077
+ const cookies = {};
4078
+ for (const pair of pairs) {
4079
+ const index = pair.indexOf("=");
4080
+ if (index <= 0) {
4081
+ continue;
4082
+ }
4083
+ const key = pair.slice(0, index);
4084
+ const value = pair.slice(index + 1);
4085
+ try {
4086
+ cookies[key] = decodeURIComponent(value);
4087
+ } catch {
4088
+ cookies[key] = value;
4089
+ }
4090
+ }
4091
+ return cookies;
4092
+ };
4093
+ var setCookie = (response, name, value, options) => {
4094
+ const segments = [`${name}=${encodeURIComponent(value)}`];
4095
+ segments.push(`Path=${options.path ?? "/"}`);
4096
+ if (typeof options.maxAge === "number") {
4097
+ segments.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
4098
+ }
4099
+ if (options.httpOnly) {
4100
+ segments.push("HttpOnly");
4101
+ }
4102
+ if (options.secure) {
4103
+ segments.push("Secure");
4104
+ }
4105
+ if (options.sameSite) {
4106
+ segments.push(`SameSite=${options.sameSite}`);
4107
+ }
4108
+ const previous = response.getHeader("Set-Cookie");
4109
+ const serialized = segments.join("; ");
4110
+ if (!previous) {
4111
+ response.setHeader("Set-Cookie", serialized);
4112
+ return;
4113
+ }
4114
+ if (Array.isArray(previous)) {
4115
+ response.setHeader("Set-Cookie", [...previous, serialized]);
4116
+ return;
4117
+ }
4118
+ response.setHeader("Set-Cookie", [String(previous), serialized]);
4119
+ };
4120
+ var verifyPassphrase = (provided, expected) => {
4121
+ const providedBuffer = Buffer.from(provided);
4122
+ const expectedBuffer = Buffer.from(expected);
4123
+ if (providedBuffer.length !== expectedBuffer.length) {
4124
+ const zero = Buffer.alloc(expectedBuffer.length);
4125
+ return timingSafeEqual(expectedBuffer, zero) && false;
4126
+ }
4127
+ return timingSafeEqual(providedBuffer, expectedBuffer);
4128
+ };
4129
+ var getRequestIp = (request) => {
4130
+ return request.socket.remoteAddress ?? "unknown";
4131
+ };
4132
+ var inferConversationTitle = (text) => {
4133
+ const normalized = text.trim().replace(/\s+/g, " ");
4134
+ if (!normalized) {
4135
+ return "New conversation";
4136
+ }
4137
+ return normalized.length <= 48 ? normalized : `${normalized.slice(0, 48)}...`;
4138
+ };
4139
+
4140
+ // src/web-ui.ts
4141
+ var require2 = createRequire(import.meta.url);
4142
+ var markedPackagePath = require2.resolve("marked");
4143
+ var markedDir = dirname2(markedPackagePath);
4144
+ var markedSource = readFileSync(join(markedDir, "marked.umd.js"), "utf-8");
4145
+ var renderManifest = (options) => {
4146
+ const name = options?.agentName ?? "Agent";
4147
+ return JSON.stringify({
4148
+ name,
4149
+ short_name: name,
4150
+ description: `${name} \u2014 AI agent powered by Poncho`,
4151
+ start_url: "/",
4152
+ display: "standalone",
4153
+ background_color: "#000000",
4154
+ theme_color: "#000000",
4155
+ icons: [
4156
+ { src: "/icon.svg", sizes: "any", type: "image/svg+xml" },
4157
+ { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
4158
+ { src: "/icon-512.png", sizes: "512x512", type: "image/png" }
4159
+ ]
4160
+ });
4161
+ };
4162
+ var renderIconSvg = (options) => {
4163
+ const letter = (options?.agentName ?? "A").charAt(0).toUpperCase();
4164
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
4165
+ <rect width="512" height="512" rx="96" fill="#000"/>
4166
+ <text x="256" y="256" dy=".35em" text-anchor="middle"
4167
+ font-family="-apple-system,BlinkMacSystemFont,sans-serif"
4168
+ font-size="280" font-weight="700" fill="#fff">${letter}</text>
4169
+ </svg>`;
4170
+ };
4171
+ var renderServiceWorker = () => `
4172
+ const CACHE_NAME = "poncho-shell-v1";
4173
+ const SHELL_URLS = ["/"];
4174
+
4175
+ self.addEventListener("install", (event) => {
4176
+ event.waitUntil(
4177
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
4178
+ );
4179
+ self.skipWaiting();
4180
+ });
4181
+
4182
+ self.addEventListener("activate", (event) => {
4183
+ event.waitUntil(
4184
+ caches.keys().then((keys) =>
4185
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
4186
+ )
4187
+ );
4188
+ self.clients.claim();
4189
+ });
4190
+
4191
+ self.addEventListener("fetch", (event) => {
4192
+ const url = new URL(event.request.url);
4193
+ if (event.request.method !== "GET" || url.pathname.startsWith("/api/")) {
4194
+ return;
4195
+ }
4196
+ event.respondWith(
4197
+ fetch(event.request)
4198
+ .then((response) => {
4199
+ const clone = response.clone();
4200
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
4201
+ return response;
4202
+ })
4203
+ .catch(() => caches.match(event.request))
4204
+ );
4205
+ });
4206
+ `;
4207
+ var renderWebUiHtml = (options) => {
4208
+ const agentInitial = (options?.agentName ?? "A").charAt(0).toUpperCase();
4209
+ const agentName = options?.agentName ?? "Agent";
4210
+ return `<!doctype html>
4211
+ <html lang="en">
4212
+ <head>
4213
+ <meta charset="utf-8">
4214
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
4215
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
4216
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
4217
+ <meta name="apple-mobile-web-app-capable" content="yes">
4218
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
4219
+ <meta name="apple-mobile-web-app-title" content="${agentName}">
4220
+ <link rel="manifest" href="/manifest.json">
4221
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
4222
+ <link rel="apple-touch-icon" href="/icon-192.png">
4223
+ <title>${agentName}</title>
4224
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
4225
+ <style>
4226
+ ${WEB_UI_STYLES}
4227
+ </style>
4228
+ </head>
4229
+ <body data-agent-initial="${agentInitial}" data-agent-name="${agentName}">
4230
+ <div class="edge-blocker-right"></div>
4231
+ <div id="auth" class="auth hidden">
4232
+ <form id="login-form" class="auth-card">
4233
+ <div class="auth-shell">
4234
+ <input id="passphrase" class="auth-input" type="password" placeholder="Passphrase" required autofocus>
4235
+ <button class="auth-submit" type="submit">
4236
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 8h8M9 5l3 3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
4237
+ </button>
4238
+ </div>
4239
+ <div id="login-error" class="error"></div>
4240
+ </form>
4241
+ </div>
4242
+
4243
+ <div id="app" class="shell hidden">
4244
+ <aside class="sidebar">
4245
+ <button id="new-chat" class="new-chat-btn">
4246
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
4247
+ </button>
4248
+ <div id="conversation-list" class="conversation-list"></div>
4249
+ <div class="sidebar-footer">
4250
+ <button id="logout" class="logout-btn">Log out</button>
4251
+ </div>
4252
+ </aside>
4253
+ <div id="sidebar-backdrop" class="sidebar-backdrop"></div>
4254
+ <main class="main">
4255
+ <div class="topbar">
4256
+ <button id="sidebar-toggle" class="sidebar-toggle">&#9776;</button>
4257
+ <div id="chat-title" class="topbar-title"></div>
4258
+ <button id="topbar-new-chat" class="topbar-new-chat">
4259
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
4260
+ </button>
4261
+ <a class="poncho-badge" href="https://github.com/cesr/poncho-ai" target="_blank" rel="noopener noreferrer"><img class="poncho-badge-avatar" src="data:image/jpeg;base64,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" alt="" aria-hidden="true">Built with Poncho</a>
4262
+ </div>
4263
+ <div class="main-body">
4264
+ <div class="main-chat">
4265
+ <div id="messages" class="messages">
4266
+ <div class="empty-state">
4267
+ <div class="assistant-avatar">${agentInitial}</div>
4268
+ <div class="empty-state-text">How can I help you today?</div>
4269
+ </div>
4270
+ </div>
4271
+ <form id="composer" class="composer">
4272
+ <div class="composer-inner">
4273
+ <div id="attachment-preview" class="attachment-preview" style="display:none"></div>
4274
+ <div class="composer-shell">
4275
+ <button id="attach-btn" class="attach-btn" type="button" title="Attach files">
4276
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
4277
+ </button>
4278
+ <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
4279
+ <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
4280
+ <div class="send-btn-wrapper" id="send-btn-wrapper">
4281
+ <svg class="context-ring" viewBox="0 0 36 36">
4282
+ <circle class="context-ring-fill" id="context-ring-fill" cx="18" cy="18" r="14.5" />
4283
+ </svg>
4284
+ <button id="send" class="send-btn" type="submit">
4285
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
4286
+ </button>
4287
+ <div class="context-tooltip" id="context-tooltip"></div>
4288
+ </div>
4289
+ </div>
4290
+ </div>
4291
+ </form>
4292
+ </div>
4293
+ <div id="browser-panel-resize" class="browser-panel-resize" style="display:none"></div>
4294
+ <aside id="browser-panel" class="browser-panel" style="display:none">
4295
+ <div class="browser-panel-header">
4296
+ <button id="browser-nav-back" class="browser-nav-btn" title="Go back" disabled>
4297
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
4298
+ </button>
4299
+ <button id="browser-nav-forward" class="browser-nav-btn" title="Go forward" disabled>
4300
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
4301
+ </button>
4302
+ <span id="browser-panel-url" class="browser-panel-url"></span>
4303
+ <button id="browser-panel-close" class="browser-panel-close" title="Hide panel">&times;</button>
4304
+ </div>
4305
+ <div class="browser-panel-viewport">
4306
+ <img id="browser-panel-frame" alt="Browser viewport" />
4307
+ <div id="browser-panel-placeholder" class="browser-panel-placeholder">No active browser session</div>
4308
+ </div>
4309
+ </aside>
4310
+ </div>
4311
+ </main>
4312
+ </div>
4313
+ <div id="drag-overlay" class="drag-overlay"><div class="drag-overlay-inner">Drop files to attach</div></div>
4314
+ <div id="lightbox" class="lightbox" style="display:none"><img /></div>
3780
4315
 
4316
+ <script>
4317
+ ${getWebUiClientScript(markedSource)}
3781
4318
  </script>
3782
4319
  </body>
3783
4320
  </html>`;
@@ -4637,8 +5174,6 @@ var buildConfigFromOnboardingAnswers = (answers) => {
4637
5174
  maxRecallConversations
4638
5175
  }
4639
5176
  };
4640
- maybeSet(storage, "url", answers["storage.url"]);
4641
- maybeSet(storage, "token", answers["storage.token"]);
4642
5177
  maybeSet(storage, "table", answers["storage.table"]);
4643
5178
  maybeSet(storage, "region", answers["storage.region"]);
4644
5179
  const authRequired = Boolean(answers["auth.required"] ?? false);
@@ -4811,7 +5346,7 @@ var runInitOnboarding = async (options) => {
4811
5346
 
4812
5347
  // src/init-feature-context.ts
4813
5348
  import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
4814
- import { dirname as dirname2, resolve as resolve2 } from "path";
5349
+ import { dirname as dirname3, resolve as resolve2 } from "path";
4815
5350
  import {
4816
5351
  ensureAgentIdentity,
4817
5352
  getAgentStoreDirectory
@@ -4851,7 +5386,7 @@ var readMarker = async (workingDir) => {
4851
5386
  };
4852
5387
  var writeMarker = async (workingDir, state) => {
4853
5388
  const markerPath = await getOnboardingMarkerPath(workingDir);
4854
- await mkdir2(dirname2(markerPath), { recursive: true });
5389
+ await mkdir2(dirname3(markerPath), { recursive: true });
4855
5390
  await writeFile2(markerPath, JSON.stringify(state, null, 2), "utf8");
4856
5391
  };
4857
5392
  var initializeOnboardingMarker = async (workingDir, options) => {
@@ -4911,7 +5446,7 @@ var consumeFirstRunIntro = async (workingDir, input2) => {
4911
5446
  };
4912
5447
 
4913
5448
  // src/index.ts
4914
- var __dirname = dirname3(fileURLToPath(import.meta.url));
5449
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
4915
5450
  var require3 = createRequire2(import.meta.url);
4916
5451
  var writeJson = (response, statusCode, payload) => {
4917
5452
  response.writeHead(statusCode, { "Content-Type": "application/json" });
@@ -5162,12 +5697,14 @@ Stopping is best-effort and keeps partial assistant output/tool activity already
5162
5697
  \`\`\`bash
5163
5698
  # Local web UI + API server
5164
5699
  poncho dev
5700
+ poncho dev --port 8080
5165
5701
 
5166
5702
  # Local interactive CLI
5167
5703
  poncho run --interactive
5168
5704
 
5169
5705
  # One-off run
5170
5706
  poncho run "Your task here"
5707
+ poncho run "Explain this code" --file ./src/index.ts
5171
5708
 
5172
5709
  # Run tests
5173
5710
  poncho test
@@ -5249,9 +5786,9 @@ How it works:
5249
5786
  - Use \`approval-required\` to require human approval for specific MCP calls or script files.
5250
5787
  - Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
5251
5788
 
5252
- Pattern format is strict slash-only:
5789
+ Pattern format:
5253
5790
 
5254
- - MCP: \`server/tool\`, \`server/*\`
5791
+ - MCP: \`mcp:server/tool\`, \`mcp:server/*\` (protocol-like prefix)
5255
5792
  - Scripts: relative paths such as \`./scripts/file.ts\`, \`./scripts/*\`, \`./tools/deploy.ts\`
5256
5793
 
5257
5794
  Skill authoring guardrails:
@@ -5309,6 +5846,7 @@ export default {
5309
5846
  },
5310
5847
  },
5311
5848
  },
5849
+ // browser: true, // Enable browser automation tools (requires @poncho-ai/browser)
5312
5850
  // webUi: false, // Disable built-in UI for API-only deployments
5313
5851
  };
5314
5852
  \`\`\`
@@ -5346,8 +5884,9 @@ cron:
5346
5884
  \`\`\`
5347
5885
 
5348
5886
  - \`poncho dev\`: jobs run via an in-process scheduler.
5349
- - \`poncho build vercel\`: generates \`vercel.json\` cron entries.
5887
+ - \`poncho build vercel\`: generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` to the same value as \`PONCHO_AUTH_TOKEN\` so Vercel can authenticate.
5350
5888
  - Docker/Fly.io: scheduler runs automatically.
5889
+ - Lambda: use AWS EventBridge to trigger \`GET /api/cron/<jobName>\` with \`Authorization: Bearer <token>\`.
5351
5890
  - Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
5352
5891
 
5353
5892
  ## Messaging (Slack)
@@ -5368,6 +5907,8 @@ Connect your agent to Slack so it responds to @mentions:
5368
5907
  messaging: [{ platform: 'slack' }]
5369
5908
  \`\`\`
5370
5909
 
5910
+ **Vercel deployments:** install \`@vercel/functions\` so Poncho can keep the serverless function alive while processing: \`npm install @vercel/functions\`
5911
+
5371
5912
  ## Messaging (Email via Resend)
5372
5913
 
5373
5914
  Connect your agent to email so users can interact by sending emails:
@@ -5388,6 +5929,8 @@ Connect your agent to email so users can interact by sending emails:
5388
5929
 
5389
5930
  For full control over outbound emails, use **tool mode** (\`mode: 'tool'\`) \u2014 the agent gets a \`send_email\` tool instead of auto-replying. See the repo README for details.
5390
5931
 
5932
+ **Vercel deployments:** install \`@vercel/functions\` so Poncho can keep the serverless function alive while processing: \`npm install @vercel/functions\`
5933
+
5391
5934
  ## Deployment
5392
5935
 
5393
5936
  \`\`\`bash
@@ -5398,8 +5941,28 @@ vercel deploy --prod
5398
5941
  # Build for Docker
5399
5942
  poncho build docker
5400
5943
  docker build -t ${name} .
5944
+ docker run -p 3000:3000 -e ANTHROPIC_API_KEY=sk-ant-... ${name}
5945
+
5946
+ # AWS Lambda
5947
+ poncho build lambda
5948
+
5949
+ # Fly.io
5950
+ poncho build fly
5951
+ fly deploy
5952
+ \`\`\`
5953
+
5954
+ Set environment variables on your deployment platform:
5955
+
5956
+ \`\`\`bash
5957
+ ANTHROPIC_API_KEY=sk-ant-... # Required
5958
+ PONCHO_AUTH_TOKEN=your-secret # Optional: protect your endpoint
5959
+ PONCHO_MAX_DURATION=55 # Optional: serverless timeout in seconds (enables auto-continuation)
5401
5960
  \`\`\`
5402
5961
 
5962
+ When \`PONCHO_MAX_DURATION\` is set, the agent automatically checkpoints and resumes across
5963
+ request cycles when it approaches the platform timeout. The web UI and client SDK handle
5964
+ this transparently.
5965
+
5403
5966
  ## Troubleshooting
5404
5967
 
5405
5968
  ### Vercel deploy issues
@@ -5502,7 +6065,7 @@ var FETCH_PAGE_SCRIPT_TEMPLATE = `export default async function run(input) {
5502
6065
  }
5503
6066
  `;
5504
6067
  var ensureFile = async (path, content) => {
5505
- await mkdir3(dirname3(path), { recursive: true });
6068
+ await mkdir3(dirname4(path), { recursive: true });
5506
6069
  await writeFile3(path, content, { encoding: "utf8", flag: "wx" });
5507
6070
  };
5508
6071
  var normalizeDeployTarget2 = (target) => {
@@ -5552,7 +6115,7 @@ var writeScaffoldFile = async (filePath, content, options) => {
5552
6115
  }
5553
6116
  }
5554
6117
  }
5555
- await mkdir3(dirname3(filePath), { recursive: true });
6118
+ await mkdir3(dirname4(filePath), { recursive: true });
5556
6119
  await writeFile3(filePath, content, "utf8");
5557
6120
  options.writtenPaths.push(relative(options.baseDir, filePath));
5558
6121
  };
@@ -6478,7 +7041,8 @@ var createRequestHandler = async (options) => {
6478
7041
  }
6479
7042
  const sessionStore = new SessionStore();
6480
7043
  const loginRateLimiter = new LoginRateLimiter();
6481
- const authToken = process.env.PONCHO_AUTH_TOKEN ?? "";
7044
+ const authTokenEnv = config?.auth?.tokenEnv ?? "PONCHO_AUTH_TOKEN";
7045
+ const authToken = process.env[authTokenEnv] ?? "";
6482
7046
  const authRequired = config?.auth?.required ?? false;
6483
7047
  const requireAuth = authRequired && authToken.length > 0;
6484
7048
  const webUiEnabled = config?.webUi !== false;
@@ -6503,6 +7067,7 @@ var createRequestHandler = async (options) => {
6503
7067
  return;
6504
7068
  }
6505
7069
  const [pathname] = request.url.split("?");
7070
+ const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
6506
7071
  if (webUiEnabled) {
6507
7072
  if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
6508
7073
  writeHtml(response, 200, renderWebUiHtml({ agentName }));
@@ -6647,6 +7212,113 @@ var createRequestHandler = async (options) => {
6647
7212
  return;
6648
7213
  }
6649
7214
  }
7215
+ const browserSession = harness.browserSession;
7216
+ if (pathname === "/api/browser/status" && request.method === "GET") {
7217
+ const cid = requestUrl.searchParams.get("conversationId") ?? "";
7218
+ writeJson(response, 200, {
7219
+ active: cid && browserSession ? browserSession.isActiveFor(cid) : false,
7220
+ url: cid && browserSession ? browserSession.getUrl(cid) ?? null : null,
7221
+ conversationId: cid || null
7222
+ });
7223
+ return;
7224
+ }
7225
+ if (pathname === "/api/browser/stream" && request.method === "GET") {
7226
+ const cid = requestUrl.searchParams.get("conversationId");
7227
+ if (!cid || !browserSession) {
7228
+ writeJson(response, 404, { error: "No browser session available" });
7229
+ return;
7230
+ }
7231
+ response.writeHead(200, {
7232
+ "Content-Type": "text/event-stream",
7233
+ "Cache-Control": "no-cache, no-transform",
7234
+ Connection: "keep-alive",
7235
+ "X-Accel-Buffering": "no"
7236
+ });
7237
+ response.flushHeaders();
7238
+ let frameCount = 0;
7239
+ const sendSse = (event, data) => {
7240
+ if (response.destroyed) return;
7241
+ response.write(`event: ${event}
7242
+ data: ${JSON.stringify(data)}
7243
+
7244
+ `);
7245
+ };
7246
+ sendSse("browser:status", {
7247
+ active: browserSession.isActiveFor(cid),
7248
+ url: browserSession.getUrl(cid),
7249
+ interactionAllowed: browserSession.isActiveFor(cid)
7250
+ });
7251
+ const removeFrame = browserSession.onFrame(cid, (frame) => {
7252
+ frameCount++;
7253
+ if (frameCount <= 3 || frameCount % 50 === 0) {
7254
+ console.log(`[poncho][browser-sse] Frame ${frameCount}: ${frame.width}x${frame.height}, data bytes: ${frame.data?.length ?? 0}`);
7255
+ }
7256
+ sendSse("browser:frame", frame);
7257
+ });
7258
+ const removeStatus = browserSession.onStatus(cid, (status) => {
7259
+ sendSse("browser:status", status);
7260
+ });
7261
+ if (browserSession.isActiveFor(cid)) {
7262
+ browserSession.screenshot(cid).then((data) => {
7263
+ if (!response.destroyed) {
7264
+ sendSse("browser:frame", { data, width: 1280, height: 720, timestamp: Date.now() });
7265
+ }
7266
+ return browserSession.startScreencast(cid);
7267
+ }).catch((err) => {
7268
+ console.error("[poncho][browser-sse] initial frame/screencast failed:", err?.message ?? err);
7269
+ });
7270
+ }
7271
+ request.on("close", () => {
7272
+ removeFrame();
7273
+ removeStatus();
7274
+ });
7275
+ return;
7276
+ }
7277
+ if (pathname === "/api/browser/input" && request.method === "POST") {
7278
+ const chunks = [];
7279
+ for await (const chunk of request) chunks.push(chunk);
7280
+ const body = JSON.parse(Buffer.concat(chunks).toString());
7281
+ const cid = body.conversationId;
7282
+ if (!cid || !browserSession || !browserSession.isActiveFor(cid)) {
7283
+ writeJson(response, 404, { error: "No active browser session" });
7284
+ return;
7285
+ }
7286
+ try {
7287
+ if (body.kind === "mouse") {
7288
+ await browserSession.injectMouse(cid, body.event);
7289
+ } else if (body.kind === "keyboard") {
7290
+ await browserSession.injectKeyboard(cid, body.event);
7291
+ } else if (body.kind === "scroll") {
7292
+ await browserSession.injectScroll(cid, body.event);
7293
+ } else if (body.kind === "paste") {
7294
+ await browserSession.injectPaste(cid, body.text ?? body.event?.text ?? "");
7295
+ } else {
7296
+ writeJson(response, 400, { error: "Unknown input kind" });
7297
+ return;
7298
+ }
7299
+ writeJson(response, 200, { ok: true });
7300
+ } catch (err) {
7301
+ writeJson(response, 500, { error: err?.message ?? "Input injection failed" });
7302
+ }
7303
+ return;
7304
+ }
7305
+ if (pathname === "/api/browser/navigate" && request.method === "POST") {
7306
+ const chunks = [];
7307
+ for await (const chunk of request) chunks.push(chunk);
7308
+ const body = JSON.parse(Buffer.concat(chunks).toString());
7309
+ const cid = body.conversationId;
7310
+ if (!cid || !browserSession || !browserSession.isActiveFor(cid)) {
7311
+ writeJson(response, 400, { error: "No active browser session" });
7312
+ return;
7313
+ }
7314
+ try {
7315
+ await browserSession.navigate(cid, body.action);
7316
+ writeJson(response, 200, { ok: true });
7317
+ } catch (err) {
7318
+ writeJson(response, 500, { error: err?.message ?? "Navigation failed" });
7319
+ }
7320
+ return;
7321
+ }
6650
7322
  if (pathname === "/api/conversations" && request.method === "GET") {
6651
7323
  const conversations = await conversationStore.list(ownerId);
6652
7324
  writeJson(response, 200, {
@@ -7691,7 +8363,7 @@ var runInteractive = async (workingDir, params) => {
7691
8363
  await harness.initialize();
7692
8364
  const identity = await ensureAgentIdentity2(workingDir);
7693
8365
  try {
7694
- const { runInteractiveInk } = await import("./run-interactive-ink-7ULE5JJI.js");
8366
+ const { runInteractiveInk } = await import("./run-interactive-ink-2JQJDP7W.js");
7695
8367
  await runInteractiveInk({
7696
8368
  harness,
7697
8369
  params,
@@ -7851,7 +8523,7 @@ var copySkillsIntoProject = async (workingDir, manifests, sourceName) => {
7851
8523
  await mkdir3(skillsDir, { recursive: true });
7852
8524
  const destinations = /* @__PURE__ */ new Map();
7853
8525
  for (const manifest of manifests) {
7854
- const sourceSkillDir = dirname3(manifest);
8526
+ const sourceSkillDir = dirname4(manifest);
7855
8527
  const skillFolderName = basename2(sourceSkillDir);
7856
8528
  if (destinations.has(skillFolderName)) {
7857
8529
  throw new Error(
@@ -7899,7 +8571,7 @@ var addSkill = async (workingDir, packageNameOrPath, options) => {
7899
8571
  var getSkillFolderNames = (manifests) => {
7900
8572
  const names = /* @__PURE__ */ new Set();
7901
8573
  for (const manifest of manifests) {
7902
- names.add(basename2(dirname3(manifest)));
8574
+ names.add(basename2(dirname4(manifest)));
7903
8575
  }
7904
8576
  return Array.from(names).sort();
7905
8577
  };
@@ -7963,7 +8635,7 @@ var listInstalledSkills = async (workingDir, sourceName) => {
7963
8635
  return [];
7964
8636
  }
7965
8637
  const manifests = await collectSkillManifests(targetRoot, sourceName ? 1 : 2);
7966
- return manifests.map((manifest) => relative(workingDir, dirname3(manifest)).split("\\").join("/")).sort();
8638
+ return manifests.map((manifest) => relative(workingDir, dirname4(manifest)).split("\\").join("/")).sort();
7967
8639
  };
7968
8640
  var listSkills = async (workingDir, sourceName) => {
7969
8641
  const skills = await listInstalledSkills(workingDir, sourceName);