@agentprojectcontext/apx 1.33.0 → 1.34.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 (172) hide show
  1. package/package.json +1 -1
  2. package/skills/apc-context/SKILL.md +2 -5
  3. package/skills/apx/SKILL.md +49 -61
  4. package/src/core/agent/a2a/reply.js +48 -0
  5. package/src/core/agent/build-agent-system.js +4 -3
  6. package/src/core/agent/channels/voice-context.js +98 -0
  7. package/src/core/agent/memory.js +2 -1
  8. package/src/core/agent/prompt-builder.js +2 -1
  9. package/src/core/agent/prompts/modes/code-build.md +1 -0
  10. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  11. package/src/core/agent/prompts/modes/index.js +28 -0
  12. package/src/core/agent/skills/loader.js +22 -18
  13. package/src/core/agent/stream/turn-accumulator.js +73 -0
  14. package/src/core/agent/suggestions.js +37 -0
  15. package/src/core/agent/tools/handlers/add-project.js +5 -2
  16. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  17. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  18. package/src/core/agent/tools/helpers.js +2 -2
  19. package/src/core/agent/tools/names.js +138 -0
  20. package/src/core/agent/tools/registry-bridge.js +6 -14
  21. package/src/core/agent/tools/registry.js +68 -65
  22. package/src/core/apc/context-copy.js +27 -0
  23. package/src/core/apc/notes.js +19 -0
  24. package/src/core/apc/parser.js +13 -6
  25. package/src/core/apc/paths.js +87 -0
  26. package/src/core/apc/scaffold.js +82 -74
  27. package/src/core/apc/skill-sync.js +13 -1
  28. package/src/core/channels/telegram/dispatch.js +595 -0
  29. package/src/core/channels/telegram/helpers.js +130 -0
  30. package/src/core/config/index.js +3 -2
  31. package/src/core/config/redact.js +95 -0
  32. package/src/core/constants/channels.js +2 -0
  33. package/src/core/constants/code-modes.js +10 -0
  34. package/src/core/constants/index.js +1 -0
  35. package/src/core/deck/manifest.js +186 -0
  36. package/src/core/engines/catalog.js +83 -0
  37. package/src/core/engines/gemini.js +28 -11
  38. package/src/core/engines/index.js +11 -1
  39. package/src/core/{tools → http-tools}/browser.js +0 -1
  40. package/src/core/{tools → http-tools}/fetch.js +0 -1
  41. package/src/core/{tools → http-tools}/glob.js +0 -1
  42. package/src/core/{tools → http-tools}/grep.js +0 -1
  43. package/src/core/{tools → http-tools}/registry.js +0 -1
  44. package/src/core/{tools → http-tools}/search.js +0 -1
  45. package/src/core/i18n/en.js +9 -0
  46. package/src/core/i18n/es.js +12 -0
  47. package/src/core/i18n/index.js +54 -0
  48. package/src/core/i18n/pt.js +9 -0
  49. package/src/core/identity/telegram.js +2 -1
  50. package/src/core/mcp/runner.js +272 -14
  51. package/src/core/mcp/sources.js +3 -2
  52. package/src/core/routines/index.js +16 -0
  53. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  54. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  55. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  56. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  57. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  58. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  59. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  60. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  61. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  62. package/src/core/stores/code-sessions.js +50 -2
  63. package/src/core/stores/routine-memory.js +1 -1
  64. package/src/core/stores/sessions-search.js +121 -0
  65. package/src/core/stores/sessions.js +38 -0
  66. package/src/core/vars/index.js +14 -0
  67. package/src/core/vars/interpolate.js +86 -0
  68. package/src/core/vars/sources.js +151 -0
  69. package/src/core/voice/audio-decode.js +38 -0
  70. package/src/core/voice/transcription.js +225 -0
  71. package/src/host/daemon/api/admin-config.js +5 -82
  72. package/src/host/daemon/api/agents.js +5 -5
  73. package/src/host/daemon/api/code.js +17 -169
  74. package/src/host/daemon/api/config.js +3 -4
  75. package/src/host/daemon/api/conversations.js +8 -29
  76. package/src/host/daemon/api/deck.js +37 -404
  77. package/src/host/daemon/api/engines.js +1 -50
  78. package/src/host/daemon/api/exec.js +1 -1
  79. package/src/host/daemon/api/mcps.js +32 -0
  80. package/src/host/daemon/api/routines.js +1 -1
  81. package/src/host/daemon/api/runtimes.js +4 -3
  82. package/src/host/daemon/api/sessions-search.js +24 -140
  83. package/src/host/daemon/api/sessions.js +12 -30
  84. package/src/host/daemon/api/shared.js +2 -1
  85. package/src/host/daemon/api/telegram.js +1 -11
  86. package/src/host/daemon/api/tools.js +6 -6
  87. package/src/host/daemon/api/transcribe.js +2 -2
  88. package/src/host/daemon/api/vars.js +137 -0
  89. package/src/host/daemon/api/voice.js +13 -290
  90. package/src/host/daemon/api.js +2 -0
  91. package/src/host/daemon/db.js +6 -6
  92. package/src/host/daemon/deck-exec.js +148 -0
  93. package/src/host/daemon/index.js +3 -3
  94. package/src/host/daemon/plugins/telegram/index.js +24 -687
  95. package/src/host/daemon/routines-scheduler.js +64 -0
  96. package/src/host/daemon/smoke.js +3 -2
  97. package/src/host/daemon/whisper-server.js +225 -0
  98. package/src/interfaces/cli/commands/agent.js +3 -2
  99. package/src/interfaces/cli/commands/command.js +2 -3
  100. package/src/interfaces/cli/commands/messages.js +6 -2
  101. package/src/interfaces/cli/commands/pair.js +5 -4
  102. package/src/interfaces/cli/commands/search.js +1 -1
  103. package/src/interfaces/cli/commands/sessions.js +3 -2
  104. package/src/interfaces/cli/commands/skills.js +36 -55
  105. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  106. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  107. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  108. package/src/interfaces/web/dist/index.html +2 -2
  109. package/src/interfaces/web/package-lock.json +182 -182
  110. package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
  111. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  112. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  113. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  114. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  115. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  116. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  117. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  118. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  119. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  120. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  121. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  122. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  123. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  124. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  125. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  126. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  127. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  128. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  129. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  130. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  131. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  132. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  133. package/src/interfaces/web/src/constants/index.ts +1 -1
  134. package/src/interfaces/web/src/i18n/en.ts +174 -7
  135. package/src/interfaces/web/src/i18n/es.ts +179 -15
  136. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  137. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  138. package/src/interfaces/web/src/lib/api.ts +1 -0
  139. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  140. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  142. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  143. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  144. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  145. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  146. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  147. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  148. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  149. package/src/interfaces/web/src/types/daemon.ts +5 -0
  150. package/src/host/daemon/transcription.js +0 -538
  151. package/src/host/daemon/whisper-transcribe.py +0 -73
  152. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
  153. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
  154. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
  155. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  156. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  157. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  158. /package/src/core/{tools → http-tools}/index.js +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  167. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  168. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  169. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  170. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  171. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  172. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -1,6 +1,8 @@
