@agentprojectcontext/apx 1.37.0 → 1.38.1

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 (68) hide show
  1. package/README.md +11 -0
  2. package/package.json +1 -1
  3. package/src/core/mascot.js +80 -80
  4. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
  5. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
  6. package/src/interfaces/web/dist/assets/{index-B6sYFQFa.css → index-hwxuTPcK.css} +1 -1
  7. package/src/interfaces/web/dist/index.html +2 -2
  8. package/src/interfaces/web/src/App.tsx +15 -24
  9. package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
  10. package/src/interfaces/web/src/components/Roby.tsx +96 -0
  11. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
  12. package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
  13. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
  14. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
  15. package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
  16. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
  17. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
  18. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
  19. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
  20. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
  21. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
  22. package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
  23. package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
  24. package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
  25. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
  26. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
  27. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
  28. package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
  29. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  30. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
  31. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
  32. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
  33. package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
  34. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
  35. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
  36. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
  37. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
  38. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
  39. package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
  40. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
  41. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
  42. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
  43. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
  44. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
  45. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
  46. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
  47. package/src/interfaces/web/src/hooks/useChat.ts +6 -5
  48. package/src/interfaces/web/src/i18n/en.ts +517 -1
  49. package/src/interfaces/web/src/i18n/es.ts +517 -1
  50. package/src/interfaces/web/src/i18n/index.ts +1 -1
  51. package/src/interfaces/web/src/lib/api/voice.ts +5 -5
  52. package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
  53. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  54. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
  55. package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
  56. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
  57. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
  58. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
  59. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
  60. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
  61. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
  62. package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
  63. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
  64. package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
  65. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
  66. package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
  67. package/src/interfaces/web/dist/assets/index-DsADpObh.js +0 -633
  68. package/src/interfaces/web/dist/assets/index-DsADpObh.js.map +0 -1
@@ -97,7 +97,7 @@ export function AgentDefaultsTab() {
97
97
  </div>
98
98
  {a.model
99
99
  ? <Badge tone="info">{a.model}</Badge>
100
- : <span className="text-[10px] text-muted-fg">modelo: default del router</span>}
100
+ : <span className="text-[10px] text-muted-fg">{t("agents_ui.model_router_default")}</span>}
101
101
  {a.description && <p className="line-clamp-3 text-xs text-muted-fg">{a.description}</p>}
102
102
  <div className="flex flex-wrap gap-1">
103
103
  {a.role && <Badge>{a.role}</Badge>}
@@ -121,9 +121,9 @@ export function AgentDefaultsTab() {
121
121
  }
122
122
 
123
123
  function SourceBadge({ source }: { source?: VaultAgent["source"] }) {
124
- if (source === "user") return <Badge tone="success">user</Badge>;
125
- if (source === "user-override") return <Badge tone="warning">override</Badge>;
126
- return <Badge tone="muted">bundled</Badge>;
124
+ if (source === "user") return <Badge tone="success">{t("agents_ui.source_user")}</Badge>;
125
+ if (source === "user-override") return <Badge tone="warning">{t("agents_ui.source_override")}</Badge>;
126
+ return <Badge tone="muted">{t("agents_ui.source_bundled")}</Badge>;
127
127
  }
128
128
 
129
129
  // Small square icon button with a tooltip; keeps the card header compact.
