@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
@@ -0,0 +1,449 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { Plus } from "lucide-react";
10
+ import { cn } from "../../lib/cn";
11
+ import { t } from "../../i18n";
12
+
13
+ // VarTokenInput
14
+ // -------------
15
+ // Single-line editor that renders `${var.NAME}` references as inline badges.
16
+ // The wrapper mirrors the shadcn <Input> visual (bg-transparent, border-input,
17
+ // focus-visible ring) so a row of regular Inputs and a row of VarTokenInputs
18
+ // look identical to the user. The badge text shows only the var NAME — the
19
+ // `${var…}` wrapper is implementation detail.
20
+
21
+ interface Token {
22
+ type: "text" | "var";
23
+ value: string;
24
+ }
25
+
26
+ const VAR_PATTERN = "\\$\\{var\\.([^}\\s]+)\\}";
27
+
28
+ function makeVarRe(flags = "g"): RegExp {
29
+ return new RegExp(VAR_PATTERN, flags);
30
+ }
31
+
32
+ function hasVarRef(s: string): boolean {
33
+ return makeVarRe("").test(s);
34
+ }
35
+
36
+ export function parseTokens(value: string): Token[] {
37
+ const out: Token[] = [];
38
+ let last = 0;
39
+ for (const m of value.matchAll(makeVarRe("g"))) {
40
+ const start = m.index ?? 0;
41
+ if (start > last) out.push({ type: "text", value: value.slice(last, start) });
42
+ out.push({ type: "var", value: m[1] });
43
+ last = start + m[0].length;
44
+ }
45
+ if (last < value.length) out.push({ type: "text", value: value.slice(last) });
46
+ return out;
47
+ }
48
+
49
+ function serializeDom(root: HTMLElement): string {
50
+ let out = "";
51
+ for (const node of Array.from(root.childNodes)) {
52
+ if (node.nodeType === Node.TEXT_NODE) {
53
+ out += node.textContent ?? "";
54
+ } else if (node instanceof HTMLElement) {
55
+ const name = node.dataset.varName;
56
+ if (name) out += `\${var.${name}}`;
57
+ else if (node.tagName === "BR") out += "";
58
+ else out += node.textContent ?? "";
59
+ }
60
+ }
61
+ // Strip zero-width chars (caret spacers + paste artifacts) and convert
62
+ // non-breaking spaces (U+00A0) back to regular spaces. Both are normal
63
+ // contentEditable side-effects but they poison header values — Asana, for
64
+ // one, rejects `Bearer\xA0token` because it expects a literal ASCII space.
65
+ return out
66
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2060\uFEFF]/g, "")
67
+ .replace(/\u00A0/g, " ");
68
+ }
69
+
70
+ function renderDom(root: HTMLElement, value: string) {
71
+ root.replaceChildren();
72
+ const tokens = parseTokens(value);
73
+ for (const tok of tokens) {
74
+ if (tok.type === "text") {
75
+ root.appendChild(document.createTextNode(tok.value));
76
+ } else {
77
+ root.appendChild(buildBadge(tok.value));
78
+ }
79
+ }
80
+ if (tokens.length === 0 || tokens[tokens.length - 1].type === "var") {
81
+ root.appendChild(document.createTextNode(""));
82
+ }
83
+ }
84
+
85
+ function buildBadge(name: string): HTMLSpanElement {
86
+ const span = document.createElement("span");
87
+ span.contentEditable = "false";
88
+ span.dataset.varName = name;
89
+ // Subtle inline-tag look: no chip-style border, just a tinted background and
90
+ // a leading "$" marker so the var ref reads as part of the line, not as a
91
+ // floating element on top of the input.
92
+ span.className =
93
+ "inline-flex items-baseline px-1 rounded bg-primary/10 text-primary font-mono text-[12px] select-none cursor-default whitespace-nowrap";
94
+ span.textContent = `$${name}`;
95
+ span.title = `\${var.${name}}`;
96
+ return span;
97
+ }
98
+
99
+ function insertAtRange(range: Range, fragment: Node) {
100
+ range.deleteContents();
101
+ range.insertNode(fragment);
102
+ range.setStartAfter(fragment);
103
+ range.collapse(true);
104
+ const sel = window.getSelection();
105
+ sel?.removeAllRanges();
106
+ sel?.addRange(range);
107
+ }
108
+
109
+ function placeCaretAtEnd(root: HTMLElement) {
110
+ const range = document.createRange();
111
+ range.selectNodeContents(root);
112
+ range.collapse(false);
113
+ const sel = window.getSelection();
114
+ sel?.removeAllRanges();
115
+ sel?.addRange(range);
116
+ }
117
+
118
+ export interface VarTokenInputHandle {
119
+ insertVar: (name: string) => void;
120
+ focus: () => void;
121
+ }
122
+
123
+ interface VarTokenInputProps {
124
+ value: string;
125
+ onChange: (next: string) => void;
126
+ placeholder?: string;
127
+ className?: string;
128
+ varNames?: string[];
129
+ onCreateVar?: () => void;
130
+ }
131
+
132
+ export const VarTokenInput = forwardRef<VarTokenInputHandle, VarTokenInputProps>(
133
+ function VarTokenInput({ value, onChange, placeholder, className, varNames = [], onCreateVar }, ref) {
134
+ const editorRef = useRef<HTMLDivElement>(null);
135
+ const savedRange = useRef<Range | null>(null);
136
+ const [pickerOpen, setPickerOpen] = useState(false);
137
+ const [pickerQuery, setPickerQuery] = useState("");
138
+ const lastSerialized = useRef(value);
139
+
140
+ // Sync DOM <- props.value only when external value diverges. Typing inside
141
+ // the editor never round-trips through React, which keeps the caret put.
142
+ useEffect(() => {
143
+ const el = editorRef.current;
144
+ if (!el) return;
145
+ if (value === lastSerialized.current && el.childNodes.length > 0) return;
146
+ renderDom(el, value);
147
+ lastSerialized.current = value;
148
+ }, [value]);
149
+
150
+ const emit = useCallback(() => {
151
+ const el = editorRef.current;
152
+ if (!el) return;
153
+ const s = serializeDom(el);
154
+ lastSerialized.current = s;
155
+ if (s !== value) onChange(s);
156
+ }, [onChange, value]);
157
+
158
+ const handleInput = useCallback(() => {
159
+ const el = editorRef.current;
160
+ if (!el) return;
161
+ const current = serializeDom(el);
162
+ if (hasVarRef(current) && hasUnbadgedRef(el)) {
163
+ const caretChar = caretCharOffset(el);
164
+ renderDom(el, current);
165
+ if (caretChar != null) restoreCaret(el, caretChar);
166
+ }
167
+ lastSerialized.current = serializeDom(el);
168
+ if (lastSerialized.current !== value) onChange(lastSerialized.current);
169
+ }, [onChange, value]);
170
+
171
+ const saveSelection = useCallback(() => {
172
+ const sel = window.getSelection();
173
+ if (!sel || sel.rangeCount === 0) return;
174
+ const range = sel.getRangeAt(0);
175
+ const el = editorRef.current;
176
+ if (el && el.contains(range.startContainer)) {
177
+ savedRange.current = range.cloneRange();
178
+ }
179
+ }, []);
180
+
181
+ const handleKeyDown = useCallback(
182
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
183
+ if (e.key === "Enter") {
184
+ // Single-line editor: never insert a newline.
185
+ e.preventDefault();
186
+ (e.currentTarget as HTMLElement).blur();
187
+ return;
188
+ }
189
+ if (e.key === "Backspace") {
190
+ const sel = window.getSelection();
191
+ if (!sel || sel.rangeCount === 0) return;
192
+ const range = sel.getRangeAt(0);
193
+ if (!range.collapsed) return;
194
+ const { startContainer, startOffset } = range;
195
+ if (startContainer.nodeType === Node.TEXT_NODE && startOffset === 0) {
196
+ const prev = startContainer.previousSibling;
197
+ if (prev instanceof HTMLElement && prev.dataset.varName) {
198
+ e.preventDefault();
199
+ prev.remove();
200
+ emit();
201
+ return;
202
+ }
203
+ } else if (startContainer === editorRef.current) {
204
+ const prev = startContainer.childNodes[startOffset - 1];
205
+ if (prev instanceof HTMLElement && prev.dataset.varName) {
206
+ e.preventDefault();
207
+ prev.remove();
208
+ emit();
209
+ return;
210
+ }
211
+ }
212
+ }
213
+ },
214
+ [emit],
215
+ );
216
+
217
+ const insertVar = useCallback(
218
+ (name: string) => {
219
+ const el = editorRef.current;
220
+ if (!el) return;
221
+ const restored = savedRange.current;
222
+ el.focus();
223
+ let range: Range;
224
+ if (restored && el.contains(restored.startContainer)) {
225
+ range = restored;
226
+ } else {
227
+ range = document.createRange();
228
+ range.selectNodeContents(el);
229
+ range.collapse(false);
230
+ }
231
+ insertAtRange(range, buildBadge(name));
232
+ savedRange.current = null;
233
+ emit();
234
+ },
235
+ [emit],
236
+ );
237
+
238
+ useImperativeHandle(
239
+ ref,
240
+ () => ({
241
+ insertVar,
242
+ focus: () => editorRef.current?.focus(),
243
+ }),
244
+ [insertVar],
245
+ );
246
+
247
+ const filtered = varNames.filter((n) =>
248
+ n.toLowerCase().includes(pickerQuery.toLowerCase()),
249
+ );
250
+
251
+ // Editor classes mirror shadcn <Input> so a row of VarTokenInputs sits
252
+ // next to plain Inputs without visual drift.
253
+ return (
254
+ <div
255
+ className={cn(
256
+ "group flex items-stretch w-full min-w-0 rounded-lg border border-input bg-transparent dark:bg-input/30 transition-colors",
257
+ "focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50",
258
+ className,
259
+ )}
260
+ >
261
+ <div className="relative flex-1 min-w-0">
262
+ <div
263
+ ref={editorRef}
264
+ role="textbox"
265
+ contentEditable
266
+ suppressContentEditableWarning
267
+ onInput={handleInput}
268
+ onKeyDown={handleKeyDown}
269
+ onBlur={emit}
270
+ onFocus={saveSelection}
271
+ onMouseUp={saveSelection}
272
+ onKeyUp={saveSelection}
273
+ className={cn(
274
+ "h-8 w-full whitespace-nowrap overflow-x-auto px-2.5 py-1 text-sm rounded-l-lg",
275
+ "focus:outline-none font-mono leading-7",
276
+ "[&_*]:align-baseline",
277
+ )}
278
+ data-placeholder={placeholder || ""}
279
+ style={{ caretColor: "currentColor" }}
280
+ />
281
+ {value === "" && placeholder && (
282
+ <span className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 select-none text-sm text-muted-foreground font-mono">
283
+ {placeholder}
284
+ </span>
285
+ )}
286
+ </div>
287
+ <div className="relative flex">
288
+ <button
289
+ type="button"
290
+ // onMouseDown prevents the editor from blurring before we capture
291
+ // the saved range — that's what kept the caret jumping to start.
292
+ onMouseDown={(e) => {
293
+ e.preventDefault();
294
+ saveSelection();
295
+ }}
296
+ onClick={() => setPickerOpen((v) => !v)}
297
+ aria-label={t("chat_ui.insert_variable")}
298
+ title={t("chat_ui.insert_variable")}
299
+ className={cn(
300
+ "flex items-center justify-center px-2 min-w-8 border-l border-input text-muted-foreground rounded-r-lg",
301
+ "hover:bg-muted/60 hover:text-foreground transition-colors",
302
+ pickerOpen && "bg-muted/60 text-foreground",
303
+ )}
304
+ >
305
+ <Plus size={14} />
306
+ </button>
307
+ {pickerOpen && (
308
+ <VarPickerPopover
309
+ query={pickerQuery}
310
+ onQuery={setPickerQuery}
311
+ varNames={filtered}
312
+ onPick={(n) => {
313
+ insertVar(n);
314
+ setPickerOpen(false);
315
+ setPickerQuery("");
316
+ }}
317
+ onClose={() => {
318
+ setPickerOpen(false);
319
+ setPickerQuery("");
320
+ }}
321
+ onCreateVar={onCreateVar}
322
+ />
323
+ )}
324
+ </div>
325
+ </div>
326
+ );
327
+ },
328
+ );
329
+
330
+ function VarPickerPopover({
331
+ query,
332
+ onQuery,
333
+ varNames,
334
+ onPick,
335
+ onClose,
336
+ onCreateVar,
337
+ }: {
338
+ query: string;
339
+ onQuery: (s: string) => void;
340
+ varNames: string[];
341
+ onPick: (n: string) => void;
342
+ onClose: () => void;
343
+ onCreateVar?: () => void;
344
+ }) {
345
+ const ref = useRef<HTMLDivElement>(null);
346
+ const inputRef = useRef<HTMLInputElement>(null);
347
+ // Focus the search field once on open, then never again — re-focus on every
348
+ // render is what made the editor click "bounce" back to the picker.
349
+ useEffect(() => {
350
+ inputRef.current?.focus({ preventScroll: true });
351
+ }, []);
352
+ useEffect(() => {
353
+ function onDoc(e: MouseEvent) {
354
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
355
+ }
356
+ document.addEventListener("mousedown", onDoc);
357
+ return () => document.removeEventListener("mousedown", onDoc);
358
+ }, [onClose]);
359
+ return (
360
+ <div
361
+ ref={ref}
362
+ className="absolute right-0 top-full z-50 mt-1 w-64 rounded-md border border-border bg-popover shadow-lg"
363
+ >
364
+ <div className="border-b border-border p-2">
365
+ <input
366
+ ref={inputRef}
367
+ value={query}
368
+ onChange={(e) => onQuery(e.target.value)}
369
+ placeholder="buscar variable…"
370
+ className="w-full rounded bg-muted/40 px-2 py-1 text-xs font-mono outline-none"
371
+ />
372
+ </div>
373
+ <ul className="max-h-44 overflow-auto p-1 text-xs">
374
+ {varNames.length === 0 && (
375
+ <li className="px-2 py-1.5 text-muted-foreground">sin coincidencias</li>
376
+ )}
377
+ {varNames.map((n) => (
378
+ <li key={n}>
379
+ <button
380
+ type="button"
381
+ // Prevent the editor blur that would happen before onClick fires.
382
+ onMouseDown={(e) => e.preventDefault()}
383
+ onClick={() => onPick(n)}
384
+ className="block w-full rounded px-2 py-1.5 text-left font-mono hover:bg-muted/60"
385
+ >
386
+ {n}
387
+ </button>
388
+ </li>
389
+ ))}
390
+ </ul>
391
+ {onCreateVar && (
392
+ <div className="border-t border-border p-1">
393
+ <button
394
+ type="button"
395
+ onMouseDown={(e) => e.preventDefault()}
396
+ onClick={() => {
397
+ onCreateVar();
398
+ onClose();
399
+ }}
400
+ className="flex w-full items-center gap-1 rounded px-2 py-1.5 text-left text-xs hover:bg-muted/60"
401
+ >
402
+ <Plus size={12} /> Crear nueva variable…
403
+ </button>
404
+ </div>
405
+ )}
406
+ </div>
407
+ );
408
+ }
409
+
410
+ function hasUnbadgedRef(root: HTMLElement): boolean {
411
+ for (const node of Array.from(root.childNodes)) {
412
+ if (node.nodeType === Node.TEXT_NODE) {
413
+ if (hasVarRef(node.textContent ?? "")) return true;
414
+ }
415
+ }
416
+ return false;
417
+ }
418
+
419
+ function caretCharOffset(root: HTMLElement): number | null {
420
+ const sel = window.getSelection();
421
+ if (!sel || sel.rangeCount === 0) return null;
422
+ const range = sel.getRangeAt(0);
423
+ if (!root.contains(range.startContainer)) return null;
424
+ const pre = range.cloneRange();
425
+ pre.selectNodeContents(root);
426
+ pre.setEnd(range.startContainer, range.startOffset);
427
+ return pre.toString().length;
428
+ }
429
+
430
+ function restoreCaret(root: HTMLElement, charOffset: number) {
431
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
432
+ let remaining = charOffset;
433
+ let node: Node | null = walker.nextNode();
434
+ while (node) {
435
+ const len = (node.textContent ?? "").length;
436
+ if (remaining <= len) {
437
+ const range = document.createRange();
438
+ range.setStart(node, remaining);
439
+ range.collapse(true);
440
+ const sel = window.getSelection();
441
+ sel?.removeAllRanges();
442
+ sel?.addRange(range);
443
+ return;
444
+ }
445
+ remaining -= len;
446
+ node = walker.nextNode();
447
+ }
448
+ placeCaretAtEnd(root);
449
+ }
@@ -7,6 +7,7 @@ import { ModelCombobox } from "../ModelCombobox";
7
7
  import { useToast } from "../Toast";
