@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
@@ -1,10 +1,13 @@
1
1
  import { useRef } from "react";
2
+ import { ArrowLeft } from "lucide-react";
2
3
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
4
  import {
4
5
  ChatInputBarContainer,
5
6
  ChatMessageListContainer,
6
7
  } from "@/components/chat/nextclaw";
8
+ import { ChatChildSessionPanel } from "@/components/chat/chat-child-session-panel";
7
9
  import { ChatWelcome } from "@/components/chat/ChatWelcome";
10
+ import { AgentAvatar } from "@/components/common/AgentAvatar";
8
11
  import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
9
12
  import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
10
13
  import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
@@ -45,6 +48,14 @@ export function ChatConversationPanel() {
45
48
  const snapshot = useChatThreadStore((state) => state.snapshot);
46
49
  const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
47
50
  const threadRef = snapshot.threadRef ?? fallbackThreadRef;
51
+ const childSessionTabs = snapshot.childSessionTabs.filter(
52
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
53
+ );
54
+ const detailSessionKey = childSessionTabs.some(
55
+ (tab) => tab.sessionKey === snapshot.activeChildSessionKey,
56
+ )
57
+ ? snapshot.activeChildSessionKey
58
+ : (childSessionTabs[childSessionTabs.length - 1]?.sessionKey ?? null);
48
59
  const shouldShowSessionHeader = Boolean(
49
60
  snapshot.sessionKey || snapshot.sessionTypeLabel,
50
61
  );
@@ -79,97 +90,145 @@ export function ChatConversationPanel() {
79
90
  }
80
91
 
81
92
  return (
82
- <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
83
- <div
84
- className={cn(
85
- "px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
86
- shouldShowSessionHeader
87
- ? "py-3 opacity-100"
88
- : "h-0 py-0 opacity-0 border-b-0",
89
- )}
90
- >
91
- <div className="min-w-0 flex-1 flex items-center gap-2">
92
- <span className="text-sm font-medium text-gray-700 truncate">
93
- {sessionHeaderTitle}
94
- </span>
95
- {snapshot.sessionTypeLabel ? (
96
- <span className="shrink-0 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
97
- {snapshot.sessionTypeLabel}
93
+ <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
94
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
95
+ {snapshot.parentSessionKey ? (
96
+ <div className="border-b border-gray-200/60 bg-white/75 px-5 py-2 backdrop-blur-sm">
97
+ <button
98
+ type="button"
99
+ onClick={presenter.chatThreadManager.goToParentSession}
100
+ className="inline-flex items-center gap-2 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900"
101
+ >
102
+ <ArrowLeft className="h-3.5 w-3.5" />
103
+ <span>
104
+ {t("chatBackToParent")}
105
+ {snapshot.parentSessionLabel?.trim()
106
+ ? ` · ${snapshot.parentSessionLabel.trim()}`
107
+ : ""}
108
+ </span>
109
+ </button>
110
+ </div>
111
+ ) : null}
112
+
113
+ <div
114
+ className={cn(
115
+ "px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
116
+ shouldShowSessionHeader
117
+ ? "py-3 opacity-100"
118
+ : "h-0 py-0 opacity-0 border-b-0",
119
+ )}
120
+ >
121
+ <div className="min-w-0 flex-1 flex items-center gap-2">
122
+ {snapshot.agentId ? (
123
+ <div className="inline-flex items-center gap-2 shrink-0 rounded-full border border-gray-200 bg-white/80 px-2 py-1">
124
+ <AgentAvatar
125
+ agentId={snapshot.agentId}
126
+ displayName={snapshot.agentDisplayName}
127
+ avatarUrl={snapshot.agentAvatarUrl}
128
+ className="h-5 w-5"
129
+ />
130
+ <span className="max-w-[120px] truncate text-xs font-medium text-gray-700">
131
+ {snapshot.agentDisplayName?.trim() || snapshot.agentId}
132
+ </span>
133
+ </div>
134
+ ) : null}
135
+ <span className="text-sm font-medium text-gray-700 truncate">
136
+ {sessionHeaderTitle}
98
137
  </span>
99
- ) : null}
100
- {snapshot.sessionProjectName ? (
101
- <ChatSessionProjectBadge
102
- sessionKey={snapshot.sessionKey ?? "draft"}
103
- projectName={snapshot.sessionProjectName}
138
+ {snapshot.sessionTypeLabel ? (
139
+ <span className="shrink-0 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
140
+ {snapshot.sessionTypeLabel}
141
+ </span>
142
+ ) : null}
143
+ {snapshot.sessionProjectName ? (
144
+ <ChatSessionProjectBadge
145
+ sessionKey={snapshot.sessionKey ?? "draft"}
146
+ projectName={snapshot.sessionProjectName}
147
+ projectRoot={snapshot.sessionProjectRoot}
148
+ persistToServer={snapshot.canDeleteSession}
149
+ />
150
+ ) : null}
151
+ </div>
152
+ {snapshot.sessionKey ? (
153
+ <ChatSessionHeaderActions
154
+ sessionKey={snapshot.sessionKey}
155
+ canDeleteSession={snapshot.canDeleteSession}
156
+ isDeletePending={snapshot.isDeletePending}
104
157
  projectRoot={snapshot.sessionProjectRoot}
105
- persistToServer={snapshot.canDeleteSession}
158
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
106
159
  />
107
160
  ) : null}
108
161
  </div>
109
- {snapshot.sessionKey ? (
110
- <ChatSessionHeaderActions
111
- sessionKey={snapshot.sessionKey}
112
- canDeleteSession={snapshot.canDeleteSession}
113
- isDeletePending={snapshot.isDeletePending}
114
- projectRoot={snapshot.sessionProjectRoot}
115
- onDeleteSession={presenter.chatThreadManager.deleteSession}
116
- />
117
- ) : null}
118
- </div>
119
162
 
120
- {shouldShowProviderHint && (
121
- <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
122
- <span className="text-xs text-amber-800">
123
- {t("chatModelNoOptions")}
124
- </span>
125
- <button
126
- type="button"
127
- onClick={presenter.chatThreadManager.goToProviders}
128
- className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
129
- >
130
- {t("chatGoConfigureProvider")}
131
- </button>
132
- </div>
133
- )}
134
-
135
- {snapshot.sessionTypeUnavailable &&
136
- snapshot.sessionTypeUnavailableMessage?.trim() && (
137
- <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
163
+ {shouldShowProviderHint && (
164
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
138
165
  <span className="text-xs text-amber-800">
139
- {snapshot.sessionTypeUnavailableMessage}
166
+ {t("chatModelNoOptions")}
140
167
  </span>
168
+ <button
169
+ type="button"
170
+ onClick={presenter.chatThreadManager.goToProviders}
171
+ className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
172
+ >
173
+ {t("chatGoConfigureProvider")}
174
+ </button>
141
175
  </div>
142
176
  )}
143
177
 
144
- <div
145
- ref={threadRef}
146
- onScroll={handleScroll}
147
- className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
148
- >
149
- {showWelcome ? (
150
- <ChatWelcome
151
- onCreateSession={presenter.chatThreadManager.createSession}
152
- />
153
- ) : hideEmptyHint ? (
154
- <div className="h-full" />
155
- ) : snapshot.messages.length === 0 ? (
156
- <div className="px-5 py-5 text-sm text-gray-500">
157
- {t("chatNoMessages")}
158
- </div>
159
- ) : (
160
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
161
- <ChatMessageListContainer
162
- key={snapshot.sessionKey ?? "draft"}
163
- messages={snapshot.messages}
164
- isSending={
165
- snapshot.isSending && snapshot.isAwaitingAssistantOutput
166
- }
178
+ {snapshot.sessionTypeUnavailable &&
179
+ snapshot.sessionTypeUnavailableMessage?.trim() && (
180
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
181
+ <span className="text-xs text-amber-800">
182
+ {snapshot.sessionTypeUnavailableMessage}
183
+ </span>
184
+ </div>
185
+ )}
186
+
187
+ <div
188
+ ref={threadRef}
189
+ onScroll={handleScroll}
190
+ className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
191
+ >
192
+ {showWelcome ? (
193
+ <ChatWelcome
194
+ onCreateSession={presenter.chatThreadManager.createSession}
195
+ agents={snapshot.availableAgents ?? []}
196
+ selectedAgentId={snapshot.agentId ?? "main"}
197
+ onSelectAgent={presenter.chatSessionListManager.setSelectedAgentId}
167
198
  />
168
- </div>
169
- )}
199
+ ) : hideEmptyHint ? (
200
+ <div className="h-full" />
201
+ ) : snapshot.messages.length === 0 ? (
202
+ <div className="px-5 py-5 text-sm text-gray-500">
203
+ {t("chatNoMessages")}
204
+ </div>
205
+ ) : (
206
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
207
+ <ChatMessageListContainer
208
+ key={snapshot.sessionKey ?? "draft"}
209
+ messages={snapshot.messages}
210
+ isSending={
211
+ snapshot.isSending && snapshot.isAwaitingAssistantOutput
212
+ }
213
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
214
+ />
215
+ </div>
216
+ )}
217
+ </div>
218
+
219
+ <ChatInputBarContainer />
170
220
  </div>
171
221
 
172
- <ChatInputBarContainer />
222
+ {detailSessionKey ? (
223
+ <ChatChildSessionPanel
224
+ tabs={childSessionTabs}
225
+ activeSessionKey={detailSessionKey}
226
+ onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
227
+ onClose={presenter.chatThreadManager.closeChildSessionDetail}
228
+ onBackToParent={presenter.chatThreadManager.goToParentSession}
229
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
230
+ />
231
+ ) : null}
173
232
  </section>
174
233
  );
175
234
  }
@@ -71,6 +71,14 @@ vi.mock('@/components/common/StatusBadge', () => ({
71
71
  StatusBadge: () => <div data-testid="status-badge" />
72
72
  }));
73
73
 
74
+ vi.mock('@/hooks/agents/useAgents', () => ({
75
+ useAgents: () => ({
76
+ data: {
77
+ agents: []
78
+ }
79
+ })
80
+ }));
81
+
74
82
  vi.mock('@/components/providers/I18nProvider', () => ({
75
83
  useI18n: () => ({
76
84
  language: 'en',
@@ -13,6 +13,7 @@ import { useNcpSessionListView, type NcpSessionListItemView } from '@/components
13
13
  import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
14
14
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
15
15
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
16
+ import { useAgents } from '@/hooks/agents/useAgents';
16
17
  import { cn } from '@/lib/utils';
17
18
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
18
19
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
@@ -23,6 +24,7 @@ import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/comp
23
24
  import { useUiStore } from '@/stores/ui.store';
24
25
  import {
25
26
  AlarmClock,
27
+ Bot,
26
28
  BookOpen,
27
29
  BrainCircuit,
28
30
  ChevronDown,
@@ -93,6 +95,7 @@ function resolveSessionTypeStatusText(option: {
93
95
  const navItems = [
94
96
  { target: '/cron', label: () => t('chatSidebarScheduledTasks'), icon: AlarmClock },
95
97
  { target: '/skills', label: () => t('chatSidebarSkills'), icon: BrainCircuit },
98
+ { target: '/agents', label: () => t('agentsPageTitle'), icon: Bot },
96
99
  ];
97
100
 
98
101
  export function ChatSidebar() {
@@ -105,12 +108,17 @@ export function ChatSidebar() {
105
108
  const inputSnapshot = useChatInputStore((state) => state.snapshot);
106
109
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
107
110
  const connectionStatus = useUiStore((state) => state.connectionStatus);
111
+ const agentsQuery = useAgents();
108
112
  const { isLoading, items } = useNcpSessionListView();
109
113
  const { language, setLanguage } = useI18n();
110
114
  const { theme, setTheme } = useTheme();
111
115
  const updateSessionLabel = useChatSessionLabel();
112
116
  const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
113
117
  const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
118
+ const agentsById = useMemo(
119
+ () => new Map((agentsQuery.data?.agents ?? []).map((agent) => [agent.id, agent])),
120
+ [agentsQuery.data?.agents]
121
+ );
114
122
 
115
123
  const groups = useMemo(() => groupSessionsByDate(items), [items]);
116
124
  const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
@@ -293,6 +301,9 @@ export function ChatSidebar() {
293
301
  runStatus={runStatus}
294
302
  context={context}
295
303
  title={sessionTitle(session)}
304
+ agentId={session.agentId ?? null}
305
+ agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
306
+ agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
296
307
  isEditing={isEditing}
297
308
  draftLabel={draftLabel}
298
309
  isSaving={isSaving}
@@ -0,0 +1,25 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { ChatWelcome } from '@/components/chat/ChatWelcome';
4
+
5
+ describe('ChatWelcome', () => {
6
+ it('renders draft agent choices and allows switching', () => {
7
+ const onCreateSession = vi.fn();
8
+ const onSelectAgent = vi.fn();
9
+
10
+ render(
11
+ <ChatWelcome
12
+ onCreateSession={onCreateSession}
13
+ agents={[
14
+ { id: 'main', displayName: 'Main' },
15
+ { id: 'engineer', displayName: 'Engineer' }
16
+ ]}
17
+ selectedAgentId="main"
18
+ onSelectAgent={onSelectAgent}
19
+ />
20
+ );
21
+
22
+ fireEvent.click(screen.getByText('Engineer'));
23
+ expect(onSelectAgent).toHaveBeenCalledWith('engineer');
24
+ });
25
+ });
@@ -1,8 +1,13 @@
1
+ import type { AgentProfileView } from '@/api/types';
2
+ import { AgentAvatar } from '@/components/common/AgentAvatar';
1
3
  import { t } from '@/lib/i18n';
2
4
  import { Bot, BrainCircuit, AlarmClock, MessageCircle } from 'lucide-react';
3
5
 
4
6
  type ChatWelcomeProps = {
5
7
  onCreateSession: () => void;
8
+ agents: AgentProfileView[];
9
+ selectedAgentId: string;
10
+ onSelectAgent: (agentId: string) => void;
6
11
  };
7
12
 
8
13
  const capabilities = [
@@ -23,7 +28,9 @@ const capabilities = [
23
28
  },
24
29
  ];
25
30
 
26
- export function ChatWelcome({ onCreateSession }: ChatWelcomeProps) {
31
+ export function ChatWelcome({ onCreateSession, agents, selectedAgentId, onSelectAgent }: ChatWelcomeProps) {
32
+ const selectedAgent = agents.find((agent) => agent.id === selectedAgentId) ?? null;
33
+
27
34
  return (
28
35
  <div className="h-full flex items-center justify-center p-8">
29
36
  <div className="max-w-lg w-full text-center">
@@ -36,6 +43,45 @@ export function ChatWelcome({ onCreateSession }: ChatWelcomeProps) {
36
43
  <h2 className="text-xl font-semibold text-gray-900 mb-2">{t('chatWelcomeTitle')}</h2>
37
44
  <p className="text-sm text-gray-500 mb-8">{t('chatWelcomeSubtitle')}</p>
38
45
 
46
+ <div className="mb-8 rounded-2xl border border-gray-200 bg-white/90 p-4 text-left shadow-card">
47
+ <div className="text-sm font-semibold text-gray-900">{t('chatDraftAgentTitle')}</div>
48
+ <p className="mt-1 text-xs text-gray-500">{t('chatDraftAgentDescription')}</p>
49
+ <div className="mt-4 flex flex-wrap gap-2">
50
+ {agents.map((agent) => {
51
+ const active = agent.id === selectedAgentId;
52
+ return (
53
+ <button
54
+ key={agent.id}
55
+ type="button"
56
+ onClick={() => onSelectAgent(agent.id)}
57
+ className={[
58
+ 'inline-flex items-center gap-2 rounded-full border px-3 py-2 text-left transition-colors',
59
+ active
60
+ ? 'border-gray-900 bg-gray-900 text-white'
61
+ : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
62
+ ].join(' ')}
63
+ >
64
+ <AgentAvatar
65
+ agentId={agent.id}
66
+ displayName={agent.displayName}
67
+ avatarUrl={agent.avatarUrl}
68
+ className="h-6 w-6"
69
+ />
70
+ <span className="text-xs font-medium">
71
+ {agent.displayName?.trim() || agent.id}
72
+ </span>
73
+ </button>
74
+ );
75
+ })}
76
+ </div>
77
+ {selectedAgent ? (
78
+ <div className="mt-4 flex items-center gap-2 text-xs text-gray-500">
79
+ <span>{t('chatDraftAgentCurrent')}:</span>
80
+ <span className="font-medium text-gray-700">{selectedAgent.displayName?.trim() || selectedAgent.id}</span>
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+
39
85
  {/* Capability cards */}
40
86
  <div className="grid grid-cols-3 gap-3">
41
87
  {capabilities.map((cap) => {
@@ -8,8 +8,9 @@ import {
8
8
  buildRenderableText,
9
9
  buildTextPart,
10
10
  } from "@/components/chat/adapters/chat-message-inline-content.adapter";
11
+ import { resolveToolInvocationAgentId } from "@/components/chat/adapters/chat-message-tool-agent-id";
11
12
  import { buildFileOperationCardData } from "@/components/chat/adapters/file-operation/card";
12
- import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
13
+ import { buildSessionRequestToolCard } from "@/components/chat/adapters/chat-message.session-request-tool-card";
13
14
  import type {
14
15
  ChatMessagePartViewModel,
15
16
  ChatToolPartViewModel,
@@ -77,6 +78,8 @@ export type ChatMessagePartSource =
77
78
  type ToolCardViewSource = ToolCard & {
78
79
  statusTone: ChatToolPartViewModel["statusTone"];
79
80
  statusLabel: string;
81
+ agentId?: string;
82
+ action?: ChatToolPartViewModel["action"];
80
83
  fileOperation?: ChatToolPartViewModel["fileOperation"];
81
84
  outputData?: unknown;
82
85
  };
@@ -179,6 +182,7 @@ function buildToolCard(
179
182
  return {
180
183
  kind: toolCard.kind,
181
184
  toolName: toolCard.name,
185
+ ...('agentId' in toolCard && toolCard.agentId ? { agentId: toolCard.agentId } : {}),
182
186
  summary: toolCard.detail,
183
187
  inputLabel: texts.toolInputLabel,
184
188
  input:
@@ -194,6 +198,9 @@ function buildToolCard(
194
198
  toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
195
199
  outputLabel: texts.toolOutputLabel,
196
200
  emptyLabel: texts.toolNoOutputLabel,
201
+ ...("action" in toolCard && toolCard.action
202
+ ? { action: toolCard.action }
203
+ : {}),
197
204
  ...("fileOperation" in toolCard && toolCard.fileOperation
198
205
  ? { fileOperation: toolCard.fileOperation }
199
206
  : {}),
@@ -330,14 +337,18 @@ function buildToolInvocationPart(
330
337
  return assetFileView;
331
338
  }
332
339
 
333
- const subagentToolCard = buildSubagentToolCard({
340
+ const sessionRequestToolCard = buildSessionRequestToolCard({
334
341
  invocation,
335
- texts,
342
+ texts: {
343
+ toolStatusRunningLabel: texts.toolStatusRunningLabel,
344
+ toolStatusCompletedLabel: texts.toolStatusCompletedLabel,
345
+ toolStatusFailedLabel: texts.toolStatusFailedLabel,
346
+ },
336
347
  });
337
- if (subagentToolCard) {
348
+ if (sessionRequestToolCard) {
338
349
  return {
339
350
  type: "tool-card",
340
- card: buildToolCard(subagentToolCard, texts),
351
+ card: buildToolCard(sessionRequestToolCard, texts),
341
352
  };
342
353
  }
343
354
 
@@ -373,9 +384,11 @@ function buildToolInvocationPart(
373
384
  const shouldShowRawResult =
374
385
  (!fileOperationCardData?.fileOperation || Boolean(invocation.error)) &&
375
386
  !shouldHideStructuredTerminalJson;
387
+ const agentId = resolveToolInvocationAgentId(invocation);
376
388
  const card: ToolCardViewSource = {
377
389
  kind: statusView.kind,
378
390
  name: invocation.toolName,
391
+ ...(agentId ? { agentId } : {}),
379
392
  detail,
380
393
  ...(input ? { input } : {}),
381
394
  text: shouldShowRawResult && rawResult ? rawResult : undefined,
@@ -0,0 +1,102 @@
1
+ import { ToolInvocationStatus } from "@nextclaw/agent-chat";
2
+ import { adaptChatMessages, type ChatMessageSource } from "@/components/chat/adapters/chat-message.adapter";
3
+
4
+ const defaultTexts = {
5
+ roleLabels: {
6
+ user: "You",
7
+ assistant: "Assistant",
8
+ tool: "Tool",
9
+ system: "System",
10
+ fallback: "Message",
11
+ },
12
+ reasoningLabel: "Reasoning",
13
+ toolCallLabel: "Tool Call",
14
+ toolResultLabel: "Tool Result",
15
+ toolInputLabel: "Input",
16
+ toolNoOutputLabel: "No output",
17
+ toolOutputLabel: "Output",
18
+ toolStatusPreparingLabel: "Preparing",
19
+ toolStatusRunningLabel: "Running",
20
+ toolStatusCompletedLabel: "Completed",
21
+ toolStatusFailedLabel: "Failed",
22
+ toolStatusCancelledLabel: "Cancelled",
23
+ imageAttachmentLabel: "Image attachment",
24
+ fileAttachmentLabel: "File attachment",
25
+ unknownPartLabel: "Unknown Part",
26
+ };
27
+
28
+ function adapt(uiMessages: ChatMessageSource[]) {
29
+ return adaptChatMessages({
30
+ uiMessages,
31
+ formatTimestamp: (value) => `formatted:${value}`,
32
+ texts: defaultTexts,
33
+ });
34
+ }
35
+
36
+ it("exposes agentId on spawn call cards when the invocation args include it", () => {
37
+ const adapted = adapt([
38
+ {
39
+ id: "assistant-spawn-call",
40
+ role: "assistant",
41
+ parts: [
42
+ {
43
+ type: "tool-invocation",
44
+ toolInvocation: {
45
+ status: ToolInvocationStatus.PARTIAL_CALL,
46
+ toolCallId: "spawn-call-args-1",
47
+ toolName: "spawn",
48
+ args: '{"agentId":"planner-agent","label":"Planner","task":"Plan the rollout"}',
49
+ result: {
50
+ kind: "nextclaw.session_request",
51
+ requestId: "request-3",
52
+ sessionId: "child-session-3",
53
+ isChildSession: true,
54
+ title: "Planner",
55
+ task: "Plan the rollout",
56
+ status: "running",
57
+ },
58
+ },
59
+ },
60
+ ],
61
+ },
62
+ ] as unknown as ChatMessageSource[]);
63
+
64
+ expect(adapted[0]?.parts[0]).toMatchObject({
65
+ type: "tool-card",
66
+ card: {
67
+ toolName: "spawn",
68
+ agentId: "planner-agent",
69
+ statusTone: "running",
70
+ },
71
+ });
72
+ });
73
+
74
+ it("exposes agentId on running tool call cards even before a session-request result exists", () => {
75
+ const adapted = adapt([
76
+ {
77
+ id: "assistant-spawn-call-running",
78
+ role: "assistant",
79
+ parts: [
80
+ {
81
+ type: "tool-invocation",
82
+ toolInvocation: {
83
+ status: ToolInvocationStatus.PARTIAL_CALL,
84
+ toolCallId: "spawn-call-running-1",
85
+ toolName: "spawn",
86
+ args: '{"agentId":"planner-agent","task":"Plan the rollout"}',
87
+ },
88
+ },
89
+ ],
90
+ },
91
+ ] as unknown as ChatMessageSource[]);
92
+
93
+ expect(adapted[0]?.parts[0]).toMatchObject({
94
+ type: "tool-card",
95
+ card: {
96
+ toolName: "spawn",
97
+ agentId: "planner-agent",
98
+ statusTone: "running",
99
+ titleLabel: "Tool Call",
100
+ },
101
+ });
102
+ });
@@ -0,0 +1,47 @@
1
+ type ToolInvocationAgentIdSource = {
2
+ args?: unknown;
3
+ parsedArgs?: unknown;
4
+ result?: unknown;
5
+ };
6
+
7
+ function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return typeof value === "object" && value !== null;
9
+ }
10
+
11
+ function parseStructuredValue(value: unknown): unknown {
12
+ if (typeof value !== "string") {
13
+ return value;
14
+ }
15
+ const trimmed = value.trim();
16
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
17
+ return value;
18
+ }
19
+ try {
20
+ return JSON.parse(trimmed) as unknown;
21
+ } catch {
22
+ return value;
23
+ }
24
+ }
25
+
26
+ function readOptionalString(value: unknown): string | null {
27
+ if (typeof value !== "string") {
28
+ return null;
29
+ }
30
+ const trimmed = value.trim();
31
+ return trimmed.length > 0 ? trimmed : null;
32
+ }
33
+
34
+ function readAgentIdFromValue(value: unknown): string | null {
35
+ const parsedValue = parseStructuredValue(value);
36
+ return isRecord(parsedValue) ? readOptionalString(parsedValue.agentId) : null;
37
+ }
38
+
39
+ export function resolveToolInvocationAgentId(
40
+ source: ToolInvocationAgentIdSource,
41
+ ): string | null {
42
+ return (
43
+ readAgentIdFromValue(source.parsedArgs) ??
44
+ readAgentIdFromValue(source.args) ??
45
+ readAgentIdFromValue(source.result)
46
+ );
47
+ }