@agentprojectcontext/apx 1.54.0 → 1.55.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/src/core/stores/messages.js +69 -0
- package/src/host/daemon/api/conversations.js +25 -0
- package/src/interfaces/cli/commands/code.js +56 -0
- package/src/interfaces/cli/index.js +1 -1
- package/src/interfaces/web/dist/assets/{index-Bks35ks-.js → index-J3_Jhr99.js} +126 -126
- package/src/interfaces/web/dist/assets/{index-Bks35ks-.js.map → index-J3_Jhr99.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +73 -18
- package/src/interfaces/web/src/hooks/useChat.ts +29 -1
- package/src/interfaces/web/src/i18n/en.ts +1 -0
- package/src/interfaces/web/src/i18n/es.ts +1 -0
- package/src/interfaces/web/src/lib/api/conversations.ts +7 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +35 -18
- package/src/interfaces/web/src/types/daemon.ts +16 -0
- package/src/interfaces/cli/commands/sys.js +0 -862
- package/src/interfaces/cli/terminal-chat/renderer.js +0 -520
|
@@ -18,7 +18,7 @@
|
|
|
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-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-J3_Jhr99.js"></script>
|
|
22
22
|
<link rel="stylesheet" crossorigin href="/assets/index-BwnMDZaD.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
@@ -20,7 +20,7 @@ import { Conversations } from "../../lib/api";
|
|
|
20
20
|
import { Input, Loading } from "../ui";
|
|
21
21
|
import { UiSelect } from "../UiSelect";
|
|
22
22
|
import { t } from "../../i18n";
|
|
23
|
-
import type { AgentEntry, ConversationListEntry } from "../../types/daemon";
|
|
23
|
+
import type { AgentEntry, ConversationListEntry, ThreadListEntry } from "../../types/daemon";
|
|
24
24
|
|
|
25
25
|
// Channel taxonomy — same channels the daemon writes ("web", "voice",
|
|
26
26
|
// "desktop", "telegram", …) folded into 8 sidebar groups. Each group has an
|
|
@@ -64,14 +64,18 @@ function channelGroup(channel?: string): ChannelGroupKey {
|
|
|
64
64
|
return "other";
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// Composite key identifying a sidebar selection:
|
|
68
|
-
//
|
|
67
|
+
// Composite key identifying a sidebar selection: a "live" agent session (no
|
|
68
|
+
// conversation file yet), a persisted conversation tied to an agent, or a
|
|
69
|
+
// super-agent channel thread from the global message ledger.
|
|
69
70
|
export type ChatKey =
|
|
70
71
|
| { kind: "live"; agentSlug: string }
|
|
71
|
-
| { kind: "conv"; agentSlug: string; convId: string }
|
|
72
|
+
| { kind: "conv"; agentSlug: string; convId: string }
|
|
73
|
+
| { kind: "thread"; channel: string; threadId: string };
|
|
72
74
|
|
|
73
75
|
export function chatKeyToString(k: ChatKey): string {
|
|
74
|
-
|
|
76
|
+
if (k.kind === "live") return `live:${k.agentSlug}`;
|
|
77
|
+
if (k.kind === "conv") return `conv:${k.agentSlug}:${k.convId}`;
|
|
78
|
+
return `thread:${k.channel}:${k.threadId}`;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
interface Props {
|
|
@@ -122,6 +126,14 @@ export function ChatList({
|
|
|
122
126
|
const [collapsed, setCollapsed] = useState<Partial<Record<ChannelGroupKey, boolean>>>({});
|
|
123
127
|
const [byAgent, setByAgent] = useState<Record<string, ConversationListEntry[]>>({});
|
|
124
128
|
|
|
129
|
+
// Super-agent channel threads (telegram, web quick-chat, desktop …) from the
|
|
130
|
+
// global message ledger — the chats that don't live in conversation files.
|
|
131
|
+
const threadsQ = useSWR(
|
|
132
|
+
`/projects/${pid}/super-agent/threads`,
|
|
133
|
+
() => Conversations.threads(pid),
|
|
134
|
+
{ revalidateOnFocus: false },
|
|
135
|
+
);
|
|
136
|
+
|
|
125
137
|
const handleLoaded = (slug: string, data: ConversationListEntry[] | undefined) => {
|
|
126
138
|
if (!data) return;
|
|
127
139
|
setByAgent((prev) => {
|
|
@@ -153,24 +165,45 @@ export function ChatList({
|
|
|
153
165
|
});
|
|
154
166
|
}, [allConvs, query, agentFilter]);
|
|
155
167
|
|
|
156
|
-
//
|
|
168
|
+
// Threads belong to the super-agent: visible with no agent filter or when
|
|
169
|
+
// the filter is the super-agent itself.
|
|
170
|
+
const filteredThreads = useMemo<ThreadListEntry[]>(() => {
|
|
171
|
+
if (agentFilter && agentFilter !== superAgentSlug) return [];
|
|
172
|
+
const q = query.trim().toLowerCase();
|
|
173
|
+
return (threadsQ.data || []).filter((th) => {
|
|
174
|
+
if (!q) return true;
|
|
175
|
+
return `${th.title} ${th.id} ${th.channel}`.toLowerCase().includes(q);
|
|
176
|
+
});
|
|
177
|
+
}, [threadsQ.data, query, agentFilter, superAgentSlug]);
|
|
178
|
+
|
|
179
|
+
// Group: live entries (one per applicable agent) + stored conversations and
|
|
180
|
+
// super-agent channel threads, folded together by channel.
|
|
181
|
+
type GroupItem =
|
|
182
|
+
| { type: "conv"; conv: ConversationListEntry; sortTs: string }
|
|
183
|
+
| { type: "thread"; thread: ThreadListEntry; sortTs: string };
|
|
184
|
+
|
|
157
185
|
const groups = useMemo(() => {
|
|
158
|
-
const byKey = new Map<ChannelGroupKey,
|
|
159
|
-
|
|
160
|
-
const key = channelGroup(c.channel);
|
|
186
|
+
const byKey = new Map<ChannelGroupKey, GroupItem[]>();
|
|
187
|
+
const push = (key: ChannelGroupKey, item: GroupItem) => {
|
|
161
188
|
const bucket = byKey.get(key);
|
|
162
|
-
if (bucket) bucket.push(
|
|
163
|
-
else byKey.set(key, [
|
|
189
|
+
if (bucket) bucket.push(item);
|
|
190
|
+
else byKey.set(key, [item]);
|
|
191
|
+
};
|
|
192
|
+
for (const c of filteredConvs) {
|
|
193
|
+
push(channelGroup(c.channel), { type: "conv", conv: c, sortTs: c.started_at || "" });
|
|
194
|
+
}
|
|
195
|
+
for (const th of filteredThreads) {
|
|
196
|
+
push(channelGroup(th.channel), { type: "thread", thread: th, sortTs: th.last_ts || th.started_at || "" });
|
|
164
197
|
}
|
|
165
198
|
return Array.from(byKey.entries())
|
|
166
199
|
.map(([key, items]) => ({
|
|
167
200
|
key,
|
|
168
201
|
items: items.sort(
|
|
169
|
-
(a, b) => new Date(b.
|
|
202
|
+
(a, b) => new Date(b.sortTs || 0).getTime() - new Date(a.sortTs || 0).getTime(),
|
|
170
203
|
),
|
|
171
204
|
}))
|
|
172
205
|
.sort((a, b) => GROUP_META[a.key].order - GROUP_META[b.key].order);
|
|
173
|
-
}, [filteredConvs]);
|
|
206
|
+
}, [filteredConvs, filteredThreads]);
|
|
174
207
|
|
|
175
208
|
// "Live" agents shown at the top: super-agent + project agents matching the
|
|
176
209
|
// current agent filter (so "filter by foo" hides the others everywhere).
|
|
@@ -196,8 +229,9 @@ export function ChatList({
|
|
|
196
229
|
[agents, superAgentSlug, superAgentLabel],
|
|
197
230
|
);
|
|
198
231
|
|
|
199
|
-
const totalCount = allConvs.length + liveAgents.length;
|
|
200
|
-
const anyLoaded =
|
|
232
|
+
const totalCount = allConvs.length + (threadsQ.data?.length || 0) + liveAgents.length;
|
|
233
|
+
const anyLoaded =
|
|
234
|
+
Object.keys(byAgent).length > 0 || agents.length === 0 || !!threadsQ.data;
|
|
201
235
|
|
|
202
236
|
return (
|
|
203
237
|
<aside className="flex h-full w-72 shrink-0 flex-col border-r border-border bg-card/30">
|
|
@@ -265,7 +299,7 @@ export function ChatList({
|
|
|
265
299
|
</ChannelGroup>
|
|
266
300
|
)}
|
|
267
301
|
|
|
268
|
-
{/* Stored conversations, grouped by channel. */}
|
|
302
|
+
{/* Stored conversations + super-agent channel threads, grouped by channel. */}
|
|
269
303
|
{groups.map((g) => (
|
|
270
304
|
<ChannelGroup
|
|
271
305
|
key={g.key}
|
|
@@ -274,7 +308,28 @@ export function ChatList({
|
|
|
274
308
|
collapsed={!!collapsed[g.key]}
|
|
275
309
|
onToggle={() => setCollapsed((p) => ({ ...p, [g.key]: !p[g.key] }))}
|
|
276
310
|
>
|
|
277
|
-
{g.items.map((
|
|
311
|
+
{g.items.map((item) => {
|
|
312
|
+
if (item.type === "thread") {
|
|
313
|
+
const th = item.thread;
|
|
314
|
+
const active =
|
|
315
|
+
selected.kind === "thread" &&
|
|
316
|
+
selected.channel === th.channel &&
|
|
317
|
+
selected.threadId === th.id;
|
|
318
|
+
return (
|
|
319
|
+
<ChatListItem
|
|
320
|
+
key={`thread-${th.channel}-${th.id}`}
|
|
321
|
+
title={th.title}
|
|
322
|
+
subtitle={[th.channel, `${th.messages} msg`].join(" · ")}
|
|
323
|
+
badge="super"
|
|
324
|
+
timeAgo={th.last_ts}
|
|
325
|
+
selected={active}
|
|
326
|
+
onClick={() =>
|
|
327
|
+
onSelect({ kind: "thread", channel: th.channel, threadId: th.id })
|
|
328
|
+
}
|
|
329
|
+
/>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const c = item.conv;
|
|
278
333
|
const active =
|
|
279
334
|
selected.kind === "conv" &&
|
|
280
335
|
selected.agentSlug === c.agent_slug &&
|
|
@@ -298,7 +353,7 @@ export function ChatList({
|
|
|
298
353
|
</ChannelGroup>
|
|
299
354
|
))}
|
|
300
355
|
|
|
301
|
-
{anyLoaded && allConvs.length === 0 && (
|
|
356
|
+
{anyLoaded && allConvs.length === 0 && filteredThreads.length === 0 && (
|
|
302
357
|
<p className="px-3 py-6 text-center text-xs text-muted-fg">
|
|
303
358
|
{t("project.chat.list.empty")}
|
|
304
359
|
</p>
|
|
@@ -58,6 +58,10 @@ export interface UseChatResult {
|
|
|
58
58
|
* Only supported for project agents (super-agent conversations aren't
|
|
59
59
|
* persisted per-file). Pass `null` to drop the binding without clearing. */
|
|
60
60
|
load: (agentSlug: string, conversationId: string) => Promise<void>;
|
|
61
|
+
/** Load a super-agent channel thread (telegram/desktop/…) as history. Not
|
|
62
|
+
* bound to a conversation file — continuing sends go out as fresh web
|
|
63
|
+
* turns with the thread as previousMessages context. */
|
|
64
|
+
loadThread: (channel: string, threadId: string) => Promise<void>;
|
|
61
65
|
streaming: boolean;
|
|
62
66
|
/** Conversation id we're bound to, if any. Lets callers reflect "live vs
|
|
63
67
|
* loaded" state in the UI. */
|
|
@@ -371,5 +375,29 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
371
375
|
[pid, streaming, onError],
|
|
372
376
|
);
|
|
373
377
|
|
|
374
|
-
|
|
378
|
+
const loadThread = useCallback(
|
|
379
|
+
async (channel: string, threadId: string) => {
|
|
380
|
+
if (streaming) return;
|
|
381
|
+
try {
|
|
382
|
+
const detail = await Conversations.thread(pid, channel, threadId);
|
|
383
|
+
const loaded: ChatMsg[] = (detail.messages ?? [])
|
|
384
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
385
|
+
.map((m) => ({
|
|
386
|
+
role: m.role as "user" | "assistant",
|
|
387
|
+
parts: [{ kind: "text", text: m.content }],
|
|
388
|
+
ts: m.ts || new Date().toISOString(),
|
|
389
|
+
}));
|
|
390
|
+
// Ledger threads have no conversation file — sends continue as fresh
|
|
391
|
+
// web turns with this history as previousMessages.
|
|
392
|
+
convoRef.current = undefined;
|
|
393
|
+
setConversationId(undefined);
|
|
394
|
+
setMsgs(loaded);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
[pid, streaming, onError],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return { msgs, send, stop, clear, load, loadThread, streaming, conversationId };
|
|
375
403
|
}
|
|
@@ -315,6 +315,7 @@ export const en = {
|
|
|
315
315
|
superagent_title: "Chat with {persona}",
|
|
316
316
|
superagent_subtitle: "Chat with {persona} — the APX super-agent. Can use tools (projects, tasks, mcps, agents).",
|
|
317
317
|
loaded_subtitle: "Loaded conversation with {slug}. Sending will append to this thread.",
|
|
318
|
+
thread_subtitle: "History with {persona} on {channel}. Replying here continues the conversation from the web.",
|
|
318
319
|
empty: "Send a message to start the conversation.",
|
|
319
320
|
placeholder: "Type something and press enter to send (shift+enter = new line)",
|
|
320
321
|
send: "Send",
|
|
@@ -316,6 +316,7 @@ export const es = {
|
|
|
316
316
|
superagent_title: "Chat con {persona}",
|
|
317
317
|
superagent_subtitle: "Chat con {persona} — el super-agente APX. Puede usar tools (proyectos, tasks, mcps, agentes).",
|
|
318
318
|
loaded_subtitle: "Conversación cargada con {slug}. Lo que mandes se agrega a este chat.",
|
|
319
|
+
thread_subtitle: "Historial con {persona} en {channel}. Si respondés acá, la conversación sigue desde la web.",
|
|
319
320
|
empty: "Mandá un mensaje para arrancar la conversación.",
|
|
320
321
|
placeholder: "Escribí algo y enter para enviar (shift+enter = nueva línea)",
|
|
321
322
|
send: "Enviar",
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { http } from "../http";
|
|
2
|
-
import type { ConversationDetail, ConversationListEntry } from "../../types/daemon";
|
|
2
|
+
import type { ConversationDetail, ConversationListEntry, ThreadListEntry, ThreadDetail } from "../../types/daemon";
|
|
3
3
|
|
|
4
4
|
export const Conversations = {
|
|
5
5
|
list: (pid: string, slug: string) =>
|
|
6
6
|
http.get<ConversationListEntry[]>(`/projects/${pid}/agents/${slug}/conversations`),
|
|
7
7
|
get: (pid: string, slug: string, id: string) =>
|
|
8
8
|
http.get<ConversationDetail>(`/projects/${pid}/agents/${slug}/conversations/${id}`),
|
|
9
|
+
// Super-agent channel threads (telegram, web quick-chat, desktop …) derived
|
|
10
|
+
// from the global message ledger — one thread per channel+day.
|
|
11
|
+
threads: (pid: string) =>
|
|
12
|
+
http.get<ThreadListEntry[]>(`/projects/${pid}/super-agent/threads`),
|
|
13
|
+
thread: (pid: string, channel: string, id: string) =>
|
|
14
|
+
http.get<ThreadDetail>(`/projects/${pid}/super-agent/threads/${channel}/${id}`),
|
|
9
15
|
compact: (pid: string, slug: string, id?: string) =>
|
|
10
16
|
http.post<{ ok?: boolean }>(
|
|
11
17
|
id
|
|
@@ -27,7 +27,7 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
27
27
|
const [creating, setCreating] = useState(false);
|
|
28
28
|
const [model, setModel] = useState("");
|
|
29
29
|
const [dismissedAskKey, setDismissedAskKey] = useState<string | null>(null);
|
|
30
|
-
const { msgs, send: sendChat, stop, clear, load, streaming, conversationId } =
|
|
30
|
+
const { msgs, send: sendChat, stop, clear, load, loadThread, streaming, conversationId } =
|
|
31
31
|
useChat(pid, (m) => toast.error(m));
|
|
32
32
|
const persona = usePersonaName();
|
|
33
33
|
|
|
@@ -45,27 +45,38 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
45
45
|
const isRoby = (slug: string | null | undefined) => slug === ROBY_SLUG;
|
|
46
46
|
|
|
47
47
|
// The agent whose dropdown badge / model we show on the right header.
|
|
48
|
+
// Channel threads always belong to the super-agent, so no project agent.
|
|
48
49
|
const activeAgent = useMemo(
|
|
49
50
|
() =>
|
|
50
|
-
selected.kind === "
|
|
51
|
-
?
|
|
51
|
+
selected.kind === "thread"
|
|
52
|
+
? undefined
|
|
52
53
|
: agentList.find((a) => a.slug === selected.agentSlug),
|
|
53
54
|
[agentList, selected],
|
|
54
55
|
);
|
|
55
|
-
const activeIsRoby = isRoby(selected.agentSlug);
|
|
56
|
+
const activeIsRoby = selected.kind === "thread" || isRoby(selected.agentSlug);
|
|
56
57
|
|
|
57
|
-
// Whenever the user picks a stored conversation
|
|
58
|
-
// with its persisted history.
|
|
59
|
-
//
|
|
58
|
+
// Whenever the user picks a stored conversation or a channel thread, reload
|
|
59
|
+
// the in-memory chat with its persisted history. Conversations bind the
|
|
60
|
+
// conversation_id (sends append to the file); threads stay unbound —
|
|
61
|
+
// continuing sends fresh web turns with the thread as context.
|
|
60
62
|
useEffect(() => {
|
|
61
63
|
if (selected.kind === "conv") {
|
|
62
64
|
void load(selected.agentSlug, selected.convId);
|
|
65
|
+
} else if (selected.kind === "thread") {
|
|
66
|
+
void loadThread(selected.channel, selected.threadId);
|
|
63
67
|
} else {
|
|
64
68
|
// Switching to a live session = drop any previously bound conversation.
|
|
65
69
|
if (conversationId) clear();
|
|
66
70
|
}
|
|
67
71
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
-
}, [
|
|
72
|
+
}, [
|
|
73
|
+
selected.kind,
|
|
74
|
+
selected.kind === "conv"
|
|
75
|
+
? selected.convId
|
|
76
|
+
: selected.kind === "thread"
|
|
77
|
+
? `${selected.channel}:${selected.threadId}`
|
|
78
|
+
: selected.agentSlug,
|
|
79
|
+
]);
|
|
69
80
|
|
|
70
81
|
const send = async (text: string) => {
|
|
71
82
|
if (activeIsRoby) {
|
|
@@ -86,16 +97,22 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
86
97
|
clear();
|
|
87
98
|
};
|
|
88
99
|
|
|
89
|
-
const headerTitle =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
const headerTitle =
|
|
101
|
+
selected.kind === "thread"
|
|
102
|
+
? `${selected.channel} · ${selected.threadId}`
|
|
103
|
+
: activeIsRoby
|
|
104
|
+
? t("project.chat.superagent_title", { persona })
|
|
105
|
+
: selected.kind === "conv"
|
|
106
|
+
? selected.convId
|
|
107
|
+
: t("project.chat.title");
|
|
108
|
+
const headerSubtitle =
|
|
109
|
+
selected.kind === "thread"
|
|
110
|
+
? t("project.chat.thread_subtitle", { channel: selected.channel, persona })
|
|
111
|
+
: activeIsRoby
|
|
112
|
+
? t("project.chat.superagent_subtitle", { persona })
|
|
113
|
+
: selected.kind === "conv"
|
|
114
|
+
? t("project.chat.loaded_subtitle", { slug: selected.agentSlug })
|
|
115
|
+
: t("project.chat.subtitle");
|
|
99
116
|
|
|
100
117
|
if (agents.isLoading) return <Loading />;
|
|
101
118
|
|
|
@@ -153,6 +153,22 @@ export interface ConversationDetail {
|
|
|
153
153
|
meta?: Record<string, unknown>;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** Super-agent channel thread (one per channel+day of the global ledger). */
|
|
157
|
+
export interface ThreadListEntry {
|
|
158
|
+
id: string; // YYYY-MM-DD
|
|
159
|
+
channel: string; // telegram | web | desktop | deck | …
|
|
160
|
+
title: string;
|
|
161
|
+
messages: number;
|
|
162
|
+
started_at: string;
|
|
163
|
+
last_ts: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ThreadDetail {
|
|
167
|
+
id: string;
|
|
168
|
+
channel: string;
|
|
169
|
+
messages: ConversationMessage[];
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
export interface PairedClient {
|
|
157
173
|
id: string;
|
|
158
174
|
label: string;
|