@lazyingart/agintiflow 0.20.186 → 0.20.188

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
@@ -85,11 +85,12 @@ aginti init --template aaps
85
85
  aginti init --template supervision
86
86
  ```
87
87
 
88
- On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, Qwen, or Venice, paste the key, and it saves to the ignored project-local `.aginti/.env` file with restricted permissions. You can rerun setup any time:
88
+ On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, paste the key, and it saves to the ignored project-local `.aginti/.env` file with restricted permissions. You can rerun setup any time:
89
89
 
90
90
  ```bash
91
91
  aginti auth
92
92
  aginti auth deepseek
93
+ aginti auth openrouter
93
94
  aginti auth venice
94
95
  aginti login grsai
95
96
  ```
@@ -99,6 +100,7 @@ Provider signup and key pages:
99
100
  | Provider | Register / key page | API base URL used by AgInTiFlow |
100
101
  | --- | --- | --- |
101
102
  | DeepSeek | [https://platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) | `https://api.deepseek.com` |
103
+ | OpenRouter | [https://openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) | `https://openrouter.ai/api/v1` |
102
104
  | Venice | [https://venice.ai/settings/api](https://venice.ai/settings/api) | `https://api.venice.ai/api/v1` |
103
105
  | OpenAI | [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) | `https://api.openai.com/v1` |
104
106
  | Qwen / DashScope | [https://bailian.console.aliyun.com/](https://bailian.console.aliyun.com/) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` |
@@ -273,6 +275,7 @@ Useful selectors:
273
275
  ```
274
276
 
275
277
  Venice routes can be used for optional uncensored or less restricted creative work. DeepSeek remains the economic default for normal engineering workflows. See [docs/model-selection.md](docs/model-selection.md) and [references/venice-model-reference.md](references/venice-model-reference.md).
278
+ OpenRouter is available as a first-class OpenAI-compatible provider with one key and company-grouped model buckets such as OpenRouter OpenAI, Anthropic, Google, DeepSeek, Qwen, Meta, Mistral, Moonshot, and xAI.
276
279
 
277
280
  ## AAPS And Large Workflows
278
281
 
@@ -348,6 +351,9 @@ Common environment variables:
348
351
  ```bash
349
352
  DEEPSEEK_API_KEY=...
350
353
  OPENAI_API_KEY=...
354
+ OPENROUTER_API_KEY=...
355
+ OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
356
+ OPENROUTER_MODEL=openrouter/auto
351
357
  QWEN_API_KEY=...
352
358
  VENICE_API_KEY=...
353
359
  GRSAI_API_KEY=...
@@ -365,6 +371,7 @@ Project-local keys:
365
371
  ```bash
366
372
  aginti init
367
373
  printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
374
+ printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
368
375
  printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
369
376
  ```
370
377
 
@@ -25,6 +25,8 @@ aginti --list-routes
25
25
 
26
26
  # one-shot overrides
27
27
  aginti --route-model deepseek-v4-flash --main-model deepseek-v4-pro "fix this project"
28
+ aginti --provider openrouter --model openrouter/auto --routing manual "try a gateway route"
29
+ aginti --provider openrouter --model anthropic/claude-sonnet-4.6 --routing manual "draft a design review"
28
30
  aginti --provider venice --model venice-uncensored-1-2 --routing manual "draft a note"
29
31
  aginti --spare-provider openai --spare-model gpt-5.4 --spare-reasoning medium "review this design"
30
32
  aginti --allow-wrappers --wrapper codex --wrapper-model gpt-5.5 "patch this bug"
@@ -63,6 +65,13 @@ Provider families:
63
65
  | DeepSeek | `deepseek-v4-flash`, `deepseek-v4-pro` |
64
66
  | Venice | `venice-uncensored-1-2`, `venice-uncensored`, `gemma-4-uncensored` |
65
67
  | OpenAI | `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`, `gpt-5.2`; each has low/medium/high/xhigh reasoning |
68
+ | OpenRouter | `openrouter/auto`, `openrouter/pareto-code`, `openrouter/free` |
69
+ | OpenRouter OpenAI | `openai/gpt-5.5`, `openai/gpt-5.4`, `openai/gpt-5.4-mini`, `openai/gpt-4o` |
70
+ | OpenRouter Anthropic | Claude routes such as `anthropic/claude-sonnet-4.6`, `anthropic/claude-opus-4.7`, `anthropic/claude-opus-4.8-fast` |
71
+ | OpenRouter Google | Gemini/Gemma routes such as `google/gemini-3.5-flash`, `google/gemini-3.1-flash-lite`, `google/gemma-4-31b-it` |
72
+ | OpenRouter DeepSeek | `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-pro`, `deepseek/deepseek-r1-0528` |
73
+ | OpenRouter Qwen | Qwen routes such as `qwen/qwen3.7-max`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-plus` |
74
+ | OpenRouter Meta/Mistral/Moonshot/xAI | `meta-llama/llama-4-maverick`, `mistralai/mistral-medium-3-5`, `moonshotai/kimi-k2.6`, `x-ai/grok-4.3` |
66
75
  | Venice GPT | GPT-family Venice routes such as `openai-gpt-55`, `openai-gpt-54`, `openai-gpt-54-mini`, `openai-gpt-53-codex`, `openai-gpt-52` |
67
76
  | Venice Gemma | Gemma instruct routes such as `google-gemma-4-31b-it`, `google-gemma-4-26b-a4b-it`, `google-gemma-3-27b-it` |
68
77
  | Venice Claude | Claude Sonnet/Opus routes such as `claude-sonnet-4-6`, `claude-opus-4-7`, `claude-opus-4-6` |
@@ -96,6 +105,16 @@ It keeps smart routing enabled. If the Venice key is missing, run `/auth venice`
96
105
  | --- | --- | --- |
97
106
  | `deepseek` | DeepSeek | Default route/main because V4 Flash and V4 Pro are cheap and strong. |
98
107
  | `openai` | OpenAI | Spare/frontier checks, Codex-family work, and explicit manual routes. |
108
+ | `openrouter` | OpenRouter | OpenAI-compatible gateway with auto/free/code routers. |
109
+ | `openrouter-openai` | OpenRouter | OpenAI-family models through OpenRouter. |
110
+ | `openrouter-anthropic` | OpenRouter | Claude-family models through OpenRouter. |
111
+ | `openrouter-google` | OpenRouter | Gemini/Gemma models through OpenRouter. |
112
+ | `openrouter-deepseek` | OpenRouter | DeepSeek models through OpenRouter. |
113
+ | `openrouter-qwen` | OpenRouter | Qwen models through OpenRouter. |
114
+ | `openrouter-meta` | OpenRouter | Meta Llama models through OpenRouter. |
115
+ | `openrouter-mistral` | OpenRouter | Mistral models through OpenRouter. |
116
+ | `openrouter-moonshot` | OpenRouter | Moonshot/Kimi models through OpenRouter. |
117
+ | `openrouter-xai` | OpenRouter | xAI Grok models through OpenRouter. |
99
118
  | `qwen` | Qwen | Chinese and general-purpose OpenAI-compatible tasks. |
100
119
  | `venice` | Venice | Venice-native text shortcuts: Venice 1.2, Venice 1.1, and Gemma 4 Uncensored. |
101
120
  | `venice-gpt` | Venice | GPT-family models through Venice. |
@@ -118,8 +137,10 @@ Keys are set per project and stored only in ignored `.aginti/.env`:
118
137
 
119
138
  ```bash
120
139
  aginti auth
140
+ aginti auth openrouter
121
141
  aginti auth venice
122
142
  aginti keys status
143
+ printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
123
144
  printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
124
145
  ```
125
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.186",
3
+ "version": "0.20.188",
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
@@ -820,6 +820,7 @@ const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
820
820
 
821
821
  const defaults = {
822
822
  openai: "gpt-5.4-mini",
823
+ openrouter: "openrouter/auto",
823
824
  deepseek: "deepseek-v4-flash",
824
825
  qwen: "qwen-plus",
825
826
  venice: "venice-uncensored-1-2",
@@ -983,6 +984,7 @@ function renderKeyStatus(status = lastKeyStatus) {
983
984
  if (!status) return;
984
985
  const providers = [
985
986
  ["OpenAI", status.openai],
987
+ ["OpenRouter", status.openrouter],
986
988
  ["DeepSeek", status.deepseek],
987
989
  ["Qwen", status.qwen],
988
990
  ["Venice", status.venice],
@@ -1009,7 +1011,7 @@ function renderKeyStatus(status = lastKeyStatus) {
1009
1011
  })
1010
1012
  );
1011
1013
  }
1012
- if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek || status.qwen || status.venice);
1014
+ if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.openrouter || status.deepseek || status.qwen || status.venice);
1013
1015
  }
1014
1016
 
1015
1017
  function renderProjectStatus(info = projectInfo) {
@@ -1251,6 +1253,37 @@ function workspaceFilesByLane() {
1251
1253
  return groups;
1252
1254
  }
1253
1255
 
1256
+ function buildWorkspaceFolderTree(files = []) {
1257
+ const root = { name: "", path: "", dirs: new Map(), files: [] };
1258
+ for (const file of files) {
1259
+ const parts = String(file.path || "")
1260
+ .split("/")
1261
+ .filter(Boolean);
1262
+ if (!parts.length) continue;
1263
+ const fileName = parts.pop();
1264
+ let node = root;
1265
+ let currentPath = "";
1266
+ for (const part of parts) {
1267
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
1268
+ if (!node.dirs.has(part)) node.dirs.set(part, { name: part, path: currentPath, dirs: new Map(), files: [] });
1269
+ node = node.dirs.get(part);
1270
+ }
1271
+ node.files.push({ ...file, name: file.name || fileName });
1272
+ }
1273
+ return root;
1274
+ }
1275
+
1276
+ function workspaceFolderCount(node) {
1277
+ if (!node) return 0;
1278
+ let count = node.files?.length || 0;
1279
+ for (const child of node.dirs?.values?.() || []) count += workspaceFolderCount(child);
1280
+ return count;
1281
+ }
1282
+
1283
+ function sortWorkspaceFiles(left, right) {
1284
+ return String(left.path || "").localeCompare(String(right.path || ""));
1285
+ }
1286
+
1254
1287
  function workspaceActiveFile() {
1255
1288
  return workspaceBrowser.files.find((file) => file.path === workspaceBrowser.activePath) || null;
1256
1289
  }
@@ -1279,12 +1312,18 @@ function renderWorkspaceExplorer() {
1279
1312
  const activeLane = workspaceBrowser.activeLane || "all";
1280
1313
  const warnings = workspaceBrowser.warnings?.length ? ` · ${workspaceBrowser.warnings.length} warning(s)` : "";
1281
1314
  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;
1315
+ if (workspaceBrowser.loading) {
1316
+ workspaceBrowserStatusEl.textContent = "Loading workspace...";
1317
+ } else if (workspaceBrowser.error) {
1318
+ workspaceBrowserStatusEl.textContent = `Workspace unavailable: ${workspaceBrowser.error}`;
1319
+ } else {
1320
+ workspaceBrowserStatusEl.innerHTML = `
1321
+ <span class="workspace-status-chip">${workspaceBrowser.files.length} files</span>
1322
+ <span class="workspace-status-chip">${escapeHtml(activeLane)}</span>
1323
+ <code class="workspace-root-path">${escapeHtml(workspaceBrowser.root || workspaceRoot())}</code>
1324
+ ${warnings || truncated ? `<span class="workspace-status-warn">${escapeHtml(`${warnings}${truncated}`.replace(/^ · /, ""))}</span>` : ""}
1325
+ `;
1326
+ }
1288
1327
 
1289
1328
  workspaceLanesEl.innerHTML = WORKSPACE_LANES.map((lane) => {
1290
1329
  const count = groups[lane.id]?.length || 0;
@@ -1302,34 +1341,69 @@ function renderWorkspaceExplorer() {
1302
1341
  `;
1303
1342
  }).join("");
1304
1343
 
1305
- const visibleFiles = (groups[activeLane] || []).slice().sort((left, right) => left.path.localeCompare(right.path));
1344
+ const visibleFiles = (groups[activeLane] || []).slice().sort(sortWorkspaceFiles);
1306
1345
  if (!visibleFiles.length) {
1307
1346
  workspaceTreeEl.innerHTML = `<p class="subtle workspace-empty">No files in this category.</p>`;
1308
1347
  return;
1309
1348
  }
1310
1349
 
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("");
1350
+ const tree = buildWorkspaceFolderTree(visibleFiles);
1351
+ workspaceTreeEl.innerHTML = `
1352
+ <div class="workspace-tree-toolbar">
1353
+ <span>${escapeHtml(WORKSPACE_LANES.find((lane) => lane.id === activeLane)?.label || "Files")}</span>
1354
+ <strong>${visibleFiles.length}</strong>
1355
+ </div>
1356
+ <div class="workspace-folder-root">
1357
+ ${renderWorkspaceFolderNode(tree, 0)}
1358
+ </div>
1359
+ `;
1360
+ }
1361
+
1362
+ function renderWorkspaceFileNode(file, depth = 0) {
1363
+ const lane = inferWorkspaceLane(file);
1364
+ const active = file.path === workspaceBrowser.activePath;
1365
+ const meta = [lane, formatBytes(file.size), file.binary ? "binary" : "text"].join(" · ");
1366
+ return `
1367
+ <button
1368
+ type="button"
1369
+ class="workspace-file-row workspace-tree-file"
1370
+ data-workspace-file="${escapeHtml(file.path)}"
1371
+ data-selected="${active}"
1372
+ draggable="true"
1373
+ style="--depth: ${depth};"
1374
+ >
1375
+ <span class="workspace-file-icon" aria-hidden="true">${file.binary ? "bin" : "txt"}</span>
1376
+ <span class="workspace-file-main">
1377
+ <strong>${escapeHtml(workspaceBaseName(file.path))}</strong>
1378
+ <small>${escapeHtml(workspaceDir(file.path) || ".")}</small>
1379
+ </span>
1380
+ <span class="workspace-file-meta">${escapeHtml(meta)}</span>
1381
+ </button>
1382
+ `;
1383
+ }
1384
+
1385
+ function renderWorkspaceFolderNode(node, depth = 0) {
1386
+ const dirs = [...(node.dirs?.values?.() || [])].sort((left, right) => left.name.localeCompare(right.name));
1387
+ const files = (node.files || []).slice().sort(sortWorkspaceFiles);
1388
+ const children = [
1389
+ ...dirs.map((child) => renderWorkspaceFolderNode(child, depth + 1)),
1390
+ ...files.map((file) => renderWorkspaceFileNode(file, depth)),
1391
+ ].join("");
1392
+ if (depth === 0) return children;
1393
+ const activeInside = workspaceBrowser.activePath && workspaceBrowser.activePath.startsWith(`${node.path}/`);
1394
+ const open = depth <= 2 || activeInside;
1395
+ return `
1396
+ <details class="workspace-folder" ${open ? "open" : ""} style="--depth: ${Math.max(depth - 1, 0)};">
1397
+ <summary>
1398
+ <span class="workspace-folder-icon" aria-hidden="true">&gt;</span>
1399
+ <span class="workspace-folder-name">${escapeHtml(node.name)}</span>
1400
+ <small>${workspaceFolderCount(node)}</small>
1401
+ </summary>
1402
+ <div class="workspace-folder-children">
1403
+ ${children}
1404
+ </div>
1405
+ </details>
1406
+ `;
1333
1407
  }
