@lazyingart/agintiflow 0.20.183 → 0.20.185

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.
@@ -7,6 +7,10 @@ AgInTiFlow separates **skills** from **tools**:
7
7
 
8
8
  The optional image-generation skills are `image_generation` and `venice_image_generation`. Both use the deterministic `generate_image` tool.
9
9
 
10
+ `generate_image` is a raster-image tool. It does not produce true SVG/vector output. If a caller requests `svg`, the tool records
11
+ `requestedFormat: "svg"`, generates a PNG fallback with `actualFormat: "png"`, and returns a `formatNotice`. If the task truly requires
12
+ editable vectors, use AgInTiFlow file tools to write deterministic SVG/LaTeX/HTML instead of calling `generate_image`.
13
+
10
14
  ## Setup
11
15
 
12
16
  Store a GRS AI or Venice key project-locally:
@@ -38,11 +42,39 @@ For image, cover, poster, illustration, photo, or logo-concept requests, the mod
38
42
  "model": "nano-banana-2",
39
43
  "outputDir": "artifacts/images/robot-cover",
40
44
  "outputStem": "robot-cover",
45
+ "format": "png",
41
46
  "aspectRatio": "1:1",
42
47
  "imageSize": "2K"
43
48
  }
44
49
  ```
45
50
 
51
+ For trusted local apps that need image generation without routing through a language-model run, the web server exposes a deterministic endpoint:
52
+
53
+ ```bash
54
+ curl -sS http://127.0.0.1:3210/api/auxiliary/generate-image \
55
+ -H 'Content-Type: application/json' \
56
+ -d '{
57
+ "prompt": "A polished cyan robot painting a circuit-board river",
58
+ "provider": "venice",
59
+ "format": "svg",
60
+ "outputDir": "artifacts/images/robot-cover",
61
+ "outputStem": "robot-cover"
62
+ }'
63
+ ```
64
+
65
+ This endpoint is intended for trusted local/server-side callers. Browser apps on another origin should use an explicit adapter/proxy
66
+ rather than relying on permissive CORS, because generation can consume API keys and write workspace artifacts.
67
+
68
+ That SVG request returns a PNG fallback contract rather than pretending SVG was generated:
69
+
70
+ ```json
71
+ {
72
+ "requestedFormat": "svg",
73
+ "actualFormat": "png",
74
+ "formatNotice": "SVG/vector output is not supported by generate_image. A raster PNG is generated instead."
75
+ }
76
+ ```
77
+
46
78
  With GRS AI, the tool uses the Nano Banana API:
47
79
 
48
80
  - `POST https://grsaiapi.com/v1/draw/nano-banana`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.183",
