@agentprojectcontext/apx 1.34.0 → 1.36.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 +1 -1
- package/skills/apx/SKILL.md +1 -1
- package/src/core/agent/build-agent-system.js +134 -58
- package/src/core/agent/channels/voice-context.js +4 -4
- package/src/core/agent/prompt-builder.js +176 -123
- package/src/core/agent/prompts/channels/code.md +12 -10
- package/src/core/agent/prompts/channels/desktop.md +5 -32
- package/src/core/agent/prompts/channels/telegram.md +4 -15
- package/src/core/agent/prompts/channels/web_code.md +11 -11
- package/src/core/agent/prompts/core/agent-base.md +24 -0
- package/src/core/agent/prompts/core/project-agent.md +11 -0
- package/src/core/agent/prompts/core/super-agent.md +21 -0
- package/src/core/agent/prompts/discipline/action.md +10 -0
- package/src/core/agent/prompts/discipline/single-segment.md +6 -0
- package/src/core/agent/prompts/discipline/two-segment.md +11 -0
- package/src/core/agent/self-memory.js +43 -1
- package/src/core/agent/skills/index-store.js +307 -0
- package/src/core/agent/skills/index.js +15 -1
- package/src/core/agent/skills/inspector.js +317 -0
- package/src/core/agent/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/_git.js +50 -0
- package/src/core/agent/tools/handlers/git-diff.js +44 -0
- package/src/core/agent/tools/handlers/git-log.js +38 -0
- package/src/core/agent/tools/handlers/git-show.js +34 -0
- package/src/core/agent/tools/handlers/git-status.js +61 -0
- package/src/core/agent/tools/names.js +31 -0
- package/src/core/agent/tools/registry.js +36 -5
- package/src/core/config/index.js +21 -0
- package/src/core/runtime-skills/apx/SKILL.md +27 -39
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
- package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
- package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
- package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
- package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
- package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
- package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
- package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
- package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
- package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
- package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
- package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
- package/src/core/stores/conversations.js +27 -2
- package/src/host/daemon/api/exec.js +2 -0
- package/src/host/daemon/api/skills.js +140 -6
- package/src/host/daemon/api/super-agent.js +56 -1
- package/src/host/daemon/index.js +17 -0
- package/src/interfaces/cli/branding.js +53 -0
- package/src/interfaces/cli/commands/skills.js +254 -0
- package/src/interfaces/cli/index.js +84 -2
- package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +1 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js +628 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +0 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +69 -1
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
- package/src/interfaces/web/src/hooks/useChat.ts +54 -2
- package/src/interfaces/web/src/i18n/en.ts +12 -1
- package/src/interfaces/web/src/i18n/es.ts +12 -1
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/skills.ts +70 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +12 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/core/agent/prompts/action-discipline.md +0 -24
- package/src/core/agent/prompts/super-agent-base.md +0 -42
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
- package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +0 -100
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Sparkles, RefreshCw, Wand2 } from "lucide-react";
|
|
4
|
+
import { Section } from "../Section";
|
|
5
|
+
import { Button, Field, Input, Loading, Badge, Switch } from "../ui";
|
|
6
|
+
import { useToast } from "../Toast";
|
|
7
|
+
import { Skills, type InspectTrace } from "../../lib/api/skills";
|
|
8
|
+
|
|
9
|
+
// Skill Inspector — per-turn skill RAG middleware. When ON, the static
|
|
10
|
+
// "available skills" slug-dump is removed from the agent's system prompt and a
|
|
11
|
+
// local RAG injects, per turn, only the skill(s) the user's message actually
|
|
12
|
+
// needs. This panel toggles the feature, tunes its thresholds, (re)builds the
|
|
13
|
+
// vector index, and offers a live dry-run so you can see what it would surface.
|
|
14
|
+
//
|
|
15
|
+
// Mirrors MemoryPanel (RAG embeddings): same Section/Field/Button idiom. Config
|
|
16
|
+
// persists under config.skills.inspector.* via the inspector PUT endpoint, so
|
|
17
|
+
// no separate global-config patch is needed.
|
|
18
|
+
|
|
19
|
+
// Numeric knobs with human labels + sane ranges. We keep them as plain number
|
|
20
|
+
// inputs (same idiom as the embeddings model fields) rather than sliders so the
|
|
21
|
+
// values are explicit and copy-pasteable.
|
|
22
|
+
const KNOBS: { key: keyof NumericKnobs; label: string; hint: string; step: number; min: number; max: number }[] = [
|
|
23
|
+
{ key: "load_threshold", label: "Umbral de carga", hint: "Similitud mínima para inyectar el CUERPO de la skill (alto = más estricto).", step: 0.01, min: 0, max: 1 },
|
|
24
|
+
{ key: "hint_threshold", label: "Umbral de sugerencia", hint: "Similitud mínima para solo SUGERIR la skill (que el agente la cargue si quiere).", step: 0.01, min: 0, max: 1 },
|
|
25
|
+
{ key: "margin", label: "Margen sobre el 2º", hint: "El top debe superar al segundo por este margen para cargar su cuerpo (evita empates flojos).", step: 0.01, min: 0, max: 1 },
|
|
26
|
+
{ key: "max_loaded", label: "Máx. cuerpos cargados", hint: "Cuántas skills se inyectan completas por turno.", step: 1, min: 0, max: 5 },
|
|
27
|
+
{ key: "max_hints", label: "Máx. sugerencias", hint: "Cuántas skills extra se nombran como sugerencia.", step: 1, min: 0, max: 8 },
|
|
28
|
+
{ key: "prompt_floor", label: "Largo mínimo del prompt", hint: "Mensajes más cortos que esto se ignoran (evita 'ok', 'hola').", step: 1, min: 0, max: 40 },
|
|
29
|
+
{ key: "body_char_cap", label: "Tope de chars del cuerpo", hint: "Recorta cuerpos de skill largos para no inflar el contexto.", step: 500, min: 500, max: 20000 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
type NumericKnobs = {
|
|
33
|
+
load_threshold: number; hint_threshold: number; margin: number;
|
|
34
|
+
max_loaded: number; max_hints: number; prompt_floor: number; body_char_cap: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function SkillsInspectorPanel() {
|
|
38
|
+
const toast = useToast();
|
|
39
|
+
const { data, mutate, isLoading } = useSWR("/skills/inspector", () => Skills.inspector());
|
|
40
|
+
const [busy, setBusy] = useState(false);
|
|
41
|
+
const [probe, setProbe] = useState("");
|
|
42
|
+
const [probeResult, setProbeResult] = useState<InspectTrace | null>(null);
|
|
43
|
+
|
|
44
|
+
if (isLoading || !data) return <Loading />;
|
|
45
|
+
|
|
46
|
+
const cfg = data.config;
|
|
47
|
+
const idx = data.index;
|
|
48
|
+
|
|
49
|
+
const apply = async (patch: Record<string, unknown>) => {
|
|
50
|
+
setBusy(true);
|
|
51
|
+
try {
|
|
52
|
+
await Skills.updateInspector(patch);
|
|
53
|
+
await mutate();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
toast.error(`No se pudo guardar: ${(e as Error).message}`);
|
|
56
|
+
} finally {
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const runIndex = async (force = false) => {
|
|
62
|
+
setBusy(true);
|
|
63
|
+
try {
|
|
64
|
+
const r = await Skills.index({ force });
|
|
65
|
+
toast.success(
|
|
66
|
+
`Indexado con ${r.embedder} (dim ${r.dim}): +${r.changed.added} ~${r.changed.refreshed} -${r.changed.removed}.`,
|
|
67
|
+
);
|
|
68
|
+
await mutate();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
toast.error(`Falló el index: ${(e as Error).message}`);
|
|
71
|
+
} finally {
|
|
72
|
+
setBusy(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const runProbe = async () => {
|
|
77
|
+
if (!probe.trim()) return;
|
|
78
|
+
setBusy(true);
|
|
79
|
+
setProbeResult(null);
|
|
80
|
+
try {
|
|
81
|
+
const r = await Skills.inspect(probe.trim());
|
|
82
|
+
setProbeResult(r.trace);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
toast.error(`Falló el dry-run: ${(e as Error).message}`);
|
|
85
|
+
} finally {
|
|
86
|
+
setBusy(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="grid gap-6 xl:grid-cols-2 xl:items-start">
|
|
92
|
+
<Section
|
|
93
|
+
title="Skill Inspector (RAG por turno)"
|
|
94
|
+
description="Función experimental. Cuando está activa, el agente NO recibe la lista completa de skills en su prompt; en cada mensaje un RAG local decide qué skill(s) cargar — el cuerpo completo si hay match fuerte, una sugerencia si hay match medio, nada si no aplica. Se reevalúa cada turno: una skill que dejó de ser relevante desaparece del contexto."
|
|
95
|
+
>
|
|
96
|
+
<div className="space-y-4">
|
|
97
|
+
<Field
|
|
98
|
+
label="Activar inspector"
|
|
99
|
+
hint="Apagado = comportamiento clásico (lista de slugs + sugerencia pasiva). Encendido = el RAG decide por turno."
|
|
100
|
+
>
|
|
101
|
+
<Switch
|
|
102
|
+
checked={cfg.enabled}
|
|
103
|
+
disabled={busy}
|
|
104
|
+
onChange={(v) => apply({ enabled: v })}
|
|
105
|
+
label={cfg.enabled ? "Encendido" : "Apagado"}
|
|
106
|
+
/>
|
|
107
|
+
</Field>
|
|
108
|
+
|
|
109
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
110
|
+
<Badge tone={idx.count > 0 ? "success" : "warning"}>
|
|
111
|
+
Índice: {idx.count} skills
|
|
112
|
+
</Badge>
|
|
113
|
+
<Badge tone="muted">{idx.embedder || "sin indexar"}</Badge>
|
|
114
|
+
{idx.dim ? <Badge tone="muted">dim {idx.dim}</Badge> : null}
|
|
115
|
+
{idx.updated_at ? (
|
|
116
|
+
<span className="text-xs text-muted-foreground">
|
|
117
|
+
actualizado {new Date(idx.updated_at).toLocaleString()}
|
|
118
|
+
</span>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="flex flex-wrap items-center gap-3 pt-1">
|
|
123
|
+
<Button variant="secondary" onClick={() => runIndex(false)} loading={busy}>
|
|
124
|
+
<RefreshCw size={14} /> Reindexar
|
|
125
|
+
</Button>
|
|
126
|
+
<Button variant="secondary" onClick={() => runIndex(true)} loading={busy}>
|
|
127
|
+
<RefreshCw size={14} /> Reindexar (forzado)
|
|
128
|
+
</Button>
|
|
129
|
+
<span className="text-xs text-muted-foreground">
|
|
130
|
+
El embedder sale de Memoria (RAG). Local con Ollama, u offline si no hay proveedor.
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</Section>
|
|
135
|
+
|
|
136
|
+
<Section
|
|
137
|
+
title="Umbrales y límites"
|
|
138
|
+
description="Ajustá qué tan agresivo es el inspector. Subir los umbrales = menos falsos positivos pero más riesgo de perderse una skill; bajarlos = lo contrario."
|
|
139
|
+
>
|
|
140
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
141
|
+
{KNOBS.map((k) => (
|
|
142
|
+
<Field key={k.key} label={k.label} hint={k.hint}>
|
|
143
|
+
<Input
|
|
144
|
+
type="number"
|
|
145
|
+
step={k.step}
|
|
146
|
+
min={k.min}
|
|
147
|
+
max={k.max}
|
|
148
|
+
defaultValue={String(cfg[k.key])}
|
|
149
|
+
disabled={busy}
|
|
150
|
+
onBlur={(ev) => {
|
|
151
|
+
const n = Number(ev.target.value);
|
|
152
|
+
if (Number.isFinite(n) && n !== cfg[k.key]) apply({ [k.key]: n });
|
|
153
|
+
}}
|
|
154
|
+
className="max-w-[12rem]"
|
|
155
|
+
/>
|
|
156
|
+
</Field>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</Section>
|
|
160
|
+
|
|
161
|
+
<Section
|
|
162
|
+
title="Probar (dry-run)"
|
|
163
|
+
description="Escribí un mensaje como lo haría un usuario y mirá qué skills cargaría/sugeriría el inspector — sin llamar al modelo. Fuerza el inspector activo aunque esté apagado arriba."
|
|
164
|
+
>
|
|
165
|
+
<div className="space-y-3">
|
|
166
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
167
|
+
<Input
|
|
168
|
+
value={probe}
|
|
169
|
+
placeholder="ej: necesito crear un video promocional con voz en off"
|
|
170
|
+
disabled={busy}
|
|
171
|
+
onChange={(ev) => setProbe(ev.target.value)}
|
|
172
|
+
onKeyDown={(ev) => { if (ev.key === "Enter") runProbe(); }}
|
|
173
|
+
className="max-w-xl flex-1"
|
|
174
|
+
/>
|
|
175
|
+
<Button variant="primary" onClick={runProbe} loading={busy}>
|
|
176
|
+
<Wand2 size={14} /> Probar
|
|
177
|
+
</Button>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{probeResult && (
|
|
181
|
+
<div className="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">
|
|
182
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
183
|
+
<Sparkles size={14} className="text-muted-foreground" />
|
|
184
|
+
<span className="text-muted-foreground">{probeResult.embedder || "—"}</span>
|
|
185
|
+
{probeResult.jit ? <Badge tone="warning">JIT (índice vacío)</Badge> : null}
|
|
186
|
+
{probeResult.reason && !probeResult.loaded?.length && !probeResult.hinted?.length ? (
|
|
187
|
+
<Badge tone="muted">{probeResult.reason}</Badge>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{probeResult.loaded?.length ? (
|
|
192
|
+
<div className="mb-1">
|
|
193
|
+
<span className="text-muted-foreground">Cargadas: </span>
|
|
194
|
+
{probeResult.loaded.map((s) => (
|
|
195
|
+
<Badge key={s} tone="success" className="mr-1">{s}</Badge>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
) : null}
|
|
199
|
+
|
|
200
|
+
{probeResult.hinted?.length ? (
|
|
201
|
+
<div className="mb-1">
|
|
202
|
+
<span className="text-muted-foreground">Sugeridas: </span>
|
|
203
|
+
{probeResult.hinted.map((s) => (
|
|
204
|
+
<Badge key={s} tone="info" className="mr-1">{s}</Badge>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
) : null}
|
|
208
|
+
|
|
209
|
+
{probeResult.scored?.length ? (
|
|
210
|
+
<div className="mt-2 space-y-0.5 font-mono text-xs text-muted-foreground">
|
|
211
|
+
{probeResult.scored.map((s) => (
|
|
212
|
+
<div key={s.slug}>{s.sim.toFixed(3)} {s.slug}</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
) : null}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</Section>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
|
-
import { SuperAgent, Agents } from "../lib/api";
|
|
2
|
+
import { SuperAgent, Agents, Conversations } from "../lib/api";
|
|
3
3
|
import type { ChatStreamEvent, ChatUsage, ConversationMessage } from "../types/daemon";
|
|
4
4
|
|
|
5
5
|
export type ToolStatus = "running" | "done" | "error" | "deduped";
|
|
@@ -32,6 +32,13 @@ export interface ChatMsg {
|
|
|
32
32
|
usage?: ChatUsage;
|
|
33
33
|
/** Operational notes (engine fallbacks, retries, suppressions). */
|
|
34
34
|
notes?: string[];
|
|
35
|
+
/** Skill Inspector decision for this turn (when the feature is on): which
|
|
36
|
+
* skills the per-turn RAG loaded inline vs merely hinted. */
|
|
37
|
+
inspector?: {
|
|
38
|
+
embedder?: string;
|
|
39
|
+
loaded?: string[];
|
|
40
|
+
hinted?: string[];
|
|
41
|
+
};
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export interface SendOptions {
|
|
@@ -46,7 +53,14 @@ export interface UseChatResult {
|
|
|
46
53
|
send: (text: string, opts?: SendOptions) => Promise<void>;
|
|
47
54
|
stop: () => void;
|
|
48
55
|
clear: () => void;
|
|
56
|
+
/** Load a persisted conversation as history and bind subsequent sends to it.
|
|
57
|
+
* Only supported for project agents (super-agent conversations aren't
|
|
58
|
+
* persisted per-file). Pass `null` to drop the binding without clearing. */
|
|
59
|
+
load: (agentSlug: string, conversationId: string) => Promise<void>;
|
|
49
60
|
streaming: boolean;
|
|
61
|
+
/** Conversation id we're bound to, if any. Lets callers reflect "live vs
|
|
62
|
+
* loaded" state in the UI. */
|
|
63
|
+
conversationId: string | undefined;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/** Concatenate the text parts of a message (for clipboard). */
|
|
@@ -138,6 +152,18 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
138
152
|
return withNote(`retry (${ev.reason || "?"})`);
|
|
139
153
|
case "tools_suppressed":
|
|
140
154
|
return withNote(`tools suppressed: ${(ev.tools || []).join(", ")}`);
|
|
155
|
+
case "skill_inspector": {
|
|
156
|
+
const insp = ev.inspector;
|
|
157
|
+
if (!insp || (!insp.loaded?.length && !insp.hinted?.length)) return turn;
|
|
158
|
+
return {
|
|
159
|
+
...turn,
|
|
160
|
+
inspector: {
|
|
161
|
+
embedder: insp.embedder,
|
|
162
|
+
loaded: insp.loaded || [],
|
|
163
|
+
hinted: insp.hinted || [],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
141
167
|
case "assistant_text":
|
|
142
168
|
return ev.text ? { ...turn, parts: [...turn.parts, { kind: "text", text: ev.text }] } : turn;
|
|
143
169
|
case "tool_start":
|
|
@@ -216,6 +242,7 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
216
242
|
export function useChat(pid: string, onError?: (msg: string) => void): UseChatResult {
|
|
217
243
|
const [msgs, setMsgs] = useState<ChatMsg[]>([]);
|
|
218
244
|
const [streaming, setStreaming] = useState(false);
|
|
245
|
+
const [conversationId, setConversationId] = useState<string | undefined>(undefined);
|
|
219
246
|
const abortRef = useRef<AbortController | null>(null);
|
|
220
247
|
const convoRef = useRef<string | undefined>(undefined);
|
|
221
248
|
|
|
@@ -264,8 +291,10 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
264
291
|
prompt: trimmed,
|
|
265
292
|
conversation_id: convoRef.current,
|
|
266
293
|
model: opts.model || undefined,
|
|
294
|
+
channel: "web",
|
|
267
295
|
});
|
|
268
296
|
convoRef.current = out.conversation_id;
|
|
297
|
+
setConversationId(out.conversation_id);
|
|
269
298
|
patchLast((m) => ({
|
|
270
299
|
...m,
|
|
271
300
|
pending: false,
|
|
@@ -315,8 +344,31 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
315
344
|
const clear = useCallback(() => {
|
|
316
345
|
if (streaming) return;
|
|
317
346
|
convoRef.current = undefined;
|
|
347
|
+
setConversationId(undefined);
|
|
318
348
|
setMsgs([]);
|
|
319
349
|
}, [streaming]);
|
|
320
350
|
|
|
321
|
-
|
|
351
|
+
const load = useCallback(
|
|
352
|
+
async (agentSlug: string, conversationId: string) => {
|
|
353
|
+
if (streaming) return;
|
|
354
|
+
try {
|
|
355
|
+
const detail = await Conversations.get(pid, agentSlug, conversationId);
|
|
356
|
+
const loaded: ChatMsg[] = detail.messages
|
|
357
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
358
|
+
.map((m) => ({
|
|
359
|
+
role: m.role as "user" | "assistant",
|
|
360
|
+
parts: [{ kind: "text", text: m.content }],
|
|
361
|
+
ts: m.ts || new Date().toISOString(),
|
|
362
|
+
}));
|
|
363
|
+
convoRef.current = conversationId;
|
|
364
|
+
setConversationId(conversationId);
|
|
365
|
+
setMsgs(loaded);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
onError?.((e as Error)?.message || "could not load conversation");
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
[pid, streaming, onError],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
return { msgs, send, stop, clear, load, streaming, conversationId };
|
|
322
374
|
}
|
|
@@ -275,7 +275,6 @@ export const en = {
|
|
|
275
275
|
tasks: "Tasks",
|
|
276
276
|
mcps: "MCPs",
|
|
277
277
|
vars: "Variables",
|
|
278
|
-
threads: "Chats",
|
|
279
278
|
logs: "Logs",
|
|
280
279
|
memories: "Memories",
|
|
281
280
|
},
|
|
@@ -301,6 +300,7 @@ export const en = {
|
|
|
301
300
|
subtitle: "Direct conversations with project agents. The super-agent does not intervene.",
|
|
302
301
|
superagent_title: "Chat with {persona}",
|
|
303
302
|
superagent_subtitle: "Chat with {persona} — the APX super-agent. Can use tools (projects, tasks, mcps, agents).",
|
|
303
|
+
loaded_subtitle: "Loaded conversation with {slug}. Sending will append to this thread.",
|
|
304
304
|
empty: "Send a message to start the conversation.",
|
|
305
305
|
placeholder: "Type something and press enter to send (shift+enter = new line)",
|
|
306
306
|
send: "Send",
|
|
@@ -316,6 +316,16 @@ export const en = {
|
|
|
316
316
|
model_label: "model",
|
|
317
317
|
model_hint: "e.g. openai:gpt-5, groq:llama-3.3-70b-versatile",
|
|
318
318
|
master_label: "Master agent",
|
|
319
|
+
list: {
|
|
320
|
+
title: "Chats",
|
|
321
|
+
new: "New",
|
|
322
|
+
search: "Search chats…",
|
|
323
|
+
all_agents: "All agents",
|
|
324
|
+
empty: "No conversations yet. Start one from the right.",
|
|
325
|
+
count: "{n} total",
|
|
326
|
+
live_with: "Live · {slug}",
|
|
327
|
+
live_subtitle: "In-memory session",
|
|
328
|
+
},
|
|
319
329
|
},
|
|
320
330
|
|
|
321
331
|
tasks: {
|
|
@@ -908,6 +918,7 @@ export const en = {
|
|
|
908
918
|
ollama_title: "Ollama (local)",
|
|
909
919
|
openai_title: "OpenAI",
|
|
910
920
|
gemini_title: "Gemini",
|
|
921
|
+
compaction_title: "History compaction",
|
|
911
922
|
},
|
|
912
923
|
|
|
913
924
|
router_panel: {
|
|
@@ -276,7 +276,6 @@ export const es = {
|
|
|
276
276
|
tasks: "Tasks",
|
|
277
277
|
mcps: "MCPs",
|
|
278
278
|
vars: "Variables",
|
|
279
|
-
threads: "Chats",
|
|
280
279
|
logs: "Logs",
|
|
281
280
|
memories: "Memorias",
|
|
282
281
|
},
|
|
@@ -302,6 +301,7 @@ export const es = {
|
|
|
302
301
|
subtitle: "Chat directo con el agente del proyecto.",
|
|
303
302
|
superagent_title: "Chat con {persona}",
|
|
304
303
|
superagent_subtitle: "Chat con {persona} — el super-agente APX. Puede usar tools (proyectos, tasks, mcps, agentes).",
|
|
304
|
+
loaded_subtitle: "Conversación cargada con {slug}. Lo que mandes se agrega a este chat.",
|
|
305
305
|
empty: "Mandá un mensaje para arrancar la conversación.",
|
|
306
306
|
placeholder: "Escribí algo y enter para enviar (shift+enter = nueva línea)",
|
|
307
307
|
send: "Enviar",
|
|
@@ -317,6 +317,16 @@ export const es = {
|
|
|
317
317
|
model_label: "modelo",
|
|
318
318
|
model_hint: "ej. openai:gpt-5, groq:llama-3.3-70b-versatile",
|
|
319
319
|
master_label: "Agente master",
|
|
320
|
+
list: {
|
|
321
|
+
title: "Chats",
|
|
322
|
+
new: "Nuevo",
|
|
323
|
+
search: "Buscar chats…",
|
|
324
|
+
all_agents: "Todos los agentes",
|
|
325
|
+
empty: "No hay conversaciones todavía. Arrancá una desde la derecha.",
|
|
326
|
+
count: "{n} en total",
|
|
327
|
+
live_with: "Live · {slug}",
|
|
328
|
+
live_subtitle: "Sesión en memoria",
|
|
329
|
+
},
|
|
320
330
|
},
|
|
321
331
|
|
|
322
332
|
tasks: {
|
|
@@ -906,6 +916,7 @@ export const es = {
|
|
|
906
916
|
ollama_title: "Ollama (local)",
|
|
907
917
|
openai_title: "OpenAI",
|
|
908
918
|
gemini_title: "Gemini",
|
|
919
|
+
compaction_title: "Compactación de historial",
|
|
909
920
|
},
|
|
910
921
|
|
|
911
922
|
router_panel: {
|
|
@@ -10,7 +10,7 @@ export const Agents = {
|
|
|
10
10
|
http.patch<AgentEntry>(`/projects/${pid}/agents/${encodeURIComponent(slug)}`, body),
|
|
11
11
|
remove: (pid: string, slug: string) =>
|
|
12
12
|
http.del<{ ok: boolean }>(`/projects/${pid}/agents/${encodeURIComponent(slug)}`),
|
|
13
|
-
chat: (pid: string, slug: string, body: { prompt: string; conversation_id?: string; model?: string }) =>
|
|
13
|
+
chat: (pid: string, slug: string, body: { prompt: string; conversation_id?: string; model?: string; channel?: string }) =>
|
|
14
14
|
http.post<{ conversation_id: string; text: string; usage?: unknown; engine: string }>(
|
|
15
15
|
`/projects/${pid}/agents/${encodeURIComponent(slug)}/chat`,
|
|
16
16
|
body,
|
|
@@ -11,6 +11,55 @@ export type SkillsList = {
|
|
|
11
11
|
skills: SkillEntry[];
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export interface InspectorConfig {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
load_threshold: number;
|
|
17
|
+
hint_threshold: number;
|
|
18
|
+
margin: number;
|
|
19
|
+
max_loaded: number;
|
|
20
|
+
max_hints: number;
|
|
21
|
+
prompt_floor: number;
|
|
22
|
+
body_char_cap: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IndexStatus {
|
|
26
|
+
count: number;
|
|
27
|
+
embedder: string | null;
|
|
28
|
+
dim: number | null;
|
|
29
|
+
updated_at: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface InspectorState {
|
|
33
|
+
config: InspectorConfig;
|
|
34
|
+
defaults: InspectorConfig;
|
|
35
|
+
keys: string[];
|
|
36
|
+
index: IndexStatus;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IndexResult {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
embedder: string;
|
|
42
|
+
dim: number;
|
|
43
|
+
planned: { missing: number; stale: number; gone: number; total: number };
|
|
44
|
+
changed: { added: number; refreshed: number; removed: number; kept: number };
|
|
45
|
+
index: IndexStatus;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface InspectTrace {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
embedder?: string;
|
|
52
|
+
scored?: { slug: string; sim: number }[];
|
|
53
|
+
loaded?: string[];
|
|
54
|
+
hinted?: string[];
|
|
55
|
+
jit?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface InspectResult {
|
|
59
|
+
trace: InspectTrace;
|
|
60
|
+
contextNote: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
14
63
|
export const Skills = {
|
|
15
64
|
/**
|
|
16
65
|
* List installed skills (bundled + user + optional project-scoped). The
|
|
@@ -22,4 +71,25 @@ export const Skills = {
|
|
|
22
71
|
? `/skills?project_path=${encodeURIComponent(projectPath)}`
|
|
23
72
|
: "/skills",
|
|
24
73
|
),
|
|
74
|
+
|
|
75
|
+
/** Skill Inspector config + index status. */
|
|
76
|
+
inspector: () => http.get<InspectorState>("/skills/inspector"),
|
|
77
|
+
|
|
78
|
+
/** Patch inspector config (toggle / tune thresholds). */
|
|
79
|
+
updateInspector: (patch: Partial<InspectorConfig>) =>
|
|
80
|
+
http.put<{ ok: boolean; config: InspectorConfig; index: IndexStatus }>(
|
|
81
|
+
"/skills/inspector",
|
|
82
|
+
patch,
|
|
83
|
+
),
|
|
84
|
+
|
|
85
|
+
/** (Re)build the inspector vector index. */
|
|
86
|
+
index: (body: { project_path?: string; force?: boolean } = {}) =>
|
|
87
|
+
http.post<IndexResult>("/skills/index", body),
|
|
88
|
+
|
|
89
|
+
/** Dry-run the inspector for a prompt (forces enabled). */
|
|
90
|
+
inspect: (prompt: string, projectPath?: string) =>
|
|
91
|
+
http.post<InspectResult>("/skills/inspect", {
|
|
92
|
+
prompt,
|
|
93
|
+
project_path: projectPath,
|
|
94
|
+
}),
|
|
25
95
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { useParams, Routes, Route, useLocation, useNavigate } from "react-router-dom";
|
|
2
|
+
import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
4
|
Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
|
|
5
5
|
MessagesSquare, Send, KeyRound,
|
|
@@ -23,7 +23,6 @@ import { RoutinesTab } from "./project/RoutinesTab";
|
|
|
23
23
|
import { TasksTab } from "./project/TasksTab";
|
|
24
24
|
import { McpsTab } from "./project/McpsTab";
|
|
25
25
|
import { VarsTab } from "./project/VarsTab";
|
|
26
|
-
import { ThreadsTab } from "./project/ThreadsTab";
|
|
27
26
|
import { ChatTab } from "./project/ChatTab";
|
|
28
27
|
import { TelegramTab } from "./project/TelegramTab";
|
|
29
28
|
import { MemoriesTab } from "./project/MemoriesTab";
|
|
@@ -31,7 +30,7 @@ import { AgentDetailScreen } from "./project/AgentDetailScreen";
|
|
|
31
30
|
|
|
32
31
|
type NavKey =
|
|
33
32
|
| "" | "chat" | "config" | "telegram"
|
|
34
|
-
| "agents" | "routines" | "tasks" | "mcps" | "vars" | "
|
|
33
|
+
| "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories";
|
|
35
34
|
|
|
36
35
|
export function ProjectScreen() {
|
|
37
36
|
const navigate = useNavigate();
|
|
@@ -83,7 +82,6 @@ export function ProjectScreen() {
|
|
|
83
82
|
{ key: "", label: t("project.nav.overview"), icon: FolderKanban },
|
|
84
83
|
{ key: "telegram", label: t("project.nav.telegram"), icon: Send },
|
|
85
84
|
{ key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
|
|
86
|
-
{ key: "threads", label: t("project.nav.threads"), icon: MessagesSquare },
|
|
87
85
|
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
88
86
|
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
89
87
|
],
|
|
@@ -146,7 +144,7 @@ export function ProjectScreen() {
|
|
|
146
144
|
<Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
|
|
147
145
|
<Route path="mcps" element={<McpsTab pid={pid} />} />
|
|
148
146
|
<Route path="vars" element={<VarsTab pid={pid} />} />
|
|
149
|
-
<Route path="threads" element={<
|
|
147
|
+
<Route path="threads" element={<Navigate to={`/p/${pid}/chat`} replace />} />
|
|
150
148
|
<Route path="chat" element={<ChatTab pid={pid} />} />
|
|
151
149
|
<Route path="*" element={<Overview pid={pid} />} />
|
|
152
150
|
</Routes>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
-
Bot, Cpu, Database, KeyRound, MessageCircle, Palette, ScrollText, Send, Smartphone, User,
|
|
4
|
+
Bot, Cpu, Database, KeyRound, MessageCircle, Palette, ScrollText, Send, Smartphone, Sparkles, User,
|
|
5
5
|
} from "lucide-react";
|
|
6
6
|
import { useNavCollapse, type TabSection } from "../components/common/TabNav";
|
|
7
7
|
import { TabLayout } from "../components/common/TabLayout";
|
|
8
8
|
import { IdentityPanel } from "../components/settings/IdentityPanel";
|
|
9
9
|
import { SuperAgentPanel } from "../components/settings/SuperAgentPanel";
|
|
10
10
|
import { MemoryPanel } from "../components/settings/MemoryPanel";
|
|
11
|
+
import { SkillsInspectorPanel } from "../components/settings/SkillsInspectorPanel";
|
|
11
12
|
import { ModelsTab } from "./base/ModelsTab";
|
|
12
13
|
import { TelegramSettingsTabs } from "../components/settings/TelegramSettingsTabs";
|
|
13
14
|
import { DevicesPanel } from "../components/settings/DevicesPanel";
|
|
@@ -17,7 +18,7 @@ import { STORAGE } from "../constants";
|
|
|
17
18
|
import { t } from "../i18n";
|
|
18
19
|
|
|
19
20
|
type TabKey =
|
|
20
|
-
| "identity" | "super_agent" | "engines" | "memory" | "telegram" | "devices" | "appearance" | "advanced";
|
|
21
|
+
| "identity" | "super_agent" | "engines" | "memory" | "skills" | "telegram" | "devices" | "appearance" | "advanced";
|
|
21
22
|
|
|
22
23
|
const SECTIONS: TabSection[] = [
|
|
23
24
|
{
|
|
@@ -33,6 +34,7 @@ const SECTIONS: TabSection[] = [
|
|
|
33
34
|
{ key: "super_agent", label: t("settings.tabs.super_agent"), icon: Bot },
|
|
34
35
|
{ key: "engines", label: t("settings.tabs.engines"), icon: Cpu },
|
|
35
36
|
{ key: "memory", label: "Memoria (RAG)", icon: Database },
|
|
37
|
+
{ key: "skills", label: "Skills (RAG)", icon: Sparkles },
|
|
36
38
|
],
|
|
37
39
|
},
|
|
38
40
|
{
|
|
@@ -50,15 +52,18 @@ const SECTIONS: TabSection[] = [
|
|
|
50
52
|
},
|
|
51
53
|
];
|
|
52
54
|
|
|
53
|
-
// Tabs whose content
|
|
54
|
-
//
|
|
55
|
-
|
|
55
|
+
// Tabs whose content lays out multiple top-level sections in a two-column grid
|
|
56
|
+
// on xl (and so wants full available width). Single-section panels (identity,
|
|
57
|
+
// super agent, devices, advanced) keep a cosier reading width so wide displays
|
|
58
|
+
// don't blow form fields up to absurd widths.
|
|
59
|
+
const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "appearance"]);
|
|
56
60
|
|
|
57
61
|
const PANELS: Record<TabKey, () => ReactElement> = {
|
|
58
62
|
identity: () => <IdentityPanel />,
|
|
59
63
|
super_agent: () => <SuperAgentPanel />,
|
|
60
64
|
engines: () => <ModelsTab />,
|
|
61
65
|
memory: () => <MemoryPanel />,
|
|
66
|
+
skills: () => <SkillsInspectorPanel />,
|
|
62
67
|
telegram: () => <TelegramSettingsTabs />,
|
|
63
68
|
devices: () => <DevicesPanel />,
|
|
64
69
|
appearance: () => <AppearancePanel />,
|
|
@@ -81,7 +86,7 @@ export function SettingsScreen() {
|
|
|
81
86
|
onChange={(k) => navigate(k === "identity" ? "/settings" : `/settings/${pathFromTab(k as TabKey)}`)}
|
|
82
87
|
collapsed={collapsed}
|
|
83
88
|
onToggleCollapse={toggle}
|
|
84
|
-
contentClassName={`
|
|
89
|
+
contentClassName={`w-full ${WIDE_TABS.has(active) ? "" : "mx-auto max-w-3xl"} space-y-6 p-6 pt-3`}
|
|
85
90
|
testId={`settings-tab-${active}`}
|
|
86
91
|
>
|
|
87
92
|
<Panel />
|
|
@@ -95,6 +100,7 @@ function tabFromPath(pathname: string): TabKey {
|
|
|
95
100
|
case "super-agent": return "super_agent";
|
|
96
101
|
case "engines": return "engines";
|
|
97
102
|
case "memory": return "memory";
|
|
103
|
+
case "skills": return "skills";
|
|
98
104
|
case "telegram": return "telegram";
|
|
99
105
|
case "devices": return "devices";
|
|
100
106
|
case "appearance": return "appearance";
|
|
@@ -120,7 +120,7 @@ export function VoiceScreen() {
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
return (
|
|
123
|
-
<div className="
|
|
123
|
+
<div className="p-6" data-testid="screen-voice">
|
|
124
124
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
125
125
|
{/* Left: TTS providers */}
|
|
126
126
|
<Section
|