@lazyingart/agintiflow 0.12.7 → 0.12.9

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 CHANGED
@@ -75,7 +75,7 @@ For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSee
75
75
 
76
76
  For larger repositories, use `--profile large-codebase` or choose **Large codebase engineering** in the web UI. The web default stays **Auto**, and Auto now escalates codebase/system/debugging prompts to the same engineering loop when needed. Complex work routes to DeepSeek v4 pro, starts with `inspect_project`, then uses search/read/patch/check loops inspired by Codex, Copilot SDK, Claude Code, Gemini CLI, Qwen, and Claw Code. See [docs/large-codebase-engineering.md](docs/large-codebase-engineering.md).
77
77
 
78
- AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. Scouts run independently for architecture, implementation, review, and research risks, then the main agent uses those notes while still doing the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..4`.
78
+ AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. Scouts run independently for architecture, implementation, review, research, context mapping, tests, git workflow, and integration risks, then a coordinator synthesis is injected for the main agent. The executor still does the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..8`.
79
79
 
80
80
  For current docs, install errors, package/toolchain setup, and source discovery, the agent has a guarded `web_search` tool. It returns compact search results without browser search-engine loops and respects configured domain allowlists. Disable with `--no-web-search`.
81
81
 
@@ -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. Enter sends, Ctrl+J inserts a newline, Tab submits/queues the message, and Esc stops the active run. CLI live input has two pending lanes: ASAP pipe messages (`→`) are written to `.sessions/<session-id>/inbox.jsonl` and drained before normal queued items at safe runner boundaries, while after-finish messages (`↳`) run as the next prompt once the current task fully finishes. 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.
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.
@@ -38,6 +38,22 @@ For complicated tasks, the agent should follow this loop:
38
38
  6. `run_command` for the narrowest relevant check first.
39
39
  7. Broaden checks only after the focused check passes.
40
40
 
41
+ ## Context Budgeting
42
+
43
+ Mature coding agents avoid keeping an entire growing project in the prompt. AgInTiFlow uses a layered context pack:
44
+
45
+ - Stable memory: `AGINTI.md`, `AGENTS.md`, README files, manifests, and package scripts.
46
+ - Project map: `inspect_project` summaries, language counts, source/test directories, and recommended reads.
47
+ - Active evidence: exact search hits, selected files, failing command output, and compact git status/diff.
48
+ - Patch context: only the nearby code needed for `apply_patch`, plus before/after hashes and compact diffs.
49
+ - Scout synthesis: cheap parallel DeepSeek scouts produce bounded advice, then a coordinator summary is injected instead of every long transcript becoming permanent context.
50
+
51
+ This keeps the main executor sober: it knows where it is in the repo, but it still re-reads exact files before editing and validates with commands rather than trusting stale memory.
52
+
53
+ ## Git Discipline
54
+
55
+ When asked to commit, pull, merge, or push, the agent should run `git status --short` and `git diff --stat` first. It should commit only intended changes, use `git fetch` and `git pull --ff-only` when remote state matters, and stop for the user on conflicts, divergence, unrelated dirty files, or any merge/rebase/reset choice. Web and CLI logs fold long command output but keep full command summaries visible.
56
+
41
57
  ## CLI And Web Parity
42
58
 
43
59
  Both CLI and web use the same task profile registry and the same model/tool schemas. Use either:
@@ -74,11 +90,15 @@ DeepSeek calls are cheap enough that complex tasks can use several short advisor
74
90
  - Implementer: predicts patch boundaries and focused checks.
75
91
  - Reviewer: looks for missing tests, risks, and instruction-compliance failures.
76
92
  - Researcher: suggests `web_search` queries when current information may matter.
93
+ - Cartographer: builds a compact context map instead of dumping the whole tree.
94
+ - Tester: finds the narrowest useful checks and setup blockers.
95
+ - Git operator: keeps status/diff/commit/pull/push workflows disciplined.
96
+ - Integrator: looks for cross-stream conflicts and ordering constraints.
77
97
 
