@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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +1 -1
  3. package/src/core/agent/build-agent-system.js +134 -58
  4. package/src/core/agent/channels/voice-context.js +4 -4
  5. package/src/core/agent/prompt-builder.js +176 -123
  6. package/src/core/agent/prompts/channels/code.md +12 -10
  7. package/src/core/agent/prompts/channels/desktop.md +5 -32
  8. package/src/core/agent/prompts/channels/telegram.md +4 -15
  9. package/src/core/agent/prompts/channels/web_code.md +11 -11
  10. package/src/core/agent/prompts/core/agent-base.md +24 -0
  11. package/src/core/agent/prompts/core/project-agent.md +11 -0
  12. package/src/core/agent/prompts/core/super-agent.md +21 -0
  13. package/src/core/agent/prompts/discipline/action.md +10 -0
  14. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  15. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  16. package/src/core/agent/self-memory.js +43 -1
  17. package/src/core/agent/skills/index-store.js +307 -0
  18. package/src/core/agent/skills/index.js +15 -1
  19. package/src/core/agent/skills/inspector.js +317 -0
  20. package/src/core/agent/super-agent.js +7 -1
  21. package/src/core/agent/tools/handlers/_git.js +50 -0
  22. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  23. package/src/core/agent/tools/handlers/git-log.js +38 -0
  24. package/src/core/agent/tools/handlers/git-show.js +34 -0
  25. package/src/core/agent/tools/handlers/git-status.js +61 -0
  26. package/src/core/agent/tools/names.js +31 -0
  27. package/src/core/agent/tools/registry.js +36 -5
  28. package/src/core/config/index.js +21 -0
  29. package/src/core/runtime-skills/apx/SKILL.md +27 -39
  30. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
  31. package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
  32. package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
  33. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
  34. package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
  35. package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
  36. package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
  37. package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
  38. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
  39. package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
  40. package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
  41. package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
  42. package/src/core/stores/conversations.js +27 -2
  43. package/src/host/daemon/api/exec.js +2 -0
  44. package/src/host/daemon/api/skills.js +140 -6
  45. package/src/host/daemon/api/super-agent.js +56 -1
  46. package/src/host/daemon/index.js +17 -0
  47. package/src/interfaces/cli/branding.js +53 -0
  48. package/src/interfaces/cli/commands/skills.js +254 -0
  49. package/src/interfaces/cli/index.js +84 -2
  50. package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +1 -0
  51. package/src/interfaces/web/dist/assets/index-DJKA763h.js +628 -0
  52. package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +1 -0
  53. package/src/interfaces/web/dist/index.html +2 -2
  54. package/src/interfaces/web/src/App.tsx +0 -1
  55. package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
  56. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
  57. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  58. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +69 -1
  59. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  60. package/src/interfaces/web/src/hooks/useChat.ts +54 -2
  61. package/src/interfaces/web/src/i18n/en.ts +12 -1
  62. package/src/interfaces/web/src/i18n/es.ts +12 -1
  63. package/src/interfaces/web/src/lib/api/agents.ts +1 -1
  64. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  65. package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
  66. package/src/interfaces/web/src/screens/SettingsScreen.tsx +12 -6
  67. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  68. package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
  69. package/src/interfaces/web/src/types/daemon.ts +10 -0
  70. package/src/core/agent/prompts/action-discipline.md +0 -24
  71. package/src/core/agent/prompts/super-agent-base.md +0 -42
  72. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
  73. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
  74. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
  75. 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
- return { msgs, send, stop, clear, streaming };
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" | "threads" | "logs" | "memories";
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={<ThreadsTab pid={pid} />} />
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 (model router + provider grid, telegram tabs) benefits
54
- // from a wider container; the rest keep the cosier 3xl reading width.
55
- const WIDE_TABS = new Set<TabKey>(["engines", "telegram"]);
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={`mx-auto w-full ${WIDE_TABS.has(active) ? "max-w-6xl" : "max-w-3xl"} space-y-6 p-6 pt-3`}
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="mx-auto max-w-6xl p-6" data-testid="screen-voice">
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