@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.
Files changed (61) hide show
  1. package/package.json +3 -2
  2. package/skills/apx/SKILL.md +3 -0
  3. package/src/core/agent/index.js +2 -0
  4. package/src/core/agent/judge.js +174 -0
  5. package/src/core/agent/model-router.js +107 -5
  6. package/src/core/agent/prompts/modes/code-build.md +1 -1
  7. package/src/core/agent/run-agent.js +149 -12
  8. package/src/core/agent/security.js +97 -0
  9. package/src/core/agent/stuck-detector.js +89 -0
  10. package/src/core/agent/super-agent.js +58 -17
  11. package/src/core/agent/tools/handlers/run-subagent.js +117 -0
  12. package/src/core/agent/tools/helpers.js +11 -1
  13. package/src/core/agent/tools/names.js +2 -0
  14. package/src/core/agent/tools/registry.js +10 -0
  15. package/src/core/artifacts/preview.js +392 -0
  16. package/src/core/artifacts/tunnel.js +169 -0
  17. package/src/core/config/index.js +61 -0
  18. package/src/core/config/redact.js +44 -0
  19. package/src/core/config/secret-values.js +132 -0
  20. package/src/core/engines/mock.js +15 -1
  21. package/src/core/logging.js +10 -3
  22. package/src/core/memory/compactor.js +65 -56
  23. package/src/core/memory/summarizer.js +125 -0
  24. package/src/core/stores/conversations-compactor.js +24 -31
  25. package/src/host/daemon/api/admin-config.js +5 -0
  26. package/src/host/daemon/api/artifact-preview.js +82 -0
  27. package/src/host/daemon/api/config.js +17 -5
  28. package/src/host/daemon/api/sessions.js +9 -0
  29. package/src/host/daemon/api/web.js +1 -1
  30. package/src/host/daemon/api.js +2 -0
  31. package/src/host/daemon/index.js +16 -1
  32. package/src/interfaces/acp/index.js +363 -0
  33. package/src/interfaces/acp/jsonrpc.js +180 -0
  34. package/src/interfaces/acp/session.js +205 -0
  35. package/src/interfaces/cli/commands/acp.js +10 -0
  36. package/src/interfaces/cli/commands/artifact.js +115 -0
  37. package/src/interfaces/cli/index.js +74 -0
  38. package/src/interfaces/web/dist/assets/index-D4BmWoDM.css +1 -0
  39. package/src/interfaces/web/dist/assets/index-vwd6yQVw.js +803 -0
  40. package/src/interfaces/web/dist/assets/index-vwd6yQVw.js.map +1 -0
  41. package/src/interfaces/web/dist/index.html +2 -2
  42. package/src/interfaces/web/package-lock.json +9 -9
  43. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +145 -2
  44. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
  45. package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
  46. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
  47. package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +236 -0
  48. package/src/interfaces/web/src/i18n/en.ts +53 -0
  49. package/src/interfaces/web/src/i18n/es.ts +53 -0
  50. package/src/interfaces/web/src/lib/api/artifacts.ts +38 -0
  51. package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
  52. package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
  53. package/src/interfaces/web/src/screens/base/ModelsTab.tsx +4 -2
  54. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
  55. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
  56. package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
  57. package/src/interfaces/web/src/screens/project/Overview.tsx +3 -2
  58. package/src/interfaces/web/src/types/daemon.ts +16 -0
  59. package/src/interfaces/web/dist/assets/index-BuII-tAi.css +0 -1
  60. package/src/interfaces/web/dist/assets/index-YmMRG--4.js +0 -778
  61. package/src/interfaces/web/dist/assets/index-YmMRG--4.js.map +0 -1
@@ -7,15 +7,21 @@ import { Badge, Button, Empty, Input, Loading, Tip } from "../../components/ui";
7
7
  import { UiSelect } from "../../components/UiSelect";
8
8
  import { useToast } from "../../components/Toast";
9
9
  import { usePersonaName } from "../../hooks/usePersonaName";
10
+ import { useProject } from "../../hooks/useProjects";
10
11
  import { t } from "../../i18n";
11
12
 
12
13
  const ENGINE_TONE: Record<string, "success" | "info" | "warning" | "muted"> = {
13
14
  apx: "success", claude: "info", codex: "warning",
14
15
  };
15
16
 
