@lazyingart/agintiflow 0.12.7 → 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 +2 -1
- 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-web-api.js +37 -0
- package/web.js +166 -4
package/README.md
CHANGED
|
@@ -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
|
}
|
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/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
|
|