@lazyingart/agintiflow 0.1.0 → 0.2.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,35 @@ 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
+ artifactExplorerTab: "Explorer",
126
+ artifactNotificationsTab: "Notifications",
127
+ artifactListTitle: "Artifacts",
128
+ artifactMarkSeenButton: "Mark seen",
129
+ artifactEmpty: "No artifacts yet.",
130
+ artifactViewerEmptyTitle: "Nothing selected",
131
+ artifactViewerEmpty: "Select an artifact to preview it here.",
132
+ artifactLoading: "Loading artifact...",
133
+ artifactLoadFailed: "Failed to load artifact.",
134
+ artifactSeenUpdated: "Notifications marked as seen.",
88
135
  },
89
136
  ar: {
90
137
  documentTitle: "AgInTiFlow",
@@ -564,12 +611,20 @@ const routingModeField = document.querySelector("#routingMode");
564
611
  const providerField = document.querySelector("#provider");
565
612
  const modelField = document.querySelector("#model");
566
613
  const routingHintEl = document.querySelector("#routing-hint");
614
+ const modelRouteStatusEl = document.querySelector("#model-route-status");
567
615
  const sandboxModeField = document.querySelector("#sandboxMode");
568
616
  const packageInstallPolicyField = document.querySelector("#packageInstallPolicy");
617
+ const packageWarningEl = document.querySelector("#package-warning");
569
618
  const logsEl = document.querySelector("#logs");
570
619
  const runMetaEl = document.querySelector("#run-meta");
571
620
  const keyStatusEl = document.querySelector("#key-status");
621
+ const allowWrapperToolsField = document.querySelector("#allowWrapperTools");
622
+ const preferredWrapperField = document.querySelector("#preferredWrapper");
572
623
  const wrapperStatusEl = document.querySelector("#wrapper-status");
624
+ const wrapperGridEl = document.querySelector("#wrapper-grid");
625
+ const workspaceStatusEl = document.querySelector("#workspace-status");
626
+ const workspaceToolsEl = document.querySelector("#workspace-tools");
627
+ const workspaceChangesEl = document.querySelector("#workspace-changes");
573
628
  const sandboxStatusEl = document.querySelector("#sandbox-status");
574
629
  const sandboxLogsEl = document.querySelector("#sandbox-logs");
575
630
  const checkSandboxButton = document.querySelector("#check-sandbox");
@@ -579,12 +634,36 @@ const chatThreadEl = document.querySelector("#chat-thread");
579
634
  const chatFormEl = document.querySelector("#chat-form");
580
635
  const chatInputEl = document.querySelector("#chat-input");
581
636
  const chatStatusEl = document.querySelector("#chat-status");
637
+ const manageSessionsButton = document.querySelector("#manage-sessions");
638
+ const openArtifactsButton = document.querySelector("#open-artifacts");
639
+ const artifactBadgeEl = document.querySelector("#artifact-badge");
640
+ const sessionManagerDialog = document.querySelector("#session-manager");
641
+ const closeSessionManagerButton = document.querySelector("#close-session-manager");
642
+ const cancelSessionManagerButton = document.querySelector("#cancel-session-manager");
643
+ const sessionManagerListEl = document.querySelector("#session-manager-list");
644
+ const sessionTitleInputEl = document.querySelector("#session-title-input");
645
+ const sessionManagerStatusEl = document.querySelector("#session-manager-status");
646
+ const autoRenameSessionButton = document.querySelector("#auto-rename-session");
647
+ const renameSessionButton = document.querySelector("#rename-session");
648
+ const deleteSessionButton = document.querySelector("#delete-session");
649
+ const artifactTunnelDialog = document.querySelector("#artifact-tunnel");
650
+ const closeArtifactTunnelButton = document.querySelector("#close-artifact-tunnel");
651
+ const artifactTabsEl = document.querySelector(".artifact-tabs");
652
+ const artifactListEl = document.querySelector("#artifact-list");
653
+ const artifactViewerTitleEl = document.querySelector("#artifact-viewer-title");
654
+ const artifactViewerMetaEl = document.querySelector("#artifact-viewer-meta");
655
+ const artifactViewerKindEl = document.querySelector("#artifact-viewer-kind");
656
+ const artifactViewerBodyEl = document.querySelector("#artifact-viewer-body");
657
+ const artifactStatusEl = document.querySelector("#artifact-status");
658
+ const markArtifactsSeenButton = document.querySelector("#mark-artifacts-seen");
582
659
  const translatableNodes = [...document.querySelectorAll("[data-i18n]")];
583
660
  const placeholderNodes = [...document.querySelectorAll("[data-i18n-placeholder]")];
661
+ const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
584
662
 
585
663
  const defaults = {
586
664
  openai: "gpt-5.4-mini",
587
665
  deepseek: "deepseek-v4-flash",
666
+ mock: "mock-agent",
588
667
  };
589
668
 
590
669
  let currentLanguage = "en";
@@ -597,6 +676,13 @@ let lastSessions = [];
597
676
  let lastKeyStatus = null;
598
677
  let lastWrappers = [];
599
678
  let lastSandbox = null;
679
+ let lastWorkspace = null;
680
+ let lastWorkspaceActivity = [];
681
+ let managedSessionId = "";
682
+ let artifactItems = [];
683
+ let selectedArtifactId = "";
684
+ let currentArtifactTab = "canvas";
685
+ let artifactUnreadCount = 0;
600
686
 
601
687
  function normalizeLanguage(language) {
602
688
  const lower = String(language || "").toLowerCase();
@@ -619,18 +705,94 @@ function renderKeyStatus(status = lastKeyStatus) {
619
705
  if (!status) return;
620
706
  keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
621
707
  status.openai ? t("availableLabel") : t("missingLabel")
622
- } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")}`;
708
+ } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
709
+ status.mock ? t("availableLabel") : t("missingLabel")
710
+ }`;
623
711
  }
624
712
 
625
713
  function renderWrapperStatus(wrappers = lastWrappers) {
626
714
  lastWrappers = wrappers || [];
627
715
  if (lastWrappers.length === 0) {
628
716
  wrapperStatusEl.textContent = "";
717
+ wrapperGridEl.innerHTML = "";
629
718
  return;
630
719
  }
631
- wrapperStatusEl.textContent = `${t("wrappersLabel")}: ${lastWrappers
720
+ const selectedWrapper = preferredWrapperField?.value || "codex";
721
+ const selected = lastWrappers.find((wrapper) => wrapper.name === selectedWrapper);
722
+ const selectedLabel = selected?.label || selectedWrapper;
723
+ const enabledPrefix = allowWrapperToolsField?.checked ? t("selectedWrapperLabel") : t("wrappersOffLabel");
724
+ wrapperStatusEl.textContent = `${enabledPrefix}: ${selectedLabel} · ${t("wrappersLabel")}: ${lastWrappers
632
725
  .map((wrapper) => `${wrapper.label || wrapper.name} ${wrapper.available ? t("availableLabel") : t("missingLabel")}`)
633
726
  .join(" · ")}`;
727
+ wrapperGridEl.innerHTML = lastWrappers
728
+ .map(
729
+ (wrapper) => `
730
+ <div class="capability-chip" data-ready="${Boolean(wrapper.available)}" data-selected="${wrapper.name === selectedWrapper}">
731
+ <strong>${escapeHtml(wrapper.label || wrapper.name)}</strong>
732
+ <span>${wrapper.name === selectedWrapper ? `${t("selectedWrapperLabel")} · ` : ""}${wrapper.available ? t("availableLabel") : t("missingLabel")}</span>
733
+ </div>
734
+ `
735
+ )
736
+ .join("");
737
+ }
738
+
739
+ function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspaceActivity) {
740
+ lastWorkspace = workspace;
741
+ lastWorkspaceActivity = activity || [];
742
+
743
+ if (!workspace) {
744
+ workspaceStatusEl.textContent = "";
745
+ workspaceToolsEl.innerHTML = "";
746
+ workspaceChangesEl.innerHTML = `<p class="subtle">${t("workspaceChangesEmpty")}</p>`;
747
+ return;
748
+ }
749
+
750
+ workspaceStatusEl.textContent = `${workspace.enabled ? t("availableLabel") : t("missingLabel")} · ${
751
+ workspace.workspace || ""
752
+ }`;
753
+ workspaceToolsEl.innerHTML = (workspace.tools || [])
754
+ .map(
755
+ (tool) => `
756
+ <div class="capability-chip" data-ready="${Boolean(workspace.enabled)}">
757
+ <strong>${escapeHtml(tool)}</strong>
758
+ <span>${(workspace.writeTools || []).includes(tool) ? "write guarded" : "read guarded"}</span>
759
+ </div>
760
+ `
761
+ )
762
+ .join("");
763
+
764
+ if (!lastWorkspaceActivity.length) {
765
+ workspaceChangesEl.innerHTML = `<p class="subtle">${t("workspaceChangesEmpty")}</p>`;
766
+ return;
767
+ }
768
+
769
+ workspaceChangesEl.innerHTML = lastWorkspaceActivity
770
+ .slice(0, 6)
771
+ .map((item) => {
772
+ const blocked = item.kind === "blocked";
773
+ const label = blocked ? t("blockedLabel") : t("changedLabel");
774
+ const path = escapeHtml(item.path || "");
775
+ const reason = blocked ? `<div class="subtle">${escapeHtml(item.reason || "")}</div>` : "";
776
+ const diff = item.diff ? `<pre class="change-diff">${escapeHtml(item.diff).slice(0, 1200)}</pre>` : "";
777
+ const hashes =
778
+ item.beforeHash || item.afterHash
779
+ ? `<div class="change-meta"><span>before=${escapeHtml((item.beforeHash || "new").slice(0, 10))}</span><span>after=${escapeHtml((item.afterHash || "").slice(0, 10))}</span></div>`
780
+ : "";
781
+ return `
782
+ <article class="change-item ${blocked ? "blocked" : ""}">
783
+ <div class="change-meta">
784
+ <span>${label}</span>
785
+ <span>${escapeHtml(item.toolName || "")}</span>
786
+ <span>${item.at ? new Date(item.at).toLocaleString() : ""}</span>
787
+ </div>
788
+ <strong class="change-path">${path}</strong>
789
+ ${reason}
790
+ ${hashes}
791
+ ${diff}
792
+ </article>
793
+ `;
794
+ })
795
+ .join("");
634
796
  }
