@agentprojectcontext/apx 1.33.1 → 1.35.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 (208) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +49 -61
  3. package/src/core/agent/a2a/reply.js +48 -0
  4. package/src/core/agent/build-agent-system.js +136 -59
  5. package/src/core/agent/channels/voice-context.js +98 -0
  6. package/src/core/agent/memory.js +2 -1
  7. package/src/core/agent/prompt-builder.js +178 -124
  8. package/src/core/agent/prompts/channels/code.md +12 -10
  9. package/src/core/agent/prompts/channels/desktop.md +5 -32
  10. package/src/core/agent/prompts/channels/telegram.md +4 -15
  11. package/src/core/agent/prompts/channels/web_code.md +11 -11
  12. package/src/core/agent/prompts/core/agent-base.md +24 -0
  13. package/src/core/agent/prompts/core/project-agent.md +11 -0
  14. package/src/core/agent/prompts/core/super-agent.md +21 -0
  15. package/src/core/agent/prompts/discipline/action.md +10 -0
  16. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  17. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  18. package/src/core/agent/prompts/modes/code-build.md +1 -0
  19. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  20. package/src/core/agent/prompts/modes/index.js +28 -0
  21. package/src/core/agent/self-memory.js +43 -1
  22. package/src/core/agent/skills/index-store.js +307 -0
  23. package/src/core/agent/skills/index.js +15 -1
  24. package/src/core/agent/skills/inspector.js +317 -0
  25. package/src/core/agent/skills/loader.js +22 -18
  26. package/src/core/agent/stream/turn-accumulator.js +73 -0
  27. package/src/core/agent/suggestions.js +37 -0
  28. package/src/core/agent/super-agent.js +7 -1
  29. package/src/core/agent/tools/handlers/_git.js +50 -0
  30. package/src/core/agent/tools/handlers/add-project.js +5 -2
  31. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  32. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  33. package/src/core/agent/tools/handlers/git-log.js +38 -0
  34. package/src/core/agent/tools/handlers/git-show.js +34 -0
  35. package/src/core/agent/tools/handlers/git-status.js +61 -0
  36. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  37. package/src/core/agent/tools/helpers.js +2 -2
  38. package/src/core/agent/tools/names.js +169 -0
  39. package/src/core/agent/tools/registry-bridge.js +6 -14
  40. package/src/core/agent/tools/registry.js +103 -69
  41. package/src/core/apc/context-copy.js +27 -0
  42. package/src/core/apc/notes.js +19 -0
  43. package/src/core/apc/parser.js +12 -5
  44. package/src/core/apc/paths.js +87 -0
  45. package/src/core/apc/scaffold.js +82 -76
  46. package/src/core/apc/skill-sync.js +10 -0
  47. package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
  48. package/src/core/config/index.js +24 -2
  49. package/src/core/config/redact.js +95 -0
  50. package/src/core/constants/channels.js +2 -0
  51. package/src/core/constants/code-modes.js +10 -0
  52. package/src/core/constants/index.js +1 -0
  53. package/src/core/deck/manifest.js +186 -0
  54. package/src/core/engines/catalog.js +83 -0
  55. package/src/core/{tools → http-tools}/browser.js +0 -1
  56. package/src/core/{tools → http-tools}/fetch.js +0 -1
  57. package/src/core/{tools → http-tools}/glob.js +0 -1
  58. package/src/core/{tools → http-tools}/grep.js +0 -1
  59. package/src/core/{tools → http-tools}/registry.js +0 -1
  60. package/src/core/{tools → http-tools}/search.js +0 -1
  61. package/src/core/i18n/en.js +9 -0
  62. package/src/core/i18n/es.js +12 -0
  63. package/src/core/i18n/index.js +54 -0
  64. package/src/core/i18n/pt.js +9 -0
  65. package/src/core/identity/telegram.js +2 -1
  66. package/src/core/mcp/runner.js +272 -14
  67. package/src/core/mcp/sources.js +3 -2
  68. package/src/core/routines/index.js +16 -0
  69. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  70. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  71. package/src/core/runtime-skills/apx/SKILL.md +83 -0
  72. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +125 -0
  73. package/src/core/runtime-skills/apx-agent/SKILL.md +97 -0
  74. package/src/core/runtime-skills/apx-mcp/SKILL.md +111 -0
  75. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +169 -0
  76. package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +20 -29
  77. package/src/core/runtime-skills/apx-routine/SKILL.md +127 -0
  78. package/src/core/runtime-skills/apx-runtime/SKILL.md +99 -0
  79. package/src/core/runtime-skills/apx-sessions/SKILL.md +232 -0
  80. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +129 -0
  81. package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +18 -21
  82. package/src/core/runtime-skills/apx-telegram/SKILL.md +120 -0
  83. package/src/core/runtime-skills/apx-voice/SKILL.md +117 -0
  84. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  85. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  86. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  87. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  88. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  89. package/src/core/stores/code-sessions.js +50 -2
  90. package/src/core/stores/routine-memory.js +1 -1
  91. package/src/core/stores/sessions-search.js +121 -0
  92. package/src/core/stores/sessions.js +38 -0
  93. package/src/core/vars/index.js +14 -0
  94. package/src/core/vars/interpolate.js +86 -0
  95. package/src/core/vars/sources.js +151 -0
  96. package/src/core/voice/audio-decode.js +38 -0
  97. package/src/core/voice/transcription.js +225 -0
  98. package/src/host/daemon/api/admin-config.js +5 -82
  99. package/src/host/daemon/api/agents.js +5 -5
  100. package/src/host/daemon/api/code.js +17 -169
  101. package/src/host/daemon/api/config.js +3 -4
  102. package/src/host/daemon/api/conversations.js +8 -29
  103. package/src/host/daemon/api/deck.js +37 -404
  104. package/src/host/daemon/api/engines.js +1 -80
  105. package/src/host/daemon/api/exec.js +1 -1
  106. package/src/host/daemon/api/mcps.js +32 -0
  107. package/src/host/daemon/api/routines.js +1 -1
  108. package/src/host/daemon/api/runtimes.js +4 -3
  109. package/src/host/daemon/api/sessions-search.js +24 -140
  110. package/src/host/daemon/api/sessions.js +12 -30
  111. package/src/host/daemon/api/shared.js +2 -1
  112. package/src/host/daemon/api/skills.js +140 -6
  113. package/src/host/daemon/api/super-agent.js +56 -1
  114. package/src/host/daemon/api/telegram.js +1 -11
  115. package/src/host/daemon/api/tools.js +6 -6
  116. package/src/host/daemon/api/transcribe.js +2 -2
  117. package/src/host/daemon/api/vars.js +137 -0
  118. package/src/host/daemon/api/voice.js +13 -290
  119. package/src/host/daemon/api.js +2 -0
  120. package/src/host/daemon/db.js +6 -6
  121. package/src/host/daemon/deck-exec.js +148 -0
  122. package/src/host/daemon/index.js +20 -3
  123. package/src/host/daemon/plugins/telegram/index.js +9 -9
  124. package/src/host/daemon/routines-scheduler.js +64 -0
  125. package/src/host/daemon/smoke.js +3 -2
  126. package/src/host/daemon/whisper-server.js +225 -0
  127. package/src/interfaces/cli/branding.js +53 -0
  128. package/src/interfaces/cli/commands/agent.js +3 -2
  129. package/src/interfaces/cli/commands/command.js +2 -3
  130. package/src/interfaces/cli/commands/messages.js +6 -2
  131. package/src/interfaces/cli/commands/pair.js +5 -4
  132. package/src/interfaces/cli/commands/search.js +1 -1
  133. package/src/interfaces/cli/commands/sessions.js +3 -2
  134. package/src/interfaces/cli/commands/skills.js +290 -55
  135. package/src/interfaces/cli/index.js +84 -2
  136. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
  137. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
  138. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +1 -0
  139. package/src/interfaces/web/dist/index.html +2 -2
  140. package/src/interfaces/web/package-lock.json +182 -182
  141. package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
  142. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  143. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  144. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +37 -4
  145. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  146. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  147. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  148. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  149. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  150. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  151. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  152. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  153. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  154. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  155. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  156. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  157. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  158. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +73 -4
  159. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  160. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  161. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  162. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  163. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  164. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  165. package/src/interfaces/web/src/constants/index.ts +1 -1
  166. package/src/interfaces/web/src/hooks/useChat.ts +19 -0
  167. package/src/interfaces/web/src/i18n/en.ts +175 -7
  168. package/src/interfaces/web/src/i18n/es.ts +180 -15
  169. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  170. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  171. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  172. package/src/interfaces/web/src/lib/api.ts +1 -0
  173. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  174. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  175. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  176. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  177. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  178. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  179. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  180. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  181. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  182. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  183. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  184. package/src/interfaces/web/src/types/daemon.ts +15 -0
  185. package/skills/apx-agency-agents/SKILL.md +0 -141
  186. package/skills/apx-agent/SKILL.md +0 -100
  187. package/skills/apx-mcp-builder/SKILL.md +0 -183
  188. package/skills/apx-routine/SKILL.md +0 -140
  189. package/skills/apx-runtime/SKILL.md +0 -117
  190. package/skills/apx-sessions/SKILL.md +0 -281
  191. package/skills/apx-skill-builder/SKILL.md +0 -153
  192. package/skills/apx-telegram/SKILL.md +0 -131
  193. package/skills/apx-voice/SKILL.md +0 -137
  194. package/src/core/agent/prompts/action-discipline.md +0 -24
  195. package/src/core/agent/prompts/super-agent-base.md +0 -42
  196. package/src/host/daemon/transcription.js +0 -538
  197. package/src/host/daemon/whisper-transcribe.py +0 -73
  198. package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
  199. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
  200. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
  201. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  202. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  203. /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
  204. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  205. /package/src/core/{tools → http-tools}/index.js +0 -0
  206. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  207. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  208. /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