8
8
  import { useGlobalConfig, useSuperAgentConfig } from "../../hooks/useGlobalConfig";
9
9
  import { ENGINE_ICONS, ENGINE_PRESETS, type EngineType } from "./providers/typeStyles";
10
+ import { t } from "../../i18n";
10
11
 
11
12
  interface ProviderInfo {
12
13
  slug: string;
@@ -151,7 +152,7 @@ export function DefaultRouterCard() {
151
152
 
152
153
  return (
153
154
  <Section
154
- title="Router de modelos"
155
+ title={t("router_panel.title")}
155
156
  description="Un solo router general (sin casos por tarea). Elegí proveedor y modelo; si el activo falla, prueba la cadena de fallback en orden."
156
157
  >
157
158
  <div className="space-y-4">
@@ -93,9 +93,9 @@ export function EnginesPanel() {
93
93
 
94
94
  return (
95
95
  <Section
96
- title="Proveedores"
96
+ title={t("engines_panel.title")}
97
97
  description="Proveedores LLM (API). Cada provider usa un engine/adapter (openai, ollama, …) con su key y URL."
98
- action={<Button size="sm" variant="primary" onClick={openCreate}><Plus size={14} /> Nuevo provider</Button>}
98
+ action={<Button size="sm" variant="primary" onClick={openCreate}><Plus size={14} /> {t("engines_panel.new_btn")}</Button>}
99
99
  >
100
100
  {providers.length === 0 ? (
101
101
  <Empty>Sin providers. Agregá uno con el botón de arriba.</Empty>
@@ -7,6 +7,7 @@ import { UiSelect } from "../UiSelect";
7
7
  import { useToast } from "../Toast";
8
8
  import { useGlobalConfig } from "../../hooks/useGlobalConfig";
9
9
  import { Embeddings, type EmbedMode } from "../../lib/api/embeddings";
10
+ import { t } from "../../i18n";
10
11
 
11
12
  // Memory / RAG embeddings configuration. Mirrors the Voice (TTS/STT) panel:
12
13
  // pick the provider + model for the cross-channel memory retriever. Persists
@@ -98,7 +99,7 @@ export function MemoryPanel() {
98
99
  return (
99
100
  <div className="space-y-6">
100
101
  <Section
101
- title="Embeddings (RAG)"
102
+ title={t("memory_panel.embeddings_title")}
102
103
  description="Modelo que vectoriza el historial de todos los canales para la memoria relevante. Igual que TTS/STT: elegí proveedor y modelo. 'Automático' prueba local primero y cae al offline si no hay nada."
103
104
  >
104
105
  <div className="space-y-3">
@@ -142,7 +143,7 @@ export function MemoryPanel() {
142
143
  </div>
143
144
  </Section>
144
145
 
145
- <Section title="Ollama (local)" description="Sin API key. Corre nomic-embed-text en tu Ollama local o cloud.">
146
+ <Section title={t("memory_panel.ollama_title")} description="Sin API key. Corre nomic-embed-text en tu Ollama local o cloud.">
146
147
  <Field label="Modelo">
147
148
  <Input
148
149
  defaultValue={emb.ollama?.model || "nomic-embed-text"}
@@ -166,7 +167,7 @@ export function MemoryPanel() {
166
167
  </Field>
167
168
  </Section>
168
169
 
169
- <Section title="OpenAI" description="text-embedding-3-small (1536 dims) u otro modelo compatible.">
170
+ <Section title={t("memory_panel.openai_title")} description="text-embedding-3-small (1536 dims) u otro modelo compatible.">
170
171
  <Field label="Modelo">
171
172
  <Input
172
173
  defaultValue={emb.openai?.model || "text-embedding-3-small"}
@@ -194,7 +195,7 @@ export function MemoryPanel() {
194
195
  </Field>
195
196
  </Section>
196
197
 
197
- <Section title="Gemini" description="text-embedding-004 (768 dims). Free tier con API key de Google.">
198
+ <Section title={t("memory_panel.gemini_title")} description="text-embedding-004 (768 dims). Free tier con API key de Google.">
198
199
  <Field label="Modelo">
199
200
  <Input
200
201
  defaultValue={emb.gemini?.model || "text-embedding-004"}
@@ -4,6 +4,7 @@ import { Tip } from "../../ui/tip";
4
4
  import { secretSuffix } from "../../../lib/secrets";
5
5
  import { ENGINE_BADGES, ENGINE_GRADIENTS, ENGINE_ICONS, ENGINE_OPTIONS, engineStyle } from "./typeStyles";
6
6
  import type { Provider } from "./types";
7
+ import { t } from "../../../i18n";
7
8
 
8
9
  export function ProviderCard({
9
10
  provider,
@@ -42,7 +43,7 @@ export function ProviderCard({
42
43
  </span>
43
44
  </div>
44
45
  <div className="flex shrink-0 items-center gap-1">
45
- <Tip content={active ? "Activo · click para desactivar" : "Inactivo · click para activar"}>
46
+ <Tip content={active ? t("providers_modal.toggle_active") : t("providers_modal.toggle_inactive")}>
46
47
  <button
47
48
  type="button"
48
49
  onClick={(e) => { e.stopPropagation(); onToggle(); }}
@@ -57,7 +58,7 @@ export function ProviderCard({
57
58
  {active ? "Active" : "Off"}
58
59
  </button>
59
60
  </Tip>
60
- <Tip content="Borrar">
61
+ <Tip content={t("providers_modal.delete")}>
61
62
  <button
62
63
  type="button"
63
64
  onClick={(e) => { e.stopPropagation(); onDelete(); }}
@@ -7,6 +7,7 @@ import { Engines } from "../../../lib/api";
7
7
  import { isSecretMarker, secretSuffix } from "../../../lib/secrets";
8
8
  import { ENGINE_ICONS, ENGINE_OPTIONS, ENGINE_PRESETS, type EngineType } from "./typeStyles";
9
9
  import type { Provider } from "./types";
10
+ import { t } from "../../../i18n";
10
11
 
11
12
  export interface ProviderSaveResult {
12
13
  provider: Provider;
@@ -269,7 +270,7 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
269
270
  <Dialog
270
271
  open={open}
271
272
  onClose={onClose}
272
- title={isEdit ? `Editar ${initial?.name || initial?.slug}` : "Nuevo provider"}
273
+ title={isEdit ? t("providers_modal.edit_title", { name: initial?.name || initial?.slug || "" }) : t("providers_modal.new_title")}
273
274
  description="Proveedor LLM. El motor (engine) define qué adapter usa (openai, ollama, …)."
274
275
  size="lg"
275
276
  footer={
@@ -362,7 +363,7 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
362
363
  options={modelOptions}
363
364
  className="flex-1"
364
365
  />
365
- <Button size="sm" variant="secondary" onClick={loadModels} disabled={loadingModels} title="Lista los modelos reales del proveedor">
366
+ <Button size="sm" variant="secondary" onClick={loadModels} disabled={loadingModels} title={t("providers_modal.list_models_hint")} aria-label={t("providers_modal.list_models_hint")}>
366
367
  {loadingModels ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
367
368
  Cargar modelos
368
369
  </Button>
@@ -5,6 +5,7 @@ import { ArrowUp, Square } from "lucide-react"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
7
  import { Button } from "@/components/ui/button"
8
+ import { t } from "@/i18n"
8
9
 
9
10
  interface ChatInputProps {
10
11
  value: string
@@ -109,8 +110,8 @@ export function ChatInput({
109
110
  size="icon-sm"
110
111
  variant="destructive"
111
112
  onClick={onStop}
112
- aria-label="Detener"
113
- title="Detener"
113
+ aria-label={t("chat_ui.stop")}
114
+ title={t("chat_ui.stop")}
114
115
  >
115
116
  <Square className="size-3.5" fill="currentColor" />
116
117
  </Button>
@@ -121,8 +122,8 @@ export function ChatInput({
121
122
  variant="default"
122
123
  onClick={onSubmit}
123
124
  disabled={!canSend}
124
- aria-label="Enviar"
125
- title="Enviar"
125
+ aria-label={t("chat_ui.send")}
126
+ title={t("chat_ui.send")}
126
127
  >
127
128
  <ArrowUp className="size-4" />
128
129
  </Button>
@@ -9,6 +9,7 @@ import { useIsMobile } from "@/hooks/use-mobile"
9
9
  import { cn } from "@/lib/utils"
10
10
  import { Button } from "@/components/ui/button"
11
11
  import { Input } from "@/components/ui/input"
12
+ import { t } from "@/i18n"
12
13
  import { Separator } from "@/components/ui/separator"
13
14
  import {
14
15
  Sheet,
@@ -284,10 +285,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
284
285
  <button
285
286
  data-sidebar="rail"
286
287
  data-slot="sidebar-rail"
287
- aria-label="Toggle Sidebar"
288
+ aria-label={t("sidebar_ui.toggle")}
288
289
  tabIndex={-1}
289
290
  onClick={toggleSidebar}
290
- title="Toggle Sidebar"
291
+ title={t("sidebar_ui.toggle")}
291
292
  className={cn(
292
293
  "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
293
294
  "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
@@ -13,6 +13,7 @@ import {
13
13
  type OpenAiTtsConfig,
14
14
  type PiperConfig,
15
15
  } from "../../lib/api/voice";
16
+ import { t } from "../../i18n";
16
17
 
17
18
  // Per-provider settings. Saved as dotted-key patches under voice.tts.<id>.
18
19
  // Secrets (api_key) follow the EnginesPanel convention: a blank field keeps
@@ -124,7 +125,7 @@ export function VoiceProviderModal({ open, providerId, config, onClose, onSave }
124
125
  <Dialog
125
126
  open={open}
126
127
  onClose={onClose}
127
- title={`Configurar ${meta?.name || providerId}`}
128
+ title={t("voice_screen.configure_provider", { name: meta?.name || providerId || "" })}
128
129
  description={meta?.note}
129
130
  size="md"
130
131
  footer={
@@ -58,7 +58,7 @@ export const ENGINE_ORDER = [
58
58
  export const PERMISSION_MODES = ["total", "automatico", "permiso"] as const;
59
59
  export type PermissionMode = (typeof PERMISSION_MODES)[number];
60
60
 
61
- /** Routine kinds — must match src/host/daemon/routines.js. */
61
+ /** Routine kinds — must match src/core/routines/runner.js. */
62
62
  export const ROUTINE_KINDS = [
63
63
  "heartbeat",
64
64
  "exec_agent",