@nextclaw/ui 0.11.23 → 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 +20 -0
- package/dist/assets/{ChannelsList-DVDu1xvz.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-Buo9HrOz.js → MarketplacePage-CRNvxtvx.js} +2 -2
- package/dist/assets/MarketplacePage-GGkEXowp.js +1 -0
- package/dist/assets/{McpMarketplacePage-JnkYwK7p.js → McpMarketplacePage-Cu7GmCcc.js} +2 -2
- package/dist/assets/{ModelConfig-BYRhgp0c.js → ModelConfig-CEpx9fro.js} +1 -1
- package/dist/assets/{ProvidersList-DmLyyHvX.js → ProvidersList-BWbUb7-2.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CDSSvH7Z.js → RemoteAccessPage-NsawrZb0.js} +1 -1
- package/dist/assets/RuntimeConfig-BJHBsVTd.js +1 -0
- package/dist/assets/{SearchConfig-D5f1EkLE.js → SearchConfig-BsaX_WYy.js} +1 -1
- package/dist/assets/{SecretsConfig-D61IKcYt.js → SecretsConfig-CgDZOd3w.js} +1 -1
- package/dist/assets/{SessionsConfig-BRIxVTEv.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-DbUyWcQz.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-COwYXDKm.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 +6 -6
- package/src/App.tsx +2 -0
- package/src/api/agents.ts +26 -0
- package/src/api/types.ts +23 -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 +141 -13
- package/src/components/chat/ChatConversationPanel.tsx +29 -7
- 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 +5 -0
- 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 +6 -0
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +24 -15
- package/src/components/chat/chat-child-session-panel.tsx +115 -49
- package/src/components/chat/chat-page-shell.tsx +8 -17
- 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 +7 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +58 -160
- 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 +66 -10
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +1 -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/presenter/chat-presenter-context.tsx +1 -0
- package/src/components/chat/stores/chat-thread.store.ts +20 -6
- 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-Z9tRzm_n.js +0 -43
- package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +0 -1
- package/dist/assets/RuntimeConfig-v7a7Fe3x.js +0 -1
- package/dist/assets/chat-session-display-D0WpnuRZ.js +0 -1
- package/dist/assets/i18n-CDHMXlRZ.js +0 -1
- package/dist/assets/index-BuwbBgmT.js +0 -6
- package/dist/assets/index-bZ8cqQIS.css +0 -1
- 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/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
- /package/src/lib/{i18n → i18n-runtime}/i18n.path-picker.ts +0 -0
|
@@ -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,6 +8,7 @@ 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
13
|
import { buildSessionRequestToolCard } from "@/components/chat/adapters/chat-message.session-request-tool-card";
|
|
13
14
|
import type {
|
|
@@ -77,6 +78,7 @@ export type ChatMessagePartSource =
|
|
|
77
78
|
type ToolCardViewSource = ToolCard & {
|
|
78
79
|
statusTone: ChatToolPartViewModel["statusTone"];
|
|
79
80
|
statusLabel: string;
|
|
81
|
+
agentId?: string;
|
|
80
82
|
action?: ChatToolPartViewModel["action"];
|
|
81
83
|
fileOperation?: ChatToolPartViewModel["fileOperation"];
|
|
82
84
|
outputData?: unknown;
|
|
@@ -180,6 +182,7 @@ function buildToolCard(
|
|
|
180
182
|
return {
|
|
181
183
|
kind: toolCard.kind,
|
|
182
184
|
toolName: toolCard.name,
|
|
185
|
+
...('agentId' in toolCard && toolCard.agentId ? { agentId: toolCard.agentId } : {}),
|
|
183
186
|
summary: toolCard.detail,
|
|
184
187
|
inputLabel: texts.toolInputLabel,
|
|
185
188
|
input:
|
|
@@ -381,9 +384,11 @@ function buildToolInvocationPart(
|
|
|
381
384
|
const shouldShowRawResult =
|
|
382
385
|
(!fileOperationCardData?.fileOperation || Boolean(invocation.error)) &&
|
|
383
386
|
!shouldHideStructuredTerminalJson;
|
|
387
|
+
const agentId = resolveToolInvocationAgentId(invocation);
|
|
384
388
|
const card: ToolCardViewSource = {
|
|
385
389
|
kind: statusView.kind,
|
|
386
390
|
name: invocation.toolName,
|
|
391
|
+
...(agentId ? { agentId } : {}),
|
|
387
392
|
detail,
|
|
388
393
|
...(input ? { input } : {}),
|
|
389
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
|
+
}
|
|
@@ -240,6 +240,7 @@ it("renders session request tool cards from structured child-session status upda
|
|
|
240
240
|
kind: "nextclaw.session_request",
|
|
241
241
|
requestId: "request-1",
|
|
242
242
|
sessionId: "child-session-1",
|
|
243
|
+
agentId: "verifier-agent",
|
|
243
244
|
isChildSession: true,
|
|
244
245
|
title: "Verifier",
|
|
245
246
|
task: "Verify 1+1=2",
|
|
@@ -257,6 +258,7 @@ it("renders session request tool cards from structured child-session status upda
|
|
|
257
258
|
type: "tool-card",
|
|
258
259
|
card: {
|
|
259
260
|
toolName: "spawn",
|
|
261
|
+
agentId: "verifier-agent",
|
|
260
262
|
summary: "title: Verifier · session: child-session-1 · task: Verify 1+1=2",
|
|
261
263
|
output: [
|
|
262
264
|
"Request ID: request-1",
|
|
@@ -280,6 +282,7 @@ it("renders session request tool cards from structured child-session status upda
|
|
|
280
282
|
kind: "open-session",
|
|
281
283
|
sessionId: "child-session-1",
|
|
282
284
|
sessionKind: "child",
|
|
285
|
+
agentId: "verifier-agent",
|
|
283
286
|
label: "Verifier",
|
|
284
287
|
parentSessionId: "parent-session-1",
|
|
285
288
|
},
|
|
@@ -304,6 +307,7 @@ it("renders regular session request tool cards with session navigation instead o
|
|
|
304
307
|
kind: "nextclaw.session_request",
|
|
305
308
|
requestId: "request-2",
|
|
306
309
|
sessionId: "session-2",
|
|
310
|
+
agentId: "research-agent",
|
|
307
311
|
isChildSession: false,
|
|
308
312
|
title: "Research thread",
|
|
309
313
|
task: "Summarize the latest findings",
|
|
@@ -320,6 +324,7 @@ it("renders regular session request tool cards with session navigation instead o
|
|
|
320
324
|
type: "tool-card",
|
|
321
325
|
card: {
|
|
322
326
|
toolName: "sessions_request",
|
|
327
|
+
agentId: "research-agent",
|
|
323
328
|
summary: "title: Research thread · session: session-2 · task: Summarize the latest findings",
|
|
324
329
|
output: [
|
|
325
330
|
"Request ID: request-2",
|
|
@@ -341,6 +346,7 @@ it("renders regular session request tool cards with session navigation instead o
|
|
|
341
346
|
kind: "open-session",
|
|
342
347
|
sessionId: "session-2",
|
|
343
348
|
sessionKind: "session",
|
|
349
|
+
agentId: "research-agent",
|
|
344
350
|
label: "Research thread",
|
|
345
351
|
},
|
|
346
352
|
},
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
summarizeToolArgs,
|
|
4
4
|
type ToolCard,
|
|
5
5
|
} from "@/lib/chat-message";
|
|
6
|
+
import { resolveToolInvocationAgentId } from "@/components/chat/adapters/chat-message-tool-agent-id";
|
|
6
7
|
import type { ChatToolPartViewModel } from "@nextclaw/agent-chat-ui";
|
|
7
8
|
|
|
8
9
|
type ToolCardViewSource = ToolCard & {
|
|
@@ -28,6 +29,7 @@ type SessionRequestResult = {
|
|
|
28
29
|
kind: string;
|
|
29
30
|
requestId?: string;
|
|
30
31
|
sessionId?: string;
|
|
32
|
+
agentId?: string;
|
|
31
33
|
isChildSession?: boolean;
|
|
32
34
|
title?: string;
|
|
33
35
|
task?: string;
|
|
@@ -118,28 +120,32 @@ export function buildSessionRequestToolCard(params: {
|
|
|
118
120
|
invocation: SessionRequestInvocation;
|
|
119
121
|
texts: SessionRequestToolCardTexts;
|
|
120
122
|
}): ToolCardViewSource | null {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
) {
|
|
123
|
+
const { invocation, texts } = params;
|
|
124
|
+
const { toolName, toolCallId, args, result } = invocation;
|
|
125
|
+
|
|
126
|
+
if (toolName !== "spawn" && toolName !== "sessions_request") {
|
|
125
127
|
return null;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
const sessionRequest = readSessionRequestResult(
|
|
130
|
+
const sessionRequest = readSessionRequestResult(result);
|
|
129
131
|
if (!sessionRequest) {
|
|
130
132
|
return null;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
const normalizedStatus = readOptionalString(sessionRequest.status)?.toLowerCase();
|
|
134
|
-
const detail = buildSessionRequestDetail(sessionRequest,
|
|
136
|
+
const detail = buildSessionRequestDetail(sessionRequest, args);
|
|
135
137
|
const output = buildSessionRequestOutput(sessionRequest);
|
|
136
138
|
const targetSessionId = readOptionalString(sessionRequest.sessionId);
|
|
139
|
+
const agentId = resolveToolInvocationAgentId({ args, result: sessionRequest });
|
|
137
140
|
const action =
|
|
138
141
|
targetSessionId
|
|
139
142
|
? {
|
|
140
143
|
kind: "open-session" as const,
|
|
141
144
|
sessionId: targetSessionId,
|
|
142
145
|
sessionKind: sessionRequest.isChildSession === true ? ("child" as const) : ("session" as const),
|
|
146
|
+
...(agentId
|
|
147
|
+
? { agentId }
|
|
148
|
+
: {}),
|
|
143
149
|
...(readOptionalString(sessionRequest.title)
|
|
144
150
|
? { label: sessionRequest.title!.trim() }
|
|
145
151
|
: {}),
|
|
@@ -152,13 +158,14 @@ export function buildSessionRequestToolCard(params: {
|
|
|
152
158
|
if (normalizedStatus === "failed") {
|
|
153
159
|
return {
|
|
154
160
|
kind: "result",
|
|
155
|
-
name:
|
|
161
|
+
name: toolName,
|
|
156
162
|
detail,
|
|
157
163
|
text: output,
|
|
158
|
-
callId:
|
|
164
|
+
callId: toolCallId || undefined,
|
|
159
165
|
hasResult: Boolean(output),
|
|
160
166
|
statusTone: "error",
|
|
161
|
-
statusLabel:
|
|
167
|
+
statusLabel: texts.toolStatusFailedLabel,
|
|
168
|
+
...(agentId ? { agentId } : {}),
|
|
162
169
|
...(action ? { action } : {}),
|
|
163
170
|
};
|
|
164
171
|
}
|
|
@@ -166,26 +173,28 @@ export function buildSessionRequestToolCard(params: {
|
|
|
166
173
|
if (normalizedStatus === "completed") {
|
|
167
174
|
return {
|
|
168
175
|
kind: "result",
|
|
169
|
-
name:
|
|
176
|
+
name: toolName,
|
|
170
177
|
detail,
|
|
171
178
|
text: output,
|
|
172
|
-
callId:
|
|
179
|
+
callId: toolCallId || undefined,
|
|
173
180
|
hasResult: Boolean(output),
|
|
174
181
|
statusTone: "success",
|
|
175
|
-
statusLabel:
|
|
182
|
+
statusLabel: texts.toolStatusCompletedLabel,
|
|
183
|
+
...(agentId ? { agentId } : {}),
|
|
176
184
|
...(action ? { action } : {}),
|
|
177
185
|
};
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
return {
|
|
181
189
|
kind: "result",
|
|
182
|
-
name:
|
|
190
|
+
name: toolName,
|
|
183
191
|
detail,
|
|
184
192
|
text: output,
|
|
185
|
-
callId:
|
|
193
|
+
callId: toolCallId || undefined,
|
|
186
194
|
hasResult: Boolean(output),
|
|
187
195
|
statusTone: "running",
|
|
188
|
-
statusLabel:
|
|
196
|
+
statusLabel: texts.toolStatusRunningLabel,
|
|
197
|
+
...(agentId ? { agentId } : {}),
|
|
189
198
|
...(action ? { action } : {}),
|
|
190
199
|
};
|
|
191
200
|
}
|