78
- Scout output is injected as advisory context only. The main agent still owns execution and must use real tools to inspect, edit, run commands, and finish. CLI flags:
98
+ Scout output is synthesized by a coordinator note and injected as advisory context only. The main agent still owns execution and must use real tools to inspect, edit, run commands, and finish. CLI flags:
79
99
 
80
100
  ```bash
81
- aginti --parallel-scouts --scout-count 4 "fix this complicated repo bug"
101
+ aginti --parallel-scouts --scout-count 8 "fix this complicated repo bug"
82
102
  aginti --no-parallel-scouts "run a cheap short task"
83
103
  ```
84
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
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 · Ctrl+J inserts a newline · Tab queues while the agent is running",
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;
@@ -741,6 +757,21 @@ function setLogs(text, mode = "active") {
741
757
  logsEl.textContent = text;
742
758
  }
743
759
 
760
+ function outputLineCount(value = "") {
761
+ const text = String(value || "");
762
+ return text ? text.split(/\r?\n/).length : 0;
763
+ }
764
+
765
+ function outputPreviewText(value = "", maxLines = 18) {
766
+ const lines = String(value || "").split(/\r?\n/);
767
+ const shown = lines.slice(0, maxLines);
768
+ return {
769
+ text: shown.join("\n"),
770
+ hidden: Math.max(lines.length - shown.length, 0),
771
+ total: value ? lines.length : 0,
772
+ };
773
+ }
774
+
744
775
  function updateStopRunButton() {
745
776
  if (!stopRunButton) return;
746
777
  stopRunButton.hidden = currentRunStatus !== "running";
@@ -1011,7 +1042,7 @@ function formPayload() {
1011
1042
  allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
1012
1043
  allowWebSearch: allowWebSearchField?.checked ?? true,
1013
1044
  allowParallelScouts: allowParallelScoutsField?.checked ?? true,
1014
- parallelScoutCount: Number(parallelScoutCountField?.value) || 3,
1045
+ parallelScoutCount: Math.min(Math.max(Number(parallelScoutCountField?.value) || 3, 1), 8),
1015
1046
  allowWrapperTools: allowWrapperToolsField.checked,
1016
1047
  preferredWrapper: preferredWrapperField.value,
1017
1048
  taskProfile: taskProfileField?.value || "auto",
@@ -1022,22 +1053,60 @@ function formPayload() {
1022
1053
  };
1023
1054
  }
1024
1055
 
1056
+ function renderCommandOutputLog(entry) {
1057
+ const data = entry.data || {};
1058
+ const command = data.command || "";
1059
+ const stdout = data.stdout || "";
1060
+ const stderr = data.stderr || "";
1061
+ const stdoutPreview = outputPreviewText(stdout);
1062
+ const stderrPreview = outputPreviewText(stderr);
1063
+ const totalLines = outputLineCount(stdout) + outputLineCount(stderr);
1064
+ const large = totalLines > 18 || stdout.length + stderr.length > 2600;
1065
+ const policy = data.commandPolicy?.category ? ` · ${data.commandPolicy.category}` : "";
1066
+ const status = data.blocked ? "blocked" : data.error ? "error" : "ok";
1067
+ const details = [
1068
+ stdout ? `<div class="log-stream-title">stdout</div><pre>${escapeHtml(stdoutPreview.text)}</pre>` : "",
1069
+ stdoutPreview.hidden > 0 ? `<div class="log-fold-note">... ${stdoutPreview.hidden} more stdout line(s) folded</div>` : "",
1070
+ stderr ? `<div class="log-stream-title">stderr</div><pre>${escapeHtml(stderrPreview.text)}</pre>` : "",
1071
+ stderrPreview.hidden > 0 ? `<div class="log-fold-note">... ${stderrPreview.hidden} more stderr line(s) folded</div>` : "",
1072
+ data.error ? `<div class="log-fold-note">${escapeHtml(data.error)}</div>` : "",
1073
+ ]
1074
+ .filter(Boolean)
1075
+ .join("");
1076
+
1077
+ return `
1078
+ <details class="log-command" ${large ? "" : "open"}>
1079
+ <summary>
1080
+ <span>${escapeHtml(`[${entry.at}] command ${status}${policy}`)}</span>
1081
+ <code>${escapeHtml(command)}</code>
1082
+ <small>stdout=${outputLineCount(stdout)} stderr=${outputLineCount(stderr)}${large ? " · folded" : ""}</small>
1083
+ </summary>
1084
+ ${details || `<div class="log-fold-note">No command output.</div>`}
1085
+ </details>
1086
+ `;
1087
+ }
1088
+
1025
1089
  function renderLogs(run) {
1026
- const lines = [];
1027
- lines.push(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`);
1028
- if (run.result) lines.push(`result=${run.result}`);
1029
- if (run.error) lines.push(`error=${run.error}`);
1030
- lines.push("");
1090
+ logsEl.dataset.mode = "active";
1091
+ const parts = [
1092
+ `<div class="log-line">${escapeHtml(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`)}</div>`,
1093
+ run.result ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>` : "",
1094
+ run.error ? `<div class="log-line error">${escapeHtml(`error=${run.error}`)}</div>` : "",
1095
+ ];
1031
1096
 
1032
1097
  for (const entry of run.logs || []) {
1033
- lines.push(`[${entry.at}] ${entry.kind}: ${entry.message}`);
1098
+ if (entry.message === "command.output") {
1099
+ parts.push(renderCommandOutputLog(entry));
1100
+ continue;
1101
+ }
1102
+ parts.push(`<div class="log-line">${escapeHtml(`[${entry.at}] ${entry.kind}: ${entry.message}`)}</div>`);
1034
1103
  if (entry.data && Object.keys(entry.data).length > 0) {
1035
- lines.push(JSON.stringify(entry.data, null, 2));
1104
+ parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1036
1105
  }
1037
- lines.push("");
1038
1106
  }
1039
1107
 
1040
- setLogs(lines.join("\n"));
1108
+ logsEl.innerHTML = parts.filter(Boolean).join("");
1109
+ logsEl.scrollTop = logsEl.scrollHeight;
1041
1110
  }
1042
1111
 
1043
1112
  function escapeHtml(value) {
@@ -1270,6 +1339,233 @@ function renderChat(chatEntries) {
1270
1339
  chatThreadEl.scrollTop = chatThreadEl.scrollHeight;
1271
1340
  }
1272
1341
 
1342
+ function afterFinishQueueKey(sessionId = currentSessionId) {
1343
+ return `agintiflow.web.afterFinishQueue.${sessionId || "none"}`;
1344
+ }
1345
+
1346
+ function loadAfterFinishQueue(sessionId = currentSessionId) {
1347
+ if (!sessionId) return [];
1348
+ try {
1349
+ const parsed = JSON.parse(localStorage.getItem(afterFinishQueueKey(sessionId)) || "[]");
1350
+ return Array.isArray(parsed) ? parsed.filter((item) => item?.id && item?.content) : [];
1351
+ } catch {
1352
+ return [];
1353
+ }
1354
+ }
1355
+
1356
+ function saveAfterFinishQueue(sessionId = currentSessionId) {
1357
+ if (!sessionId) return;
1358
+ localStorage.setItem(afterFinishQueueKey(sessionId), JSON.stringify(pendingAfterFinishItems));
1359
+ }
1360
+
1361
+ function newLocalQueueId() {
1362
+ if (globalThis.crypto?.randomUUID) return `after-${globalThis.crypto.randomUUID()}`;
1363
+ return `after-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1364
+ }
1365
+
1366
+ function compactMessage(content, limit = 220) {
1367
+ const text = String(content || "").replace(/\s+/g, " ").trim();
1368
+ return text.length > limit ? `${text.slice(0, limit - 1)}...` : text;
1369
+ }
1370
+
1371
+ function renderPendingMessages() {
1372
+ if (!chatPendingEl) return;
1373
+ const inboxItems = pendingInboxItems || [];
1374
+ const afterItems = pendingAfterFinishItems || [];
1375
+ if (inboxItems.length === 0 && afterItems.length === 0) {
1376
+ chatPendingEl.hidden = true;
1377
+ chatPendingEl.innerHTML = "";
1378
+ return;
1379
+ }
1380
+
1381
+ const section = (label, items, kind) => {
1382
+ if (!items.length) return "";
1383
+ return `
1384
+ <div class="pending-section">
1385
+ <div class="pending-label">${escapeHtml(label)}</div>
1386
+ ${items
1387
+ .map(
1388
+ (item) => `
1389
+ <div class="pending-item">
1390
+ <div class="pending-copy">
1391
+ <span class="pending-kind ${kind}">${kind === "asap" ? "→" : "↳"}</span>
1392
+ <span>${escapeHtml(compactMessage(item.content))}</span>
1393
+ </div>
1394
+ <div class="pending-actions">
1395
+ <button type="button" class="mini-button" data-edit-${kind}="${escapeHtml(item.id)}">${t(
1396
+ "editQueuedMessage"
1397
+ )}</button>
1398
+ <button type="button" class="mini-button danger" data-remove-${kind}="${escapeHtml(item.id)}">${t(
1399
+ "removeQueuedMessage"
1400
+ )}</button>
1401
+ </div>
1402
+ </div>
1403
+ `
1404
+ )
1405
+ .join("")}
1406
+ </div>
1407
+ `;
1408
+ };
1409
+
1410
+ chatPendingEl.hidden = false;
1411
+ chatPendingEl.innerHTML = `
1412
+ <div class="pending-header">
1413
+ <strong>${t("pendingMessagesTitle")}</strong>
1414
+ <span>${t("queuedConsumedHint")}</span>
1415
+ </div>
1416
+ ${section(t("asapQueueLabel"), inboxItems, "asap")}
1417
+ ${section(t("afterFinishQueueLabel"), afterItems, "after")}
1418
+ `;
1419
+ }
1420
+
1421
+ async function refreshInbox() {
1422
+ if (!currentSessionId) {
1423
+ pendingInboxItems = [];
1424
+ pendingAfterFinishItems = [];
1425
+ renderPendingMessages();
1426
+ return;
1427
+ }
1428
+
1429
+ pendingAfterFinishItems = loadAfterFinishQueue(currentSessionId);
1430
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox`);
1431
+ if (!response.ok) {
1432
+ pendingInboxItems = [];
1433
+ renderPendingMessages();
1434
+ return;
1435
+ }
1436
+ const data = await response.json();
1437
+ pendingInboxItems = data.items || [];
1438
+ renderPendingMessages();
1439
+ }
1440
+
1441
+ async function updateInboxItem(itemId, content) {
1442
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox/${encodeURIComponent(itemId)}`, {
1443
+ method: "PATCH",
1444
+ headers: { "Content-Type": "application/json" },
1445
+ body: JSON.stringify({ content }),
1446
+ });
1447
+ const data = await response.json().catch(() => ({}));
1448
+ if (!response.ok) throw new Error(data.error || t("failedContinue"));
1449
+ await refreshInbox();
1450
+ }
1451
+
1452
+ async function removeInboxItem(itemId) {
1453
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox/${encodeURIComponent(itemId)}`, {
1454
+ method: "DELETE",
1455
+ });
1456
+ const data = await response.json().catch(() => ({}));
1457
+ if (!response.ok) throw new Error(data.error || t("failedContinue"));
1458
+ await refreshInbox();
1459
+ }
1460
+
1461
+ function editAfterFinishItem(itemId) {
1462
+ const item = pendingAfterFinishItems.find((candidate) => candidate.id === itemId);
1463
+ if (!item) return;
1464
+ pendingAfterFinishItems = pendingAfterFinishItems.filter((candidate) => candidate.id !== itemId);
1465
+ saveAfterFinishQueue();
1466
+ chatInputEl.value = item.content;
1467
+ chatInputEl.focus();
1468
+ renderPendingMessages();
1469
+ }
1470
+
1471
+ function removeAfterFinishItem(itemId) {
1472
+ pendingAfterFinishItems = pendingAfterFinishItems.filter((candidate) => candidate.id !== itemId);
1473
+ saveAfterFinishQueue();
1474
+ renderPendingMessages();
1475
+ }
1476
+
1477
+ function queueAfterFinishFromInput() {
1478
+ if (!currentSessionId) {
1479
+ chatStatusEl.textContent = t("selectSessionFirst");
1480
+ return;
1481
+ }
1482
+ const content = chatInputEl.value.trim();
1483
+ if (!content) {
1484
+ chatStatusEl.textContent = t("messageRequired");
1485
+ return;
1486
+ }
1487
+ pendingAfterFinishItems = [
1488
+ ...pendingAfterFinishItems,
1489
+ {
1490
+ id: newLocalQueueId(),
1491
+ content,
1492
+ timestamp: new Date().toISOString(),
1493
+ source: "web",
1494
+ },
1495
+ ];
1496
+ saveAfterFinishQueue();
1497
+ chatInputEl.value = "";
1498
+ chatStatusEl.textContent = t("queuedAfterFinishStatus");
1499
+ renderPendingMessages();
1500
+ }
1501
+
1502
+ async function pipeMessageFromInput() {
1503
+ if (!currentSessionId) {
1504
+ chatStatusEl.textContent = t("selectSessionFirst");
1505
+ return;
1506
+ }
1507
+ const content = chatInputEl.value.trim();
1508
+ if (!content) {
1509
+ chatStatusEl.textContent = t("messageRequired");
1510
+ return;
1511
+ }
1512
+
1513
+ chatStatusEl.textContent = t("sendingStatus");
1514
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/inbox`, {
1515
+ method: "POST",
1516
+ headers: { "Content-Type": "application/json" },
1517
+ body: JSON.stringify({ content, priority: "asap" }),
1518
+ });
1519
+ const data = await response.json().catch(() => ({}));
1520
+ if (!response.ok) {
1521
+ chatStatusEl.textContent = data.error || t("failedContinue");
1522
+ return;
1523
+ }
1524
+ chatInputEl.value = "";
1525
+ chatStatusEl.textContent = t("queuedAsapStatus");
1526
+ pendingInboxItems = data.item ? [...pendingInboxItems, data.item] : pendingInboxItems;
1527
+ renderPendingMessages();
1528
+ await refreshInbox();
1529
+ }
1530
+
1531
+ async function flushAfterFinishQueue() {
1532
+ if (!currentSessionId || flushingAfterFinish || currentRunStatus === "running" || pendingAfterFinishItems.length === 0) return;
1533
+ flushingAfterFinish = true;
1534
+ const next = pendingAfterFinishItems[0];
1535
+ pendingAfterFinishItems = pendingAfterFinishItems.slice(1);
1536
+ saveAfterFinishQueue();
1537
+ renderPendingMessages();
1538
+
1539
+ try {
1540
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/messages`, {
1541
+ method: "POST",
1542
+ headers: { "Content-Type": "application/json" },
1543
+ body: JSON.stringify({
1544
+ ...formPayload(),
1545
+ content: next.content,
1546
+ }),
1547
+ });
1548
+ const data = await response.json().catch(() => ({}));
1549
+ if (!response.ok) throw new Error(data.error || t("failedContinue"));
1550
+ currentSessionId = data.sessionId;
1551
+ currentRunStatus = data.queued ? currentRunStatus : "running";
1552
+ updateStopRunButton();
1553
+ chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
1554
+ await refreshSessions();
1555
+ await refreshChat();
1556
+ if (pollTimer) clearInterval(pollTimer);
1557
+ pollTimer = setInterval(refreshRun, 1500);
1558
+ await refreshRun();
1559
+ } catch (error) {
1560
+ pendingAfterFinishItems = [next, ...pendingAfterFinishItems];
1561
+ saveAfterFinishQueue();
1562
+ renderPendingMessages();
1563
+ chatStatusEl.textContent = error instanceof Error ? error.message : String(error);
1564
+ } finally {
1565
+ flushingAfterFinish = false;
1566
+ }
1567
+ }
1568
+
1273
1569
  function artifactReadKey(sessionId = currentSessionId) {
1274
1570
  return `agintiflow.artifacts.readIds.${sessionId || "none"}`;
1275
1571
  }
@@ -1682,6 +1978,7 @@ async function refreshRun() {
1682
1978
  runMetaEl.textContent = `${run.status} · ${run.sessionId}`;
1683
1979
  renderLogs(run);
1684
1980
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
1981
+ await refreshInbox();
1685
1982
 
1686
1983
  if (run.status === "finished" || run.status === "failed" || run.status === "stopped") {
1687
1984
  clearInterval(pollTimer);
@@ -1690,6 +1987,7 @@ async function refreshRun() {
1690
1987
  await refreshSessions();
1691
1988
  await refreshWorkspaceChanges();
1692
1989
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
1990
+ if (run.status === "finished") await flushAfterFinishQueue();
1693
1991
  }
1694
1992
  }
1695
1993
 
@@ -1711,6 +2009,9 @@ async function stopCurrentRun() {
1711
2009
 
1712
2010
  async function refreshChat() {
1713
2011
  if (!currentSessionId) {
2012
+ pendingInboxItems = [];
2013
+ pendingAfterFinishItems = [];
2014
+ renderPendingMessages();
1714
2015
  renderChat([]);
1715
2016
  return;
1716
2017
  }
@@ -1722,6 +2023,9 @@ async function refreshChat() {
1722
2023
  }
1723
2024
 
1724
2025
  const data = await response.json();
2026
+ pendingInboxItems = data.inbox || [];
2027
+ pendingAfterFinishItems = loadAfterFinishQueue(currentSessionId);
2028
+ renderPendingMessages();
1725
2029
  renderChat(data.chat || []);
1726
2030
  }
1727
2031
 
@@ -2019,24 +2323,54 @@ form.addEventListener("input", schedulePreferenceSave);
2019
2323
  form.addEventListener("change", schedulePreferenceSave);
2020
2324
 
2021
2325
  chatInputEl.addEventListener("keydown", (event) => {
2022
- if (event.key === "j" && event.ctrlKey && !event.metaKey && !event.altKey) {
2326
+ if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
2023
2327
  event.preventDefault();
2024
- const start = chatInputEl.selectionStart ?? chatInputEl.value.length;
2025
- const end = chatInputEl.selectionEnd ?? chatInputEl.value.length;
2026
- chatInputEl.value = `${chatInputEl.value.slice(0, start)}\n${chatInputEl.value.slice(end)}`;
2027
- chatInputEl.selectionStart = chatInputEl.selectionEnd = start + 1;
2328
+ chatFormEl.requestSubmit();
2329
+ }
2330
+ });
2331
+
2332
+ pipeMessageButton?.addEventListener("click", () => {
2333
+ pipeMessageFromInput().catch((error) => {
2334
+ chatStatusEl.textContent = String(error);
2335
+ });
2336
+ });
2337
+
2338
+ queueAfterFinishButton?.addEventListener("click", queueAfterFinishFromInput);
2339
+
2340
+ chatPendingEl?.addEventListener("click", (event) => {
2341
+ const target = event.target.closest("button");
2342
+ if (!target) return;
2343
+
2344
+ const editAsap = target.dataset.editAsap;
2345
+ const removeAsap = target.dataset.removeAsap;
2346
+ const editAfter = target.dataset.editAfter;
2347
+ const removeAfter = target.dataset.removeAfter;
2348
+
2349
+ if (editAsap) {
2350
+ const item = pendingInboxItems.find((candidate) => candidate.id === editAsap);
2351
+ const next = prompt(t("editQueuedMessage"), item?.content || "");
2352
+ if (next !== null) {
2353
+ updateInboxItem(editAsap, next).catch((error) => {
2354
+ chatStatusEl.textContent = String(error);
2355
+ });
2356
+ }
2028
2357
  return;
2029
2358
  }
2030
2359
 
2031
- if (event.key === "Tab") {
2032
- event.preventDefault();
2033
- chatFormEl.requestSubmit();
2360
+ if (removeAsap) {
2361
+ removeInboxItem(removeAsap).catch((error) => {
2362
+ chatStatusEl.textContent = String(error);
2363
+ });
2034
2364
  return;
2035
2365
  }
2036
2366
 
2037
- if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
2038
- event.preventDefault();
2039
- chatFormEl.requestSubmit();
2367
+ if (editAfter) {
2368
+ editAfterFinishItem(editAfter);
2369
+ return;
2370
+ }
2371
+
2372
+ if (removeAfter) {
2373
+ removeAfterFinishItem(removeAfter);
2040
2374
  }
2041
2375
  });
2042
2376
 
@@ -2108,10 +2442,13 @@ sessionSelectEl.addEventListener("change", async () => {
2108
2442
  currentSessionId = sessionSelectEl.value;
2109
2443
  if (!currentSessionId) {
2110
2444
  currentRunStatus = "";
2445
+ pendingInboxItems = [];
2446
+ pendingAfterFinishItems = [];
2111
2447
  updateStopRunButton();
2112
2448
  runMetaEl.textContent = "";
2113
2449
  setLogs(t("noRunSelected"), "empty");
2114
2450
  renderChat([]);
2451
+ renderPendingMessages();
2115
2452
  artifactItems = [];
2116
2453
  selectedArtifactId = "";
2117
2454
  renderArtifactShell();
@@ -2158,18 +2495,8 @@ chatFormEl.addEventListener("submit", async (event) => {
2158
2495
  updateStopRunButton();
2159
2496
  chatInputEl.value = "";
2160
2497
  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
2498
  await refreshSessions();
2172
- if (!data.queued) await refreshChat();
2499
+ await refreshChat();
2173
2500
  await refreshWorkspaceChanges();
2174
2501
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
2175
2502
 
package/public/index.html CHANGED
@@ -205,7 +205,7 @@
205
205
 
206
206
  <label>
207
207
  <span data-i18n="parallelScoutCountLabel">Scout count</span>
208
- <input id="parallelScoutCount" name="parallelScoutCount" type="number" min="1" max="4" value="3" />
208
+ <input id="parallelScoutCount" name="parallelScoutCount" type="number" min="1" max="8" value="3" />
209
209
  </label>
210
210
 
211
211
  <div class="grid wrapper-controls">
@@ -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 · Ctrl+J inserts a newline · Tab queues while the agent is running
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>
@@ -333,7 +338,7 @@
333
338
  <div id="run-meta" class="subtle"></div>
334
339
  </div>
335
340
  </div>
336
- <pre id="logs" class="logs" data-i18n="noRunStarted">No run started.</pre>
341
+ <div id="logs" class="logs" data-i18n="noRunStarted">No run started.</div>
337
342
  </section>
338
343
  </section>
339
344
  </main>