@agentprojectcontext/apx 1.66.0 → 1.68.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/package.json +3 -2
- package/skills/apx/SKILL.md +3 -0
- package/src/core/agent/index.js +2 -0
- package/src/core/agent/judge.js +174 -0
- package/src/core/agent/model-router.js +107 -5
- package/src/core/agent/prompts/modes/code-build.md +1 -1
- package/src/core/agent/run-agent.js +149 -12
- package/src/core/agent/security.js +97 -0
- package/src/core/agent/stuck-detector.js +89 -0
- package/src/core/agent/super-agent.js +58 -17
- package/src/core/agent/tools/handlers/run-subagent.js +117 -0
- package/src/core/agent/tools/helpers.js +11 -1
- package/src/core/agent/tools/names.js +2 -0
- package/src/core/agent/tools/registry.js +10 -0
- package/src/core/artifacts/preview.js +392 -0
- package/src/core/artifacts/tunnel.js +169 -0
- package/src/core/config/index.js +61 -0
- package/src/core/config/redact.js +44 -0
- package/src/core/config/secret-values.js +132 -0
- package/src/core/engines/mock.js +15 -1
- package/src/core/logging.js +10 -3
- package/src/core/memory/compactor.js +65 -56
- package/src/core/memory/summarizer.js +125 -0
- package/src/core/stores/conversations-compactor.js +24 -31
- package/src/host/daemon/api/admin-config.js +5 -0
- package/src/host/daemon/api/artifact-preview.js +82 -0
- package/src/host/daemon/api/config.js +17 -5
- package/src/host/daemon/api/sessions.js +9 -0
- package/src/host/daemon/api/web.js +1 -1
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/index.js +16 -1
- package/src/interfaces/acp/index.js +363 -0
- package/src/interfaces/acp/jsonrpc.js +180 -0
- package/src/interfaces/acp/session.js +205 -0
- package/src/interfaces/cli/commands/acp.js +10 -0
- package/src/interfaces/cli/commands/artifact.js +115 -0
- package/src/interfaces/cli/index.js +74 -0
- package/src/interfaces/web/dist/assets/index-D4BmWoDM.css +1 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js +803 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +9 -9
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +145 -2
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
- package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
- package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +236 -0
- package/src/interfaces/web/src/i18n/en.ts +53 -0
- package/src/interfaces/web/src/i18n/es.ts +53 -0
- package/src/interfaces/web/src/lib/api/artifacts.ts +38 -0
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
- package/src/interfaces/web/src/screens/base/ModelsTab.tsx +4 -2
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
- package/src/interfaces/web/src/screens/project/Overview.tsx +3 -2
- package/src/interfaces/web/src/types/daemon.ts +16 -0
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +0 -1
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js +0 -778
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js.map +0 -1
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
|
|
19
19
|
<link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
|
|
20
20
|
<link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-vwd6yQVw.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D4BmWoDM.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -2059,9 +2059,9 @@
|
|
|
2059
2059
|
"license": "MIT"
|
|
2060
2060
|
},
|
|
2061
2061
|
"node_modules/@types/node": {
|
|
2062
|
-
"version": "26.1.
|
|
2063
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.
|
|
2064
|
-
"integrity": "sha512-
|
|
2062
|
+
"version": "26.1.1",
|
|
2063
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
|
2064
|
+
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
|
2065
2065
|
"devOptional": true,
|
|
2066
2066
|
"license": "MIT",
|
|
2067
2067
|
"dependencies": {
|
|
@@ -2580,9 +2580,9 @@
|
|
|
2580
2580
|
"license": "MIT"
|
|
2581
2581
|
},
|
|
2582
2582
|
"node_modules/electron-to-chromium": {
|
|
2583
|
-
"version": "1.5.
|
|
2584
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2585
|
-
"integrity": "sha512-
|
|
2583
|
+
"version": "1.5.389",
|
|
2584
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
|
|
2585
|
+
"integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
|
|
2586
2586
|
"dev": true,
|
|
2587
2587
|
"license": "ISC"
|
|
2588
2588
|
},
|
|
@@ -3195,9 +3195,9 @@
|
|
|
3195
3195
|
}
|
|
3196
3196
|
},
|
|
3197
3197
|
"node_modules/node-releases": {
|
|
3198
|
-
"version": "2.0.
|
|
3199
|
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.
|
|
3200
|
-
"integrity": "sha512-
|
|
3198
|
+
"version": "2.0.51",
|
|
3199
|
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
|
3200
|
+
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
|
|
3201
3201
|
"dev": true,
|
|
3202
3202
|
"license": "MIT",
|
|
3203
3203
|
"engines": {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import useSWR from "swr";
|
|
3
|
-
import { Copy, RefreshCw, Trash2, FileCode2, Play, Pencil, Eye, SquarePen } from "lucide-react";
|
|
3
|
+
import { Copy, RefreshCw, Trash2, FileCode2, Play, Pencil, Eye, SquarePen, ExternalLink, Share2, Square } from "lucide-react";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
5
|
import { t } from "../../i18n";
|
|
6
6
|
import { Empty, Spinner } from "../ui";
|
|
7
|
-
import { Artifacts, type ArtifactEntry, type ArtifactRunResult } from "../../lib/api/artifacts";
|
|
7
|
+
import { Artifacts, type ArtifactEntry, type ArtifactRunResult, type ArtifactPreview } from "../../lib/api/artifacts";
|
|
8
|
+
|
|
9
|
+
// Artifacts we can render in a browser via an ephemeral preview server.
|
|
10
|
+
const PREVIEWABLE = /\.(html?|jsx|tsx|js)$/i;
|
|
8
11
|
import { useToast } from "../Toast";
|
|
9
12
|
import {
|
|
10
13
|
Dialog,
|
|
@@ -41,6 +44,12 @@ function ArtifactRow({
|
|
|
41
44
|
const [runResult, setRunResult] = useState<ArtifactRunResult | null>(null);
|
|
42
45
|
const toast = useToast();
|
|
43
46
|
|
|
47
|
+
// Preview / share state
|
|
48
|
+
const [preview, setPreview] = useState<ArtifactPreview | null>(null);
|
|
49
|
+
const [previewing, setPreviewing] = useState(false);
|
|
50
|
+
const [sharing, setSharing] = useState(false);
|
|
51
|
+
const previewable = PREVIEWABLE.test(entry.name);
|
|
52
|
+
|
|
44
53
|
// Rename state
|
|
45
54
|
const [renaming, setRenaming] = useState(false);
|
|
46
55
|
const [renameValue, setRenameValue] = useState(entry.name);
|
|
@@ -84,6 +93,45 @@ function ArtifactRow({
|
|
|
84
93
|
}
|
|
85
94
|
};
|
|
86
95
|
|
|
96
|
+
// Start (or reuse) an ephemeral preview server and open it in a new tab.
|
|
97
|
+
const startPreview = async () => {
|
|
98
|
+
setPreviewing(true);
|
|
99
|
+
try {
|
|
100
|
+
const p = await Artifacts.preview(pid, entry.name);
|
|
101
|
+
setPreview(p);
|
|
102
|
+
window.open(p.url, "_blank", "noopener,noreferrer");
|
|
103
|
+
} catch (e) {
|
|
104
|
+
toast.error((e as Error).message);
|
|
105
|
+
} finally {
|
|
106
|
+
setPreviewing(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Expose the running preview through a public tunnel and open it.
|
|
111
|
+
const share = async () => {
|
|
112
|
+
if (!preview) return;
|
|
113
|
+
setSharing(true);
|
|
114
|
+
try {
|
|
115
|
+
const tn = await Artifacts.openTunnel(preview.id);
|
|
116
|
+
setPreview({ ...preview, tunnel: { id: tn.id, url: tn.url, provider: tn.provider } });
|
|
117
|
+
window.open(tn.url, "_blank", "noopener,noreferrer");
|
|
118
|
+
} catch (e) {
|
|
119
|
+
toast.error((e as Error).message);
|
|
120
|
+
} finally {
|
|
121
|
+
setSharing(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const stopPreview = async () => {
|
|
126
|
+
if (!preview) return;
|
|
127
|
+
try {
|
|
128
|
+
await Artifacts.stopPreview(preview.id);
|
|
129
|
+
} catch {
|
|
130
|
+
/* ignore — already gone */
|
|
131
|
+
}
|
|
132
|
+
setPreview(null);
|
|
133
|
+
};
|
|
134
|
+
|
|
87
135
|
const remove = async () => {
|
|
88
136
|
setDeleting(true);
|
|
89
137
|
try {
|
|
@@ -232,6 +280,37 @@ function ArtifactRow({
|
|
|
232
280
|
</Tip>
|
|
233
281
|
)}
|
|
234
282
|
|
|
283
|
+
{/* Preview — serve on an ephemeral local server and open in a tab.
|
|
284
|
+
Only for browser-renderable artifacts (HTML / React / JS). */}
|
|
285
|
+
{previewable && (
|
|
286
|
+
<Tip content={t("code_module.artifacts_preview_hint")}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
disabled={previewing}
|
|
290
|
+
onClick={() => void startPreview()}
|
|
291
|
+
className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-sky-500/15 text-sky-700 hover:bg-sky-500/25 disabled:opacity-60 dark:text-sky-300"
|
|
292
|
+
>
|
|
293
|
+
{previewing ? <Spinner size={10} /> : <ExternalLink className="size-3" />}
|
|
294
|
+
{t("code_module.artifacts_preview")}
|
|
295
|
+
</button>
|
|
296
|
+
</Tip>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{/* Share — open a public tunnel to the running preview. */}
|
|
300
|
+
{preview && !preview.tunnel && (
|
|
301
|
+
<Tip content={t("code_module.artifacts_share_hint")}>
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
disabled={sharing}
|
|
305
|
+
onClick={() => void share()}
|
|
306
|
+
className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-amber-500/15 text-amber-700 hover:bg-amber-500/25 disabled:opacity-60 dark:text-amber-300"
|
|
307
|
+
>
|
|
308
|
+
{sharing ? <Spinner size={10} /> : <Share2 className="size-3" />}
|
|
309
|
+
{t("code_module.artifacts_share")}
|
|
310
|
+
</button>
|
|
311
|
+
</Tip>
|
|
312
|
+
)}
|
|
313
|
+
|
|
235
314
|
{/* Eliminar — confirmation dialog */}
|
|
236
315
|
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
237
316
|
<Tip content={t("code_module.artifacts_delete")}>
|
|
@@ -289,6 +368,70 @@ function ArtifactRow({
|
|
|
289
368
|
</code>
|
|
290
369
|
</div>
|
|
291
370
|
|
|
371
|
+
{/* Running preview: local + optional public URL, with a stop button. */}
|
|
372
|
+
{preview && (
|
|
373
|
+
<div className="space-y-1 rounded border border-sky-500/30 bg-sky-500/5 p-2">
|
|
374
|
+
<div className="flex items-center gap-2">
|
|
375
|
+
<span className="shrink-0 text-[10px] font-medium text-muted-foreground">
|
|
376
|
+
{t("code_module.artifacts_preview_local")}
|
|
377
|
+
</span>
|
|
378
|
+
<a
|
|
379
|
+
href={preview.url}
|
|
380
|
+
target="_blank"
|
|
381
|
+
rel="noopener noreferrer"
|
|
382
|
+
className="min-w-0 flex-1 truncate font-mono text-[10px] text-sky-700 underline hover:text-sky-900 dark:text-sky-300"
|
|
383
|
+
>
|
|
384
|
+
{preview.url}
|
|
385
|
+
</a>
|
|
386
|
+
<Tip content={t("code_module.artifacts_copy_url")}>
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
onClick={() => void copy(preview.url)}
|
|
390
|
+
className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
391
|
+
>
|
|
392
|
+
<Copy className="size-3" />
|
|
393
|
+
</button>
|
|
394
|
+
</Tip>
|
|
395
|
+
<Tip content={t("code_module.artifacts_stop_preview")}>
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => void stopPreview()}
|
|
399
|
+
className="shrink-0 rounded p-0.5 text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950"
|
|
400
|
+
>
|
|
401
|
+
<Square className="size-3" />
|
|
402
|
+
</button>
|
|
403
|
+
</Tip>
|
|
404
|
+
</div>
|
|
405
|
+
{preview.tunnel && (
|
|
406
|
+
<div className="flex items-center gap-2">
|
|
407
|
+
<span className="shrink-0 text-[10px] font-medium text-amber-700 dark:text-amber-300">
|
|
408
|
+
{t("code_module.artifacts_preview_public")}
|
|
409
|
+
</span>
|
|
410
|
+
<a
|
|
411
|
+
href={preview.tunnel.url}
|
|
412
|
+
target="_blank"
|
|
413
|
+
rel="noopener noreferrer"
|
|
414
|
+
className="min-w-0 flex-1 truncate font-mono text-[10px] text-amber-700 underline hover:text-amber-900 dark:text-amber-300"
|
|
415
|
+
>
|
|
416
|
+
{preview.tunnel.url}
|
|
417
|
+
</a>
|
|
418
|
+
<span className="shrink-0 font-mono text-[9px] text-muted-foreground">
|
|
419
|
+
{preview.tunnel.provider}
|
|
420
|
+
</span>
|
|
421
|
+
<Tip content={t("code_module.artifacts_copy_url")}>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={() => void copy(preview.tunnel!.url)}
|
|
425
|
+
className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
426
|
+
>
|
|
427
|
+
<Copy className="size-3" />
|
|
428
|
+
</button>
|
|
429
|
+
</Tip>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
292
435
|
{/* Run result display */}
|
|
293
436
|
{runResult && (
|
|
294
437
|
<div className="space-y-1">
|
|
@@ -32,6 +32,7 @@ export function ConfigTabsEditor({
|
|
|
32
32
|
onSaveFields,
|
|
33
33
|
onSaveJson,
|
|
34
34
|
busy,
|
|
35
|
+
hideJson = false,
|
|
35
36
|
}: {
|
|
36
37
|
sections: ConfigSection[];
|
|
37
38
|
source: Record<string, unknown>;
|
|
@@ -42,6 +43,8 @@ export function ConfigTabsEditor({
|
|
|
42
43
|
onSaveFields: (set: Record<string, unknown>, unset: string[]) => Promise<void>;
|
|
43
44
|
onSaveJson: (next: Record<string, unknown>) => Promise<void>;
|
|
44
45
|
busy?: boolean;
|
|
46
|
+
/** Hide the raw-JSON tab (used when JSON editing lives in its own top tab). */
|
|
47
|
+
hideJson?: boolean;
|
|
45
48
|
}) {
|
|
46
49
|
const firstKey = sections[0]?.key || "json";
|
|
47
50
|
const [draft, setDraft] = useState<Record<string, unknown>>({});
|
|
@@ -92,51 +95,63 @@ export function ConfigTabsEditor({
|
|
|
92
95
|
}
|
|
93
96
|
};
|
|
94
97
|
|
|
98
|
+
const sectionBody = (section: ConfigSection) => (
|
|
99
|
+
<div className="space-y-4">
|
|
100
|
+
{section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
|
|
101
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
102
|
+
{section.fields.map((field) => (
|
|
103
|
+
<ConfigFieldControl
|
|
104
|
+
key={field.path}
|
|
105
|
+
field={field}
|
|
106
|
+
value={draft[field.path]}
|
|
107
|
+
inherited={getDotted(placeholderSource, field.path)}
|
|
108
|
+
onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
<Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Single section with no JSON tab → render the fields flat (no inner tab bar),
|
|
117
|
+
// so the parent's top-level tab is the only tab the user sees.
|
|
118
|
+
if (hideJson && sections.length <= 1) {
|
|
119
|
+
return sections[0] ? sectionBody(sections[0]) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
return (
|
|
96
123
|
<Tabs defaultValue={firstKey} className="space-y-4">
|
|
97
124
|
<TabsList className="flex flex-wrap">
|
|
98
125
|
{sections.map((section) => (
|
|
99
126
|
<TabsTrigger key={section.key} value={section.key}>{section.label}</TabsTrigger>
|
|
100
127
|
))}
|
|
101
|
-
<TabsTrigger value="json">JSON</TabsTrigger>
|
|
128
|
+
{!hideJson && <TabsTrigger value="json">JSON</TabsTrigger>}
|
|
102
129
|
</TabsList>
|
|
103
130
|
|
|
104
131
|
{sections.map((section) => (
|
|
105
132
|
<TabsContent key={section.key} value={section.key}>
|
|
106
|
-
|
|
107
|
-
{section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
|
|
108
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
109
|
-
{section.fields.map((field) => (
|
|
110
|
-
<ConfigFieldControl
|
|
111
|
-
key={field.path}
|
|
112
|
-
field={field}
|
|
113
|
-
value={draft[field.path]}
|
|
114
|
-
inherited={getDotted(placeholderSource, field.path)}
|
|
115
|
-
onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
|
|
116
|
-
/>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
<Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
|
|
120
|
-
</div>
|
|
133
|
+
{sectionBody(section)}
|
|
121
134
|
</TabsContent>
|
|
122
135
|
))}
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
<div>
|
|
127
|
-
<
|
|
128
|
-
|
|
137
|
+
{!hideJson && (
|
|
138
|
+
<TabsContent value="json">
|
|
139
|
+
<div className="space-y-3">
|
|
140
|
+
<div>
|
|
141
|
+
<h3 className="text-sm font-medium">{jsonTitle}</h3>
|
|
142
|
+
{jsonDescription && <p className="text-xs text-muted-fg">{jsonDescription}</p>}
|
|
143
|
+
</div>
|
|
144
|
+
<Textarea
|
|
145
|
+
rows={18}
|
|
146
|
+
className="font-mono text-xs"
|
|
147
|
+
value={raw}
|
|
148
|
+
onChange={(event) => setRaw(event.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
|
151
|
+
<Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
|
|
129
152
|
</div>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
className="font-mono text-xs"
|
|
133
|
-
value={raw}
|
|
134
|
-
onChange={(event) => setRaw(event.target.value)}
|
|
135
|
-
/>
|
|
136
|
-
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
|
137
|
-
<Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
|
|
138
|
-
</div>
|
|
139
|
-
</TabsContent>
|
|
153
|
+
</TabsContent>
|
|
154
|
+
)}
|
|
140
155
|
</Tabs>
|
|
141
156
|
);
|
|
142
157
|
}
|
|
@@ -4,7 +4,10 @@ import { t } from "../../i18n";
|
|
|
4
4
|
|
|
5
5
|
// These are functions (not module-level consts) so t() runs per-render with the
|
|
6
6
|
// active locale — a frozen const would lock the strings to the locale at import.
|
|
7
|
-
|
|
7
|
+
// Config is organized by concept: Settings (routing / super-agent behaviour) and
|
|
8
|
+
// Engines (provider keys) are separate top-level tabs. Telegram is edited on its
|
|
9
|
+
// own channel page (the canonical mechanism), not here.
|
|
10
|
+
export function projectSettingsSections(): ConfigSection[] {
|
|
8
11
|
return [
|
|
9
12
|
{
|
|
10
13
|
key: "routing",
|
|
@@ -22,16 +25,11 @@ export function projectOverrideSections(): ConfigSection[] {
|
|
|
22
25
|
{ path: "super_agent.system", label: t("settings_ui.cfg_extra_prompt"), kind: "textarea" },
|
|
23
26
|
],
|
|
24
27
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ path: "telegram.chat_id", label: t("settings_ui.cfg_chat_id") },
|
|
31
|
-
{ path: "telegram.bot_token", label: t("settings_ui.cfg_bot_token"), kind: "password" },
|
|
32
|
-
{ path: "telegram.respond_with_engine", label: t("settings_ui.cfg_respond_with_engine"), kind: "boolean" },
|
|
33
|
-
],
|
|
34
|
-
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function projectEnginesSections(): ConfigSection[] {
|
|
32
|
+
return [
|
|
35
33
|
{
|
|
36
34
|
key: "engines",
|
|
37
35
|
label: t("settings_ui.cfg_engines_label"),
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Brain, Bot, Crown, RefreshCw } from "lucide-react";
|
|
4
|
+
import { Agents, Projects } from "../../lib/api";
|
|
5
|
+
import type { AgentEntry, FileContent } from "../../types/daemon";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import { Spinner, Empty } from "../ui";
|
|
8
|
+
import { useToast } from "../Toast";
|
|
9
|
+
import { t } from "../../i18n";
|
|
10
|
+
import { FileViewer } from "../files/FileViewer";
|
|
11
|
+
|
|
12
|
+
// Which memory is open in the right pane.
|
|
13
|
+
type Sel = { kind: "project" } | { kind: "agent"; slug: string };
|
|
14
|
+
|
|
15
|
+
function selId(s: Sel): string {
|
|
16
|
+
return s.kind === "project" ? "project" : `agent:${s.slug}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// On-disk-ish path shown in the viewer header (mirrors the real memory.md
|
|
20
|
+
// locations so it reads familiarly, like the docs surface).
|
|
21
|
+
function selPath(s: Sel): string {
|
|
22
|
+
return s.kind === "project" ? ".apc/memory.md" : `agents/${s.slug}/memory.md`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadBody(pid: string, s: Sel): Promise<string> {
|
|
26
|
+
return s.kind === "project"
|
|
27
|
+
? Projects.memory.get(pid).then((r) => r.body)
|
|
28
|
+
: Agents.memory.get(pid, s.slug).then((r) => r.body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveBody(pid: string, s: Sel, body: string): Promise<void> {
|
|
32
|
+
return s.kind === "project"
|
|
33
|
+
? Projects.memory.put(pid, body).then(() => {})
|
|
34
|
+
: Agents.memory.put(pid, s.slug, body).then(() => {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SidebarItem({
|
|
38
|
+
active, onClick, icon: Icon, iconClass, label, sub,
|
|
39
|
+
}: {
|
|
40
|
+
active: boolean;
|
|
41
|
+
onClick: () => void;
|
|
42
|
+
icon: typeof Bot;
|
|
43
|
+
iconClass?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
sub?: string;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-[13px]",
|
|
53
|
+
active ? "bg-primary/15 text-foreground" : "text-foreground/80 hover:bg-accent/40",
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<Icon className={cn("size-3.5 shrink-0", iconClass ?? "text-muted-foreground")} />
|
|
57
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
58
|
+
{sub && <span className="shrink-0 truncate text-[11px] text-muted-foreground">{sub}</span>}
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Docs-style two-pane browser for durable memory: a sidebar listing the project
|
|
64
|
+
// ("General") memory plus every agent's memory, and the shared FileViewer on the
|
|
65
|
+
// right for markdown edit / split-preview / save — the same surface as /docs.
|
|
66
|
+
export function MemoryBrowser({ pid }: { pid: string }) {
|
|
67
|
+
const toast = useToast();
|
|
68
|
+
const [sel, setSel] = useState<Sel>({ kind: "project" });
|
|
69
|
+
|
|
70
|
+
const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
|
|
71
|
+
|
|
72
|
+
const bodyKey = `/memory/${pid}/${selId(sel)}`;
|
|
73
|
+
const body = useSWR(bodyKey, () => loadBody(pid, sel));
|
|
74
|
+
|
|
75
|
+
// Adapt the raw memory body into the FileContent shape FileViewer expects.
|
|
76
|
+
const file = useMemo<FileContent | null>(() => {
|
|
77
|
+
if (body.data === undefined) return null;
|
|
78
|
+
const content = body.data ?? "";
|
|
79
|
+
return {
|
|
80
|
+
path: selPath(sel),
|
|
81
|
+
name: "memory.md",
|
|
82
|
+
kind: "markdown",
|
|
83
|
+
size: content.length,
|
|
84
|
+
modified: "",
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
content,
|
|
87
|
+
};
|
|
88
|
+
}, [body.data, sel]);
|
|
89
|
+
|
|
90
|
+
const onSave = async (content: string) => {
|
|
91
|
+
await saveBody(pid, sel, content);
|
|
92
|
+
toast.success(t("project.memories.saved"));
|
|
93
|
+
void body.mutate(content, { revalidate: false });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const list = (agents.data || []) as AgentEntry[];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex h-full min-h-0 overflow-hidden rounded-xl border border-border bg-card">
|
|
100
|
+
{/* Sidebar */}
|
|
101
|
+
<div className="flex w-64 shrink-0 flex-col border-r border-border">
|
|
102
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
|
103
|
+
<Brain className="size-4 text-muted-foreground" />
|
|
104
|
+
<span className="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
105
|
+
{t("project.memories.sidebar_title")}
|
|
106
|
+
</span>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => { void agents.mutate(); void body.mutate(); }}
|
|
110
|
+
className="text-muted-foreground hover:text-foreground"
|
|
111
|
+
aria-label={t("common.refresh")}
|
|
112
|
+
>
|
|
113
|
+
<RefreshCw className={agents.isValidating ? "size-3.5 animate-spin" : "size-3.5"} />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
|
118
|
+
{/* General / project memory */}
|
|
119
|
+
<p className="px-1.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
120
|
+
{t("project.memories.general_group")}
|
|
121
|
+
</p>
|
|
122
|
+
<SidebarItem
|
|
123
|
+
active={sel.kind === "project"}
|
|
124
|
+
onClick={() => setSel({ kind: "project" })}
|
|
125
|
+
icon={Brain}
|
|
126
|
+
iconClass="text-sky-500"
|
|
127
|
+
label={t("project.memories.general_item")}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Agent memories */}
|
|
131
|
+
<p className="px-1.5 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
132
|
+
{t("project.memories.agents_title")}
|
|
133
|
+
</p>
|
|
134
|
+
{agents.isLoading ? (
|
|
135
|
+
<div className="flex justify-center py-4"><Spinner size={14} /></div>
|
|
136
|
+
) : list.length === 0 ? (
|
|
137
|
+
<div className="px-1.5 py-2">
|
|
138
|
+
<Empty>{t("project.memories.no_agents")}</Empty>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
list.map((a) => (
|
|
142
|
+
<SidebarItem
|
|
143
|
+
key={a.slug}
|
|
144
|
+
active={sel.kind === "agent" && sel.slug === a.slug}
|
|
145
|
+
onClick={() => setSel({ kind: "agent", slug: a.slug })}
|
|
146
|
+
icon={a.is_master ? Crown : Bot}
|
|
147
|
+
iconClass={a.is_master ? "text-violet-400" : "text-muted-foreground"}
|
|
148
|
+
label={a.slug}
|
|
149
|
+
sub={a.role || undefined}
|
|
150
|
+
/>
|
|
151
|
+
))
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Viewer */}
|
|
157
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
158
|
+
<FileViewer file={file} loading={body.isLoading} onSave={onSave} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|