@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.
- package/CHANGELOG.md +30 -0
- package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-NKNKsf1J.js} +1 -1
- package/dist/assets/ChatPage-p23OnnEI.js +43 -0
- package/dist/assets/DocBrowser-C8b2uPgL.js +1 -0
- package/dist/assets/{DocBrowser-BmtBLFU0.js → DocBrowser-DxdSujSc.js} +1 -1
- package/dist/assets/{DocBrowserContext-YIKkPb76.js → DocBrowserContext-CQ-8jMha.js} +1 -1
- package/dist/assets/{LogoBadge-F7ZWdxLT.js → LogoBadge-D-KQIN4U.js} +1 -1
- package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-CRNvxtvx.js} +2 -2
- package/dist/assets/MarketplacePage-GGkEXowp.js +1 -0
- package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-Cu7GmCcc.js} +2 -2
- package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-CEpx9fro.js} +1 -1
- package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-BWbUb7-2.js} +1 -1
- package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-NsawrZb0.js} +1 -1
- package/dist/assets/RuntimeConfig-BJHBsVTd.js +1 -0
- package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-BsaX_WYy.js} +1 -1
- package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-CgDZOd3w.js} +1 -1
- package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-Dd-KM7F7.js} +2 -2
- package/dist/assets/{book-open-CXoF5nQC.js → book-open-FnK2xCQd.js} +1 -1
- package/dist/assets/chat-session-display-BD_AN71I.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CvRWvTy5.js → chunk-JZWAC4HX-B5l0hr_u.js} +1 -1
- package/dist/assets/{config-DJswxxE8.js → config-JKmXfZ3q.js} +1 -1
- package/dist/assets/{createLucideIcon-CjGHOWb6.js → createLucideIcon-o1WWhwhd.js} +1 -1
- package/dist/assets/{dist-nqTTbVdA.js → dist-C_moWYv7.js} +1 -1
- package/dist/assets/{dist-Cl2QB-2y.js → dist-DazA6Wd_.js} +1 -1
- package/dist/assets/{external-link-tIO7zING.js → external-link-BKje3SiD.js} +1 -1
- package/dist/assets/{hash-JWUyl1pT.js → hash-DfW4DT8O.js} +1 -1
- package/dist/assets/i18n-BK1w-oBy.js +1 -0
- package/dist/assets/index-BZaB1TqM.js +6 -0
- package/dist/assets/index-DaR9igPC.css +1 -0
- package/dist/assets/{label-BIpeNu4r.js → label-BzDWmdOe.js} +1 -1
- package/dist/assets/loader-circle-DdZPxBUz.js +1 -0
- package/dist/assets/{logos-DThdM9lk.js → logos-CTLlde_T.js} +1 -1
- package/dist/assets/{page-layout-D3Xo605Z.js → page-layout-BagR3t59.js} +1 -1
- package/dist/assets/plus-DP2PSCPO.js +1 -0
- package/dist/assets/{popover-BJRUGA_H.js → popover-5DWhNfd4.js} +1 -1
- package/dist/assets/{provider-models-bz5y28rq.js → provider-models-DJ29qHuA.js} +1 -1
- package/dist/assets/{react-7ZHqQtEV.js → react-C3yu5yge.js} +1 -1
- package/dist/assets/{refresh-ccw-CC6-_QuL.js → refresh-ccw-BAJf-h7w.js} +1 -1
- package/dist/assets/{save-DJM5RRWW.js → save-aa6z4GJL.js} +1 -1
- package/dist/assets/search-pD6ZwQYF.js +1 -0
- package/dist/assets/{security-config-T5zpg16O.js → security-config-DRDxrApx.js} +1 -1
- package/dist/assets/{select-DSkTc61S.js → select-BHJPiJWt.js} +1 -1
- package/dist/assets/skeleton-D6kCk9Y6.js +1 -0
- package/dist/assets/{status-dot-LNBlDu3q.js → status-dot-DUwsTIdv.js} +1 -1
- package/dist/assets/{switch-Bo-Y46HZ.js → switch-B6nCfcOB.js} +1 -1
- package/dist/assets/{tabs-custom-DXv507_2.js → tabs-custom-B57SMElx.js} +1 -1
- package/dist/assets/{trash-2-DFZmW6Gg.js → trash-2-CrjYH5ok.js} +1 -1
- package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-DsxnXB1B.js} +1 -1
- package/dist/assets/{useMutation-DrZrOgVL.js → useMutation-oTTWXgLG.js} +1 -1
- package/dist/assets/x-CTIQHUuD.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +5 -5
- package/src/App.tsx +2 -0
- package/src/api/agents.ts +26 -0
- package/src/api/types.ts +27 -2
- package/src/components/agents/AgentsPage.test.tsx +70 -0
- package/src/components/agents/AgentsPage.tsx +353 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +144 -8
- package/src/components/chat/ChatConversationPanel.tsx +136 -77
- package/src/components/chat/ChatSidebar.test.tsx +8 -0
- package/src/components/chat/ChatSidebar.tsx +11 -0
- package/src/components/chat/ChatWelcome.test.tsx +25 -0
- package/src/components/chat/ChatWelcome.tsx +47 -1
- package/src/components/chat/adapters/chat-message-part.adapter.ts +18 -5
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +102 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.ts +47 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +89 -9
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +200 -0
- package/src/components/chat/chat-child-session-panel.tsx +166 -0
- package/src/components/chat/chat-page-runtime.test.ts +1 -0
- package/src/components/chat/chat-page-shell.tsx +8 -17
- package/src/components/chat/chat-session-display.test.ts +1 -0
- package/src/components/chat/chat-session-route.ts +0 -14
- package/src/components/chat/chat-sidebar-session-item.tsx +16 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +2 -1
- package/src/components/chat/containers/chat-message-list.container.tsx +11 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +153 -190
- package/src/components/chat/ncp/README.md +3 -0
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +2 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +106 -1
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +23 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +32 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +128 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +52 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +101 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +72 -0
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +5 -1
- package/src/components/chat/stores/chat-thread.store.ts +25 -1
- package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
- package/src/components/common/AgentAvatar.tsx +63 -0
- package/src/components/common/agent-identity/agent-identity-avatar.tsx +27 -0
- package/src/components/common/agent-identity/index.ts +3 -0
- package/src/components/common/agent-identity/use-agent-identity.ts +50 -0
- package/src/components/config/RuntimeConfig.tsx +13 -79
- package/src/components/config/runtime-config-agent.utils.ts +95 -0
- package/src/components/layout/AppLayout.tsx +3 -1
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/app-layout.test.tsx +30 -0
- package/src/components/ui/tabs.tsx +2 -0
- package/src/hooks/README.md +3 -0
- package/src/hooks/agents/useAgents.ts +44 -0
- package/src/lib/i18n.agents.ts +66 -0
- package/src/lib/i18n.chat.ts +5 -0
- package/src/lib/i18n.ts +4 -4
- package/src/lib/ui-document-title.ts +1 -0
- package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
- package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
- package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
- package/dist/assets/RuntimeConfig-Cmn2xPQO.js +0 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
- package/dist/assets/i18n-CDHMXlRZ.js +0 -1
- package/dist/assets/index-BlH4-cBw.css +0 -1
- package/dist/assets/index-C6d0xmtm.js +0 -6
- package/dist/assets/loader-circle-Cs8XVFTw.js +0 -1
- package/dist/assets/plus-PHf8q-Ct.js +0 -1
- package/dist/assets/search-C91yH_6y.js +0 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +0 -1
- package/dist/assets/x-D7Q1yqSF.js +0 -1
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
- /package/src/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
- /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
|
|
83
|
-
<div
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 (
|
|
348
|
+
if (sessionRequestToolCard) {
|
|
338
349
|
return {
|
|
339
350
|
type: "tool-card",
|
|
340
|
-
card: buildToolCard(
|
|
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
|
+
}
|