3
+ "version": "0.20.185",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -145,6 +145,14 @@ const translations = {
145
145
  wrappersOffLabel: "wrapper tools off",
146
146
  wrapperCapabilityTitle: "Agent wrappers",
147
147
  workspaceCapabilityTitle: "Workspace files",
148
+ workspaceExplorerTitle: "Workspace explorer",
149
+ workspaceExplorerHelp: "Browse, classify, drag into chat, and open project files.",
150
+ workspaceRefreshButton: "Refresh",
151
+ workspaceAutomapButton: "Auto map",
152
+ workspaceWorkbenchTitle: "Workspace workbench",
153
+ workspaceNoFileSelected: "Select a file from the explorer.",
154
+ workspaceSaveButton: "Save file",
155
+ workspaceRevertButton: "Revert",
148
156
  mcpCapabilityTitle: "MCP servers",
149
157
  mcpEmpty: "No MCP servers configured.",
150
158
  workspaceToolsLabel: "File tools",
@@ -746,6 +754,19 @@ const wrapperGridEl = document.querySelector("#wrapper-grid");
746
754
  const workspaceStatusEl = document.querySelector("#workspace-status");
747
755
  const workspaceToolsEl = document.querySelector("#workspace-tools");
748
756
  const workspaceChangesEl = document.querySelector("#workspace-changes");
757
+ const workspaceExplorerEl = document.querySelector("#workspace-explorer");
758
+ const workspaceBrowserStatusEl = document.querySelector("#workspace-browser-status");
759
+ const workspaceLanesEl = document.querySelector("#workspace-lanes");
760
+ const workspaceTreeEl = document.querySelector("#workspace-tree");
761
+ const workspaceRefreshButton = document.querySelector("#workspace-refresh");
762
+ const workspaceAutomapButton = document.querySelector("#workspace-automap");
763
+ const workspaceActiveTitleEl = document.querySelector("#workspace-active-title");
764
+ const workspaceActiveMetaEl = document.querySelector("#workspace-active-meta");
765
+ const workspacePreviewEl = document.querySelector("#workspace-preview");
766
+ const workspaceEditorEl = document.querySelector("#workspace-editor");
767
+ const workspaceSaveButton = document.querySelector("#workspace-save");
768
+ const workspaceRevertButton = document.querySelector("#workspace-revert");
769
+ const workspaceEditorStatusEl = document.querySelector("#workspace-editor-status");
749
770
  const mcpStatusEl = document.querySelector("#mcp-status");
750
771
  const mcpGridEl = document.querySelector("#mcp-grid");
751
772
  const mcpWarningsEl = document.querySelector("#mcp-warnings");
@@ -835,6 +856,31 @@ let currentArtifactTab = "canvas";
835
856
  let artifactUnreadCount = 0;
836
857
  let lastAnnouncedRunKey = "";
837
858
  let quickVeniceModeActive = false;
859
+ const WORKSPACE_LANES = [
860
+ { id: "all", label: "All" },
861
+ { id: "source", label: "Source" },
862
+ { id: "scripts", label: "Scripts" },
863
+ { id: "docs", label: "Docs" },
864
+ { id: "data", label: "Data" },
865
+ { id: "media", label: "Media" },
866
+ { id: "references", label: "Refs" },
867
+ { id: "artifacts", label: "Artifacts" },
868
+ { id: "logs", label: "Logs" },
869
+ { id: "other", label: "Other" },
870
+ ];
871
+ let workspaceBrowser = {
872
+ root: "",
873
+ files: [],
874
+ warnings: [],
875
+ activeLane: "all",
876
+ activePath: "",
877
+ activeContent: "",
878
+ savedContent: "",
879
+ overrides: {},
880
+ loading: false,
881
+ error: "",
882
+ initialized: false,
883
+ };
838
884
 
839
885
  function normalizeLanguage(language) {
840
886
  const lower = String(language || "").toLowerCase();
@@ -1156,6 +1202,278 @@ function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspac
1156
1202
  .join("");
1157
1203
  }
1158
1204
 
1205
+ function workspaceRoot() {
1206
+ return commandCwdField?.value.trim() || projectInfo?.commandCwd || projectInfo?.root || "";
1207
+ }
1208
+
1209
+ function workspaceExt(filePath = "") {
1210
+ const name = String(filePath || "").toLowerCase();
1211
+ const index = name.lastIndexOf(".");
1212
+ return index >= 0 ? name.slice(index) : "";
1213
+ }
1214
+
1215
+ function workspaceDir(filePath = "") {
1216
+ const normalized = String(filePath || "").replace(/\\/g, "/");
1217
+ const index = normalized.lastIndexOf("/");
1218
+ return index >= 0 ? normalized.slice(0, index) : "";
1219
+ }
1220
+
1221
+ function workspaceBaseName(filePath = "") {
1222
+ return String(filePath || "").replace(/\\/g, "/").split("/").filter(Boolean).pop() || filePath;
1223
+ }
1224
+
1225
+ function inferWorkspaceLane(file = {}) {
1226
+ const filePath = String(file.path || "").toLowerCase();
1227
+ const first = filePath.split("/")[0] || "";
1228
+ const ext = workspaceExt(filePath);
1229
+ if (workspaceBrowser.overrides[file.path]) return workspaceBrowser.overrides[file.path];
1230
+ if (["artifacts", "runs", "outputs", "reports"].includes(first)) return "artifacts";
1231
+ if (["logs", "log", "events"].includes(first) || ext === ".log") return "logs";
1232
+ if (["data", "datasets", "tables"].includes(first) || [".csv", ".tsv", ".jsonl", ".xlsx", ".parquet"].includes(ext)) return "data";
1233
+ if (["docs", "doc", "references", "reference"].includes(first) || [".md", ".txt", ".tex", ".rst"].includes(ext)) return "docs";
1234
+ if (["refs", "bibliography"].includes(first) || [".bib", ".ris", ".enw"].includes(ext)) return "references";
1235
+ if (["scripts", "bin"].includes(first) || [".py", ".r", ".sh", ".m", ".pl"].includes(ext)) return "scripts";
1236
+ if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".pdf", ".mp4", ".mov", ".wav", ".mp3"].includes(ext)) return "media";
1237
+ if ([".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".html", ".go", ".rs", ".c", ".cpp", ".h", ".hpp", ".java", ".rb", ".php", ".lua"].includes(ext)) {
1238
+ return "source";
1239
+ }
1240
+ return "other";
1241
+ }
1242
+
1243
+ function workspaceFilesByLane() {
1244
+ const groups = Object.fromEntries(WORKSPACE_LANES.map((lane) => [lane.id, []]));
1245
+ for (const file of workspaceBrowser.files) {
1246
+ groups.all.push(file);
1247
+ const lane = inferWorkspaceLane(file);
1248
+ if (!groups[lane]) groups.other.push(file);
1249
+ else groups[lane].push(file);
1250
+ }
1251
+ return groups;
1252
+ }
1253
+
1254
+ function workspaceActiveFile() {
1255
+ return workspaceBrowser.files.find((file) => file.path === workspaceBrowser.activePath) || null;
1256
+ }
1257
+
1258
+ function formatBytes(size = 0) {
1259
+ const value = Number(size || 0);
1260
+ if (value < 1024) return `${value} B`;
1261
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
1262
+ return `${(value / 1024 / 1024).toFixed(1)} MB`;
1263
+ }
1264
+
1265
+ function workspaceRawUrl(filePath = "") {
1266
+ const params = new URLSearchParams({ commandCwd: workspaceBrowser.root || workspaceRoot(), path: filePath });
1267
+ return `/api/workspace/raw?${params.toString()}`;
1268
+ }
1269
+
1270
+ function setWorkspaceEditorStatus(message = "", kind = "info") {
1271
+ if (!workspaceEditorStatusEl) return;
1272
+ workspaceEditorStatusEl.textContent = message;
1273
+ workspaceEditorStatusEl.dataset.kind = kind;
1274
+ }
1275
+
1276
+ function renderWorkspaceExplorer() {
1277
+ if (!workspaceExplorerEl || !workspaceLanesEl || !workspaceTreeEl || !workspaceBrowserStatusEl) return;
1278
+ const groups = workspaceFilesByLane();
1279
+ const activeLane = workspaceBrowser.activeLane || "all";
1280
+ const warnings = workspaceBrowser.warnings?.length ? ` · ${workspaceBrowser.warnings.length} warning(s)` : "";
1281
+ const truncated = workspaceBrowser.truncated ? " · truncated" : "";
1282
+ const status = workspaceBrowser.loading
1283
+ ? "Loading workspace..."
1284
+ : workspaceBrowser.error
1285
+ ? `Workspace unavailable: ${workspaceBrowser.error}`
1286
+ : `${workspaceBrowser.files.length} files · ${workspaceBrowser.root || workspaceRoot()}${warnings}${truncated}`;
1287
+ workspaceBrowserStatusEl.textContent = status;
1288
+
1289
+ workspaceLanesEl.innerHTML = WORKSPACE_LANES.map((lane) => {
1290
+ const count = groups[lane.id]?.length || 0;
1291
+ return `
1292
+ <button
1293
+ type="button"
1294
+ class="workspace-lane"
1295
+ data-workspace-lane="${escapeHtml(lane.id)}"
1296
+ data-selected="${lane.id === activeLane}"
1297
+ title="Drop a file here to classify it as ${escapeHtml(lane.label)}"
1298
+ >
1299
+ <span>${escapeHtml(lane.label)}</span>
1300
+ <strong>${count}</strong>
1301
+ </button>
1302
+ `;
1303
+ }).join("");
1304
+
1305
+ const visibleFiles = (groups[activeLane] || []).slice().sort((left, right) => left.path.localeCompare(right.path));
1306
+ if (!visibleFiles.length) {
1307
+ workspaceTreeEl.innerHTML = `<p class="subtle workspace-empty">No files in this category.</p>`;
1308
+ return;
1309
+ }
1310
+
1311
+ workspaceTreeEl.innerHTML = visibleFiles
1312
+ .map((file) => {
1313
+ const lane = inferWorkspaceLane(file);
1314
+ const active = file.path === workspaceBrowser.activePath;
1315
+ const meta = [lane, formatBytes(file.size), file.binary ? "binary" : "text"].join(" · ");
1316
+ return `
1317
+ <button
1318
+ type="button"
1319
+ class="workspace-file-row"
1320
+ data-workspace-file="${escapeHtml(file.path)}"
1321
+ data-selected="${active}"
1322
+ draggable="true"
1323
+ >
1324
+ <span class="workspace-file-main">
1325
+ <strong>${escapeHtml(workspaceBaseName(file.path))}</strong>
1326
+ <small>${escapeHtml(workspaceDir(file.path) || ".")}</small>
1327
+ </span>
1328
+ <span class="workspace-file-meta">${escapeHtml(meta)}</span>
1329
+ </button>
1330
+ `;
1331
+ })
1332
+ .join("");
1333
+ }
1334
+
1335
+ function renderWorkspaceWorkbench() {
1336
+ if (!workspaceActiveTitleEl || !workspaceActiveMetaEl || !workspacePreviewEl || !workspaceEditorEl) return;
1337
+ const file = workspaceActiveFile();
1338
+ const dirty = workspaceBrowser.activeContent !== workspaceBrowser.savedContent;
1339
+
1340
+ if (!file) {
1341
+ workspaceActiveTitleEl.textContent = t("workspaceNoFileSelected");
1342
+ workspaceActiveMetaEl.textContent = t("workspaceNoFileSelected");
1343
+ workspacePreviewEl.innerHTML = `<p class="subtle">${t("workspaceNoFileSelected")}</p>`;
1344
+ workspaceEditorEl.hidden = true;
1345
+ if (workspaceSaveButton) workspaceSaveButton.disabled = true;
1346
+ if (workspaceRevertButton) workspaceRevertButton.disabled = true;
1347
+ setWorkspaceEditorStatus("");
1348
+ return;
1349
+ }
1350
+
1351
+ workspaceActiveTitleEl.textContent = file.path;
1352
+ workspaceActiveMetaEl.textContent = `${inferWorkspaceLane(file)} · ${formatBytes(file.size)} · ${
1353
+ file.binary ? "preview-only" : dirty ? "edited" : "saved"
1354
+ }`;
1355
+
1356
+ const ext = workspaceExt(file.path);
1357
+ const rawUrl = workspaceRawUrl(file.path);
1358
+ if (file.binary) {
1359
+ workspaceEditorEl.hidden = true;
1360
+ if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(ext)) {
1361
+ workspacePreviewEl.innerHTML = `<img class="workspace-media-preview" src="${escapeHtml(rawUrl)}" alt="${escapeHtml(file.path)}" />`;
1362
+ } else if (ext === ".pdf") {
1363
+ workspacePreviewEl.innerHTML = `<iframe class="workspace-pdf-preview" src="${escapeHtml(rawUrl)}" title="${escapeHtml(file.path)}"></iframe>`;
1364
+ } else {
1365
+ workspacePreviewEl.innerHTML = `<p class="subtle">Binary file preview is unavailable. Use the raw file path or artifact canvas for specialized previews.</p>`;
1366
+ }
1367
+ if (workspaceSaveButton) workspaceSaveButton.disabled = true;
1368
+ if (workspaceRevertButton) workspaceRevertButton.disabled = true;
1369
+ setWorkspaceEditorStatus("Preview-only binary file.");
1370
+ return;
1371
+ }
1372
+
1373
+ workspacePreviewEl.innerHTML = `<pre class="workspace-code-preview">${escapeHtml(workspaceBrowser.activeContent).slice(
1374
+ 0,
1375
+ 12000
1376
+ )}</pre>`;
1377
+ workspaceEditorEl.hidden = false;
1378
+ if (document.activeElement !== workspaceEditorEl && workspaceEditorEl.value !== workspaceBrowser.activeContent) {
1379
+ workspaceEditorEl.value = workspaceBrowser.activeContent;
1380
+ }
1381
+ if (workspaceSaveButton) workspaceSaveButton.disabled = !dirty;
1382
+ if (workspaceRevertButton) workspaceRevertButton.disabled = !dirty;
1383
+ setWorkspaceEditorStatus(dirty ? "Unsaved editor changes." : "File loaded.");
1384
+ }
1385
+
1386
+ function renderWorkspaceBrowser() {
1387
+ renderWorkspaceExplorer();
1388
+ renderWorkspaceWorkbench();
1389
+ }
1390
+
1391
+ async function refreshWorkspaceSnapshot({ preserveActive = true } = {}) {
1392
+ if (!workspaceExplorerEl) return;
1393
+ workspaceBrowser.loading = true;
1394
+ workspaceBrowser.error = "";
1395
+ renderWorkspaceExplorer();
1396
+ try {
1397
+ const response = await fetch("/api/workspace/snapshot", {
1398
+ method: "POST",
1399
+ headers: { "Content-Type": "application/json" },
1400
+ body: JSON.stringify({ commandCwd: workspaceRoot() }),
1401
+ });
1402
+ const data = await response.json().catch(() => ({}));
1403
+ if (!response.ok) throw new Error(data.error || "Workspace snapshot failed.");
1404
+ workspaceBrowser = {
1405
+ ...workspaceBrowser,
1406
+ root: data.root || workspaceRoot(),
1407
+ files: data.files || [],
1408
+ warnings: data.warnings || [],
1409
+ truncated: Boolean(data.truncated),
1410
+ loading: false,
1411
+ error: "",
1412
+ initialized: true,
1413
+ };
1414
+ if (!preserveActive || !workspaceBrowser.files.some((file) => file.path === workspaceBrowser.activePath)) {
1415
+ workspaceBrowser.activePath = workspaceBrowser.files.find((file) => !file.binary)?.path || workspaceBrowser.files[0]?.path || "";
1416
+ const active = workspaceActiveFile();
1417
+ workspaceBrowser.activeContent = active?.content || "";
1418
+ workspaceBrowser.savedContent = active?.content || "";
1419
+ } else {
1420
+ const active = workspaceActiveFile();
1421
+ if (!active?.binary && workspaceBrowser.activeContent === workspaceBrowser.savedContent) {
1422
+ workspaceBrowser.activeContent = active?.content || "";
1423
+ workspaceBrowser.savedContent = active?.content || "";
1424
+ }
1425
+ }
1426
+ } catch (error) {
1427
+ workspaceBrowser.loading = false;
1428
+ workspaceBrowser.error = error instanceof Error ? error.message : String(error);
1429
+ }
1430
+ renderWorkspaceBrowser();
1431
+ }
1432
+
1433
+ function selectWorkspaceFile(filePath = "") {
1434
+ const file = workspaceBrowser.files.find((item) => item.path === filePath);
1435
+ if (!file) return;
1436
+ workspaceBrowser.activePath = file.path;
1437
+ workspaceBrowser.activeContent = file.content || "";
1438
+ workspaceBrowser.savedContent = file.content || "";
1439
+ renderWorkspaceBrowser();
1440
+ }
1441
+
1442
+ async function saveWorkspaceEditor() {
1443
+ const file = workspaceActiveFile();
1444
+ if (!file || file.binary || !workspaceEditorEl) return;
1445
+ const content = workspaceEditorEl.value;
1446
+ setWorkspaceEditorStatus("Saving...");
1447
+ const response = await fetch("/api/workspace/write", {
1448
+ method: "POST",
1449
+ headers: { "Content-Type": "application/json" },
1450
+ body: JSON.stringify({ commandCwd: workspaceBrowser.root || workspaceRoot(), path: file.path, content }),
1451
+ });
1452
+ const data = await response.json().catch(() => ({}));
1453
+ if (!response.ok) {
1454
+ setWorkspaceEditorStatus(data.error || "Save failed.", "error");
1455
+ return;
1456
+ }
1457
+ workspaceBrowser.activeContent = content;
1458
+ workspaceBrowser.savedContent = content;
1459
+ showToast(`Saved ${file.path}.`);
1460
+ await refreshWorkspaceSnapshot({ preserveActive: true });
1461
+ }
1462
+
1463
+ function revertWorkspaceEditor() {
1464
+ workspaceBrowser.activeContent = workspaceBrowser.savedContent;
1465
+ if (workspaceEditorEl) workspaceEditorEl.value = workspaceBrowser.savedContent;
1466
+ renderWorkspaceWorkbench();
1467
+ }
1468
+
1469
+ function insertWorkspaceReference(filePath = "") {
1470
+ if (!chatInputEl || !filePath) return;
1471
+ const reference = `@file ${filePath}`;
1472
+ const current = chatInputEl.value.trimEnd();
1473
+ chatInputEl.value = current ? `${current}\n${reference}` : reference;
1474
+ chatInputEl.focus();
1475
+ }
1476
+
1159
1477
  function renderMcpPanel(mcp = lastMcp) {
1160
1478
  lastMcp = mcp || null;
1161
1479
  if (!mcpStatusEl || !mcpGridEl || !mcpWarningsEl) return;
@@ -1663,6 +1981,7 @@ function applyLanguage(language, { persist = true } = {}) {
1663
1981
  renderProjectStatus();
1664
1982
  renderWrapperStatus();
1665
1983
  renderWorkspacePanel();
1984
+ renderWorkspaceBrowser();
1666
1985
  renderSandboxStatus();
1667
1986
  updateRoutingHint();
1668
1987
  updatePermissionHint();
@@ -3085,6 +3404,7 @@ async function refreshRun() {
3085
3404
  await refreshChat();
3086
3405
  await refreshSessions();
3087
3406
  await refreshWorkspaceChanges();
3407
+ await refreshWorkspaceSnapshot({ preserveActive: true });
3088
3408
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
3089
3409
  if (run.status === "finished") await flushAfterFinishQueue();
3090
3410
  }
@@ -3335,6 +3655,7 @@ async function startRunFromGoal(goal, { clearComposer = false } = {}) {
3335
3655
  sessionSelectEl.value = currentSessionId;
3336
3656
  await refreshChat();
3337
3657
  await refreshWorkspaceChanges();
3658
+ await refreshWorkspaceSnapshot({ preserveActive: true });
3338
3659
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
3339
3660
 
3340
3661
  if (pollTimer) clearInterval(pollTimer);
@@ -3589,6 +3910,77 @@ commandCwdField?.addEventListener("input", () => {
3589
3910
  commandCwdField?.addEventListener("change", () => {
3590
3911
  schedulePreferenceSave();
3591
3912
  refreshCommandCwdSuggestions(commandCwdField.value).catch(() => {});
3913
+ refreshWorkspaceSnapshot({ preserveActive: false }).catch(() => {});
3914
+ });
3915
+
3916
+ workspaceRefreshButton?.addEventListener("click", () => {
3917
+ refreshWorkspaceSnapshot({ preserveActive: true }).catch((error) => {
3918
+ setWorkspaceEditorStatus(String(error), "error");
3919
+ });
3920
+ });
3921
+
3922
+ workspaceAutomapButton?.addEventListener("click", () => {
3923
+ workspaceBrowser.overrides = {};
3924
+ renderWorkspaceBrowser();
3925
+ showToast("Workspace files auto-mapped by general category.");
3926
+ });
3927
+
3928
+ workspaceLanesEl?.addEventListener("click", (event) => {
3929
+ const button = event.target.closest("[data-workspace-lane]");
3930
+ if (!button) return;
3931
+ workspaceBrowser.activeLane = button.dataset.workspaceLane || "all";
3932
+ renderWorkspaceBrowser();
3933
+ });
3934
+
3935
+ workspaceLanesEl?.addEventListener("dragover", (event) => {
3936
+ if (event.target.closest("[data-workspace-lane]")) event.preventDefault();
3937
+ });
3938
+
3939
+ workspaceLanesEl?.addEventListener("drop", (event) => {
3940
+ const button = event.target.closest("[data-workspace-lane]");
3941
+ const filePath = event.dataTransfer?.getData("text/agintiflow-workspace-file") || event.dataTransfer?.getData("text/plain");
3942
+ const lane = button?.dataset.workspaceLane;
3943
+ if (!button || !filePath || !lane || lane === "all") return;
3944
+ event.preventDefault();
3945
+ workspaceBrowser.overrides[filePath] = lane;
3946
+ workspaceBrowser.activeLane = lane;
3947
+ renderWorkspaceBrowser();
3948
+ });
3949
+
3950
+ workspaceTreeEl?.addEventListener("click", (event) => {
3951
+ const row = event.target.closest("[data-workspace-file]");
3952
+ if (!row) return;
3953
+ selectWorkspaceFile(row.dataset.workspaceFile || "");
3954
+ });
3955
+
3956
+ workspaceTreeEl?.addEventListener("dragstart", (event) => {
3957
+ const row = event.target.closest("[data-workspace-file]");
3958
+ if (!row || !event.dataTransfer) return;
3959
+ event.dataTransfer.setData("text/agintiflow-workspace-file", row.dataset.workspaceFile || "");
3960
+ event.dataTransfer.setData("text/plain", row.dataset.workspaceFile || "");
3961
+ event.dataTransfer.effectAllowed = "copyMove";
3962
+ });
3963
+
3964
+ workspaceEditorEl?.addEventListener("input", () => {
3965
+ workspaceBrowser.activeContent = workspaceEditorEl.value;
3966
+ renderWorkspaceWorkbench();
3967
+ });
3968
+
3969
+ workspaceSaveButton?.addEventListener("click", () => {
3970
+ saveWorkspaceEditor().catch((error) => setWorkspaceEditorStatus(String(error), "error"));
3971
+ });
3972
+
3973
+ workspaceRevertButton?.addEventListener("click", revertWorkspaceEditor);
3974
+
3975
+ chatInputEl?.addEventListener("dragover", (event) => {
3976
+ if (event.dataTransfer?.types?.includes("text/agintiflow-workspace-file")) event.preventDefault();
3977
+ });
3978
+
3979
+ chatInputEl?.addEventListener("drop", (event) => {
3980
+ const filePath = event.dataTransfer?.getData("text/agintiflow-workspace-file");
3981
+ if (!filePath) return;
3982
+ event.preventDefault();
3983
+ insertWorkspaceReference(filePath);
3592
3984
  });
3593
3985
 
3594
3986
  chatInputEl.addEventListener("keydown", (event) => {
@@ -3724,6 +4116,7 @@ chatFormEl.addEventListener("submit", async (event) => {
3724
4116
  await refreshSessions();
3725
4117
  await refreshChat();
3726
4118
  await refreshWorkspaceChanges();
4119
+ await refreshWorkspaceSnapshot({ preserveActive: true });
3727
4120
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
3728
4121
 
3729
4122
  if (pollTimer) clearInterval(pollTimer);
@@ -3835,6 +4228,7 @@ async function loadConfig() {
3835
4228
  renderWorkspacePanel(data.workspace, []);
3836
4229
  renderMcpPanel(data.mcp || null);
3837
4230
  await refreshWorkspaceChanges();
4231
+ await refreshWorkspaceSnapshot({ preserveActive: false });
3838
4232
  renderSandboxLogs(data.sandbox?.logs || []);
3839
4233
  if (quickVeniceModeActive) applyVeniceMode(true);
3840
4234
  updateRoutingHint();
package/public/index.html CHANGED
@@ -223,6 +223,22 @@
223
223
  <button id="open-settings" type="button" class="secondary settings-button">Advanced settings</button>
224
224
  </form>
225
225
  </details>
226
+
227
+ <section id="workspace-explorer" class="workspace-explorer-card" aria-labelledby="workspace-explorer-title">
228
+ <div class="workspace-explorer-head">
229
+ <div>
230
+ <h2 id="workspace-explorer-title" data-i18n="workspaceExplorerTitle">Workspace explorer</h2>
231
+ <p class="subtle" data-i18n="workspaceExplorerHelp">Browse, classify, drag into chat, and open project files.</p>
232
+ </div>
233
+ <div class="workspace-explorer-actions">
234
+ <button id="workspace-refresh" type="button" class="secondary" data-i18n="workspaceRefreshButton">Refresh</button>
235
+ <button id="workspace-automap" type="button" class="secondary" data-i18n="workspaceAutomapButton">Auto map</button>
236
+ </div>
237
+ </div>
238
+ <div id="workspace-browser-status" class="subtle workspace-browser-status" aria-live="polite">Loading workspace...</div>
239
+ <div id="workspace-lanes" class="workspace-lanes" aria-label="Workspace file categories"></div>
240
+ <div id="workspace-tree" class="workspace-tree" aria-live="polite"></div>
241
+ </section>
226
242
  </section>
227
243
 
228
244
  <section class="stack">
@@ -306,6 +322,23 @@
306
322
  <div id="chat-pending" class="chat-pending" hidden></div>
307
323
  </section>
308
324
 
325
+ <section id="workspace-workbench" class="panel workspace-workbench">
326
+ <div class="header-row">
327
+ <div>
328
+ <h2 data-i18n="workspaceWorkbenchTitle">Workspace workbench</h2>
329
+ <p id="workspace-active-meta" class="subtle" data-i18n="workspaceNoFileSelected">Select a file from the explorer.</p>
330
+ </div>
331
+ <div class="workspace-editor-actions">
332
+ <button id="workspace-revert" type="button" class="secondary" disabled data-i18n="workspaceRevertButton">Revert</button>
333
+ <button id="workspace-save" type="button" disabled data-i18n="workspaceSaveButton">Save file</button>
334
+ </div>
335
+ </div>
336
+ <div id="workspace-active-title" class="workspace-active-title" data-i18n="workspaceNoFileSelected">Select a file from the explorer.</div>
337
+ <div id="workspace-preview" class="workspace-preview"></div>
338
+ <textarea id="workspace-editor" class="workspace-editor" spellcheck="false" hidden></textarea>
339
+ <div id="workspace-editor-status" class="subtle workspace-editor-status"></div>
340
+ </section>
341
+
309
342
  <section class="panel output-panel">
310
343
  <div class="header-row">
311
344
  <h2 data-i18n="runOutputTitle">Run output</h2>