16
- export function SessionsTab() {
17
+ // `pid` present + not base → scope to that project's local folder. Base (or no
18
+ // pid) shows every session across engines and folders.
19
+ export function SessionsTab({ pid }: { pid?: string } = {}) {
17
20
  const toast = useToast();
18
21
  const persona = usePersonaName();
22
+ const isBase = !pid || String(pid) === "0";
23
+ const { project } = useProject(isBase ? "" : pid);
24
+ const cwd = isBase ? undefined : project?.path || undefined;
19
25
  const [engine, setEngine] = useState("");
20
26
  const [input, setInput] = useState("");
21
27
  const [query, setQuery] = useState("");
@@ -28,10 +34,10 @@ export function SessionsTab() {
28
34
  }, [input]);
29
35
 
30
36
  const paged = usePagedQuery<SessionRow>({
31
- key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}`,
37
+ key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}&cwd=${cwd || ""}`,
32
38
  fetchPage: (limit, offset) =>
33
- Sessions.page({ engine: engine || undefined, q: query || undefined, deep, limit, offset }),
34
- resetKey: `${engine}|${query}|${deep ? 1 : 0}`,
39
+ Sessions.page({ engine: engine || undefined, q: query || undefined, deep, cwd, limit, offset }),
40
+ resetKey: `${engine}|${query}|${deep ? 1 : 0}|${cwd || ""}`,
35
41
  });
36
42
 
37
43
  const clear = () => { setInput(""); setQuery(""); setEngine(""); setDeep(false); };
