@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/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: fast unless complex",
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: "Smart routing uses DeepSeek v4 flash by default and escalates to DeepSeek v4 pro for complex work.",
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
- wrapperStatusEl.textContent = `${t("wrappersLabel")}: ${lastWrappers
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
- allowWrapperTools: document.querySelector("#allowWrapperTools").checked,
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(/[&<>]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[ch]));
951
+ return String(value || "").replace(
952
+ /[&<>"']/g,
953
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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 goal = escapeHtml(`${session.goal.slice(0, 60)}${session.goal.length > 60 ? "..." : ""}`);
791
- const label = `${escapeHtml(session.provider)} · ${goal}`;
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 (!modelField.value.trim() || modelField.value === defaults.openai || modelField.value === defaults.deepseek) {
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-readonly" : "host");
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("#allowWrapperTools").checked = prefs.allowWrapperTools ?? false;
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