@agentprojectcontext/apx 1.56.2 → 1.57.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.
@@ -1,14 +1,14 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
  import { useSearchParams } from "react-router-dom";
3
- import useSWR from "swr";
4
- import { Plus, Trash2 } from "lucide-react";
5
- import { Agents } from "../../lib/api";
3
+ import useSWR, { mutate } from "swr";
4
+ import { Plus, RotateCcw, Trash2 } from "lucide-react";
5
+ import { Agents, Conversations } from "../../lib/api";
6
6
  import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch } from "../../components/ui";
7
7
  import { Composer } from "../../components/chat/Composer";
8
8
  import { MessageList } from "../../components/chat/MessageList";
9
9
  import { ContextBar } from "../../components/chat/ContextBar";
10
10
  import { InlineAskPanel, pendingAskQuestions } from "../../components/chat/InlineAskPanel";
11
- import { ChatList, type ChatKey } from "../../components/chat/ChatList";
11
+ import { ChatList, type ChatKey, type ChatSelectionMeta } from "../../components/chat/ChatList";
12
12
  import { useChat } from "../../hooks/useChat";
13
13
  import { useToast } from "../../components/Toast";
14
14
  import { t } from "../../i18n";
@@ -22,24 +22,52 @@ const ROBY_SLUG = "__super_agent__";
22
22
 
23
23
  export function ChatTab({ pid }: { pid: string }) {
24
24
  const toast = useToast();
25
- const [params] = useSearchParams();
25
+ const [params, setSearchParams] = useSearchParams();
26
26
  const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
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, loadThread, streaming, conversationId } =
30
+ const { msgs, send: sendChat, stop, clear, load, loadThread, streaming } =
31
31
  useChat(pid, (m) => toast.error(m));
32
32
  const persona = usePersonaName();
33
33
 
34
34
  // Selection state — drives both the sidebar highlight and the right-pane
35
- // header. Defaults to a live session with the super-agent so the chat works
36
- // even on a brand-new project with zero agents and zero conversations.
37
- const initialFromUrl = params.get("agent");
38
- const [selected, setSelected] = useState<ChatKey>(
39
- initialFromUrl
40
- ? { kind: "live", agentSlug: initialFromUrl }
41
- : { kind: "live", agentSlug: ROBY_SLUG },
42
- );
35
+ // header. Restored from the URL query on mount (so a chat is deep-linkable),
36
+ // defaulting to a live session with the super-agent so the chat works even on
37
+ // a brand-new project with zero agents and zero conversations.
38
+ const [selected, setSelected] = useState<ChatKey>(() => {
39
+ const agent = params.get("agent");
40
+ const conv = params.get("conv");
41
+ const channel = params.get("channel");
42
+ const thread = params.get("thread");
43
+ if (channel && thread) return { kind: "thread", channel, threadId: thread };
44
+ if (agent && conv) return { kind: "conv", agentSlug: agent, convId: conv };
45
+ if (agent) return { kind: "live", agentSlug: agent };
46
+ return { kind: "live", agentSlug: ROBY_SLUG };
47
+ });
48
+ // Display metadata for the current selection (channel/created date/title),
49
+ // carried from the sidebar so the header can show it without a second fetch.
50
+ const [selectedMeta, setSelectedMeta] = useState<ChatSelectionMeta | undefined>(undefined);
51
+ const [confirmDelete, setConfirmDelete] = useState(false);
52
+ const [deleting, setDeleting] = useState(false);
53
+
54
+ // Select a chat and mirror its id into the URL query so the current chat is
55
+ // shareable/deep-linkable. `replace` keeps navigation history clean.
56
+ const selectChat = (key: ChatKey, meta?: ChatSelectionMeta) => {
57
+ setSelected(key);
58
+ setSelectedMeta(meta);
59
+ const next = new URLSearchParams();
60
+ if (key.kind === "conv") {
61
+ next.set("agent", key.agentSlug);
62
+ next.set("conv", key.convId);
63
+ } else if (key.kind === "thread") {
64
+ next.set("channel", key.channel);
65
+ next.set("thread", key.threadId);
66
+ } else {
67
+ next.set("agent", key.agentSlug);
68
+ }
69
+ setSearchParams(next, { replace: true });
70
+ };
43
71
 