635
797
 
636
798
  function renderSandboxStatus(status = lastSandbox) {
@@ -685,6 +847,24 @@ function updateRoutingHint() {
685
847
  modelField.value = preset.model || modelField.value;
686
848
  }
687
849
  }
850
+
851
+ modelRouteStatusEl.textContent = `${t("modelRouteStatus")}: ${providerField.value} / ${
852
+ modelField.value.trim() || defaults[providerField.value] || ""
853
+ }`;
854
+ }
855
+
856
+ function updatePackageWarning() {
857
+ const policy = packageInstallPolicyField.value || "prompt";
858
+ const sandboxMode = sandboxModeField.value || "host";
859
+ const key =
860
+ policy === "allow" && sandboxMode === "host"
861
+ ? "packageWarningHostAllow"
862
+ : policy === "allow"
863
+ ? "packageWarningAllow"
864
+ : policy === "block"
865
+ ? "packageWarningBlock"
866
+ : "packageWarningPrompt";
867
+ packageWarningEl.textContent = t(key);
688
868
  }
689
869
 
690
870
  function applyLanguage(language, { persist = true } = {}) {
@@ -703,12 +883,20 @@ function applyLanguage(language, { persist = true } = {}) {
703
883
  node.placeholder = t(node.dataset.i18nPlaceholder);
704
884
  }
705
885
 
886
+ for (const node of ariaLabelNodes) {
887
+ node.setAttribute("aria-label", t(node.dataset.i18nAriaLabel));
888
+ }
889
+
706
890
  renderKeyStatus();
707
891
  renderWrapperStatus();
892
+ renderWorkspacePanel();
708
893
  renderSandboxStatus();
709
894
  updateRoutingHint();
895
+ updatePackageWarning();
710
896
  renderSessions(lastSessions);
897
+ renderSessionManager();
711
898
  renderChat(lastChatEntries);
899
+ renderArtifactShell();
712
900
  if (persist) schedulePreferenceSave();
713
901
  }
