@agentprojectcontext/apx 1.33.1 → 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.
- package/package.json +1 -1
- package/skills/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +4 -3
- package/src/core/agent/channels/voice-context.js +98 -0
- package/src/core/agent/memory.js +2 -1
- package/src/core/agent/prompt-builder.js +2 -1
- package/src/core/agent/prompts/modes/code-build.md +1 -0
- package/src/core/agent/prompts/modes/code-plan.md +1 -0
- package/src/core/agent/prompts/modes/index.js +28 -0
- package/src/core/agent/skills/loader.js +22 -18
- package/src/core/agent/stream/turn-accumulator.js +73 -0
- package/src/core/agent/suggestions.js +37 -0
- package/src/core/agent/tools/handlers/add-project.js +5 -2
- package/src/core/agent/tools/handlers/call-runtime.js +3 -2
- package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
- package/src/core/agent/tools/helpers.js +2 -2
- package/src/core/agent/tools/names.js +138 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +68 -65
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +12 -5
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -76
- package/src/core/apc/skill-sync.js +10 -0
- package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
- package/src/core/config/index.js +3 -2
- package/src/core/config/redact.js +95 -0
- package/src/core/constants/channels.js +2 -0
- package/src/core/constants/code-modes.js +10 -0
- package/src/core/constants/index.js +1 -0
- package/src/core/deck/manifest.js +186 -0
- package/src/core/engines/catalog.js +83 -0
- package/src/core/{tools → http-tools}/browser.js +0 -1
- package/src/core/{tools → http-tools}/fetch.js +0 -1
- package/src/core/{tools → http-tools}/glob.js +0 -1
- package/src/core/{tools → http-tools}/grep.js +0 -1
- package/src/core/{tools → http-tools}/registry.js +0 -1
- package/src/core/{tools → http-tools}/search.js +0 -1
- package/src/core/i18n/en.js +9 -0
- package/src/core/i18n/es.js +12 -0
- package/src/core/i18n/index.js +54 -0
- package/src/core/i18n/pt.js +9 -0
- package/src/core/identity/telegram.js +2 -1
- package/src/core/mcp/runner.js +272 -14
- package/src/core/mcp/sources.js +3 -2
- package/src/core/routines/index.js +16 -0
- package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
- package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
- package/src/core/runtime-skills/apx/SKILL.md +95 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
- package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
- package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
- package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
- package/src/core/stores/code-sessions.js +50 -2
- package/src/core/stores/routine-memory.js +1 -1
- package/src/core/stores/sessions-search.js +121 -0
- package/src/core/stores/sessions.js +38 -0
- package/src/core/vars/index.js +14 -0
- package/src/core/vars/interpolate.js +86 -0
- package/src/core/vars/sources.js +151 -0
- package/src/core/voice/audio-decode.js +38 -0
- package/src/core/voice/transcription.js +225 -0
- package/src/host/daemon/api/admin-config.js +5 -82
- package/src/host/daemon/api/agents.js +5 -5
- package/src/host/daemon/api/code.js +17 -169
- package/src/host/daemon/api/config.js +3 -4
- package/src/host/daemon/api/conversations.js +8 -29
- package/src/host/daemon/api/deck.js +37 -404
- package/src/host/daemon/api/engines.js +1 -80
- package/src/host/daemon/api/exec.js +1 -1
- package/src/host/daemon/api/mcps.js +32 -0
- package/src/host/daemon/api/routines.js +1 -1
- package/src/host/daemon/api/runtimes.js +4 -3
- package/src/host/daemon/api/sessions-search.js +24 -140
- package/src/host/daemon/api/sessions.js +12 -30
- package/src/host/daemon/api/shared.js +2 -1
- package/src/host/daemon/api/telegram.js +1 -11
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/transcribe.js +2 -2
- package/src/host/daemon/api/vars.js +137 -0
- package/src/host/daemon/api/voice.js +13 -290
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/db.js +6 -6
- package/src/host/daemon/deck-exec.js +148 -0
- package/src/host/daemon/index.js +3 -3
- package/src/host/daemon/plugins/telegram/index.js +9 -9
- package/src/host/daemon/routines-scheduler.js +64 -0
- package/src/host/daemon/smoke.js +3 -2
- package/src/host/daemon/whisper-server.js +225 -0
- package/src/interfaces/cli/commands/agent.js +3 -2
- package/src/interfaces/cli/commands/command.js +2 -3
- package/src/interfaces/cli/commands/messages.js +6 -2
- package/src/interfaces/cli/commands/pair.js +5 -4
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/sessions.js +3 -2
- package/src/interfaces/cli/commands/skills.js +36 -55
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +182 -182
- package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
- package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
- package/src/interfaces/web/src/constants/index.ts +1 -1
- package/src/interfaces/web/src/i18n/en.ts +174 -7
- package/src/interfaces/web/src/i18n/es.ts +179 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/vars.ts +38 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
- package/src/interfaces/web/src/types/daemon.ts +5 -0
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
- /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
- /package/src/core/{tools → http-tools}/index.js +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
- /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
- /package/src/{host/daemon → core/stores}/conversations.js +0 -0
- /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="
|
|
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="
|
|
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} />
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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 ? "
|
|
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="
|
|
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 ?
|
|
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="
|
|
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="
|
|
113
|
-
title="
|
|
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="
|
|
125
|
-
title="
|
|
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="
|
|
288
|
+
aria-label={t("sidebar_ui.toggle")}
|
|
288
289
|
tabIndex={-1}
|
|
289
290
|
onClick={toggleSidebar}
|
|
290
|
-
title="
|
|
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={
|
|
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/
|
|
61
|
+
/** Routine kinds — must match src/core/routines/runner.js. */
|
|
62
62
|
export const ROUTINE_KINDS = [
|
|
63
63
|
"heartbeat",
|
|
64
64
|
"exec_agent",
|