@lazyingart/agintiflow 0.12.6 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/docs/agent-runtime-pipe.md +3 -1
- package/package.json +1 -1
- package/public/app.js +297 -23
- package/public/index.html +6 -1
- package/public/styles.css +110 -0
- package/scripts/smoke-cli-chat.js +21 -4
- package/scripts/smoke-web-api.js +37 -0
- package/src/interactive-cli.js +55 -5
- package/web.js +166 -4
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ aginti
|
|
|
67
67
|
aginti chat
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed first, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. The input panel always shows the current `cwd` footer. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and
|
|
70
|
+
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed first, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. The input panel always shows the current `cwd` footer and a single live status row, so long goals and tool updates are compacted instead of flooding the transcript. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, tables, and red/green patch diff lines. Resuming a session prints a compact recent-chat preview before the prompt. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
|
|
71
71
|
|
|
72
72
|
`aginti init` creates `AGINTI.md` at the project root. This is the editable project-instruction file for both CLI and web runs, similar to `AGENTS.md` or project memory in other agents. Keep durable preferences, commands, and constraints there, but never secrets. You can edit it manually or ask in chat, for example: `update AGINTI.md to remember that this project uses pytest and npm run check`.
|
|
73
73
|
|
|
@@ -365,7 +365,7 @@ Package policy values:
|
|
|
365
365
|
|
|
366
366
|
Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode the project folder is mounted as `/workspace`; any file written to `/workspace/report.pdf` appears on the host as `<your-project>/report.pdf`. CLI runs print both the host workspace and the Docker mapping before execution. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
|
|
367
367
|
|
|
368
|
-
The web chat mirrors the CLI session store.
|
|
368
|
+
The web chat mirrors the CLI session store while keeping browser-native controls. Launch `aginti web` from the same project folder and it reads the same `.sessions/` history as `aginti` and `aginti resume`. In the web UI, Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP message (`→`) to `.sessions/<session-id>/inbox.jsonl` so an active CLI or web agent can consume it at the next safe boundary. `Queue after finish` stores a browser-local next prompt (`↳`) and starts it after the current web run finishes; queued items have Edit and Remove buttons. Esc or Stop stops active web runs. Generated local sites should use the built-in `preview_workspace` or `open_workspace_file` tools; AgInTiFlow avoids transient localhost servers inside Docker because those containers stop between commands and their ports are not host-published.
|
|
369
369
|
|
|
370
370
|
Safe preflight endpoints:
|
|
371
371
|
|
|
@@ -376,6 +376,7 @@ curl -X POST http://127.0.0.1:3210/api/sandbox/preflight \
|
|
|
376
376
|
-d '{"sandboxMode":"docker-workspace","buildImage":true}'
|
|
377
377
|
curl http://127.0.0.1:3210/api/workspace/changes
|
|
378
378
|
curl "http://127.0.0.1:3210/api/sessions/<session-id>/artifacts"
|
|
379
|
+
curl "http://127.0.0.1:3210/api/sessions/<session-id>/inbox"
|
|
379
380
|
```
|
|
380
381
|
|
|
381
382
|
These endpoints report Docker/image/workspace readiness, recent sandbox logs, file-change provenance, and renderable artifact metadata without returning API keys or npm tokens. Artifact content is loaded on demand through guarded session/workspace reads.
|
|
@@ -7,10 +7,12 @@ AgInTiFlow keeps CLI and web runs equivalent by using the project folder as the
|
|
|
7
7
|
- Web settings database: `<project>/.sessions/web-state.sqlite`.
|
|
8
8
|
- Runtime inbox: `<project>/.sessions/<session-id>/inbox.jsonl`.
|
|
9
9
|
|
|
10
|
-
When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
|
|
10
|
+
When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The web API exposes `GET /api/sessions/:id/inbox`, `POST /api/sessions/:id/inbox`, `PATCH /api/sessions/:id/inbox/:itemId`, and `DELETE /api/sessions/:id/inbox/:itemId` so browser users can inspect, edit, or remove pending pipe messages before the runner consumes them. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
|
|
11
11
|
|
|
12
12
|
The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. The current command cwd is rendered below the input panel in both idle and running states.
|
|
13
13
|
|
|
14
|
+
The web UI uses a related but browser-appropriate pattern. Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP inbox item shared with CLI. `Queue after finish` keeps a browser-local next prompt and starts it after the current web-owned run finishes. Both lanes render in a pending panel with Edit and Remove buttons instead of terminal-only keybindings.
|
|
15
|
+
|
|
14
16
|
Runs can be stopped without corrupting session state. The CLI listens for Esc or Ctrl+C during an active run, and the web UI exposes a Stop button plus Esc. Both paths send an abort signal, persist `session.stopped`, and leave the session resumable through `aginti resume <session-id>`.
|
|
15
17
|
|
|
16
18
|
Default execution is Docker workspace mode with package installs approved inside the sandbox. The project is mounted at `/workspace`; persistent agent toolchain folders are mounted at `/aginti-home`, `/aginti-cache`, and `/aginti-env` from `~/.agintiflow/docker/`. Python, conda, and other language-level environments should be installed under `/aginti-env` so they survive across runs. Apt/apk package changes are ephemeral unless the Docker image is rebuilt.
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -122,8 +122,18 @@ const translations = {
|
|
|
122
122
|
sendingStatus: "Sending...",
|
|
123
123
|
runningStatus: "Running...",
|
|
124
124
|
queuedStatus: "Queued for the running agent.",
|
|
125
|
+
queuedAsapStatus: "Piped into the shared session inbox.",
|
|
126
|
+
queuedAfterFinishStatus: "Queued for the next web run.",
|
|
125
127
|
failedContinue: "Failed to continue the conversation.",
|
|
126
|
-
chatShortcutHelp: "Enter sends ·
|
|
128
|
+
chatShortcutHelp: "Enter sends · Shift+Enter adds a newline · use buttons for pipe/queue control",
|
|
129
|
+
pipeMessageButton: "Pipe to run",
|
|
130
|
+
queueAfterFinishButton: "Queue after finish",
|
|
131
|
+
pendingMessagesTitle: "Pending messages",
|
|
132
|
+
asapQueueLabel: "ASAP pipe",
|
|
133
|
+
afterFinishQueueLabel: "After finish",
|
|
134
|
+
editQueuedMessage: "Edit",
|
|
135
|
+
removeQueuedMessage: "Remove",
|
|
136
|
+
queuedConsumedHint: "ASAP pipe messages are shared with CLI and consumed at the next safe agent boundary.",
|
|
127
137
|
conversationMenuLabel: "Manage conversations",
|
|
128
138
|
manageConversationsTitle: "Manage conversations",
|
|
129
139
|
manageConversationsHelp: "Rename, auto-rename, or delete saved chat history.",
|
|
@@ -671,7 +681,10 @@ const sessionSelectEl = document.querySelector("#session-select");
|
|
|
671
681
|
const chatThreadEl = document.querySelector("#chat-thread");
|
|
672
682
|
const chatFormEl = document.querySelector("#chat-form");
|
|
673
683
|
const chatInputEl = document.querySelector("#chat-input");
|
|
684
|
+
const chatPendingEl = document.querySelector("#chat-pending");
|
|
674
685
|
const chatStatusEl = document.querySelector("#chat-status");
|
|
686
|
+
const pipeMessageButton = document.querySelector("#pipe-message");
|
|
687
|
+
const queueAfterFinishButton = document.querySelector("#queue-after-finish");
|
|
675
688
|
const manageSessionsButton = document.querySelector("#manage-sessions");
|
|
676
689
|
const openArtifactsButton = document.querySelector("#open-artifacts");
|
|
677
690
|
const artifactBadgeEl = document.querySelector("#artifact-badge");
|
|
@@ -714,6 +727,9 @@ let pollTimer = null;
|
|
|
714
727
|
let saveTimer = null;
|
|
715
728
|
let lastChatEntries = [];
|
|
716
729
|
let lastSessions = [];
|
|
730
|
+
let pendingInboxItems = [];
|
|
731
|
+
let pendingAfterFinishItems = [];
|
|
732
|
+
let flushingAfterFinish = false;
|
|
717
733
|
let lastKeyStatus = null;
|
|
718
734
|
let lastWrappers = [];
|
|
719
735
|
let lastSandbox = null;
|
|
@@ -1270,6 +1286,233 @@ function renderChat(chatEntries) {
|
|
|
1270
1286
|
chatThreadEl.scrollTop = chatThreadEl.scrollHeight;
|
|
1271
1287
|
}
|
|
1272
1288
|
|
|
1289
|
+
function afterFinishQueueKey(sessionId = currentSessionId) {
|
|
1290
|
+
return `agintiflow.web.afterFinishQueue.${sessionId || "none"}`;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function loadAfterFinishQueue(sessionId = currentSessionId) {
|
|
1294
|
+
if (!sessionId) return [];
|
|
1295
|
+
try {
|
|
1296
|
+
const parsed = JSON.parse(localStorage.getItem(afterFinishQueueKey(sessionId)) || "[]");
|
|
1297
|
+
return Array.isArray(parsed) ? parsed.filter((item) => item?.id && item?.content) : [];
|
|
1298
|
+
} catch {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function saveAfterFinishQueue(sessionId = currentSessionId) {
|
|
1304
|
+
if (!sessionId) return;
|
|
1305
|
+
localStorage.setItem(afterFinishQueueKey(sessionId), JSON.stringify(pendingAfterFinishItems));
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function newLocalQueueId() {
|
|
1309
|
+
if (globalThis.crypto?.randomUUID) return `after-${globalThis.crypto.randomUUID()}`;
|
|
1310
|
+
return `after-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function compactMessage(content, limit = 220) {
|
|
1314
|
+
const text = String(content || "").replace(/\s+/g, " ").trim();
|
|
1315
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}...` : text;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function renderPendingMessages() {
|
|
1319
|
+
if (!chatPendingEl) return;
|
|
1320
|
+
const inboxItems = pendingInboxItems || [];
|
|
1321
|
+
const afterItems = pendingAfterFinishItems || [];
|
|
1322
|
+
if (inboxItems.length === 0 && afterItems.length === 0) {
|
|
1323
|
+
chatPendingEl.hidden = true;
|
|
1324
|
+
chatPendingEl.innerHTML = "";
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const section = (label, items, kind) => {
|
|
1329
|
+
if (!items.length) return "";
|
|
1330
|
+
return `
|
|
1331
|
+
<div class="pending-section">
|
|
1332
|
+
<div class="pending-label">${escapeHtml(label)}</div>
|
|
1333
|
+
${items
|
|
1334
|
+
.map(
|
|
1335
|
+
(item) => `
|
|
1336
|
+
<div class="pending-item">
|
|
1337
|
+
<div class="pending-copy">
|
|
1338
|
+
<span class="pending-kind ${kind}">${kind === "asap" ? "→" : "↳"}</span>
|
|
1339
|
+
<span>${escapeHtml(compactMessage(item.content))}</span>
|
|
1340
|
+
</div>
|
|
1341
|
+
<div class="pending-actions">
|
|
1342
|
+
<button type="button" class="mini-button" data-edit-${kind}="${escapeHtml(item.id)}">${t(
|
|
1343
|
+
"editQueuedMessage"
|
|
1344
|
+
)}</button>
|
|
1345
|
+
<button type="button" class="mini-button danger" data-remove-${kind}="${escapeHtml(item.id)}">${t(
|
|
1346
|
+
"removeQueuedMessage"
|
|
1347
|
+
)}</button>
|
|
1348
|
+
</div>
|
|
1349
|
+
</div>
|
|
1350
|
+
`
|
|
1351
|
+
)
|
|
1352
|
+
.join("")}
|
|
1353
|
+
</div>
|
|
1354
|
+
`;
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
chatPendingEl.hidden = false;
|
|
1358
|
+
chatPendingEl.innerHTML = `
|
|
1359
|
+
<div class="pending-header">
|
|
1360
|
+
<strong>${t("pendingMessagesTitle")}</strong>
|
|
1361
|
+
<span>${t("queuedConsumedHint")}</span>
|
|
1362
|
+
</div>
|
|
1363
|
+
${section(t("asapQueueLabel"), inboxItems, "asap")}
|
|
1364
|
+
${section(t("afterFinishQueueLabel"), afterItems, "after")}
|
|
1365
|
+
`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function refreshInbox() {
|
|
1369
|
+
if (!currentSessionId) {
|
|
1370
|
+
pendingInboxItems = [];
|
|
1371
|
+
pendingAfterFinishItems = [];
|
|
1372
|
+
renderPendingMessages();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
pendingAfterFinishItems = loadAfterFinishQueue(currentSessionId);
|
|
1377
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox`);
|
|
1378
|
+
if (!response.ok) {
|
|
1379
|
+
pendingInboxItems = [];
|
|
1380
|
+
renderPendingMessages();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const data = await response.json();
|
|
1384
|
+
pendingInboxItems = data.items || [];
|
|
1385
|
+
renderPendingMessages();
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function updateInboxItem(itemId, content) {
|
|
1389
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox/${encodeURIComponent(itemId)}`, {
|
|
1390
|
+
method: "PATCH",
|
|
1391
|
+
headers: { "Content-Type": "application/json" },
|
|
1392
|
+
body: JSON.stringify({ content }),
|
|
1393
|
+
});
|
|
1394
|
+
const data = await response.json().catch(() => ({}));
|
|
1395
|
+
if (!response.ok) throw new Error(data.error || t("failedContinue"));
|
|
1396
|
+
await refreshInbox();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function removeInboxItem(itemId) {
|
|
1400
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox/${encodeURIComponent(itemId)}`, {
|
|
1401
|
+
method: "DELETE",
|
|
1402
|
+
});
|
|
1403
|
+
const data = await response.json().catch(() => ({}));
|
|
1404
|
+
if (!response.ok) throw new Error(data.error || t("failedContinue"));
|
|
1405
|
+
await refreshInbox();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function editAfterFinishItem(itemId) {
|
|
1409
|
+
const item = pendingAfterFinishItems.find((candidate) => candidate.id === itemId);
|
|
1410
|
+
if (!item) return;
|
|
1411
|
+
pendingAfterFinishItems = pendingAfterFinishItems.filter((candidate) => candidate.id !== itemId);
|
|
1412
|
+
saveAfterFinishQueue();
|
|
1413
|
+
chatInputEl.value = item.content;
|
|
1414
|
+
chatInputEl.focus();
|
|
1415
|
+
renderPendingMessages();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function removeAfterFinishItem(itemId) {
|
|
1419
|
+
pendingAfterFinishItems = pendingAfterFinishItems.filter((candidate) => candidate.id !== itemId);
|
|
1420
|
+
saveAfterFinishQueue();
|
|
1421
|
+
renderPendingMessages();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function queueAfterFinishFromInput() {
|
|
1425
|
+
if (!currentSessionId) {
|
|
1426
|
+
chatStatusEl.textContent = t("selectSessionFirst");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const content = chatInputEl.value.trim();
|
|
1430
|
+
if (!content) {
|
|
1431
|
+
chatStatusEl.textContent = t("messageRequired");
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
pendingAfterFinishItems = [
|
|
1435
|
+
...pendingAfterFinishItems,
|
|
1436
|
+
{
|
|
1437
|
+
id: newLocalQueueId(),
|
|
1438
|
+
content,
|
|
1439
|
+
timestamp: new Date().toISOString(),
|
|
1440
|
+
source: "web",
|
|
1441
|
+
},
|
|
1442
|
+
];
|
|
1443
|
+
saveAfterFinishQueue();
|
|
1444
|
+
chatInputEl.value = "";
|
|
1445
|
+
chatStatusEl.textContent = t("queuedAfterFinishStatus");
|
|
1446
|
+
renderPendingMessages();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async function pipeMessageFromInput() {
|
|
1450
|
+
if (!currentSessionId) {
|
|
1451
|
+
chatStatusEl.textContent = t("selectSessionFirst");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const content = chatInputEl.value.trim();
|
|
1455
|
+
if (!content) {
|
|
1456
|
+
chatStatusEl.textContent = t("messageRequired");
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
chatStatusEl.textContent = t("sendingStatus");
|
|
1461
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox`, {
|
|
1462
|
+
method: "POST",
|
|
1463
|
+
headers: { "Content-Type": "application/json" },
|
|
1464
|
+
body: JSON.stringify({ content, priority: "asap" }),
|
|
1465
|
+
});
|
|
1466
|
+
const data = await response.json().catch(() => ({}));
|
|
1467
|
+
if (!response.ok) {
|
|
1468
|
+
chatStatusEl.textContent = data.error || t("failedContinue");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
chatInputEl.value = "";
|
|
1472
|
+
chatStatusEl.textContent = t("queuedAsapStatus");
|
|
1473
|
+
pendingInboxItems = data.item ? [...pendingInboxItems, data.item] : pendingInboxItems;
|
|
1474
|
+
renderPendingMessages();
|
|
1475
|
+
await refreshInbox();
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function flushAfterFinishQueue() {
|
|
1479
|
+
if (!currentSessionId || flushingAfterFinish || currentRunStatus === "running" || pendingAfterFinishItems.length === 0) return;
|
|
1480
|
+
flushingAfterFinish = true;
|
|
1481
|
+
const next = pendingAfterFinishItems[0];
|
|
1482
|
+
pendingAfterFinishItems = pendingAfterFinishItems.slice(1);
|
|
1483
|
+
saveAfterFinishQueue();
|
|
1484
|
+
renderPendingMessages();
|
|
1485
|
+
|
|
1486
|
+
try {
|
|
1487
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/messages`, {
|
|
1488
|
+
method: "POST",
|
|
1489
|
+
headers: { "Content-Type": "application/json" },
|
|
1490
|
+
body: JSON.stringify({
|
|
1491
|
+
...formPayload(),
|
|
1492
|
+
content: next.content,
|
|
1493
|
+
}),
|
|
1494
|
+
});
|
|
1495
|
+
const data = await response.json().catch(() => ({}));
|
|
1496
|
+
if (!response.ok) throw new Error(data.error || t("failedContinue"));
|
|
1497
|
+
currentSessionId = data.sessionId;
|
|
1498
|
+
currentRunStatus = data.queued ? currentRunStatus : "running";
|
|
1499
|
+
updateStopRunButton();
|
|
1500
|
+
chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
|
|
1501
|
+
await refreshSessions();
|
|
1502
|
+
await refreshChat();
|
|
1503
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1504
|
+
pollTimer = setInterval(refreshRun, 1500);
|
|
1505
|
+
await refreshRun();
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
pendingAfterFinishItems = [next, ...pendingAfterFinishItems];
|
|
1508
|
+
saveAfterFinishQueue();
|
|
1509
|
+
renderPendingMessages();
|
|
1510
|
+
chatStatusEl.textContent = error instanceof Error ? error.message : String(error);
|
|
1511
|
+
} finally {
|
|
1512
|
+
flushingAfterFinish = false;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1273
1516
|
function artifactReadKey(sessionId = currentSessionId) {
|
|
1274
1517
|
return `agintiflow.artifacts.readIds.${sessionId || "none"}`;
|
|
1275
1518
|
}
|
|
@@ -1682,6 +1925,7 @@ async function refreshRun() {
|
|
|
1682
1925
|
runMetaEl.textContent = `${run.status} · ${run.sessionId}`;
|
|
1683
1926
|
renderLogs(run);
|
|
1684
1927
|
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
1928
|
+
await refreshInbox();
|
|
1685
1929
|
|
|
1686
1930
|
if (run.status === "finished" || run.status === "failed" || run.status === "stopped") {
|
|
1687
1931
|
clearInterval(pollTimer);
|
|
@@ -1690,6 +1934,7 @@ async function refreshRun() {
|
|
|
1690
1934
|
await refreshSessions();
|
|
1691
1935
|
await refreshWorkspaceChanges();
|
|
1692
1936
|
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
1937
|
+
if (run.status === "finished") await flushAfterFinishQueue();
|
|
1693
1938
|
}
|
|
1694
1939
|
}
|
|
1695
1940
|
|
|
@@ -1711,6 +1956,9 @@ async function stopCurrentRun() {
|
|
|
1711
1956
|
|
|
1712
1957
|
async function refreshChat() {
|
|
1713
1958
|
if (!currentSessionId) {
|
|
1959
|
+
pendingInboxItems = [];
|
|
1960
|
+
pendingAfterFinishItems = [];
|
|
1961
|
+
renderPendingMessages();
|
|
1714
1962
|
renderChat([]);
|
|
1715
1963
|
return;
|
|
1716
1964
|
}
|
|
@@ -1722,6 +1970,9 @@ async function refreshChat() {
|
|
|
1722
1970
|
}
|
|
1723
1971
|
|
|
1724
1972
|
const data = await response.json();
|
|
1973
|
+
pendingInboxItems = data.inbox || [];
|
|
1974
|
+
pendingAfterFinishItems = loadAfterFinishQueue(currentSessionId);
|
|
1975
|
+
renderPendingMessages();
|
|
1725
1976
|
renderChat(data.chat || []);
|
|
1726
1977
|
}
|
|
1727
1978
|
|
|
@@ -2019,24 +2270,54 @@ form.addEventListener("input", schedulePreferenceSave);
|
|
|
2019
2270
|
form.addEventListener("change", schedulePreferenceSave);
|
|
2020
2271
|
|
|
2021
2272
|
chatInputEl.addEventListener("keydown", (event) => {
|
|
2022
|
-
if (event.key === "
|
|
2273
|
+
if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
2023
2274
|
event.preventDefault();
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2275
|
+
chatFormEl.requestSubmit();
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
pipeMessageButton?.addEventListener("click", () => {
|
|
2280
|
+
pipeMessageFromInput().catch((error) => {
|
|
2281
|
+
chatStatusEl.textContent = String(error);
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
queueAfterFinishButton?.addEventListener("click", queueAfterFinishFromInput);
|
|
2286
|
+
|
|
2287
|
+
chatPendingEl?.addEventListener("click", (event) => {
|
|
2288
|
+
const target = event.target.closest("button");
|
|
2289
|
+
if (!target) return;
|
|
2290
|
+
|
|
2291
|
+
const editAsap = target.dataset.editAsap;
|
|
2292
|
+
const removeAsap = target.dataset.removeAsap;
|
|
2293
|
+
const editAfter = target.dataset.editAfter;
|
|
2294
|
+
const removeAfter = target.dataset.removeAfter;
|
|
2295
|
+
|
|
2296
|
+
if (editAsap) {
|
|
2297
|
+
const item = pendingInboxItems.find((candidate) => candidate.id === editAsap);
|
|
2298
|
+
const next = prompt(t("editQueuedMessage"), item?.content || "");
|
|
2299
|
+
if (next !== null) {
|
|
2300
|
+
updateInboxItem(editAsap, next).catch((error) => {
|
|
2301
|
+
chatStatusEl.textContent = String(error);
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2028
2304
|
return;
|
|
2029
2305
|
}
|
|
2030
2306
|
|
|
2031
|
-
if (
|
|
2032
|
-
|
|
2033
|
-
|
|
2307
|
+
if (removeAsap) {
|
|
2308
|
+
removeInboxItem(removeAsap).catch((error) => {
|
|
2309
|
+
chatStatusEl.textContent = String(error);
|
|
2310
|
+
});
|
|
2034
2311
|
return;
|
|
2035
2312
|
}
|
|
2036
2313
|
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
|
|
2314
|
+
if (editAfter) {
|
|
2315
|
+
editAfterFinishItem(editAfter);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (removeAfter) {
|
|
2320
|
+
removeAfterFinishItem(removeAfter);
|
|
2040
2321
|
}
|
|
2041
2322
|
});
|
|
2042
2323
|
|
|
@@ -2108,10 +2389,13 @@ sessionSelectEl.addEventListener("change", async () => {
|
|
|
2108
2389
|
currentSessionId = sessionSelectEl.value;
|
|
2109
2390
|
if (!currentSessionId) {
|
|
2110
2391
|
currentRunStatus = "";
|
|
2392
|
+
pendingInboxItems = [];
|
|
2393
|
+
pendingAfterFinishItems = [];
|
|
2111
2394
|
updateStopRunButton();
|
|
2112
2395
|
runMetaEl.textContent = "";
|
|
2113
2396
|
setLogs(t("noRunSelected"), "empty");
|
|
2114
2397
|
renderChat([]);
|
|
2398
|
+
renderPendingMessages();
|
|
2115
2399
|
artifactItems = [];
|
|
2116
2400
|
selectedArtifactId = "";
|
|
2117
2401
|
renderArtifactShell();
|
|
@@ -2158,18 +2442,8 @@ chatFormEl.addEventListener("submit", async (event) => {
|
|
|
2158
2442
|
updateStopRunButton();
|
|
2159
2443
|
chatInputEl.value = "";
|
|
2160
2444
|
chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
|
|
2161
|
-
if (data.queued) {
|
|
2162
|
-
renderChat([
|
|
2163
|
-
...lastChatEntries,
|
|
2164
|
-
{
|
|
2165
|
-
role: "user",
|
|
2166
|
-
content,
|
|
2167
|
-
at: new Date().toISOString(),
|
|
2168
|
-
},
|
|
2169
|
-
]);
|
|
2170
|
-
}
|
|
2171
2445
|
await refreshSessions();
|
|
2172
|
-
|
|
2446
|
+
await refreshChat();
|
|
2173
2447
|
await refreshWorkspaceChanges();
|
|
2174
2448
|
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
2175
2449
|
|
package/public/index.html
CHANGED
|
@@ -313,10 +313,15 @@
|
|
|
313
313
|
placeholder="Continue the selected conversation here."
|
|
314
314
|
></textarea>
|
|
315
315
|
<p class="chat-shortcuts subtle" data-i18n="chatShortcutHelp">
|
|
316
|
-
Enter sends ·
|
|
316
|
+
Enter sends · Shift+Enter adds a newline · use buttons for pipe/queue control
|
|
317
317
|
</p>
|
|
318
|
+
<div id="chat-pending" class="chat-pending" hidden></div>
|
|
318
319
|
<div class="actions">
|
|
319
320
|
<button type="submit" data-i18n="sendMessageButton">Send message</button>
|
|
321
|
+
<button id="pipe-message" type="button" class="secondary" data-i18n="pipeMessageButton">Pipe to run</button>
|
|
322
|
+
<button id="queue-after-finish" type="button" class="secondary" data-i18n="queueAfterFinishButton">
|
|
323
|
+
Queue after finish
|
|
324
|
+
</button>
|
|
320
325
|
<span id="chat-status" class="subtle"></span>
|
|
321
326
|
</div>
|
|
322
327
|
</form>
|
package/public/styles.css
CHANGED
|
@@ -742,6 +742,108 @@ button.danger {
|
|
|
742
742
|
line-height: 1.35;
|
|
743
743
|
}
|
|
744
744
|
|
|
745
|
+
.chat-pending {
|
|
746
|
+
display: grid;
|
|
747
|
+
gap: 10px;
|
|
748
|
+
padding: 10px;
|
|
749
|
+
border: 1px solid rgba(15, 118, 110, 0.16);
|
|
750
|
+
border-radius: 14px;
|
|
751
|
+
background:
|
|
752
|
+
linear-gradient(135deg, rgba(240, 253, 250, 0.9), rgba(255, 251, 235, 0.72)),
|
|
753
|
+
rgba(255, 255, 255, 0.78);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.pending-header {
|
|
757
|
+
display: flex;
|
|
758
|
+
flex-wrap: wrap;
|
|
759
|
+
gap: 8px 12px;
|
|
760
|
+
align-items: baseline;
|
|
761
|
+
justify-content: space-between;
|
|
762
|
+
color: var(--muted);
|
|
763
|
+
font-size: 0.78rem;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.pending-header strong,
|
|
767
|
+
.pending-label {
|
|
768
|
+
color: var(--ink);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.pending-section {
|
|
772
|
+
display: grid;
|
|
773
|
+
gap: 7px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.pending-label {
|
|
777
|
+
font-size: 0.74rem;
|
|
778
|
+
font-weight: 800;
|
|
779
|
+
letter-spacing: 0.06em;
|
|
780
|
+
text-transform: uppercase;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.pending-item {
|
|
784
|
+
display: grid;
|
|
785
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
786
|
+
gap: 8px;
|
|
787
|
+
align-items: center;
|
|
788
|
+
padding: 8px 10px;
|
|
789
|
+
border: 1px solid rgba(15, 118, 110, 0.12);
|
|
790
|
+
border-radius: 12px;
|
|
791
|
+
background: rgba(255, 255, 255, 0.82);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.pending-copy {
|
|
795
|
+
min-width: 0;
|
|
796
|
+
display: flex;
|
|
797
|
+
gap: 8px;
|
|
798
|
+
align-items: flex-start;
|
|
799
|
+
color: var(--ink);
|
|
800
|
+
font-size: 0.86rem;
|
|
801
|
+
line-height: 1.35;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.pending-kind {
|
|
805
|
+
flex: 0 0 auto;
|
|
806
|
+
display: inline-grid;
|
|
807
|
+
width: 20px;
|
|
808
|
+
height: 20px;
|
|
809
|
+
place-items: center;
|
|
810
|
+
border-radius: 999px;
|
|
811
|
+
font-size: 0.82rem;
|
|
812
|
+
font-weight: 900;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.pending-kind.asap {
|
|
816
|
+
color: #075985;
|
|
817
|
+
background: rgba(14, 165, 233, 0.14);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.pending-kind.after {
|
|
821
|
+
color: #92400e;
|
|
822
|
+
background: rgba(245, 158, 11, 0.16);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.pending-actions {
|
|
826
|
+
display: flex;
|
|
827
|
+
flex-wrap: wrap;
|
|
828
|
+
gap: 6px;
|
|
829
|
+
justify-content: flex-end;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.mini-button {
|
|
833
|
+
min-height: auto;
|
|
834
|
+
padding: 5px 8px;
|
|
835
|
+
border-radius: 9px;
|
|
836
|
+
font-size: 0.76rem;
|
|
837
|
+
background: rgba(15, 118, 110, 0.08);
|
|
838
|
+
color: #0f766e;
|
|
839
|
+
box-shadow: none;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.mini-button.danger {
|
|
843
|
+
color: #b91c1c;
|
|
844
|
+
background: rgba(239, 68, 68, 0.08);
|
|
845
|
+
}
|
|
846
|
+
|
|
745
847
|
.modal {
|
|
746
848
|
width: min(720px, calc(100% - 28px));
|
|
747
849
|
max-width: 100%;
|
|
@@ -1152,6 +1254,14 @@ button.danger {
|
|
|
1152
1254
|
grid-template-columns: 1fr;
|
|
1153
1255
|
}
|
|
1154
1256
|
|
|
1257
|
+
.pending-item {
|
|
1258
|
+
grid-template-columns: 1fr;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.pending-actions {
|
|
1262
|
+
justify-content: flex-start;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1155
1265
|
.panel {
|
|
1156
1266
|
padding: 15px;
|
|
1157
1267
|
}
|
|
@@ -69,6 +69,22 @@ try {
|
|
|
69
69
|
if (!renderedMarkdown.includes("Check") || !renderedMarkdown.includes("Present")) {
|
|
70
70
|
throw new Error("terminal markdown renderer dropped table content");
|
|
71
71
|
}
|
|
72
|
+
if ((renderedMarkdown.match(/Present/g) || []).length !== 1) {
|
|
73
|
+
throw new Error("terminal markdown renderer duplicated table rows");
|
|
74
|
+
}
|
|
75
|
+
const renderedDiff = stripMarkdown(
|
|
76
|
+
[
|
|
77
|
+
"Diff:",
|
|
78
|
+
"--- a/example.txt",
|
|
79
|
+
"+++ b/example.txt",
|
|
80
|
+
"@@ line 1 @@",
|
|
81
|
+
"-old",
|
|
82
|
+
"+new",
|
|
83
|
+
].join("\n")
|
|
84
|
+
);
|
|
85
|
+
if (!renderedDiff.includes("-old") || !renderedDiff.includes("+new")) {
|
|
86
|
+
throw new Error("terminal markdown renderer dropped patch diff lines");
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
const promptLayout = buildPromptLayout(`${"x".repeat(180)}\nsecond line`, 95, 80, 24);
|
|
74
90
|
const visibleLengths = promptLayout.renderedRows.map((line) =>
|
|
@@ -86,13 +102,14 @@ try {
|
|
|
86
102
|
}
|
|
87
103
|
const queuedPromptLayout = buildPromptLayout("follow up", 9, 90, 24, {
|
|
88
104
|
commandCwd: "/tmp/aginti-project",
|
|
105
|
+
statusLine: "running · tool: apply_patch with a very long request that should be compacted in the panel",
|
|
89
106
|
pendingAsap: [{ content: "apply this to the running task" }],
|
|
90
107
|
pendingQueued: [{ content: "run this after the current task" }],
|
|
91
108
|
});
|
|
92
109
|
const queuedText = queuedPromptLayout.renderedRows
|
|
93
110
|
.map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""))
|
|
94
111
|
.join("\n");
|
|
95
|
-
if (!queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
|
|
112
|
+
if (!queuedText.includes("run running · tool: apply_patch") || !queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
|
|
96
113
|
throw new Error("terminal prompt layout did not render live input queue and cwd footer");
|
|
97
114
|
}
|
|
98
115
|
|
|
@@ -119,8 +136,8 @@ try {
|
|
|
119
136
|
if (!result.stdout.includes("Interactive agent chat")) {
|
|
120
137
|
throw new Error("interactive chat did not print its banner");
|
|
121
138
|
}
|
|
122
|
-
if (!result.stdout.includes("status=
|
|
123
|
-
throw new Error("interactive chat did not print
|
|
139
|
+
if (!result.stdout.includes("status=idle session=")) {
|
|
140
|
+
throw new Error("interactive chat did not print final run status");
|
|
124
141
|
}
|
|
125
142
|
if (!/aginti>\s*\r?\n\s*\|\s+Mock run complete\./.test(result.stdout)) {
|
|
126
143
|
throw new Error("assistant response did not render with a fresh-line response gutter");
|
|
@@ -139,7 +156,7 @@ try {
|
|
|
139
156
|
{
|
|
140
157
|
ok: true,
|
|
141
158
|
projectRoot: tempRoot,
|
|
142
|
-
checks: ["markdown-render", "prompt-layout", "live-input-layout", "agent-response-gutter", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-preview"],
|
|
159
|
+
checks: ["markdown-render", "markdown-table-no-duplicate", "patch-diff-render", "prompt-layout", "live-input-status-layout", "agent-response-gutter", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-preview"],
|
|
143
160
|
},
|
|
144
161
|
null,
|
|
145
162
|
2
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { SessionStore } from "../src/session-store.js";
|
|
7
8
|
|
|
8
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-api-smoke-"));
|
|
@@ -165,6 +166,38 @@ try {
|
|
|
165
166
|
|
|
166
167
|
const chat = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/chat`);
|
|
167
168
|
if (!Array.isArray(chat.chat) || chat.chat.length < 2) throw new Error("chat history was not persisted");
|
|
169
|
+
if (!Array.isArray(chat.inbox)) throw new Error("chat endpoint did not include shared inbox state");
|
|
170
|
+
|
|
171
|
+
const queued = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/inbox`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ content: "continue from web inbox smoke", priority: "asap" }),
|
|
175
|
+
});
|
|
176
|
+
if (!queued.item?.id || queued.item.priority !== "asap") throw new Error("inbox queue endpoint did not return an ASAP item");
|
|
177
|
+
|
|
178
|
+
const inbox = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/inbox`);
|
|
179
|
+
if (!inbox.items?.some((item) => item.id === queued.item.id)) throw new Error("inbox endpoint did not list queued item");
|
|
180
|
+
|
|
181
|
+
const edited = await fetchJson(
|
|
182
|
+
`/api/sessions/${encodeURIComponent(runStart.sessionId)}/inbox/${encodeURIComponent(queued.item.id)}`,
|
|
183
|
+
{
|
|
184
|
+
method: "PATCH",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify({ content: "edited web inbox smoke" }),
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
if (!edited.item?.content.includes("edited web inbox smoke")) throw new Error("inbox edit endpoint did not persist content");
|
|
190
|
+
|
|
191
|
+
const deletedInbox = await fetchJson(
|
|
192
|
+
`/api/sessions/${encodeURIComponent(runStart.sessionId)}/inbox/${encodeURIComponent(queued.item.id)}`,
|
|
193
|
+
{
|
|
194
|
+
method: "DELETE",
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
if (!deletedInbox.ok) throw new Error("inbox delete endpoint failed");
|
|
198
|
+
const inboxStore = new SessionStore(path.join(runtimeDir, ".sessions"), runStart.sessionId);
|
|
199
|
+
const remainingInbox = await inboxStore.loadInbox();
|
|
200
|
+
if (remainingInbox.some((item) => item.id === queued.item.id)) throw new Error("inbox delete endpoint left item on disk");
|
|
168
201
|
|
|
169
202
|
const renamed = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}`, {
|
|
170
203
|
method: "PATCH",
|
|
@@ -248,6 +281,10 @@ try {
|
|
|
248
281
|
"/api/sandbox/preflight",
|
|
249
282
|
"/api/runs",
|
|
250
283
|
"/api/sessions/:id/chat",
|
|
284
|
+
"/api/sessions/:id/inbox",
|
|
285
|
+
"POST /api/sessions/:id/inbox",
|
|
286
|
+
"PATCH /api/sessions/:id/inbox/:itemId",
|
|
287
|
+
"DELETE /api/sessions/:id/inbox/:itemId",
|
|
251
288
|
"PATCH /api/sessions/:id",
|
|
252
289
|
"POST /api/sessions/:id/auto-title",
|
|
253
290
|
"DELETE /api/sessions/:id",
|
package/src/interactive-cli.js
CHANGED
|
@@ -28,6 +28,7 @@ const ansi = {
|
|
|
28
28
|
userBg: "\x1b[48;5;24m\x1b[38;5;231m",
|
|
29
29
|
agentBg: "\x1b[48;5;29m\x1b[38;5;231m",
|
|
30
30
|
responseBg: "\x1b[48;5;25m\x1b[38;5;231m",
|
|
31
|
+
statusBg: "\x1b[48;5;23m\x1b[38;5;231m",
|
|
31
32
|
systemBg: "\x1b[48;5;236m\x1b[38;5;245m",
|
|
32
33
|
};
|
|
33
34
|
const brandPalette = ["\x1b[38;5;45m", "\x1b[38;5;81m", "\x1b[38;5;86m", "\x1b[38;5;118m", "\x1b[38;5;226m"];
|
|
@@ -152,6 +153,7 @@ function compactLine(value = "", limit = 96) {
|
|
|
152
153
|
export function stripMarkdown(text) {
|
|
153
154
|
const lines = String(text || "").split(/\r?\n/);
|
|
154
155
|
let inFence = false;
|
|
156
|
+
let diffContext = 0;
|
|
155
157
|
const rendered = [];
|
|
156
158
|
|
|
157
159
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -166,6 +168,15 @@ export function stripMarkdown(text) {
|
|
|
166
168
|
continue;
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
if (/^\s*Diff:\s*$/i.test(line)) diffContext = 80;
|
|
172
|
+
const patchLine = renderPatchLine(line, { active: diffContext > 0 || inFence });
|
|
173
|
+
if (patchLine) {
|
|
174
|
+
rendered.push(patchLine);
|
|
175
|
+
diffContext = 80;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (diffContext > 0) diffContext -= 1;
|
|
179
|
+
|
|
169
180
|
if (!inFence) {
|
|
170
181
|
if (/^\s*[-*_]{3,}\s*$/.test(line)) {
|
|
171
182
|
rendered.push(color("-".repeat(42), ansi.dim));
|
|
@@ -203,6 +214,17 @@ export function stripMarkdown(text) {
|
|
|
203
214
|
return rendered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
204
215
|
}
|
|
205
216
|
|
|
217
|
+
function renderPatchLine(line = "", { active = false } = {}) {
|
|
218
|
+
const value = String(line || "");
|
|
219
|
+
if (/^diff --git\s+/.test(value)) return color(value, ansi.bold, ansi.cyan);
|
|
220
|
+
if (/^@@\s+/.test(value)) return color(value, ansi.cyan);
|
|
221
|
+
if (/^---\s+a\//.test(value)) return color(value, ansi.red);
|
|
222
|
+
if (/^\+\+\+\s+b\//.test(value)) return color(value, ansi.green);
|
|
223
|
+
if (active && /^\+(?!\+\+)/.test(value)) return color(value, ansi.green);
|
|
224
|
+
if (active && /^-(?!--)/.test(value)) return color(value, ansi.red);
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
function splitMarkdownTableRow(line = "") {
|
|
207
229
|
const trimmed = String(line || "").trim();
|
|
208
230
|
if (!trimmed.includes("|")) return null;
|
|
@@ -519,6 +541,11 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
|
|
|
519
541
|
const renderedRows = [];
|
|
520
542
|
let renderedCursorRow = cursorRow - visible.start;
|
|
521
543
|
|
|
544
|
+
if (options.statusLine) {
|
|
545
|
+
renderedRows.push(panelLine(` run ${compactLine(options.statusLine, lineWidth - 8)}`, ansi.statusBg, lineWidth));
|
|
546
|
+
renderedCursorRow += 1;
|
|
547
|
+
}
|
|
548
|
+
|
|
522
549
|
const pendingAsap = Array.isArray(options.pendingAsap) ? options.pendingAsap : [];
|
|
523
550
|
const pendingQueued = Array.isArray(options.pendingQueued) ? options.pendingQueued : [];
|
|
524
551
|
for (const item of pendingAsap.slice(-4)) {
|
|
@@ -887,6 +914,7 @@ class LiveRunInput {
|
|
|
887
914
|
this.preferredColumn = null;
|
|
888
915
|
this.pendingAsap = [];
|
|
889
916
|
this.pendingQueued = [];
|
|
917
|
+
this.statusLine = "";
|
|
890
918
|
this.wasRaw = Boolean(input.isRaw);
|
|
891
919
|
this.started = false;
|
|
892
920
|
this.handler = this.handleKey.bind(this);
|
|
@@ -952,6 +980,7 @@ class LiveRunInput {
|
|
|
952
980
|
}
|
|
953
981
|
this.rendered = renderPromptBuffer(this.buffer, this.cursor, this.rendered, {
|
|
954
982
|
commandCwd: this.commandCwd,
|
|
983
|
+
statusLine: this.statusLine,
|
|
955
984
|
pendingAsap: this.pendingAsap,
|
|
956
985
|
pendingQueued: this.pendingQueued,
|
|
957
986
|
});
|
|
@@ -972,9 +1001,17 @@ class LiveRunInput {
|
|
|
972
1001
|
this.redraw();
|
|
973
1002
|
}
|
|
974
1003
|
|
|
1004
|
+
setStatus(value = "") {
|
|
1005
|
+
const nextStatus = compactLine(value, Math.max(terminalWidth() - 16, 36));
|
|
1006
|
+
if (this.statusLine === nextStatus) return;
|
|
1007
|
+
this.statusLine = nextStatus;
|
|
1008
|
+
this.redraw();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
975
1011
|
moveVertical(direction) {
|
|
976
1012
|
const layout = buildPromptLayout(this.buffer, this.cursor, terminalWidth(), terminalHeight(), {
|
|
977
1013
|
commandCwd: this.commandCwd,
|
|
1014
|
+
statusLine: this.statusLine,
|
|
978
1015
|
pendingAsap: this.pendingAsap,
|
|
979
1016
|
pendingQueued: this.pendingQueued,
|
|
980
1017
|
});
|
|
@@ -1215,8 +1252,14 @@ async function printResumeHistory(state, { limit = 8 } = {}) {
|
|
|
1215
1252
|
}
|
|
1216
1253
|
|
|
1217
1254
|
function printStatusEvent(state, label, details = "") {
|
|
1218
|
-
|
|
1219
|
-
|
|
1255
|
+
const safeDetails = compactLine(details, 72);
|
|
1256
|
+
state.lastEvent = safeDetails ? `${label}: ${safeDetails}` : label;
|
|
1257
|
+
const statusText = `${state.status || "running"} · ${state.lastEvent}`;
|
|
1258
|
+
if (activeRunInput) {
|
|
1259
|
+
activeRunInput.setStatus(statusText);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
printSystemLine(`status=${statusText}`);
|
|
1220
1263
|
}
|
|
1221
1264
|
|
|
1222
1265
|
function attachRunInterrupts(controller) {
|
|
@@ -1595,15 +1638,19 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1595
1638
|
|
|
1596
1639
|
state.sessionId = config.resume || config.sessionId || state.sessionId;
|
|
1597
1640
|
state.status = "running";
|
|
1598
|
-
state.activeGoal = prompt
|
|
1641
|
+
state.activeGoal = compactLine(prompt, 84);
|
|
1599
1642
|
state.lastEvent = "";
|
|
1600
|
-
printSystemLine(`session=${state.sessionId}`);
|
|
1601
|
-
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
1602
1643
|
|
|
1603
1644
|
const store = new SessionStore(config.sessionsDir, state.sessionId);
|
|
1604
1645
|
const liveInput = new LiveRunInput({ state, store, controller });
|
|
1605
1646
|
const liveStarted = liveInput.start();
|
|
1606
1647
|
const detachInterrupts = liveStarted ? () => {} : attachRunInterrupts(controller);
|
|
1648
|
+
if (liveStarted) {
|
|
1649
|
+
liveInput.setStatus(`running · ${state.activeGoal}`);
|
|
1650
|
+
} else {
|
|
1651
|
+
printSystemLine(`session=${state.sessionId}`);
|
|
1652
|
+
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
1653
|
+
}
|
|
1607
1654
|
let result;
|
|
1608
1655
|
let queuedAfterFinish = [];
|
|
1609
1656
|
try {
|
|
@@ -1619,6 +1666,9 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1619
1666
|
printHeading(text);
|
|
1620
1667
|
} else if (options.error) {
|
|
1621
1668
|
outputLine(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
|
|
1669
|
+
} else if (options.kind === "meta" && liveStarted) {
|
|
1670
|
+
const trimmed = String(text || "").trim();
|
|
1671
|
+
if (trimmed) liveInput.setStatus(trimmed);
|
|
1622
1672
|
} else {
|
|
1623
1673
|
printSystemLine(text);
|
|
1624
1674
|
}
|
package/web.js
CHANGED
|
@@ -349,16 +349,49 @@ async function loadChat(sessionId) {
|
|
|
349
349
|
|
|
350
350
|
if (!state && !meta) return null;
|
|
351
351
|
|
|
352
|
+
const inbox = await store.loadInbox().catch(() => []);
|
|
352
353
|
return {
|
|
353
354
|
sessionId,
|
|
354
355
|
goal: state?.goal || meta?.goal || "",
|
|
355
356
|
title: meta?.title || "",
|
|
356
357
|
provider: state?.provider || meta?.provider || "",
|
|
357
358
|
model: state?.model || meta?.model || "",
|
|
359
|
+
status: meta?.status || "",
|
|
360
|
+
inbox,
|
|
358
361
|
chat: deriveChatFromState(state, meta),
|
|
359
362
|
};
|
|
360
363
|
}
|
|
361
364
|
|
|
365
|
+
function pushActiveInboxLog(sessionId, message, data = {}) {
|
|
366
|
+
const active = runs.get(sessionId);
|
|
367
|
+
if (active?.status !== "running") return;
|
|
368
|
+
active.logs.push({
|
|
369
|
+
at: new Date().toISOString(),
|
|
370
|
+
kind: "event",
|
|
371
|
+
message,
|
|
372
|
+
data,
|
|
373
|
+
});
|
|
374
|
+
active.updatedAt = new Date().toISOString();
|
|
375
|
+
db.upsertSession(active);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function loadInboxSession(sessionId) {
|
|
379
|
+
if (!isSafeSessionId(sessionId)) {
|
|
380
|
+
return { error: "Invalid session id.", status: 400 };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const data = await loadChat(sessionId);
|
|
384
|
+
if (!data) {
|
|
385
|
+
return { error: "Session not found.", status: 404 };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { store: sessionStore(sessionId), data };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function queuedMessagePreview(content) {
|
|
392
|
+
return String(content || "").replace(/\s+/g, " ").trim().slice(0, 180);
|
|
393
|
+
}
|
|
394
|
+
|
|
362
395
|
function normalizeSessionTitle(title) {
|
|
363
396
|
return String(title || "")
|
|
364
397
|
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
@@ -687,6 +720,128 @@ app.get("/api/sessions/:sessionId/chat", async (req, res) => {
|
|
|
687
720
|
res.json(data);
|
|
688
721
|
});
|
|
689
722
|
|
|
723
|
+
app.get("/api/sessions/:sessionId/inbox", async (req, res) => {
|
|
724
|
+
const loaded = await loadInboxSession(req.params.sessionId);
|
|
725
|
+
if (loaded.error) {
|
|
726
|
+
res.status(loaded.status || 500).json({ error: loaded.error });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
res.json({
|
|
731
|
+
ok: true,
|
|
732
|
+
sessionId: req.params.sessionId,
|
|
733
|
+
items: loaded.data.inbox || [],
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
app.post("/api/sessions/:sessionId/inbox", async (req, res) => {
|
|
738
|
+
const loaded = await loadInboxSession(req.params.sessionId);
|
|
739
|
+
if (loaded.error) {
|
|
740
|
+
res.status(loaded.status || 500).json({ error: loaded.error });
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const content = String(req.body?.content || "").trim();
|
|
745
|
+
if (!content) {
|
|
746
|
+
res.status(400).json({ error: "Message content is required." });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const priority = req.body?.priority === "normal" ? "normal" : "asap";
|
|
751
|
+
const item = await loaded.store.appendInbox(content, { source: "web", priority });
|
|
752
|
+
await loaded.store
|
|
753
|
+
.appendEvent("conversation.queued_input", {
|
|
754
|
+
itemId: item?.id,
|
|
755
|
+
priority,
|
|
756
|
+
source: "web",
|
|
757
|
+
preview: queuedMessagePreview(content),
|
|
758
|
+
})
|
|
759
|
+
.catch(() => {});
|
|
760
|
+
pushActiveInboxLog(req.params.sessionId, "conversation.queued_input", {
|
|
761
|
+
itemId: item?.id,
|
|
762
|
+
priority,
|
|
763
|
+
source: "web",
|
|
764
|
+
preview: queuedMessagePreview(content),
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
res.json({ ok: true, sessionId: req.params.sessionId, item });
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
app.patch("/api/sessions/:sessionId/inbox/:itemId", async (req, res) => {
|
|
771
|
+
const loaded = await loadInboxSession(req.params.sessionId);
|
|
772
|
+
if (loaded.error) {
|
|
773
|
+
res.status(loaded.status || 500).json({ error: loaded.error });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const content = String(req.body?.content || "").trim();
|
|
778
|
+
if (!content) {
|
|
779
|
+
res.status(400).json({ error: "Message content is required." });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const items = await loaded.store.loadInbox();
|
|
784
|
+
const index = items.findIndex((item) => item.id === req.params.itemId);
|
|
785
|
+
if (index < 0) {
|
|
786
|
+
res.status(404).json({ error: "Queued message not found. It may already have been consumed by the agent." });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const item = {
|
|
791
|
+
...items[index],
|
|
792
|
+
content,
|
|
793
|
+
updatedAt: new Date().toISOString(),
|
|
794
|
+
editedBy: "web",
|
|
795
|
+
};
|
|
796
|
+
items[index] = item;
|
|
797
|
+
await loaded.store.saveInbox(items);
|
|
798
|
+
await loaded.store
|
|
799
|
+
.appendEvent("conversation.queued_input_edited", {
|
|
800
|
+
itemId: item.id,
|
|
801
|
+
source: "web",
|
|
802
|
+
preview: queuedMessagePreview(content),
|
|
803
|
+
})
|
|
804
|
+
.catch(() => {});
|
|
805
|
+
pushActiveInboxLog(req.params.sessionId, "conversation.queued_input_edited", {
|
|
806
|
+
itemId: item.id,
|
|
807
|
+
source: "web",
|
|
808
|
+
preview: queuedMessagePreview(content),
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
res.json({ ok: true, sessionId: req.params.sessionId, item });
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
app.delete("/api/sessions/:sessionId/inbox/:itemId", async (req, res) => {
|
|
815
|
+
const loaded = await loadInboxSession(req.params.sessionId);
|
|
816
|
+
if (loaded.error) {
|
|
817
|
+
res.status(loaded.status || 500).json({ error: loaded.error });
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const items = await loaded.store.loadInbox();
|
|
822
|
+
const item = items.find((candidate) => candidate.id === req.params.itemId);
|
|
823
|
+
if (!item) {
|
|
824
|
+
res.status(404).json({ error: "Queued message not found. It may already have been consumed by the agent." });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
await loaded.store.saveInbox(items.filter((candidate) => candidate.id !== req.params.itemId));
|
|
829
|
+
await loaded.store
|
|
830
|
+
.appendEvent("conversation.queued_input_removed", {
|
|
831
|
+
itemId: item.id,
|
|
832
|
+
source: "web",
|
|
833
|
+
preview: queuedMessagePreview(item.content),
|
|
834
|
+
})
|
|
835
|
+
.catch(() => {});
|
|
836
|
+
pushActiveInboxLog(req.params.sessionId, "conversation.queued_input_removed", {
|
|
837
|
+
itemId: item.id,
|
|
838
|
+
source: "web",
|
|
839
|
+
preview: queuedMessagePreview(item.content),
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
res.json({ ok: true, sessionId: req.params.sessionId, itemId: item.id });
|
|
843
|
+
});
|
|
844
|
+
|
|
690
845
|
app.patch("/api/sessions/:sessionId", async (req, res) => {
|
|
691
846
|
const sessionId = req.params.sessionId;
|
|
692
847
|
if (!isSafeSessionId(sessionId)) {
|
|
@@ -795,17 +950,24 @@ app.post("/api/sessions/:sessionId/messages", async (req, res) => {
|
|
|
795
950
|
const active = runs.get(sessionId);
|
|
796
951
|
if (active?.status === "running") {
|
|
797
952
|
const store = sessionStore(sessionId);
|
|
798
|
-
await store.appendInbox(content, { source: "web" });
|
|
799
|
-
await store
|
|
953
|
+
const item = await store.appendInbox(content, { source: "web", priority: "asap" });
|
|
954
|
+
await store
|
|
955
|
+
.appendEvent("conversation.queued_input", {
|
|
956
|
+
itemId: item?.id,
|
|
957
|
+
priority: "asap",
|
|
958
|
+
source: "web",
|
|
959
|
+
preview: queuedMessagePreview(content),
|
|
960
|
+
})
|
|
961
|
+
.catch(() => {});
|
|
800
962
|
active.logs.push({
|
|
801
963
|
at: new Date().toISOString(),
|
|
802
964
|
kind: "event",
|
|
803
965
|
message: "conversation.queued_input",
|
|
804
|
-
data: {
|
|
966
|
+
data: { itemId: item?.id, priority: "asap", source: "web", preview: queuedMessagePreview(content) },
|
|
805
967
|
});
|
|
806
968
|
active.updatedAt = new Date().toISOString();
|
|
807
969
|
db.upsertSession(active);
|
|
808
|
-
res.json({ sessionId, queued: true });
|
|
970
|
+
res.json({ sessionId, queued: true, item });
|
|
809
971
|
return;
|
|
810
972
|
}
|
|
811
973
|
|