1
- import { useEffect, useRef, useState } from "react";
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
  import { AlertTriangle, ChevronDown } from "lucide-react";
3
4
  import { cn } from "../lib/cn";
5
+ import { t } from "../i18n";
4
6
 
5
7
  // Editable combobox: type freely, matching options appear below; click one to
6
8
  // pick it, or keep your own text. Behaves like a text input that is also a
@@ -25,21 +27,50 @@ export function ModelCombobox({
25
27
  const [open, setOpen] = useState(false);
26
28
  const [query, setQuery] = useState(value);
27
29
  const wrapRef = useRef<HTMLDivElement | null>(null);
30
+ const listRef = useRef<HTMLUListElement | null>(null);
31
+ const [menuRect, setMenuRect] = useState<{ top: number; left: number; width: number } | null>(null);
28
32
 
29
33
  useEffect(() => { setQuery(value); }, [value]);
30
34
 
31
- // Close on outside click.
35
+ // Position the portal'd list right under the input, in viewport coords.
36
+ // Recompute on open, scroll, and resize so it tracks the trigger even
37
+ // inside a scrolling modal.
38
+ useLayoutEffect(() => {
39
+ if (!open) return;
40
+ const compute = () => {
41
+ const el = wrapRef.current;
42
+ if (!el) return;
43
+ const r = el.getBoundingClientRect();
44
+ setMenuRect({ top: r.bottom + 4, left: r.left, width: r.width });
45
+ };
46
+ compute();
47
+ window.addEventListener("scroll", compute, true);
48
+ window.addEventListener("resize", compute);
49
+ return () => {
50
+ window.removeEventListener("scroll", compute, true);
51
+ window.removeEventListener("resize", compute);
52
+ };
53
+ }, [open]);
54
+
55
+ // Close on outside click — must allow clicks inside the portal'd list too.
32
56
  useEffect(() => {
33
57
  if (!open) return;
34
58
  const onDoc = (e: MouseEvent) => {
35
- if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
59
+ const t = e.target as Node;
60
+ if (wrapRef.current?.contains(t)) return;
61
+ if (listRef.current?.contains(t)) return;
62
+ setOpen(false);
36
63
  };
37
64
  document.addEventListener("mousedown", onDoc);
38
65
  return () => document.removeEventListener("mousedown", onDoc);
39
66
  }, [open]);
40
67
 
68
+ // If the visible text matches the committed value, the user hasn't typed
69
+ // anything new yet — show the full list so they can see every option.
70
+ // Only narrow when they actively start filtering.
41
71
  const q = query.trim().toLowerCase();
42
- const filtered = q ? options.filter((o) => o.toLowerCase().includes(q)) : options;
72
+ const isUntouched = query === value;
73
+ const filtered = q && !isUntouched ? options.filter((o) => o.toLowerCase().includes(q)) : options;
43
74
 
44
75
  const pick = (o: string) => { onChange(o); setQuery(o); setOpen(false); };
45
76
 
@@ -52,7 +83,7 @@ export function ModelCombobox({
52
83
  )}
53
84
  >
54
85
  {invalid && (
55
- <span title={invalidHint || "Modelo/proveedor no disponible"}>
86
+ <span title={invalidHint || t("models_ui.invalid_hint")}>
56
87
  <AlertTriangle className="size-3.5 shrink-0 text-amber-400" />
57
88
  </span>
58
89
  )}
@@ -73,8 +104,12 @@ export function ModelCombobox({
73
104
  </button>
74
105
  </div>
75
106
 
76
- {open && filtered.length > 0 && (
77
- <ul className="absolute z-50 mt-1 max-h-56 w-full overflow-y-auto rounded-lg border border-border bg-popover p-1 shadow-md ring-1 ring-foreground/10">
107
+ {open && filtered.length > 0 && menuRect && createPortal(
108
+ <ul
109
+ ref={listRef}
110
+ style={{ position: "fixed", top: menuRect.top, left: menuRect.left, width: menuRect.width }}
111
+ className="z-[1000] max-h-56 overflow-y-auto rounded-lg border border-border bg-popover p-1 shadow-md ring-1 ring-foreground/10"
112
+ >
78
113
  {filtered.map((o) => (
79
114
  <li key={o}>
80
115
  <button
@@ -89,7 +124,8 @@ export function ModelCombobox({
89
124
  </button>
90
125
  </li>
91
126
  ))}
92
- </ul>
127
+ </ul>,
128
+ document.body,
93
129
  )}
94
130
  </div>
95
131
  );
@@ -43,7 +43,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
43
43
  <Dialog
44
44
  open={!!channel}
45
45
  onClose={onClose}
46
- title={channel?.name ? `Editar canal: ${channel.name}` : "Nuevo canal de Telegram"}
46
+ title={channel?.name ? t("telegram_channel_dialog.edit_title", { name: channel.name }) : t("telegram_channel_dialog.new_title")}
47
47
  description="POST /telegram/channels (upsert) — PATCH /telegram/channels/:name (parcial)."
48
48
  footer={
49
49
  <>
@@ -0,0 +1,76 @@
1
+ import { MessageCircleQuestion } from "lucide-react";
2
+ import { cn } from "../../lib/cn";
3
+ import { t } from "../../i18n";
4
+
5
+ interface QA {
6
+ question: string;
7
+ answer: string;
8
+ skipped: boolean;
9
+ }
10
+
11
+ // Parse the compiled answer text emitted by InlineAskPanel.compileAnswers.
12
+ // Returns null when the text doesn't match the expected pattern (so callers
13
+ // can fall back to the standard user bubble).
14
+ export function parseAskAnswerText(text: string): QA[] | null {
15
+ const lines = text.split("\n");
16
+ const pairs: QA[] = [];
17
+ let current: QA | null = null;
18
+ for (const line of lines) {
19
+ if (line.startsWith("- ")) {
20
+ if (current) pairs.push(current);
21
+ current = { question: line.slice(2), answer: "", skipped: false };
22
+ } else if (line.startsWith(" → ") && current) {
23
+ const a = line.slice(4);
24
+ current.answer = a;
25
+ current.skipped = a === "(omitido)";
26
+ } else {
27
+ return null;
28
+ }
29
+ }
30
+ if (current) pairs.push(current);
31
+ return pairs.length > 0 ? pairs : null;
32
+ }
33
+
34
+ interface Props {
35
+ text: string;
36
+ }
37
+
38
+ // Full-width centered card rendered between the assistant turn that asked the
39
+ // questions and the next assistant turn. Replaces the right-aligned user bubble
40
+ // so the Q&A reads as a single coherent block instead of an opaque user reply.
41
+ export function AskAnswersCard({ text }: Props) {
42
+ const pairs = parseAskAnswerText(text);
43
+ if (!pairs) return null;
44
+ return (
45
+ <div className="flex w-full justify-center">
46
+ <div
47
+ className="w-full max-w-[85%] rounded-2xl border border-border/70 bg-card/40 px-4 py-3 shadow-sm"
48
+ data-testid="ask-answers-card"
49
+ >
50
+ <div className="mb-2 flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
51
+ <MessageCircleQuestion className="size-3.5" />
52
+ <span>{t("ask_panel.answers_header")}</span>
53
+ </div>
54
+ <ul className="space-y-2.5">
55
+ {pairs.map((p, i) => (
56
+ <li key={i} className="space-y-0.5">
57
+ <div className="text-sm font-medium leading-snug text-foreground">
58
+ {p.question}
59
+ </div>
60
+ <div
61
+ className={cn(
62
+ "whitespace-pre-wrap text-[13px] leading-snug",
63
+ p.skipped
64
+ ? "italic text-muted-foreground/70"
65
+ : "text-muted-foreground",
66
+ )}
67
+ >
68
+ {p.answer}
69
+ </div>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -2,7 +2,9 @@ import { Bot, Copy, User, Info } from "lucide-react";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { ToolCall } from "./ToolCall";
4
4
  import { AskQuestionsCard } from "./AskQuestionsCard";
5
+ import { AskAnswersCard, parseAskAnswerText } from "./AskAnswersCard";
5
6
  import { textOf, type ChatMsg } from "../../hooks/useChat";
7
+ import { t } from "../../i18n";
6
8
 
7
9
  interface Props {
8
10
  msg: ChatMsg;
@@ -10,14 +12,24 @@ interface Props {
10
12
  * ask_questions tool call is still waiting for the user vs already answered
11
13
  * (a later user message would push this assistant turn off the bottom). */
12
14
  isLast?: boolean;
15
+ /** True when this user message is the reply to a preceding `ask_questions`
16
+ * call. Renders as a full-width centered card instead of the user bubble. */
17
+ isAskAnswer?: boolean;
13
18
  onCopy?: (text: string) => void;
14
19
  }
15
20
 
16
- export function MessageBubble({ msg, isLast, onCopy }: Props) {
21
+ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
17
22
  const mine = msg.role === "user";
18
23
  const copyText = textOf(msg);
19
24
  const hasTools = msg.parts.some((p) => p.kind === "tool");
20
25
 
26
+ if (mine && isAskAnswer) {
27
+ const text = textOf(msg);
28
+ if (parseAskAnswerText(text)) {
29
+ return <AskAnswersCard text={text} />;
30
+ }
31
+ }
32
+
21
33
  return (
22
34
  <div className={cn("group flex items-start gap-2", mine ? "justify-end" : "justify-start")}>
23
35
  {!mine && (
@@ -82,9 +94,10 @@ export function MessageBubble({ msg, isLast, onCopy }: Props) {
82
94
  type="button"
83
95
  onClick={() => onCopy(copyText)}
84
96
  className="inline-flex items-center gap-1 hover:text-foreground"
85
- title="Copiar"
97
+ title={t("chat_ui.copy")}
98
+ aria-label={t("chat_ui.copy")}
86
99
  >
87
- <Copy size={10} /> copiar
100
+ <Copy size={10} /> {t("chat_ui.copy")}
88
101
  </button>
89
102
  )}
90
103
  </div>
@@ -28,9 +28,31 @@ export function MessageList({ msgs, onCopy }: Props) {
28
28
  return (
29
29
  <div className="space-y-4 px-3 py-4">
30
30
  {msgs.map((m, i) => (
31
- <MessageBubble key={i} msg={m} isLast={i === lastIdx} onCopy={onCopy} />
31
+ <MessageBubble
32
+ key={i}
33
+ msg={m}
34
+ isLast={i === lastIdx}
35
+ isAskAnswer={isAnswerToAsk(msgs, i)}
36
+ onCopy={onCopy}
37
+ />
32
38
  ))}
33
39
  <div ref={bottomRef} />
34
40
  </div>
35
41
  );
36
42
  }
43
+
44
+ // A user message is an "ask answer" when the preceding assistant turn ended on
45
+ // an ask_questions tool call (its last tool part). The InlineAskPanel compiles
46
+ // the user's picks into a single text reply, which we then render as a centered
47
+ // full-width card instead of the standard right-aligned user bubble.
48
+ function isAnswerToAsk(msgs: ChatMsg[], i: number): boolean {
49
+ const m = msgs[i];
50
+ if (!m || m.role !== "user") return false;
51
+ const prev = msgs[i - 1];
52
+ if (!prev || prev.role !== "assistant") return false;
53
+ for (let j = prev.parts.length - 1; j >= 0; j--) {
54
+ const p = prev.parts[j];
55
+ if (p.kind === "tool") return p.tool === "ask_questions";
56
+ }
57
+ return false;
58
+ }
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
2
2
  import { Server, ChevronDown, X, Check } from "lucide-react";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { Engines } from "../../lib/api";
5
+ import { t } from "../../i18n";
5
6
 
6
7
  // Compact model picker for the chat composer (the panda.project pattern): a
7
8
  // small "Server · <model>" button that opens a dropdown to pick a model
@@ -79,7 +80,8 @@ export function ModelPicker({
79
80
  "hover:bg-accent/60 hover:text-foreground",
80
81
  value && "text-foreground",
81
82
  )}
82
- title="Elegir modelo (o Auto)"
83
+ title={t("chat_ui.pick_model")}
84
+ aria-label={t("chat_ui.pick_model")}
83
85
  >
84
86
  <Server className="size-3 shrink-0" />
85
87
  <span className="truncate font-mono">{label}</span>
@@ -137,7 +137,7 @@ function ArtifactRow({
137
137
  ) : (
138
138
  <span className="min-w-0 flex-1 truncate font-mono">{entry.name}</span>
139
139
  )}
140
- <Tip content="Renombrar">
140
+ <Tip content={t("code_module.artifacts_rename")}>
141
141
  <button
142
142
  type="button"
143
143
  onClick={startRename}
@@ -170,7 +170,7 @@ function ArtifactRow({
170
170
  <div className="flex flex-wrap items-center gap-1 mt-1">
171
171
  {/* Ver button */}
172
172
  <Dialog open={viewOpen} onOpenChange={setViewOpen}>
173
- <Tip content="Ver contenido">
173
+ <Tip content={t("code_module.artifacts_view")}>
174
174
  <button
175
175
  type="button"
176
176
  onClick={() => setViewOpen(true)}
@@ -198,7 +198,7 @@ function ArtifactRow({
198
198
  </Dialog>
199
199
 
200
200
  {/* Editar — opens as a file tab in the main panel */}
201
- <Tip content="Editar contenido">
201
+ <Tip content={t("code_module.artifacts_edit")}>
202
202
  <button
203
203
  type="button"
204
204
  onClick={() => onEditArtifact?.(entry.name)}
@@ -338,7 +338,7 @@ export function CodeArtifactsTab({ pid, onRunInTerminal, onEditArtifact }: Props
338
338
  ? t("code_module.artifacts_count", { n: entries.length })
339
339
  : ""}
340
340
  </span>
341
- <Tip content="Recargar">
341
+ <Tip content={t("code_module.reload")}>
342
342
  <button
343
343
  type="button"
344
344
  onClick={() => void list.mutate()}
@@ -61,7 +61,7 @@ export function CodeChangesTab({ changes, loading, onRefresh }: Props) {
61
61
  <span className="text-[11px] text-muted-foreground">
62
62
  {files.length > 0 ? t("code_module.changes_files", { n: files.length }) : ""}
63
63
  </span>
64
- <Tip content="Recargar">
64
+ <Tip content={t("code_module.reload")}>
65
65
  <button
66
66
  type="button"
67
67
  onClick={onRefresh}
@@ -4,6 +4,7 @@ import { cn } from "../../lib/cn";
4
4
  import { Empty, Spinner } from "../ui";
5
5
  import { Tip } from "../ui/tip";
6
6
  import { http } from "../../lib/http";
7
+ import { t } from "../../i18n";
7
8
 
8
9
  interface FileNode {
9
10
  name: string;
@@ -164,7 +165,7 @@ export function CodeFileTree({
164
165
  <div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
165
166
  <span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Archivos</span>
166
167
  <div className="flex items-center gap-0.5">
167
- <Tip content="Colapsar todo">
168
+ <Tip content={t("code_module.tree_collapse_all")}>
168
169
  <button
169
170
  type="button"
170
171
  onClick={collapseAll}
@@ -174,7 +175,7 @@ export function CodeFileTree({
174
175
  <ChevronsUpDown className="size-3" />
175
176
  </button>
176
177
  </Tip>
177
- <Tip content="Recargar">
178
+ <Tip content={t("code_module.reload")}>
178
179
  <button
179
180
  type="button"
180
181
  onClick={() => void loadFiles()}
@@ -3,6 +3,7 @@ import { Save, RotateCcw } from "lucide-react";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { Spinner } from "../ui";
5
5
  import { Tip } from "../ui/tip";
6
+ import { t } from "../../i18n";
6
7
 
7
8
  export function CodeFileViewer({
8
9
  path,
@@ -47,7 +48,7 @@ export function CodeFileViewer({
47
48
  </span>
48
49
  {editable && (
49
50
  <>
50
- <Tip content="Descartar cambios">
51
+ <Tip content={t("code_module.discard_changes")}>
51
52
  <button
52
53
  type="button"
53
54
  onClick={() => setDraft(content)}
@@ -58,7 +59,7 @@ export function CodeFileViewer({
58
59
  Descartar
59
60
  </button>
60
61
  </Tip>
61
- <Tip content="Guardar (Cmd/Ctrl+S)">
62
+ <Tip content={t("code_module.save_shortcut_hint")}>
62
63
  <button
63
64
  type="button"
64
65
  onClick={() => void save()}
@@ -3,6 +3,7 @@ import { Terminal as TerminalIcon, Eraser, X } from "lucide-react";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { Tip } from "../ui/tip";
5
5
  import { http } from "../../lib/http";
6
+ import { t } from "../../i18n";
6
7
 
7
8
  interface Line {
8
9
  type: "cmd" | "out" | "err";
@@ -83,7 +84,7 @@ export function CodeTerminal({
83
84
  <div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1">
84
85
  <TerminalIcon className="size-3 text-muted-foreground" />
85
86
  <span className="flex-1 text-[11px] text-muted-foreground">Terminal</span>
86
- <Tip content="Limpiar">
87
+ <Tip content={t("code_module.terminal_clear")}>
87
88
  <button
88
89
  type="button"
89
90
  onClick={() => setLines([])}
@@ -92,7 +93,7 @@ export function CodeTerminal({
92
93
  <Eraser className="size-3" />
93
94
  </button>
94
95
  </Tip>
95
- <Tip content="Cerrar terminal">
96
+ <Tip content={t("code_module.terminal_close")}>
96
97
  <button
97
98
  type="button"
98
99
  onClick={() => onClose?.()}
@@ -5,6 +5,7 @@ import { useGlobalConfig } from "../../hooks/useGlobalConfig";
5
5
  import { flattenObject } from "../../lib/config-values";
6
6
  import { isSecretMarker } from "../../lib/secrets";
7
7
  import { Loading } from "../ui";
8
+ import { t } from "../../i18n";
8
9
 
9
10
  export function GlobalConfigEditor() {
10
11
  const { config, isLoading, patch, mutate } = useGlobalConfig();
@@ -23,7 +24,7 @@ export function GlobalConfigEditor() {
23
24
 
24
25
  return (
25
26
  <Section
26
- title="Config APX"
27
+ title={t("global_config.title")}
27
28
  description="Config general en ~/.apx/config.json. Editable por tabs; JSON queda separado."
28
29
  >
29
30
  <ConfigTabsEditor
@@ -2,6 +2,7 @@ import { useState } from "react";
2
2
  import { Badge, Switch } from "../ui";
3
3
  import { cn } from "../../lib/cn";
4
4
  import type { DeckWidget } from "../../lib/api/deck";
5
+ import { t } from "../../i18n";
5
6
 
6
7
  // Status badge tone mapping.
7
8
  function statusTone(s: DeckWidget["status"]): "success" | "muted" | "warning" | "info" {
@@ -63,7 +64,7 @@ export function WidgetRow({ widget, onToggle }: WidgetRowProps) {
63
64
  >
64
65
  {/* Source dot */}
65
66
  <span
66
- title={widget.source === "apx" ? "Widget nativo APX" : "Widget externo"}
67
+ title={widget.source === "apx" ? t("deck_screen.widget_native") : t("deck_screen.widget_external")}
67
68
  className={cn(
68
69
  "size-2 shrink-0 rounded-full",
69
70
  widget.source === "apx" ? "bg-emerald-500" : "bg-sky-400"
@@ -0,0 +1,93 @@
1
+ import { Plus, Trash2 } from "lucide-react";
2
+ import { Button, Input } from "../ui";
3
+ import { VarTokenInput } from "./VarTokenInput";
4
+
5
+ // Editable list of {key,value} pairs. Used for MCP env (stdio) and headers
6
+ // (http). Values are run through VarTokenInput so `${var.X}` references
7
+ // render as inline badges and can be inserted via the picker.
8
+
9
+ export interface KvRow {
10
+ key: string;
11
+ value: string;
12
+ }
13
+
14
+ export function rowsFromRecord(rec?: Record<string, string> | null): KvRow[] {
15
+ if (!rec) return [];
16
+ return Object.entries(rec).map(([key, value]) => ({ key, value: String(value) }));
17
+ }
18
+
19
+ export function recordFromRows(rows: KvRow[]): Record<string, string> {
20
+ const out: Record<string, string> = {};
21
+ for (const r of rows) {
22
+ if (!r.key.trim()) continue;
23
+ out[r.key.trim()] = r.value;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ interface KeyValueListProps {
29
+ rows: KvRow[];
30
+ onChange: (next: KvRow[]) => void;
31
+ keyPlaceholder?: string;
32
+ valuePlaceholder?: string;
33
+ varNames?: string[];
34
+ onCreateVar?: () => void;
35
+ emptyLabel?: string;
36
+ }
37
+
38
+ export function KeyValueList({
39
+ rows,
40
+ onChange,
41
+ keyPlaceholder = "KEY",
42
+ valuePlaceholder = "value",
43
+ varNames,
44
+ onCreateVar,
45
+ emptyLabel,
46
+ }: KeyValueListProps) {
47
+ const update = (i: number, patch: Partial<KvRow>) => {
48
+ const next = rows.slice();
49
+ next[i] = { ...next[i], ...patch };
50
+ onChange(next);
51
+ };
52
+ const remove = (i: number) => onChange(rows.filter((_, j) => j !== i));
53
+ const add = () => onChange([...rows, { key: "", value: "" }]);
54
+
55
+ return (
56
+ <div className="space-y-2">
57
+ {rows.length === 0 && emptyLabel && (
58
+ <p className="text-[11px] text-muted-foreground">{emptyLabel}</p>
59
+ )}
60
+ {rows.map((row, i) => (
61
+ <div key={i} className="flex items-start gap-2">
62
+ <Input
63
+ value={row.key}
64
+ onChange={(e) => update(i, { key: e.target.value })}
65
+ placeholder={keyPlaceholder}
66
+ className="w-40 font-mono text-xs"
67
+ />
68
+ <div className="flex-1">
69
+ <VarTokenInput
70
+ value={row.value}
71
+ onChange={(v) => update(i, { value: v })}
72
+ placeholder={valuePlaceholder}
73
+ varNames={varNames}
74
+ onCreateVar={onCreateVar}
75
+ />
76
+ </div>
77
+ <Button
78
+ type="button"
79
+ size="sm"
80
+ variant="ghost"
81
+ onClick={() => remove(i)}
82
+ aria-label="quitar fila"
83
+ >
84
+ <Trash2 size={13} />
85
+ </Button>
86
+ </div>
87
+ ))}
88
+ <Button type="button" size="sm" variant="ghost" onClick={add}>
89
+ <Plus size={12} /> Agregar fila
90
+ </Button>
91
+ </div>
92
+ );
93
+ }