@agentprojectcontext/apx 1.34.0 → 1.36.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 +1 -1
- package/src/core/agent/build-agent-system.js +134 -58
- package/src/core/agent/channels/voice-context.js +4 -4
- package/src/core/agent/prompt-builder.js +176 -123
- package/src/core/agent/prompts/channels/code.md +12 -10
- package/src/core/agent/prompts/channels/desktop.md +5 -32
- package/src/core/agent/prompts/channels/telegram.md +4 -15
- package/src/core/agent/prompts/channels/web_code.md +11 -11
- package/src/core/agent/prompts/core/agent-base.md +24 -0
- package/src/core/agent/prompts/core/project-agent.md +11 -0
- package/src/core/agent/prompts/core/super-agent.md +21 -0
- package/src/core/agent/prompts/discipline/action.md +10 -0
- package/src/core/agent/prompts/discipline/single-segment.md +6 -0
- package/src/core/agent/prompts/discipline/two-segment.md +11 -0
- package/src/core/agent/self-memory.js +43 -1
- package/src/core/agent/skills/index-store.js +307 -0
- package/src/core/agent/skills/index.js +15 -1
- package/src/core/agent/skills/inspector.js +317 -0
- package/src/core/agent/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/_git.js +50 -0
- package/src/core/agent/tools/handlers/git-diff.js +44 -0
- package/src/core/agent/tools/handlers/git-log.js +38 -0
- package/src/core/agent/tools/handlers/git-show.js +34 -0
- package/src/core/agent/tools/handlers/git-status.js +61 -0
- package/src/core/agent/tools/names.js +31 -0
- package/src/core/agent/tools/registry.js +36 -5
- package/src/core/config/index.js +21 -0
- package/src/core/runtime-skills/apx/SKILL.md +27 -39
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
- package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
- package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
- package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
- package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
- package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
- package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
- package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
- package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
- package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
- package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
- package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
- package/src/core/stores/conversations.js +27 -2
- package/src/host/daemon/api/exec.js +2 -0
- package/src/host/daemon/api/skills.js +140 -6
- package/src/host/daemon/api/super-agent.js +56 -1
- package/src/host/daemon/index.js +17 -0
- package/src/interfaces/cli/branding.js +53 -0
- package/src/interfaces/cli/commands/skills.js +254 -0
- package/src/interfaces/cli/index.js +84 -2
- package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +1 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js +628 -0
- package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +0 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +69 -1
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
- package/src/interfaces/web/src/hooks/useChat.ts +54 -2
- package/src/interfaces/web/src/i18n/en.ts +12 -1
- package/src/interfaces/web/src/i18n/es.ts +12 -1
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/skills.ts +70 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +12 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/core/agent/prompts/action-discipline.md +0 -24
- package/src/core/agent/prompts/super-agent-base.md +0 -42
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
- package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +0 -100
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
|
|
19
19
|
<link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
|
|
20
20
|
<link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-DJKA763h.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cm0KyPoZ.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -179,7 +179,6 @@ function settingsLabel(key?: string) {
|
|
|
179
179
|
function projectLabel(key?: string) {
|
|
180
180
|
switch (key) {
|
|
181
181
|
case "chat": return t("project.nav.chat");
|
|
182
|
-
case "threads": return t("project.nav.threads");
|
|
183
182
|
case "telegram": return t("project.nav.telegram");
|
|
184
183
|
case "agents": return t("project.nav.agents");
|
|
185
184
|
case "routines": return t("project.nav.routines");
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState, type ElementType } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import {
|
|
5
|
+
Bot,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Clock,
|
|
9
|
+
FolderOpen,
|
|
10
|
+
MessageSquare,
|
|
11
|
+
Mic,
|
|
12
|
+
Monitor,
|
|
13
|
+
Plus,
|
|
14
|
+
Send,
|
|
15
|
+
Sparkles,
|
|
16
|
+
Timer,
|
|
17
|
+
User,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
import { Conversations } from "../../lib/api";
|
|
20
|
+
import { Input, Loading } from "../ui";
|
|
21
|
+
import { UiSelect } from "../UiSelect";
|
|
22
|
+
import { t } from "../../i18n";
|
|
23
|
+
import type { AgentEntry, ConversationListEntry } from "../../types/daemon";
|
|
24
|
+
|
|
25
|
+
// Channel taxonomy — same channels the daemon writes ("web", "voice",
|
|
26
|
+
// "desktop", "telegram", …) folded into 8 sidebar groups. Each group has an
|
|
27
|
+
// icon + a fixed display order so the sidebar is stable across reloads.
|
|
28
|
+
export type ChannelGroupKey =
|
|
29
|
+
| "live"
|
|
30
|
+
| "telegram"
|
|
31
|
+
| "voice"
|
|
32
|
+
| "desktop"
|
|
33
|
+
| "web"
|
|
34
|
+
| "a2a"
|
|
35
|
+
| "schedule"
|
|
36
|
+
| "other";
|
|
37
|
+
|
|
38
|
+
interface GroupMeta {
|
|
39
|
+
label: string;
|
|
40
|
+
icon: ElementType;
|
|
41
|
+
order: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const GROUP_META: Record<ChannelGroupKey, GroupMeta> = {
|
|
45
|
+
live: { label: "Live", icon: Sparkles, order: 0 },
|
|
46
|
+
telegram: { label: "Telegram", icon: Send, order: 1 },
|
|
47
|
+
voice: { label: "Voice", icon: Mic, order: 2 },
|
|
48
|
+
desktop: { label: "Desktop", icon: Monitor, order: 3 },
|
|
49
|
+
web: { label: "Web", icon: MessageSquare, order: 4 },
|
|
50
|
+
a2a: { label: "Agent ↔ Agent", icon: Bot, order: 5 },
|
|
51
|
+
schedule: { label: "Schedule", icon: Timer, order: 6 },
|
|
52
|
+
other: { label: "Other", icon: FolderOpen, order: 7 },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function channelGroup(channel?: string): ChannelGroupKey {
|
|
56
|
+
if (!channel) return "web";
|
|
57
|
+
const c = channel.toLowerCase();
|
|
58
|
+
if (c === "telegram") return "telegram";
|
|
59
|
+
if (c === "voice" || c === "overlay") return "voice";
|
|
60
|
+
if (c === "desktop") return "desktop";
|
|
61
|
+
if (c === "web" || c === "sidebar" || c === "web-sidebar") return "web";
|
|
62
|
+
if (c === "a2a" || c.startsWith("agent")) return "a2a";
|
|
63
|
+
if (c === "schedule" || c === "cron" || c === "routine") return "schedule";
|
|
64
|
+
return "other";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Composite key identifying a sidebar selection: either a "live" agent session
|
|
68
|
+
// (no conversation file yet) or a persisted conversation tied to an agent.
|
|
69
|
+
export type ChatKey =
|
|
70
|
+
| { kind: "live"; agentSlug: string }
|
|
71
|
+
| { kind: "conv"; agentSlug: string; convId: string };
|
|
72
|
+
|
|
73
|
+
export function chatKeyToString(k: ChatKey): string {
|
|
74
|
+
return k.kind === "live" ? `live:${k.agentSlug}` : `conv:${k.agentSlug}:${k.convId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Props {
|
|
78
|
+
pid: string;
|
|
79
|
+
agents: AgentEntry[];
|
|
80
|
+
/** Virtual super-agent slug used by the dropdown (kept in sync with ChatTab). */
|
|
81
|
+
superAgentSlug: string;
|
|
82
|
+
superAgentLabel: string;
|
|
83
|
+
selected: ChatKey;
|
|
84
|
+
onSelect: (key: ChatKey) => void;
|
|
85
|
+
onNewChat: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Per-agent SWR fetcher. Lives in a child so adding/removing agents doesn't
|
|
89
|
+
// violate the rules of hooks in the parent.
|
|
90
|
+
function AgentConvFetcher({
|
|
91
|
+
pid,
|
|
92
|
+
slug,
|
|
93
|
+
onLoaded,
|
|
94
|
+
}: {
|
|
95
|
+
pid: string;
|
|
96
|
+
slug: string;
|
|
97
|
+
onLoaded: (slug: string, data: ConversationListEntry[] | undefined) => void;
|
|
98
|
+
}) {
|
|
99
|
+
const { data } = useSWR(
|
|
100
|
+
`/projects/${pid}/agents/${slug}/conversations`,
|
|
101
|
+
() => Conversations.list(pid, slug),
|
|
102
|
+
{ revalidateOnFocus: false },
|
|
103
|
+
);
|
|
104
|
+
// Bubble fetched data up to the parent on every change.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
onLoaded(slug, data);
|
|
107
|
+
}, [slug, data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function ChatList({
|
|
112
|
+
pid,
|
|
113
|
+
agents,
|
|
114
|
+
superAgentSlug,
|
|
115
|
+
superAgentLabel,
|
|
116
|
+
selected,
|
|
117
|
+
onSelect,
|
|
118
|
+
onNewChat,
|
|
119
|
+
}: Props) {
|
|
120
|
+
const [query, setQuery] = useState("");
|
|
121
|
+
const [agentFilter, setAgentFilter] = useState("");
|
|
122
|
+
const [collapsed, setCollapsed] = useState<Partial<Record<ChannelGroupKey, boolean>>>({});
|
|
123
|
+
const [byAgent, setByAgent] = useState<Record<string, ConversationListEntry[]>>({});
|
|
124
|
+
|
|
125
|
+
const handleLoaded = (slug: string, data: ConversationListEntry[] | undefined) => {
|
|
126
|
+
if (!data) return;
|
|
127
|
+
setByAgent((prev) => {
|
|
128
|
+
const cur = prev[slug];
|
|
129
|
+
if (cur && cur.length === data.length && cur === data) return prev;
|
|
130
|
+
return { ...prev, [slug]: data };
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const allConvs = useMemo<ConversationListEntry[]>(() => {
|
|
135
|
+
const out: ConversationListEntry[] = [];
|
|
136
|
+
for (const a of agents) {
|
|
137
|
+
for (const c of byAgent[a.slug] || []) {
|
|
138
|
+
out.push({ ...c, agent_slug: c.agent_slug || a.slug });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}, [agents, byAgent]);
|
|
143
|
+
|
|
144
|
+
const filteredConvs = useMemo(() => {
|
|
145
|
+
const q = query.trim().toLowerCase();
|
|
146
|
+
return allConvs.filter((c) => {
|
|
147
|
+
if (agentFilter && c.agent_slug !== agentFilter) return false;
|
|
148
|
+
if (q) {
|
|
149
|
+
const hay = `${c.title || ""} ${c.id} ${c.agent_slug}`.toLowerCase();
|
|
150
|
+
if (!hay.includes(q)) return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
}, [allConvs, query, agentFilter]);
|
|
155
|
+
|
|
156
|
+
// Group: live entries (one per applicable agent) + stored conversations by channel.
|
|
157
|
+
const groups = useMemo(() => {
|
|
158
|
+
const byKey = new Map<ChannelGroupKey, ConversationListEntry[]>();
|
|
159
|
+
for (const c of filteredConvs) {
|
|
160
|
+
const key = channelGroup(c.channel);
|
|
161
|
+
const bucket = byKey.get(key);
|
|
162
|
+
if (bucket) bucket.push(c);
|
|
163
|
+
else byKey.set(key, [c]);
|
|
164
|
+
}
|
|
165
|
+
return Array.from(byKey.entries())
|
|
166
|
+
.map(([key, items]) => ({
|
|
167
|
+
key,
|
|
168
|
+
items: items.sort(
|
|
169
|
+
(a, b) => new Date(b.started_at || 0).getTime() - new Date(a.started_at || 0).getTime(),
|
|
170
|
+
),
|
|
171
|
+
}))
|
|
172
|
+
.sort((a, b) => GROUP_META[a.key].order - GROUP_META[b.key].order);
|
|
173
|
+
}, [filteredConvs]);
|
|
174
|
+
|
|
175
|
+
// "Live" agents shown at the top: super-agent + project agents matching the
|
|
176
|
+
// current agent filter (so "filter by foo" hides the others everywhere).
|
|
177
|
+
const liveAgents = useMemo(() => {
|
|
178
|
+
if (agentFilter === superAgentSlug) {
|
|
179
|
+
return [{ slug: superAgentSlug, label: superAgentLabel }];
|
|
180
|
+
}
|
|
181
|
+
const out: { slug: string; label: string }[] = [];
|
|
182
|
+
if (!agentFilter) out.push({ slug: superAgentSlug, label: superAgentLabel });
|
|
183
|
+
for (const a of agents) {
|
|
184
|
+
if (agentFilter && a.slug !== agentFilter) continue;
|
|
185
|
+
out.push({ slug: a.slug, label: a.slug });
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}, [agents, agentFilter, superAgentSlug, superAgentLabel]);
|
|
189
|
+
|
|
190
|
+
const agentOptions = useMemo(
|
|
191
|
+
() => [
|
|
192
|
+
{ value: "", label: t("project.chat.list.all_agents") },
|
|
193
|
+
{ value: superAgentSlug, label: superAgentLabel },
|
|
194
|
+
...agents.map((a) => ({ value: a.slug, label: a.slug })),
|
|
195
|
+
],
|
|
196
|
+
[agents, superAgentSlug, superAgentLabel],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const totalCount = allConvs.length + liveAgents.length;
|
|
200
|
+
const anyLoaded = Object.keys(byAgent).length > 0 || agents.length === 0;
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<aside className="flex h-full w-72 shrink-0 flex-col border-r border-border bg-card/30">
|
|
204
|
+
{/* Hidden per-agent fetchers — one SWR per agent, but with a stable
|
|
205
|
+
component per slug so rules-of-hooks are respected. */}
|
|
206
|
+
{agents.map((a) => (
|
|
207
|
+
<AgentConvFetcher key={a.slug} pid={pid} slug={a.slug} onLoaded={handleLoaded} />
|
|
208
|
+
))}
|
|
209
|
+
|
|
210
|
+
<header className="flex h-[57px] shrink-0 items-center justify-between gap-2 border-b border-border px-3">
|
|
211
|
+
<div className="min-w-0">
|
|
212
|
+
<p className="truncate text-sm font-semibold">{t("project.chat.list.title")}</p>
|
|
213
|
+
<p className="text-[10px] text-muted-fg">
|
|
214
|
+
{t("project.chat.list.count", { n: totalCount })}
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={onNewChat}
|
|
220
|
+
className="inline-flex items-center gap-1 rounded-md border border-border bg-accent/60 px-2 py-1 text-[11px] font-medium hover:bg-accent"
|
|
221
|
+
>
|
|
222
|
+
<Plus className="size-3" /> {t("project.chat.list.new")}
|
|
223
|
+
</button>
|
|
224
|
+
</header>
|
|
225
|
+
|
|
226
|
+
<div className="space-y-2 border-b border-border p-2">
|
|
227
|
+
<Input
|
|
228
|
+
value={query}
|
|
229
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
230
|
+
placeholder={t("project.chat.list.search")}
|
|
231
|
+
/>
|
|
232
|
+
<UiSelect value={agentFilter} onChange={setAgentFilter} options={agentOptions} />
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="flex-1 space-y-3 overflow-y-auto p-2">
|
|
236
|
+
{!anyLoaded && (
|
|
237
|
+
<div className="px-2 py-1">
|
|
238
|
+
<Loading />
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Live group — synthetic, always first. */}
|
|
243
|
+
{liveAgents.length > 0 && (
|
|
244
|
+
<ChannelGroup
|
|
245
|
+
keyName="live"
|
|
246
|
+
count={liveAgents.length}
|
|
247
|
+
collapsed={!!collapsed.live}
|
|
248
|
+
onToggle={() => setCollapsed((p) => ({ ...p, live: !p.live }))}
|
|
249
|
+
>
|
|
250
|
+
{liveAgents.map((a) => {
|
|
251
|
+
const isRoby = a.slug === superAgentSlug;
|
|
252
|
+
const active =
|
|
253
|
+
selected.kind === "live" && selected.agentSlug === a.slug;
|
|
254
|
+
return (
|
|
255
|
+
<ChatListItem
|
|
256
|
+
key={`live-${a.slug}`}
|
|
257
|
+
title={isRoby ? a.label : t("project.chat.list.live_with", { slug: a.slug })}
|
|
258
|
+
subtitle={t("project.chat.list.live_subtitle")}
|
|
259
|
+
badge={isRoby ? "super" : a.slug}
|
|
260
|
+
selected={active}
|
|
261
|
+
onClick={() => onSelect({ kind: "live", agentSlug: a.slug })}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</ChannelGroup>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{/* Stored conversations, grouped by channel. */}
|
|
269
|
+
{groups.map((g) => (
|
|
270
|
+
<ChannelGroup
|
|
271
|
+
key={g.key}
|
|
272
|
+
keyName={g.key}
|
|
273
|
+
count={g.items.length}
|
|
274
|
+
collapsed={!!collapsed[g.key]}
|
|
275
|
+
onToggle={() => setCollapsed((p) => ({ ...p, [g.key]: !p[g.key] }))}
|
|
276
|
+
>
|
|
277
|
+
{g.items.map((c) => {
|
|
278
|
+
const active =
|
|
279
|
+
selected.kind === "conv" &&
|
|
280
|
+
selected.agentSlug === c.agent_slug &&
|
|
281
|
+
selected.convId === c.id;
|
|
282
|
+
return (
|
|
283
|
+
<ChatListItem
|
|
284
|
+
key={`${c.agent_slug}-${c.id}`}
|
|
285
|
+
title={c.title || c.id}
|
|
286
|
+
subtitle={[c.agent_slug, `${c.messages ?? 0} msg`]
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
.join(" · ")}
|
|
289
|
+
badge={c.agent_slug}
|
|
290
|
+
timeAgo={c.started_at}
|
|
291
|
+
selected={active}
|
|
292
|
+
onClick={() =>
|
|
293
|
+
onSelect({ kind: "conv", agentSlug: c.agent_slug, convId: c.id })
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
})}
|
|
298
|
+
</ChannelGroup>
|
|
299
|
+
))}
|
|
300
|
+
|
|
301
|
+
{anyLoaded && allConvs.length === 0 && (
|
|
302
|
+
<p className="px-3 py-6 text-center text-xs text-muted-fg">
|
|
303
|
+
{t("project.chat.list.empty")}
|
|
304
|
+
</p>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</aside>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function ChannelGroup({
|
|
312
|
+
keyName,
|
|
313
|
+
count,
|
|
314
|
+
collapsed,
|
|
315
|
+
onToggle,
|
|
316
|
+
children,
|
|
317
|
+
}: {
|
|
318
|
+
keyName: ChannelGroupKey;
|
|
319
|
+
count: number;
|
|
320
|
+
collapsed: boolean;
|
|
321
|
+
onToggle: () => void;
|
|
322
|
+
children: React.ReactNode;
|
|
323
|
+
}) {
|
|
324
|
+
const meta = GROUP_META[keyName];
|
|
325
|
+
const Icon = meta.icon;
|
|
326
|
+
return (
|
|
327
|
+
<section className="space-y-1">
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
onClick={onToggle}
|
|
331
|
+
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-muted-fg hover:bg-accent/30"
|
|
332
|
+
>
|
|
333
|
+
<span className="inline-flex items-center gap-1.5">
|
|
334
|
+
{collapsed ? (
|
|
335
|
+
<ChevronRight className="size-3" />
|
|
336
|
+
) : (
|
|
337
|
+
<ChevronDown className="size-3" />
|
|
338
|
+
)}
|
|
339
|
+
<Icon className="size-3" />
|
|
340
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider">
|
|
341
|
+
{meta.label}
|
|
342
|
+
</span>
|
|
343
|
+
</span>
|
|
344
|
+
<span className="text-[10px]">{count}</span>
|
|
345
|
+
</button>
|
|
346
|
+
{!collapsed && <div className="space-y-0.5">{children}</div>}
|
|
347
|
+
</section>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ChatListItem({
|
|
352
|
+
title,
|
|
353
|
+
subtitle,
|
|
354
|
+
badge,
|
|
355
|
+
timeAgo,
|
|
356
|
+
selected,
|
|
357
|
+
onClick,
|
|
358
|
+
}: {
|
|
359
|
+
title: string;
|
|
360
|
+
subtitle?: string;
|
|
361
|
+
badge?: string;
|
|
362
|
+
timeAgo?: string;
|
|
363
|
+
selected?: boolean;
|
|
364
|
+
onClick: () => void;
|
|
365
|
+
}) {
|
|
366
|
+
return (
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
onClick={onClick}
|
|
370
|
+
className={clsx(
|
|
371
|
+
"w-full rounded-md border px-2.5 py-2 text-left transition-colors",
|
|
372
|
+
selected
|
|
373
|
+
? "border-primary/50 bg-primary/10"
|
|
374
|
+
: "border-transparent hover:border-border hover:bg-accent/40",
|
|
375
|
+
)}
|
|
376
|
+
>
|
|
377
|
+
<div className="flex items-start justify-between gap-2">
|
|
378
|
+
<p className={clsx("truncate text-sm", selected ? "font-semibold" : "font-medium")}>
|
|
379
|
+
{title}
|
|
380
|
+
</p>
|
|
381
|
+
{timeAgo && (
|
|
382
|
+
<span className="inline-flex shrink-0 items-center gap-0.5 text-[10px] text-muted-fg">
|
|
383
|
+
<Clock className="size-2.5" />
|
|
384
|
+
{formatTimeAgo(timeAgo)}
|
|
385
|
+
</span>
|
|
386
|
+
)}
|
|
387
|
+
</div>
|
|
388
|
+
<div className="mt-0.5 flex items-center justify-between gap-2 text-[10px] text-muted-fg">
|
|
389
|
+
<span className="truncate">{subtitle}</span>
|
|
390
|
+
{badge && (
|
|
391
|
+
<span className="inline-flex shrink-0 items-center gap-1 rounded bg-accent/50 px-1.5 py-0.5">
|
|
392
|
+
<User className="size-2.5" />
|
|
393
|
+
{badge}
|
|
394
|
+
</span>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</button>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function formatTimeAgo(iso?: string): string {
|
|
402
|
+
if (!iso) return "";
|
|
403
|
+
const ts = new Date(iso).getTime();
|
|
404
|
+
if (!Number.isFinite(ts)) return "";
|
|
405
|
+
const diff = Date.now() - ts;
|
|
406
|
+
const m = Math.floor(diff / 60000);
|
|
407
|
+
if (m < 1) return "now";
|
|
408
|
+
if (m < 60) return `${m}m`;
|
|
409
|
+
const h = Math.floor(m / 60);
|
|
410
|
+
if (h < 24) return `${h}h`;
|
|
411
|
+
return `${Math.floor(h / 24)}d`;
|
|
412
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bot, Copy, User, Info } from "lucide-react";
|
|
1
|
+
import { Bot, Copy, User, Info, Sparkles } from "lucide-react";
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { ToolCall } from "./ToolCall";
|
|
4
4
|
import { AskQuestionsCard } from "./AskQuestionsCard";
|
|
@@ -49,6 +49,26 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
|
|
|
49
49
|
</div>
|
|
50
50
|
)}
|
|
51
51
|
|
|
52
|
+
{/* Skill Inspector: which skills the per-turn RAG injected for this turn. */}
|
|
53
|
+
{!mine && msg.inspector && (msg.inspector.loaded?.length || msg.inspector.hinted?.length) ? (
|
|
54
|
+
<div
|
|
55
|
+
className="flex flex-wrap items-center gap-1 text-[10px] text-sky-400/90"
|
|
56
|
+
title={`Skill Inspector (${msg.inspector.embedder || "RAG"}) eligió estas skills para este turno`}
|
|
57
|
+
>
|
|
58
|
+
<Sparkles size={10} />
|
|
59
|
+
{msg.inspector.loaded?.map((s) => (
|
|
60
|
+
<span key={`l-${s}`} className="rounded bg-sky-500/15 px-1 py-0.5 font-mono">
|
|
61
|
+
{s}
|
|
62
|
+
</span>
|
|
63
|
+
))}
|
|
64
|
+
{msg.inspector.hinted?.map((s) => (
|
|
65
|
+
<span key={`h-${s}`} className="rounded border border-sky-500/30 px-1 py-0.5 font-mono opacity-70">
|
|
66
|
+
{s}?
|
|
67
|
+
</span>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
|
|
52
72
|
{/* Ordered parts: interleaved assistant text + tool calls. */}
|
|
53
73
|
{msg.parts.map((part, i) =>
|
|
54
74
|
part.kind === "tool" ? (
|
|
@@ -30,7 +30,7 @@ export function AppearancePanel() {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
|
-
<div className="
|
|
33
|
+
<div className="grid gap-6 xl:grid-cols-2 xl:items-start">
|
|
34
34
|
<Section title={t("settings.appearance")}>
|
|
35
35
|
<div className="flex items-center gap-2">
|
|
36
36
|
<Button variant={theme === "light" ? "primary" : "secondary"} onClick={() => set("light")}>{t("settings.light_mode")}</Button>
|
|
@@ -36,6 +36,10 @@ interface MemoryCfg {
|
|
|
36
36
|
openai?: { api_key?: string; model?: string; base_url?: string };
|
|
37
37
|
gemini?: { api_key?: string; model?: string };
|
|
38
38
|
};
|
|
39
|
+
compact_threshold?: number;
|
|
40
|
+
keep_recent?: number;
|
|
41
|
+
compact_model?: string;
|
|
42
|
+
compact_fallback_model?: string;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
const isMarker = (v: string) => v.startsWith("***");
|
|
@@ -97,7 +101,7 @@ export function MemoryPanel() {
|
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
return (
|
|
100
|
-
<div className="
|
|
104
|
+
<div className="grid gap-6 xl:grid-cols-2 xl:items-start">
|
|
101
105
|
<Section
|
|
102
106
|
title={t("memory_panel.embeddings_title")}
|
|
103
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."
|
|
@@ -222,6 +226,70 @@ export function MemoryPanel() {
|
|
|
222
226
|
/>
|
|
223
227
|
</Field>
|
|
224
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>
|
|
225
293
|
</div>
|
|
226
294
|
);
|
|
227
295
|
}
|