714
902
 
@@ -726,7 +914,9 @@ function formPayload() {
726
914
  packageInstallPolicy: packageInstallPolicyField.value,
727
915
  headless: document.querySelector("#headless").checked,
728
916
  allowShellTool: document.querySelector("#allowShellTool").checked,
729
- allowWrapperTools: document.querySelector("#allowWrapperTools").checked,
917
+ allowFileTools: document.querySelector("#allowFileTools").checked,
918
+ allowWrapperTools: allowWrapperToolsField.checked,
919
+ preferredWrapper: preferredWrapperField.value,
730
920
  useDockerSandbox: sandboxModeField.value !== "host",
731
921
  dockerSandboxImage: document.querySelector("#dockerSandboxImage").value.trim(),
732
922
  allowPasswords: document.querySelector("#allowPasswords").checked,
@@ -753,7 +943,10 @@ function renderLogs(run) {
753
943
  }
754
944
 
755
945
  function escapeHtml(value) {
756
- return String(value || "").replace(/[&<>]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[ch]));
946
+ return String(value || "").replace(
947
+ /[&<>"']/g,
948
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]
949
+ );
757
950
  }
758
951
 
759
952
  function renderChat(chatEntries) {
@@ -781,14 +974,218 @@ function renderChat(chatEntries) {
781
974
  chatThreadEl.scrollTop = chatThreadEl.scrollHeight;
782
975
  }
783
976
 
977
+ function artifactReadKey(sessionId = currentSessionId) {
978
+ return `agintiflow.artifacts.readIds.${sessionId || "none"}`;
979
+ }
980
+
981
+ function getArtifactReadIds(sessionId = currentSessionId) {
982
+ try {
983
+ const raw = localStorage.getItem(artifactReadKey(sessionId)) || "[]";
984
+ const parsed = JSON.parse(raw);
985
+ return new Set(Array.isArray(parsed) ? parsed : []);
986
+ } catch {
987
+ return new Set();
988
+ }
989
+ }
990
+
991
+ function saveArtifactReadIds(readIds, sessionId = currentSessionId) {
992
+ if (!sessionId) return;
993
+ try {
994
+ localStorage.setItem(artifactReadKey(sessionId), JSON.stringify([...readIds].slice(-800)));
995
+ } catch {
996
+ // Local storage is optional for notification state.
997
+ }
998
+ }
999
+
1000
+ function computeUnreadArtifactCount() {
1001
+ const readIds = getArtifactReadIds();
1002
+ return artifactItems.filter((item) => !readIds.has(item.id)).length;
1003
+ }
1004
+
1005
+ function markArtifactRead(artifactId) {
1006
+ if (!artifactId) return;
1007
+ const readIds = getArtifactReadIds();
1008
+ readIds.add(artifactId);
1009
+ saveArtifactReadIds(readIds);
1010
+ artifactUnreadCount = computeUnreadArtifactCount();
1011
+ }
1012
+
1013
+ function markAllArtifactsRead() {
1014
+ const readIds = getArtifactReadIds();
1015
+ for (const item of artifactItems) readIds.add(item.id);
1016
+ saveArtifactReadIds(readIds);
1017
+ artifactUnreadCount = 0;
1018
+ }
1019
+
1020
+ function renderArtifactBadge(count = computeUnreadArtifactCount()) {
1021
+ artifactUnreadCount = Number(count) || 0;
1022
+ if (!artifactBadgeEl) return;
1023
+ artifactBadgeEl.textContent = artifactUnreadCount > 99 ? "99+" : String(artifactUnreadCount);
1024
+ artifactBadgeEl.hidden = artifactUnreadCount <= 0;
1025
+ }
1026
+
1027
+ function artifactLabel(item) {
1028
+ const source = item.source ? item.source.replace(/-/g, " ") : "artifact";
1029
+ const when = item.createdAt ? new Date(item.createdAt).toLocaleString() : "";
1030
+ return [item.kind, source, when].filter(Boolean).join(" · ");
1031
+ }
1032
+
1033
+ function renderArtifactList() {
1034
+ if (!artifactListEl) return;
1035
+ const visible = artifactItems.filter((item) => item.tab === currentArtifactTab);
1036
+ const readIds = getArtifactReadIds();
1037
+
1038
+ document.querySelectorAll(".artifact-tab").forEach((tab) => {
1039
+ tab.dataset.selected = String(tab.dataset.artifactTab === currentArtifactTab);
1040
+ });
1041
+
1042
+ if (!currentSessionId) {
1043
+ artifactListEl.innerHTML = `<p class="subtle">${t("selectSessionFirst")}</p>`;
1044
+ return;
1045
+ }
1046
+
1047
+ if (visible.length === 0) {
1048
+ artifactListEl.innerHTML = `<p class="subtle">${t("artifactEmpty")}</p>`;
1049
+ return;
1050
+ }
1051
+
1052
+ artifactListEl.innerHTML = visible
1053
+ .map(
1054
+ (item) => {
1055
+ const read = readIds.has(item.id);
1056
+ return `
1057
+ <button class="artifact-row" type="button" data-artifact-id="${escapeHtml(item.id)}" data-selected="${item.id === selectedArtifactId}" data-read="${read}">
1058
+ <span class="artifact-row-top">
1059
+ <strong>${escapeHtml(item.title || item.path || item.kind)}</strong>
1060
+ <span>${read ? escapeHtml(item.kind) : `New · ${escapeHtml(item.kind)}`}</span>
1061
+ </span>
1062
+ <span class="artifact-row-meta">${escapeHtml(artifactLabel(item))}</span>
1063
+ <span class="artifact-row-preview">${escapeHtml(item.preview || item.path || "")}</span>
1064
+ </button>
1065
+ `;
1066
+ }
1067
+ )
1068
+ .join("");
1069
+ }
1070
+
1071
+ function resetArtifactViewer() {
1072
+ artifactViewerTitleEl.textContent = t("artifactViewerEmptyTitle");
1073
+ artifactViewerMetaEl.textContent = "";
1074
+ artifactViewerKindEl.textContent = "";
1075
+ artifactViewerBodyEl.innerHTML = `<p class="subtle">${t("artifactViewerEmpty")}</p>`;
1076
+ }
1077
+
1078
+ function renderArtifactShell() {
1079
+ renderArtifactBadge();
1080
+ renderArtifactList();
1081
+ if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1082
+ resetArtifactViewer();
1083
+ }
1084
+ }
1085
+
1086
+ function renderArtifactContent(content) {
1087
+ const item = artifactItems.find((candidate) => candidate.id === content.id);
1088
+ artifactViewerTitleEl.textContent = content.title || item?.title || t("artifactViewerEmptyTitle");
1089
+ artifactViewerMetaEl.textContent = [content.path || item?.path, item ? artifactLabel(item) : ""].filter(Boolean).join(" · ");
1090
+ artifactViewerKindEl.textContent = content.kind || item?.kind || "";
1091
+
1092
+ if (content.dataUrl && (content.kind === "pdf" || content.mime === "application/pdf")) {
1093
+ artifactViewerBodyEl.innerHTML = `
1094
+ <iframe class="artifact-pdf-frame" src="${content.dataUrl}" title="${escapeHtml(content.title || "Artifact PDF")}"></iframe>
1095
+ `;
1096
+ return;
1097
+ }
1098
+
1099
+ if (content.dataUrl) {
1100
+ artifactViewerBodyEl.innerHTML = `
1101
+ <figure class="artifact-image-frame">
1102
+ <img class="artifact-preview-image" src="${content.dataUrl}" alt="${escapeHtml(content.title || "Artifact image")}" />
1103
+ </figure>
1104
+ `;
1105
+ return;
1106
+ }
1107
+
1108
+ const text = typeof content.text === "string" ? content.text : "";
1109
+ artifactViewerBodyEl.innerHTML = `
1110
+ <textarea class="artifact-editor" readonly spellcheck="false">${escapeHtml(text)}</textarea>
1111
+ `;
1112
+ }
1113
+
1114
+ async function refreshArtifacts({ loadSelected = false } = {}) {
1115
+ if (!currentSessionId) {
1116
+ artifactItems = [];
1117
+ selectedArtifactId = "";
1118
+ artifactUnreadCount = 0;
1119
+ renderArtifactShell();
1120
+ return;
1121
+ }
1122
+
1123
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts`);
1124
+ if (!response.ok) return;
1125
+ const data = await response.json();
1126
+ artifactItems = data.items || [];
1127
+ artifactUnreadCount = computeUnreadArtifactCount();
1128
+ if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1129
+ selectedArtifactId = data.selectedItemId || artifactItems[0]?.id || "";
1130
+ }
1131
+ renderArtifactShell();
1132
+ if (loadSelected && selectedArtifactId) await selectArtifact(selectedArtifactId, { persist: false });
1133
+ }
1134
+
1135
+ async function selectArtifact(artifactId, { persist = true } = {}) {
1136
+ if (!currentSessionId || !artifactId) return;
1137
+ selectedArtifactId = artifactId;
1138
+ renderArtifactList();
1139
+ artifactStatusEl.textContent = t("artifactLoading");
1140
+
1141
+ if (persist) {
1142
+ await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/select`, {
1143
+ method: "POST",
1144
+ headers: { "Content-Type": "application/json" },
1145
+ body: JSON.stringify({ artifactId }),
1146
+ }).catch(() => {});
1147
+ }
1148
+
1149
+ const response = await fetch(
1150
+ `/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}`
1151
+ );
1152
+ const data = await response.json().catch(() => ({}));
1153
+ if (!response.ok) {
1154
+ artifactStatusEl.textContent = data.error || t("artifactLoadFailed");
1155
+ return;
1156
+ }
1157
+
1158
+ artifactStatusEl.textContent = "";
1159
+ markArtifactRead(artifactId);
1160
+ renderArtifactBadge();
1161
+ renderArtifactList();
1162
+ renderArtifactContent(data);
1163
+ }
1164
+
1165
+ async function openArtifactTunnel() {
1166
+ if (!currentSessionId) {
1167
+ chatStatusEl.textContent = t("selectSessionFirst");
1168
+ return;
1169
+ }
1170
+ artifactStatusEl.textContent = "";
1171
+ artifactTunnelDialog.showModal();
1172
+ await refreshArtifacts({ loadSelected: true });
1173
+ }
1174
+
1175
+ function closeArtifactTunnel() {
1176
+ artifactTunnelDialog.close();
1177
+ artifactStatusEl.textContent = "";
1178
+ }
1179
+
784
1180
  function renderSessions(sessions) {
785
1181
  lastSessions = sessions || [];
786
1182
  const current = sessionSelectEl.value;
787
1183
  const options = [`<option value="">${t("selectRecentSession")}</option>`]
788
1184
  .concat(
789
1185
  lastSessions.map((session) => {
790
- const goal = escapeHtml(`${session.goal.slice(0, 60)}${session.goal.length > 60 ? "..." : ""}`);
791
- const label = `${escapeHtml(session.provider)} · ${goal}`;
1186
+ const rawTitle = session.title || session.goal || session.sessionId;
1187
+ const title = escapeHtml(`${rawTitle.slice(0, 60)}${rawTitle.length > 60 ? "..." : ""}`);
1188
+ const label = `${escapeHtml(session.provider)} · ${title}`;
792
1189
  return `<option value="${escapeHtml(session.sessionId)}">${label}</option>`;
793
1190
  })
794
1191
  )
@@ -800,10 +1197,58 @@ function renderSessions(sessions) {
800
1197
  }
801
1198
  }