44
72
  const agentList = agents.data || [];
45
73
  const isRoby = (slug: string | null | undefined) => slug === ROBY_SLUG;
@@ -95,27 +123,61 @@ export function ChatTab({ pid }: { pid: string }) {
95
123
  catch { /* ignore */ }
96
124
  };
97
125
 
98
- const onNewChat = () => {
99
- setSelected({ kind: "live", agentSlug: ROBY_SLUG });
126
+ // "+ New" from the sidebar: start a fresh in-memory session with the picked
127
+ // agent (super-agent or a project agent). It materialises in the Web group
128
+ // once the first message is sent.
129
+ const onNewChat = (agentSlug: string) => {
130
+ selectChat({ kind: "live", agentSlug });
131
+ clear();
132
+ };
133
+
134
+ // "New session" header button: reset the pane but stay with the current
135
+ // agent (Roby for channel threads / the super-agent, else the project agent).
136
+ const newSession = () => {
137
+ const agentSlug = activeIsRoby ? ROBY_SLUG : activeAgent?.slug ?? selected.agentSlug;
138
+ selectChat({ kind: "live", agentSlug });
100
139
  clear();
101
140
  };
102
141
 
142
+ // "Delete" header button: permanently remove the persisted conversation
143
+ // (agent `.md` file) or channel thread (ledger day-file), then reset the pane
144
+ // and revalidate the sidebar list so the entry disappears.
145
+ const doDelete = async () => {
146
+ setDeleting(true);
147
+ try {
148
+ if (selected.kind === "conv") {
149
+ await Conversations.remove(pid, selected.agentSlug, selected.convId);
150
+ void mutate(`/projects/${pid}/agents/${selected.agentSlug}/conversations`);
151
+ } else if (selected.kind === "thread") {
152
+ await Conversations.removeThread(pid, selected.channel, selected.threadId);
153
+ void mutate(`/projects/${pid}/super-agent/threads`);
154
+ }
155
+ toast.success(t("project.chat.deleted"));
156
+ setConfirmDelete(false);
157
+ newSession();
158
+ } catch (e) {
159
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
160
+ } finally {
161
+ setDeleting(false);
162
+ }
163
+ };
164
+
165
+ // Header shows "Created {date} · {channel} · {agent}" (or "New chat · …" for a
166
+ // fresh session with no persisted date yet), per the sidebar redesign.
167
+ const agentLabel = activeIsRoby ? persona : activeAgent?.slug ?? selected.agentSlug;
168
+ const channelLabel =
169
+ selected.kind === "thread" ? selected.channel : selectedMeta?.channel || "web";
170
+ const createdIso =
171
+ selected.kind === "thread" ? selected.threadId : selectedMeta?.createdAt;
172
+
103
173
  const headerTitle =
104
- selected.kind === "thread"
105
- ? `${selected.channel} · ${selected.threadId}`
106
- : activeIsRoby
107
- ? t("project.chat.superagent_title", { persona })
108
- : selected.kind === "conv"
109
- ? selected.convId
110
- : t("project.chat.title");
111
- const headerSubtitle =
112
- selected.kind === "thread"
113
- ? t("project.chat.thread_subtitle", { channel: selected.channel, persona })
114
- : activeIsRoby
115
- ? t("project.chat.superagent_subtitle", { persona })
116
- : selected.kind === "conv"
117
- ? t("project.chat.loaded_subtitle", { slug: selected.agentSlug })
118
- : t("project.chat.subtitle");
174
+ selected.kind === "live"
175
+ ? t("project.chat.live_title", { agent: agentLabel })
176
+ : selectedMeta?.title ||
177
+ (selected.kind === "thread" ? selected.threadId : selected.convId);
178
+ const headerSubtitle = createdIso
179
+ ? t("project.chat.meta_created", { date: formatDate(createdIso), channel: channelLabel })
180
+ : t("project.chat.meta_new", { channel: channelLabel });
119
181
 
120
182
  if (agents.isLoading) return <Loading />;
121
183
 
@@ -127,7 +189,7 @@ export function ChatTab({ pid }: { pid: string }) {
127
189
  superAgentSlug={ROBY_SLUG}
128
190
  superAgentLabel={t("agents_ui.super_agent_label", { persona })}
129
191
  selected={selected}
130
- onSelect={setSelected}
192
+ onSelect={selectChat}
131
193
  onNewChat={onNewChat}
132
194
  />
133
195
 
@@ -141,9 +203,8 @@ export function ChatTab({ pid }: { pid: string }) {
141
203
  {activeIsRoby ? (
142
204
  <Badge tone="success">{t("agents_ui.super_agent_badge")}</Badge>
143
205
  ) : (
144
- activeAgent?.model && <Badge tone="info">{activeAgent.model}</Badge>
206
+ <Badge tone="info">{agentLabel}</Badge>
145
207
  )}
146
- {selected.kind === "conv" && <Badge tone="info">{conversationId || "…"}</Badge>}
147
208
  {!agentList.length && !activeIsRoby && (
148
209
  <Button variant="primary" size="sm" onClick={() => setCreating(true)}>
149
210
  <Plus size={14} /> {t("project.chat.create_agent")}
@@ -153,10 +214,20 @@ export function ChatTab({ pid }: { pid: string }) {
153
214
  variant="ghost"
154
215
  size="sm"
155
216
  disabled={streaming || msgs.length === 0}
156
- onClick={onNewChat}
217
+ onClick={newSession}
157
218
  >
158
- <Trash2 size={13} /> {t("project.chat.clear")}
219
+ <RotateCcw size={13} /> {t("project.chat.new_session")}
159
220
  </Button>
221
+ {(selected.kind === "conv" || selected.kind === "thread") && (
222
+ <Button
223
+ variant="destructive"
224
+ size="sm"
225
+ disabled={streaming}
226
+ onClick={() => setConfirmDelete(true)}
227
+ >
228
+ <Trash2 size={13} /> {t("project.chat.delete")}
229
+ </Button>
230
+ )}
160
231
  </div>
161
232
  </header>
162
233
 
@@ -198,10 +269,39 @@ export function ChatTab({ pid }: { pid: string }) {
198
269
  onClose={() => setCreating(false)}
199
270
  onCreated={() => { setCreating(false); agents.mutate(); }}
200
271
  />
272
+
273
+ <Dialog
274
+ open={confirmDelete}
275
+ onClose={() => setConfirmDelete(false)}
276
+ title={t("project.chat.delete_confirm_title")}
277
+ description={t("project.chat.delete_confirm_desc")}
278
+ size="sm"
279
+ footer={
280
+ <>
281
+ <Button variant="ghost" onClick={() => setConfirmDelete(false)} disabled={deleting}>
282
+ {t("common.cancel")}
283
+ </Button>
284
+ <Button variant="destructive" onClick={doDelete} loading={deleting}>
285
+ <Trash2 size={14} /> {t("project.chat.delete")}
286
+ </Button>
287
+ </>
288
+ }
289
+ >
290
+ <p className="text-sm text-muted-fg">{headerTitle}</p>
291
+ </Dialog>
201
292
  </div>
202
293
  );
203
294
  }
204
295
 
296
+ // Localised short date for the header "Created {date}" line. Falls back to the
297
+ // raw string for anything Date can't parse.
298
+ function formatDate(iso?: string): string {
299
+ if (!iso) return "";
300
+ const d = new Date(iso);
301
+ if (Number.isNaN(d.getTime())) return iso;
302
+ return d.toLocaleDateString();
303
+ }
304
+
205
305
  function CreateAgentDialog({
206
306
  open,
207
307
  onClose,
@@ -143,6 +143,11 @@ export interface ConversationMessage {
143
143
  content: string;
144
144
  ts?: string;
145
145
  name?: string;
146
+ /** Present on role:"tool" rows from the global ledger — the tool name and its
147
+ * structured args/result, so the viewer can render a ToolCall part. */
148
+ tool?: string;
149
+ args?: Record<string, unknown>;
150
+ result?: unknown;
146
151
  }
147
152
 
148
153
  export interface ConversationDetail {