@agentprojectcontext/apx 1.53.7 → 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.
@@ -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-Bks35ks-.js"></script>
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: either a "live" agent session
68
- // (no conversation file yet) or a persisted conversation tied to an agent.
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
- return k.kind === "live" ? `live:${k.agentSlug}` : `conv:${k.agentSlug}:${k.convId}`;
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
- // Group: live entries (one per applicable agent) + stored conversations by channel.
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, ConversationListEntry[]>();
159
- for (const c of filteredConvs) {
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(c);
163
- else byKey.set(key, [c]);
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.started_at || 0).getTime() - new Date(a.started_at || 0).getTime(),
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 = Object.keys(byAgent).length > 0 || agents.length === 0;
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((c) => {
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
- return { msgs, send, stop, clear, load, streaming, conversationId };
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 === "live"
51
- ? agentList.find((a) => a.slug === selected.agentSlug)
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, reload the in-memory chat
58
- // with its persisted history. The hook itself binds the conversation_id so
59
- // subsequent sends append to the same file.
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
- }, [selected.kind, selected.kind === "conv" ? selected.convId : selected.agentSlug]);
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 = activeIsRoby
90
- ? t("project.chat.superagent_title", { persona })
91
- : selected.kind === "conv"
92
- ? selected.convId
93
- : t("project.chat.title");
94
- const headerSubtitle = activeIsRoby
95
- ? t("project.chat.superagent_subtitle", { persona })
96
- : selected.kind === "conv"
97
- ? t("project.chat.loaded_subtitle", { slug: selected.agentSlug })
98
- : t("project.chat.subtitle");
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;