1334
1408
 
1335
1409
  function renderWorkspaceWorkbench() {
@@ -1539,6 +1613,7 @@ function fieldValue(field) {
1539
1613
  const providerLabels = {
1540
1614
  deepseek: "DeepSeek",
1541
1615
  openai: "OpenAI",
1616
+ openrouter: "OpenRouter",
1542
1617
  qwen: "Qwen",
1543
1618
  venice: "Venice",
1544
1619
  mock: "Mock local",
@@ -1562,14 +1637,14 @@ function setChoiceOptions(select, options, selectedValue = "", fallbackValue = "
1562
1637
  }
1563
1638
 
1564
1639
  function configuredTextProviders({ includeMock = true } = {}) {
1565
- const base = ["deepseek", "openai", "qwen", "venice"];
1640
+ const base = ["deepseek", "openai", "openrouter", "qwen", "venice"];
1566
1641
  if (includeMock) base.push("mock");
1567
1642
  if (Object.keys(modelCatalog || {}).length === 0) return base;
1568
1643
  return base.filter((provider) => provider === "mock" || Array.isArray(modelCatalog[provider]));
1569
1644
  }
1570
1645
 
1571
1646
  function refreshProviderDropdowns() {
1572
- setChoiceOptions(setupProviderField, ["deepseek", "openai", "qwen", "venice", "grsai"], setupProviderField?.value || "deepseek", "deepseek");
1647
+ setChoiceOptions(setupProviderField, ["deepseek", "openai", "openrouter", "qwen", "venice", "grsai"], setupProviderField?.value || "deepseek", "deepseek");
1573
1648
  setChoiceOptions(providerField, configuredTextProviders({ includeMock: true }), providerField?.value || "deepseek", "deepseek");
1574
1649
  setChoiceOptions(routeProviderField, configuredTextProviders({ includeMock: true }), routeProviderField?.value || "deepseek", "deepseek");
1575
1650
  setChoiceOptions(mainProviderField, configuredTextProviders({ includeMock: false }), mainProviderField?.value || "deepseek", "deepseek");
@@ -3802,7 +3877,13 @@ deleteSessionButton.addEventListener("click", () => {
3802
3877
  });
3803
3878
 
3804
3879
  providerField.addEventListener("change", () => {
3805
- if (providerField.value === "mock" || providerField.value === "venice" || providerField.value === "openai" || providerField.value === "qwen") {
3880
+ if (
3881
+ providerField.value === "mock" ||
3882
+ providerField.value === "venice" ||
3883
+ providerField.value === "openai" ||
3884
+ providerField.value === "openrouter" ||
3885
+ providerField.value === "qwen"
3886
+ ) {
3806
3887
  routingModeField.value = "manual";
3807
3888
  }
3808
3889
 
@@ -4136,6 +4217,7 @@ async function loadConfig() {
4136
4217
  taskProfiles = data.taskProfiles || [];
4137
4218
  projectInfo = data.project || null;
4138
4219
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
4220
+ defaults.openrouter = data.defaults?.openrouter?.model || defaults.openrouter;
4139
4221
  defaults.qwen = data.defaults?.qwen?.model || defaults.qwen;
4140
4222
  defaults.venice = data.defaults?.venice?.model || defaults.venice;
4141
4223
  defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
package/public/index.html CHANGED
@@ -65,10 +65,10 @@
65
65
  <div>
66
66
  <strong data-i18n="setupTitle">Provider setup</strong>
67
67
  <p class="subtle" data-i18n="setupHelp">
68
- DeepSeek/OpenAI/Qwen/Venice keys are missing. Use mock mode, export an env var, or save a project-local key.
68
+ DeepSeek/OpenAI/OpenRouter/Qwen/Venice keys are missing. Use mock mode, export an env var, or save a project-local key.
69
69
  </p>
70
70
  <p class="subtle" data-i18n="setupEnvHelp">
71
- Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, VENICE_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
71
+ Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, QWEN_API_KEY, VENICE_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
72
72
  </p>
73
73
  <p class="subtle key-links">
74
74
  <span data-i18n="setupKeyLinksLabel">Get keys:</span>
@@ -76,6 +76,8 @@
76
76
  <span aria-hidden="true">·</span>
77
77
  <a href="https://platform.openai.com/api-keys" target="_blank" rel="noreferrer" data-i18n="openaiKeyLink">OpenAI API keys</a>
78
78
  <span aria-hidden="true">·</span>
79
+ <a href="https://openrouter.ai/settings/keys" target="_blank" rel="noreferrer">OpenRouter API keys</a>
80
+ <span aria-hidden="true">·</span>
79
81
  <a href="https://venice.ai" target="_blank" rel="noreferrer">Venice API</a>
80
82
  </p>
81
83
  </div>
@@ -85,6 +87,7 @@
85
87
  <select id="setup-provider">
86
88
  <option value="deepseek">DeepSeek</option>
87
89
  <option value="openai">OpenAI</option>
90
+ <option value="openrouter">OpenRouter</option>
88
91
  <option value="qwen">Qwen</option>
89
92
  <option value="venice">Venice</option>
90
93
  <option value="grsai">GRS AI image</option>
@@ -132,6 +135,7 @@
132
135
  <select id="provider" name="provider">
133
136
  <option value="deepseek">DeepSeek</option>
134
137
  <option value="openai">OpenAI</option>
138
+ <option value="openrouter">OpenRouter</option>
135
139
  <option value="qwen">Qwen</option>
136
140
  <option value="venice">Venice</option>
137
141
  <option value="mock" data-i18n="mockProviderOption">Mock local</option>
@@ -414,6 +418,7 @@
414
418
  <select id="routeProvider" name="routeProvider">
415
419
  <option value="deepseek">DeepSeek</option>
416
420
  <option value="openai">OpenAI</option>
421
+ <option value="openrouter">OpenRouter</option>
417
422
  <option value="qwen">Qwen</option>
418
423
  <option value="venice">Venice</option>
419
424
  <option value="mock">Mock</option>
@@ -427,6 +432,7 @@
427
432
  <select id="mainProvider" name="mainProvider">
428
433
  <option value="deepseek">DeepSeek</option>
429
434
  <option value="openai">OpenAI</option>
435
+ <option value="openrouter">OpenRouter</option>
430
436
  <option value="qwen">Qwen</option>
431
437
  <option value="venice">Venice</option>
432
438
  </select>
@@ -438,6 +444,7 @@
438
444
  <span>Spare</span>
439
445
  <select id="spareProvider" name="spareProvider">
440
446
  <option value="openai">OpenAI</option>
447
+ <option value="openrouter">OpenRouter</option>
441
448
  <option value="deepseek">DeepSeek</option>
442
449
  <option value="qwen">Qwen</option>
443
450
  <option value="venice">Venice</option>
package/public/styles.css CHANGED
@@ -959,10 +959,43 @@ button.danger {
959
959
  }
960
960
 
961
961
  .workspace-browser-status {
962
+ display: flex;
963
+ flex-wrap: wrap;
964
+ align-items: center;
965
+ gap: 7px;
962
966
  line-height: 1.35;
963
967
  overflow-wrap: anywhere;
964
968
  }
965
969
 
970
+ .workspace-status-chip {
971
+ padding: 4px 8px;
972
+ border: 1px solid rgba(15, 118, 110, 0.16);
973
+ border-radius: 999px;
974
+ background: rgba(255, 253, 250, 0.86);
975
+ color: var(--ink);
976
+ font-size: 0.76rem;
977
+ font-weight: 700;
978
+ text-transform: capitalize;
979
+ }
980
+
981
+ .workspace-root-path {
982
+ min-width: 0;
983
+ max-width: 100%;
984
+ padding: 5px 8px;
985
+ border: 1px solid rgba(15, 118, 110, 0.14);
986
+ border-radius: 10px;
987
+ background: rgba(13, 148, 136, 0.07);
988
+ color: #155e75;
989
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
990
+ font-size: 0.76rem;
991
+ overflow-wrap: anywhere;
992
+ }
993
+
994
+ .workspace-status-warn {
995
+ color: #b45309;
996
+ font-size: 0.76rem;
997
+ }
998
+
966
999
  .workspace-lanes {
967
1000
  display: grid;
968
1001
  grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
@@ -995,11 +1028,105 @@ button.danger {
995
1028
  }
996
1029
 
997
1030
  .workspace-tree {
998
- display: grid;
999
- gap: 7px;
1000
1031
  max-height: 440px;
1001
1032
  overflow: auto;
1002
- padding-right: 2px;
1033
+ padding: 8px;
1034
+ border: 1px solid rgba(15, 118, 110, 0.12);
1035
+ border-radius: 16px;
1036
+ background:
1037
+ linear-gradient(180deg, rgba(255, 253, 250, 0.88), rgba(240, 253, 250, 0.58)),
1038
+ rgba(255, 255, 255, 0.72);
1039
+ }
1040
+
1041
+ .workspace-tree-toolbar {
1042
+ position: sticky;
1043
+ top: 0;
1044
+ z-index: 1;
1045
+ display: flex;
1046
+ justify-content: space-between;
1047
+ align-items: center;
1048
+ gap: 10px;
1049
+ margin: -8px -8px 8px;
1050
+ padding: 8px 10px;
1051
+ border-bottom: 1px solid rgba(15, 118, 110, 0.1);
1052
+ background: rgba(255, 253, 250, 0.95);
1053
+ color: var(--muted);
1054
+ font-size: 0.78rem;
1055
+ font-weight: 700;
1056
+ text-transform: uppercase;
1057
+ letter-spacing: 0.03em;
1058
+ }
1059
+
1060
+ .workspace-tree-toolbar strong {
1061
+ color: var(--accent);
1062
+ }
1063
+
1064
+ .workspace-folder-root,
1065
+ .workspace-folder-children {
1066
+ display: grid;
1067
+ gap: 4px;
1068
+ }
1069
+
1070
+ .workspace-folder {
1071
+ margin-left: calc(var(--depth, 0) * 12px);
1072
+ }
1073
+
1074
+ .workspace-folder summary {
1075
+ display: flex;
1076
+ align-items: center;
1077
+ gap: 7px;
1078
+ min-height: 32px;
1079
+ padding: 6px 8px;
1080
+ border: 1px solid transparent;
1081
+ border-radius: 11px;
1082
+ color: var(--ink);
1083
+ cursor: pointer;
1084
+ list-style: none;
1085
+ user-select: none;
1086
+ }
1087
+
1088
+ .workspace-folder summary::-webkit-details-marker {
1089
+ display: none;
1090
+ }
1091
+
1092
+ .workspace-folder summary:hover {
1093
+ border-color: rgba(15, 118, 110, 0.16);
1094
+ background: rgba(240, 253, 250, 0.72);
1095
+ }
1096
+
1097
+ .workspace-folder-icon {
1098
+ display: inline-grid;
1099
+ place-items: center;
1100
+ width: 18px;
1101
+ height: 18px;
1102
+ border: 1px solid rgba(15, 118, 110, 0.18);
1103
+ border-radius: 6px;
1104
+ color: var(--accent);
1105
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1106
+ font-size: 0.68rem;
1107
+ transition: transform 0.16s ease;
1108
+ }
1109
+
1110
+ .workspace-folder[open] > summary .workspace-folder-icon {
1111
+ transform: rotate(90deg);
1112
+ }
1113
+
1114
+ .workspace-folder-name {
1115
+ min-width: 0;
1116
+ overflow: hidden;
1117
+ text-overflow: ellipsis;
1118
+ white-space: nowrap;
1119
+ font-weight: 750;
1120
+ }
1121
+
1122
+ .workspace-folder summary small {
1123
+ margin-left: auto;
1124
+ color: var(--muted);
1125
+ font-size: 0.72rem;
1126
+ }
1127
+
1128
+ .workspace-folder-children {
1129
+ margin-top: 4px;
1003
1130
  }
1004
1131
 
1005
1132
  .workspace-empty {
@@ -1011,18 +1138,23 @@ button.danger {
1011
1138
 
1012
1139
  .workspace-file-row {
1013
1140
  display: grid;
1014
- grid-template-columns: minmax(0, 1fr);
1015
- gap: 3px;
1141
+ grid-template-columns: auto minmax(0, 1fr);
1142
+ gap: 6px 8px;
1143
+ align-items: center;
1016
1144
  width: 100%;
1017
- padding: 10px 11px;
1145
+ padding: 8px 10px;
1018
1146
  border: 1px solid rgba(217, 119, 6, 0.16);
1019
- border-radius: 14px;
1147
+ border-radius: 12px;
1020
1148
  background: rgba(255, 253, 250, 0.72);
1021
1149
  color: var(--ink);
1022
1150
  cursor: pointer;
1023
1151
  text-align: left;
1024
1152
  }
1025
1153
 
1154
+ .workspace-tree-file {
1155
+ margin-left: calc(var(--depth, 0) * 12px);
1156
+ }
1157
+
1026
1158
  .workspace-file-row:hover {
1027
1159
  border-color: rgba(15, 118, 110, 0.34);
1028
1160
  background: rgba(240, 253, 250, 0.72);
@@ -1030,10 +1162,24 @@ button.danger {
1030
1162
 
1031
1163
  .workspace-file-row[data-selected="true"] {
1032
1164
  border-color: rgba(15, 118, 110, 0.58);
1033
- background: linear-gradient(135deg, rgba(15, 118, 110, 0.18), rgba(255, 255, 255, 0.92));
1165
+ background: linear-gradient(135deg, rgba(15, 118, 110, 0.22), rgba(255, 255, 255, 0.96));
1034
1166
  box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
1035
1167
  }
1036
1168
 
1169
+ .workspace-file-icon {
1170
+ display: inline-grid;
1171
+ place-items: center;
1172
+ width: 30px;
1173
+ height: 24px;
1174
+ border-radius: 8px;
1175
+ background: rgba(15, 118, 110, 0.09);
1176
+ color: #155e75;
1177
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1178
+ font-size: 0.62rem;
1179
+ font-weight: 800;
1180
+ text-transform: uppercase;
1181
+ }
1182
+
1037
1183
  .workspace-file-main {
1038
1184
  display: grid;
1039
1185
  gap: 1px;
@@ -1050,6 +1196,7 @@ button.danger {
1050
1196
 
1051
1197
  .workspace-file-main small,
1052
1198
  .workspace-file-meta {
1199
+ grid-column: 2;
1053
1200
  color: var(--muted);
1054
1201
  font-size: 0.76rem;
1055
1202
  }
@@ -10,6 +10,28 @@ This note records the working publication route for AgInTiFlow so future release
10
10
 
11
11
  Latest verified release:
12
12
 
13
+ - Version: `0.20.187`
14
+ - Commit: `9339b2e`
15
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26701125864`
16
+ - Workflow result: success
17
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.187`.
18
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.187` then `aginti --version` returned `0.20.187`.
19
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.187"` from the global npm package path.
20
+ - Workspace explorer verification: `POST /api/workspace/snapshot` returned no `.aginti/`, `.aginti-work/`, `.sessions/`, or `.env` entries for the project root.
21
+
22
+ Previous verified release:
23
+
24
+ - Version: `0.20.186`
25
+ - Commit: `f32a748`
26
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700782132`
27
+ - Workflow result: success
28
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.186`.
29
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.186` then `aginti --version` returned `0.20.186`.
30
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.186"` from the global npm package path.
31
+ - Direct image CLI verification: `aginti image --json --dry-run --format svg` returned `requestedFormat:"svg"`, `actualFormat:"png"`, and a clear raster PNG fallback notice.
32
+
33
+ Previous verified release:
34
+
13
35
  - Version: `0.20.185`
14
36
  - Commit: `58d6087`
15
37
  - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700387931`