802
1199
 
1200
+ function currentManagedSession() {
1201
+ return lastSessions.find((session) => session.sessionId === managedSessionId) || lastSessions[0] || null;
1202
+ }
1203
+
1204
+ function renderSessionManager(message = "") {
1205
+ if (!sessionManagerListEl) return;
1206
+ if (!managedSessionId && currentSessionId) managedSessionId = currentSessionId;
1207
+ if (!lastSessions.some((session) => session.sessionId === managedSessionId)) {
1208
+ managedSessionId = lastSessions[0]?.sessionId || "";
1209
+ }
1210
+
1211
+ const selected = currentManagedSession();
1212
+ sessionTitleInputEl.value = selected ? selected.title || selected.goal || "" : "";
1213
+ sessionTitleInputEl.disabled = !selected;
1214
+ autoRenameSessionButton.disabled = !selected;
1215
+ renameSessionButton.disabled = !selected;
1216
+ deleteSessionButton.disabled = !selected;
1217
+ sessionManagerStatusEl.textContent = message;
1218
+
1219
+ if (!lastSessions.length) {
1220
+ sessionManagerListEl.innerHTML = `<p class="subtle">${t("noSessionsToManage")}</p>`;
1221
+ return;
1222
+ }
1223
+
1224
+ sessionManagerListEl.innerHTML = lastSessions
1225
+ .map((session) => {
1226
+ const title = session.title || session.goal || session.sessionId;
1227
+ const meta = [session.status, session.provider, session.updatedAt ? new Date(session.updatedAt).toLocaleString() : ""]
1228
+ .filter(Boolean)
1229
+ .join(" · ");
1230
+ return `
1231
+ <button class="session-row" type="button" data-session-id="${escapeHtml(session.sessionId)}" data-selected="${session.sessionId === managedSessionId}">
1232
+ <strong>${escapeHtml(title)}</strong>
1233
+ <span>${escapeHtml(meta)}</span>
1234
+ </button>
1235
+ `;
1236
+ })
1237
+ .join("");
1238
+ }
1239
+
803
1240
  async function refreshSessions() {
804
1241
  const response = await fetch("/api/sessions");
805
1242
  const data = await response.json();
806
1243
  renderSessions(data.sessions || []);
1244
+ renderSessionManager();
1245
+ }
1246
+
1247
+ async function refreshWorkspaceChanges() {
1248
+ const response = await fetch("/api/workspace/changes");
1249
+ if (!response.ok) return;
1250
+ const data = await response.json();
1251
+ renderWorkspacePanel(data.workspace, data.activity || []);
807
1252
  }
