@poncho-ai/cli 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +18 -0
- package/dist/{chunk-COLXQM6J.js → chunk-T2F6ICXI.js} +595 -45
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-Z3U5SV4C.js → run-interactive-ink-7FP5PT7Q.js} +83 -6
- package/package.json +5 -3
- package/src/index.ts +206 -26
- package/src/run-interactive-ink.ts +84 -5
- package/src/web-ui.ts +391 -17
- package/test/cli.test.ts +5 -0
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* produces reliable output with native scroll and text selection.
|
|
8
8
|
*/
|
|
9
9
|
import * as readline from "node:readline";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { resolve, basename } from "node:path";
|
|
10
12
|
import { stdout } from "node:process";
|
|
11
13
|
import {
|
|
12
14
|
parseAgentFile,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
type AgentHarness,
|
|
15
17
|
type ConversationStore,
|
|
16
18
|
} from "@poncho-ai/harness";
|
|
17
|
-
import type { AgentEvent, Message, TokenUsage } from "@poncho-ai/sdk";
|
|
19
|
+
import type { AgentEvent, FileInput, Message, TokenUsage } from "@poncho-ai/sdk";
|
|
18
20
|
import { inferConversationTitle } from "./web-ui.js";
|
|
19
21
|
import { consumeFirstRunIntro } from "./init-feature-context.js";
|
|
20
22
|
import { resolveHarnessEnvironment } from "./index.js";
|
|
@@ -135,6 +137,40 @@ const streamTextAsTokens = async (text: string): Promise<void> => {
|
|
|
135
137
|
}
|
|
136
138
|
};
|
|
137
139
|
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// File helpers
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const EXT_MIME: Record<string, string> = {
|
|
145
|
+
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
|
|
146
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
147
|
+
pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm",
|
|
148
|
+
mp3: "audio/mpeg", wav: "audio/wav", txt: "text/plain",
|
|
149
|
+
json: "application/json", csv: "text/csv", html: "text/html",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const extToMime = (ext: string): string => EXT_MIME[ext] ?? "application/octet-stream";
|
|
153
|
+
|
|
154
|
+
const readPendingFiles = async (
|
|
155
|
+
files: Array<{ path: string; resolved: string }>,
|
|
156
|
+
): Promise<FileInput[]> => {
|
|
157
|
+
const results: FileInput[] = [];
|
|
158
|
+
for (const f of files) {
|
|
159
|
+
try {
|
|
160
|
+
const buf = await readFile(f.resolved);
|
|
161
|
+
const ext = f.resolved.split(".").pop()?.toLowerCase() ?? "";
|
|
162
|
+
results.push({
|
|
163
|
+
data: buf.toString("base64"),
|
|
164
|
+
mediaType: extToMime(ext),
|
|
165
|
+
filename: basename(f.path),
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
console.log(`${C.yellow}warn: could not read ${f.path}, skipping${C.reset}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return results;
|
|
172
|
+
};
|
|
173
|
+
|
|
138
174
|
// ---------------------------------------------------------------------------
|
|
139
175
|
// Slash commands
|
|
140
176
|
// ---------------------------------------------------------------------------
|
|
@@ -145,6 +181,7 @@ type InteractiveState = {
|
|
|
145
181
|
messages: Message[];
|
|
146
182
|
turn: number;
|
|
147
183
|
activeConversationId: string | null;
|
|
184
|
+
pendingFiles: Array<{ path: string; resolved: string }>;
|
|
148
185
|
};
|
|
149
186
|
|
|
150
187
|
const computeTurn = (messages: Message[]): number =>
|
|
@@ -168,7 +205,7 @@ const handleSlash = async (
|
|
|
168
205
|
if (norm === "/help") {
|
|
169
206
|
console.log(
|
|
170
207
|
gray(
|
|
171
|
-
"commands> /help /clear /exit /tools /list /open <id> /new [title] /delete [id] /continue /reset [all]",
|
|
208
|
+
"commands> /help /clear /exit /tools /attach <path> /files /list /open <id> /new [title] /delete [id] /continue /reset [all]",
|
|
172
209
|
),
|
|
173
210
|
);
|
|
174
211
|
return { shouldExit: false };
|
|
@@ -296,6 +333,33 @@ const handleSlash = async (
|
|
|
296
333
|
console.log(gray(`conversations> reset ${conversation.conversationId}`));
|
|
297
334
|
return { shouldExit: false };
|
|
298
335
|
}
|
|
336
|
+
if (norm === "/attach") {
|
|
337
|
+
const filePath = args.join(" ").trim();
|
|
338
|
+
if (!filePath) {
|
|
339
|
+
console.log(yellow("usage> /attach <path>"));
|
|
340
|
+
return { shouldExit: false };
|
|
341
|
+
}
|
|
342
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
343
|
+
try {
|
|
344
|
+
await readFile(resolvedPath);
|
|
345
|
+
state.pendingFiles.push({ path: filePath, resolved: resolvedPath });
|
|
346
|
+
console.log(gray(`attached> ${filePath} [${state.pendingFiles.length} file(s) queued]`));
|
|
347
|
+
} catch {
|
|
348
|
+
console.log(yellow(`attach> file not found: ${filePath}`));
|
|
349
|
+
}
|
|
350
|
+
return { shouldExit: false };
|
|
351
|
+
}
|
|
352
|
+
if (norm === "/files") {
|
|
353
|
+
if (state.pendingFiles.length === 0) {
|
|
354
|
+
console.log(gray("files> none attached"));
|
|
355
|
+
} else {
|
|
356
|
+
console.log(gray("files>"));
|
|
357
|
+
for (const f of state.pendingFiles) {
|
|
358
|
+
console.log(gray(` ${f.path}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { shouldExit: false };
|
|
362
|
+
}
|
|
299
363
|
console.log(yellow(`Unknown command: ${command}`));
|
|
300
364
|
return { shouldExit: false };
|
|
301
365
|
};
|
|
@@ -369,7 +433,10 @@ export const runInteractiveInk = async ({
|
|
|
369
433
|
console.log(gray(' Type "exit" to quit, "/help" for commands'));
|
|
370
434
|
console.log(gray(" Press Ctrl+C during a run to stop streaming output."));
|
|
371
435
|
console.log(
|
|
372
|
-
gray(" Conversation
|
|
436
|
+
gray(" Conversation: /list /open <id> /new [title] /delete [id] /continue /reset [all]"),
|
|
437
|
+
);
|
|
438
|
+
console.log(
|
|
439
|
+
gray(" Files: /attach <path> /files\n"),
|
|
373
440
|
);
|
|
374
441
|
const intro = await consumeFirstRunIntro(workingDir, {
|
|
375
442
|
agentName: metadata.agentName,
|
|
@@ -390,6 +457,7 @@ export const runInteractiveInk = async ({
|
|
|
390
457
|
let activeConversationId: string | null = null;
|
|
391
458
|
let showToolPayloads = false;
|
|
392
459
|
let activeRunAbortController: AbortController | null = null;
|
|
460
|
+
let pendingFiles: Array<{ path: string; resolved: string }> = [];
|
|
393
461
|
|
|
394
462
|
rl.on("SIGINT", () => {
|
|
395
463
|
if (activeRunAbortController && !activeRunAbortController.signal.aborted) {
|
|
@@ -403,10 +471,12 @@ export const runInteractiveInk = async ({
|
|
|
403
471
|
|
|
404
472
|
// --- Main loop -------------------------------------------------------------
|
|
405
473
|
|
|
406
|
-
const prompt = `${C.cyan}you> ${C.reset}`;
|
|
407
|
-
|
|
408
474
|
// eslint-disable-next-line no-constant-condition
|
|
409
475
|
while (true) {
|
|
476
|
+
const filesTag = pendingFiles.length > 0
|
|
477
|
+
? `${C.dim}[${pendingFiles.length} file(s)] ${C.reset}`
|
|
478
|
+
: "";
|
|
479
|
+
const prompt = `${filesTag}${C.cyan}you> ${C.reset}`;
|
|
410
480
|
let task: string;
|
|
411
481
|
try {
|
|
412
482
|
task = await ask(rl, prompt);
|
|
@@ -431,6 +501,7 @@ export const runInteractiveInk = async ({
|
|
|
431
501
|
messages,
|
|
432
502
|
turn,
|
|
433
503
|
activeConversationId,
|
|
504
|
+
pendingFiles,
|
|
434
505
|
};
|
|
435
506
|
const slashResult = await handleSlash(
|
|
436
507
|
trimmed,
|
|
@@ -443,6 +514,7 @@ export const runInteractiveInk = async ({
|
|
|
443
514
|
messages = interactiveState.messages;
|
|
444
515
|
turn = interactiveState.turn;
|
|
445
516
|
activeConversationId = interactiveState.activeConversationId;
|
|
517
|
+
pendingFiles = interactiveState.pendingFiles;
|
|
446
518
|
continue;
|
|
447
519
|
}
|
|
448
520
|
|
|
@@ -477,11 +549,18 @@ export const runInteractiveInk = async ({
|
|
|
477
549
|
const startedAt = Date.now();
|
|
478
550
|
activeRunAbortController = new AbortController();
|
|
479
551
|
|
|
552
|
+
const turnFiles = pendingFiles.length > 0 ? await readPendingFiles(pendingFiles) : [];
|
|
553
|
+
if (pendingFiles.length > 0) {
|
|
554
|
+
console.log(gray(` sending ${turnFiles.length} file(s)`));
|
|
555
|
+
pendingFiles = [];
|
|
556
|
+
}
|
|
557
|
+
|
|
480
558
|
try {
|
|
481
559
|
for await (const event of harness.run({
|
|
482
560
|
task: trimmed,
|
|
483
561
|
parameters: params,
|
|
484
562
|
messages,
|
|
563
|
+
files: turnFiles.length > 0 ? turnFiles : undefined,
|
|
485
564
|
abortSignal: activeRunAbortController.signal,
|
|
486
565
|
})) {
|
|
487
566
|
if (event.type === "run:started") {
|
package/src/web-ui.ts
CHANGED
|
@@ -1077,7 +1077,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1077
1077
|
border-radius: 24px;
|
|
1078
1078
|
display: flex;
|
|
1079
1079
|
align-items: end;
|
|
1080
|
-
padding: 4px 6px 4px
|
|
1080
|
+
padding: 4px 6px 4px 6px;
|
|
1081
1081
|
transition: border-color 0.15s;
|
|
1082
1082
|
}
|
|
1083
1083
|
.composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
|
|
@@ -1090,7 +1090,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1090
1090
|
min-height: 40px;
|
|
1091
1091
|
max-height: 200px;
|
|
1092
1092
|
resize: none;
|
|
1093
|
-
padding:
|
|
1093
|
+
padding: 11px 0 8px;
|
|
1094
1094
|
font-size: 14px;
|
|
1095
1095
|
line-height: 1.5;
|
|
1096
1096
|
margin-top: -4px;
|
|
@@ -1118,6 +1118,149 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1118
1118
|
.send-btn.stop-mode:hover { background: #565656; }
|
|
1119
1119
|
.send-btn:disabled { opacity: 0.2; cursor: default; }
|
|
1120
1120
|
.send-btn:disabled:hover { background: #ededed; }
|
|
1121
|
+
.attach-btn {
|
|
1122
|
+
width: 32px;
|
|
1123
|
+
height: 32px;
|
|
1124
|
+
background: rgba(255,255,255,0.08);
|
|
1125
|
+
border: 0;
|
|
1126
|
+
border-radius: 50%;
|
|
1127
|
+
color: #999;
|
|
1128
|
+
cursor: pointer;
|
|
1129
|
+
display: grid;
|
|
1130
|
+
place-items: center;
|
|
1131
|
+
flex-shrink: 0;
|
|
1132
|
+
margin-bottom: 2px;
|
|
1133
|
+
margin-right: 8px;
|
|
1134
|
+
transition: color 0.15s, background 0.15s;
|
|
1135
|
+
}
|
|
1136
|
+
.attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
|
|
1137
|
+
.attachment-preview {
|
|
1138
|
+
display: flex;
|
|
1139
|
+
gap: 8px;
|
|
1140
|
+
padding: 8px 0;
|
|
1141
|
+
flex-wrap: wrap;
|
|
1142
|
+
}
|
|
1143
|
+
.attachment-chip {
|
|
1144
|
+
display: inline-flex;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
gap: 6px;
|
|
1147
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1148
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1149
|
+
border-radius: 9999px;
|
|
1150
|
+
padding: 4px 10px 4px 6px;
|
|
1151
|
+
font-size: 11px;
|
|
1152
|
+
color: #777;
|
|
1153
|
+
max-width: 200px;
|
|
1154
|
+
cursor: pointer;
|
|
1155
|
+
backdrop-filter: blur(6px);
|
|
1156
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1157
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
1158
|
+
}
|
|
1159
|
+
.attachment-chip:hover {
|
|
1160
|
+
color: #ededed;
|
|
1161
|
+
border-color: rgba(255, 255, 255, 0.25);
|
|
1162
|
+
background: rgba(0, 0, 0, 0.75);
|
|
1163
|
+
}
|
|
1164
|
+
.attachment-chip img {
|
|
1165
|
+
width: 20px;
|
|
1166
|
+
height: 20px;
|
|
1167
|
+
object-fit: cover;
|
|
1168
|
+
border-radius: 50%;
|
|
1169
|
+
flex-shrink: 0;
|
|
1170
|
+
cursor: pointer;
|
|
1171
|
+
}
|
|
1172
|
+
.attachment-chip .file-icon {
|
|
1173
|
+
width: 20px;
|
|
1174
|
+
height: 20px;
|
|
1175
|
+
border-radius: 50%;
|
|
1176
|
+
background: rgba(255,255,255,0.1);
|
|
1177
|
+
display: grid;
|
|
1178
|
+
place-items: center;
|
|
1179
|
+
font-size: 11px;
|
|
1180
|
+
flex-shrink: 0;
|
|
1181
|
+
}
|
|
1182
|
+
.attachment-chip .remove-attachment {
|
|
1183
|
+
cursor: pointer;
|
|
1184
|
+
color: #555;
|
|
1185
|
+
font-size: 14px;
|
|
1186
|
+
margin-left: 2px;
|
|
1187
|
+
line-height: 1;
|
|
1188
|
+
transition: color 0.15s;
|
|
1189
|
+
}
|
|
1190
|
+
.attachment-chip .remove-attachment:hover { color: #fff; }
|
|
1191
|
+
.attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
|
|
1192
|
+
.user-bubble .user-file-attachments {
|
|
1193
|
+
display: flex;
|
|
1194
|
+
gap: 6px;
|
|
1195
|
+
flex-wrap: wrap;
|
|
1196
|
+
margin-top: 8px;
|
|
1197
|
+
}
|
|
1198
|
+
.user-file-attachments img {
|
|
1199
|
+
max-width: 200px;
|
|
1200
|
+
max-height: 160px;
|
|
1201
|
+
border-radius: 8px;
|
|
1202
|
+
object-fit: cover;
|
|
1203
|
+
cursor: pointer;
|
|
1204
|
+
transition: opacity 0.15s;
|
|
1205
|
+
}
|
|
1206
|
+
.user-file-attachments img:hover { opacity: 0.85; }
|
|
1207
|
+
.lightbox {
|
|
1208
|
+
position: fixed;
|
|
1209
|
+
inset: 0;
|
|
1210
|
+
z-index: 9999;
|
|
1211
|
+
display: flex;
|
|
1212
|
+
align-items: center;
|
|
1213
|
+
justify-content: center;
|
|
1214
|
+
background: rgba(0,0,0,0);
|
|
1215
|
+
backdrop-filter: blur(0px);
|
|
1216
|
+
cursor: zoom-out;
|
|
1217
|
+
transition: background 0.25s ease, backdrop-filter 0.25s ease;
|
|
1218
|
+
}
|
|
1219
|
+
.lightbox.active {
|
|
1220
|
+
background: rgba(0,0,0,0.85);
|
|
1221
|
+
backdrop-filter: blur(8px);
|
|
1222
|
+
}
|
|
1223
|
+
.lightbox img {
|
|
1224
|
+
max-width: 90vw;
|
|
1225
|
+
max-height: 90vh;
|
|
1226
|
+
border-radius: 8px;
|
|
1227
|
+
object-fit: contain;
|
|
1228
|
+
transform: scale(0.4);
|
|
1229
|
+
opacity: 0;
|
|
1230
|
+
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.25s ease;
|
|
1231
|
+
}
|
|
1232
|
+
.lightbox.active img {
|
|
1233
|
+
transform: scale(1);
|
|
1234
|
+
opacity: 1;
|
|
1235
|
+
}
|
|
1236
|
+
.user-file-badge {
|
|
1237
|
+
display: inline-flex;
|
|
1238
|
+
align-items: center;
|
|
1239
|
+
gap: 4px;
|
|
1240
|
+
background: rgba(0,0,0,0.2);
|
|
1241
|
+
border-radius: 6px;
|
|
1242
|
+
padding: 4px 8px;
|
|
1243
|
+
font-size: 12px;
|
|
1244
|
+
color: rgba(255,255,255,0.8);
|
|
1245
|
+
}
|
|
1246
|
+
.drag-overlay {
|
|
1247
|
+
position: fixed;
|
|
1248
|
+
inset: 0;
|
|
1249
|
+
background: rgba(0,0,0,0.6);
|
|
1250
|
+
z-index: 9999;
|
|
1251
|
+
display: none;
|
|
1252
|
+
align-items: center;
|
|
1253
|
+
justify-content: center;
|
|
1254
|
+
pointer-events: none;
|
|
1255
|
+
}
|
|
1256
|
+
.drag-overlay.active { display: flex; }
|
|
1257
|
+
.drag-overlay-inner {
|
|
1258
|
+
border: 2px dashed rgba(255,255,255,0.4);
|
|
1259
|
+
border-radius: 16px;
|
|
1260
|
+
padding: 40px 60px;
|
|
1261
|
+
color: #fff;
|
|
1262
|
+
font-size: 16px;
|
|
1263
|
+
}
|
|
1121
1264
|
.disclaimer {
|
|
1122
1265
|
text-align: center;
|
|
1123
1266
|
color: #333;
|
|
@@ -1261,7 +1404,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1261
1404
|
</div>
|
|
1262
1405
|
<form id="composer" class="composer">
|
|
1263
1406
|
<div class="composer-inner">
|
|
1407
|
+
<div id="attachment-preview" class="attachment-preview" style="display:none"></div>
|
|
1264
1408
|
<div class="composer-shell">
|
|
1409
|
+
<button id="attach-btn" class="attach-btn" type="button" title="Attach files">
|
|
1410
|
+
<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>
|
|
1411
|
+
</button>
|
|
1412
|
+
<input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
|
|
1265
1413
|
<textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
|
|
1266
1414
|
<button id="send" class="send-btn" type="submit">
|
|
1267
1415
|
<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>
|
|
@@ -1271,6 +1419,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1271
1419
|
</form>
|
|
1272
1420
|
</main>
|
|
1273
1421
|
</div>
|
|
1422
|
+
<div id="drag-overlay" class="drag-overlay"><div class="drag-overlay-inner">Drop files to attach</div></div>
|
|
1423
|
+
<div id="lightbox" class="lightbox" style="display:none"><img /></div>
|
|
1274
1424
|
|
|
1275
1425
|
<script>
|
|
1276
1426
|
// Marked library (inlined)
|
|
@@ -1293,7 +1443,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1293
1443
|
activeStreamRunId: null,
|
|
1294
1444
|
isMessagesPinnedToBottom: true,
|
|
1295
1445
|
confirmDeleteId: null,
|
|
1296
|
-
approvalRequestsInFlight: {}
|
|
1446
|
+
approvalRequestsInFlight: {},
|
|
1447
|
+
pendingFiles: [],
|
|
1297
1448
|
};
|
|
1298
1449
|
|
|
1299
1450
|
const agentInitial = document.body.dataset.agentInitial || "A";
|
|
@@ -1315,7 +1466,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1315
1466
|
send: $("send"),
|
|
1316
1467
|
shell: $("app"),
|
|
1317
1468
|
sidebarToggle: $("sidebar-toggle"),
|
|
1318
|
-
sidebarBackdrop: $("sidebar-backdrop")
|
|
1469
|
+
sidebarBackdrop: $("sidebar-backdrop"),
|
|
1470
|
+
attachBtn: $("attach-btn"),
|
|
1471
|
+
fileInput: $("file-input"),
|
|
1472
|
+
attachmentPreview: $("attachment-preview"),
|
|
1473
|
+
dragOverlay: $("drag-overlay"),
|
|
1474
|
+
lightbox: $("lightbox"),
|
|
1319
1475
|
};
|
|
1320
1476
|
const sendIconMarkup =
|
|
1321
1477
|
'<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>';
|
|
@@ -1811,7 +1967,47 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1811
1967
|
wrap.appendChild(content);
|
|
1812
1968
|
row.appendChild(wrap);
|
|
1813
1969
|
} else {
|
|
1814
|
-
|
|
1970
|
+
const bubble = document.createElement("div");
|
|
1971
|
+
bubble.className = "user-bubble";
|
|
1972
|
+
if (typeof m.content === "string") {
|
|
1973
|
+
bubble.textContent = m.content;
|
|
1974
|
+
} else if (Array.isArray(m.content)) {
|
|
1975
|
+
const textParts = m.content.filter(p => p.type === "text").map(p => p.text).join("");
|
|
1976
|
+
if (textParts) {
|
|
1977
|
+
const textEl = document.createElement("div");
|
|
1978
|
+
textEl.textContent = textParts;
|
|
1979
|
+
bubble.appendChild(textEl);
|
|
1980
|
+
}
|
|
1981
|
+
const fileParts = m.content.filter(p => p.type === "file");
|
|
1982
|
+
if (fileParts.length > 0) {
|
|
1983
|
+
const filesEl = document.createElement("div");
|
|
1984
|
+
filesEl.className = "user-file-attachments";
|
|
1985
|
+
fileParts.forEach(fp => {
|
|
1986
|
+
if (fp.mediaType && fp.mediaType.startsWith("image/")) {
|
|
1987
|
+
const img = document.createElement("img");
|
|
1988
|
+
if (fp._localBlob) {
|
|
1989
|
+
if (!fp._cachedUrl) fp._cachedUrl = URL.createObjectURL(fp._localBlob);
|
|
1990
|
+
img.src = fp._cachedUrl;
|
|
1991
|
+
} else if (fp.data && fp.data.startsWith("poncho-upload://")) {
|
|
1992
|
+
img.src = "/api/uploads/" + encodeURIComponent(fp.data.replace("poncho-upload://", ""));
|
|
1993
|
+
} else if (fp.data && (fp.data.startsWith("http://") || fp.data.startsWith("https://"))) {
|
|
1994
|
+
img.src = fp.data;
|
|
1995
|
+
} else if (fp.data) {
|
|
1996
|
+
img.src = "data:" + fp.mediaType + ";base64," + fp.data;
|
|
1997
|
+
}
|
|
1998
|
+
img.alt = fp.filename || "image";
|
|
1999
|
+
filesEl.appendChild(img);
|
|
2000
|
+
} else {
|
|
2001
|
+
const badge = document.createElement("span");
|
|
2002
|
+
badge.className = "user-file-badge";
|
|
2003
|
+
badge.textContent = "📎 " + (fp.filename || "file");
|
|
2004
|
+
filesEl.appendChild(badge);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
bubble.appendChild(filesEl);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
row.appendChild(bubble);
|
|
1815
2011
|
}
|
|
1816
2012
|
col.appendChild(row);
|
|
1817
2013
|
});
|
|
@@ -2317,12 +2513,62 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2317
2513
|
}
|
|
2318
2514
|
};
|
|
2319
2515
|
|
|
2516
|
+
const renderAttachmentPreview = () => {
|
|
2517
|
+
const el = elements.attachmentPreview;
|
|
2518
|
+
if (state.pendingFiles.length === 0) {
|
|
2519
|
+
el.style.display = "none";
|
|
2520
|
+
el.innerHTML = "";
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
el.style.display = "flex";
|
|
2524
|
+
el.innerHTML = state.pendingFiles.map((f, i) => {
|
|
2525
|
+
const isImage = f.type.startsWith("image/");
|
|
2526
|
+
const thumbHtml = isImage
|
|
2527
|
+
? '<img src="' + URL.createObjectURL(f) + '" alt="" />'
|
|
2528
|
+
: '<span class="file-icon">📎</span>';
|
|
2529
|
+
return '<div class="attachment-chip" data-idx="' + i + '">'
|
|
2530
|
+
+ thumbHtml
|
|
2531
|
+
+ '<span class="filename">' + escapeHtml(f.name) + '</span>'
|
|
2532
|
+
+ '<span class="remove-attachment" data-idx="' + i + '">×</span>'
|
|
2533
|
+
+ '</div>';
|
|
2534
|
+
}).join("");
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2537
|
+
const addFiles = (fileList) => {
|
|
2538
|
+
for (const f of fileList) {
|
|
2539
|
+
if (f.size > 25 * 1024 * 1024) {
|
|
2540
|
+
alert("File too large: " + f.name + " (max 25MB)");
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
state.pendingFiles.push(f);
|
|
2544
|
+
}
|
|
2545
|
+
renderAttachmentPreview();
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2320
2548
|
const sendMessage = async (text) => {
|
|
2321
2549
|
const messageText = (text || "").trim();
|
|
2322
2550
|
if (!messageText || state.isStreaming) {
|
|
2323
2551
|
return;
|
|
2324
2552
|
}
|
|
2325
|
-
const
|
|
2553
|
+
const filesToSend = [...state.pendingFiles];
|
|
2554
|
+
state.pendingFiles = [];
|
|
2555
|
+
renderAttachmentPreview();
|
|
2556
|
+
let userContent;
|
|
2557
|
+
if (filesToSend.length > 0) {
|
|
2558
|
+
userContent = [{ type: "text", text: messageText }];
|
|
2559
|
+
for (const f of filesToSend) {
|
|
2560
|
+
userContent.push({
|
|
2561
|
+
type: "file",
|
|
2562
|
+
data: URL.createObjectURL(f),
|
|
2563
|
+
mediaType: f.type,
|
|
2564
|
+
filename: f.name,
|
|
2565
|
+
_localBlob: f,
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
} else {
|
|
2569
|
+
userContent = messageText;
|
|
2570
|
+
}
|
|
2571
|
+
const localMessages = [...(state.activeMessages || []), { role: "user", content: userContent }];
|
|
2326
2572
|
let assistantMessage = {
|
|
2327
2573
|
role: "assistant",
|
|
2328
2574
|
content: "",
|
|
@@ -2365,13 +2611,38 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2365
2611
|
assistantMessage._currentText = "";
|
|
2366
2612
|
}
|
|
2367
2613
|
};
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2614
|
+
let _continuationMessage = messageText;
|
|
2615
|
+
let _totalSteps = 0;
|
|
2616
|
+
let _maxSteps = 0;
|
|
2617
|
+
while (_continuationMessage) {
|
|
2618
|
+
let _shouldContinue = false;
|
|
2619
|
+
let fetchOpts;
|
|
2620
|
+
if (filesToSend.length > 0 && _continuationMessage === messageText) {
|
|
2621
|
+
const formData = new FormData();
|
|
2622
|
+
formData.append("message", _continuationMessage);
|
|
2623
|
+
for (const f of filesToSend) {
|
|
2624
|
+
formData.append("files", f, f.name);
|
|
2625
|
+
}
|
|
2626
|
+
fetchOpts = {
|
|
2627
|
+
method: "POST",
|
|
2628
|
+
credentials: "include",
|
|
2629
|
+
headers: { "x-csrf-token": state.csrfToken },
|
|
2630
|
+
body: formData,
|
|
2631
|
+
signal: streamAbortController.signal,
|
|
2632
|
+
};
|
|
2633
|
+
} else {
|
|
2634
|
+
fetchOpts = {
|
|
2635
|
+
method: "POST",
|
|
2636
|
+
credentials: "include",
|
|
2637
|
+
headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
|
|
2638
|
+
body: JSON.stringify({ message: _continuationMessage }),
|
|
2639
|
+
signal: streamAbortController.signal,
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
const response = await fetch(
|
|
2643
|
+
"/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
|
|
2644
|
+
fetchOpts,
|
|
2645
|
+
);
|
|
2375
2646
|
if (!response.ok || !response.body) {
|
|
2376
2647
|
throw new Error("Failed to stream response");
|
|
2377
2648
|
}
|
|
@@ -2546,11 +2817,17 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2546
2817
|
renderIfActiveConversation(true);
|
|
2547
2818
|
}
|
|
2548
2819
|
if (eventName === "run:completed") {
|
|
2549
|
-
|
|
2550
|
-
if (
|
|
2551
|
-
|
|
2820
|
+
_totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
|
|
2821
|
+
if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
|
|
2822
|
+
if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
|
|
2823
|
+
_shouldContinue = true;
|
|
2824
|
+
} else {
|
|
2825
|
+
finalizeAssistantMessage();
|
|
2826
|
+
if (!assistantMessage.content || assistantMessage.content.length === 0) {
|
|
2827
|
+
assistantMessage.content = String(payload.result?.response || "");
|
|
2828
|
+
}
|
|
2829
|
+
renderIfActiveConversation(false);
|
|
2552
2830
|
}
|
|
2553
|
-
renderIfActiveConversation(false);
|
|
2554
2831
|
}
|
|
2555
2832
|
if (eventName === "run:cancelled") {
|
|
2556
2833
|
finalizeAssistantMessage();
|
|
@@ -2568,6 +2845,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2568
2845
|
}
|
|
2569
2846
|
});
|
|
2570
2847
|
}
|
|
2848
|
+
if (!_shouldContinue) break;
|
|
2849
|
+
_continuationMessage = "Continue";
|
|
2850
|
+
}
|
|
2571
2851
|
// Update active state only if user is still on this conversation.
|
|
2572
2852
|
if (state.activeConversationId === streamConversationId) {
|
|
2573
2853
|
state.activeMessages = localMessages;
|
|
@@ -2714,6 +2994,100 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2714
2994
|
await sendMessage(value);
|
|
2715
2995
|
});
|
|
2716
2996
|
|
|
2997
|
+
elements.attachBtn.addEventListener("click", () => elements.fileInput.click());
|
|
2998
|
+
elements.fileInput.addEventListener("change", () => {
|
|
2999
|
+
if (elements.fileInput.files && elements.fileInput.files.length > 0) {
|
|
3000
|
+
addFiles(elements.fileInput.files);
|
|
3001
|
+
elements.fileInput.value = "";
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
elements.attachmentPreview.addEventListener("click", (e) => {
|
|
3005
|
+
const rm = e.target.closest(".remove-attachment");
|
|
3006
|
+
if (rm) {
|
|
3007
|
+
const idx = parseInt(rm.dataset.idx, 10);
|
|
3008
|
+
state.pendingFiles.splice(idx, 1);
|
|
3009
|
+
renderAttachmentPreview();
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
let dragCounter = 0;
|
|
3014
|
+
document.addEventListener("dragenter", (e) => {
|
|
3015
|
+
e.preventDefault();
|
|
3016
|
+
dragCounter++;
|
|
3017
|
+
if (dragCounter === 1) elements.dragOverlay.classList.add("active");
|
|
3018
|
+
});
|
|
3019
|
+
document.addEventListener("dragleave", (e) => {
|
|
3020
|
+
e.preventDefault();
|
|
3021
|
+
dragCounter--;
|
|
3022
|
+
if (dragCounter <= 0) { dragCounter = 0; elements.dragOverlay.classList.remove("active"); }
|
|
3023
|
+
});
|
|
3024
|
+
document.addEventListener("dragover", (e) => e.preventDefault());
|
|
3025
|
+
document.addEventListener("drop", (e) => {
|
|
3026
|
+
e.preventDefault();
|
|
3027
|
+
dragCounter = 0;
|
|
3028
|
+
elements.dragOverlay.classList.remove("active");
|
|
3029
|
+
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
|
3030
|
+
addFiles(e.dataTransfer.files);
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
// Paste files/images from clipboard
|
|
3035
|
+
elements.prompt.addEventListener("paste", (e) => {
|
|
3036
|
+
const items = e.clipboardData && e.clipboardData.items;
|
|
3037
|
+
if (!items) return;
|
|
3038
|
+
const files = [];
|
|
3039
|
+
for (let i = 0; i < items.length; i++) {
|
|
3040
|
+
if (items[i].kind === "file") {
|
|
3041
|
+
const f = items[i].getAsFile();
|
|
3042
|
+
if (f) files.push(f);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
if (files.length > 0) {
|
|
3046
|
+
e.preventDefault();
|
|
3047
|
+
addFiles(files);
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
|
|
3051
|
+
// Lightbox: open/close helpers
|
|
3052
|
+
const lightboxImg = elements.lightbox.querySelector("img");
|
|
3053
|
+
const openLightbox = (src) => {
|
|
3054
|
+
lightboxImg.src = src;
|
|
3055
|
+
elements.lightbox.style.display = "flex";
|
|
3056
|
+
requestAnimationFrame(() => {
|
|
3057
|
+
requestAnimationFrame(() => elements.lightbox.classList.add("active"));
|
|
3058
|
+
});
|
|
3059
|
+
};
|
|
3060
|
+
const closeLightbox = () => {
|
|
3061
|
+
elements.lightbox.classList.remove("active");
|
|
3062
|
+
elements.lightbox.addEventListener("transitionend", function handler() {
|
|
3063
|
+
elements.lightbox.removeEventListener("transitionend", handler);
|
|
3064
|
+
elements.lightbox.style.display = "none";
|
|
3065
|
+
lightboxImg.src = "";
|
|
3066
|
+
});
|
|
3067
|
+
};
|
|
3068
|
+
elements.lightbox.addEventListener("click", closeLightbox);
|
|
3069
|
+
document.addEventListener("keydown", (e) => {
|
|
3070
|
+
if (e.key === "Escape" && elements.lightbox.style.display !== "none") closeLightbox();
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
// Lightbox from message images
|
|
3074
|
+
elements.messages.addEventListener("click", (e) => {
|
|
3075
|
+
const img = e.target;
|
|
3076
|
+
if (!(img instanceof HTMLImageElement) || !img.closest(".user-file-attachments")) return;
|
|
3077
|
+
openLightbox(img.src);
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
// Lightbox from attachment preview chips
|
|
3081
|
+
elements.attachmentPreview.addEventListener("click", (e) => {
|
|
3082
|
+
if (e.target.closest(".remove-attachment")) return;
|
|
3083
|
+
const chip = e.target.closest(".attachment-chip");
|
|
3084
|
+
if (!chip) return;
|
|
3085
|
+
const img = chip.querySelector("img");
|
|
3086
|
+
if (!img) return;
|
|
3087
|
+
e.stopPropagation();
|
|
3088
|
+
openLightbox(img.src);
|
|
3089
|
+
});
|
|
3090
|
+
|
|
2717
3091
|
elements.messages.addEventListener("click", async (event) => {
|
|
2718
3092
|
const target = event.target;
|
|
2719
3093
|
if (!(target instanceof Element)) {
|
package/test/cli.test.ts
CHANGED
|
@@ -155,6 +155,11 @@ vi.mock("@poncho-ai/harness", () => ({
|
|
|
155
155
|
TelemetryEmitter: class {
|
|
156
156
|
async emit(): Promise<void> {}
|
|
157
157
|
},
|
|
158
|
+
createUploadStore: async () => ({
|
|
159
|
+
put: async () => "poncho-upload://test",
|
|
160
|
+
get: async () => Buffer.from(""),
|
|
161
|
+
delete: async () => {},
|
|
162
|
+
}),
|
|
158
163
|
}));
|
|
159
164
|
|
|
160
165
|
import {
|