@lazyingart/agintiflow 0.1.0 → 0.3.0
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/AGENTS.md +6 -3
- package/README.md +68 -15
- package/docker/sandbox.Dockerfile +8 -0
- package/package.json +11 -3
- package/public/app.js +785 -11
- package/public/index.html +166 -20
- package/public/styles.css +677 -15
- package/scripts/setup-agent-toolchain-docker.sh +51 -0
- package/scripts/smoke-coding-tools.js +138 -0
- package/scripts/smoke-toolchain-docker.js +185 -0
- package/scripts/smoke-web-api.js +217 -0
- package/src/agent-runner.js +286 -21
- package/src/artifact-tunnel.js +401 -0
- package/src/cli.js +47 -3
- package/src/command-policy.js +21 -0
- package/src/config.js +9 -2
- package/src/docker-sandbox.js +38 -8
- package/src/guardrails.js +31 -0
- package/src/model-client.js +300 -6
- package/src/model-routing.js +40 -0
- package/src/session-store.js +4 -0
- package/src/tool-wrappers.js +6 -0
- package/src/web-db.js +48 -5
- package/src/workspace-tools.js +438 -0
- package/web.js +265 -14
package/public/app.js
CHANGED
|
@@ -20,15 +20,18 @@ const translations = {
|
|
|
20
20
|
intro:
|
|
21
21
|
"Web-first agent platform with smart model routing, resumable runs, guarded tools, and optional external agent wrappers.",
|
|
22
22
|
routingModeLabel: "Routing policy",
|
|
23
|
-
routingSmartOption: "Smart:
|
|
23
|
+
routingSmartOption: "Smart: flash/pro/wrappers",
|
|
24
24
|
routingFastOption: "DeepSeek v4 flash",
|
|
25
25
|
routingComplexOption: "DeepSeek v4 pro",
|
|
26
26
|
routingManualOption: "Manual provider/model",
|
|
27
|
-
routingHintSmart:
|
|
27
|
+
routingHintSmart:
|
|
28
|
+
"Smart routing uses DeepSeek v4 flash for short work, DeepSeek v4 pro for complex tasks, and the selected wrapper only when wrapper tools are enabled.",
|
|
28
29
|
routingHintFast: "Fast route: DeepSeek v4 flash for normal browser, shell, and short coding tasks.",
|
|
29
30
|
routingHintComplex: "Complex route: DeepSeek v4 pro for deeper implementation, debugging, and design work.",
|
|
30
31
|
routingHintManual: "Manual route uses the provider and model fields exactly as entered.",
|
|
32
|
+
modelRouteStatus: "Active route",
|
|
31
33
|
providerLabel: "Provider",
|
|
34
|
+
mockProviderOption: "Mock local",
|
|
32
35
|
modelLabel: "Model",
|
|
33
36
|
goalLabel: "Goal",
|
|
34
37
|
goalPlaceholder: "Open a site and summarize it, or use run_command for simple terminal inspection.",
|
|
@@ -46,9 +49,15 @@ const translations = {
|
|
|
46
49
|
packageBlockOption: "Blocked",
|
|
47
50
|
packagePromptOption: "Require approval",
|
|
48
51
|
packageAllowOption: "Approved in Docker",
|
|
52
|
+
packageWarningBlock: "Package, conda, venv, and npm setup commands are blocked.",
|
|
53
|
+
packageWarningPrompt: "Package or environment setup will stop for approval before it can run.",
|
|
54
|
+
packageWarningAllow: "Package setup is approved only in Docker workspace-write mode.",
|
|
55
|
+
packageWarningHostAllow: "Host mode cannot run approved package setup; switch to Docker workspace-write.",
|
|
49
56
|
headlessLabel: "Headless browser",
|
|
50
57
|
shellToolLabel: "Enable shell tool",
|
|
58
|
+
fileToolLabel: "Enable file tools",
|
|
51
59
|
wrapperToolLabel: "Enable agent wrappers",
|
|
60
|
+
preferredWrapperLabel: "Preferred wrapper",
|
|
52
61
|
dockerSandboxLabel: "Use Docker sandbox",
|
|
53
62
|
allowPasswordsLabel: "Allow password typing",
|
|
54
63
|
allowDestructiveLabel: "Allow destructive actions",
|
|
@@ -67,6 +76,15 @@ const translations = {
|
|
|
67
76
|
youLabel: "You",
|
|
68
77
|
keysLabel: "Keys",
|
|
69
78
|
wrappersLabel: "Wrappers",
|
|
79
|
+
selectedWrapperLabel: "Selected",
|
|
80
|
+
wrappersOffLabel: "wrapper tools off",
|
|
81
|
+
wrapperCapabilityTitle: "Agent wrappers",
|
|
82
|
+
workspaceCapabilityTitle: "Workspace files",
|
|
83
|
+
workspaceToolsLabel: "File tools",
|
|
84
|
+
workspaceChangesEmpty: "No file changes yet.",
|
|
85
|
+
changedLabel: "changed",
|
|
86
|
+
blockedLabel: "blocked",
|
|
87
|
+
mockLabel: "Mock",
|
|
70
88
|
sandboxStatusTitle: "Sandbox status",
|
|
71
89
|
sandboxStatusEmpty: "Not checked yet.",
|
|
72
90
|
sandboxLogsEmpty: "No sandbox logs yet.",
|
|
@@ -85,6 +103,40 @@ const translations = {
|
|
|
85
103
|
sendingStatus: "Sending...",
|
|
86
104
|
runningStatus: "Running...",
|
|
87
105
|
failedContinue: "Failed to continue the conversation.",
|
|
106
|
+
conversationMenuLabel: "Manage conversations",
|
|
107
|
+
manageConversationsTitle: "Manage conversations",
|
|
108
|
+
manageConversationsHelp: "Rename, auto-rename, or delete saved chat history.",
|
|
109
|
+
conversationTitleLabel: "Conversation title",
|
|
110
|
+
conversationTitlePlaceholder: "Short title for this conversation",
|
|
111
|
+
autoRenameButton: "Auto rename",
|
|
112
|
+
renameButton: "Rename",
|
|
113
|
+
deleteButton: "Delete",
|
|
114
|
+
cancelButton: "Cancel",
|
|
115
|
+
noSessionsToManage: "No saved conversations yet.",
|
|
116
|
+
titleRequired: "Title is required.",
|
|
117
|
+
sessionRenamed: "Conversation renamed.",
|
|
118
|
+
sessionDeleted: "Conversation deleted.",
|
|
119
|
+
deleteConfirm: "Delete this conversation and its local session history?",
|
|
120
|
+
manageSessionFailed: "Failed to update conversation.",
|
|
121
|
+
artifactTunnelLabel: "Open canvas and artifacts",
|
|
122
|
+
artifactTunnelTitle: "Canvas & artifacts",
|
|
123
|
+
artifactTunnelHelp: "Agent-selected canvas items, generated files, screenshots, diffs, and final answers.",
|
|
124
|
+
artifactCanvasTab: "Canvas",
|
|
125
|
+
artifactPdfTab: "PDF Reader",
|
|
126
|
+
artifactExplorerTab: "Explorer",
|
|
127
|
+
artifactNotificationsTab: "Notifications",
|
|
128
|
+
artifactListTitle: "Artifacts",
|
|
129
|
+
artifactMarkSeenButton: "Mark seen",
|
|
130
|
+
artifactEmpty: "No artifacts yet.",
|
|
131
|
+
artifactViewerEmptyTitle: "Nothing selected",
|
|
132
|
+
artifactViewerEmpty: "Select an artifact to preview it here.",
|
|
133
|
+
artifactLoading: "Loading artifact...",
|
|
134
|
+
artifactLoadFailed: "Failed to load artifact.",
|
|
135
|
+
artifactSeenUpdated: "Notifications marked as seen.",
|
|
136
|
+
artifactDownloadLabel: "Download artifact",
|
|
137
|
+
artifactDownloadPreparing: "Preparing download...",
|
|
138
|
+
artifactDownloadReady: "Download started.",
|
|
139
|
+
artifactDownloadFailed: "Download failed.",
|
|
88
140
|
},
|
|
89
141
|
ar: {
|
|
90
142
|
documentTitle: "AgInTiFlow",
|
|
@@ -564,12 +616,20 @@ const routingModeField = document.querySelector("#routingMode");
|
|
|
564
616
|
const providerField = document.querySelector("#provider");
|
|
565
617
|
const modelField = document.querySelector("#model");
|
|
566
618
|
const routingHintEl = document.querySelector("#routing-hint");
|
|
619
|
+
const modelRouteStatusEl = document.querySelector("#model-route-status");
|
|
567
620
|
const sandboxModeField = document.querySelector("#sandboxMode");
|
|
568
621
|
const packageInstallPolicyField = document.querySelector("#packageInstallPolicy");
|
|
622
|
+
const packageWarningEl = document.querySelector("#package-warning");
|
|
569
623
|
const logsEl = document.querySelector("#logs");
|
|
570
624
|
const runMetaEl = document.querySelector("#run-meta");
|
|
571
625
|
const keyStatusEl = document.querySelector("#key-status");
|
|
626
|
+
const allowWrapperToolsField = document.querySelector("#allowWrapperTools");
|
|
627
|
+
const preferredWrapperField = document.querySelector("#preferredWrapper");
|
|
572
628
|
const wrapperStatusEl = document.querySelector("#wrapper-status");
|
|
629
|
+
const wrapperGridEl = document.querySelector("#wrapper-grid");
|
|
630
|
+
const workspaceStatusEl = document.querySelector("#workspace-status");
|
|
631
|
+
const workspaceToolsEl = document.querySelector("#workspace-tools");
|
|
632
|
+
const workspaceChangesEl = document.querySelector("#workspace-changes");
|
|
573
633
|
const sandboxStatusEl = document.querySelector("#sandbox-status");
|
|
574
634
|
const sandboxLogsEl = document.querySelector("#sandbox-logs");
|
|
575
635
|
const checkSandboxButton = document.querySelector("#check-sandbox");
|
|
@@ -579,12 +639,36 @@ const chatThreadEl = document.querySelector("#chat-thread");
|
|
|
579
639
|
const chatFormEl = document.querySelector("#chat-form");
|
|
580
640
|
const chatInputEl = document.querySelector("#chat-input");
|
|
581
641
|
const chatStatusEl = document.querySelector("#chat-status");
|
|
642
|
+
const manageSessionsButton = document.querySelector("#manage-sessions");
|
|
643
|
+
const openArtifactsButton = document.querySelector("#open-artifacts");
|
|
644
|
+
const artifactBadgeEl = document.querySelector("#artifact-badge");
|
|
645
|
+
const sessionManagerDialog = document.querySelector("#session-manager");
|
|
646
|
+
const closeSessionManagerButton = document.querySelector("#close-session-manager");
|
|
647
|
+
const cancelSessionManagerButton = document.querySelector("#cancel-session-manager");
|
|
648
|
+
const sessionManagerListEl = document.querySelector("#session-manager-list");
|
|
649
|
+
const sessionTitleInputEl = document.querySelector("#session-title-input");
|
|
650
|
+
const sessionManagerStatusEl = document.querySelector("#session-manager-status");
|
|
651
|
+
const autoRenameSessionButton = document.querySelector("#auto-rename-session");
|
|
652
|
+
const renameSessionButton = document.querySelector("#rename-session");
|
|
653
|
+
const deleteSessionButton = document.querySelector("#delete-session");
|
|
654
|
+
const artifactTunnelDialog = document.querySelector("#artifact-tunnel");
|
|
655
|
+
const closeArtifactTunnelButton = document.querySelector("#close-artifact-tunnel");
|
|
656
|
+
const artifactTabsEl = document.querySelector(".artifact-tabs");
|
|
657
|
+
const artifactListEl = document.querySelector("#artifact-list");
|
|
658
|
+
const artifactViewerTitleEl = document.querySelector("#artifact-viewer-title");
|
|
659
|
+
const artifactViewerMetaEl = document.querySelector("#artifact-viewer-meta");
|
|
660
|
+
const artifactViewerKindEl = document.querySelector("#artifact-viewer-kind");
|
|
661
|
+
const artifactViewerBodyEl = document.querySelector("#artifact-viewer-body");
|
|
662
|
+
const artifactStatusEl = document.querySelector("#artifact-status");
|
|
663
|
+
const markArtifactsSeenButton = document.querySelector("#mark-artifacts-seen");
|
|
582
664
|
const translatableNodes = [...document.querySelectorAll("[data-i18n]")];
|
|
583
665
|
const placeholderNodes = [...document.querySelectorAll("[data-i18n-placeholder]")];
|
|
666
|
+
const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
|
|
584
667
|
|
|
585
668
|
const defaults = {
|
|
586
669
|
openai: "gpt-5.4-mini",
|
|
587
670
|
deepseek: "deepseek-v4-flash",
|
|
671
|
+
mock: "mock-agent",
|
|
588
672
|
};
|
|
589
673
|
|
|
590
674
|
let currentLanguage = "en";
|
|
@@ -597,6 +681,13 @@ let lastSessions = [];
|
|
|
597
681
|
let lastKeyStatus = null;
|
|
598
682
|
let lastWrappers = [];
|
|
599
683
|
let lastSandbox = null;
|
|
684
|
+
let lastWorkspace = null;
|
|
685
|
+
let lastWorkspaceActivity = [];
|
|
686
|
+
let managedSessionId = "";
|
|
687
|
+
let artifactItems = [];
|
|
688
|
+
let selectedArtifactId = "";
|
|
689
|
+
let currentArtifactTab = "canvas";
|
|
690
|
+
let artifactUnreadCount = 0;
|
|
600
691
|
|
|
601
692
|
function normalizeLanguage(language) {
|
|
602
693
|
const lower = String(language || "").toLowerCase();
|
|
@@ -619,18 +710,94 @@ function renderKeyStatus(status = lastKeyStatus) {
|
|
|
619
710
|
if (!status) return;
|
|
620
711
|
keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
|
|
621
712
|
status.openai ? t("availableLabel") : t("missingLabel")
|
|
622
|
-
} · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")}
|
|
713
|
+
} · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
|
|
714
|
+
status.mock ? t("availableLabel") : t("missingLabel")
|
|
715
|
+
}`;
|
|
623
716
|
}
|
|
624
717
|
|
|
625
718
|
function renderWrapperStatus(wrappers = lastWrappers) {
|
|
626
719
|
lastWrappers = wrappers || [];
|
|
627
720
|
if (lastWrappers.length === 0) {
|
|
628
721
|
wrapperStatusEl.textContent = "";
|
|
722
|
+
wrapperGridEl.innerHTML = "";
|
|
629
723
|
return;
|
|
630
724
|
}
|
|
631
|
-
|
|
725
|
+
const selectedWrapper = preferredWrapperField?.value || "codex";
|
|
726
|
+
const selected = lastWrappers.find((wrapper) => wrapper.name === selectedWrapper);
|
|
727
|
+
const selectedLabel = selected?.label || selectedWrapper;
|
|
728
|
+
const enabledPrefix = allowWrapperToolsField?.checked ? t("selectedWrapperLabel") : t("wrappersOffLabel");
|
|
729
|
+
wrapperStatusEl.textContent = `${enabledPrefix}: ${selectedLabel} · ${t("wrappersLabel")}: ${lastWrappers
|
|
632
730
|
.map((wrapper) => `${wrapper.label || wrapper.name} ${wrapper.available ? t("availableLabel") : t("missingLabel")}`)
|
|
633
731
|
.join(" · ")}`;
|
|
732
|
+
wrapperGridEl.innerHTML = lastWrappers
|
|
733
|
+
.map(
|
|
734
|
+
(wrapper) => `
|
|
735
|
+
<div class="capability-chip" data-ready="${Boolean(wrapper.available)}" data-selected="${wrapper.name === selectedWrapper}">
|
|
736
|
+
<strong>${escapeHtml(wrapper.label || wrapper.name)}</strong>
|
|
737
|
+
<span>${wrapper.name === selectedWrapper ? `${t("selectedWrapperLabel")} · ` : ""}${wrapper.available ? t("availableLabel") : t("missingLabel")}</span>
|
|
738
|
+
</div>
|
|
739
|
+
`
|
|
740
|
+
)
|
|
741
|
+
.join("");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspaceActivity) {
|
|
745
|
+
lastWorkspace = workspace;
|
|
746
|
+
lastWorkspaceActivity = activity || [];
|
|
747
|
+
|
|
748
|
+
if (!workspace) {
|
|
749
|
+
workspaceStatusEl.textContent = "";
|
|
750
|
+
workspaceToolsEl.innerHTML = "";
|
|
751
|
+
workspaceChangesEl.innerHTML = `<p class="subtle">${t("workspaceChangesEmpty")}</p>`;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
workspaceStatusEl.textContent = `${workspace.enabled ? t("availableLabel") : t("missingLabel")} · ${
|
|
756
|
+
workspace.workspace || ""
|
|
757
|
+
}`;
|
|
758
|
+
workspaceToolsEl.innerHTML = (workspace.tools || [])
|
|
759
|
+
.map(
|
|
760
|
+
(tool) => `
|
|
761
|
+
<div class="capability-chip" data-ready="${Boolean(workspace.enabled)}">
|
|
762
|
+
<strong>${escapeHtml(tool)}</strong>
|
|
763
|
+
<span>${(workspace.writeTools || []).includes(tool) ? "write guarded" : "read guarded"}</span>
|
|
764
|
+
</div>
|
|
765
|
+
`
|
|
766
|
+
)
|
|
767
|
+
.join("");
|
|
768
|
+
|
|
769
|
+
if (!lastWorkspaceActivity.length) {
|
|
770
|
+
workspaceChangesEl.innerHTML = `<p class="subtle">${t("workspaceChangesEmpty")}</p>`;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
workspaceChangesEl.innerHTML = lastWorkspaceActivity
|
|
775
|
+
.slice(0, 6)
|
|
776
|
+
.map((item) => {
|
|
777
|
+
const blocked = item.kind === "blocked";
|
|
778
|
+
const label = blocked ? t("blockedLabel") : t("changedLabel");
|
|
779
|
+
const path = escapeHtml(item.path || "");
|
|
780
|
+
const reason = blocked ? `<div class="subtle">${escapeHtml(item.reason || "")}</div>` : "";
|
|
781
|
+
const diff = item.diff ? `<pre class="change-diff">${escapeHtml(item.diff).slice(0, 1200)}</pre>` : "";
|
|
782
|
+
const hashes =
|
|
783
|
+
item.beforeHash || item.afterHash
|
|
784
|
+
? `<div class="change-meta"><span>before=${escapeHtml((item.beforeHash || "new").slice(0, 10))}</span><span>after=${escapeHtml((item.afterHash || "").slice(0, 10))}</span></div>`
|
|
785
|
+
: "";
|
|
786
|
+
return `
|
|
787
|
+
<article class="change-item ${blocked ? "blocked" : ""}">
|
|
788
|
+
<div class="change-meta">
|
|
789
|
+
<span>${label}</span>
|
|
790
|
+
<span>${escapeHtml(item.toolName || "")}</span>
|
|
791
|
+
<span>${item.at ? new Date(item.at).toLocaleString() : ""}</span>
|
|
792
|
+
</div>
|
|
793
|
+
<strong class="change-path">${path}</strong>
|
|
794
|
+
${reason}
|
|
795
|
+
${hashes}
|
|
796
|
+
${diff}
|
|
797
|
+
</article>
|
|
798
|
+
`;
|
|
799
|
+
})
|
|
800
|
+
.join("");
|
|
634
801
|
}
|
|
635
802
|
|
|
636
803
|
function renderSandboxStatus(status = lastSandbox) {
|
|
@@ -685,6 +852,24 @@ function updateRoutingHint() {
|
|
|
685
852
|
modelField.value = preset.model || modelField.value;
|
|
686
853
|
}
|
|
687
854
|
}
|
|
855
|
+
|
|
856
|
+
modelRouteStatusEl.textContent = `${t("modelRouteStatus")}: ${providerField.value} / ${
|
|
857
|
+
modelField.value.trim() || defaults[providerField.value] || ""
|
|
858
|
+
}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function updatePackageWarning() {
|
|
862
|
+
const policy = packageInstallPolicyField.value || "prompt";
|
|
863
|
+
const sandboxMode = sandboxModeField.value || "host";
|
|
864
|
+
const key =
|
|
865
|
+
policy === "allow" && sandboxMode === "host"
|
|
866
|
+
? "packageWarningHostAllow"
|
|
867
|
+
: policy === "allow"
|
|
868
|
+
? "packageWarningAllow"
|
|
869
|
+
: policy === "block"
|
|
870
|
+
? "packageWarningBlock"
|
|
871
|
+
: "packageWarningPrompt";
|
|
872
|
+
packageWarningEl.textContent = t(key);
|
|
688
873
|
}
|
|
689
874
|
|
|
690
875
|
function applyLanguage(language, { persist = true } = {}) {
|
|
@@ -703,12 +888,20 @@ function applyLanguage(language, { persist = true } = {}) {
|
|
|
703
888
|
node.placeholder = t(node.dataset.i18nPlaceholder);
|
|
704
889
|
}
|
|
705
890
|
|
|
891
|
+
for (const node of ariaLabelNodes) {
|
|
892
|
+
node.setAttribute("aria-label", t(node.dataset.i18nAriaLabel));
|
|
893
|
+
}
|
|
894
|
+
|
|
706
895
|
renderKeyStatus();
|
|
707
896
|
renderWrapperStatus();
|
|
897
|
+
renderWorkspacePanel();
|
|
708
898
|
renderSandboxStatus();
|
|
709
899
|
updateRoutingHint();
|
|
900
|
+
updatePackageWarning();
|
|
710
901
|
renderSessions(lastSessions);
|
|
902
|
+
renderSessionManager();
|
|
711
903
|
renderChat(lastChatEntries);
|
|
904
|
+
renderArtifactShell();
|
|
712
905
|
if (persist) schedulePreferenceSave();
|
|
713
906
|
}
|
|
714
907
|
|
|
@@ -726,7 +919,9 @@ function formPayload() {
|
|
|
726
919
|
packageInstallPolicy: packageInstallPolicyField.value,
|
|
727
920
|
headless: document.querySelector("#headless").checked,
|
|
728
921
|
allowShellTool: document.querySelector("#allowShellTool").checked,
|
|
729
|
-
|
|
922
|
+
allowFileTools: document.querySelector("#allowFileTools").checked,
|
|
923
|
+
allowWrapperTools: allowWrapperToolsField.checked,
|
|
924
|
+
preferredWrapper: preferredWrapperField.value,
|
|
730
925
|
useDockerSandbox: sandboxModeField.value !== "host",
|
|
731
926
|
dockerSandboxImage: document.querySelector("#dockerSandboxImage").value.trim(),
|
|
732
927
|
allowPasswords: document.querySelector("#allowPasswords").checked,
|
|
@@ -753,7 +948,10 @@ function renderLogs(run) {
|
|
|
753
948
|
}
|
|
754
949
|
|
|
755
950
|
function escapeHtml(value) {
|
|
756
|
-
return String(value || "").replace(
|
|
951
|
+
return String(value || "").replace(
|
|
952
|
+
/[&<>"']/g,
|
|
953
|
+
(ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]
|
|
954
|
+
);
|
|
757
955
|
}
|
|
758
956
|
|
|
759
957
|
function renderChat(chatEntries) {
|
|
@@ -781,14 +979,338 @@ function renderChat(chatEntries) {
|
|
|
781
979
|
chatThreadEl.scrollTop = chatThreadEl.scrollHeight;
|
|
782
980
|
}
|
|
783
981
|
|
|
982
|
+
function artifactReadKey(sessionId = currentSessionId) {
|
|
983
|
+
return `agintiflow.artifacts.readIds.${sessionId || "none"}`;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function getArtifactReadIds(sessionId = currentSessionId) {
|
|
987
|
+
try {
|
|
988
|
+
const raw = localStorage.getItem(artifactReadKey(sessionId)) || "[]";
|
|
989
|
+
const parsed = JSON.parse(raw);
|
|
990
|
+
return new Set(Array.isArray(parsed) ? parsed : []);
|
|
991
|
+
} catch {
|
|
992
|
+
return new Set();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function saveArtifactReadIds(readIds, sessionId = currentSessionId) {
|
|
997
|
+
if (!sessionId) return;
|
|
998
|
+
try {
|
|
999
|
+
localStorage.setItem(artifactReadKey(sessionId), JSON.stringify([...readIds].slice(-800)));
|
|
1000
|
+
} catch {
|
|
1001
|
+
// Local storage is optional for notification state.
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function computeUnreadArtifactCount() {
|
|
1006
|
+
const readIds = getArtifactReadIds();
|
|
1007
|
+
return artifactItems.filter((item) => !readIds.has(item.id)).length;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function markArtifactRead(artifactId) {
|
|
1011
|
+
if (!artifactId) return;
|
|
1012
|
+
const readIds = getArtifactReadIds();
|
|
1013
|
+
readIds.add(artifactId);
|
|
1014
|
+
saveArtifactReadIds(readIds);
|
|
1015
|
+
artifactUnreadCount = computeUnreadArtifactCount();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function markAllArtifactsRead() {
|
|
1019
|
+
const readIds = getArtifactReadIds();
|
|
1020
|
+
for (const item of artifactItems) readIds.add(item.id);
|
|
1021
|
+
saveArtifactReadIds(readIds);
|
|
1022
|
+
artifactUnreadCount = 0;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function renderArtifactBadge(count = computeUnreadArtifactCount()) {
|
|
1026
|
+
artifactUnreadCount = Number(count) || 0;
|
|
1027
|
+
if (!artifactBadgeEl) return;
|
|
1028
|
+
artifactBadgeEl.textContent = artifactUnreadCount > 99 ? "99+" : String(artifactUnreadCount);
|
|
1029
|
+
artifactBadgeEl.hidden = artifactUnreadCount <= 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function artifactLabel(item) {
|
|
1033
|
+
const source = item.source ? item.source.replace(/-/g, " ") : "artifact";
|
|
1034
|
+
const when = item.createdAt ? new Date(item.createdAt).toLocaleString() : "";
|
|
1035
|
+
return [item.kind, source, when].filter(Boolean).join(" · ");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function isPdfArtifact(item) {
|
|
1039
|
+
return item?.kind === "pdf" || item?.mime === "application/pdf" || /\.pdf$/i.test(item?.path || "");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function artifactVisibleInCurrentTab(item) {
|
|
1043
|
+
if (currentArtifactTab === "pdf") return isPdfArtifact(item);
|
|
1044
|
+
return item.tab === currentArtifactTab;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function preferredArtifactForCurrentTab(fallbackId = "") {
|
|
1048
|
+
const current = artifactItems.find((item) => item.id === selectedArtifactId && artifactVisibleInCurrentTab(item));
|
|
1049
|
+
if (current) return current;
|
|
1050
|
+
|
|
1051
|
+
const fallback = artifactItems.find((item) => item.id === fallbackId && artifactVisibleInCurrentTab(item));
|
|
1052
|
+
if (fallback) return fallback;
|
|
1053
|
+
|
|
1054
|
+
return artifactItems.find(artifactVisibleInCurrentTab) || null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function renderArtifactList() {
|
|
1058
|
+
if (!artifactListEl) return;
|
|
1059
|
+
const visible = artifactItems.filter(artifactVisibleInCurrentTab);
|
|
1060
|
+
const readIds = getArtifactReadIds();
|
|
1061
|
+
|
|
1062
|
+
document.querySelectorAll(".artifact-tab").forEach((tab) => {
|
|
1063
|
+
tab.dataset.selected = String(tab.dataset.artifactTab === currentArtifactTab);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
if (!currentSessionId) {
|
|
1067
|
+
artifactListEl.innerHTML = `<p class="subtle">${t("selectSessionFirst")}</p>`;
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (visible.length === 0) {
|
|
1072
|
+
artifactListEl.innerHTML = `<p class="subtle">${t("artifactEmpty")}</p>`;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
artifactListEl.innerHTML = visible
|
|
1077
|
+
.map(
|
|
1078
|
+
(item) => {
|
|
1079
|
+
const read = readIds.has(item.id);
|
|
1080
|
+
return `
|
|
1081
|
+
<div class="artifact-row" data-selected="${item.id === selectedArtifactId}" data-read="${read}">
|
|
1082
|
+
<button class="artifact-row-main" type="button" data-artifact-id="${escapeHtml(item.id)}">
|
|
1083
|
+
<span class="artifact-row-top">
|
|
1084
|
+
<strong>${escapeHtml(item.title || item.path || item.kind)}</strong>
|
|
1085
|
+
<span>${read ? escapeHtml(item.kind) : `New · ${escapeHtml(item.kind)}`}</span>
|
|
1086
|
+
</span>
|
|
1087
|
+
<span class="artifact-row-meta">${escapeHtml(artifactLabel(item))}</span>
|
|
1088
|
+
<span class="artifact-row-preview">${escapeHtml(item.preview || item.path || "")}</span>
|
|
1089
|
+
</button>
|
|
1090
|
+
<button
|
|
1091
|
+
class="artifact-menu-button"
|
|
1092
|
+
type="button"
|
|
1093
|
+
data-artifact-download-id="${escapeHtml(item.id)}"
|
|
1094
|
+
aria-label="${escapeHtml(t("artifactDownloadLabel"))}"
|
|
1095
|
+
title="${escapeHtml(t("artifactDownloadLabel"))}"
|
|
1096
|
+
>...</button>
|
|
1097
|
+
</div>
|
|
1098
|
+
`;
|
|
1099
|
+
}
|
|
1100
|
+
)
|
|
1101
|
+
.join("");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function resetArtifactViewer() {
|
|
1105
|
+
artifactViewerTitleEl.textContent = t("artifactViewerEmptyTitle");
|
|
1106
|
+
artifactViewerMetaEl.textContent = "";
|
|
1107
|
+
artifactViewerKindEl.textContent = "";
|
|
1108
|
+
artifactViewerBodyEl.innerHTML = `<p class="subtle">${t("artifactViewerEmpty")}</p>`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function renderArtifactShell() {
|
|
1112
|
+
renderArtifactBadge();
|
|
1113
|
+
renderArtifactList();
|
|
1114
|
+
const selected = artifactItems.find((item) => item.id === selectedArtifactId);
|
|
1115
|
+
if (!selected || !artifactVisibleInCurrentTab(selected)) {
|
|
1116
|
+
resetArtifactViewer();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function renderArtifactContent(content) {
|
|
1121
|
+
const item = artifactItems.find((candidate) => candidate.id === content.id);
|
|
1122
|
+
artifactViewerTitleEl.textContent = content.title || item?.title || t("artifactViewerEmptyTitle");
|
|
1123
|
+
artifactViewerMetaEl.textContent = [content.path || item?.path, item ? artifactLabel(item) : ""].filter(Boolean).join(" · ");
|
|
1124
|
+
artifactViewerKindEl.textContent = content.kind || item?.kind || "";
|
|
1125
|
+
|
|
1126
|
+
if (content.dataUrl && (content.kind === "pdf" || content.mime === "application/pdf")) {
|
|
1127
|
+
artifactViewerBodyEl.innerHTML = `
|
|
1128
|
+
<iframe class="artifact-pdf-frame" src="${content.dataUrl}" title="${escapeHtml(content.title || "Artifact PDF")}"></iframe>
|
|
1129
|
+
`;
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (content.dataUrl) {
|
|
1134
|
+
artifactViewerBodyEl.innerHTML = `
|
|
1135
|
+
<figure class="artifact-image-frame">
|
|
1136
|
+
<img class="artifact-preview-image" src="${content.dataUrl}" alt="${escapeHtml(content.title || "Artifact image")}" />
|
|
1137
|
+
</figure>
|
|
1138
|
+
`;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const text = typeof content.text === "string" ? content.text : "";
|
|
1143
|
+
artifactViewerBodyEl.innerHTML = `
|
|
1144
|
+
<textarea class="artifact-editor" readonly spellcheck="false">${escapeHtml(text)}</textarea>
|
|
1145
|
+
`;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function artifactDownloadName(item, content) {
|
|
1149
|
+
const sourceName = content.path || item?.path || content.title || item?.title || `artifact-${content.id || "download"}`;
|
|
1150
|
+
const fallbackExt =
|
|
1151
|
+
content.mime === "application/pdf" || content.kind === "pdf"
|
|
1152
|
+
? ".pdf"
|
|
1153
|
+
: content.mime?.startsWith("image/png")
|
|
1154
|
+
? ".png"
|
|
1155
|
+
: content.mime?.startsWith("image/svg")
|
|
1156
|
+
? ".svg"
|
|
1157
|
+
: content.kind === "json"
|
|
1158
|
+
? ".json"
|
|
1159
|
+
: content.kind === "diff"
|
|
1160
|
+
? ".diff"
|
|
1161
|
+
: content.kind === "markdown"
|
|
1162
|
+
? ".md"
|
|
1163
|
+
: ".txt";
|
|
1164
|
+
const base = sourceName
|
|
1165
|
+
.split("/")
|
|
1166
|
+
.filter(Boolean)
|
|
1167
|
+
.pop()
|
|
1168
|
+
?.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
1169
|
+
.replace(/^-+|-+$/g, "");
|
|
1170
|
+
const name = base || `artifact-${content.id || Date.now()}`;
|
|
1171
|
+
return /\.[A-Za-z0-9]{1,8}$/.test(name) ? name : `${name}${fallbackExt}`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function blobFromDataUrl(dataUrl) {
|
|
1175
|
+
const [meta = "", payload = ""] = String(dataUrl || "").split(",", 2);
|
|
1176
|
+
const mime = meta.match(/^data:([^;,]+)/)?.[1] || "application/octet-stream";
|
|
1177
|
+
if (meta.includes(";base64")) {
|
|
1178
|
+
const binary = atob(payload);
|
|
1179
|
+
const bytes = new Uint8Array(binary.length);
|
|
1180
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
1181
|
+
return new Blob([bytes], { type: mime });
|
|
1182
|
+
}
|
|
1183
|
+
return new Blob([decodeURIComponent(payload)], { type: mime });
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async function downloadArtifact(artifactId) {
|
|
1187
|
+
if (!currentSessionId || !artifactId) return;
|
|
1188
|
+
const item = artifactItems.find((candidate) => candidate.id === artifactId);
|
|
1189
|
+
artifactStatusEl.textContent = t("artifactDownloadPreparing");
|
|
1190
|
+
|
|
1191
|
+
try {
|
|
1192
|
+
const response = await fetch(
|
|
1193
|
+
`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}`
|
|
1194
|
+
);
|
|
1195
|
+
const data = await response.json().catch(() => ({}));
|
|
1196
|
+
if (!response.ok) throw new Error(data.error || t("artifactDownloadFailed"));
|
|
1197
|
+
|
|
1198
|
+
const blob = data.dataUrl
|
|
1199
|
+
? blobFromDataUrl(data.dataUrl)
|
|
1200
|
+
: new Blob([typeof data.text === "string" ? data.text : JSON.stringify(data, null, 2)], {
|
|
1201
|
+
type: data.mime || "text/plain;charset=utf-8",
|
|
1202
|
+
});
|
|
1203
|
+
const url = URL.createObjectURL(blob);
|
|
1204
|
+
const link = document.createElement("a");
|
|
1205
|
+
link.href = url;
|
|
1206
|
+
link.download = artifactDownloadName(item, data);
|
|
1207
|
+
document.body.append(link);
|
|
1208
|
+
link.click();
|
|
1209
|
+
link.remove();
|
|
1210
|
+
URL.revokeObjectURL(url);
|
|
1211
|
+
|
|
1212
|
+
markArtifactRead(artifactId);
|
|
1213
|
+
renderArtifactBadge();
|
|
1214
|
+
renderArtifactList();
|
|
1215
|
+
artifactStatusEl.textContent = t("artifactDownloadReady");
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
artifactStatusEl.textContent = error instanceof Error ? error.message : t("artifactDownloadFailed");
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async function refreshArtifacts({ loadSelected = false } = {}) {
|
|
1222
|
+
if (!currentSessionId) {
|
|
1223
|
+
artifactItems = [];
|
|
1224
|
+
selectedArtifactId = "";
|
|
1225
|
+
artifactUnreadCount = 0;
|
|
1226
|
+
renderArtifactShell();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts`);
|
|
1231
|
+
if (!response.ok) return;
|
|
1232
|
+
const data = await response.json();
|
|
1233
|
+
artifactItems = data.items || [];
|
|
1234
|
+
artifactUnreadCount = computeUnreadArtifactCount();
|
|
1235
|
+
|
|
1236
|
+
if (loadSelected) {
|
|
1237
|
+
const backendSelected = artifactItems.find((item) => item.id === data.selectedItemId);
|
|
1238
|
+
const pdfCandidate = artifactItems.find((item) => item.id === selectedArtifactId && isPdfArtifact(item))
|
|
1239
|
+
|| (backendSelected && isPdfArtifact(backendSelected) ? backendSelected : null)
|
|
1240
|
+
|| artifactItems.find(isPdfArtifact);
|
|
1241
|
+
if (pdfCandidate) {
|
|
1242
|
+
currentArtifactTab = "pdf";
|
|
1243
|
+
selectedArtifactId = pdfCandidate.id;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
|
|
1248
|
+
selectedArtifactId = data.selectedItemId || artifactItems[0]?.id || "";
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const visiblePreferred = preferredArtifactForCurrentTab(data.selectedItemId);
|
|
1252
|
+
if (visiblePreferred) {
|
|
1253
|
+
selectedArtifactId = visiblePreferred.id;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
renderArtifactShell();
|
|
1257
|
+
if (loadSelected && selectedArtifactId) await selectArtifact(selectedArtifactId, { persist: false });
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function selectArtifact(artifactId, { persist = true } = {}) {
|
|
1261
|
+
if (!currentSessionId || !artifactId) return;
|
|
1262
|
+
selectedArtifactId = artifactId;
|
|
1263
|
+
renderArtifactList();
|
|
1264
|
+
artifactStatusEl.textContent = t("artifactLoading");
|
|
1265
|
+
|
|
1266
|
+
if (persist) {
|
|
1267
|
+
await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/select`, {
|
|
1268
|
+
method: "POST",
|
|
1269
|
+
headers: { "Content-Type": "application/json" },
|
|
1270
|
+
body: JSON.stringify({ artifactId }),
|
|
1271
|
+
}).catch(() => {});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const response = await fetch(
|
|
1275
|
+
`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}`
|
|
1276
|
+
);
|
|
1277
|
+
const data = await response.json().catch(() => ({}));
|
|
1278
|
+
if (!response.ok) {
|
|
1279
|
+
artifactStatusEl.textContent = data.error || t("artifactLoadFailed");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
artifactStatusEl.textContent = "";
|
|
1284
|
+
markArtifactRead(artifactId);
|
|
1285
|
+
renderArtifactBadge();
|
|
1286
|
+
renderArtifactList();
|
|
1287
|
+
renderArtifactContent(data);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function openArtifactTunnel() {
|
|
1291
|
+
if (!currentSessionId) {
|
|
1292
|
+
chatStatusEl.textContent = t("selectSessionFirst");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
artifactStatusEl.textContent = "";
|
|
1296
|
+
artifactTunnelDialog.showModal();
|
|
1297
|
+
await refreshArtifacts({ loadSelected: true });
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function closeArtifactTunnel() {
|
|
1301
|
+
artifactTunnelDialog.close();
|
|
1302
|
+
artifactStatusEl.textContent = "";
|
|
1303
|
+
}
|
|
1304
|
+
|
|
784
1305
|
function renderSessions(sessions) {
|
|
785
1306
|
lastSessions = sessions || [];
|
|
786
1307
|
const current = sessionSelectEl.value;
|
|
787
1308
|
const options = [`<option value="">${t("selectRecentSession")}</option>`]
|
|
788
1309
|
.concat(
|
|
789
1310
|
lastSessions.map((session) => {
|
|
790
|
-
const
|
|
791
|
-
const
|
|
1311
|
+
const rawTitle = session.title || session.goal || session.sessionId;
|
|
1312
|
+
const title = escapeHtml(`${rawTitle.slice(0, 60)}${rawTitle.length > 60 ? "..." : ""}`);
|
|
1313
|
+
const label = `${escapeHtml(session.provider)} · ${title}`;
|
|
792
1314
|
return `<option value="${escapeHtml(session.sessionId)}">${label}</option>`;
|
|
793
1315
|
})
|
|
794
1316
|
)
|
|
@@ -800,10 +1322,58 @@ function renderSessions(sessions) {
|
|
|
800
1322
|
}
|
|
801
1323
|
}
|
|
802
1324
|
|
|
1325
|
+
function currentManagedSession() {
|
|
1326
|
+
return lastSessions.find((session) => session.sessionId === managedSessionId) || lastSessions[0] || null;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function renderSessionManager(message = "") {
|
|
1330
|
+
if (!sessionManagerListEl) return;
|
|
1331
|
+
if (!managedSessionId && currentSessionId) managedSessionId = currentSessionId;
|
|
1332
|
+
if (!lastSessions.some((session) => session.sessionId === managedSessionId)) {
|
|
1333
|
+
managedSessionId = lastSessions[0]?.sessionId || "";
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const selected = currentManagedSession();
|
|
1337
|
+
sessionTitleInputEl.value = selected ? selected.title || selected.goal || "" : "";
|
|
1338
|
+
sessionTitleInputEl.disabled = !selected;
|
|
1339
|
+
autoRenameSessionButton.disabled = !selected;
|
|
1340
|
+
renameSessionButton.disabled = !selected;
|
|
1341
|
+
deleteSessionButton.disabled = !selected;
|
|
1342
|
+
sessionManagerStatusEl.textContent = message;
|
|
1343
|
+
|
|
1344
|
+
if (!lastSessions.length) {
|
|
1345
|
+
sessionManagerListEl.innerHTML = `<p class="subtle">${t("noSessionsToManage")}</p>`;
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
sessionManagerListEl.innerHTML = lastSessions
|
|
1350
|
+
.map((session) => {
|
|
1351
|
+
const title = session.title || session.goal || session.sessionId;
|
|
1352
|
+
const meta = [session.status, session.provider, session.updatedAt ? new Date(session.updatedAt).toLocaleString() : ""]
|
|
1353
|
+
.filter(Boolean)
|
|
1354
|
+
.join(" · ");
|
|
1355
|
+
return `
|
|
1356
|
+
<button class="session-row" type="button" data-session-id="${escapeHtml(session.sessionId)}" data-selected="${session.sessionId === managedSessionId}">
|
|
1357
|
+
<strong>${escapeHtml(title)}</strong>
|
|
1358
|
+
<span>${escapeHtml(meta)}</span>
|
|
1359
|
+
</button>
|
|
1360
|
+
`;
|
|
1361
|
+
})
|
|
1362
|
+
.join("");
|
|
1363
|
+
}
|
|
1364
|
+
|
|
803
1365
|
async function refreshSessions() {
|
|
804
1366
|
const response = await fetch("/api/sessions");
|
|
805
1367
|
const data = await response.json();
|
|
806
1368
|
renderSessions(data.sessions || []);
|
|
1369
|
+
renderSessionManager();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function refreshWorkspaceChanges() {
|
|
1373
|
+
const response = await fetch("/api/workspace/changes");
|
|
1374
|
+
if (!response.ok) return;
|
|
1375
|
+
const data = await response.json();
|
|
1376
|
+
renderWorkspacePanel(data.workspace, data.activity || []);
|
|
807
1377
|
}
|
|
808
1378
|
|
|
809
1379
|
async function refreshRun() {
|
|
@@ -813,12 +1383,15 @@ async function refreshRun() {
|
|
|
813
1383
|
const run = await response.json();
|
|
814
1384
|
runMetaEl.textContent = `${run.status} · ${run.sessionId}`;
|
|
815
1385
|
renderLogs(run);
|
|
1386
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
816
1387
|
|
|
817
1388
|
if (run.status === "finished" || run.status === "failed") {
|
|
818
1389
|
clearInterval(pollTimer);
|
|
819
1390
|
pollTimer = null;
|
|
820
1391
|
await refreshChat();
|
|
821
1392
|
await refreshSessions();
|
|
1393
|
+
await refreshWorkspaceChanges();
|
|
1394
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
822
1395
|
}
|
|
823
1396
|
}
|
|
824
1397
|
|
|
@@ -880,6 +1453,88 @@ async function runSandboxPreflight() {
|
|
|
880
1453
|
.join("\n\n");
|
|
881
1454
|
}
|
|
882
1455
|
|
|
1456
|
+
async function openSessionManager() {
|
|
1457
|
+
await refreshSessions();
|
|
1458
|
+
managedSessionId = currentSessionId || managedSessionId || lastSessions[0]?.sessionId || "";
|
|
1459
|
+
renderSessionManager();
|
|
1460
|
+
sessionManagerDialog.showModal();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function closeSessionManager() {
|
|
1464
|
+
sessionManagerDialog.close();
|
|
1465
|
+
sessionManagerStatusEl.textContent = "";
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async function renameManagedSession() {
|
|
1469
|
+
const selected = currentManagedSession();
|
|
1470
|
+
if (!selected) return;
|
|
1471
|
+
|
|
1472
|
+
const title = sessionTitleInputEl.value.trim();
|
|
1473
|
+
if (!title) {
|
|
1474
|
+
sessionManagerStatusEl.textContent = t("titleRequired");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}`, {
|
|
1479
|
+
method: "PATCH",
|
|
1480
|
+
headers: { "Content-Type": "application/json" },
|
|
1481
|
+
body: JSON.stringify({ title }),
|
|
1482
|
+
});
|
|
1483
|
+
const data = await response.json().catch(() => ({}));
|
|
1484
|
+
if (!response.ok) {
|
|
1485
|
+
sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
await refreshSessions();
|
|
1490
|
+
renderSessionManager(t("sessionRenamed"));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
async function autoRenameManagedSession() {
|
|
1494
|
+
const selected = currentManagedSession();
|
|
1495
|
+
if (!selected) return;
|
|
1496
|
+
|
|
1497
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}/auto-title`, {
|
|
1498
|
+
method: "POST",
|
|
1499
|
+
});
|
|
1500
|
+
const data = await response.json().catch(() => ({}));
|
|
1501
|
+
if (!response.ok) {
|
|
1502
|
+
sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
await refreshSessions();
|
|
1507
|
+
renderSessionManager(t("sessionRenamed"));
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
async function deleteManagedSession() {
|
|
1511
|
+
const selected = currentManagedSession();
|
|
1512
|
+
if (!selected || !confirm(t("deleteConfirm"))) return;
|
|
1513
|
+
|
|
1514
|
+
const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}`, {
|
|
1515
|
+
method: "DELETE",
|
|
1516
|
+
});
|
|
1517
|
+
const data = await response.json().catch(() => ({}));
|
|
1518
|
+
if (!response.ok) {
|
|
1519
|
+
sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (currentSessionId === selected.sessionId) {
|
|
1524
|
+
currentSessionId = "";
|
|
1525
|
+
sessionSelectEl.value = "";
|
|
1526
|
+
runMetaEl.textContent = "";
|
|
1527
|
+
setLogs(t("noRunSelected"), "empty");
|
|
1528
|
+
renderChat([]);
|
|
1529
|
+
artifactItems = [];
|
|
1530
|
+
selectedArtifactId = "";
|
|
1531
|
+
renderArtifactShell();
|
|
1532
|
+
}
|
|
1533
|
+
managedSessionId = "";
|
|
1534
|
+
await refreshSessions();
|
|
1535
|
+
renderSessionManager(t("sessionDeleted"));
|
|
1536
|
+
}
|
|
1537
|
+
|
|
883
1538
|
function schedulePreferenceSave() {
|
|
884
1539
|
clearTimeout(saveTimer);
|
|
885
1540
|
saveTimer = setTimeout(() => {
|
|
@@ -908,13 +1563,117 @@ preflightSandboxButton.addEventListener("click", () => {
|
|
|
908
1563
|
});
|
|
909
1564
|
});
|
|
910
1565
|
|
|
1566
|
+
manageSessionsButton.addEventListener("click", () => {
|
|
1567
|
+
openSessionManager().catch((error) => {
|
|
1568
|
+
chatStatusEl.textContent = String(error);
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
openArtifactsButton.addEventListener("click", () => {
|
|
1573
|
+
openArtifactTunnel().catch((error) => {
|
|
1574
|
+
chatStatusEl.textContent = String(error);
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
closeSessionManagerButton.addEventListener("click", closeSessionManager);
|
|
1579
|
+
cancelSessionManagerButton.addEventListener("click", closeSessionManager);
|
|
1580
|
+
closeArtifactTunnelButton.addEventListener("click", closeArtifactTunnel);
|
|
1581
|
+
|
|
1582
|
+
sessionManagerDialog.addEventListener("click", (event) => {
|
|
1583
|
+
if (event.target === sessionManagerDialog) closeSessionManager();
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
artifactTunnelDialog.addEventListener("click", (event) => {
|
|
1587
|
+
if (event.target === artifactTunnelDialog) closeArtifactTunnel();
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
artifactTabsEl.addEventListener("click", (event) => {
|
|
1591
|
+
const tab = event.target.closest("[data-artifact-tab]");
|
|
1592
|
+
if (!tab) return;
|
|
1593
|
+
currentArtifactTab = tab.dataset.artifactTab || "canvas";
|
|
1594
|
+
const preferred = preferredArtifactForCurrentTab();
|
|
1595
|
+
if (preferred) selectedArtifactId = preferred.id;
|
|
1596
|
+
renderArtifactShell();
|
|
1597
|
+
if (selectedArtifactId) {
|
|
1598
|
+
selectArtifact(selectedArtifactId, { persist: false }).catch((error) => {
|
|
1599
|
+
artifactStatusEl.textContent = String(error);
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
artifactListEl.addEventListener("click", (event) => {
|
|
1605
|
+
const downloadButton = event.target.closest("[data-artifact-download-id]");
|
|
1606
|
+
if (downloadButton) {
|
|
1607
|
+
event.stopPropagation();
|
|
1608
|
+
downloadArtifact(downloadButton.dataset.artifactDownloadId || "").catch((error) => {
|
|
1609
|
+
artifactStatusEl.textContent = String(error);
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const row = event.target.closest("[data-artifact-id]");
|
|
1615
|
+
if (!row) return;
|
|
1616
|
+
selectArtifact(row.dataset.artifactId || "").catch((error) => {
|
|
1617
|
+
artifactStatusEl.textContent = String(error);
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
markArtifactsSeenButton.addEventListener("click", () => {
|
|
1622
|
+
markAllArtifactsRead();
|
|
1623
|
+
renderArtifactBadge();
|
|
1624
|
+
renderArtifactList();
|
|
1625
|
+
artifactStatusEl.textContent = t("artifactSeenUpdated");
|
|
1626
|
+
refreshArtifacts({ loadSelected: false }).catch(() => {});
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
sessionManagerListEl.addEventListener("click", (event) => {
|
|
1630
|
+
const row = event.target.closest("[data-session-id]");
|
|
1631
|
+
if (!row) return;
|
|
1632
|
+
managedSessionId = row.dataset.sessionId || "";
|
|
1633
|
+
renderSessionManager();
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
renameSessionButton.addEventListener("click", () => {
|
|
1637
|
+
renameManagedSession().catch((error) => {
|
|
1638
|
+
sessionManagerStatusEl.textContent = String(error);
|
|
1639
|
+
});
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
autoRenameSessionButton.addEventListener("click", () => {
|
|
1643
|
+
autoRenameManagedSession().catch((error) => {
|
|
1644
|
+
sessionManagerStatusEl.textContent = String(error);
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
deleteSessionButton.addEventListener("click", () => {
|
|
1649
|
+
deleteManagedSession().catch((error) => {
|
|
1650
|
+
sessionManagerStatusEl.textContent = String(error);
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
|
|
911
1654
|
providerField.addEventListener("change", () => {
|
|
912
|
-
if (
|
|
1655
|
+
if (providerField.value === "mock") {
|
|
1656
|
+
routingModeField.value = "manual";
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (
|
|
1660
|
+
!modelField.value.trim() ||
|
|
1661
|
+
modelField.value === defaults.openai ||
|
|
1662
|
+
modelField.value === defaults.deepseek ||
|
|
1663
|
+
modelField.value === defaults.mock
|
|
1664
|
+
) {
|
|
913
1665
|
modelField.value = defaults[providerField.value] || "";
|
|
914
1666
|
}
|
|
1667
|
+
updateRoutingHint();
|
|
915
1668
|
schedulePreferenceSave();
|
|
916
1669
|
});
|
|
917
1670
|
|
|
1671
|
+
modelField.addEventListener("input", updateRoutingHint);
|
|
1672
|
+
sandboxModeField.addEventListener("change", updatePackageWarning);
|
|
1673
|
+
packageInstallPolicyField.addEventListener("change", updatePackageWarning);
|
|
1674
|
+
allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
|
|
1675
|
+
preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
|
|
1676
|
+
|
|
918
1677
|
form.addEventListener("input", schedulePreferenceSave);
|
|
919
1678
|
form.addEventListener("change", schedulePreferenceSave);
|
|
920
1679
|
|
|
@@ -953,6 +1712,8 @@ form.addEventListener("submit", async (event) => {
|
|
|
953
1712
|
runMetaEl.textContent = `${t("runningStatus").replace("...", "")} · ${currentSessionId}`;
|
|
954
1713
|
await refreshSessions();
|
|
955
1714
|
await refreshChat();
|
|
1715
|
+
await refreshWorkspaceChanges();
|
|
1716
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
956
1717
|
|
|
957
1718
|
if (pollTimer) clearInterval(pollTimer);
|
|
958
1719
|
pollTimer = setInterval(refreshRun, 1500);
|
|
@@ -965,10 +1726,14 @@ sessionSelectEl.addEventListener("change", async () => {
|
|
|
965
1726
|
runMetaEl.textContent = "";
|
|
966
1727
|
setLogs(t("noRunSelected"), "empty");
|
|
967
1728
|
renderChat([]);
|
|
1729
|
+
artifactItems = [];
|
|
1730
|
+
selectedArtifactId = "";
|
|
1731
|
+
renderArtifactShell();
|
|
968
1732
|
return;
|
|
969
1733
|
}
|
|
970
1734
|
await refreshRun();
|
|
971
1735
|
await refreshChat();
|
|
1736
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
972
1737
|
});
|
|
973
1738
|
|
|
974
1739
|
chatFormEl.addEventListener("submit", async (event) => {
|
|
@@ -1007,6 +1772,8 @@ chatFormEl.addEventListener("submit", async (event) => {
|
|
|
1007
1772
|
chatStatusEl.textContent = t("runningStatus");
|
|
1008
1773
|
await refreshSessions();
|
|
1009
1774
|
await refreshChat();
|
|
1775
|
+
await refreshWorkspaceChanges();
|
|
1776
|
+
await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
|
|
1010
1777
|
|
|
1011
1778
|
if (pollTimer) clearInterval(pollTimer);
|
|
1012
1779
|
pollTimer = setInterval(refreshRun, 1500);
|
|
@@ -1020,6 +1787,7 @@ async function loadConfig() {
|
|
|
1020
1787
|
routingPresets = data.routing?.presets || {};
|
|
1021
1788
|
defaults.openai = data.defaults?.openai?.model || defaults.openai;
|
|
1022
1789
|
defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
|
|
1790
|
+
defaults.mock = data.defaults?.mock?.model || defaults.mock;
|
|
1023
1791
|
|
|
1024
1792
|
applyLanguage(prefs.language || normalizeLanguage(navigator.language || "en"), { persist: false });
|
|
1025
1793
|
|
|
@@ -1031,18 +1799,23 @@ async function loadConfig() {
|
|
|
1031
1799
|
document.querySelector("#commandCwd").value = prefs.commandCwd || "/home/lachlan/ProjectsLFS/Agent";
|
|
1032
1800
|
document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
|
|
1033
1801
|
document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
|
|
1034
|
-
sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-
|
|
1802
|
+
sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
|
|
1035
1803
|
packageInstallPolicyField.value = prefs.packageInstallPolicy || "prompt";
|
|
1036
1804
|
document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
|
|
1037
|
-
document.querySelector("#
|
|
1805
|
+
document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
|
|
1806
|
+
allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
|
|
1807
|
+
preferredWrapperField.value = prefs.preferredWrapper || "codex";
|
|
1038
1808
|
document.querySelector("#dockerSandboxImage").value = prefs.dockerSandboxImage || "agintiflow-sandbox:latest";
|
|
1039
1809
|
document.querySelector("#allowPasswords").checked = prefs.allowPasswords ?? false;
|
|
1040
1810
|
document.querySelector("#allowDestructive").checked = prefs.allowDestructive ?? false;
|
|
1041
1811
|
|
|
1042
1812
|
renderKeyStatus(data.keyStatus);
|
|
1043
1813
|
renderWrapperStatus(data.wrappers || []);
|
|
1814
|
+
renderWorkspacePanel(data.workspace, []);
|
|
1815
|
+
await refreshWorkspaceChanges();
|
|
1044
1816
|
renderSandboxLogs(data.sandbox?.logs || []);
|
|
1045
1817
|
updateRoutingHint();
|
|
1818
|
+
updatePackageWarning();
|
|
1046
1819
|
|
|
1047
1820
|
renderSessions(data.sessions || []);
|
|
1048
1821
|
if (data.sessions && data.sessions.length > 0) {
|
|
@@ -1050,6 +1823,7 @@ async function loadConfig() {
|
|
|
1050
1823
|
sessionSelectEl.value = currentSessionId;
|
|
1051
1824
|
await refreshRun();
|
|
1052
1825
|
await refreshChat();
|
|
1826
|
+
await refreshArtifacts({ loadSelected: false });
|
|
1053
1827
|
}
|
|
1054
1828
|
}
|
|
1055
1829
|
|