@agentprojectcontext/apx 1.36.0 → 1.38.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 (79) hide show
  1. package/README.md +81 -3
  2. package/package.json +1 -1
  3. package/src/core/mascot.js +80 -80
  4. package/src/host/daemon/api/agents.js +6 -0
  5. package/src/host/daemon/api/conversations.js +9 -2
  6. package/src/host/daemon/api/web.js +20 -1
  7. package/src/host/daemon/desktop-ws.js +31 -0
  8. package/src/host/daemon/index.js +12 -2
  9. package/src/interfaces/cli/commands/agent.js +20 -0
  10. package/src/interfaces/cli/commands/chat.js +15 -6
  11. package/src/interfaces/cli/commands/identity.js +20 -1
  12. package/src/interfaces/cli/commands/update.js +2 -0
  13. package/src/interfaces/cli/index.js +14 -0
  14. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
  15. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
  16. package/src/interfaces/web/dist/assets/index-hwxuTPcK.css +1 -0
  17. package/src/interfaces/web/dist/index.html +2 -2
  18. package/src/interfaces/web/src/App.tsx +20 -9
  19. package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
  20. package/src/interfaces/web/src/components/Roby.tsx +96 -0
  21. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
  22. package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
  23. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
  24. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
  25. package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
  26. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
  27. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
  28. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
  29. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
  30. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
  31. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
  32. package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
  33. package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
  34. package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
  35. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
  36. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
  37. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
  38. package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
  39. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  40. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
  41. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
  42. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
  43. package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
  44. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
  45. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
  46. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
  47. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
  48. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
  49. package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
  50. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
  51. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
  52. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
  53. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
  54. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
  55. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
  56. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
  57. package/src/interfaces/web/src/hooks/useChat.ts +6 -5
  58. package/src/interfaces/web/src/i18n/en.ts +519 -2
  59. package/src/interfaces/web/src/i18n/es.ts +519 -2
  60. package/src/interfaces/web/src/i18n/index.ts +1 -1
  61. package/src/interfaces/web/src/lib/api/voice.ts +5 -5
  62. package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
  63. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  64. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
  65. package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
  66. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
  67. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
  68. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
  69. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
  70. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
  71. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
  72. package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
  73. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
  74. package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
  75. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
  76. package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
  77. package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +0 -1
  78. package/src/interfaces/web/dist/assets/index-DJKA763h.js +0 -628
  79. package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +0 -1
@@ -18,7 +18,7 @@ import { AgentBrainGraph, type BrainNode } from "./AgentBrainGraph";
18
18
  type TabKey = "overview" | "memories" | "records" | "sleep" | "brain" | "config";