@@ -231,7 +231,7 @@ function VaultAgentDialog({
231
231
  >
232
232
  <div className="space-y-3">
233
233
  {isNew && (
234
- <Field label="slug" hint="kebab-case, ej. reviewer, my-agent, content-writer">
234
+ <Field label="slug" hint={t("agents_ui.slug_kebab_hint")}>
235
235
  <Input autoFocus value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="reviewer" />
236
236
  </Field>
237
237
  )}
@@ -254,13 +254,13 @@ function VaultAgentDialog({
254
254
  <Field label="description">
255
255
  <Input value={description} onChange={(e) => setDescription(e.target.value)} />
256
256
  </Field>
257
- <Field label="skills" hint="separadas por coma">
257
+ <Field label="skills" hint={t("agents_ui.comma_separated")}>
258
258
  <Input value={skills} onChange={(e) => setSkills(e.target.value)} placeholder="code-review, git" />
259
259
  </Field>
260
- <Field label="tools" hint="separadas por coma">
260
+ <Field label="tools" hint={t("agents_ui.comma_separated")}>
261
261
  <Input value={tools} onChange={(e) => setTools(e.target.value)} placeholder="read, write, run" />
262
262
  </Field>
263
- <Field label="body" hint="markdown — extiende el system prompt del agente">
263
+ <Field label="body" hint={t("agents_ui.body_hint")}>
264
264
  <Textarea
265
265
  value={body}
266
266
  onChange={(e) => setBody(e.target.value)}
@@ -1,13 +1,14 @@
1
1
  import { Construction } from "lucide-react";
2
2
  import { Section } from "../../components/Section";
3
+ import { t } from "../../i18n";
3
4
 
4
5
  export function ComingSoon({ title, note }: { title: string; note?: string }) {
5
6
  return (
6
- <Section title={title} description="Vista del menú Base (espacio general).">
7
+ <Section title={title} description={t("settings_ui.base_menu_view")}>
7
8
  <div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border bg-muted/20 py-16 text-center">
8
9
  <Construction className="size-8 text-muted-fg" />
9
10
  <div>
10
- <p className="text-sm font-medium">Próximamente</p>
11
+ <p className="text-sm font-medium">{t("settings_ui.coming_soon")}</p>
11
12
  {note && <p className="mt-1 max-w-md text-xs text-muted-fg">{note}</p>}
12
13
  </div>
13
14
  </div>
@@ -243,7 +243,7 @@ export function CodeScreen() {
243
243
  abortRef.current = ctrl;
244
244
  const onEvent = (ev: CodeStreamEvent) => {
245
245
  if (ev.type === "error") {
246
- toast.error(ev.error || "error");
246
+ toast.error(ev.error || t("modules_ui.code_stream_error"));
247
247
  return;
248
248
  }
249
249
  patchLast((m) => applyStreamEvent(m, ev));
@@ -276,7 +276,7 @@ export function CodeScreen() {
276
276
  const copyToClipboard = async (text: string) => {
277
277
  try {
278
278
  await navigator.clipboard.writeText(text);
279
- toast.info("Copiado.");
279
+ toast.info(t("modules_ui.code_copied"));
280
280
  } catch {
281
281
  /* ignore */
282
282
  }
@@ -296,7 +296,7 @@ export function CodeScreen() {
296
296
  project: pid,
297
297
  })
298
298
  .then((r) => {
299
- const content = r.stdout || r.stderr || "(vacío)";
299
+ const content = r.stdout || r.stderr || t("modules_ui.code_file_empty");
300
300
  setOpenFiles((prev) =>
301
301
  prev.map((f) => (f.path === path ? { ...f, content, loading: false } : f)),
302
302
  );
@@ -304,7 +304,7 @@ export function CodeScreen() {
304
304
  .catch((e: Error) => {
305
305
  setOpenFiles((prev) =>
306
306
  prev.map((f) =>
307
- f.path === path ? { ...f, content: `Error: ${e.message}`, loading: false } : f,
307
+ f.path === path ? { ...f, content: t("modules_ui.code_file_error", { msg: e.message }), loading: false } : f,
308
308
  ),
309
309
  );
310
310
  });
@@ -338,7 +338,7 @@ export function CodeScreen() {
338
338
  .catch((e: Error) => {
339
339
  setOpenFiles((prev) =>
340
340
  prev.map((f) =>
341
- f.path === tabPath ? { ...f, content: `Error: ${e.message}`, loading: false } : f,
341
+ f.path === tabPath ? { ...f, content: t("modules_ui.code_file_error", { msg: e.message }), loading: false } : f,
342
342
  ),
343
343
  );
344
344
  });
@@ -355,7 +355,7 @@ export function CodeScreen() {
355
355
  setOpenFiles((prev) =>
356
356
  prev.map((f) => (f.path === path ? { ...f, content } : f)),
357
357
  );
358
- toast.info("Guardado.");
358
+ toast.info(t("modules_ui.code_saved"));
359
359
  } catch (e) {
360
360
  toast.error((e as Error).message);
361
361
  }
@@ -366,7 +366,7 @@ export function CodeScreen() {
366
366
  const hasProjects = !projects.isLoading && projectList.length > 0;
367
367
 
368
368
  const agentOptions = useMemo(() => {
369
- const base = [{ value: SUPER_AGENT_VALUE, label: "super-agent", icon: Bot, description: "Agente principal con todas las herramientas" }];
369
+ const base = [{ value: SUPER_AGENT_VALUE, label: t("modules_ui.code_super_agent"), icon: Bot, description: t("modules_ui.code_super_agent_desc") }];
370
370
  const project = (agentsData.data || []).map((a) => ({
371
371
  value: a.slug,
372
372
  label: a.slug,
@@ -406,10 +406,10 @@ export function CodeScreen() {
406
406
  sid ? (
407
407
  <div className="flex items-center gap-0.5">
408
408
  {[
409
- { Icon: PanelLeft, open: leftOpen, toggle: toggleLeft, title: "Lista de sesiones" },
410
- { Icon: FolderTree, open: worktreeOpen, toggle: toggleTree, title: "Árbol de archivos" },
411
- { Icon: Terminal, open: termOpen, toggle: toggleTerm, title: "Terminal" },
412
- { Icon: PanelRight, open: rightOpen, toggle: toggleRight, title: "Panel de contexto" },
409
+ { Icon: PanelLeft, open: leftOpen, toggle: toggleLeft, title: t("modules_ui.code_panel_sessions") },
410
+ { Icon: FolderTree, open: worktreeOpen, toggle: toggleTree, title: t("modules_ui.code_panel_tree") },
411
+ { Icon: Terminal, open: termOpen, toggle: toggleTerm, title: t("modules_ui.code_panel_terminal") },
412
+ { Icon: PanelRight, open: rightOpen, toggle: toggleRight, title: t("modules_ui.code_panel_context") },
413
413
  ].map(({ Icon, open, toggle, title }) => (
414
414
  <Tip key={title} content={title}>
415
415
  <button
@@ -510,7 +510,7 @@ export function CodeScreen() {
510
510
  className="flex shrink-0 items-center gap-1.5 border-r border-border px-3 py-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent/40 data-[active=true]:text-foreground"
511
511
  >
512
512
  <MessageSquare className="size-3 shrink-0" />
513
- Chat
513
+ {t("modules_ui.code_chat_tab")}
514
514
  </button>
515
515
  {/* File tabs */}
516
516
  {openFiles.map((f) => {
@@ -51,13 +51,13 @@ export function DeckScreen() {
51
51
  );
52
52
  toast.success(
53
53
  enabled
54
- ? `Widget ${widgetId} habilitado.`
55
- : `Widget ${widgetId} deshabilitado.`
54
+ ? t("modules_ui.deck_widget_enabled", { id: widgetId })
55
+ : t("modules_ui.deck_widget_disabled", { id: widgetId })
56
56
  );
57
57
  // Re-validate after a short delay to get the server's persisted state.
58
58
  setTimeout(() => mutate(), 800);
59
59
  } catch (e: unknown) {
60
- const msg = e instanceof Error ? e.message : "Error al guardar";
60
+ const msg = e instanceof Error ? e.message : t("modules_ui.deck_save_error");
61
61
  toast.error(msg);
62
62
  }
63
63
  };
@@ -85,10 +85,10 @@ export function DeckScreen() {
85
85
  title={t("deck_screen.widgets_title")}
86
86
  description={
87
87
  isLoading
88
- ? "Cargando manifest…"
88
+ ? t("modules_ui.deck_loading_manifest")
89
89
  : error
90
- ? "Error al cargar el manifest."
91
- : `${widgets.length} widgets · ${enabledCount} externos habilitados`
90
+ ? t("modules_ui.deck_manifest_error")
91
+ : t("modules_ui.deck_widgets_summary", { count: widgets.length, enabled: enabledCount })
92
92
  }
93
93
  action={
94
94
  <Button size="sm" variant="ghost" onClick={() => mutate()} disabled={isLoading} title={t("deck_screen.reload_manifest")} aria-label={t("deck_screen.reload_manifest")}>
@@ -96,23 +96,23 @@ export function DeckScreen() {
96
96
  </Button>
97
97
  }
98
98
  >
99
- {isLoading && <Loading label="Cargando manifest del Deck…" />}
99
+ {isLoading && <Loading label={t("modules_ui.deck_loading_manifest_full")} />}
100
100
 
101
101
  {!isLoading && error && (
102
102
  <Empty>
103
- No se pudo cargar el manifest del Deck.{" "}
103
+ {t("modules_ui.deck_manifest_load_failed")}{" "}
104
104
  <button
105
105
  type="button"
106
106
  className="ml-1 underline"
107
107
  onClick={() => mutate()}
108
108
  >
109
- Reintentar
109
+ {t("modules_ui.deck_retry")}
110
110
  </button>
111
111
  </Empty>
112
112
  )}
113
113
 
114
114
  {!isLoading && !error && widgets.length === 0 && (
115
- <Empty>No hay widgets en el manifest.</Empty>
115
+ <Empty>{t("modules_ui.deck_no_widgets")}</Empty>
116
116
  )}
117
117
 
118
118
  {!isLoading && !error && widgets.length > 0 && (
@@ -133,22 +133,22 @@ export function DeckScreen() {
133
133
 
134
134
  {/* Active project + stats (read-only context) */}
135
135
  {data?.apx && (
136
- <Section title={t("deck_screen.context_title")} description="Información que el Deck ve del daemon.">
136
+ <Section title={t("deck_screen.context_title")} description={t("modules_ui.deck_context_desc")}>
137
137
  <div className="space-y-2 text-sm" data-testid="deck-apx-context">
138
138
  <div className="flex items-center gap-2">
139
- <span className="text-muted-fg">Proyecto activo:</span>
139
+ <span className="text-muted-fg">{t("modules_ui.deck_active_project")}</span>
140
140
  <span className="font-medium">
141
141
  {data.apx.active_project
142
142
  ? data.apx.active_project.name
143
- : "ninguno"}
143
+ : t("modules_ui.deck_none")}
144
144
  </span>
145
145
  </div>
146
146
  <div className="flex items-center gap-2">
147
- <span className="text-muted-fg">Proyectos registrados:</span>
147
+ <span className="text-muted-fg">{t("modules_ui.deck_registered_projects")}</span>
148
148
  <span className="font-medium">{data.apx.projects.length}</span>
149
149
  </div>
150
150
  <div className="flex items-center gap-2">
151
- <span className="text-muted-fg">Plugins activos:</span>
151
+ <span className="text-muted-fg">{t("modules_ui.deck_active_plugins")}</span>
152
152
  <span className="font-medium">
153
153
  {Object.keys(data.apx.plugins).join(", ") || "—"}
154
154
  </span>
@@ -10,14 +10,14 @@ import { Desktop, fetchDesktopMessages, type GlobalMessage } from "../../lib/api
10
10
  import { t } from "../../i18n";
11
11
 
12
12
  const DEFAULT_SHORTCUT = "CommandOrControl+G";
13
- const POSITION_OPTS = [
14
- { value: "left", label: "Izquierda" },
15
- { value: "center", label: "Centro" },
16
- { value: "right", label: "Derecha" },
13
+ const positionOpts = () => [
14
+ { value: "left", label: t("modules_ui.desktop_pos_left") },
15
+ { value: "center", label: t("modules_ui.desktop_pos_center") },
16
+ { value: "right", label: t("modules_ui.desktop_pos_right") },
17
17
  ];
18
- const THEME_OPTS = [
19
- { value: "light", label: "Claro" },
20
- { value: "dark", label: "Oscuro" },
18
+ const themeOpts = () => [
19
+ { value: "light", label: t("modules_ui.desktop_theme_light") },
20
+ { value: "dark", label: t("modules_ui.desktop_theme_dark") },
21
21
  ];
22
22
 
23
23
  // Desktop module — manage the floating voice window (the Electron app launched
@@ -71,7 +71,7 @@ export function DesktopScreen() {
71
71
  setBusy(true);
72
72
  try {
73
73
  await patch({ "desktop.shortcut": next });
74
- toast.success("Atajo guardado. Reiniciá la ventana (apx desktop stop && start) para aplicarlo.");
74
+ toast.success(t("modules_ui.desktop_shortcut_saved"));
75
75
  } catch (e) { toast.error((e as Error).message); }
76
76
  finally { setBusy(false); }
77
77
  };
@@ -90,7 +90,7 @@ export function DesktopScreen() {
90
90
  try {
91
91
  await Desktop.autostartSet(v);
92
92
  await mutateAutostart();
93
- toast.success(v ? "Autostart activado para el próximo login." : "Autostart desactivado.");
93
+ toast.success(v ? t("modules_ui.desktop_autostart_on") : t("modules_ui.desktop_autostart_off"));
94
94
  } catch (e) { toast.error((e as Error).message); }
95
95
  finally { setAutostartBusy(false); }
96
96
  };
@@ -101,28 +101,28 @@ export function DesktopScreen() {
101
101
  <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
102
102
  {/* ── LEFT: configuration + status ─────────────────────────────── */}
103
103
  <div className="space-y-6">
104
- <Section title={t("desktop_screen.status_title")} description="La ventana se lanza desde la terminal o por autostart.">
104
+ <Section title={t("desktop_screen.status_title")} description={t("modules_ui.desktop_status_desc")}>
105
105
  {stLoading ? <Loading /> : (
106
106
  <div className="flex items-center gap-2 text-sm">
107
107
  <StatusDot ok={running} />
108
- <span className="font-medium">{running ? "En ejecución" : "Detenida"}</span>
108
+ <span className="font-medium">{running ? t("modules_ui.desktop_running") : t("modules_ui.desktop_stopped")}</span>
109
109
  <button
110
110
  type="button"
111
111
  onClick={() => mutateStatus()}
112
112
  className="ml-2 text-xs text-muted-fg underline-offset-2 hover:underline"
113
113
  >
114
- refrescar
114
+ {t("modules_ui.desktop_refresh")}
115
115
  </button>
116
116
  </div>
117
117
  )}
118
118
  <p className="mt-3 text-xs text-muted-fg">
119
- Desde terminal: <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>
119
+ {t("modules_ui.desktop_from_terminal")} <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>
120
120
  </p>
121
121
  </Section>
122
122
 
123
123
  <Section
124
124
  title={t("desktop_screen.autostart_title")}
125
- description="Lanza la ventana al iniciar sesión del usuario. Equivalente a `apx desktop install` (no requiere sudo)."
125
+ description={t("modules_ui.desktop_autostart_desc")}
126
126
  >
127
127
  {!autostart ? <Loading /> : (
128
128
  <div className="flex items-center justify-between gap-3">
@@ -130,23 +130,23 @@ export function DesktopScreen() {
130
130
  checked={autostart.enabled}
131
131
  onChange={toggleAutostart}
132
132
  disabled={autostartBusy}
133
- label={autostart.enabled ? "Activado" : "Desactivado"}
133
+ label={autostart.enabled ? t("common.enabled") : t("common.disabled")}
134
134
  />
135
- <span className="text-xs text-muted-fg">platform: {autostart.platform}</span>
135
+ <span className="text-xs text-muted-fg">{t("modules_ui.desktop_platform", { platform: autostart.platform })}</span>
136
136
  </div>
137
137
  )}
138
138
  </Section>
139
139
 
140
140
  <Section
141
141
  title={t("desktop_screen.shortcut_title")}
142
- description="Botón de acceso rápido global que muestra/oculta la ventana y arranca a escuchar."
142
+ description={t("modules_ui.desktop_shortcut_desc")}
143
143
  >
144
144
  {cfgLoading ? <Loading /> : (
145
145
  <div className="flex items-end gap-3">
146
146
  <div className="flex-1">
147
147
  <Field
148
- label="Acelerador"
149
- hint='Formato Electron, p. ej. "CommandOrControl+G" o "CommandOrControl+Shift+Space". Reiniciá la ventana para aplicar.'
148
+ label={t("modules_ui.desktop_accelerator")}
149
+ hint={t("modules_ui.desktop_accelerator_hint")}
150
150
  >
151
151
  <Input
152
152
  value={shortcut}
@@ -164,28 +164,28 @@ export function DesktopScreen() {
164
164
  loading={busy}
165
165
  disabled={!shortcut.trim() || shortcut.trim() === savedShortcut}
166
166
  >
167
- Guardar
167
+ {t("common.save")}
168
168
  </Button>
169
169
  </div>
170
170
  )}
171
171
  </Section>
172
172
 
173
- <Section title={t("desktop_screen.appearance_title")} description="Tema y posición de la ventana en la pantalla.">
173
+ <Section title={t("desktop_screen.appearance_title")} description={t("modules_ui.desktop_appearance_desc")}>
174
174
  {cfgLoading ? <Loading /> : (
175
175
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
176
- <Field label="Tema" hint="Reiniciá la ventana para aplicar.">
176
+ <Field label={t("modules_ui.desktop_theme")} hint={t("modules_ui.desktop_restart_apply")}>
177
177
  <UiSelect
178
178
  value={theme}
179
- onChange={(v) => patchKey("desktop.theme", v, `Tema: ${v}.`)}
180
- options={THEME_OPTS}
179
+ onChange={(v) => patchKey("desktop.theme", v, t("modules_ui.desktop_theme_set", { value: v }))}
180
+ options={themeOpts()}
181
181
  disabled={busy}
182
182
  />
183
183
  </Field>
184
- <Field label="Posición" hint='"izquierda" / "centro" / "derecha" del borde superior.'>
184
+ <Field label={t("modules_ui.desktop_position")} hint={t("modules_ui.desktop_position_hint")}>
185
185
  <UiSelect
186
186
  value={position}
187
- onChange={(v) => patchKey("desktop.position", v, `Posición: ${v}.`)}
188
- options={POSITION_OPTS}
187
+ onChange={(v) => patchKey("desktop.position", v, t("modules_ui.desktop_position_set", { value: v }))}
188
+ options={positionOpts()}
189
189
  disabled={busy}
190
190
  />
191
191
  </Field>
@@ -195,19 +195,19 @@ export function DesktopScreen() {
195
195
 
196
196
  <Section
197
197
  title={t("desktop_screen.activation_title")}
198
- description="El plugin del daemon procesa los mensajes. STT se configura en Voces."
198
+ description={t("modules_ui.desktop_activation_desc")}
199
199
  >
200
200
  {cfgLoading ? <Loading /> : (
201
201
  <div className="space-y-3">
202
202
  <Switch
203
203
  checked={enabled}
204
- onChange={(v) => patchKey("desktop.enabled", v, v ? "Desktop activado." : "Desktop desactivado.")}
204
+ onChange={(v) => patchKey("desktop.enabled", v, v ? t("modules_ui.desktop_enabled_toast") : t("modules_ui.desktop_disabled_toast"))}
205
205
  disabled={busy}
206
- label={enabled ? "Plugin activado (responde mensajes)" : "Plugin desactivado"}
206
+ label={enabled ? t("modules_ui.desktop_plugin_on") : t("modules_ui.desktop_plugin_off")}
207
207
  />
208
208
  <p className="text-xs text-muted-fg">
209
- Motor de voz a texto: <Link to="/m/voice" className="font-medium text-fg underline underline-offset-2">Voces</Link>{" "}
210
- (whisper local, idioma, modelo).
209
+ {t("modules_ui.desktop_stt_engine")} <Link to="/m/voice" className="font-medium text-fg underline underline-offset-2">{t("nav.modules.voice")}</Link>{" "}
210
+ {t("modules_ui.desktop_stt_engine_suffix")}
211
211
  </p>
212
212
  </div>
213
213
  )}
@@ -218,14 +218,14 @@ export function DesktopScreen() {
218
218
  <div>
219
219
  <Section
220
220
  title={t("desktop_screen.last_conv_title")}
221
- description="Lo último charlado con el agente desde la ventana flotante."
221
+ description={t("modules_ui.desktop_last_conv_desc")}
222
222
  action={
223
223
  <button
224
224
  type="button"
225
225
  onClick={() => mutateMsgs()}
226
226
  className="text-xs text-muted-fg underline-offset-2 hover:underline"
227
227
  >
228
- refrescar
228
+ {t("modules_ui.desktop_refresh")}
229
229
  </button>
230
230
  }
231
231
  >
@@ -248,7 +248,7 @@ function DesktopLastConversation({ messages, loading }: { messages: GlobalMessag
248
248
  const groups = useMemo(() => groupExchanges(messages), [messages]);
249
249
 
250
250
  if (loading) return <Loading />;
251
- if (!messages.length) return <Empty>Sin mensajes todavía. Mandale algo a la ventana de escritorio para que aparezca aquí.</Empty>;
251
+ if (!messages.length) return <Empty>{t("modules_ui.desktop_no_messages")}</Empty>;
252
252
 
253
253
  return (
254
254
  <div className="space-y-3 max-h-[560px] overflow-y-auto pr-1">
@@ -267,11 +267,11 @@ function MessageLine({ m }: { m: GlobalMessage }) {
267
267
  return (
268
268
  <div className="py-1">
269
269
  <div className="flex items-baseline gap-2 text-[11px] text-muted-fg">
270
- <span className="font-semibold">{isUser ? "Vos" : "Roby"}</span>
270
+ <span className="font-semibold">{isUser ? t("modules_ui.desktop_you") : t("modules_ui.desktop_roby")}</span>
271
271
  <span>{when}</span>
272
272
  </div>
273
273
  <div className={"mt-0.5 text-sm leading-snug whitespace-pre-wrap " + (isUser ? "text-muted-fg" : "text-fg")}>
274
- {(m.body || "").trim() || <span className="italic opacity-50">(vacío)</span>}
274
+ {(m.body || "").trim() || <span className="italic opacity-50">{t("modules_ui.desktop_empty_msg")}</span>}
275
275
  </div>
276
276
  </div>
277
277
  );
@@ -51,7 +51,7 @@ export function VoiceScreen() {
51
51
  try {
52
52
  await patch({ "voice.tts.provider": id, "voice.tts.mode": "single" });
53
53
  await mutateProviders();
54
- toast.success(`Motor por defecto: ${id}.`);
54
+ toast.success(t("voice_ui.toast_default_engine", { id }));
55
55
  } catch (e) {
56
56
  toast.error((e as Error).message);
57
57
  } finally {
@@ -71,7 +71,7 @@ export function VoiceScreen() {
71
71
  }
72
72
  await patch(set);
73
73
  await mutateProviders();
74
- toast.success(next === "chain" ? "Modo: cadena con fallback." : "Modo: solo el motor por defecto.");
74
+ toast.success(next === "chain" ? t("voice_ui.toast_mode_chain") : t("voice_ui.toast_mode_single"));
75
75
  } catch (e) {
76
76
  toast.error((e as Error).message);
77
77
  } finally {
@@ -107,13 +107,13 @@ export function VoiceScreen() {
107
107
  await patch(set, unset.length ? unset : undefined);
108
108
  await mutateProviders();
109
109
  await mutateCfg();
110
- toast.success("Configuración de voz guardada.");
110
+ toast.success(t("voice_ui.toast_config_saved"));
111
111
  };
112
112
 
113
113
  const patchStt = async (set: Record<string, unknown>, unset?: string[]) => {
114
114
  try {
115
115
  await patch(set, unset);
116
- toast.success("Transcripción actualizada.");
116
+ toast.success(t("voice_ui.toast_transcription_updated"));
117
117
  } catch (e) {
118
118
  toast.error((e as Error).message);
119
119
  }
@@ -125,12 +125,12 @@ export function VoiceScreen() {
125
125
  {/* Left: TTS providers */}
126
126
  <Section
127
127
  title={t("voice_screen.providers_title")}
128
- description="Motores de síntesis. El estado lo reporta el daemon en vivo. Elegí cuál usar por defecto."
128
+ description={t("voice_ui.providers_desc")}
129
129
  >
130
130
  {provLoading || cfgLoading ? (
131
131
  <Loading />
132
132
  ) : provError ? (
133
- <Empty>No se pudieron cargar los proveedores: {(provError as Error).message}</Empty>
133
+ <Empty>{t("voice_ui.providers_load_error", { msg: (provError as Error).message })}</Empty>
134
134
  ) : (
135
135
  <VoiceProviderList
136
136
  engines={engines}
@@ -149,13 +149,13 @@ export function VoiceScreen() {
149
149
 
150
150
  {/* Right: test + STT */}
151
151
  <div className="space-y-6">
152
- <Section title={t("voice_screen.test_title")} description='Elegí con qué motor sintetizar y, si aplica, cómo querés que hable.'>
152
+ <Section title={t("voice_screen.test_title")} description={t("voice_ui.test_desc")}>
153
153
  <VoiceTestCard engines={engines} defaultProvider={configuredProvider} mode={mode} />
154
154
  </Section>
155
155
 
156
156
  <Section
157
157
  title={t("voice_screen.stt_title")}
158
- description="Motor de voz a texto que usan el deck, Telegram y la CLI al escuchar."
158
+ description={t("voice_ui.stt_desc")}
159
159
  >
160
160
  {cfgLoading ? <Loading /> : <VoiceSttCard config={transcriptionCfg} onPatch={patchStt} />}
161
161
  </Section>
@@ -3,6 +3,7 @@ import {
3
3
  forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide,
4
4
  type Simulation,
5
5
  } from "d3-force";
6
+ import { t } from "../../i18n";
6
7
 
7
8
  // Animated relational "brain" graph (d3-force + SVG), inspired by panda's
8
9
  // AgentBrainGraphCanvas. Center = agent; items are real project data
@@ -26,10 +27,12 @@ const KIND_COLOR: Record<BrainNode["kind"], string> = {
26
27
  agent: "#a78bfa", memory: "#38bdf8", thread: "#34d399",
27
28
  task: "#fbbf24", routine: "#f472b6", agentlink: "#c084fc",
28
29
  };
29
- const KIND_LABEL: Record<BrainNode["kind"], string> = {
30
- agent: "agente", memory: "memoria", thread: "thread",
31
- task: "task", routine: "rutina", agentlink: "jerarquía",
32
- };
30
+ function kindLabels(): Record<BrainNode["kind"], string> {
31
+ return {
32
+ agent: t("agents_ui.kind_agent"), memory: t("agents_ui.kind_memory"), thread: t("agents_ui.kind_thread"),
33
+ task: t("agents_ui.kind_task"), routine: t("agents_ui.kind_routine"), agentlink: t("agents_ui.kind_hierarchy"),
34
+ };
35
+ }
33
36
 
34
37
  const W = 760, H = 460;
35
38
 
@@ -130,12 +133,15 @@ export function AgentBrainGraph({ center, nodes }: { center: string; nodes: Brai
130
133
  </div>
131
134
 
132
135
  <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-fg">
133
- {(Object.keys(KIND_LABEL) as BrainNode["kind"][]).filter((k) => k !== "agent").map((k) => (
134
- <span key={k} className="inline-flex items-center gap-1">
135
- <span className="size-2 rounded-full" style={{ background: KIND_COLOR[k] }} /> {KIND_LABEL[k]}
136
- </span>
137
- ))}
138
- <span className="ml-auto">{nodes.length} nodos · arrastrá para reacomodar</span>
136
+ {(() => {
137
+ const labels = kindLabels();
138
+ return (Object.keys(labels) as BrainNode["kind"][]).filter((k) => k !== "agent").map((k) => (
139
+ <span key={k} className="inline-flex items-center gap-1">
140
+ <span className="size-2 rounded-full" style={{ background: KIND_COLOR[k] }} /> {labels[k]}
141
+ </span>
142
+ ));
143
+ })()}
144
+ <span className="ml-auto">{t("agents_ui.nodes_drag_hint", { n: String(nodes.length) })}</span>
139
145
  </div>
140
146
  {selected && (
141
147
  <div className="rounded-lg border border-border bg-card p-3 text-xs">