@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 +32 -2
- package/docs/project-agent-lessons.md +41 -0
- package/package.json +1 -1
- package/public/app.js +225 -11
- package/public/index.html +41 -0
- package/public/styles.css +115 -5
- package/scripts/smoke-coding-tools.js +19 -4
- package/scripts/smoke-web-api.js +43 -0
- package/src/agent-runner.js +14 -0
- package/src/cli.js +179 -0
- package/src/config.js +6 -2
- package/src/model-client.js +28 -7
- package/src/project.js +332 -0
- package/src/task-profiles.js +85 -0
- package/src/web-db.js +6 -2
- package/web.js +47 -6
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 --
|
|
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-
|
|
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
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(
|
|
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
|
-
<
|
|
1058
|
-
<
|
|
1059
|
-
<
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
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 || "
|
|
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>
|