@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 +3 -2
- package/docs/agent-runtime-pipe.md +3 -1
- package/docs/large-codebase-engineering.md +22 -2
- package/package.json +1 -1
- package/public/app.js +360 -33
- package/public/index.html +8 -3
- package/public/styles.css +168 -0
- package/scripts/smoke-capabilities.js +13 -1
- package/scripts/smoke-web-api.js +37 -0
- package/src/agent-runner.js +2 -0
- package/src/capabilities.js +6 -0
- package/src/cli.js +1 -1
- package/src/command-policy.js +32 -5
- package/src/config.js +9 -1
- package/src/engineering-guidance.js +7 -0
- package/src/interactive-cli.js +82 -4
- package/src/parallel-scouts.js +55 -1
- package/src/workspace-tools.js +13 -1
- package/web.js +166 -4
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
|
|
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.
|
|
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
|
|
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
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;
|
|
@@ -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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
2326
|
+
if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
2023
2327
|
event.preventDefault();
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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 (
|
|
2032
|
-
|
|
2033
|
-
|
|
2360
|
+
if (removeAsap) {
|
|
2361
|
+
removeInboxItem(removeAsap).catch((error) => {
|
|
2362
|
+
chatStatusEl.textContent = String(error);
|
|
2363
|
+
});
|
|
2034
2364
|
return;
|
|
2035
2365
|
}
|
|
2036
2366
|
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
-
|
|
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="
|
|
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 ·
|
|
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
|
-
<
|
|
341
|
+
<div id="logs" class="logs" data-i18n="noRunStarted">No run started.</div>
|
|
337
342
|
</section>
|
|
338
343
|
</section>
|
|
339
344
|
</main>
|