808
1253
 
809
1254
  async function refreshRun() {
@@ -813,12 +1258,15 @@ async function refreshRun() {
813
1258
  const run = await response.json();
814
1259
  runMetaEl.textContent = `${run.status} · ${run.sessionId}`;
815
1260
  renderLogs(run);
1261
+ await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
816
1262
 
817
1263
  if (run.status === "finished" || run.status === "failed") {
818
1264
  clearInterval(pollTimer);
819
1265
  pollTimer = null;
820
1266
  await refreshChat();
821
1267
  await refreshSessions();
1268
+ await refreshWorkspaceChanges();
1269
+ await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
822
1270
  }
823
1271
  }
824
1272
 
@@ -880,6 +1328,88 @@ async function runSandboxPreflight() {
880
1328
  .join("\n\n");
881
1329
  }
882
1330
 
1331
+ async function openSessionManager() {
1332
+ await refreshSessions();
1333
+ managedSessionId = currentSessionId || managedSessionId || lastSessions[0]?.sessionId || "";
1334
+ renderSessionManager();
1335
+ sessionManagerDialog.showModal();
1336
+ }
1337
+
1338
+ function closeSessionManager() {
1339
+ sessionManagerDialog.close();
1340
+ sessionManagerStatusEl.textContent = "";
1341
+ }
1342
+
1343
+ async function renameManagedSession() {
1344
+ const selected = currentManagedSession();
1345
+ if (!selected) return;
1346
+
1347
+ const title = sessionTitleInputEl.value.trim();
1348
+ if (!title) {
1349
+ sessionManagerStatusEl.textContent = t("titleRequired");
1350
+ return;
1351
+ }
1352
+
1353
+ const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}`, {
1354
+ method: "PATCH",
1355
+ headers: { "Content-Type": "application/json" },
1356
+ body: JSON.stringify({ title }),
1357
+ });
1358
+ const data = await response.json().catch(() => ({}));
1359
+ if (!response.ok) {
1360
+ sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
1361
+ return;
1362
+ }
1363
+
1364
+ await refreshSessions();
1365
+ renderSessionManager(t("sessionRenamed"));
1366
+ }
1367
+
1368
+ async function autoRenameManagedSession() {
1369
+ const selected = currentManagedSession();
1370
+ if (!selected) return;
1371
+
1372
+ const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}/auto-title`, {
1373
+ method: "POST",
1374
+ });
1375
+ const data = await response.json().catch(() => ({}));
1376
+ if (!response.ok) {
1377
+ sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
1378
+ return;
1379
+ }
1380
+
1381
+ await refreshSessions();
1382
+ renderSessionManager(t("sessionRenamed"));
1383
+ }
1384
+
1385
+ async function deleteManagedSession() {
1386
+ const selected = currentManagedSession();
1387
+ if (!selected || !confirm(t("deleteConfirm"))) return;
1388
+
1389
+ const response = await fetch(`/api/sessions/${encodeURIComponent(selected.sessionId)}`, {
1390
+ method: "DELETE",
1391
+ });
1392
+ const data = await response.json().catch(() => ({}));
1393
+ if (!response.ok) {
1394
+ sessionManagerStatusEl.textContent = data.error || t("manageSessionFailed");
1395
+ return;
1396
+ }
1397
+
1398
+ if (currentSessionId === selected.sessionId) {
1399
+ currentSessionId = "";
1400
+ sessionSelectEl.value = "";
1401
+ runMetaEl.textContent = "";
1402
+ setLogs(t("noRunSelected"), "empty");
1403
+ renderChat([]);
1404
+ artifactItems = [];
1405
+ selectedArtifactId = "";
1406
+ renderArtifactShell();
1407
+ }
1408
+ managedSessionId = "";
1409
+ await refreshSessions();
1410
+ renderSessionManager(t("sessionDeleted"));
1411
+ }
1412
+
883
1413
  function schedulePreferenceSave() {
884
1414
  clearTimeout(saveTimer);
885
1415
  saveTimer = setTimeout(() => {
@@ -908,13 +1438,101 @@ preflightSandboxButton.addEventListener("click", () => {
908
1438
  });
909
1439
  });
910
1440
 
1441
+ manageSessionsButton.addEventListener("click", () => {
1442
+ openSessionManager().catch((error) => {
1443
+ chatStatusEl.textContent = String(error);
1444
+ });
1445
+ });
1446
+
1447
+ openArtifactsButton.addEventListener("click", () => {
1448
+ openArtifactTunnel().catch((error) => {
1449
+ chatStatusEl.textContent = String(error);
1450
+ });
1451
+ });
1452
+
1453
+ closeSessionManagerButton.addEventListener("click", closeSessionManager);
1454
+ cancelSessionManagerButton.addEventListener("click", closeSessionManager);
1455
+ closeArtifactTunnelButton.addEventListener("click", closeArtifactTunnel);
1456
+
1457
+ sessionManagerDialog.addEventListener("click", (event) => {
1458
+ if (event.target === sessionManagerDialog) closeSessionManager();
1459
+ });
1460
+
1461
+ artifactTunnelDialog.addEventListener("click", (event) => {
1462
+ if (event.target === artifactTunnelDialog) closeArtifactTunnel();
1463
+ });
1464
+
1465
+ artifactTabsEl.addEventListener("click", (event) => {
1466
+ const tab = event.target.closest("[data-artifact-tab]");
1467
+ if (!tab) return;
1468
+ currentArtifactTab = tab.dataset.artifactTab || "canvas";
1469
+ renderArtifactShell();
1470
+ });
1471
+
1472
+ artifactListEl.addEventListener("click", (event) => {
1473
+ const row = event.target.closest("[data-artifact-id]");
1474
+ if (!row) return;
1475
+ selectArtifact(row.dataset.artifactId || "").catch((error) => {
1476
+ artifactStatusEl.textContent = String(error);
1477
+ });
1478
+ });
1479
+
1480
+ markArtifactsSeenButton.addEventListener("click", () => {
1481
+ markAllArtifactsRead();
1482
+ renderArtifactBadge();
1483
+ renderArtifactList();
1484
+ artifactStatusEl.textContent = t("artifactSeenUpdated");
1485
+ refreshArtifacts({ loadSelected: false }).catch(() => {});
1486
+ });
1487
+
1488
+ sessionManagerListEl.addEventListener("click", (event) => {
1489
+ const row = event.target.closest("[data-session-id]");
1490
+ if (!row) return;
1491
+ managedSessionId = row.dataset.sessionId || "";
1492
+ renderSessionManager();
1493
+ });
1494
+
1495
+ renameSessionButton.addEventListener("click", () => {
1496
+ renameManagedSession().catch((error) => {
1497
+ sessionManagerStatusEl.textContent = String(error);
1498
+ });
1499
+ });
1500
+
1501
+ autoRenameSessionButton.addEventListener("click", () => {
1502
+ autoRenameManagedSession().catch((error) => {
1503
+ sessionManagerStatusEl.textContent = String(error);
1504
+ });
1505
+ });
1506
+
1507
+ deleteSessionButton.addEventListener("click", () => {
1508
+ deleteManagedSession().catch((error) => {
1509
+ sessionManagerStatusEl.textContent = String(error);
1510
+ });
1511
+ });
1512
+
911
1513
  providerField.addEventListener("change", () => {
912
- if (!modelField.value.trim() || modelField.value === defaults.openai || modelField.value === defaults.deepseek) {
1514
+ if (providerField.value === "mock") {
1515
+ routingModeField.value = "manual";
1516
+ }
1517
+
1518
+ if (
1519
+ !modelField.value.trim() ||
1520
+ modelField.value === defaults.openai ||
1521
+ modelField.value === defaults.deepseek ||
1522
+ modelField.value === defaults.mock
1523
+ ) {
913
1524
  modelField.value = defaults[providerField.value] || "";
914
1525
  }
1526
+ updateRoutingHint();
915
1527
  schedulePreferenceSave();
916
1528
  });
917
1529
 
1530
+ modelField.addEventListener("input", updateRoutingHint);
1531
+ sandboxModeField.addEventListener("change", updatePackageWarning);
1532
+ packageInstallPolicyField.addEventListener("change", updatePackageWarning);
1533
+ allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
1534
+ preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
1535
+
918
1536
  form.addEventListener("input", schedulePreferenceSave);
919
1537
  form.addEventListener("change", schedulePreferenceSave);
920
1538
 
@@ -953,6 +1571,8 @@ form.addEventListener("submit", async (event) => {
953
1571
  runMetaEl.textContent = `${t("runningStatus").replace("...", "")} · ${currentSessionId}`;
954
1572
  await refreshSessions();
955
1573
  await refreshChat();
1574
+ await refreshWorkspaceChanges();
1575
+ await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
956
1576
 
957
1577
  if (pollTimer) clearInterval(pollTimer);
958
1578
  pollTimer = setInterval(refreshRun, 1500);
@@ -965,10 +1585,14 @@ sessionSelectEl.addEventListener("change", async () => {
965
1585
  runMetaEl.textContent = "";
966
1586
  setLogs(t("noRunSelected"), "empty");
967
1587
  renderChat([]);
1588
+ artifactItems = [];
1589
+ selectedArtifactId = "";
1590
+ renderArtifactShell();
968
1591
  return;
969
1592
  }
970
1593
  await refreshRun();
971
1594
  await refreshChat();
1595
+ await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
972
1596
  });
973
1597
 
974
1598
  chatFormEl.addEventListener("submit", async (event) => {
@@ -1007,6 +1631,8 @@ chatFormEl.addEventListener("submit", async (event) => {
1007
1631
  chatStatusEl.textContent = t("runningStatus");
1008
1632
  await refreshSessions();
1009
1633
  await refreshChat();
1634
+ await refreshWorkspaceChanges();
1635
+ await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
1010
1636
 
1011
1637
  if (pollTimer) clearInterval(pollTimer);
1012
1638
  pollTimer = setInterval(refreshRun, 1500);
@@ -1020,6 +1646,7 @@ async function loadConfig() {
1020
1646
  routingPresets = data.routing?.presets || {};
1021
1647
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
1022
1648
  defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
1649
+ defaults.mock = data.defaults?.mock?.model || defaults.mock;
1023
1650
 
1024
1651
  applyLanguage(prefs.language || normalizeLanguage(navigator.language || "en"), { persist: false });
1025
1652
 
@@ -1031,18 +1658,23 @@ async function loadConfig() {
1031
1658
  document.querySelector("#commandCwd").value = prefs.commandCwd || "/home/lachlan/ProjectsLFS/Agent";
1032
1659
  document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
1033
1660
  document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
1034
- sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-readonly" : "host");
1661
+ sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
1035
1662
  packageInstallPolicyField.value = prefs.packageInstallPolicy || "prompt";
1036
1663
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
1037
- document.querySelector("#allowWrapperTools").checked = prefs.allowWrapperTools ?? false;
1664
+ document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
1665
+ allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
1666
+ preferredWrapperField.value = prefs.preferredWrapper || "codex";
1038
1667
  document.querySelector("#dockerSandboxImage").value = prefs.dockerSandboxImage || "agintiflow-sandbox:latest";
1039
1668
  document.querySelector("#allowPasswords").checked = prefs.allowPasswords ?? false;
1040
1669
  document.querySelector("#allowDestructive").checked = prefs.allowDestructive ?? false;
1041
1670
 
1042
1671
  renderKeyStatus(data.keyStatus);
1043
1672
  renderWrapperStatus(data.wrappers || []);
1673
+ renderWorkspacePanel(data.workspace, []);
1674
+ await refreshWorkspaceChanges();
1044
1675
  renderSandboxLogs(data.sandbox?.logs || []);
1045
1676
  updateRoutingHint();
1677
+ updatePackageWarning();
1046
1678
 
1047
1679
  renderSessions(data.sessions || []);
1048
1680
  if (data.sessions && data.sessions.length > 0) {
@@ -1050,6 +1682,7 @@ async function loadConfig() {
1050
1682
  sessionSelectEl.value = currentSessionId;
1051
1683
  await refreshRun();
1052
1684
  await refreshChat();
1685
+ await refreshArtifacts({ loadSelected: false });
1053
1686
  }
1054
1687
  }
1055
1688