@nextclaw/ui 0.11.22 → 0.12.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-NKNKsf1J.js} +1 -1
  3. package/dist/assets/ChatPage-p23OnnEI.js +43 -0
  4. package/dist/assets/DocBrowser-C8b2uPgL.js +1 -0
  5. package/dist/assets/{DocBrowser-BmtBLFU0.js → DocBrowser-DxdSujSc.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-YIKkPb76.js → DocBrowserContext-CQ-8jMha.js} +1 -1
  7. package/dist/assets/{LogoBadge-F7ZWdxLT.js → LogoBadge-D-KQIN4U.js} +1 -1
  8. package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-CRNvxtvx.js} +2 -2
  9. package/dist/assets/MarketplacePage-GGkEXowp.js +1 -0
  10. package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-Cu7GmCcc.js} +2 -2
  11. package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-CEpx9fro.js} +1 -1
  12. package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-BWbUb7-2.js} +1 -1
  13. package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-NsawrZb0.js} +1 -1
  14. package/dist/assets/RuntimeConfig-BJHBsVTd.js +1 -0
  15. package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-BsaX_WYy.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-CgDZOd3w.js} +1 -1
  17. package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-Dd-KM7F7.js} +2 -2
  18. package/dist/assets/{book-open-CXoF5nQC.js → book-open-FnK2xCQd.js} +1 -1
  19. package/dist/assets/chat-session-display-BD_AN71I.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-CvRWvTy5.js → chunk-JZWAC4HX-B5l0hr_u.js} +1 -1
  21. package/dist/assets/{config-DJswxxE8.js → config-JKmXfZ3q.js} +1 -1
  22. package/dist/assets/{createLucideIcon-CjGHOWb6.js → createLucideIcon-o1WWhwhd.js} +1 -1
  23. package/dist/assets/{dist-nqTTbVdA.js → dist-C_moWYv7.js} +1 -1
  24. package/dist/assets/{dist-Cl2QB-2y.js → dist-DazA6Wd_.js} +1 -1
  25. package/dist/assets/{external-link-tIO7zING.js → external-link-BKje3SiD.js} +1 -1
  26. package/dist/assets/{hash-JWUyl1pT.js → hash-DfW4DT8O.js} +1 -1
  27. package/dist/assets/i18n-BK1w-oBy.js +1 -0
  28. package/dist/assets/index-BZaB1TqM.js +6 -0
  29. package/dist/assets/index-DaR9igPC.css +1 -0
  30. package/dist/assets/{label-BIpeNu4r.js → label-BzDWmdOe.js} +1 -1
  31. package/dist/assets/loader-circle-DdZPxBUz.js +1 -0
  32. package/dist/assets/{logos-DThdM9lk.js → logos-CTLlde_T.js} +1 -1
  33. package/dist/assets/{page-layout-D3Xo605Z.js → page-layout-BagR3t59.js} +1 -1
  34. package/dist/assets/plus-DP2PSCPO.js +1 -0
  35. package/dist/assets/{popover-BJRUGA_H.js → popover-5DWhNfd4.js} +1 -1
  36. package/dist/assets/{provider-models-bz5y28rq.js → provider-models-DJ29qHuA.js} +1 -1
  37. package/dist/assets/{react-7ZHqQtEV.js → react-C3yu5yge.js} +1 -1
  38. package/dist/assets/{refresh-ccw-CC6-_QuL.js → refresh-ccw-BAJf-h7w.js} +1 -1
  39. package/dist/assets/{save-DJM5RRWW.js → save-aa6z4GJL.js} +1 -1
  40. package/dist/assets/search-pD6ZwQYF.js +1 -0
  41. package/dist/assets/{security-config-T5zpg16O.js → security-config-DRDxrApx.js} +1 -1
  42. package/dist/assets/{select-DSkTc61S.js → select-BHJPiJWt.js} +1 -1
  43. package/dist/assets/skeleton-D6kCk9Y6.js +1 -0
  44. package/dist/assets/{status-dot-LNBlDu3q.js → status-dot-DUwsTIdv.js} +1 -1
  45. package/dist/assets/{switch-Bo-Y46HZ.js → switch-B6nCfcOB.js} +1 -1
  46. package/dist/assets/{tabs-custom-DXv507_2.js → tabs-custom-B57SMElx.js} +1 -1
  47. package/dist/assets/{trash-2-DFZmW6Gg.js → trash-2-CrjYH5ok.js} +1 -1
  48. package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-DsxnXB1B.js} +1 -1
  49. package/dist/assets/{useMutation-DrZrOgVL.js → useMutation-oTTWXgLG.js} +1 -1
  50. package/dist/assets/x-CTIQHUuD.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +5 -5
  53. package/src/App.tsx +2 -0
  54. package/src/api/agents.ts +26 -0
  55. package/src/api/types.ts +27 -2
  56. package/src/components/agents/AgentsPage.test.tsx +70 -0
  57. package/src/components/agents/AgentsPage.tsx +353 -0
  58. package/src/components/chat/ChatConversationPanel.test.tsx +144 -8
  59. package/src/components/chat/ChatConversationPanel.tsx +136 -77
  60. package/src/components/chat/ChatSidebar.test.tsx +8 -0
  61. package/src/components/chat/ChatSidebar.tsx +11 -0
  62. package/src/components/chat/ChatWelcome.test.tsx +25 -0
  63. package/src/components/chat/ChatWelcome.tsx +47 -1
  64. package/src/components/chat/adapters/chat-message-part.adapter.ts +18 -5
  65. package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +102 -0
  66. package/src/components/chat/adapters/chat-message-tool-agent-id.ts +47 -0
  67. package/src/components/chat/adapters/chat-message.adapter.test.ts +89 -9
  68. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +200 -0
  69. package/src/components/chat/chat-child-session-panel.tsx +166 -0
  70. package/src/components/chat/chat-page-runtime.test.ts +1 -0
  71. package/src/components/chat/chat-page-shell.tsx +8 -17
  72. package/src/components/chat/chat-session-display.test.ts +1 -0
  73. package/src/components/chat/chat-session-route.ts +0 -14
  74. package/src/components/chat/chat-sidebar-session-item.tsx +16 -1
  75. package/src/components/chat/containers/chat-input-bar.container.tsx +2 -1
  76. package/src/components/chat/containers/chat-message-list.container.tsx +11 -0
  77. package/src/components/chat/ncp/NcpChatPage.tsx +153 -190
  78. package/src/components/chat/ncp/README.md +3 -0
  79. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +2 -0
  80. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +106 -1
  81. package/src/components/chat/ncp/ncp-session-adapter.test.ts +23 -0
  82. package/src/components/chat/ncp/ncp-session-adapter.ts +32 -0
  83. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +128 -0
  84. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +52 -0
  85. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +101 -0
  86. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +72 -0
  87. package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
  88. package/src/components/chat/presenter/chat-presenter-context.tsx +5 -1
  89. package/src/components/chat/stores/chat-thread.store.ts +25 -1
  90. package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
  91. package/src/components/common/AgentAvatar.tsx +63 -0
  92. package/src/components/common/agent-identity/agent-identity-avatar.tsx +27 -0
  93. package/src/components/common/agent-identity/index.ts +3 -0
  94. package/src/components/common/agent-identity/use-agent-identity.ts +50 -0
  95. package/src/components/config/RuntimeConfig.tsx +13 -79
  96. package/src/components/config/runtime-config-agent.utils.ts +95 -0
  97. package/src/components/layout/AppLayout.tsx +3 -1
  98. package/src/components/layout/Sidebar.tsx +6 -1
  99. package/src/components/layout/app-layout.test.tsx +30 -0
  100. package/src/components/ui/tabs.tsx +2 -0
  101. package/src/hooks/README.md +3 -0
  102. package/src/hooks/agents/useAgents.ts +44 -0
  103. package/src/lib/i18n.agents.ts +66 -0
  104. package/src/lib/i18n.chat.ts +5 -0
  105. package/src/lib/i18n.ts +4 -4
  106. package/src/lib/ui-document-title.ts +1 -0
  107. package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
  108. package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
  109. package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
  110. package/dist/assets/RuntimeConfig-Cmn2xPQO.js +0 -1
  111. package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
  112. package/dist/assets/i18n-CDHMXlRZ.js +0 -1
  113. package/dist/assets/index-BlH4-cBw.css +0 -1
  114. package/dist/assets/index-C6d0xmtm.js +0 -6
  115. package/dist/assets/loader-circle-Cs8XVFTw.js +0 -1
  116. package/dist/assets/plus-PHf8q-Ct.js +0 -1
  117. package/dist/assets/search-C91yH_6y.js +0 -1
  118. package/dist/assets/skeleton-Dzg-HOiN.js +0 -1
  119. package/dist/assets/x-D7Q1yqSF.js +0 -1
  120. package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
  121. /package/src/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
  122. /package/src/lib/{i18n → i18n-runtime}/i18n.path-picker.ts +0 -0
