@lazyingart/agintiflow 0.2.0 → 0.4.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/README.md CHANGED
@@ -38,8 +38,12 @@ Install the published CLI:
38
38
 
39
39
  ```bash
40
40
  npm install -g @lazyingart/agintiflow
41
+ cd /path/to/your-project
42
+ aginti init
43
+ aginti doctor
41
44
  aginti --list-routes
42
- aginti --sandbox-status --sandbox-mode docker-readonly --cwd "$PWD"
45
+ aginti --list-profiles
46
+ aginti --sandbox-status --sandbox-mode docker-readonly
43
47
  ```
44
48
 
45
49
  Launch the local web UI from an installed package:
@@ -49,10 +53,23 @@ aginti web --port 3210
49
53
  # then open http://127.0.0.1:3210
50
54
  ```
51
55
 
56
+ `aginti web` uses the folder it is launched from as the project root, default working directory, session store, and settings database. CLI and web runs share the same project-local `.sessions/` folder.
57
+
52
58
  Run the installed CLI without a live provider key by using the local mock route:
53
59
 
54
60
  ```bash
55
- aginti --provider mock --routing manual --allow-shell --cwd "$PWD" "Report the current directory"
61
+ aginti --provider mock --routing manual --allow-file-tools "Create notes/hello.md with a smoke-test note"
62
+ ```
63
+
64
+ Useful project commands:
65
+
66
+ ```bash
67
+ aginti keys status
68
+ printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
69
+ aginti sessions list
70
+ aginti sessions show <session-id>
71
+ aginti resume <session-id> "continue with a short follow-up"
72
+ aginti --profile code --provider mock --routing manual "Create notes/hello.md"
56
73
  ```
57
74
 
58
75
  Run from a source checkout:
@@ -101,6 +118,9 @@ The package exposes both `aginti` and `aginti-cli`; they run the same CLI entryp
101
118
  The web app includes:
102
119
 
103
120
  - Routing dropdown for smart, fast, complex, and manual model selection.
121
+ - Project folder indicator showing project root, command cwd, session folder, and session database.
122
+ - First-run provider setup panel with mock fallback and project-local DeepSeek/OpenAI key save.
123
+ - Task profile dropdown for code, writing, design docs, Python, shell, Node, AAPS, LaTeX, and system maintenance workflows.
104
124
  - Provider dropdown for DeepSeek, OpenAI, and local mock mode when manual routing is needed.
105
125
  - Language dropdown with 11 persisted UI locales.
106
126
  - Editable model field, with DeepSeek v4 flash as the fast default and DeepSeek v4 pro as the complex route.
@@ -162,6 +182,7 @@ PACKAGE_INSTALL_POLICY=prompt
162
182
  USE_DOCKER_SANDBOX=true
163
183
  DOCKER_SANDBOX_IMAGE=agintiflow-sandbox:latest
164
184
  COMMAND_CWD=/home/lachlan/ProjectsLFS/Agent
185
+ AGINTI_TASK_PROFILE=auto
165
186
  ```
166
187
 
167
188
  Defaults:
@@ -180,6 +201,15 @@ Provider credentials:
180
201
  | OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |
181
202
  | DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |
182
203
 
204
+ Project-local credentials can be stored without committing secrets:
205
+
206
+ ```bash
207
+ aginti init
208
+ printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
209
+ ```
210
+
211
+ This writes `.aginti/.env` with `0600` permissions and adds safe `.gitignore` entries. APIs and logs expose only key presence, never raw values.
212
+
183
213
  ## Agent Wrappers
184
214
 
185
215
  AgInTiFlow can expose external coding agents as advisory tools when `ALLOW_WRAPPER_TOOLS=true` or the web UI toggle is enabled. Wrappers are not a replacement for the core runner; they are used for second opinions, codebase analysis, or planning when they are installed and authenticated. The preferred wrapper defaults to Codex and can be changed with `PREFERRED_WRAPPER=codex`, `aginti --allow-wrappers --wrapper codex`, or the web UI dropdown.