19
19
  function buildTabs(): { key: TabKey; label: string; icon: typeof Bot }[] {
20
20
  return [
21
- { key: "overview", label: "Explorer", icon: Gauge },
21
+ { key: "overview", label: t("agents_ui.tab_explorer"), icon: Gauge },
22
22
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
23
23
  { key: "records", label: t("project.agent_detail.records_title"), icon: Activity },
24
24
  { key: "sleep", label: t("project.agent_detail.sleep_title"), icon: Heart },
@@ -27,15 +27,16 @@ function buildTabs(): { key: TabKey; label: string; icon: typeof Bot }[] {
27
27
  ];
28
28
  }
29
29
 
30
- const TYPE_OPTIONS = [
31
- { value: "", label: "— sin tipo —" },
32
- { value: "orchestrator", label: "Orchestrator", description: "Coordina al equipo y delega." },
33
- { value: "specialist", label: "Specialist", description: "Experto en un dominio; ejecuta tareas." },
34
- { value: "assistant", label: "Assistant", description: "Ayudante conversacional." },
35
- { value: "worker", label: "Worker", description: "Corre tareas autónomas." },
36
- { value: "monitor", label: "Monitor", description: "Observa estado y reporta." },
37
- ];
38
- // Note: TYPE_OPTIONS labels are intentionally not externalized — they are proper nouns / technical terms
30
+ function typeOptions() {
31
+ return [
32
+ { value: "", label: t("agents_ui.type_none") },
33
+ { value: "orchestrator", label: t("agents_ui.type_orchestrator"), description: t("agents_ui.type_orchestrator_desc") },
34
+ { value: "specialist", label: t("agents_ui.type_specialist"), description: t("agents_ui.type_specialist_desc") },
35
+ { value: "assistant", label: t("agents_ui.type_assistant"), description: t("agents_ui.type_assistant_desc") },
36
+ { value: "worker", label: t("agents_ui.type_worker"), description: t("agents_ui.type_worker_desc") },
37
+ { value: "monitor", label: t("agents_ui.type_monitor"), description: t("agents_ui.type_monitor_desc") },
38
+ ];
39
+ }
39
40
  const csv = (s: string) => s.split(",").map((x) => x.trim()).filter(Boolean);
40
41
 
41
42
  const routinesForAgent = (rs: RoutineEntry[], slug: string) =>
@@ -122,10 +123,10 @@ export function AgentDetailScreen({ pid }: { pid: string }) {
122
123
  {tab === "overview" && (
123
124
  <div className="space-y-4">
124
125
  <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
125
- <Stat label="Threads" value={threads.data?.length ?? 0} icon={MessagesSquare} />
126
- <Stat label="Records" value={records.data?.length ?? 0} icon={Activity} />
127
- <Stat label="Tasks" value={myTasks.length} icon={Gauge} />
128
- <Stat label="Heartbeats" value={myRoutines.length} icon={Heart} />
126
+ <Stat label={t("agents_ui.stat_threads")} value={threads.data?.length ?? 0} icon={MessagesSquare} />
127
+ <Stat label={t("agents_ui.stat_records")} value={records.data?.length ?? 0} icon={Activity} />
128
+ <Stat label={t("agents_ui.stat_tasks")} value={myTasks.length} icon={Gauge} />
129
+ <Stat label={t("agents_ui.stat_heartbeats")} value={myRoutines.length} icon={Heart} />
129
130
  </div>
130
131
  <div className="grid gap-3 sm:grid-cols-2">
131
132
  <Section title={t("agent_detail_extra.skills_title")} description="">
@@ -243,10 +244,10 @@ function AgentConfigForm({
243
244
  };
244
245
 
245
246
  return (
246
- <Section title={t("project.agent_detail.config_title")} description={`.apc/agents/${agent.slug}.md — definición (frontmatter + system prompt).`}>
247
+ <Section title={t("project.agent_detail.config_title")} description={`.apc/agents/${agent.slug}.md — ${t("agents_ui.config_def_desc")}`}>
247
248
  <div className="space-y-3">
248
249
  <div className="grid grid-cols-2 gap-3">
249
- <Field label={t("project.agent_detail.type_label")}><UiSelect value={type} onChange={setType} options={TYPE_OPTIONS} /></Field>
250
+ <Field label={t("project.agent_detail.type_label")}><UiSelect value={type} onChange={setType} options={typeOptions()} /></Field>
250
251
  <Field label={t("project.agent_detail.area_label")} hint={t("project.agent_detail.area_hint")}><Input value={area} onChange={(e) => setArea(e.target.value)} placeholder={t("project.agent_detail.area_ph")} /></Field>
251
252
  </div>
252
253
  <div className="grid grid-cols-2 gap-3">
@@ -302,7 +303,7 @@ function MemoryEditor({ pid, slug, initial, onSaved }: { pid: string; slug: stri
302
303
  finally { setBusy(false); }
303
304
  };
304
305
  return (
305
- <Section title={t("project.agent_detail.memory_title")} description={`~/.apx/projects/<id>/agents/${slug}/memory.md — hechos durables que el agente recuerda.`}>
306
+ <Section title={t("project.agent_detail.memory_title")} description={`~/.apx/projects/<id>/agents/${slug}/memory.md — ${t("agents_ui.memory_durable_desc")}`}>
306
307
  <Textarea rows={16} className="font-mono text-xs" value={value} onChange={(e) => setValue(e.target.value)} placeholder={t("project.agent_detail.memory_empty")} />
307
308
  <div className="mt-2 flex items-center justify-between">
308
309
  <span className="text-[11px] text-muted-fg">{value.length} {t("project.memories.chars")}</span>
@@ -361,14 +362,14 @@ function SleepView({ routines }: { routines: RoutineEntry[] }) {
361
362
  <div className="flex items-center gap-2">
362
363
  <span className={cn("size-2 rounded-full", err ? "bg-destructive" : running ? "bg-emerald-400" : "bg-muted-fg/40")} />
363
364
  <span className="text-sm font-medium">{r.name}</span>
364
- <Badge tone={running ? "success" : "muted"}>{running ? "running" : "paused"}</Badge>
365
- {err && <Badge tone="danger">last: error</Badge>}
365
+ <Badge tone={running ? "success" : "muted"}>{running ? t("agents_ui.running") : t("agents_ui.paused")}</Badge>
366
+ {err && <Badge tone="danger">{t("agents_ui.last_error")}</Badge>}
366
367
  </div>
367
368
  <div className="mt-2 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
368
- <Field2 label="Tick" value={r.schedule} />
369
- <Field2 label="Next tick" value={r.next_run_at ? new Date(r.next_run_at).toLocaleString() : "—"} />
370
- <Field2 label="Last tick" value={r.last_run_at ? new Date(r.last_run_at).toLocaleString() : "—"} />
371
- <Field2 label="Last run" value={r.last_status || "—"} />
369
+ <Field2 label={t("agents_ui.field_tick")} value={r.schedule} />
370
+ <Field2 label={t("agents_ui.field_next_tick")} value={r.next_run_at ? new Date(r.next_run_at).toLocaleString() : "—"} />
371
+ <Field2 label={t("agents_ui.field_last_tick")} value={r.last_run_at ? new Date(r.last_run_at).toLocaleString() : "—"} />
372
+ <Field2 label={t("agents_ui.field_last_run")} value={r.last_status || "—"} />
372
373
  </div>
373
374
  {r.last_error && <p className="mt-2 rounded-md bg-destructive/10 px-2 py-1 text-[11px] text-destructive">{r.last_error}</p>}
374
375
  </div>
@@ -399,7 +400,7 @@ function ToolsPicker({ value, onChange }: { value: string; onChange: (v: string)
399
400
  };
400
401
  const custom = selected.filter((s) => !catalog.some((tool) => tool.name === s));
401
402
  return (
402
- <Field label="Tools" hint={t("project.agent_detail.tools_hint")}>
403
+ <Field label={t("agents_ui.tools_label")} hint={t("project.agent_detail.tools_hint")}>
403
404
  <div className="flex flex-wrap gap-1.5">
404
405
  {catalog.map((tool) => {
405
406
  const on = selected.includes(tool.name);
@@ -105,7 +105,7 @@ export function ChatTab({ pid }: { pid: string }) {
105
105
  pid={pid}
106
106
  agents={agentList}
107
107
  superAgentSlug={ROBY_SLUG}
108
- superAgentLabel={`${persona} (super-agent)`}
108
+ superAgentLabel={t("agents_ui.super_agent_label", { persona })}
109
109
  selected={selected}
110
110
  onSelect={setSelected}
111
111
  onNewChat={onNewChat}
@@ -119,7 +119,7 @@ export function ChatTab({ pid }: { pid: string }) {
119
119
  </div>
120
120
  <div className="flex items-center gap-2">
121
121
  {activeIsRoby ? (
122
- <Badge tone="success">super-agent</Badge>
122
+ <Badge tone="success">{t("agents_ui.super_agent_badge")}</Badge>
123
123
  ) : (
124
124
  activeAgent?.model && <Badge tone="info">{activeAgent.model}</Badge>
125
125
  )}
@@ -7,7 +7,7 @@ import { Section } from "../../components/Section";
7
7
  import { Button, Dialog, Empty, Loading } from "../../components/ui";
8
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
9
9
  import { ConfigTabsEditor } from "../../components/config/ConfigTabsEditor";
10
- import { APC_PROJECT_SECTIONS, PROJECT_OVERRIDE_SECTIONS } from "../../components/config/project-config-sections";
10
+ import { apcProjectSections, projectOverrideSections } from "../../components/config/project-config-sections";
11
11
  import { useToast } from "../../components/Toast";
12
12
  import { useProject } from "../../hooks/useProjects";
13
13
  import { flattenObject } from "../../lib/config-values";
@@ -48,7 +48,7 @@ export function ConfigTab({ pid }: { pid: string }) {
48
48
 
49
49
  <TabsContent value="override">
50
50
  <ConfigTabsEditor
51
- sections={PROJECT_OVERRIDE_SECTIONS}
51
+ sections={projectOverrideSections()}
52
52
  source={cfg.data.project_only}
53
53
  placeholderSource={cfg.data.effective}
54
54
  jsonTitle={cfg.data.project_config_path}
@@ -65,7 +65,7 @@ export function ConfigTab({ pid }: { pid: string }) {
65
65
 
66
66
  <TabsContent value="project">
67
67
  <ConfigTabsEditor
68
- sections={APC_PROJECT_SECTIONS}
68
+ sections={apcProjectSections()}
69
69
  source={cfg.data.apc_project || {}}
70
70
  jsonTitle={cfg.data.project_json_path}
71
71
  jsonDescription=".apc/project.json. Metadata APC portable."
@@ -18,12 +18,6 @@ type DialogMode =
18
18
  | { kind: "new" }
19
19
  | { kind: "edit"; entry: McpEntry };
20
20
 
21
- const SOURCE_LABEL: Record<string, string> = {
22
- apc: "Shared",
23
- runtime: "Runtime",
24
- global: "Global",
25
- };
26
-
27
21
  const SOURCE_TONE: Record<string, "info" | "muted" | "success"> = {
28
22
  apc: "info",
29
23
  runtime: "success",
@@ -37,7 +31,10 @@ function sourceToScope(source: string): McpScope {
37
31
  }
38
32
 
39
33
  function sourceLabel(source: string): string {
40
- return SOURCE_LABEL[source] ?? source;
34
+ if (source === "apc") return t("project.mcps.scope_shared");
35
+ if (source === "runtime") return t("project.mcps.scope_runtime");
36
+ if (source === "global") return t("project.mcps.scope_global");
37
+ return source;
41
38
  }
42
39
 
43
40
  export function McpsTab({ pid }: { pid: string }) {
@@ -505,12 +502,12 @@ function ArgsList({
505
502
  <VarTokenInput
506
503
  value={a}
507
504
  onChange={(v) => update(i, v)}
508
- placeholder="--flag o valor"
505
+ placeholder={t("agents_ui.arg_placeholder")}
509
506
  varNames={varNames}
510
507
  onCreateVar={onCreateVar}
511
508
  />
512
509
  </div>
513
- <Button type="button" size="sm" variant="ghost" onClick={() => remove(i)} aria-label="quitar arg">
510
+ <Button type="button" size="sm" variant="ghost" onClick={() => remove(i)} aria-label={t("agents_ui.remove_arg")}>
514
511
  <Trash2 size={13} />
515
512
  </Button>
516
513
  </div>
@@ -16,14 +16,19 @@ function splitLines(v: string): string[] {
16
16
  type Kind = RoutineEntry["kind"];
17
17
 
18
18
  // Friendly action types (maps to routines.js kinds).
19
- const KIND_META: Record<Kind, { label: string; desc: string; icon: typeof Bot }> = {
20
- exec_agent: { label: "Agente del proyecto", desc: "Ejecuta un agente del proyecto con un prompt. Elegís cuál.", icon: Bot },
21
- super_agent: { label: "Super-agente", desc: "Llama al super-agente de APX con un prompt.", icon: Crown },
22
- telegram: { label: "Telegram", desc: "Manda un mensaje fijo a un canal de Telegram. No usa modelo ni agente.", icon: Send },
23
- shell: { label: "Shell", desc: "Corre un comando de shell. Sin prompt ni pre/post — el comando es la acción.", icon: Terminal },
24
- heartbeat: { label: "Latido (heartbeat)", desc: "No hace nada salvo escribir una línea en los logs cada vez que corre. Sirve para confirmar que el scheduler está vivo. Si no sabés si lo necesitás, no lo uses.", icon: Heart },
25
- };
26
- const KIND_OPTIONS = (Object.keys(KIND_META) as Kind[]).map((k) => ({ value: k, label: KIND_META[k].label, description: KIND_META[k].desc, icon: KIND_META[k].icon }));
19
+ function kindMeta(): Record<Kind, { label: string; desc: string; icon: typeof Bot }> {
20
+ return {
21
+ exec_agent: { label: t("agents_ui.kind_exec_agent"), desc: t("agents_ui.kind_exec_agent_desc"), icon: Bot },
22
+ super_agent: { label: t("agents_ui.kind_super_agent"), desc: t("agents_ui.kind_super_agent_desc"), icon: Crown },
23
+ telegram: { label: t("agents_ui.kind_telegram"), desc: t("agents_ui.kind_telegram_desc"), icon: Send },
24
+ shell: { label: t("agents_ui.kind_shell"), desc: t("agents_ui.kind_shell_desc"), icon: Terminal },
25
+ heartbeat: { label: t("agents_ui.kind_heartbeat"), desc: t("agents_ui.kind_heartbeat_desc"), icon: Heart },
26
+ };
27
+ }
28
+ function kindOptions() {
29
+ const meta = kindMeta();
30
+ return (Object.keys(meta) as Kind[]).map((k) => ({ value: k, label: meta[k].label, description: meta[k].desc, icon: meta[k].icon }));
31
+ }
27
32
 
28
33
  // "every:10m" → "cada 10 minutos", cron/once → legible.
29
34
  function scheduleHuman(s?: string): string {
@@ -33,42 +38,51 @@ function scheduleHuman(s?: string): string {
33
38
  const m = v.match(/^(\d+)(s|m|h|d)$/);
34
39
  if (m) {
35
40
  const n = m[1];
36
- const unit = { s: "segundos", m: "minutos", h: "horas", d: "días" }[m[2]] || m[2];
37
- return `cada ${n} ${unit}`;
41
+ const unit = {
42
+ s: t("agents_ui.unit_seconds"),
43
+ m: t("agents_ui.unit_minutes"),
44
+ h: t("agents_ui.unit_hours"),
45
+ d: t("agents_ui.unit_days"),
46
+ }[m[2]] || m[2];
47
+ return t("agents_ui.every_n_unit", { n, unit });
38
48
  }
39
- return `cada ${v}`;
49
+ return t("agents_ui.every_v", { v });
40
50
  }
41
- if (s.startsWith("once:")) return `una vez · ${new Date(s.slice(5)).toLocaleString()}`;
51
+ if (s.startsWith("once:")) return `once · ${new Date(s.slice(5)).toLocaleString()}`;
42
52
  if (s.startsWith("cron ")) return `cron · ${s.slice(5)}`;
43
53
  return s;
44
54
  }
45
55
 
46
- const SCHED_PRESETS = [
47
- { label: "cada 10 min", value: "every:10m" },
48
- { label: "cada hora", value: "every:1h" },
49
- { label: "diario 9am", value: "cron 0 9 * * *" },
50
- { label: "días hábiles 9am", value: "cron 0 9 * * 1-5" },
51
- ];
56
+ function schedPresets() {
57
+ return [
58
+ { label: t("agents_ui.preset_every_10m"), value: "every:10m" },
59
+ { label: t("agents_ui.preset_hourly"), value: "every:1h" },
60
+ { label: t("agents_ui.preset_daily_9am"), value: "cron 0 9 * * *" },
61
+ { label: t("agents_ui.preset_weekdays_9am"), value: "cron 0 9 * * 1-5" },
62
+ ];
63
+ }
52
64
 
53
65
  // Template/env vars the routine runner exposes (src/core/routines/runner.js).
54
- const VARS = [
55
- { v: "{{pre_output}}", where: "prompt", desc: "Salida de los pre-commands, inyectada en el prompt." },
56
- { v: "$APX_LLM_OUTPUT", where: "post", desc: "Respuesta del agente / super-agente." },
57
- { v: "$APX_STATUS", where: "post", desc: "ok | error." },
58
- { v: "$APX_SKIPPED", where: "post", desc: "1 si la acción se salteó." },
59
- { v: "$APX_PRE_OUTPUT", where: "post", desc: "Salida de los pre-commands." },
60
- { v: "$APX_PRE_OUTPUT_FILE", where: "post", desc: "Archivo con la salida de pre (para outputs grandes)." },
61
- { v: "$APX_PRE_EXIT", where: "post", desc: "Exit code de los pre-commands." },
62
- { v: "$APX_ROUTINE", where: "pre/post", desc: "Nombre de la rutina." },
63
- ];
66
+ function routineVars() {
67
+ return [
68
+ { v: "{{pre_output}}", where: "prompt", desc: t("agents_ui.var_pre_output_prompt") },
69
+ { v: "$APX_LLM_OUTPUT", where: "post", desc: t("agents_ui.var_llm_output") },
70
+ { v: "$APX_STATUS", where: "post", desc: t("agents_ui.var_status") },
71
+ { v: "$APX_SKIPPED", where: "post", desc: t("agents_ui.var_skipped") },
72
+ { v: "$APX_PRE_OUTPUT", where: "post", desc: t("agents_ui.var_pre_output") },
73
+ { v: "$APX_PRE_OUTPUT_FILE", where: "post", desc: t("agents_ui.var_pre_output_file") },
74
+ { v: "$APX_PRE_EXIT", where: "post", desc: t("agents_ui.var_pre_exit") },
75
+ { v: "$APX_ROUTINE", where: "pre/post", desc: t("agents_ui.var_routine") },
76
+ ];
77
+ }
64
78
 
65
79
  function actionSummary(kind: Kind, spec: Record<string, unknown>): string {
66
80
  switch (kind) {
67
- case "exec_agent": return spec.agent ? `Ejecuta el agente "${spec.agent}"` : "Ejecuta un agente (falta elegir)";
68
- case "super_agent": return "Llama al super-agente";
69
- case "telegram": return `Envía Telegram a "${spec.channel || "default"}"`;
70
- case "shell": return spec.command ? `Corre: ${String(spec.command).slice(0, 40)}` : "Corre un comando shell";
71
- case "heartbeat": return "Deja un latido en logs";
81
+ case "exec_agent": return spec.agent ? t("agents_ui.summary_runs_agent", { agent: String(spec.agent) }) : t("agents_ui.summary_runs_agent_none");
82
+ case "super_agent": return t("agents_ui.summary_super_agent");
83
+ case "telegram": return t("agents_ui.summary_telegram", { channel: String(spec.channel || "default") });
84
+ case "shell": return spec.command ? t("agents_ui.summary_runs_cmd", { cmd: String(spec.command).slice(0, 40) }) : t("agents_ui.summary_shell");
85
+ case "heartbeat": return t("agents_ui.summary_heartbeat");
72
86
  }
73
87
  }
74
88
 
@@ -103,7 +117,7 @@ export function RoutinesTab({ pid }: { pid: string }) {
103
117
  {!list.isLoading && (list.data?.length ?? 0) === 0 && <Empty>{t("project.routines.empty")}</Empty>}
104
118
  <ul className="space-y-2 text-sm">
105
119
  {(list.data || []).map((row) => {
106
- const meta = KIND_META[row.kind];
120
+ const meta = kindMeta()[row.kind];
107
121
  const Icon = meta?.icon || Zap;
108
122
  const err = row.last_status === "error";
109
123
  return (
@@ -123,7 +137,7 @@ export function RoutinesTab({ pid }: { pid: string }) {
123
137
  </div>
124
138
  <div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
125
139
  <Switch checked={row.enabled} onChange={() => toggle(row)} />
126
- <Button size="sm" variant="secondary" onClick={() => runNow(row)}><Play size={13} /> Run</Button>
140
+ <Button size="sm" variant="secondary" onClick={() => runNow(row)}><Play size={13} /> {t("common.run")}</Button>
127
141
  <Button size="sm" variant="destructive" onClick={() => remove(row)}><Trash2 size={13} /></Button>
128
142
  </div>
129
143
  </div>
@@ -132,7 +146,7 @@ export function RoutinesTab({ pid }: { pid: string }) {
132
146
  <span>{actionSummary(row.kind, row.spec || {})}</span>
133
147
  {row.next_run_at && <span>{t("project.routines.next_run")} {new Date(row.next_run_at).toLocaleString()}</span>}
134
148
  <span className={cn(row.last_status === "ok" && "text-emerald-500", err && "text-destructive")}>
135
- última: {row.last_status || "—"}
149
+ {t("agents_ui.last_label")} {row.last_status || "—"}
136
150
  </span>
137
151
  </div>
138
152
  {row.last_error && <div className="mt-2 rounded-md bg-destructive/10 px-2 py-1 text-xs text-destructive">{row.last_error}</div>}
@@ -228,20 +242,20 @@ function RoutineEditor({
228
242
  const postSteps = usesPrePost ? splitLines(post) : [];
229
243
  const actionLabel = (() => {
230
244
  switch (kind) {
231
- case "exec_agent": return agent ? `Agente "${agent}" responde el prompt` : "Agente (elegí cuál) responde el prompt";
232
- case "super_agent": return "El super-agente responde el prompt";
233
- case "telegram": return `Manda Telegram al canal "${tgChannel}"`;
234
- case "shell": return command ? `Corre: ${command.slice(0, 48)}` : "Corre el comando shell";
235
- case "heartbeat": return "Deja un latido en logs";
245
+ case "exec_agent": return agent ? t("agents_ui.action_agent_answers", { agent }) : t("agents_ui.action_agent_pick_answers");
246
+ case "super_agent": return t("agents_ui.action_super_answers");
247
+ case "telegram": return t("agents_ui.action_telegram_channel", { channel: tgChannel });
248
+ case "shell": return command ? t("agents_ui.summary_runs_cmd", { cmd: command.slice(0, 48) }) : t("agents_ui.action_runs_shell");
249
+ case "heartbeat": return t("agents_ui.summary_heartbeat");
236
250
  }
237
251
  })();
238
252
  const usesPrompt = usesPrePost;
239
253
 
240
- const ActionIcon = KIND_META[kind].icon;
254
+ const ActionIcon = kindMeta()[kind].icon;
241
255
  const steps = [
242
- ...preSteps.map((c, i) => ({ id: `pre-${i}`, icon: Terminal, label: "Pre", detail: c, action: false })),
256
+ ...preSteps.map((c, i) => ({ id: `pre-${i}`, icon: Terminal, label: t("agents_ui.step_pre"), detail: c, action: false })),
243
257
  { id: "action", icon: ActionIcon, label: actionLabel, detail: usesPrompt && prompt ? prompt.slice(0, 90) : undefined, action: true },
244
- ...postSteps.map((c, i) => ({ id: `post-${i}`, icon: Terminal, label: "Post", detail: c, action: false })),
258
+ ...postSteps.map((c, i) => ({ id: `post-${i}`, icon: Terminal, label: t("agents_ui.step_post"), detail: c, action: false })),
245
259
  ];
246
260
 
247
261
  return (
@@ -272,9 +286,9 @@ function RoutineEditor({
272
286
  <Input value={name} disabled={!!draft?.name} onChange={(e) => setName(e.target.value)} placeholder="resumen-diario" />
273
287
  </Field>
274
288
  <Field label={t("project.routines.kind_field")}>
275
- <UiSelect value={kind} onChange={(v) => setKind(v as Kind)} options={KIND_OPTIONS} />
289
+ <UiSelect value={kind} onChange={(v) => setKind(v as Kind)} options={kindOptions()} />
276
290
  </Field>
277
- <p className="-mt-1 text-[11px] text-muted-fg">{KIND_META[kind].desc}</p>
291
+ <p className="-mt-1 text-[11px] text-muted-fg">{kindMeta()[kind].desc}</p>
278
292
  {kind === "exec_agent" && (
279
293
  <Field label={t("project.routines.agent_field")} hint={t("project.routines.agent_hint")}>
280
294
  <UiSelect value={agent} onChange={setAgent} placeholder={agentsList.isLoading ? t("project.routines.agent_loading") : t("project.routines.agent_pick")}
@@ -284,7 +298,7 @@ function RoutineEditor({
284
298
  <Field label={t("project.routines.schedule_field")} hint={t("project.routines.schedule_hint")}>
285
299
  <div className="space-y-2">
286
300
  <div className="flex flex-wrap gap-1">
287
- {SCHED_PRESETS.map((s) => (
301
+ {schedPresets().map((s) => (
288
302
  <button key={s.value} type="button" onClick={() => setSchedule(s.value)}
289
303
  className={cn("rounded-md border px-2 py-0.5 text-[11px]", schedule === s.value ? "border-emerald-500/50 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
290
304
  {s.label}
@@ -292,7 +306,7 @@ function RoutineEditor({
292
306
  ))}
293
307
  <button type="button" onClick={() => setSchedule("manual")}
294
308
  className={cn("rounded-md border px-2 py-0.5 text-[11px]", schedule === "manual" ? "border-emerald-500/50 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
295
- Manual
309
+ {t("agents_ui.preset_manual")}
296
310
  </button>
297
311
  </div>
298
312
  <Input value={schedule} onChange={(e) => setSchedule(e.target.value)} placeholder="every:10m · cron 0 9 * * 1-5 · once:ISO · manual" />
@@ -320,10 +334,10 @@ function RoutineEditor({
320
334
  <>
321
335
  <div className="grid grid-cols-2 gap-3">
322
336
  <Field label={t("project.routines.tg_channel")}><Input value={tgChannel} onChange={(e) => setTgChannel(e.target.value)} placeholder="default" /></Field>
323
- <Field label={t("project.routines.tg_chat_id")}><Input value={tgChatId} onChange={(e) => setTgChatId(e.target.value)} placeholder="(usa el del canal)" /></Field>
337
+ <Field label={t("project.routines.tg_chat_id")}><Input value={tgChatId} onChange={(e) => setTgChatId(e.target.value)} placeholder={t("agents_ui.tg_chat_id_ph")} /></Field>
324
338
  </div>
325
339
  <Field label={t("project.routines.tg_text")} hint={t("project.routines.tg_text_hint")}>
326
- <Textarea rows={8} value={tgText} onChange={(e) => setTgText(e.target.value)} placeholder="mensaje a enviar" />
340
+ <Textarea rows={8} value={tgText} onChange={(e) => setTgText(e.target.value)} placeholder={t("agents_ui.tg_text_ph")} />
327
341
  </Field>
328
342
  </>
329
343
  )}
@@ -339,7 +353,7 @@ function RoutineEditor({
339
353
  {kind === "heartbeat" && (
340
354
  <div className="grid grid-cols-2 gap-3">
341
355
  <Field label={t("project.routines.hb_channel")}><Input value={hbChannel} onChange={(e) => setHbChannel(e.target.value)} placeholder="heartbeat" /></Field>
342
- <Field label={t("project.routines.hb_message")}><Input value={hbMessage} onChange={(e) => setHbMessage(e.target.value)} placeholder="sigo vivo" /></Field>
356
+ <Field label={t("project.routines.hb_message")}><Input value={hbMessage} onChange={(e) => setHbMessage(e.target.value)} placeholder={t("agents_ui.hb_message_ph")} /></Field>
343
357
  </div>
344
358
  )}
345
359
 
@@ -355,7 +369,7 @@ function RoutineEditor({
355
369
  <div className="rounded-lg border border-border bg-muted/10 p-3">
356
370
  <div className="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-fg">{t("project.routines.vars_title")}</div>
357
371
  <div className="flex flex-wrap gap-1.5">
358
- {VARS.map((v) => (
372
+ {routineVars().map((v) => (
359
373
  <span key={v.v} title={v.desc} className="inline-flex items-center gap-1 rounded-md border border-border bg-card px-1.5 py-0.5 font-mono text-[10px]">
360
374
  {v.v}<span className="not-italic text-muted-fg">· {v.where}</span>
361
375
  </span>
@@ -97,7 +97,7 @@ export function TelegramTab({ pid }: { pid: string }) {
97
97
  {enabled && (
98
98
  <>
99
99
  <div className="grid grid-cols-2 gap-3">
100
- <Field label={t("project.telegram.bot_token")} hint={existing?.bot_token ? `${secretHint(existing.bot_token)} — vacío = mantener` : t("project.telegram.bot_hint_none")}>
100
+ <Field label={t("project.telegram.bot_token")} hint={existing?.bot_token ? `${secretHint(existing.bot_token)} ${t("telegram_ui.empty_keep")}` : t("project.telegram.bot_hint_none")}>
101
101
  <Input type="password" value={botToken} onChange={(e) => setBotToken(e.target.value)} placeholder={existing?.bot_token ? secretHint(existing.bot_token) : ""} />
102
102
  </Field>
103
103
  <Field label={t("project.telegram.chat_id")}>