@@ -223,7 +223,7 @@ it("keeps structured terminal results as structured data instead of raw json out
223
223
  });
224
224
  });
225
225
 
226
- it("renders spawn tool cards from structured subagent status updates", () => {
226
+ it("renders session request tool cards from structured child-session status updates", () => {
227
227
  const adapted = adapt([
228
228
  {
229
229
  id: "assistant-subagent",
@@ -237,12 +237,16 @@ it("renders spawn tool cards from structured subagent status updates", () => {
237
237
  toolName: "spawn",
238
238
  args: '{"label":"Verifier","task":"Verify 1+1=2"}',
239
239
  result: {
240
- kind: "nextclaw.subagent_run",
241
- runId: "subagent-1",
242
- label: "Verifier",
240
+ kind: "nextclaw.session_request",
241
+ requestId: "request-1",
242
+ sessionId: "child-session-1",
243
+ agentId: "verifier-agent",
244
+ isChildSession: true,
245
+ title: "Verifier",
243
246
  task: "Verify 1+1=2",
244
247
  status: "completed",
245
- result: "Verified 1+1=2.",
248
+ finalResponseText: "Verified 1+1=2.",
249
+ parentSessionId: "parent-session-1",
246
250
  },
247
251
  },
248
252
  },
@@ -254,21 +258,97 @@ it("renders spawn tool cards from structured subagent status updates", () => {
254
258
  type: "tool-card",
255
259
  card: {
256
260
  toolName: "spawn",
257
- summary: "label: Verifier · run: subagent-1 · task: Verify 1+1=2",
261
+ agentId: "verifier-agent",
262
+ summary: "title: Verifier · session: child-session-1 · task: Verify 1+1=2",
258
263
  output: [
259
- "Run ID: subagent-1",
264
+ "Request ID: request-1",
260
265
  "",
261
- "Label: Verifier",
266
+ "Session ID: child-session-1",
267
+ "",
268
+ "Target: child",
269
+ "",
270
+ "Title: Verifier",
262
271
  "",
263
272
  "Task:",
264
273
  "Verify 1+1=2",
265
274
  "",
266
- "Result:",
275
+ "Final Response:",
267
276
  "Verified 1+1=2.",
268
277
  ].join("\n"),
269
278
  statusTone: "success",
270
279
  statusLabel: "Completed",
271
280
  titleLabel: "Tool Result",
281
+ action: {
282
+ kind: "open-session",
283
+ sessionId: "child-session-1",
284
+ sessionKind: "child",
285
+ agentId: "verifier-agent",
286
+ label: "Verifier",
287
+ parentSessionId: "parent-session-1",
288
+ },
289
+ },
290
+ });
291
+ });
292
+
293
+ it("renders regular session request tool cards with session navigation instead of child navigation", () => {
294
+ const adapted = adapt([
295
+ {
296
+ id: "assistant-session-request",
297
+ role: "assistant",
298
+ parts: [
299
+ {
300
+ type: "tool-invocation",
301
+ toolInvocation: {
302
+ status: ToolInvocationStatus.RESULT,
303
+ toolCallId: "session-request-call-1",
304
+ toolName: "sessions_request",
305
+ args: '{"sessionId":"session-2","task":"Summarize the latest findings"}',
306
+ result: {
307
+ kind: "nextclaw.session_request",
308
+ requestId: "request-2",
309
+ sessionId: "session-2",
310
+ agentId: "research-agent",
311
+ isChildSession: false,
312
+ title: "Research thread",
313
+ task: "Summarize the latest findings",
314
+ status: "completed",
315
+ finalResponseText: "Here is the summary.",
316
+ },
317
+ },
318
+ },
319
+ ],
320
+ },
321
+ ] as unknown as ChatMessageSource[]);
322
+
323
+ expect(adapted[0]?.parts[0]).toMatchObject({
324
+ type: "tool-card",
325
+ card: {
326
+ toolName: "sessions_request",
327
+ agentId: "research-agent",
328
+ summary: "title: Research thread · session: session-2 · task: Summarize the latest findings",
329
+ output: [
330
+ "Request ID: request-2",
331
+ "",
332
+ "Session ID: session-2",
333
+ "",
334
+ "Target: session",
335
+ "",
336
+ "Title: Research thread",
337
+ "",
338
+ "Task:",
339
+ "Summarize the latest findings",
340
+ "",
341
+ "Final Response:",
342
+ "Here is the summary.",
343
+ ].join("\n"),
344
+ statusTone: "success",
345
+ action: {
346
+ kind: "open-session",
347
+ sessionId: "session-2",
348
+ sessionKind: "session",
349
+ agentId: "research-agent",
350
+ label: "Research thread",
351
+ },
272
352
  },
273
353
  });
274
354
  });
@@ -0,0 +1,200 @@
1
+ import {
2
+ stringifyUnknown,
3
+ summarizeToolArgs,
4
+ type ToolCard,
5
+ } from "@/lib/chat-message";
6
+ import { resolveToolInvocationAgentId } from "@/components/chat/adapters/chat-message-tool-agent-id";
7
+ import type { ChatToolPartViewModel } from "@nextclaw/agent-chat-ui";
8
+
9
+ type ToolCardViewSource = ToolCard & {
10
+ statusTone: ChatToolPartViewModel["statusTone"];
11
+ statusLabel: string;
12
+ action?: ChatToolPartViewModel["action"];
13
+ };
14
+
15
+ type SessionRequestInvocation = {
16
+ toolName: string;
17
+ toolCallId?: string;
18
+ args?: unknown;
19
+ result?: unknown;
20
+ };
21
+
22
+ type SessionRequestToolCardTexts = {
23
+ toolStatusRunningLabel: string;
24
+ toolStatusCompletedLabel: string;
25
+ toolStatusFailedLabel: string;
26
+ };
27
+
28
+ type SessionRequestResult = {
29
+ kind: string;
30
+ requestId?: string;
31
+ sessionId?: string;
32
+ agentId?: string;
33
+ isChildSession?: boolean;
34
+ title?: string;
35
+ task?: string;
36
+ status?: string;
37
+ message?: unknown;
38
+ finalResponseText?: unknown;
39
+ error?: unknown;
40
+ parentSessionId?: string;
41
+ };
42
+
43
+ function isRecord(value: unknown): value is Record<string, unknown> {
44
+ return typeof value === "object" && value !== null;
45
+ }
46
+
47
+ function readOptionalString(value: unknown): string | null {
48
+ if (typeof value !== "string") {
49
+ return null;
50
+ }
51
+ const trimmed = value.trim();
52
+ return trimmed.length > 0 ? trimmed : null;
53
+ }
54
+
55
+ function readSessionRequestResult(value: unknown): SessionRequestResult | null {
56
+ if (!isRecord(value) || value.kind !== "nextclaw.session_request") {
57
+ return null;
58
+ }
59
+ return value as SessionRequestResult;
60
+ }
61
+
62
+ function buildSessionRequestDetail(
63
+ result: SessionRequestResult,
64
+ fallbackArgs: unknown,
65
+ ): string | undefined {
66
+ const detailParts = [
67
+ readOptionalString(result.title)
68
+ ? `title: ${result.title?.trim()}`
69
+ : null,
70
+ readOptionalString(result.sessionId)
71
+ ? `session: ${result.sessionId?.trim()}`
72
+ : null,
73
+ readOptionalString(result.task)
74
+ ? `task: ${result.task?.trim()}`
75
+ : null,
76
+ ].filter((value): value is string => Boolean(value));
77
+
78
+ return detailParts.join(" · ") || summarizeToolArgs(fallbackArgs);
79
+ }
80
+
81
+ function buildSessionRequestOutput(result: SessionRequestResult): string | undefined {
82
+ const requestId = readOptionalString(result.requestId);
83
+ const sessionId = readOptionalString(result.sessionId);
84
+ const title = readOptionalString(result.title);
85
+ const task = readOptionalString(result.task);
86
+ const messageText =
87
+ typeof result.message !== "undefined"
88
+ ? stringifyUnknown(result.message).trim()
89
+ : "";
90
+ const finalResponseText =
91
+ typeof result.finalResponseText !== "undefined"
92
+ ? stringifyUnknown(result.finalResponseText).trim()
93
+ : "";
94
+ const errorText =
95
+ typeof result.error !== "undefined"
96
+ ? stringifyUnknown(result.error).trim()
97
+ : "";
98
+
99
+ const sections = [
100
+ requestId ? `Request ID: ${requestId}` : null,
101
+ sessionId ? `Session ID: ${sessionId}` : null,
102
+ typeof result.isChildSession === "boolean"
103
+ ? `Target: ${result.isChildSession ? "child" : "session"}`
104
+ : null,
105
+ title ? `Title: ${title}` : null,
106
+ task ? `Task:\n${task}` : null,
107
+ finalResponseText
108
+ ? `Final Response:\n${finalResponseText}`
109
+ : errorText
110
+ ? `Error:\n${errorText}`
111
+ : messageText
112
+ ? `Status:\n${messageText}`
113
+ : null,
114
+ ].filter((value): value is string => Boolean(value));
115
+
116
+ return sections.length > 0 ? sections.join("\n\n") : undefined;
117
+ }
118
+
119
+ export function buildSessionRequestToolCard(params: {
120
+ invocation: SessionRequestInvocation;
121
+ texts: SessionRequestToolCardTexts;
122
+ }): ToolCardViewSource | null {
123
+ const { invocation, texts } = params;
124
+ const { toolName, toolCallId, args, result } = invocation;
125
+
126
+ if (toolName !== "spawn" && toolName !== "sessions_request") {
127
+ return null;
128
+ }
129
+
130
+ const sessionRequest = readSessionRequestResult(result);
131
+ if (!sessionRequest) {
132
+ return null;
133
+ }
134
+
135
+ const normalizedStatus = readOptionalString(sessionRequest.status)?.toLowerCase();
136
+ const detail = buildSessionRequestDetail(sessionRequest, args);
137
+ const output = buildSessionRequestOutput(sessionRequest);
138
+ const targetSessionId = readOptionalString(sessionRequest.sessionId);
139
+ const agentId = resolveToolInvocationAgentId({ args, result: sessionRequest });
140
+ const action =
141
+ targetSessionId
142
+ ? {
143
+ kind: "open-session" as const,
144
+ sessionId: targetSessionId,
145
+ sessionKind: sessionRequest.isChildSession === true ? ("child" as const) : ("session" as const),
146
+ ...(agentId
147
+ ? { agentId }
148
+ : {}),
149
+ ...(readOptionalString(sessionRequest.title)
150
+ ? { label: sessionRequest.title!.trim() }
151
+ : {}),
152
+ ...(readOptionalString(sessionRequest.parentSessionId)
153
+ ? { parentSessionId: sessionRequest.parentSessionId!.trim() }
154
+ : {}),
155
+ }
156
+ : undefined;
157
+
158
+ if (normalizedStatus === "failed") {
159
+ return {
160
+ kind: "result",
161
+ name: toolName,
162
+ detail,
163
+ text: output,
164
+ callId: toolCallId || undefined,
165
+ hasResult: Boolean(output),
166
+ statusTone: "error",
167
+ statusLabel: texts.toolStatusFailedLabel,
168
+ ...(agentId ? { agentId } : {}),
169
+ ...(action ? { action } : {}),
170
+ };
171
+ }
172
+
173
+ if (normalizedStatus === "completed") {
174
+ return {
175
+ kind: "result",
176
+ name: toolName,
177
+ detail,
178
+ text: output,
179
+ callId: toolCallId || undefined,
180
+ hasResult: Boolean(output),
181
+ statusTone: "success",
182
+ statusLabel: texts.toolStatusCompletedLabel,
183
+ ...(agentId ? { agentId } : {}),
184
+ ...(action ? { action } : {}),
185
+ };
186
+ }
187
+
188
+ return {
189
+ kind: "result",
190
+ name: toolName,
191
+ detail,
192
+ text: output,
193
+ callId: toolCallId || undefined,
194
+ hasResult: Boolean(output),
195
+ statusTone: "running",
196
+ statusLabel: texts.toolStatusRunningLabel,
197
+ ...(agentId ? { agentId } : {}),
198
+ ...(action ? { action } : {}),
199
+ };
200
+ }
@@ -0,0 +1,166 @@
1
+ import { ArrowLeft, Loader2, X } from 'lucide-react';
2
+ import { ChatMessageListContainer } from '@/components/chat/containers/chat-message-list.container';
3
+ import { useNcpChildSessionTabsView } from '@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view';
4
+ import { useNcpSessionConversation } from '@/components/chat/ncp/session-conversation/use-ncp-session-conversation';
5
+ import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
6
+ import { AgentIdentityAvatar } from '@/components/common/agent-identity';
7
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
8
+ import { t } from '@/lib/i18n';
9
+ import { cn } from '@/lib/utils';
10
+ import type { ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
11
+
12
+ type ChatChildSessionPanelProps = {
13
+ tabs: readonly ChatChildSessionTab[];
14
+ activeSessionKey: string;
15
+ onSelectSession: (sessionKey: string) => void;
16
+ onClose: () => void;
17
+ onBackToParent: () => void;
18
+ onToolAction?: (action: ChatToolActionViewModel) => void;
19
+ };
20
+
21
+ function ChildSessionPanelConversation({
22
+ sessionKey,
23
+ onToolAction,
24
+ }: {
25
+ sessionKey: string;
26
+ onToolAction?: (action: ChatToolActionViewModel) => void;
27
+ }) {
28
+ const agent = useNcpSessionConversation(sessionKey);
29
+ const messages = agent.visibleMessages;
30
+
31
+ if (agent.isHydrating) {
32
+ return (
33
+ <div className="flex h-full items-center justify-center text-sm text-gray-500">
34
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
35
+ {t('chatChildSessionLoading')}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ if (agent.hydrateError) {
41
+ return (
42
+ <div className="px-4 py-5 text-sm text-rose-600">
43
+ {agent.hydrateError.message}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ if (messages.length === 0 && !agent.isRunning) {
49
+ return (
50
+ <div className="px-4 py-5 text-sm text-gray-500">
51
+ {t('chatChildSessionEmpty')}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div className="px-4 py-5">
58
+ <ChatMessageListContainer
59
+ messages={messages}
60
+ isSending={agent.isRunning}
61
+ onToolAction={onToolAction}
62
+ />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export function ChatChildSessionPanel({
68
+ tabs,
69
+ activeSessionKey,
70
+ onSelectSession,
71
+ onClose,
72
+ onBackToParent,
73
+ onToolAction,
74
+ }: ChatChildSessionPanelProps) {
75
+ const resolvedTabs = useNcpChildSessionTabsView(tabs);
76
+ const activeTab =
77
+ resolvedTabs.find((tab) => tab.sessionKey === activeSessionKey) ??
78
+ resolvedTabs[0] ??
79
+ null;
80
+ const hasParentSession = resolvedTabs.some((tab) => Boolean(tab.parentSessionKey));
81
+ const shouldShowTabs = resolvedTabs.length > 1;
82
+
83
+ if (!activeTab) {
84
+ return null;
85
+ }
86
+
87
+ return (
88
+ <aside className="hidden md:flex md:w-[24rem] lg:w-[28rem] shrink-0 border-l border-gray-200/70 bg-white/90 backdrop-blur-sm">
89
+ <div className="flex h-full min-h-0 w-full flex-col">
90
+ <div className="border-b border-gray-200/70 px-4 py-3">
91
+ <div className="flex items-center justify-between gap-3">
92
+ <button
93
+ type="button"
94
+ onClick={onBackToParent}
95
+ className={cn(
96
+ 'inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900',
97
+ !hasParentSession && 'pointer-events-none opacity-0',
98
+ )}
99
+ >
100
+ <ArrowLeft className="h-3.5 w-3.5" />
101
+ <span>{t('chatBackToParent')}</span>
102
+ </button>
103
+ <button
104
+ type="button"
105
+ onClick={onClose}
106
+ className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
107
+ aria-label={t('chatChildSessionClosePanel')}
108
+ >
109
+ <X className="h-4 w-4" />
110
+ </button>
111
+ </div>
112
+ {!shouldShowTabs ? (
113
+ <div className="mt-3 flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
114
+ {activeTab.agentId ? (
115
+ <AgentIdentityAvatar
116
+ agentId={activeTab.agentId}
117
+ className="h-5 w-5 shrink-0"
118
+ />
119
+ ) : null}
120
+ <span className="truncate" title={activeTab.sessionKey}>
121
+ {activeTab.title}
122
+ </span>
123
+ </div>
124
+ ) : null}
125
+ {shouldShowTabs ? (
126
+ <div className="mt-3 overflow-x-auto custom-scrollbar">
127
+ <Tabs value={activeSessionKey} onValueChange={onSelectSession}>
128
+ <TabsList className="h-auto min-w-max justify-start gap-1.5 rounded-none bg-transparent p-0 text-gray-500">
129
+ {resolvedTabs.map((tab) => (
130
+ <TabsTrigger
131
+ key={tab.sessionKey}
132
+ value={tab.sessionKey}
133
+ className="gap-2 rounded-full border border-gray-200/80 bg-white/85 px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-none hover:border-primary/30 hover:text-primary data-[state=active]:border-primary/30 data-[state=active]:bg-primary-50/70 data-[state=active]:text-primary data-[state=active]:shadow-sm"
134
+ >
135
+ {tab.agentId ? (
136
+ <AgentIdentityAvatar
137
+ agentId={tab.agentId}
138
+ className="h-4 w-4 shrink-0"
139
+ />
140
+ ) : null}
141
+ <span className="max-w-[132px] truncate">{tab.title}</span>
142
+ </TabsTrigger>
143
+ ))}
144
+ </TabsList>
145
+ </Tabs>
146
+ </div>
147
+ ) : null}
148
+ </div>
149
+
150
+ <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
151
+ {resolvedTabs.map((tab) => (
152
+ <div
153
+ key={tab.sessionKey}
154
+ className={cn(tab.sessionKey === activeSessionKey ? 'block' : 'hidden')}
155
+ >
156
+ <ChildSessionPanelConversation
157
+ sessionKey={tab.sessionKey}
158
+ onToolAction={onToolAction}
159
+ />
160
+ </div>
161
+ ))}
162
+ </div>
163
+ </div>
164
+ </aside>
165
+ );
166
+ }
@@ -29,6 +29,7 @@ function createSession(overrides: Partial<SessionEntryView> & Pick<SessionEntryV
29
29
  updatedAt: overrides.updatedAt ?? '2026-03-19T00:00:00.000Z',
30
30
  sessionType: overrides.sessionType ?? 'native',
31
31
  sessionTypeMutable: overrides.sessionTypeMutable ?? false,
32
+ isChildSession: overrides.isChildSession ?? false,
32
33
  messageCount: overrides.messageCount ?? 0,
33
34
  ...(overrides.label ? { label: overrides.label } : {}),
34
35
  ...(overrides.preferredModel ? { preferredModel: overrides.preferredModel } : {}),
@@ -2,10 +2,11 @@ import { useEffect } from 'react';
2
2
  import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
3
  import { ChatSidebar } from '@/components/chat/ChatSidebar';
4
4
  import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
5
+ import { AgentsPage } from '@/components/agents/AgentsPage';
5
6
  import { CronConfig } from '@/components/config/CronConfig';
6
7
  import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
7
8
 
8
- export type MainPanelView = 'chat' | 'cron' | 'skills';
9
+ export type MainPanelView = 'chat' | 'cron' | 'skills' | 'agents';
9
10
 
10
11
  export type ChatPageProps = {
11
12
  view: MainPanelView;
@@ -15,12 +16,9 @@ type UseChatSessionSyncParams = {
15
16
  view: MainPanelView;
16
17
  routeSessionKey: string | null;
17
18
  selectedSessionKey: string | null;
18
- selectedAgentId: string;
19
19
  setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
20
- setSelectedAgentId: Dispatch<SetStateAction<string>>;
21
20
  selectedSessionKeyRef: MutableRefObject<string | null>;
22
21
  resetStreamState: () => void;
23
- resolveAgentIdFromSessionKey: (sessionKey: string) => string | null;
24
22
  };
25
23
 
26
24
  export function useChatSessionSync(params: UseChatSessionSyncParams): void {
@@ -28,12 +26,9 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
28
26
  view,
29
27
  routeSessionKey,
30
28
  selectedSessionKey,
31
- selectedAgentId,
32
29
  setSelectedSessionKey,
33
- setSelectedAgentId,
34
30
  selectedSessionKeyRef,
35
31
  resetStreamState,
36
- resolveAgentIdFromSessionKey
37
32
  } = params;
38
33
 
39
34
  useEffect(() => {
@@ -52,16 +47,6 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
52
47
  }
53
48
  }, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
54
49
 
55
- useEffect(() => {
56
- const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
57
- if (!inferred) {
58
- return;
59
- }
60
- if (selectedAgentId !== inferred) {
61
- setSelectedAgentId(inferred);
62
- }
63
- }, [resolveAgentIdFromSessionKey, selectedAgentId, selectedSessionKey, setSelectedAgentId]);
64
-
65
50
  useEffect(() => {
66
51
  selectedSessionKeyRef.current = selectedSessionKey;
67
52
  }, [selectedSessionKey, selectedSessionKeyRef]);
@@ -87,6 +72,12 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
87
72
  <CronConfig />
88
73
  </div>
89
74
  </div>
75
+ ) : view === 'agents' ? (
76
+ <div className="h-full overflow-auto custom-scrollbar">
77
+ <div className="mx-auto w-full max-w-[min(1180px,100%)] px-6 py-5">
78
+ <AgentsPage />
79
+ </div>
80
+ </div>
90
81
  ) : (
91
82
  <div className="h-full overflow-hidden">
92
83
  <div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
@@ -9,6 +9,7 @@ function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryV
9
9
  updatedAt: '2026-03-31T10:00:00.000Z',
10
10
  sessionType: 'native',
11
11
  sessionTypeMutable: false,
12
+ isChildSession: false,
12
13
  messageCount: 3,
13
14
  ...overrides
14
15
  };
@@ -1,19 +1,5 @@
1
1
  const SESSION_ROUTE_PREFIX = 'sid_';
2
2
 
3
- export function resolveAgentIdFromSessionKey(sessionKey: string): string | null {
4
- const match = /^agent:([^:]+):/i.exec(sessionKey.trim());
5
- if (!match) {
6
- return null;
7
- }
8
- const value = match[1]?.trim();
9
- return value ? value : null;
10
- }
11
-
12
- export function buildNewSessionKey(agentId: string): string {
13
- const slug = Math.random().toString(36).slice(2, 8);
14
- return `agent:${agentId}:ui:direct:web-${Date.now().toString(36)}${slug}`;
15
- }
16
-
17
3
  export function encodeSessionRouteId(sessionKey: string): string {
18
4
  const bytes = new TextEncoder().encode(sessionKey);
19
5
  let binary = '';
@@ -1,4 +1,5 @@
1
1
  import type { SessionEntryView } from '@/api/types';
2
+ import { AgentAvatar } from '@/components/common/AgentAvatar';
2
3
  import { SessionContextIconNode } from '@/components/common/session-context-icon';
3
4
  import { SessionRunBadge } from '@/components/common/SessionRunBadge';
4
5
  import { Button } from '@/components/ui/button';
@@ -15,6 +16,9 @@ type ChatSidebarSessionItemProps = {
15
16
  runStatus?: SessionRunStatus;
16
17
  context: SessionContextView;
17
18
  title: string;
19
+ agentId?: string | null;
20
+ agentLabel?: string | null;
21
+ agentAvatarUrl?: string | null;
18
22
  isEditing: boolean;
19
23
  draftLabel: string;
20
24
  isSaving: boolean;
@@ -32,6 +36,9 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
32
36
  runStatus,
33
37
  context,
34
38
  title,
39
+ agentId,
40
+ agentLabel,
41
+ agentAvatarUrl,
35
42
  isEditing,
36
43
  draftLabel,
37
44
  isSaving,
@@ -105,6 +112,14 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
105
112
  <button type="button" onClick={onSelect} className="w-full text-left">
106
113
  <div className="grid grid-cols-[minmax(0,1fr)_0.875rem] items-center gap-1.5 pr-8">
107
114
  <span className="flex min-w-0 items-center gap-1.5">
115
+ {agentId ? (
116
+ <AgentAvatar
117
+ agentId={agentId}
118
+ displayName={agentLabel}
119
+ avatarUrl={agentAvatarUrl}
120
+ className="h-5 w-5 shrink-0"
121
+ />
122
+ ) : null}
108
123
  <span className="truncate font-medium">{title}</span>
109
124
  {context.label ? (
110
125
  <span
@@ -129,7 +144,7 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
129
144
  </span>
130
145
  </div>
131
146
  <div className="mt-0.5 text-[11px] text-gray-400 truncate">
132
- {session.messageCount} · {formatDateTime(session.updatedAt)}
147
+ {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
133
148
  </div>
134
149
  </button>
135
150
  <button
@@ -87,8 +87,9 @@ export function ChatInputBarContainer() {
87
87
  const inputBarRef = useRef<ChatInputBarHandle | null>(null);
88
88
  const fileInputRef = useRef<HTMLInputElement | null>(null);
89
89
 
90
- const skillScopeLabels = useMemo<Record<'project' | 'workspace', string>>(() => {
90
+ const skillScopeLabels = useMemo<Record<'builtin' | 'project' | 'workspace', string>>(() => {
91
91
  return {
92
+ builtin: t('chatSkillScopeBuiltin'),
92
93
  project: t('chatSkillScopeProject'),
93
94
  workspace: t('chatSkillScopeWorkspace'),
94
95
  };