@@ -0,0 +1,41 @@
1
+ # Project-Agent UX Lessons
2
+
3
+ Round 8 distilled implementation lessons from local agent projects and applied the highest-value subset to AgInTiFlow.
4
+
5
+ ## Codex
6
+
7
+ - Treat the launch directory as the working project unless the user overrides it.
8
+ - Keep patch/file tools deterministic and auditable: edits should be workspace-local, compactly diffed, and easy to inspect.
9
+ - Resume should be a first-class CLI flow, not only a hidden runtime detail.
10
+
11
+ ## Claude Code
12
+
13
+ - Terminal UX should be simple: install, `cd` into a project, run the agent.
14
+ - Project context matters more than global state for everyday coding work.
15
+ - Natural-language prompts should map to file edits, shell checks, and git-aware workflows without extra ceremony.
16
+
17
+ ## Gemini CLI
18
+
19
+ - Session history should be project-scoped so runs from different folders do not mix.
20
+ - Checkpoints/resume flows need discoverable commands and clear current-project boundaries.
21
+ - Extensibility works best as named profiles/skills rather than hardcoded one-off prompts.
22
+
23
+ ## Copilot SDK
24
+
25
+ - BYOK status should report only presence/absence and env-var names, never raw key material.
26
+ - Session APIs should expose metadata, logs, workspace state, and model/provider choices for web clients.
27
+ - Tool and permission hooks should be visible enough that users can understand why actions were allowed or blocked.
28
+
29
+ ## Claw Code
30
+
31
+ - First-run `init` and `doctor` commands reduce setup ambiguity.
32
+ - Container/sandbox readiness should be diagnosed before complex tasks.
33
+ - Parity and smoke checks should run against isolated project folders, not only the source repo.
34
+
35
+ ## Applied In AgInTiFlow
36
+
37
+ - `aginti web` now defaults command execution to the folder it was launched from.
38
+ - CLI and web share `.sessions/` in the project root.
39
+ - `aginti init`, `aginti doctor`, `aginti keys`, `aginti sessions`, and `aginti resume` provide basic project lifecycle control.
40
+ - Task profiles wire lightweight skill prompts into CLI/web runs while keeping the LLM responsible for the main plan.
41
+ - Project-local `.aginti/.env` can store provider keys safely with 0600 permissions and ignored git entries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -19,6 +19,18 @@ const translations = {
19
19
  languageLabel: "Language",
20
20
  intro:
21
21
  "Web-first agent platform with smart model routing, resumable runs, guarded tools, and optional external agent wrappers.",
22
+ projectStatusTitle: "Project folder",
23
+ setupTitle: "Provider setup",
24
+ setupHelp:
25
+ "DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.",
26
+ setupEnvHelp:
27
+ "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.",
28
+ setupProviderLabel: "Provider",
29
+ setupKeyLabel: "API key",
30
+ saveKeyButton: "Save local key",
31
+ keySavedStatus: "Local key saved. Raw values are never returned by the API.",
32
+ keySaveFailed: "Failed to save local key.",
33
+ taskProfileLabel: "Task profile",
22
34
  routingModeLabel: "Routing policy",
23
35
  routingSmartOption: "Smart: flash/pro/wrappers",
24
36
  routingFastOption: "DeepSeek v4 flash",
@@ -122,6 +134,7 @@ const translations = {
122
134
  artifactTunnelTitle: "Canvas & artifacts",
123
135
  artifactTunnelHelp: "Agent-selected canvas items, generated files, screenshots, diffs, and final answers.",
124
136
  artifactCanvasTab: "Canvas",
137
+ artifactPdfTab: "PDF Reader",
125
138
  artifactExplorerTab: "Explorer",
126
139
  artifactNotificationsTab: "Notifications",
127
140
  artifactListTitle: "Artifacts",
@@ -132,6 +145,10 @@ const translations = {
132
145
  artifactLoading: "Loading artifact...",
133
146
  artifactLoadFailed: "Failed to load artifact.",
134
147
  artifactSeenUpdated: "Notifications marked as seen.",
148
+ artifactDownloadLabel: "Download artifact",
149
+ artifactDownloadPreparing: "Preparing download...",
150
+ artifactDownloadReady: "Download started.",
151
+ artifactDownloadFailed: "Download failed.",
135
152
  },
136
153
  ar: {
137
154
  documentTitle: "AgInTiFlow",
@@ -612,6 +629,13 @@ const providerField = document.querySelector("#provider");
612
629
  const modelField = document.querySelector("#model");
613
630
  const routingHintEl = document.querySelector("#routing-hint");
614
631
  const modelRouteStatusEl = document.querySelector("#model-route-status");
632
+ const projectStatusEl = document.querySelector("#project-status");
633
+ const setupCardEl = document.querySelector("#setup-card");
634
+ const setupProviderField = document.querySelector("#setup-provider");
635
+ const setupApiKeyField = document.querySelector("#setup-api-key");
636
+ const saveApiKeyButton = document.querySelector("#save-api-key");
637
+ const setupStatusEl = document.querySelector("#setup-status");
638
+ const taskProfileField = document.querySelector("#taskProfile");
615
639
  const sandboxModeField = document.querySelector("#sandboxMode");
616
640
  const packageInstallPolicyField = document.querySelector("#packageInstallPolicy");
617
641
  const packageWarningEl = document.querySelector("#package-warning");
@@ -668,6 +692,8 @@ const defaults = {
668
692
 
669
693
  let currentLanguage = "en";
670
694
  let routingPresets = {};
695
+ let taskProfiles = [];
696
+ let projectInfo = null;
671
697
  let currentSessionId = "";
672
698
  let pollTimer = null;
673
699
  let saveTimer = null;
@@ -708,6 +734,28 @@ function renderKeyStatus(status = lastKeyStatus) {
708
734
  } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
709
735
  status.mock ? t("availableLabel") : t("missingLabel")
710
736
  }`;
737
+ if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
738
+ }
739
+
740
+ function renderProjectStatus(info = projectInfo) {
741
+ projectInfo = info;
742
+ if (!projectStatusEl || !info) return;
743
+ projectStatusEl.textContent = [
744
+ `root=${info.root || ""}`,
745
+ `cwd=${info.commandCwd || ""}`,
746
+ `sessions=${info.sessionsDir || ""}`,
747
+ `db=${info.sessionDbPath || ""}`,
748
+ `shared=${info.sharedSessionFolder ? "yes" : "no"}`,
749
+ ].join(" · ");
750
+ }
751
+
752
+ function renderTaskProfiles(selected = "auto") {
753
+ if (!taskProfileField) return;
754
+ const profiles = taskProfiles.length ? taskProfiles : [{ id: "auto", label: "Auto" }];
755
+ taskProfileField.innerHTML = profiles
756
+ .map((profile) => `<option value="${escapeHtml(profile.id)}">${escapeHtml(profile.label || profile.id)}</option>`)
757
+ .join("");
758
+ taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
711
759
  }
712
760
 
713
761
  function renderWrapperStatus(wrappers = lastWrappers) {
@@ -888,6 +936,7 @@ function applyLanguage(language, { persist = true } = {}) {
888
936
  }
889
937
 
890
938
  renderKeyStatus();
939
+ renderProjectStatus();
891
940
  renderWrapperStatus();
892
941
  renderWorkspacePanel();
893
942
  renderSandboxStatus();
@@ -917,6 +966,7 @@ function formPayload() {
917
966
  allowFileTools: document.querySelector("#allowFileTools").checked,
918
967
  allowWrapperTools: allowWrapperToolsField.checked,
919
968
  preferredWrapper: preferredWrapperField.value,
969
+ taskProfile: taskProfileField?.value || "auto",
920
970
  useDockerSandbox: sandboxModeField.value !== "host",
921
971
  dockerSandboxImage: document.querySelector("#dockerSandboxImage").value.trim(),
922
972
  allowPasswords: document.querySelector("#allowPasswords").checked,
@@ -1030,9 +1080,28 @@ function artifactLabel(item) {
1030
1080
  return [item.kind, source, when].filter(Boolean).join(" · ");
1031
1081
  }
1032
1082
 
1083
+ function isPdfArtifact(item) {
1084
+ return item?.kind === "pdf" || item?.mime === "application/pdf" || /\.pdf$/i.test(item?.path || "");
1085
+ }
1086
+
1087
+ function artifactVisibleInCurrentTab(item) {
1088
+ if (currentArtifactTab === "pdf") return isPdfArtifact(item);
1089
+ return item.tab === currentArtifactTab;
1090
+ }
1091
+
1092
+ function preferredArtifactForCurrentTab(fallbackId = "") {
1093
+ const current = artifactItems.find((item) => item.id === selectedArtifactId && artifactVisibleInCurrentTab(item));
1094
+ if (current) return current;
1095
+
1096
+ const fallback = artifactItems.find((item) => item.id === fallbackId && artifactVisibleInCurrentTab(item));
1097
+ if (fallback) return fallback;
1098
+
1099
+ return artifactItems.find(artifactVisibleInCurrentTab) || null;
1100
+ }
1101
+
1033
1102
  function renderArtifactList() {
1034
1103
  if (!artifactListEl) return;
1035
- const visible = artifactItems.filter((item) => item.tab === currentArtifactTab);
1104
+ const visible = artifactItems.filter(artifactVisibleInCurrentTab);
1036
1105
  const readIds = getArtifactReadIds();
1037
1106
 
1038
1107
  document.querySelectorAll(".artifact-tab").forEach((tab) => {
@@ -1054,14 +1123,23 @@ function renderArtifactList() {
1054
1123
  (item) => {
1055
1124
  const read = readIds.has(item.id);
1056
1125
  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>
1126
+ <div class="artifact-row" data-selected="${item.id === selectedArtifactId}" data-read="${read}">
1127
+ <button class="artifact-row-main" type="button" data-artifact-id="${escapeHtml(item.id)}">
1128
+ <span class="artifact-row-top">
1129
+ <strong>${escapeHtml(item.title || item.path || item.kind)}</strong>
1130
+ <span>${read ? escapeHtml(item.kind) : `New · ${escapeHtml(item.kind)}`}</span>
1131
+ </span>
1132
+ <span class="artifact-row-meta">${escapeHtml(artifactLabel(item))}</span>
1133
+ <span class="artifact-row-preview">${escapeHtml(item.preview || item.path || "")}</span>
1134
+ </button>
1135
+ <button
1136
+ class="artifact-menu-button"
1137
+ type="button"
1138
+ data-artifact-download-id="${escapeHtml(item.id)}"
1139
+ aria-label="${escapeHtml(t("artifactDownloadLabel"))}"
1140
+ title="${escapeHtml(t("artifactDownloadLabel"))}"
1141
+ >...</button>
1142
+ </div>
1065
1143
  `;
1066
1144
  }
1067
1145
  )
@@ -1078,7 +1156,8 @@ function resetArtifactViewer() {
1078
1156
  function renderArtifactShell() {
1079
1157
  renderArtifactBadge();
1080
1158
  renderArtifactList();
1081
- if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1159
+ const selected = artifactItems.find((item) => item.id === selectedArtifactId);
1160
+ if (!selected || !artifactVisibleInCurrentTab(selected)) {
1082
1161
  resetArtifactViewer();
1083
1162
  }
1084
1163
  }
@@ -1111,6 +1190,79 @@ function renderArtifactContent(content) {
1111
1190
  `;
1112
1191
  }
1113
1192
 
1193
+ function artifactDownloadName(item, content) {
1194
+ const sourceName = content.path || item?.path || content.title || item?.title || `artifact-${content.id || "download"}`;
1195
+ const fallbackExt =
1196
+ content.mime === "application/pdf" || content.kind === "pdf"
1197
+ ? ".pdf"
1198
+ : content.mime?.startsWith("image/png")
1199
+ ? ".png"
1200
+ : content.mime?.startsWith("image/svg")
1201
+ ? ".svg"
1202
+ : content.kind === "json"
1203
+ ? ".json"
1204
+ : content.kind === "diff"
1205
+ ? ".diff"
1206
+ : content.kind === "markdown"
1207
+ ? ".md"
1208
+ : ".txt";
1209
+ const base = sourceName
1210
+ .split("/")
1211
+ .filter(Boolean)
1212
+ .pop()
1213
+ ?.replace(/[^A-Za-z0-9._-]+/g, "-")
1214
+ .replace(/^-+|-+$/g, "");
1215
+ const name = base || `artifact-${content.id || Date.now()}`;
1216
+ return /\.[A-Za-z0-9]{1,8}$/.test(name) ? name : `${name}${fallbackExt}`;
1217
+ }
1218
+
1219
+ function blobFromDataUrl(dataUrl) {
1220
+ const [meta = "", payload = ""] = String(dataUrl || "").split(",", 2);
1221
+ const mime = meta.match(/^data:([^;,]+)/)?.[1] || "application/octet-stream";
1222
+ if (meta.includes(";base64")) {
1223
+ const binary = atob(payload);
1224
+ const bytes = new Uint8Array(binary.length);
1225
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1226
+ return new Blob([bytes], { type: mime });
1227
+ }
1228
+ return new Blob([decodeURIComponent(payload)], { type: mime });
1229
+ }
1230
+
1231
+ async function downloadArtifact(artifactId) {
1232
+ if (!currentSessionId || !artifactId) return;
1233
+ const item = artifactItems.find((candidate) => candidate.id === artifactId);
1234
+ artifactStatusEl.textContent = t("artifactDownloadPreparing");
1235
+
1236
+ try {
1237
+ const response = await fetch(
1238
+ `/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}`
1239
+ );
1240
+ const data = await response.json().catch(() => ({}));
1241
+ if (!response.ok) throw new Error(data.error || t("artifactDownloadFailed"));
1242
+
1243
+ const blob = data.dataUrl
1244
+ ? blobFromDataUrl(data.dataUrl)
1245
+ : new Blob([typeof data.text === "string" ? data.text : JSON.stringify(data, null, 2)], {
1246
+ type: data.mime || "text/plain;charset=utf-8",
1247
+ });
1248
+ const url = URL.createObjectURL(blob);
1249
+ const link = document.createElement("a");
1250
+ link.href = url;
1251
+ link.download = artifactDownloadName(item, data);
1252
+ document.body.append(link);
1253
+ link.click();
1254
+ link.remove();
1255
+ URL.revokeObjectURL(url);
1256
+
1257
+ markArtifactRead(artifactId);
1258
+ renderArtifactBadge();
1259
+ renderArtifactList();
1260
+ artifactStatusEl.textContent = t("artifactDownloadReady");
1261
+ } catch (error) {
1262
+ artifactStatusEl.textContent = error instanceof Error ? error.message : t("artifactDownloadFailed");
1263
+ }
1264
+ }
1265
+
1114
1266
  async function refreshArtifacts({ loadSelected = false } = {}) {
1115
1267
  if (!currentSessionId) {
1116
1268
  artifactItems = [];
@@ -1125,9 +1277,27 @@ async function refreshArtifacts({ loadSelected = false } = {}) {
1125
1277
  const data = await response.json();
1126
1278
  artifactItems = data.items || [];
1127
1279
  artifactUnreadCount = computeUnreadArtifactCount();
1280
+
1281
+ if (loadSelected) {
1282
+ const backendSelected = artifactItems.find((item) => item.id === data.selectedItemId);
1283
+ const pdfCandidate = artifactItems.find((item) => item.id === selectedArtifactId && isPdfArtifact(item))
1284
+ || (backendSelected && isPdfArtifact(backendSelected) ? backendSelected : null)
1285
+ || artifactItems.find(isPdfArtifact);
1286
+ if (pdfCandidate) {
1287
+ currentArtifactTab = "pdf";
1288
+ selectedArtifactId = pdfCandidate.id;
1289
+ }
1290
+ }
1291
+
1128
1292
  if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1129
1293
  selectedArtifactId = data.selectedItemId || artifactItems[0]?.id || "";
1130
1294
  }
1295
+
1296
+ const visiblePreferred = preferredArtifactForCurrentTab(data.selectedItemId);
1297
+ if (visiblePreferred) {
1298
+ selectedArtifactId = visiblePreferred.id;
1299
+ }
1300
+
1131
1301
  renderArtifactShell();
1132
1302
  if (loadSelected && selectedArtifactId) await selectArtifact(selectedArtifactId, { persist: false });
1133
1303
  }
@@ -1466,10 +1636,26 @@ artifactTabsEl.addEventListener("click", (event) => {
1466
1636
  const tab = event.target.closest("[data-artifact-tab]");
1467
1637
  if (!tab) return;
1468
1638
  currentArtifactTab = tab.dataset.artifactTab || "canvas";
1639
+ const preferred = preferredArtifactForCurrentTab();
1640
+ if (preferred) selectedArtifactId = preferred.id;
1469
1641
  renderArtifactShell();
1642
+ if (selectedArtifactId) {
1643
+ selectArtifact(selectedArtifactId, { persist: false }).catch((error) => {
1644
+ artifactStatusEl.textContent = String(error);
1645
+ });
1646
+ }
1470
1647
  });
1471
1648
 
1472
1649
  artifactListEl.addEventListener("click", (event) => {
1650
+ const downloadButton = event.target.closest("[data-artifact-download-id]");
1651
+ if (downloadButton) {
1652
+ event.stopPropagation();
1653
+ downloadArtifact(downloadButton.dataset.artifactDownloadId || "").catch((error) => {
1654
+ artifactStatusEl.textContent = String(error);
1655
+ });
1656
+ return;
1657
+ }
1658
+
1473
1659
  const row = event.target.closest("[data-artifact-id]");
1474
1660
  if (!row) return;
1475
1661
  selectArtifact(row.dataset.artifactId || "").catch((error) => {
@@ -1532,6 +1718,30 @@ sandboxModeField.addEventListener("change", updatePackageWarning);
1532
1718
  packageInstallPolicyField.addEventListener("change", updatePackageWarning);
1533
1719
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
1534
1720
  preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
1721
+ taskProfileField?.addEventListener("change", schedulePreferenceSave);
1722
+
1723
+ saveApiKeyButton?.addEventListener("click", async () => {
1724
+ const provider = setupProviderField.value || "deepseek";
1725
+ const apiKey = setupApiKeyField.value.trim();
1726
+ if (!apiKey) {
1727
+ setupStatusEl.textContent = t("setupKeyLabel");
1728
+ return;
1729
+ }
1730
+ setupStatusEl.textContent = t("sendingStatus");
1731
+ const response = await fetch(`/api/keys/${encodeURIComponent(provider)}`, {
1732
+ method: "POST",
1733
+ headers: { "Content-Type": "application/json" },
1734
+ body: JSON.stringify({ apiKey }),
1735
+ });
1736
+ const data = await response.json().catch(() => ({}));
1737
+ setupApiKeyField.value = "";
1738
+ if (!response.ok) {
1739
+ setupStatusEl.textContent = data.error || t("keySaveFailed");
1740
+ return;
1741
+ }
1742
+ setupStatusEl.textContent = t("keySavedStatus");
1743
+ renderKeyStatus(data.keyStatus);
1744
+ });
1535
1745
 
1536
1746
  form.addEventListener("input", schedulePreferenceSave);
1537
1747
  form.addEventListener("change", schedulePreferenceSave);
@@ -1644,6 +1854,8 @@ async function loadConfig() {
1644
1854
  const data = await response.json();
1645
1855
  const prefs = data.preferences || {};
1646
1856
  routingPresets = data.routing?.presets || {};
1857
+ taskProfiles = data.taskProfiles || [];
1858
+ projectInfo = data.project || null;
1647
1859
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
1648
1860
  defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
1649
1861
  defaults.mock = data.defaults?.mock?.model || defaults.mock;
@@ -1653,9 +1865,10 @@ async function loadConfig() {
1653
1865
  routingModeField.value = prefs.routingMode || "smart";
1654
1866
  providerField.value = prefs.provider || "deepseek";
1655
1867
  modelField.value = prefs.model || defaults[providerField.value] || "deepseek-v4-flash";
1868
+ renderTaskProfiles(prefs.taskProfile || "auto");
1656
1869
  document.querySelector("#startUrl").value = prefs.startUrl || "";
1657
1870
  document.querySelector("#allowedDomains").value = prefs.allowedDomains || "";
1658
- document.querySelector("#commandCwd").value = prefs.commandCwd || "/home/lachlan/ProjectsLFS/Agent";
1871
+ document.querySelector("#commandCwd").value = prefs.commandCwd || data.project?.root || "";
1659
1872
  document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
1660
1873
  document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
1661
1874
  sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
@@ -1669,6 +1882,7 @@ async function loadConfig() {
1669
1882
  document.querySelector("#allowDestructive").checked = prefs.allowDestructive ?? false;
1670
1883
 
1671
1884
  renderKeyStatus(data.keyStatus);
1885
+ renderProjectStatus(data.project);
1672
1886
  renderWrapperStatus(data.wrappers || []);
1673
1887
  renderWorkspacePanel(data.workspace, []);
1674
1888
  await refreshWorkspaceChanges();
package/public/index.html CHANGED
@@ -36,6 +36,39 @@
36
36
  <p class="subtle" data-i18n="intro">
37
37
  Browser agent with provider selection, resumable runs, persisted settings, and an optional guarded shell tool with Docker sandbox support.
38
38
  </p>
39
+ <section class="project-card">
40
+ <strong data-i18n="projectStatusTitle">Project folder</strong>
41
+ <p id="project-status" class="subtle">Loading project context...</p>
42
+ </section>
43
+
44
+ <section id="setup-card" class="setup-card" hidden>
45
+ <div>
46
+ <strong data-i18n="setupTitle">Provider setup</strong>
47
+ <p class="subtle" data-i18n="setupHelp">
48
+ DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.
49
+ </p>
50
+ <p class="subtle" data-i18n="setupEnvHelp">
51
+ Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
52
+ </p>
53
+ </div>
54
+ <div class="grid">
55
+ <label>
56
+ <span data-i18n="setupProviderLabel">Provider</span>
57
+ <select id="setup-provider">
58
+ <option value="deepseek">DeepSeek</option>
59
+ <option value="openai">OpenAI</option>
60
+ </select>
61
+ </label>
62
+ <label>
63
+ <span data-i18n="setupKeyLabel">API key</span>
64
+ <input id="setup-api-key" type="password" autocomplete="off" placeholder="Stored in .aginti/.env" />
65
+ </label>
66
+ </div>
67
+ <div class="actions">
68
+ <button id="save-api-key" type="button" class="secondary" data-i18n="saveKeyButton">Save local key</button>
69
+ <span id="setup-status" class="subtle"></span>
70
+ </div>
71
+ </section>
39
72
 
40
73
  <form id="run-form">
41
74
  <label>
@@ -67,6 +100,13 @@
67
100
  <p id="routing-hint" class="subtle route-hint"></p>
68
101
  <p id="model-route-status" class="subtle route-hint"></p>
69
102
 
103
+ <label>
104
+ <span data-i18n="taskProfileLabel">Task profile</span>
105
+ <select id="taskProfile" name="taskProfile">
106
+ <option value="auto">Auto</option>
107
+ </select>
108
+ </label>
109
+
70
110
  <label>
71
111
  <span data-i18n="goalLabel">Goal</span>
72
112
  <textarea
@@ -308,6 +348,7 @@
308
348
  </div>
309
349
  <div class="artifact-tabs" role="tablist" aria-label="Artifact tunnel tabs">
310
350
  <button id="artifact-tab-canvas" class="artifact-tab" type="button" data-artifact-tab="canvas" data-selected="true" data-i18n="artifactCanvasTab">Canvas</button>
351
+ <button id="artifact-tab-pdf" class="artifact-tab" type="button" data-artifact-tab="pdf" data-i18n="artifactPdfTab">PDF Reader</button>
311
352
  <button id="artifact-tab-explorer" class="artifact-tab" type="button" data-artifact-tab="explorer" data-i18n="artifactExplorerTab">Explorer</button>
312
353
  <button id="artifact-tab-notifications" class="artifact-tab" type="button" data-artifact-tab="notifications" data-i18n="artifactNotificationsTab">Notifications</button>
313
354
  </div>