@lazyingart/agintiflow 0.20.164 → 0.20.165
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/package.json +5 -3
- package/public/app.js +235 -82
- package/public/index.html +8 -1
- package/public/styles.css +83 -1
- package/scripts/smoke-web-api.js +34 -0
- package/scripts/smoke-web-ui.js +147 -0
- package/web.js +48 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.165",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"scripts/smoke-toolchain-docker.js",
|
|
77
77
|
"scripts/smoke-web-autostart.js",
|
|
78
78
|
"scripts/smoke-web-api.js",
|
|
79
|
+
"scripts/smoke-web-ui.js",
|
|
79
80
|
"scripts/smoke-webapp-command.js",
|
|
80
81
|
"scripts/smoke-web-port-fallback.js",
|
|
81
82
|
"src/",
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
"scripts": {
|
|
91
92
|
"start": "node run.js",
|
|
92
93
|
"web": "node web.js",
|
|
93
|
-
"check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/app.js && node --check scripts/seed-supervised-homework.js && node --check scripts/smoke-mcp.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
|
|
94
|
+
"check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/app.js && node --check scripts/seed-supervised-homework.js && node --check scripts/smoke-mcp.js && node --check scripts/smoke-web-ui.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
|
|
94
95
|
"setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
|
|
95
96
|
"smoke:coding-tools": "node scripts/smoke-coding-tools.js",
|
|
96
97
|
"smoke:dynamic-step-budget": "node scripts/smoke-dynamic-step-budget.js",
|
|
@@ -112,6 +113,7 @@
|
|
|
112
113
|
"smoke:runtime-compat": "node scripts/smoke-runtime-compat.js",
|
|
113
114
|
"smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
|
|
114
115
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
116
|
+
"smoke:web-ui": "node scripts/smoke-web-ui.js",
|
|
115
117
|
"smoke:web-autostart": "node scripts/smoke-web-autostart.js",
|
|
116
118
|
"smoke:webapp-command": "node scripts/smoke-webapp-command.js",
|
|
117
119
|
"smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
123
125
|
"publish:env": "node scripts/npm-publish-from-env.js publish --access public",
|
|
124
126
|
"publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
|
|
125
|
-
"test": "npm run check && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
127
|
+
"test": "npm run check && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
126
128
|
"pack:dry-run": "npm pack --dry-run",
|
|
127
129
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
128
130
|
},
|
package/public/app.js
CHANGED
|
@@ -99,6 +99,20 @@ const translations = {
|
|
|
99
99
|
stopRunButton: "Stop",
|
|
100
100
|
stoppingRun: "Stopping...",
|
|
101
101
|
stopRunFailed: "Failed to stop run.",
|
|
102
|
+
newSessionButton: "New",
|
|
103
|
+
runStateIdle: "Ready",
|
|
104
|
+
runStateIdleDetail: "Select a conversation or type a new goal below.",
|
|
105
|
+
runStateRunning: "Running",
|
|
106
|
+
runStateFinished: "Finished",
|
|
107
|
+
runStateStopped: "Stopped",
|
|
108
|
+
runStateFailed: "Failed",
|
|
109
|
+
composerStartButton: "Start new run",
|
|
110
|
+
composerPlaceholderNoSession: "Type a goal to start a new agent run.",
|
|
111
|
+
composerPlaceholderSession: "Continue the selected conversation here.",
|
|
112
|
+
toastRunStarted: "Run started.",
|
|
113
|
+
toastRunFinished: "Run finished.",
|
|
114
|
+
toastRunStopped: "Run stopped.",
|
|
115
|
+
toastRunFailed: "Run failed.",
|
|
102
116
|
runOutputTitle: "Run output",
|
|
103
117
|
noRunStarted: "No run started.",
|
|
104
118
|
conversationTitle: "Conversation",
|
|
@@ -716,9 +730,15 @@ const sandboxLogsEl = document.querySelector("#sandbox-logs");
|
|
|
716
730
|
const checkSandboxButton = document.querySelector("#check-sandbox");
|
|
717
731
|
const preflightSandboxButton = document.querySelector("#preflight-sandbox");
|
|
718
732
|
const sessionSelectEl = document.querySelector("#session-select");
|
|
733
|
+
const newSessionButton = document.querySelector("#new-session");
|
|
734
|
+
const runStateEl = document.querySelector("#run-state");
|
|
735
|
+
const runStateTextEl = document.querySelector("#run-state-text");
|
|
736
|
+
const runStateDetailEl = document.querySelector("#run-state-detail");
|
|
737
|
+
const toastRegionEl = document.querySelector("#toast-region");
|
|
719
738
|
const chatThreadEl = document.querySelector("#chat-thread");
|
|
720
739
|
const chatFormEl = document.querySelector("#chat-form");
|
|
721
740
|
const chatInputEl = document.querySelector("#chat-input");
|
|
741
|
+
const chatSubmitButton = document.querySelector("#chat-submit");
|
|
722
742
|
const chatPendingEl = document.querySelector("#chat-pending");
|
|
723
743
|
const chatStatusEl = document.querySelector("#chat-status");
|
|
724
744
|
const pipeMessageButton = document.querySelector("#pipe-message");
|
|
@@ -789,6 +809,7 @@ let artifactItems = [];
|
|
|
789
809
|
let selectedArtifactId = "";
|
|
790
810
|
let currentArtifactTab = "canvas";
|
|
791
811
|
let artifactUnreadCount = 0;
|
|
812
|
+
let lastAnnouncedRunKey = "";
|
|
792
813
|
|
|
793
814
|
function normalizeLanguage(language) {
|
|
794
815
|
const lower = String(language || "").toLowerCase();
|
|
@@ -806,6 +827,36 @@ function setLogs(text, mode = "active") {
|
|
|
806
827
|
logsEl.textContent = text;
|
|
807
828
|
}
|
|
808
829
|
|
|
830
|
+
function isRunningStatus(status = currentRunStatus) {
|
|
831
|
+
return status === "running";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function isTerminalStatus(status = currentRunStatus) {
|
|
835
|
+
return ["finished", "failed", "stopped"].includes(status);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function runStatusLabel(status = currentRunStatus) {
|
|
839
|
+
if (status === "running") return t("runStateRunning");
|
|
840
|
+
if (status === "finished") return t("runStateFinished");
|
|
841
|
+
if (status === "failed") return t("runStateFailed");
|
|
842
|
+
if (status === "stopped") return t("runStateStopped");
|
|
843
|
+
return t("runStateIdle");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function showToast(message, kind = "info") {
|
|
847
|
+
if (!toastRegionEl || !message) return;
|
|
848
|
+
const toast = document.createElement("div");
|
|
849
|
+
toast.className = "toast";
|
|
850
|
+
toast.dataset.kind = kind;
|
|
851
|
+
toast.textContent = message;
|
|
852
|
+
toastRegionEl.append(toast);
|
|
853
|
+
setTimeout(() => {
|
|
854
|
+
toast.style.opacity = "0";
|
|
855
|
+
toast.style.transform = "translateY(6px)";
|
|
856
|
+
}, 2600);
|
|
857
|
+
setTimeout(() => toast.remove(), 3200);
|
|
858
|
+
}
|
|
859
|
+
|
|
809
860
|
function outputLineCount(value = "") {
|
|
810
861
|
const text = String(value || "");
|
|
811
862
|
return text ? text.split(/\r?\n/).length : 0;
|
|
@@ -823,8 +874,37 @@ function outputPreviewText(value = "", maxLines = 18) {
|
|
|
823
874
|
|
|
824
875
|
function updateStopRunButton() {
|
|
825
876
|
if (!stopRunButton) return;
|
|
826
|
-
stopRunButton.hidden =
|
|
827
|
-
stopRunButton.disabled =
|
|
877
|
+
stopRunButton.hidden = !isRunningStatus();
|
|
878
|
+
stopRunButton.disabled = !isRunningStatus();
|
|
879
|
+
if (pipeMessageButton) pipeMessageButton.disabled = !currentSessionId;
|
|
880
|
+
if (queueAfterFinishButton) queueAfterFinishButton.disabled = !currentSessionId;
|
|
881
|
+
if (chatSubmitButton) chatSubmitButton.textContent = currentSessionId ? t("sendMessageButton") : t("composerStartButton");
|
|
882
|
+
if (chatInputEl) {
|
|
883
|
+
chatInputEl.placeholder = currentSessionId ? t("composerPlaceholderSession") : t("composerPlaceholderNoSession");
|
|
884
|
+
}
|
|
885
|
+
if (runStateEl) {
|
|
886
|
+
const status = currentRunStatus || "idle";
|
|
887
|
+
runStateEl.dataset.status = status;
|
|
888
|
+
runStateTextEl.textContent = runStatusLabel(status);
|
|
889
|
+
const selected = currentSessionId ? `session=${currentSessionId}` : t("runStateIdleDetail");
|
|
890
|
+
runStateDetailEl.textContent = isRunningStatus(status) || isTerminalStatus(status) ? selected : t("runStateIdleDetail");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function setRunStatus(status = "", { sessionId, detail = "", announce = false } = {}) {
|
|
895
|
+
const previous = currentRunStatus;
|
|
896
|
+
currentRunStatus = String(status || "");
|
|
897
|
+
if (sessionId !== undefined) currentSessionId = sessionId;
|
|
898
|
+
updateStopRunButton();
|
|
899
|
+
if (detail && runStateDetailEl) runStateDetailEl.textContent = detail;
|
|
900
|
+
|
|
901
|
+
const key = `${currentSessionId}:${currentRunStatus}`;
|
|
902
|
+
if (!announce || key === lastAnnouncedRunKey || previous === currentRunStatus) return;
|
|
903
|
+
lastAnnouncedRunKey = key;
|
|
904
|
+
if (currentRunStatus === "running") showToast(t("toastRunStarted"));
|
|
905
|
+
if (currentRunStatus === "finished") showToast(t("toastRunFinished"));
|
|
906
|
+
if (currentRunStatus === "stopped") showToast(t("toastRunStopped"));
|
|
907
|
+
if (currentRunStatus === "failed") showToast(t("toastRunFailed"), "error");
|
|
828
908
|
}
|
|
829
909
|
|
|
830
910
|
function renderKeyStatus(status = lastKeyStatus) {
|
|
@@ -880,10 +960,10 @@ function recommendedMaxStepsForProfile(profile = "auto", goal = "") {
|
|
|
880
960
|
return 24;
|
|
881
961
|
}
|
|
882
962
|
|
|
883
|
-
function ensureRecommendedMaxStepsForCurrentTask() {
|
|
963
|
+
function ensureRecommendedMaxStepsForCurrentTask(goalOverride = "") {
|
|
884
964
|
const maxStepsField = document.querySelector("#maxSteps");
|
|
885
965
|
const goalField = document.querySelector("#goal");
|
|
886
|
-
const recommended = recommendedMaxStepsForProfile(taskProfileField?.value || "auto", goalField?.value || "");
|
|
966
|
+
const recommended = recommendedMaxStepsForProfile(taskProfileField?.value || "auto", goalOverride || goalField?.value || "");
|
|
887
967
|
if (maxStepsField && Number(maxStepsField.value || 0) < recommended) {
|
|
888
968
|
maxStepsField.value = String(recommended);
|
|
889
969
|
}
|
|
@@ -891,6 +971,7 @@ function ensureRecommendedMaxStepsForCurrentTask() {
|
|
|
891
971
|
|
|
892
972
|
function renderWrapperStatus(wrappers = lastWrappers) {
|
|
893
973
|
lastWrappers = wrappers || [];
|
|
974
|
+
refreshProviderDropdowns();
|
|
894
975
|
if (lastWrappers.length === 0) {
|
|
895
976
|
wrapperStatusEl.textContent = "";
|
|
896
977
|
wrapperGridEl.innerHTML = "";
|
|
@@ -1045,6 +1126,51 @@ function fieldValue(field) {
|
|
|
1045
1126
|
return String(field?.value || "").trim();
|
|
1046
1127
|
}
|
|
1047
1128
|
|
|
1129
|
+
const providerLabels = {
|
|
1130
|
+
deepseek: "DeepSeek",
|
|
1131
|
+
openai: "OpenAI",
|
|
1132
|
+
qwen: "Qwen",
|
|
1133
|
+
venice: "Venice",
|
|
1134
|
+
mock: "Mock local",
|
|
1135
|
+
grsai: "GRS AI image",
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
function setChoiceOptions(select, options, selectedValue = "", fallbackValue = "") {
|
|
1139
|
+
if (!select) return "";
|
|
1140
|
+
const normalized = (options || [])
|
|
1141
|
+
.map((item) => (typeof item === "string" ? { id: item, label: providerLabels[item] || item } : item))
|
|
1142
|
+
.filter((item) => item?.id);
|
|
1143
|
+
const selected = String(selectedValue || fallbackValue || "").trim();
|
|
1144
|
+
if (selected && !normalized.some((item) => item.id === selected)) {
|
|
1145
|
+
normalized.unshift({ id: selected, label: providerLabels[selected] || `Custom: ${selected}` });
|
|
1146
|
+
}
|
|
1147
|
+
select.innerHTML = normalized
|
|
1148
|
+
.map((item) => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.label || item.id)}</option>`)
|
|
1149
|
+
.join("");
|
|
1150
|
+
select.value = selected && normalized.some((item) => item.id === selected) ? selected : normalized[0]?.id || "";
|
|
1151
|
+
return select.value;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function configuredTextProviders({ includeMock = true } = {}) {
|
|
1155
|
+
const base = ["deepseek", "openai", "qwen", "venice"];
|
|
1156
|
+
if (includeMock) base.push("mock");
|
|
1157
|
+
if (Object.keys(modelCatalog || {}).length === 0) return base;
|
|
1158
|
+
return base.filter((provider) => provider === "mock" || Array.isArray(modelCatalog[provider]));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function refreshProviderDropdowns() {
|
|
1162
|
+
setChoiceOptions(setupProviderField, ["deepseek", "openai", "qwen", "venice", "grsai"], setupProviderField?.value || "deepseek", "deepseek");
|
|
1163
|
+
setChoiceOptions(providerField, configuredTextProviders({ includeMock: true }), providerField?.value || "deepseek", "deepseek");
|
|
1164
|
+
setChoiceOptions(routeProviderField, configuredTextProviders({ includeMock: true }), routeProviderField?.value || "deepseek", "deepseek");
|
|
1165
|
+
setChoiceOptions(mainProviderField, configuredTextProviders({ includeMock: false }), mainProviderField?.value || "deepseek", "deepseek");
|
|
1166
|
+
setChoiceOptions(spareProviderField, configuredTextProviders({ includeMock: false }), spareProviderField?.value || "openai", "openai");
|
|
1167
|
+
setChoiceOptions(auxiliaryProviderField, ["grsai", "venice"], auxiliaryProviderField?.value || "grsai", "grsai");
|
|
1168
|
+
const wrappers = lastWrappers.length
|
|
1169
|
+
? lastWrappers.map((wrapper) => ({ id: wrapper.name, label: wrapper.label || wrapper.name }))
|
|
1170
|
+
: ["codex", "claude", "gemini", "copilot", "qwen"].map((name) => ({ id: name, label: providerLabels[name] || name }));
|
|
1171
|
+
setChoiceOptions(preferredWrapperField, wrappers, preferredWrapperField?.value || "codex", "codex");
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1048
1174
|
function providerModelOptions(provider = providerField.value) {
|
|
1049
1175
|
return (modelCatalog[provider] || []).filter((item) => !item.hidden);
|
|
1050
1176
|
}
|
|
@@ -1108,6 +1234,7 @@ function setSelectOptions(select, options, selectedValue = "", fallbackValue = "
|
|
|
1108
1234
|
}
|
|
1109
1235
|
|
|
1110
1236
|
function refreshModelDropdowns() {
|
|
1237
|
+
refreshProviderDropdowns();
|
|
1111
1238
|
const provider = providerField?.value || "deepseek";
|
|
1112
1239
|
setSelectOptions(modelField, providerModelOptions(provider), fieldValue(modelField), defaults[provider]);
|
|
1113
1240
|
setSelectOptions(
|
|
@@ -1926,7 +2053,7 @@ async function pipeMessageFromInput() {
|
|
|
1926
2053
|
}
|
|
1927
2054
|
|
|
1928
2055
|
async function flushAfterFinishQueue() {
|
|
1929
|
-
if (!currentSessionId || flushingAfterFinish ||
|
|
2056
|
+
if (!currentSessionId || flushingAfterFinish || isRunningStatus() || pendingAfterFinishItems.length === 0) return;
|
|
1930
2057
|
flushingAfterFinish = true;
|
|
1931
2058
|
const next = pendingAfterFinishItems[0];
|
|
1932
2059
|
pendingAfterFinishItems = pendingAfterFinishItems.slice(1);
|
|
@@ -1944,9 +2071,11 @@ async function flushAfterFinishQueue() {
|
|
|
1944
2071
|
});
|
|
1945
2072
|
const data = await response.json().catch(() => ({}));
|
|
1946
2073
|
if (!response.ok) throw new Error(data.error || t("failedContinue"));
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2074
|
+
if (!data.queued) setRunStatus("running", { sessionId: data.sessionId, announce: true });
|
|
2075
|
+
else {
|
|
2076
|
+
currentSessionId = data.sessionId;
|
|
2077
|
+
updateStopRunButton();
|
|
2078
|
+
}
|
|
1950
2079
|
chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
|
|
1951
2080
|
await refreshSessions();
|
|
1952
2081
|
await refreshChat();
|
|
@@ -2408,20 +2537,26 @@ async function refreshWorkspaceChanges() {
|
|
|
2408
2537
|
}
|
|
2409
2538
|
|
|
2410
2539
|
async function refreshRun() {
|
|
2411
|
-
if (!currentSessionId)
|
|
2540
|
+
if (!currentSessionId) {
|
|
2541
|
+
setRunStatus("", { sessionId: "" });
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2412
2544
|
const response = await fetch(`/api/runs/${encodeURIComponent(currentSessionId)}`);
|
|
2413
|
-
if (!response.ok)
|
|
2545
|
+
if (!response.ok) {
|
|
2546
|
+
setRunStatus("", { detail: t("noRunSelected") });
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2414
2549
|
const run = await response.json();
|
|
2415
|
-
|
|
2416
|
-
updateStopRunButton();
|
|
2550
|
+
setRunStatus(run.status || "", { sessionId: run.sessionId, announce: true });
|
|
2417
2551
|
runMetaEl.textContent = `${run.status} · ${run.sessionId}`;
|
|
2418
2552
|
renderLogs(run);
|
|
2419
2553
|
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
2420
2554
|
await refreshInbox();
|
|
2421
2555
|
|
|
2422
|
-
if (run.status
|
|
2556
|
+
if (isTerminalStatus(run.status)) {
|
|
2423
2557
|
clearInterval(pollTimer);
|
|
2424
2558
|
pollTimer = null;
|
|
2559
|
+
chatStatusEl.textContent = run.status === "finished" ? t("runStateFinished") : run.error || runStatusLabel(run.status);
|
|
2425
2560
|
await refreshChat();
|
|
2426
2561
|
await refreshSessions();
|
|
2427
2562
|
await refreshWorkspaceChanges();
|
|
@@ -2431,7 +2566,7 @@ async function refreshRun() {
|
|
|
2431
2566
|
}
|
|
2432
2567
|
|
|
2433
2568
|
async function stopCurrentRun() {
|
|
2434
|
-
if (!currentSessionId ||
|
|
2569
|
+
if (!currentSessionId || !isRunningStatus()) return;
|
|
2435
2570
|
stopRunButton.disabled = true;
|
|
2436
2571
|
chatStatusEl.textContent = t("stoppingRun");
|
|
2437
2572
|
const response = await fetch(`/api/runs/${encodeURIComponent(currentSessionId)}/stop`, {
|
|
@@ -2440,6 +2575,7 @@ async function stopCurrentRun() {
|
|
|
2440
2575
|
if (!response.ok) {
|
|
2441
2576
|
const data = await response.json().catch(() => ({}));
|
|
2442
2577
|
chatStatusEl.textContent = data.error || t("stopRunFailed");
|
|
2578
|
+
if (data.status) setRunStatus(data.status, { announce: true });
|
|
2443
2579
|
updateStopRunButton();
|
|
2444
2580
|
return;
|
|
2445
2581
|
}
|
|
@@ -2466,7 +2602,7 @@ async function approvePermission(action) {
|
|
|
2466
2602
|
permissionModeField.value = data.permissionMode;
|
|
2467
2603
|
applyPermissionModeToForm(data.permissionMode);
|
|
2468
2604
|
}
|
|
2469
|
-
|
|
2605
|
+
if (action !== "no") setRunStatus("running", { announce: true });
|
|
2470
2606
|
chatStatusEl.textContent = action === "no" ? t("permissionApprovalNo") : t("runningStatus");
|
|
2471
2607
|
updateStopRunButton();
|
|
2472
2608
|
await refreshRun();
|
|
@@ -2608,20 +2744,80 @@ async function deleteManagedSession() {
|
|
|
2608
2744
|
}
|
|
2609
2745
|
|
|
2610
2746
|
if (currentSessionId === selected.sessionId) {
|
|
2611
|
-
|
|
2612
|
-
sessionSelectEl.value = "";
|
|
2613
|
-
runMetaEl.textContent = "";
|
|
2614
|
-
setLogs(t("noRunSelected"), "empty");
|
|
2615
|
-
renderChat([]);
|
|
2616
|
-
artifactItems = [];
|
|
2617
|
-
selectedArtifactId = "";
|
|
2618
|
-
renderArtifactShell();
|
|
2747
|
+
clearCurrentSessionScope({ focus: false });
|
|
2619
2748
|
}
|
|
2620
2749
|
managedSessionId = "";
|
|
2621
2750
|
await refreshSessions();
|
|
2622
2751
|
renderSessionManager(t("sessionDeleted"));
|
|
2623
2752
|
}
|
|
2624
2753
|
|
|
2754
|
+
function clearCurrentSessionScope({ focus = true } = {}) {
|
|
2755
|
+
currentSessionId = "";
|
|
2756
|
+
setRunStatus("", { sessionId: "" });
|
|
2757
|
+
pendingInboxItems = [];
|
|
2758
|
+
pendingAfterFinishItems = [];
|
|
2759
|
+
sessionSelectEl.value = "";
|
|
2760
|
+
runMetaEl.textContent = "";
|
|
2761
|
+
setLogs(t("noRunSelected"), "empty");
|
|
2762
|
+
renderChat([]);
|
|
2763
|
+
renderPendingMessages();
|
|
2764
|
+
artifactItems = [];
|
|
2765
|
+
selectedArtifactId = "";
|
|
2766
|
+
renderArtifactShell();
|
|
2767
|
+
chatStatusEl.textContent = "";
|
|
2768
|
+
if (focus) chatInputEl.focus();
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
async function startRunFromGoal(goal, { clearComposer = false } = {}) {
|
|
2772
|
+
const normalizedGoal = String(goal || "").trim();
|
|
2773
|
+
if (!normalizedGoal) {
|
|
2774
|
+
setLogs(t("goalRequired"), "empty");
|
|
2775
|
+
chatStatusEl.textContent = t("goalRequired");
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
ensureRecommendedMaxStepsForCurrentTask(normalizedGoal);
|
|
2779
|
+
|
|
2780
|
+
const payload = {
|
|
2781
|
+
...formPayload(),
|
|
2782
|
+
goal: normalizedGoal,
|
|
2783
|
+
};
|
|
2784
|
+
|
|
2785
|
+
setLogs(t("startingRun"));
|
|
2786
|
+
runMetaEl.textContent = "";
|
|
2787
|
+
chatStatusEl.textContent = t("startingRun");
|
|
2788
|
+
showToast(t("startingRun"));
|
|
2789
|
+
|
|
2790
|
+
const response = await fetch("/api/runs", {
|
|
2791
|
+
method: "POST",
|
|
2792
|
+
headers: { "Content-Type": "application/json" },
|
|
2793
|
+
body: JSON.stringify(payload),
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
const data = await response.json().catch(() => ({}));
|
|
2797
|
+
if (!response.ok) {
|
|
2798
|
+
setLogs(data.error || t("failedStartRun"));
|
|
2799
|
+
chatStatusEl.textContent = data.error || t("failedStartRun");
|
|
2800
|
+
showToast(data.error || t("failedStartRun"), "error");
|
|
2801
|
+
return false;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
currentSessionId = data.sessionId;
|
|
2805
|
+
setRunStatus("running", { sessionId: data.sessionId, announce: true });
|
|
2806
|
+
sessionSelectEl.value = currentSessionId;
|
|
2807
|
+
runMetaEl.textContent = `${runStatusLabel("running")} · ${currentSessionId}`;
|
|
2808
|
+
if (clearComposer) chatInputEl.value = "";
|
|
2809
|
+
await refreshSessions();
|
|
2810
|
+
sessionSelectEl.value = currentSessionId;
|
|
2811
|
+
await refreshChat();
|
|
2812
|
+
await refreshWorkspaceChanges();
|
|
2813
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
2814
|
+
|
|
2815
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
2816
|
+
pollTimer = setInterval(refreshRun, 1500);
|
|
2817
|
+
await refreshRun();
|
|
2818
|
+
return true;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2625
2821
|
function schedulePreferenceSave() {
|
|
2626
2822
|
clearTimeout(saveTimer);
|
|
2627
2823
|
saveTimer = setTimeout(() => {
|
|
@@ -2656,6 +2852,10 @@ manageSessionsButton.addEventListener("click", () => {
|
|
|
2656
2852
|
});
|
|
2657
2853
|
});
|
|
2658
2854
|
|
|
2855
|
+
newSessionButton?.addEventListener("click", () => {
|
|
2856
|
+
clearCurrentSessionScope();
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2659
2859
|
openArtifactsButton.addEventListener("click", () => {
|
|
2660
2860
|
openArtifactTunnel().catch((error) => {
|
|
2661
2861
|
chatStatusEl.textContent = String(error);
|
|
@@ -2906,7 +3106,7 @@ logsEl?.addEventListener("click", (event) => {
|
|
|
2906
3106
|
});
|
|
2907
3107
|
|
|
2908
3108
|
window.addEventListener("keydown", (event) => {
|
|
2909
|
-
if (event.key !== "Escape" ||
|
|
3109
|
+
if (event.key !== "Escape" || !isRunningStatus()) return;
|
|
2910
3110
|
const tag = event.target?.tagName?.toLowerCase();
|
|
2911
3111
|
if (tag === "select" || tag === "button") return;
|
|
2912
3112
|
event.preventDefault();
|
|
@@ -2920,62 +3120,13 @@ form.addEventListener("submit", async (event) => {
|
|
|
2920
3120
|
event.preventDefault();
|
|
2921
3121
|
|
|
2922
3122
|
const goal = document.querySelector("#goal").value.trim();
|
|
2923
|
-
|
|
2924
|
-
setLogs(t("goalRequired"), "empty");
|
|
2925
|
-
return;
|
|
2926
|
-
}
|
|
2927
|
-
ensureRecommendedMaxStepsForCurrentTask();
|
|
2928
|
-
|
|
2929
|
-
const payload = {
|
|
2930
|
-
...formPayload(),
|
|
2931
|
-
goal,
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
setLogs(t("startingRun"));
|
|
2935
|
-
runMetaEl.textContent = "";
|
|
2936
|
-
chatStatusEl.textContent = "";
|
|
2937
|
-
|
|
2938
|
-
const response = await fetch("/api/runs", {
|
|
2939
|
-
method: "POST",
|
|
2940
|
-
headers: { "Content-Type": "application/json" },
|
|
2941
|
-
body: JSON.stringify(payload),
|
|
2942
|
-
});
|
|
2943
|
-
|
|
2944
|
-
const data = await response.json();
|
|
2945
|
-
if (!response.ok) {
|
|
2946
|
-
setLogs(data.error || t("failedStartRun"));
|
|
2947
|
-
return;
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
currentSessionId = data.sessionId;
|
|
2951
|
-
currentRunStatus = "running";
|
|
2952
|
-
updateStopRunButton();
|
|
2953
|
-
sessionSelectEl.value = currentSessionId;
|
|
2954
|
-
runMetaEl.textContent = `${t("runningStatus").replace("...", "")} · ${currentSessionId}`;
|
|
2955
|
-
await refreshSessions();
|
|
2956
|
-
await refreshChat();
|
|
2957
|
-
await refreshWorkspaceChanges();
|
|
2958
|
-
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
2959
|
-
|
|
2960
|
-
if (pollTimer) clearInterval(pollTimer);
|
|
2961
|
-
pollTimer = setInterval(refreshRun, 1500);
|
|
2962
|
-
await refreshRun();
|
|
3123
|
+
await startRunFromGoal(goal);
|
|
2963
3124
|
});
|
|
2964
3125
|
|
|
2965
3126
|
sessionSelectEl.addEventListener("change", async () => {
|
|
2966
3127
|
currentSessionId = sessionSelectEl.value;
|
|
2967
3128
|
if (!currentSessionId) {
|
|
2968
|
-
|
|
2969
|
-
pendingInboxItems = [];
|
|
2970
|
-
pendingAfterFinishItems = [];
|
|
2971
|
-
updateStopRunButton();
|
|
2972
|
-
runMetaEl.textContent = "";
|
|
2973
|
-
setLogs(t("noRunSelected"), "empty");
|
|
2974
|
-
renderChat([]);
|
|
2975
|
-
renderPendingMessages();
|
|
2976
|
-
artifactItems = [];
|
|
2977
|
-
selectedArtifactId = "";
|
|
2978
|
-
renderArtifactShell();
|
|
3129
|
+
clearCurrentSessionScope({ focus: false });
|
|
2979
3130
|
return;
|
|
2980
3131
|
}
|
|
2981
3132
|
await refreshRun();
|
|
@@ -2986,17 +3137,17 @@ sessionSelectEl.addEventListener("change", async () => {
|
|
|
2986
3137
|
chatFormEl.addEventListener("submit", async (event) => {
|
|
2987
3138
|
event.preventDefault();
|
|
2988
3139
|
|
|
2989
|
-
if (!currentSessionId) {
|
|
2990
|
-
chatStatusEl.textContent = t("selectSessionFirst");
|
|
2991
|
-
return;
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
3140
|
const content = chatInputEl.value.trim();
|
|
2995
3141
|
if (!content) {
|
|
2996
3142
|
chatStatusEl.textContent = t("messageRequired");
|
|
2997
3143
|
return;
|
|
2998
3144
|
}
|
|
2999
3145
|
|
|
3146
|
+
if (!currentSessionId) {
|
|
3147
|
+
await startRunFromGoal(content, { clearComposer: true });
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3000
3151
|
chatStatusEl.textContent = t("sendingStatus");
|
|
3001
3152
|
|
|
3002
3153
|
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/messages`, {
|
|
@@ -3015,8 +3166,8 @@ chatFormEl.addEventListener("submit", async (event) => {
|
|
|
3015
3166
|
}
|
|
3016
3167
|
|
|
3017
3168
|
currentSessionId = data.sessionId;
|
|
3018
|
-
|
|
3019
|
-
updateStopRunButton();
|
|
3169
|
+
if (!data.queued) setRunStatus("running", { sessionId: data.sessionId, announce: true });
|
|
3170
|
+
else updateStopRunButton();
|
|
3020
3171
|
chatInputEl.value = "";
|
|
3021
3172
|
chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
|
|
3022
3173
|
await refreshSessions();
|
|
@@ -3047,6 +3198,7 @@ async function loadConfig() {
|
|
|
3047
3198
|
defaults.mock = data.defaults?.mock?.model || defaults.mock;
|
|
3048
3199
|
|
|
3049
3200
|
applyLanguage(prefs.language || normalizeLanguage(navigator.language || "en"), { persist: false });
|
|
3201
|
+
refreshProviderDropdowns();
|
|
3050
3202
|
|
|
3051
3203
|
routingModeField.value = prefs.routingMode || "smart";
|
|
3052
3204
|
providerField.value = prefs.provider || "deepseek";
|
|
@@ -3107,6 +3259,7 @@ async function loadConfig() {
|
|
|
3107
3259
|
renderKeyStatus(data.keyStatus);
|
|
3108
3260
|
renderProjectStatus(data.project);
|
|
3109
3261
|
renderWrapperStatus(data.wrappers || []);
|
|
3262
|
+
refreshProviderDropdowns();
|
|
3110
3263
|
renderWorkspacePanel(data.workspace, []);
|
|
3111
3264
|
renderMcpPanel(data.mcp || null);
|
|
3112
3265
|
await refreshWorkspaceChanges();
|
package/public/index.html
CHANGED
|
@@ -213,6 +213,7 @@
|
|
|
213
213
|
<select id="session-select" class="session-select">
|
|
214
214
|
<option value="" data-i18n="noSessionSelected">No session selected</option>
|
|
215
215
|
</select>
|
|
216
|
+
<button id="new-session" class="icon-button" type="button" data-i18n="newSessionButton">New</button>
|
|
216
217
|
<button
|
|
217
218
|
id="open-artifacts"
|
|
218
219
|
class="icon-button artifact-button"
|
|
@@ -234,6 +235,11 @@
|
|
|
234
235
|
</button>
|
|
235
236
|
</div>
|
|
236
237
|
</div>
|
|
238
|
+
<div id="run-state" class="run-state" data-status="idle" role="status" aria-live="polite">
|
|
239
|
+
<span id="run-state-dot" class="run-state-dot" aria-hidden="true"></span>
|
|
240
|
+
<strong id="run-state-text" data-i18n="runStateIdle">Ready</strong>
|
|
241
|
+
<span id="run-state-detail" class="subtle" data-i18n="runStateIdleDetail">Select a conversation or type a new goal below.</span>
|
|
242
|
+
</div>
|
|
237
243
|
<form id="chat-form" class="chat-form">
|
|
238
244
|
<textarea
|
|
239
245
|
id="chat-input"
|
|
@@ -245,7 +251,7 @@
|
|
|
245
251
|
Enter sends · Shift+Enter adds a newline · use buttons for pipe/queue control
|
|
246
252
|
</p>
|
|
247
253
|
<div class="actions">
|
|
248
|
-
<button type="submit" data-i18n="sendMessageButton">Send message</button>
|
|
254
|
+
<button id="chat-submit" type="submit" data-i18n="sendMessageButton">Send message</button>
|
|
249
255
|
<button id="pipe-message" type="button" class="secondary" data-i18n="pipeMessageButton">Pipe to run</button>
|
|
250
256
|
<button id="queue-after-finish" type="button" class="secondary" data-i18n="queueAfterFinishButton">
|
|
251
257
|
Queue after finish
|
|
@@ -311,6 +317,7 @@
|
|
|
311
317
|
</section>
|
|
312
318
|
</section>
|
|
313
319
|
</main>
|
|
320
|
+
<div id="toast-region" class="toast-region" aria-live="polite" aria-atomic="true"></div>
|
|
314
321
|
|
|
315
322
|
<dialog id="settings-modal" class="modal settings-modal">
|
|
316
323
|
<section class="modal-card settings-modal-card" aria-labelledby="settings-modal-title">
|
package/public/styles.css
CHANGED
|
@@ -242,6 +242,8 @@ h1 {
|
|
|
242
242
|
.role-editor-grid {
|
|
243
243
|
display: grid;
|
|
244
244
|
gap: 10px;
|
|
245
|
+
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
|
246
|
+
align-items: start;
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
.role-editor-grid label {
|
|
@@ -806,6 +808,83 @@ button.danger {
|
|
|
806
808
|
max-width: none;
|
|
807
809
|
}
|
|
808
810
|
|
|
811
|
+
.run-state {
|
|
812
|
+
display: grid;
|
|
813
|
+
grid-template-columns: auto auto minmax(0, 1fr);
|
|
814
|
+
gap: 8px;
|
|
815
|
+
align-items: center;
|
|
816
|
+
padding: 10px 12px;
|
|
817
|
+
border: 1px solid rgba(15, 118, 110, 0.16);
|
|
818
|
+
border-radius: 14px;
|
|
819
|
+
background: linear-gradient(135deg, rgba(255, 253, 250, 0.9), rgba(240, 253, 250, 0.76));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.run-state[data-status="running"] {
|
|
823
|
+
border-color: rgba(15, 118, 110, 0.34);
|
|
824
|
+
background: linear-gradient(135deg, rgba(204, 251, 241, 0.9), rgba(254, 243, 199, 0.62));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.run-state[data-status="failed"],
|
|
828
|
+
.run-state[data-status="stopped"] {
|
|
829
|
+
border-color: rgba(185, 28, 28, 0.22);
|
|
830
|
+
background: linear-gradient(135deg, rgba(254, 242, 242, 0.92), rgba(255, 251, 235, 0.7));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.run-state-dot {
|
|
834
|
+
width: 10px;
|
|
835
|
+
height: 10px;
|
|
836
|
+
border-radius: 999px;
|
|
837
|
+
background: #94a3b8;
|
|
838
|
+
box-shadow: 0 0 0 4px rgba(148, 163, 184, 0.14);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.run-state[data-status="running"] .run-state-dot {
|
|
842
|
+
background: #0f766e;
|
|
843
|
+
box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.16);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.run-state[data-status="finished"] .run-state-dot {
|
|
847
|
+
background: #15803d;
|
|
848
|
+
box-shadow: 0 0 0 4px rgba(21, 128, 61, 0.15);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.run-state[data-status="failed"] .run-state-dot,
|
|
852
|
+
.run-state[data-status="stopped"] .run-state-dot {
|
|
853
|
+
background: #b91c1c;
|
|
854
|
+
box-shadow: 0 0 0 4px rgba(185, 28, 28, 0.13);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.run-state strong {
|
|
858
|
+
color: var(--ink);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.toast-region {
|
|
862
|
+
position: fixed;
|
|
863
|
+
right: 18px;
|
|
864
|
+
bottom: 18px;
|
|
865
|
+
z-index: 60;
|
|
866
|
+
display: grid;
|
|
867
|
+
width: min(360px, calc(100vw - 36px));
|
|
868
|
+
gap: 10px;
|
|
869
|
+
pointer-events: none;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.toast {
|
|
873
|
+
padding: 12px 14px;
|
|
874
|
+
border: 1px solid rgba(15, 118, 110, 0.22);
|
|
875
|
+
border-radius: 16px;
|
|
876
|
+
background: rgba(255, 253, 250, 0.96);
|
|
877
|
+
box-shadow: 0 18px 48px rgba(36, 28, 15, 0.16);
|
|
878
|
+
color: var(--ink);
|
|
879
|
+
font-size: 0.9rem;
|
|
880
|
+
pointer-events: auto;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.toast[data-kind="error"] {
|
|
884
|
+
border-color: rgba(185, 28, 28, 0.25);
|
|
885
|
+
background: rgba(254, 242, 242, 0.96);
|
|
886
|
+
}
|
|
887
|
+
|
|
809
888
|
.logs {
|
|
810
889
|
width: 100%;
|
|
811
890
|
min-width: 0;
|
|
@@ -1313,6 +1392,8 @@ button.danger {
|
|
|
1313
1392
|
.settings-modal-card {
|
|
1314
1393
|
max-height: min(860px, calc(100vh - 28px));
|
|
1315
1394
|
overflow: auto;
|
|
1395
|
+
display: grid;
|
|
1396
|
+
gap: 14px;
|
|
1316
1397
|
}
|
|
1317
1398
|
|
|
1318
1399
|
.settings-section {
|
|
@@ -1699,7 +1780,8 @@ button.danger {
|
|
|
1699
1780
|
}
|
|
1700
1781
|
|
|
1701
1782
|
.settings-grid,
|
|
1702
|
-
.settings-checks
|
|
1783
|
+
.settings-checks,
|
|
1784
|
+
.role-editor-grid {
|
|
1703
1785
|
grid-template-columns: 1fr;
|
|
1704
1786
|
}
|
|
1705
1787
|
}
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { SessionStore } from "../src/session-store.js";
|
|
8
8
|
import { projectPaths, sessionStoreOptions } from "../src/project.js";
|
|
9
|
+
import { WebDatabase } from "../src/web-db.js";
|
|
9
10
|
|
|
10
11
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
12
|
const fixtureMcpServer = path.join(repoRoot, "scripts", "fixtures", "mcp-stdio-smoke-server.mjs");
|
|
@@ -95,6 +96,9 @@ try {
|
|
|
95
96
|
if (chatThreadIndex < 0 || chatPendingIndex < 0 || chatPendingIndex < chatThreadIndex) {
|
|
96
97
|
throw new Error("pending message panel must render below the chat thread");
|
|
97
98
|
}
|
|
99
|
+
for (const marker of ['id="new-session"', 'id="run-state"', 'id="toast-region"', 'id="chat-submit"']) {
|
|
100
|
+
if (!webAppHtml.includes(marker)) throw new Error(`web UI is missing ${marker}`);
|
|
101
|
+
}
|
|
98
102
|
|
|
99
103
|
const config = await fetchJson("/api/config");
|
|
100
104
|
if (!config.keyStatus?.mock) throw new Error("mock provider is not advertised by /api/config");
|
|
@@ -337,6 +341,35 @@ try {
|
|
|
337
341
|
const changes = await fetchJson("/api/workspace/changes");
|
|
338
342
|
if (!Array.isArray(changes.activity)) throw new Error("workspace changes endpoint returned an invalid payload");
|
|
339
343
|
|
|
344
|
+
const staleSessionId = "stale-running-smoke";
|
|
345
|
+
const staleStore = new SessionStore(paths.globalSessionsDir, staleSessionId, sessionStoreOptions(runtimeDir, staleSessionId));
|
|
346
|
+
await staleStore.ensure();
|
|
347
|
+
await staleStore.appendEvent("session.finished", {
|
|
348
|
+
result: "stale running session reconciled",
|
|
349
|
+
mode: "smoke",
|
|
350
|
+
});
|
|
351
|
+
const staleDb = new WebDatabase(runtimeDir);
|
|
352
|
+
staleDb.upsertSession({
|
|
353
|
+
sessionId: staleSessionId,
|
|
354
|
+
provider: "mock",
|
|
355
|
+
model: "mock-agent",
|
|
356
|
+
goal: "Synthetic stale running session.",
|
|
357
|
+
status: "running",
|
|
358
|
+
startedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
359
|
+
updatedAt: new Date(Date.now() - 30_000).toISOString(),
|
|
360
|
+
endedAt: "",
|
|
361
|
+
result: "",
|
|
362
|
+
error: "",
|
|
363
|
+
projectRoot: runtimeDir,
|
|
364
|
+
commandCwd: runtimeDir,
|
|
365
|
+
projectSessionsDir: paths.sessionsDir,
|
|
366
|
+
sessionDir: staleStore.sessionDir,
|
|
367
|
+
});
|
|
368
|
+
const reconciledStaleRun = await fetchJson(`/api/runs/${encodeURIComponent(staleSessionId)}`);
|
|
369
|
+
if (reconciledStaleRun.status !== "finished" || !reconciledStaleRun.result.includes("reconciled")) {
|
|
370
|
+
throw new Error(`orphaned running session was not reconciled: ${JSON.stringify(reconciledStaleRun)}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
340
373
|
const canvasRunStart = await fetchJson("/api/runs", {
|
|
341
374
|
method: "POST",
|
|
342
375
|
headers: { "Content-Type": "application/json" },
|
|
@@ -443,6 +476,7 @@ try {
|
|
|
443
476
|
"POST /api/sessions/:id/auto-title",
|
|
444
477
|
"DELETE /api/sessions/:id",
|
|
445
478
|
"/api/workspace/changes",
|
|
479
|
+
"stale running status reconciliation",
|
|
446
480
|
"/api/sessions/:id/artifacts",
|
|
447
481
|
"/api/sessions/:id/artifacts/:artifactId",
|
|
448
482
|
"/api/sessions/:id/artifacts/:artifactId/raw",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-ui-"));
|
|
11
|
+
const agintiflowHome = path.join(runtimeDir, ".agintiflow-home");
|
|
12
|
+
const port = 47000 + Math.floor(Math.random() * 1000);
|
|
13
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
14
|
+
const server = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(port), "--host", "127.0.0.1"], {
|
|
15
|
+
cwd: runtimeDir,
|
|
16
|
+
env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
AGINTIFLOW_HOME: agintiflowHome,
|
|
19
|
+
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
20
|
+
},
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
server.stdout.on("data", (chunk) => {
|
|
27
|
+
stdout += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
server.stderr.on("data", (chunk) => {
|
|
30
|
+
stderr += chunk.toString();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function delay(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function waitForHealth() {
|
|
38
|
+
const deadline = Date.now() + 15000;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
if (server.exitCode !== null) break;
|
|
41
|
+
try {
|
|
42
|
+
const health = await fetch(`${baseUrl}/health`).then((response) => response.json());
|
|
43
|
+
if (health.ok) return health;
|
|
44
|
+
} catch {
|
|
45
|
+
await delay(250);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`web server did not become healthy. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function waitForRunState(page, status, timeout = 20000) {
|
|
52
|
+
await page.waitForFunction(
|
|
53
|
+
(expected) => document.querySelector("#run-state")?.dataset.status === expected,
|
|
54
|
+
status,
|
|
55
|
+
{ timeout }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function waitForTerminalRunState(page, timeout = 30000) {
|
|
60
|
+
await page.waitForFunction(
|
|
61
|
+
() => ["finished", "failed", "stopped"].includes(document.querySelector("#run-state")?.dataset.status || ""),
|
|
62
|
+
null,
|
|
63
|
+
{ timeout }
|
|
64
|
+
);
|
|
65
|
+
return page.locator("#run-state").evaluate((node) => node.dataset.status || "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function optionValues(page, selector) {
|
|
69
|
+
return page.locator(selector).evaluate((node) => [...node.options].map((option) => option.value));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let browser;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await waitForHealth();
|
|
76
|
+
browser = await chromium.launch({ headless: true });
|
|
77
|
+
const page = await browser.newPage();
|
|
78
|
+
const runPayloads = [];
|
|
79
|
+
page.on("request", (request) => {
|
|
80
|
+
if (request.url().endsWith("/api/runs") && request.method() === "POST") {
|
|
81
|
+
runPayloads.push(request.postData() || "");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
|
85
|
+
await page.waitForSelector("#chat-input");
|
|
86
|
+
|
|
87
|
+
const initialSubmit = await page.locator("#chat-submit").innerText();
|
|
88
|
+
if (!/start new run/i.test(initialSubmit)) throw new Error(`composer did not default to new-run mode: ${initialSubmit}`);
|
|
89
|
+
if (await page.locator("#stop-run").isVisible()) throw new Error("stop button is visible before a run starts");
|
|
90
|
+
|
|
91
|
+
await page.selectOption("#routingMode", "manual");
|
|
92
|
+
await page.selectOption("#provider", "mock");
|
|
93
|
+
await page.selectOption("#model", "mock-agent");
|
|
94
|
+
await page.fill("#maxSteps", "4");
|
|
95
|
+
await page.fill("#chat-input", "Say hello from the web UI composer in one concise sentence.");
|
|
96
|
+
await page.click("#chat-submit");
|
|
97
|
+
await waitForRunState(page, "running");
|
|
98
|
+
if (!(await page.locator("#stop-run").isVisible())) throw new Error("stop button did not appear while run was active");
|
|
99
|
+
await page.click("#stop-run");
|
|
100
|
+
await waitForTerminalRunState(page);
|
|
101
|
+
if (await page.locator("#stop-run").isVisible()) throw new Error("stop button stayed visible after terminal run state");
|
|
102
|
+
if (!(await page.locator(".toast").first().isVisible().catch(() => false))) {
|
|
103
|
+
throw new Error("running status toast did not render");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await page.click("#new-session");
|
|
107
|
+
await waitForRunState(page, "idle");
|
|
108
|
+
const resetSubmit = await page.locator("#chat-submit").innerText();
|
|
109
|
+
if (!/start new run/i.test(resetSubmit)) throw new Error("new session did not reset composer to start mode");
|
|
110
|
+
|
|
111
|
+
await page.click("#open-settings");
|
|
112
|
+
await page.waitForSelector("#settings-modal[open]");
|
|
113
|
+
const routeProviders = await optionValues(page, "#routeProvider");
|
|
114
|
+
for (const provider of ["deepseek", "openai", "qwen", "venice", "mock"]) {
|
|
115
|
+
if (!routeProviders.includes(provider)) throw new Error(`settings route provider dropdown is missing ${provider}`);
|
|
116
|
+
}
|
|
117
|
+
const wrapperOptions = await optionValues(page, "#preferredWrapper");
|
|
118
|
+
for (const wrapper of ["codex", "claude", "gemini", "copilot", "qwen"]) {
|
|
119
|
+
if (!wrapperOptions.includes(wrapper)) throw new Error(`preferred wrapper dropdown is missing ${wrapper}`);
|
|
120
|
+
}
|
|
121
|
+
await page.click("#done-settings");
|
|
122
|
+
|
|
123
|
+
console.log(
|
|
124
|
+
JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
ok: true,
|
|
127
|
+
runtimeDir,
|
|
128
|
+
checks: [
|
|
129
|
+
"composer-starts-new-run",
|
|
130
|
+
"running-stop-button-visible",
|
|
131
|
+
"running-status-toast",
|
|
132
|
+
"terminal-stop-button-hidden",
|
|
133
|
+
"new-session-resets-scope",
|
|
134
|
+
"settings-provider-dropdowns",
|
|
135
|
+
"settings-wrapper-dropdowns",
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
null,
|
|
139
|
+
2
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
} finally {
|
|
143
|
+
await browser?.close().catch(() => {});
|
|
144
|
+
server.kill("SIGTERM");
|
|
145
|
+
await delay(150);
|
|
146
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
147
|
+
}
|
package/web.js
CHANGED
|
@@ -105,18 +105,47 @@ async function loadStoredRun(sessionId) {
|
|
|
105
105
|
|
|
106
106
|
const store = sessionStore(sessionId);
|
|
107
107
|
const events = await store.loadEvents();
|
|
108
|
+
const terminalEvent = [...events]
|
|
109
|
+
.reverse()
|
|
110
|
+
.find((event) => ["session.finished", "session.failed", "session.stopped"].includes(event.type));
|
|
111
|
+
const orphanedRunning = meta.status === "running" && !runs.has(sessionId);
|
|
112
|
+
const reconciled = { ...meta };
|
|
113
|
+
|
|
114
|
+
if (orphanedRunning && terminalEvent) {
|
|
115
|
+
reconciled.status =
|
|
116
|
+
terminalEvent.type === "session.finished" ? "finished" : terminalEvent.type === "session.failed" ? "failed" : "stopped";
|
|
117
|
+
reconciled.endedAt = reconciled.endedAt || terminalEvent.timestamp || new Date().toISOString();
|
|
118
|
+
reconciled.updatedAt = terminalEvent.timestamp || reconciled.updatedAt || reconciled.endedAt;
|
|
119
|
+
reconciled.result =
|
|
120
|
+
reconciled.status === "finished" ? String(terminalEvent.data?.result || reconciled.result || "") : reconciled.result || "";
|
|
121
|
+
reconciled.error =
|
|
122
|
+
reconciled.status === "failed"
|
|
123
|
+
? String(terminalEvent.data?.error || terminalEvent.data?.reason || reconciled.error || "Run failed.")
|
|
124
|
+
: reconciled.status === "stopped"
|
|
125
|
+
? String(terminalEvent.data?.reason || reconciled.error || "Run stopped.")
|
|
126
|
+
: reconciled.error || "";
|
|
127
|
+
db.upsertSession(reconciled);
|
|
128
|
+
} else if (orphanedRunning) {
|
|
129
|
+
reconciled.status = "stopped";
|
|
130
|
+
reconciled.endedAt = reconciled.endedAt || new Date().toISOString();
|
|
131
|
+
reconciled.updatedAt = reconciled.endedAt;
|
|
132
|
+
reconciled.error =
|
|
133
|
+
reconciled.error ||
|
|
134
|
+
"Run is not active in this web process. It may have ended before the web server restarted.";
|
|
135
|
+
db.upsertSession(reconciled);
|
|
136
|
+
}
|
|
108
137
|
|
|
109
138
|
return {
|
|
110
|
-
sessionId:
|
|
111
|
-
status:
|
|
112
|
-
provider:
|
|
113
|
-
model:
|
|
114
|
-
goal:
|
|
115
|
-
title:
|
|
116
|
-
startedAt:
|
|
117
|
-
endedAt:
|
|
118
|
-
result:
|
|
119
|
-
error:
|
|
139
|
+
sessionId: reconciled.sessionId,
|
|
140
|
+
status: reconciled.status,
|
|
141
|
+
provider: reconciled.provider,
|
|
142
|
+
model: reconciled.model,
|
|
143
|
+
goal: reconciled.goal,
|
|
144
|
+
title: reconciled.title || "",
|
|
145
|
+
startedAt: reconciled.startedAt,
|
|
146
|
+
endedAt: reconciled.endedAt || null,
|
|
147
|
+
result: reconciled.result || "",
|
|
148
|
+
error: reconciled.error || "",
|
|
120
149
|
logs: mapEventLogs(events).slice(-300),
|
|
121
150
|
};
|
|
122
151
|
}
|
|
@@ -562,8 +591,8 @@ async function ensureNotRunning(sessionId) {
|
|
|
562
591
|
throw new Error("This session is already running.");
|
|
563
592
|
}
|
|
564
593
|
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
594
|
+
const stored = await loadStoredRun(sessionId);
|
|
595
|
+
if (stored?.status === "running") {
|
|
567
596
|
throw new Error("This session is already running.");
|
|
568
597
|
}
|
|
569
598
|
}
|
|
@@ -717,12 +746,16 @@ function wireRun(record, config) {
|
|
|
717
746
|
onEvent: (type, data = {}) => push("event", type, data),
|
|
718
747
|
})
|
|
719
748
|
.then((result) => {
|
|
720
|
-
record.status = result?.stopped ? "stopped" : "finished";
|
|
749
|
+
record.status = result?.failed ? "failed" : result?.stopped ? "stopped" : "finished";
|
|
721
750
|
record.result = result?.result || "";
|
|
722
|
-
record.error = result?.
|
|
751
|
+
record.error = result?.failed
|
|
752
|
+
? result.reason || result.result || "Run failed."
|
|
753
|
+
: result?.stopped
|
|
754
|
+
? result.reason || "Run stopped before finish."
|
|
755
|
+
: "";
|
|
723
756
|
record.updatedAt = new Date().toISOString();
|
|
724
757
|
record.endedAt = new Date().toISOString();
|
|
725
|
-
push("session", result?.stopped ? "Run stopped" : "Run finished", {
|
|
758
|
+
push("session", result?.failed ? "Run failed" : result?.stopped ? "Run stopped" : "Run finished", {
|
|
726
759
|
result: record.result,
|
|
727
760
|
reason: result?.reason || "",
|
|
728
761
|
});
|