@agentprojectcontext/apx 1.37.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 (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
@@ -18,8 +18,8 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-DsADpObh.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-B6sYFQFa.css">
21
+ <script type="module" crossorigin src="/assets/index-CQc_5t8F.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-hwxuTPcK.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -12,6 +12,7 @@ import { CodeScreen } from "./screens/modules/CodeScreen";
12
12
  import { AddProjectDialog } from "./components/AddProjectDialog";
13
13
  import { PairingScreen } from "./screens/PairingScreen";
14
14
  import { RobyBubble } from "./components/RobyBubble";
15
+ import { Roby, RobyEmpty, type RobyMood } from "./components/Roby";
15
16
  import { ToastProvider } from "./components/Toast";
16
17
  import { Button } from "./components/ui/button";
17
18
  import { TooltipProvider } from "./components/ui/tooltip";
@@ -31,6 +32,7 @@ export function App() {
31
32
  if (auth.status === "error") {
32
33
  return (
33
34
  <Splash
35
+ mood="sad"
34
36
  text={t("daemon.unreachable")}
35
37
  sub={`${t("daemon.unreachable_hint")}\n\n${auth.reason}`}
36
38
  />
@@ -197,13 +199,11 @@ function projectLabel(key?: string) {
197
199
  }
198
200
  }
199
201
 
200
- function Splash({ text, sub }: { text: string; sub?: string }) {
202
+ function Splash({ text, sub, mood = "happy" }: { text: string; sub?: string; mood?: RobyMood }) {
201
203
  return (
202
204
  <div className="grid h-screen w-screen place-items-center bg-background text-foreground">
203
- <div className="text-center">
204
- <div className="font-mono text-xs text-muted-fg whitespace-pre leading-none mb-4">
205
- {" ▄███████▄\n █ ██ ██ █\n █ ◔ ◔ █\n █ ╰~╯ █\n ▀███████▀"}
206
- </div>
205
+ <div className="flex flex-col items-center text-center">
206
+ <Roby mood={mood} className="mb-4 text-xs" />
207
207
  <div className="text-foreground">{text}</div>
208
208
  {sub && <pre className="mt-2 max-w-xl whitespace-pre-wrap text-sm text-muted-fg">{sub}</pre>}
209
209
  </div>
@@ -213,27 +213,18 @@ function Splash({ text, sub }: { text: string; sub?: string }) {
213
213
 
214
214
  function NotFound() {
215
215
  const navigate = useNavigate();
216
- // Roby, but lost — a confused riff on the Splash mascot (asymmetric eyes,
217
- // little "o" mouth, a floating "?"). Tinted with the APX brand green.
218
- const robyLost =
219
- " ?\n ▄███████▄\n █ ██ ██ █\n █ ◑ ◐ █\n █ o █\n ▀███████▀";
220
216
  return (
221
- <div className="grid h-full place-items-center p-8" data-testid="screen-not-found">
222
- <div className="flex flex-col items-center text-center">
223
- <pre
224
- aria-hidden
225
- className="mb-6 select-none whitespace-pre font-mono text-xs leading-none text-emerald-500"
226
- >
227
- {robyLost}
228
- </pre>
229
- <div className="font-mono text-7xl font-semibold leading-none tracking-tight text-foreground">
230
- {t("not_found.title")}
231
- </div>
232
- <p className="mt-4 max-w-sm text-sm text-muted-fg">{t("not_found.message")}</p>
233
- <Button variant="outline" className="mt-6" onClick={() => navigate("/")}>
217
+ <RobyEmpty
218
+ testId="screen-not-found"
219
+ mood="confused"
220
+ title={t("not_found.title")}
221
+ titleClassName="text-7xl"
222
+ message={t("not_found.message")}
223
+ action={
224
+ <Button variant="outline" onClick={() => navigate("/")}>
234
225
  {t("not_found.home")}
235
226
  </Button>
236
- </div>
237
- </div>
227
+ }
228
+ />
238
229
  );
239
230
  }
@@ -11,7 +11,7 @@ export function ModelCombobox({
11
11
  value,
12
12
  onChange,
13
13
  options,
14
- placeholder = "elegí o escribí un modelo…",
14
+ placeholder = t("shared_ui.model_combobox_ph"),
15
15
  invalid,
16
16
  invalidHint,
17
17
  className,
@@ -0,0 +1,96 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // Roby — the APX mascot. Web mirror of the CLI version in src/core/mascot.js:
5
+ // a chunky ▄███████▄ head with two screen-eyes and a tiny mouth, drawn as clean
6
+ // emerald line art. Keep the FACES table below in sync with the CLI's MOODS so
7
+ // the same character shows up identically across terminal and web.
8
+ export type RobyMood = "happy" | "wave" | "confused" | "sad" | "excited" | "sleeping";
9
+
10
+ // Each mood = a pair of pupils, a mouth glyph, and an optional floating accent.
11
+ // NB: the sad mouth is "◠" (single-cell) rather than the CLI's "︵": that glyph
12
+ // is East-Asian-wide (~1.7 cells) in the browser monospace font and would push
13
+ // the head's right edge out of line. "◠" is the single-width frown inverse of
14
+ // the happy "‿".
15
+ const FACES: Record<RobyMood, { eyes: [string, string]; mouth: string; top?: string }> = {
16
+ happy: { eyes: ["◕", "◕"], mouth: "‿" },
17
+ wave: { eyes: ["◕", "◕"], mouth: "▽", top: "·" },
18
+ confused: { eyes: ["◑", "◐"], mouth: "o", top: "?" },
19
+ sad: { eyes: ["╥", "╥"], mouth: "◠" },
20
+ excited: { eyes: ["★", "★"], mouth: "▽", top: "✦" },
21
+ sleeping: { eyes: ["−", "−"], mouth: "‿", top: "z z" },
22
+ };
23
+
24
+ export function Roby({ mood = "happy", className }: { mood?: RobyMood; className?: string }) {
25
+ const face = FACES[mood] ?? FACES.happy;
26
+ const [el, er] = face.eyes;
27
+ // Recessed eye "screens" + floating accent sit in a dimmer emerald; the bright
28
+ // frame, pupils and mouth inherit text-emerald-400 from the wrapper.
29
+ const dim = "text-emerald-700 dark:text-emerald-600/70";
30
+ return (
31
+ <div
32
+ aria-hidden
33
+ className={cn(
34
+ "select-none whitespace-pre font-mono leading-none text-emerald-400",
35
+ className
36
+ )}
37
+ >
38
+ {face.top && (
39
+ <div>
40
+ <span className={dim}>{` ${face.top}`}</span>
41
+ </div>
42
+ )}
43
+ <div>{" ▄███████▄"}</div>
44
+ <div>
45
+ {" █ "}
46
+ <span className={dim}>{"██"}</span>
47
+ {" "}
48
+ <span className={dim}>{"██"}</span>
49
+ {" █"}
50
+ </div>
51
+ <div>{` █ ${el} ${er} █`}</div>
52
+ <div>{` █ ${face.mouth} █`}</div>
53
+ <div>{" ▀███████▀"}</div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ // Centered "Roby + message" layout shared by the 404 and the project-not-found
59
+ // screens. `title` is optional and rendered large (e.g. a giant "404").
60
+ export function RobyEmpty({
61
+ mood = "confused",
62
+ title,
63
+ titleClassName,
64
+ message,
65
+ action,
66
+ className,
67
+ testId,
68
+ }: {
69
+ mood?: RobyMood;
70
+ title?: ReactNode;
71
+ titleClassName?: string;
72
+ message?: ReactNode;
73
+ action?: ReactNode;
74
+ className?: string;
75
+ testId?: string;
76
+ }) {
77
+ return (
78
+ <div className={cn("grid h-full place-items-center p-8", className)} data-testid={testId}>
79
+ <div className="flex flex-col items-center text-center">
80
+ <Roby mood={mood} className="mb-6 text-sm" />
81
+ {title != null && (
82
+ <div
83
+ className={cn(
84
+ "font-mono font-semibold leading-none tracking-tight text-foreground",
85
+ titleClassName
86
+ )}
87
+ >
88
+ {title}
89
+ </div>
90
+ )}
91
+ {message != null && <p className="mt-4 max-w-sm text-sm text-muted-fg">{message}</p>}
92
+ {action && <div className="mt-6">{action}</div>}
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -23,7 +23,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
23
23
  }, [channel?.name]);
24
24
 
25
25
  const submit = async () => {
26
- if (!draft.name?.trim()) { toast.error("name requerido"); return; }
26
+ if (!draft.name?.trim()) { toast.error(t("telegram_channel_dialog.name_required")); return; }
27
27
  setBusy(true);
28
28
  try {
29
29
  const isExisting = channel && channel.name !== "";
@@ -32,7 +32,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
32
32
  } else {
33
33
  await Telegram.channels.upsert(draft);
34
34
  }
35
- toast.success("Canal guardado.");
35
+ toast.success(t("telegram_channel_dialog.saved"));
36
36
  onSaved();
37
37
  } catch (e) {
38
38
  toast.error((e as Error).message);
@@ -44,7 +44,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
44
44
  open={!!channel}
45
45
  onClose={onClose}
46
46
  title={channel?.name ? t("telegram_channel_dialog.edit_title", { name: channel.name }) : t("telegram_channel_dialog.new_title")}
47
- description="POST /telegram/channels (upsert) — PATCH /telegram/channels/:name (parcial)."
47
+ description={t("telegram_ui.channel_dialog_desc")}
48
48
  footer={
49
49
  <>
50
50
  <Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
@@ -53,10 +53,10 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
53
53
  }
54
54
  >
55
55
  <div className="space-y-3">
56
- <Field label="name (slug interno)">
56
+ <Field label={t("telegram_channel_dialog.name_label")}>
57
57
  <Input value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} disabled={!!channel?.name} />
58
58
  </Field>
59
- <Field label="bot_token" hint={channel?.bot_token ? secretHint(channel.bot_token) : "Token del BotFather. Se guarda en ~/.apx/config.json."}>
59
+ <Field label={t("telegram_channel_dialog.token_label")} hint={channel?.bot_token ? secretHint(channel.bot_token) : t("telegram_ui.bot_token_hint")}>
60
60
  <Input
61
61
  type="password"
62
62
  value={draft.bot_token || ""}
@@ -64,18 +64,18 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
64
64
  placeholder={channel?.bot_token ? secretHint(channel.bot_token) : ""}
65
65
  />
66
66
  </Field>
67
- <Field label="chat_id">
67
+ <Field label={t("telegram_channel_dialog.chat_id")}>
68
68
  <Input value={draft.chat_id || ""} onChange={(e) => setDraft({ ...draft, chat_id: e.target.value })} />
69
69
  </Field>
70
- <Field label="project" hint="Slug o id del proyecto al que pinear este canal (opcional).">
70
+ <Field label={t("telegram_channel_dialog.project_label")} hint={t("telegram_channel_dialog.project_hint")}>
71
71
  <Input value={draft.project || ""} onChange={(e) => setDraft({ ...draft, project: e.target.value })} />
72
72
  </Field>
73
- <Field label="route_to_agent" hint="Agente que contesta; vacío = super-agent APX.">
73
+ <Field label={t("telegram_channel_dialog.route_label")} hint={t("telegram_channel_dialog.route_hint")}>
74
74
  <Input value={draft.route_to_agent || ""} onChange={(e) => setDraft({ ...draft, route_to_agent: e.target.value })} />
75
75
  </Field>
76
76
  <Field
77
- label="owner_user_id"
78
- hint="user_id de Telegram del dueño de este canal. Override del rol global a 'owner' acá. Si lo dejás vacío, el primer mensaje privado lo reclama."
77
+ label={t("telegram_channel_dialog.owner_label")}
78
+ hint={t("telegram_channel_dialog.owner_hint")}
79
79
  >
80
80
  <Input
81
81
  value={draft.owner_user_id != null ? String(draft.owner_user_id) : ""}
@@ -89,7 +89,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
89
89
  <Switch
90
90
  checked={!!draft.respond_with_engine}
91
91
  onChange={(v) => setDraft({ ...draft, respond_with_engine: v })}
92
- label="Responder con engine (no echo)"
92
+ label={t("telegram_channel_dialog.respond_label")}
93
93
  />
94
94
  </div>
95
95
  </Dialog>
@@ -20,7 +20,7 @@ export function TelegramSendDialog({ channel, onClose }: Props) {
20
20
  setBusy(true);
21
21
  try {
22
22
  await Telegram.send({ text, channel: channel.name });
23
- toast.success("Mensaje enviado.");
23
+ toast.success(t("telegram_ui.message_sent"));
24
24
  onClose();
25
25
  } catch (e) {
26
26
  toast.error((e as Error).message);
@@ -31,16 +31,16 @@ export function TelegramSendDialog({ channel, onClose }: Props) {
31
31
  <Dialog
32
32
  open={!!channel}
33
33
  onClose={onClose}
34
- title={channel ? `${t("admin.telegram_send_test_title")} ${channel.name}` : ""}
35
- description={channel ? `chat_id: ${channel.chat_id || "—"}` : ""}
34
+ title={channel ? t("telegram_send_dialog.title", { name: channel.name }) : ""}
35
+ description={channel ? t("telegram_ui.send_chat_id", { id: channel.chat_id || "—" }) : ""}
36
36
  footer={
37
37
  <>
38
38
  <Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
39
- <Button variant="primary" onClick={submit} loading={busy}>Enviar</Button>
39
+ <Button variant="primary" onClick={submit} loading={busy}>{t("chat_ui.send")}</Button>
40
40
  </>
41
41
  }
42
42
  >
43
- <Field label="Texto">
43
+ <Field label={t("telegram_ui.message_label")}>
44
44
  <Textarea rows={4} value={text} onChange={(e) => setText(e.target.value)} />
45
45
  </Field>
46
46
  </Dialog>
@@ -53,7 +53,7 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
53
53
  {!mine && msg.inspector && (msg.inspector.loaded?.length || msg.inspector.hinted?.length) ? (
54
54
  <div
55
55
  className="flex flex-wrap items-center gap-1 text-[10px] text-sky-400/90"
56
- title={`Skill Inspector (${msg.inspector.embedder || "RAG"}) eligió estas skills para este turno`}
56
+ title={t("shared_ui.skill_inspector_title", { embedder: msg.inspector.embedder || "RAG" })}
57
57
  >
58
58
  <Sparkles size={10} />
59
59
  {msg.inspector.loaded?.map((s) => (
@@ -107,7 +107,7 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
107
107
  <span>{formatTs(msg.ts)}</span>
108
108
  {!mine && msg.model && <span className="font-mono">· {msg.model}</span>}
109
109
  {!mine && hasTools && (
110
- <span>· {msg.parts.filter((p) => p.kind === "tool").length} tools</span>
110
+ <span>· {t("shared_ui.tools_count", { n: msg.parts.filter((p) => p.kind === "tool").length })}</span>
111
111
  )}
112
112
  {onCopy && copyText && (
113
113
  <button
@@ -64,7 +64,7 @@ export function ModelPicker({
64
64
 
65
65
  const q = query.trim().toLowerCase();
66
66
  const filtered = q ? options.filter((o) => o.toLowerCase().includes(q)) : options;
67
- const label = value || "Auto";
67
+ const label = value || t("shared_ui.auto");
68
68
 
69
69
  const pick = (m: string) => { onChange(m); setOpen(false); setQuery(""); };
70
70
 
@@ -93,7 +93,7 @@ export function ModelPicker({
93
93
  <input
94
94
  autoFocus
95
95
  value={query}
96
- placeholder="filtrar o escribir modelo…"
96
+ placeholder={t("shared_ui.model_filter_ph")}
97
97
  onChange={(e) => setQuery(e.target.value)}
98
98
  onKeyDown={(e) => { if (e.key === "Enter" && query.trim()) pick(query.trim()); }}
99
99
  className="mb-1 w-full rounded-md border border-border bg-background px-2 py-1 text-xs outline-none focus:border-foreground/30"
@@ -108,11 +108,11 @@ export function ModelPicker({
108
108
  !value && "bg-accent/50",
109
109
  )}
110
110
  >
111
- <span className="flex items-center gap-1.5"><X className="size-3" /> Auto (router decide)</span>
111
+ <span className="flex items-center gap-1.5"><X className="size-3" /> {t("shared_ui.auto_router")}</span>
112
112
  {!value && <Check className="size-3" />}
113
113
  </button>
114
114
  </li>
115
- {!loaded && <li className="px-2 py-1 text-[11px] text-muted-fg">cargando modelos…</li>}
115
+ {!loaded && <li className="px-2 py-1 text-[11px] text-muted-fg">{t("shared_ui.loading_models")}</li>}
116
116
  {loaded && filtered.length === 0 && query.trim() && (
117
117
  <li>
118
118
  <button
@@ -120,7 +120,7 @@ export function ModelPicker({
120
120
  onMouseDown={(e) => { e.preventDefault(); pick(query.trim()); }}
121
121
  className="w-full rounded-md px-2 py-1 text-left font-mono text-xs hover:bg-accent hover:text-accent-fg"
122
122
  >
123
- usar {query.trim()}
123
+ {t("shared_ui.use_value", { value: query.trim() })}
124
124
  </button>
125
125
  </li>
126
126
  )}
@@ -19,28 +19,32 @@ import {
19
19
  } from "lucide-react";
20
20
  import { cn } from "../../lib/cn";
21
21
  import type { ToolPart } from "../../hooks/useChat";
22
+ import { t } from "../../i18n";
22
23
 
23
24
  // Map registered tool names (core/agent tools) to an icon + friendly label.
24
- const TOOL_META: Record<string, { icon: typeof Wrench; label: string }> = {
25
- read_file: { icon: FileText, label: "Leer archivo" },
26
- write_file: { icon: FilePlus, label: "Escribir archivo" },
27
- edit_file: { icon: FilePen, label: "Editar archivo" },
28
- list_files: { icon: FolderTree, label: "Listar archivos" },
29
- search_files: { icon: Search, label: "Buscar en archivos" },
30
- search_messages: { icon: Search, label: "Buscar mensajes" },
31
- tail_messages: { icon: Search, label: "Últimos mensajes" },
32
- run_shell: { icon: Terminal, label: "Ejecutar shell" },
33
- send_telegram: { icon: Send, label: "Enviar Telegram" },
34
- call_agent: { icon: Bot, label: "Llamar agente" },
35
- call_mcp: { icon: Plug, label: "Llamar MCP" },
36
- call_runtime: { icon: Bot, label: "Llamar runtime" },
37
- create_task: { icon: ListTodo, label: "Crear tarea" },
38
- };
25
+ // Built per-call so t() runs against the active locale at render time.
26
+ function toolMeta(): Record<string, { icon: typeof Wrench; label: string }> {
27
+ return {
28
+ read_file: { icon: FileText, label: t("shared_ui.tool_read_file") },
29
+ write_file: { icon: FilePlus, label: t("shared_ui.tool_write_file") },
30
+ edit_file: { icon: FilePen, label: t("shared_ui.tool_edit_file") },
31
+ list_files: { icon: FolderTree, label: t("shared_ui.tool_list_files") },
32
+ search_files: { icon: Search, label: t("shared_ui.tool_search_files") },
33
+ search_messages: { icon: Search, label: t("shared_ui.tool_search_messages") },
34
+ tail_messages: { icon: Search, label: t("shared_ui.tool_tail_messages") },
35
+ run_shell: { icon: Terminal, label: t("shared_ui.tool_run_shell") },
36
+ send_telegram: { icon: Send, label: t("shared_ui.tool_send_telegram") },
37
+ call_agent: { icon: Bot, label: t("shared_ui.tool_call_agent") },
38
+ call_mcp: { icon: Plug, label: t("shared_ui.tool_call_mcp") },
39
+ call_runtime: { icon: Bot, label: t("shared_ui.tool_call_runtime") },
40
+ create_task: { icon: ListTodo, label: t("shared_ui.tool_create_task") },
41
+ };
42
+ }
39
43
 
40
44
  const FILE_TOOLS = new Set(["write_file", "edit_file"]);
41
45
 
42
46
  function metaFor(tool: string) {
43
- return TOOL_META[tool] || { icon: Wrench, label: tool };
47
+ return toolMeta()[tool] || { icon: Wrench, label: tool };
44
48
  }
45
49
 
46
50
  // Best-effort one-line argument summary shown next to the tool title.
@@ -104,7 +108,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
104
108
  <span className="shrink-0 font-medium">{label}</span>
105
109
  {summary && <span className="truncate font-mono text-muted-foreground">{summary}</span>}
106
110
  <span className="ml-auto flex items-center gap-1">
107
- {part.status === "deduped" && <span className="text-[10px] text-amber-400">dedup</span>}
111
+ {part.status === "deduped" && <span className="text-[10px] text-amber-400">{t("shared_ui.dedup")}</span>}
108
112
  <StatusIcon status={part.status} />
109
113
  </span>
110
114
  </button>
@@ -113,7 +117,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
113
117
  <div className="space-y-2 border-t border-border/60 px-2.5 py-2">
114
118
  {part.args && Object.keys(part.args).length > 0 && (
115
119
  <div>
116
- <div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">args</div>
120
+ <div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">{t("shared_ui.args")}</div>
117
121
  <pre className="max-h-48 overflow-auto rounded-md bg-background/60 p-2 font-mono text-[11px] leading-relaxed text-foreground">
118
122
  {pretty(part.args)}
119
123
  </pre>
@@ -121,7 +125,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
121
125
  )}
122
126
  {part.result !== undefined && (
123
127
  <div>
124
- <div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">result</div>
128
+ <div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">{t("shared_ui.result")}</div>
125
129
  <pre
126
130
  className={cn(
127
131
  "max-h-64 overflow-auto rounded-md bg-background/60 p-2 font-mono text-[11px] leading-relaxed",
@@ -63,7 +63,7 @@ function ArtifactRow({
63
63
  const copy = async (text: string) => {
64
64
  try {
65
65
  await navigator.clipboard.writeText(text);
66
- toast.info("Copiado.");
66
+ toast.info(t("modules_ui.code_copied"));
67
67
  } catch {
68
68
  /* ignore */
69
69
  }
@@ -75,8 +75,8 @@ function ArtifactRow({
75
75
  try {
76
76
  const r = await Artifacts.run(pid, entry.name);
77
77
  setRunResult(r);
78
- if (r.ok) toast.info(`exit 0 ${r.durationMs}ms`);
79
- else toast.error(`exit ${r.exitCode ?? r.signal ?? "?"}${r.timedOut ? " (timeout)" : ""}`);
78
+ if (r.ok) toast.info(t("modules_ui.code_artifact_exit_ok", { ms: r.durationMs ?? 0 }));
79
+ else toast.error(t("modules_ui.code_artifact_exit_fail", { code: r.exitCode ?? r.signal ?? "?", timeout: r.timedOut ? t("modules_ui.code_artifact_timeout_suffix") : "" }));
80
80
  } catch (e) {
81
81
  toast.error((e as Error).message);
82
82
  } finally {
@@ -177,7 +177,7 @@ function ArtifactRow({
177
177
  className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-blue-500/15 text-blue-700 hover:bg-blue-500/25 dark:text-blue-300"
178
178
  >
179
179
  <Eye className="size-3" />
180
- Ver
180
+ {t("modules_ui.code_artifact_view_short")}
181
181
  </button>
182
182
  </Tip>
183
183
  <DialogContent className="sm:max-w-lg">
@@ -205,7 +205,7 @@ function ArtifactRow({
205
205
  className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-violet-500/15 text-violet-700 hover:bg-violet-500/25 dark:text-violet-300"
206
206
  >
207
207
  <SquarePen className="size-3" />
208
- Editar
208
+ {t("modules_ui.code_artifact_edit_short")}
209
209
  </button>
210
210
  </Tip>
211
211
 
@@ -252,7 +252,7 @@ function ArtifactRow({
252
252
  />
253
253
  }
254
254
  >
255
- Cancelar
255
+ {t("common.cancel")}
256
256
  </DialogClose>
257
257
  <button
258
258
  type="button"
@@ -266,7 +266,7 @@ function ArtifactRow({
266
266
  )}
267
267
  >
268
268
  {deleting && <Spinner size={10} />}
269
- Eliminar
269
+ {t("code_module.delete")}
270
270
  </button>
271
271
  </DialogFooter>
272
272
  </DialogContent>
@@ -292,16 +292,16 @@ function ArtifactRow({
292
292
  : "bg-rose-500/15 text-rose-700 dark:text-rose-300",
293
293
  )}
294
294
  >
295
- exit {runResult.exitCode ?? runResult.signal ?? "?"}
295
+ {t("modules_ui.code_artifact_exit_badge", { code: runResult.exitCode ?? runResult.signal ?? "?" })}
296
296
  </span>
297
297
  {runResult.timedOut && (
298
298
  <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
299
- timeout
299
+ {t("modules_ui.code_artifact_timeout")}
300
300
  </span>
301
301
  )}
302
302
  {runResult.truncated && (
303
303
  <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
304
- truncated
304
+ {t("modules_ui.code_artifact_truncated")}
305
305
  </span>
306
306
  )}
307
307
  <span className="font-mono text-muted-foreground">
@@ -56,18 +56,18 @@ export function CodeContextTab({ turns, session }: Props) {
56
56
 
57
57
  return (
58
58
  <div className="space-y-1 p-3" data-testid="code-context-tab">
59
- <Row label={t("code_module.ctx_model")} value={m.model || "auto"} />
60
- {session?.mode && <Row label="Modo" value={session.mode} />}
61
- {session?.agentSlug && <Row label="Agente" value={session.agentSlug} />}
59
+ <Row label={t("code_module.ctx_model")} value={m.model || t("modules_ui.code_ctx_auto")} />
60
+ {session?.mode && <Row label={t("modules_ui.code_ctx_mode")} value={session.mode} />}
61
+ {session?.agentSlug && <Row label={t("modules_ui.code_ctx_agent")} value={session.agentSlug} />}
62
62
  <Row
63
63
  label={t("code_module.ctx_messages")}
64
- value={`${m.userMsgs} usuario · ${m.assistantMsgs} asistente`}
64
+ value={t("modules_ui.code_ctx_msgs_value", { user: m.userMsgs, assistant: m.assistantMsgs })}
65
65
  />
66
66
  <Row label={t("code_module.ctx_input")} value={m.input.toLocaleString()} />
67
67
  <Row label={t("code_module.ctx_output")} value={m.output.toLocaleString()} />
68
- <Row label="Tokens Total" value={(m.input + m.output).toLocaleString()} />
69
- {session?.createdAt && <Row label="Creado" value={fmtDate(session.createdAt)} />}
70
- {session?.updatedAt && <Row label="Actividad" value={fmtDate(session.updatedAt)} />}
68
+ <Row label={t("modules_ui.code_ctx_tokens_total")} value={(m.input + m.output).toLocaleString()} />
69
+ {session?.createdAt && <Row label={t("modules_ui.code_ctx_created")} value={fmtDate(session.createdAt)} />}
70
+ {session?.updatedAt && <Row label={t("modules_ui.code_ctx_activity")} value={fmtDate(session.updatedAt)} />}
71
71
 
72
72
  <hr className="border-border my-2" />
73
73
 
@@ -1,5 +1,6 @@
1
1
  import { FolderGit2 } from "lucide-react";
2
2
  import { UiSelect } from "../UiSelect";
3
+ import { t } from "../../i18n";
3
4
  import type { ProjectEntry } from "../../types/daemon";
4
5
 
5
6
  interface Props {
@@ -16,7 +17,7 @@ interface Props {
16
17
  // their path) so the dropdown is human-readable.
17
18
  export function CodeProjectPicker({ projects, value, onChange, disabled }: Props) {
18
19
  const options = projects.map((p) => {
19
- const base = p.path?.split("/").filter(Boolean).pop() || `proyecto ${p.id}`;
20
+ const base = p.path?.split("/").filter(Boolean).pop() || t("modules_ui.code_project_fallback", { id: p.id });
20
21
  return {
21
22
  value: String(p.id),
22
23
  label: p.name || base,
@@ -31,7 +32,7 @@ export function CodeProjectPicker({ projects, value, onChange, disabled }: Props
31
32
  value={value}
32
33
  onChange={onChange}
33
34
  options={options}
34
- placeholder="Elegí un proyecto…"
35
+ placeholder={t("modules_ui.code_pick_project_ph")}
35
36
  disabled={disabled}
36
37
  />
37
38
  </div>
@@ -5,6 +5,7 @@ import { useState, useEffect, useCallback, Fragment, type ElementType } from "re
5
5
  import { PanelLeft } from "lucide-react";
6
6
  import { cn } from "../../lib/cn";
7
7
  import { Tip } from "../ui/tip";
8
+ import { t } from "../../i18n";
8
9
 
9
10
  export interface TabItem {
10
11
  key: string;
@@ -46,11 +47,11 @@ export function useNavCollapse(storageKey: string) {
46
47
 
47
48
  export function NavToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
48
49
  return (
49
- <Tip content={collapsed ? "Expandir menú" : "Colapsar menú"} side="bottom">
50
+ <Tip content={collapsed ? t("settings_ui.expand_menu") : t("settings_ui.collapse_menu")} side="bottom">
50
51
  <button
51
52
  type="button"
52
53
  onClick={onToggle}
53
- aria-label={collapsed ? "Expandir menú" : "Colapsar menú"}
54
+ aria-label={collapsed ? t("settings_ui.expand_menu") : t("settings_ui.collapse_menu")}
54
55
  className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-fg transition-colors hover:bg-accent hover:text-foreground"
55
56
  >
56
57
  <PanelLeft className={cn("size-4 transition-transform", collapsed && "rotate-180")} />
@@ -4,6 +4,7 @@ import { UiSelect } from "../UiSelect";
4
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
5
5
  import { getDotted, parseConfigJson } from "../../lib/config-values";
6
6
  import { isSecretMarker, secretHint } from "../../lib/secrets";
7
+ import { t } from "../../i18n";
7
8
 
8
9
  export type ConfigField = {
9
10
  path: string;
@@ -27,7 +28,7 @@ export function ConfigTabsEditor({
27
28
  placeholderSource,
28
29
  jsonTitle,
29
30
  jsonDescription,
30
- saveLabel = "Guardar",
31
+ saveLabel = t("common.save"),
31
32
  onSaveFields,
32
33
  onSaveJson,
33
34
  busy,
@@ -133,7 +134,7 @@ export function ConfigTabsEditor({
133
134
  onChange={(event) => setRaw(event.target.value)}
134
135
  />
135
136
  {jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
136
- <Button variant="primary" loading={busy} onClick={saveJson}>Guardar JSON</Button>
137
+ <Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
137
138
  </div>
138
139
  </TabsContent>
139
140
  </Tabs>
@@ -25,13 +25,13 @@ export function GlobalConfigEditor() {
25
25
  return (
26
26
  <Section
27
27
  title={t("global_config.title")}
28
- description="Config general en ~/.apx/config.json. Editable por tabs; JSON queda separado."
28
+ description={t("settings_ui.global_config_desc")}
29
29
  >
30
30
  <ConfigTabsEditor
31
31
  sections={GLOBAL_CONFIG_SECTIONS}
32
32
  source={config as Record<string, unknown>}
33
33
  jsonTitle="~/.apx/config.json"
34
- jsonDescription="Secretos redacted no se sobrescriben."
34
+ jsonDescription={t("settings_ui.global_json_desc")}
35
35
  onSaveFields={async (set, unset) => {
36
36
  await patch(set, unset);
37
37
  mutate();