@@ -72,7 +78,7 @@ export function SessionsTab() {
72
78
  <Section
73
79
  fullHeight
74
80
  title={t("base.sessions_title")}
75
- description={t("base.sessions_desc")}
81
+ description={isBase ? t("base.sessions_desc") : t("base.sessions_desc_scoped", { path: cwd || "…" })}
76
82
  action={
77
83
  <Tip content={t("base.sessions_refresh")}>
78
84
  <Button size="sm" variant="secondary" onClick={() => paged.mutate()}><RefreshCw size={13} /></Button>
@@ -1,16 +1,17 @@
1
- import { useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
3
  import useSWR from "swr";
4
4
  import { RefreshCw, Trash2 } from "lucide-react";
5
5
  import { Projects } from "../../lib/api";
6
6
  import { Section } from "../../components/Section";
7
- import { Button, Dialog, Empty, Loading } from "../../components/ui";
7
+ import { Button, Dialog, Empty, Loading, Textarea } from "../../components/ui";
8
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
9
9
  import { ConfigTabsEditor } from "../../components/config/ConfigTabsEditor";
10
- import { apcProjectSections, projectOverrideSections } from "../../components/config/project-config-sections";
10
+ import { apcProjectSections, projectSettingsSections, projectEnginesSections } from "../../components/config/project-config-sections";
11
+ import { TelegramTab } from "./TelegramTab";
11
12
  import { useToast } from "../../components/Toast";
12
13
  import { useProject } from "../../hooks/useProjects";
13
- import { flattenObject } from "../../lib/config-values";
14
+ import { flattenObject, parseConfigJson } from "../../lib/config-values";
14
15
  import { isSecretMarker } from "../../lib/secrets";
15
16
  import { t } from "../../i18n";
16
17
 
@@ -36,54 +37,91 @@ export function ConfigTab({ pid }: { pid: string }) {
36
37
  cfg.mutate();
37
38
  };
38
39
 
40
+ const saveOverrideFields = async (set: Record<string, unknown>, unset: string[]) => {
41
+ await Projects.config.set(pid, set);
42
+ if (unset.length) await Projects.config.unset(pid, unset);
43
+ toast.success(t("project.config.save_fields_success"));
44
+ cfg.mutate();
45
+ };
46
+
39
47
  return (
40
48
  <div className="space-y-6">
41
49
  <Section title={t("project.config.section_title")} description={t("project.config.section_desc")}>
42
- <Tabs defaultValue="override" className="space-y-4">
43
- <TabsList>
44
- <TabsTrigger value="override">Override</TabsTrigger>
45
- <TabsTrigger value="project">APC project</TabsTrigger>
46
- <TabsTrigger value="effective">Effective</TabsTrigger>
50
+ {/* Organized by concept: Settings · Engines · Telegram · Project · JSON. */}
51
+ <Tabs defaultValue="settings" className="space-y-4">
52
+ <TabsList className="flex flex-wrap">
53
+ <TabsTrigger value="settings">{t("project.config.tab_settings")}</TabsTrigger>
54
+ <TabsTrigger value="engines">{t("settings_ui.cfg_engines_label")}</TabsTrigger>
55
+ {!isBase && <TabsTrigger value="telegram">{t("project.nav.telegram")}</TabsTrigger>}
56
+ <TabsTrigger value="project">{t("project.config.tab_project")}</TabsTrigger>
57
+ <TabsTrigger value="json">JSON</TabsTrigger>
47
58
  </TabsList>
48
59
 
49
- <TabsContent value="override">
60
+ <TabsContent value="settings">
50
61
  <ConfigTabsEditor
51
- sections={projectOverrideSections()}
62
+ sections={projectSettingsSections()}
52
63
  source={cfg.data.project_only}
53
64
  placeholderSource={cfg.data.effective}
54
65
  jsonTitle={cfg.data.project_config_path}
55
- jsonDescription=".apc/config.json. Overrides del proyecto."
56
- onSaveFields={async (set, unset) => {
57
- await Projects.config.set(pid, set);
58
- if (unset.length) await Projects.config.unset(pid, unset);
59
- toast.success(t("project.config.save_fields_success"));
60
- cfg.mutate();
61
- }}
66
+ onSaveFields={saveOverrideFields}
62
67
  onSaveJson={saveOverrideJson}
68
+ hideJson
63
69
  />
64
70
  </TabsContent>
65
71
 
72
+ <TabsContent value="engines">
73
+ <ConfigTabsEditor
74
+ sections={projectEnginesSections()}
75
+ source={cfg.data.project_only}
76
+ placeholderSource={cfg.data.effective}
77
+ jsonTitle={cfg.data.project_config_path}
78
+ onSaveFields={saveOverrideFields}
79
+ onSaveJson={saveOverrideJson}
80
+ hideJson
81
+ />
82
+ </TabsContent>
83
+
84
+ {!isBase && (
85
+ <TabsContent value="telegram">
86
+ <TelegramTab pid={pid} />
87
+ </TabsContent>
88
+ )}
89
+
66
90
  <TabsContent value="project">
67
91
  <ConfigTabsEditor
68
92
  sections={apcProjectSections()}
69
93
  source={cfg.data.apc_project || {}}
70
94
  jsonTitle={cfg.data.project_json_path}
71
- jsonDescription=".apc/project.json. Metadata APC portable."
72
95
  onSaveFields={async (set, unset) => {
73
96
  await Projects.apcProject.set(pid, cleanSet(set), unset);
74
97
  toast.success(t("project.config.save_meta_success"));
75
98
  cfg.mutate();
76
99
  }}
77
100
  onSaveJson={saveProjectJson}
101
+ hideJson
78
102
  />
79
103
  </TabsContent>
80
104
 
81
- <TabsContent value="effective">
82
- <div className="space-y-2">
83
- <p className="text-xs text-muted-fg">{t("project.config.effective_read")}</p>
84
- <pre className="max-h-96 overflow-auto rounded-lg border border-border bg-muted/40 p-3 text-xs">
85
- {JSON.stringify(cfg.data.effective, null, 2)}
86
- </pre>
105
+ <TabsContent value="json">
106
+ <div className="space-y-6">
107
+ <JsonEditor
108
+ title={cfg.data.project_config_path}
109
+ description=".apc/config.json overrides del proyecto."
110
+ source={cfg.data.project_only}
111
+ onSave={saveOverrideJson}
112
+ />
113
+ <JsonEditor
114
+ title={cfg.data.project_json_path}
115
+ description=".apc/project.json — metadata APC portable."
116
+ source={cfg.data.apc_project || {}}
117
+ onSave={saveProjectJson}
118
+ />
119
+ <div className="space-y-2">
120
+ <p className="text-xs text-muted-fg">{t("project.config.effective_read")}</p>
121
+ <pre className="max-h-96 overflow-auto rounded-lg border border-border bg-muted/40 p-3 text-xs">
122
+ {JSON.stringify(cfg.data.effective, null, 2)}
123
+ </pre>
124
+ </div>
87
125
  </div>
88
126
  </TabsContent>
89
127
  </Tabs>
@@ -216,6 +254,53 @@ function DangerZone({
216
254
  );
217
255
  }
218
256
 
257
+ // Raw JSON editor for one config file (redacted secrets echo back untouched —
258
+ // the daemon restores real values on save).
259
+ function JsonEditor({
260
+ title,
261
+ description,
262
+ source,
263
+ onSave,
264
+ }: {
265
+ title: string;
266
+ description?: string;
267
+ source: Record<string, unknown>;
268
+ onSave: (next: Record<string, unknown>) => Promise<void>;
269
+ }) {
270
+ const [raw, setRaw] = useState("");
271
+ const [error, setError] = useState("");
272
+ const [busy, setBusy] = useState(false);
273
+
274
+ useEffect(() => {
275
+ setRaw(JSON.stringify(source || {}, null, 2));
276
+ setError("");
277
+ }, [source]);
278
+
279
+ const save = async () => {
280
+ setError("");
281
+ setBusy(true);
282
+ try {
283
+ await onSave(parseConfigJson(raw));
284
+ } catch (e) {
285
+ setError((e as Error).message);
286
+ } finally {
287
+ setBusy(false);
288
+ }
289
+ };
290
+
291
+ return (
292
+ <div className="space-y-2">
293
+ <div>
294
+ <h3 className="text-sm font-medium">{title}</h3>
295
+ {description && <p className="text-xs text-muted-fg">{description}</p>}
296
+ </div>
297
+ <Textarea rows={14} className="font-mono text-xs" value={raw} onChange={(e) => setRaw(e.target.value)} />
298
+ {error && <p className="text-xs text-destructive">{error}</p>}
299
+ <Button variant="primary" loading={busy} onClick={save}>{t("settings_ui.save_json")}</Button>
300
+ </div>
301
+ );
302
+ }
303
+
219
304
  function cleanSet(set: Record<string, unknown>) {
220
305
  const out: Record<string, unknown> = {};
221
306
  for (const [key, value] of Object.entries(flattenObject(set))) {
@@ -1,134 +1,13 @@
1
- import { useEffect, useState } from "react";
2
- import useSWR from "swr";
3
- import { Bot, Brain, ChevronDown, ChevronRight, Crown, Save } from "lucide-react";
4
- import { Agents, Projects } from "../../lib/api";
5
- import type { AgentEntry } from "../../types/daemon";
6
- import { Section } from "../../components/Section";
7
- import { Button, Empty, Loading, Textarea } from "../../components/ui";
8
- import { useToast } from "../../components/Toast";
9
- import { t } from "../../i18n";
10
-
11
- // Editable markdown memory block with dirty-tracking + save.
12
- function MemoryEditor({
13
- load,
14
- save,
15
- rows = 10,
16
- placeholder,
17
- }: {
18
- load: () => Promise<string>;
19
- save: (body: string) => Promise<void>;
20
- rows?: number;
21
- placeholder?: string;
22
- }) {
23
- const toast = useToast();
24
- const [original, setOriginal] = useState<string | null>(null);
25
- const [value, setValue] = useState("");
26
- const [busy, setBusy] = useState(false);
27
-
28
- useEffect(() => {
29
- let live = true;
30
- load().then((b) => { if (live) { setOriginal(b); setValue(b); } }).catch(() => { if (live) { setOriginal(""); setValue(""); } });
31
- return () => { live = false; };
32
- }, [load]);
33
-
34
- if (original === null) return <Loading />;
35
- const dirty = value !== original;
36
-
37
- const onSave = async () => {
38
- setBusy(true);
39
- try {
40
- await save(value);
41
- setOriginal(value);
42
- toast.success(t("project.memories.saved"));
43
- } catch (e) { toast.error((e as Error).message); }
44
- finally { setBusy(false); }
45
- };
46
-
47
- return (
48
- <div className="space-y-2">
49
- <Textarea
50
- rows={rows}
51
- className="font-mono text-xs"
52
- value={value}
53
- onChange={(e) => setValue(e.target.value)}
54
- placeholder={placeholder}
55
- />
56
- <div className="flex items-center justify-between">
57
- <span className="text-[11px] text-muted-fg">{value.length} {t("project.memories.chars")}</span>
58
- <Button size="sm" variant="primary" loading={busy} disabled={!dirty} onClick={onSave}>
59
- <Save size={12} /> {t("project.memories.save_btn")}
60
- </Button>
61
- </div>
62
- </div>
63
- );
64
- }
65
-
66
- function AgentMemoryRow({ pid, agent }: { pid: string; agent: AgentEntry }) {
67
- const [open, setOpen] = useState(false);
68
- const Icon = agent.is_master ? Crown : Bot;
69
- return (
70
- <li className="rounded-xl border border-border bg-muted/30">
71
- <button
72
- type="button"
73
- onClick={() => setOpen((v) => !v)}
74
- className="flex w-full items-center gap-3 px-3 py-2 text-left"
75
- >
76
- {open ? <ChevronDown size={14} className="text-muted-fg" /> : <ChevronRight size={14} className="text-muted-fg" />}
77
- <Icon size={14} className={agent.is_master ? "text-violet-400" : "text-muted-fg"} />
78
- <span className="text-sm font-medium">{agent.slug}</span>
79
- {agent.role && <span className="text-xs text-muted-fg">· {agent.role}</span>}
80
- </button>
81
- {open && (
82
- <div className="border-t border-border p-3">
83
- <MemoryEditor
84
- rows={8}
85
- load={() => Agents.memory.get(pid, agent.slug).then((r) => r.body)}
86
- save={(body) => Agents.memory.put(pid, agent.slug, body).then(() => {})}
87
- />
88
- </div>
89
- )}
90
- </li>
91
- );
92
- }
1
+ import { MemoryBrowser } from "../../components/memory/MemoryBrowser";
93
2
 
3
+ // Durable memory surface. Same docs-style two-pane browser as /docs: a sidebar
4
+ // listing the project ("General") memory plus every agent's memory, and a shared
5
+ // markdown editor (edit / split-preview / save) on the right. Project memory is
6
+ // .apc/memory.md; agent memory is ~/.apx/projects/<id>/agents/<slug>/memory.md.
94
7
  export function MemoriesTab({ pid }: { pid: string }) {
95
- const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
96
-
97
8
  return (
98
- <div className="space-y-6">
99
- <Section
100
- title={t("project.memories.project_title")}
101
- description={t("project.memories.project_desc")}
102
- >
103
- <div className="flex items-start gap-3">
104
- <div className="mt-1 flex size-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-600 to-indigo-600">
105
- <Brain className="size-4 text-white" />
106
- </div>
107
- <div className="min-w-0 flex-1">
108
- <MemoryEditor
109
- rows={12}
110
- load={() => Projects.memory.get(pid).then((r) => r.body)}
111
- save={(body) => Projects.memory.put(pid, body).then(() => {})}
112
- placeholder={t("project.memories.project_ph")}
113
- />
114
- </div>
115
- </div>
116
- </Section>
117
-
118
- <Section
119
- title={t("project.memories.agents_title")}
120
- description={t("project.memories.agents_desc")}
121
- >
122
- {agents.isLoading && <Loading />}
123
- {!agents.isLoading && (agents.data?.length ?? 0) === 0 && (
124
- <Empty>{t("project.memories.no_agents")}</Empty>
125
- )}
126
- <ul className="space-y-2">
127
- {(agents.data || []).map((a) => (
128
- <AgentMemoryRow key={a.slug} pid={pid} agent={a} />
129
- ))}
130
- </ul>
131
- </Section>
9
+ <div className="h-full">
10
+ <MemoryBrowser pid={pid} />
132
11
  </div>
133
12
  );
134
13
  }
@@ -35,8 +35,9 @@ export function Overview({ pid }: { pid: string }) {
35
35
  <Card title={t("project.overview.artifacts")} value={artifacts.data?.length ?? "…"} href={`/p/${pid}/artifacts`} icon={FileCode2} />
36
36
  </div>
37
37
 
38
- {/* Task workflow strip */}
39
- {summary.data && summary.data.open > 0 && (
38
+ {/* Task workflow strip — always shown once the summary loads, so every
39
+ status reads at least 0 (parity with the base dashboard). */}
40
+ {summary.data && (
40
41
  <div className="flex flex-wrap gap-2">
41
42
  {TASK_STATUS_ORDER.map((s) => (
42
43
  <NavLink
@@ -293,6 +293,22 @@ export interface SuperAgentConfig {
293
293
  models?: string[];
294
294
  order?: string[];
295
295
  };
296
+ // Content-based routing (RouterLLM pattern): prefer a model per turn by
297
+ // features. Composes with model_fallback (failover) — see RoutingPanel.
298
+ routing?: {
299
+ enabled?: boolean;
300
+ rules?: Array<{
301
+ model: string;
302
+ when?: {
303
+ has_image?: boolean;
304
+ min_prompt_chars?: number;
305
+ max_prompt_chars?: number;
306
+ min_context_chars?: number;
307
+ channels?: string[];
308
+ keywords?: string[];
309
+ };
310
+ }>;
311
+ };
296
312
  }
297
313
 
298
314
  /** ~/.apx/config.json shape (partial — only what we read/write today). */