@@ -35,6 +36,10 @@ interface MemoryCfg {
35
36
  openai?: { api_key?: string; model?: string; base_url?: string };
36
37
  gemini?: { api_key?: string; model?: string };
37
38
  };
39
+ compact_threshold?: number;
40
+ keep_recent?: number;
41
+ compact_model?: string;
42
+ compact_fallback_model?: string;
38
43
  }
39
44
 
40
45
  const isMarker = (v: string) => v.startsWith("***");
@@ -98,7 +103,7 @@ export function MemoryPanel() {
98
103
  return (
99
104
  <div className="space-y-6">
100
105
  <Section
101
- title="Embeddings (RAG)"
106
+ title={t("memory_panel.embeddings_title")}
102
107
  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
108
  >
104
109
  <div className="space-y-3">
@@ -142,7 +147,7 @@ export function MemoryPanel() {
142
147
  </div>
143
148
  </Section>
144
149
 
145
- <Section title="Ollama (local)" description="Sin API key. Corre nomic-embed-text en tu Ollama local o cloud.">
150
+ <Section title={t("memory_panel.ollama_title")} description="Sin API key. Corre nomic-embed-text en tu Ollama local o cloud.">
146
151
  <Field label="Modelo">
147
152
  <Input
148
153
  defaultValue={emb.ollama?.model || "nomic-embed-text"}
@@ -166,7 +171,7 @@ export function MemoryPanel() {
166
171
  </Field>
167
172
  </Section>
168
173
 
169
- <Section title="OpenAI" description="text-embedding-3-small (1536 dims) u otro modelo compatible.">
174
+ <Section title={t("memory_panel.openai_title")} description="text-embedding-3-small (1536 dims) u otro modelo compatible.">
170
175
  <Field label="Modelo">
171
176
  <Input
172
177
  defaultValue={emb.openai?.model || "text-embedding-3-small"}
@@ -194,7 +199,7 @@ export function MemoryPanel() {
194
199
  </Field>
195
200
  </Section>
196
201
 
197
- <Section title="Gemini" description="text-embedding-004 (768 dims). Free tier con API key de Google.">
202
+ <Section title={t("memory_panel.gemini_title")} description="text-embedding-004 (768 dims). Free tier con API key de Google.">
198
203
  <Field label="Modelo">
199
204
  <Input
200
205
  defaultValue={emb.gemini?.model || "text-embedding-004"}
@@ -221,6 +226,70 @@ export function MemoryPanel() {
221
226
  />
222
227
  </Field>
223
228
  </Section>
229
+
230
+ <Section
231
+ title={t("memory_panel.compaction_title")}
232
+ description="Cuando un chat supera el umbral de turnos, los más viejos se resumen con un LLM liviano (local) y se guardan como [RESUMEN COMPACTADO], manteniendo el contexto acotado. Corre fuera del hot-path: el turno actual usa el resumen que ya exista."
233
+ >
234
+ <div className="grid gap-3 sm:grid-cols-2">
235
+ <Field label="Umbral de compactación" hint="Compactar una vez que el chat supera estos turnos (por defecto 60).">
236
+ <Input
237
+ type="number"
238
+ min={1}
239
+ defaultValue={mem.compact_threshold ?? 60}
240
+ placeholder="60"
241
+ disabled={busy}
242
+ onBlur={(ev) => {
243
+ const n = parseInt(ev.target.value, 10);
244
+ if (Number.isFinite(n) && n > 0 && n !== mem.compact_threshold) {
245
+ apply({ "memory.compact_threshold": n });
246
+ }
247
+ }}
248
+ className="max-w-[10rem]"
249
+ />
250
+ </Field>
251
+ <Field label="Turnos recientes a preservar" hint="Turnos verbatim que NUNCA se compactan (por defecto 40). Debe ser menor al umbral.">
252
+ <Input
253
+ type="number"
254
+ min={1}
255
+ defaultValue={mem.keep_recent ?? 40}
256
+ placeholder="40"
257
+ disabled={busy}
258
+ onBlur={(ev) => {
259
+ const n = parseInt(ev.target.value, 10);
260
+ if (Number.isFinite(n) && n > 0 && n !== mem.keep_recent) {
261
+ apply({ "memory.keep_recent": n });
262
+ }
263
+ }}
264
+ className="max-w-[10rem]"
265
+ />
266
+ </Field>
267
+ </div>
268
+ <Field label="Modelo de compactación" hint="LLM liviano para resumir. Ideal uno local (Ollama) para no gastar. Formato proveedor:modelo.">
269
+ <Input
270
+ defaultValue={mem.compact_model || "ollama:gemma4:31b-cloud"}
271
+ placeholder="ollama:gemma4:31b-cloud"
272
+ disabled={busy}
273
+ onBlur={(ev) => {
274
+ const v = ev.target.value.trim();
275
+ if (v && v !== mem.compact_model) apply({ "memory.compact_model": v });
276
+ }}
277
+ className="max-w-md"
278
+ />
279
+ </Field>
280
+ <Field label="Modelo de fallback" hint="Se usa si el de compactación falla. Vacío cae al modelo del super-agente.">
281
+ <Input
282
+ defaultValue={mem.compact_fallback_model || ""}
283
+ placeholder="(vacío → modelo del super-agente)"
284
+ disabled={busy}
285
+ onBlur={(ev) => {
286
+ const v = ev.target.value.trim();
287
+ if (v !== (mem.compact_fallback_model || "")) apply({ "memory.compact_fallback_model": v });
288
+ }}
289
+ className="max-w-md"
290
+ />
291
+ </Field>
292
+ </Section>
224
293
  </div>
225
294
  );
226
295
  }