@nextclaw/ui 0.11.22 → 0.11.23
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 +10 -0
- package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
- package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
- package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
- package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
- package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
- package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
- package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
- package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
- package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
- package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
- package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
- package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
- package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
- package/dist/assets/index-bZ8cqQIS.css +1 -0
- package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
- package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/api/types.ts +4 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
- package/src/components/chat/ChatConversationPanel.tsx +114 -77
- package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
- package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
- package/src/components/chat/chat-child-session-panel.tsx +100 -0
- package/src/components/chat/chat-page-runtime.test.ts +1 -0
- package/src/components/chat/chat-session-display.test.ts +1 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
- package/src/components/chat/stores/chat-thread.store.ts +11 -1
- package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
- package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
- package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
- package/dist/assets/index-BlH4-cBw.css +0 -1
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { ArrowLeft, Loader2, X } from 'lucide-react';
|
|
4
|
+
import { fetchNcpSessionMessages } from '@/api/ncp-session';
|
|
5
|
+
import { ChatMessageListContainer } from '@/components/chat/containers/chat-message-list.container';
|
|
6
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
import type { ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
|
|
9
|
+
|
|
10
|
+
type ChatChildSessionPanelProps = {
|
|
11
|
+
sessionKey: string;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onBackToParent: () => void;
|
|
15
|
+
onToolAction?: (action: ChatToolActionViewModel) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function ChatChildSessionPanel({
|
|
19
|
+
sessionKey,
|
|
20
|
+
title,
|
|
21
|
+
onClose,
|
|
22
|
+
onBackToParent,
|
|
23
|
+
onToolAction,
|
|
24
|
+
}: ChatChildSessionPanelProps) {
|
|
25
|
+
const detailParentSessionKey = useChatThreadStore(
|
|
26
|
+
(state) => state.snapshot.childSessionDetailParentSessionKey,
|
|
27
|
+
);
|
|
28
|
+
const query = useQuery({
|
|
29
|
+
queryKey: ['ncp-session-messages', sessionKey, 'child-panel'],
|
|
30
|
+
queryFn: () => fetchNcpSessionMessages(sessionKey, 300),
|
|
31
|
+
staleTime: 5_000,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const messages = query.data?.messages ?? [];
|
|
35
|
+
const headerTitle = useMemo(() => title?.trim() || sessionKey, [sessionKey, title]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<aside className="hidden md:flex md:w-[24rem] lg:w-[28rem] shrink-0 border-l border-gray-200/70 bg-white/90 backdrop-blur-sm">
|
|
39
|
+
<div className="flex h-full min-h-0 w-full flex-col">
|
|
40
|
+
<div className="border-b border-gray-200/70 px-4 py-3">
|
|
41
|
+
<div className="flex items-center justify-between gap-3">
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onBackToParent}
|
|
45
|
+
className={cn(
|
|
46
|
+
'inline-flex items-center gap-1 text-xs font-medium text-gray-600 hover:text-gray-900',
|
|
47
|
+
!detailParentSessionKey && 'pointer-events-none opacity-0',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
51
|
+
<span>Back to parent</span>
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onClose}
|
|
56
|
+
className="rounded-md border border-gray-200 p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
|
57
|
+
aria-label="Close child session panel"
|
|
58
|
+
>
|
|
59
|
+
<X className="h-4 w-4" />
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="mt-2">
|
|
63
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
|
|
64
|
+
Child Session
|
|
65
|
+
</div>
|
|
66
|
+
<div className="mt-1 text-sm font-semibold text-gray-900">
|
|
67
|
+
{headerTitle}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="mt-1 text-[11px] text-gray-500">{sessionKey}</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
|
|
74
|
+
{query.isLoading ? (
|
|
75
|
+
<div className="flex h-full items-center justify-center text-sm text-gray-500">
|
|
76
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
77
|
+
Loading child session
|
|
78
|
+
</div>
|
|
79
|
+
) : query.isError ? (
|
|
80
|
+
<div className="px-4 py-5 text-sm text-rose-600">
|
|
81
|
+
{(query.error as Error).message}
|
|
82
|
+
</div>
|
|
83
|
+
) : messages.length === 0 ? (
|
|
84
|
+
<div className="px-4 py-5 text-sm text-gray-500">
|
|
85
|
+
No child session messages yet.
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<div className="px-4 py-5">
|
|
89
|
+
<ChatMessageListContainer
|
|
90
|
+
messages={messages}
|
|
91
|
+
isSending={false}
|
|
92
|
+
onToolAction={onToolAction}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</aside>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -29,6 +29,7 @@ function createSession(overrides: Partial<SessionEntryView> & Pick<SessionEntryV
|
|
|
29
29
|
updatedAt: overrides.updatedAt ?? '2026-03-19T00:00:00.000Z',
|
|
30
30
|
sessionType: overrides.sessionType ?? 'native',
|
|
31
31
|
sessionTypeMutable: overrides.sessionTypeMutable ?? false,
|
|
32
|
+
isChildSession: overrides.isChildSession ?? false,
|
|
32
33
|
messageCount: overrides.messageCount ?? 0,
|
|
33
34
|
...(overrides.label ? { label: overrides.label } : {}),
|
|
34
35
|
...(overrides.preferredModel ? { preferredModel: overrides.preferredModel } : {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { NcpMessage } from "@nextclaw/ncp";
|
|
3
3
|
import {
|
|
4
|
+
type ChatToolActionViewModel,
|
|
4
5
|
type ChatMessageViewModel,
|
|
5
6
|
ChatMessageList,
|
|
6
7
|
} from "@nextclaw/agent-chat-ui";
|
|
@@ -18,6 +19,7 @@ type ChatMessageListContainerProps = {
|
|
|
18
19
|
messages: readonly NcpMessage[];
|
|
19
20
|
isSending: boolean;
|
|
20
21
|
className?: string;
|
|
22
|
+
onToolAction?: (action: ChatToolActionViewModel) => void;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
const messageViewModelCache = new WeakMap<
|
|
@@ -69,6 +71,7 @@ export function ChatMessageListContainer({
|
|
|
69
71
|
messages: rawMessages,
|
|
70
72
|
isSending,
|
|
71
73
|
className,
|
|
74
|
+
onToolAction,
|
|
72
75
|
}: ChatMessageListContainerProps) {
|
|
73
76
|
const { language } = useI18n();
|
|
74
77
|
const texts = useMemo<ChatMessageAdapterTexts>(
|
|
@@ -125,6 +128,7 @@ export function ChatMessageListContainer({
|
|
|
125
128
|
hasAssistantDraft={hasAssistantDraft}
|
|
126
129
|
className={className}
|
|
127
130
|
texts={messageTexts}
|
|
131
|
+
onToolAction={onToolAction}
|
|
128
132
|
/>
|
|
129
133
|
);
|
|
130
134
|
}
|
|
@@ -1,30 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type MutableRefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
|
|
3
10
|
import {
|
|
4
11
|
buildNcpRequestEnvelope,
|
|
5
12
|
useHydratedNcpAgent,
|
|
6
|
-
type NcpConversationSeed
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import {
|
|
13
|
+
type NcpConversationSeed,
|
|
14
|
+
type NcpConversationSeedLoader,
|
|
15
|
+
} from "@nextclaw/ncp-react";
|
|
16
|
+
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|
17
|
+
import { API_BASE } from "@/api/api-base";
|
|
18
|
+
import { fetchNcpSessionMessages } from "@/api/ncp-session";
|
|
19
|
+
import {
|
|
20
|
+
ChatPageLayout,
|
|
21
|
+
type ChatPageProps,
|
|
22
|
+
useChatSessionSync,
|
|
23
|
+
} from "@/components/chat/chat-page-shell";
|
|
24
|
+
import { sessionDisplayName } from "@/components/chat/chat-session-display";
|
|
25
|
+
import {
|
|
26
|
+
buildInlineSkillTokensFromComposer,
|
|
27
|
+
CHAT_UI_INLINE_TOKENS_METADATA_KEY,
|
|
28
|
+
} from "@/components/chat/chat-inline-token.utils";
|
|
29
|
+
import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
|
|
30
|
+
import {
|
|
31
|
+
parseSessionKeyFromRoute,
|
|
32
|
+
resolveAgentIdFromSessionKey,
|
|
33
|
+
} from "@/components/chat/chat-session-route";
|
|
34
|
+
import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
|
|
35
|
+
import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
|
|
36
|
+
import {
|
|
37
|
+
adaptNcpSessionSummary,
|
|
38
|
+
createNcpSessionId,
|
|
39
|
+
} from "@/components/chat/ncp/ncp-session-adapter";
|
|
40
|
+
import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
|
|
41
|
+
import type { ResumeRunParams } from "@/components/chat/chat-stream/types";
|
|
42
|
+
import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
|
|
43
|
+
import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
|
|
44
|
+
import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
|
|
45
|
+
import { useConfirmDialog } from "@/hooks/useConfirmDialog";
|
|
46
|
+
import { normalizeRequestedSkills } from "@/lib/chat-runtime-utils";
|
|
47
|
+
import {
|
|
48
|
+
getSessionProjectName,
|
|
49
|
+
normalizeSessionProjectRootValue,
|
|
50
|
+
} from "@/lib/session-project/session-project.utils";
|
|
28
51
|
|
|
29
52
|
export function buildNcpSendMetadata(payload: {
|
|
30
53
|
model?: string;
|
|
@@ -67,33 +90,89 @@ function isMissingNcpSessionError(error: unknown): boolean {
|
|
|
67
90
|
if (!(error instanceof Error)) {
|
|
68
91
|
return false;
|
|
69
92
|
}
|
|
70
|
-
return error.message.includes(
|
|
93
|
+
return error.message.includes("ncp session not found:");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type NcpSeedSessionSummary = {
|
|
97
|
+
sessionId: string;
|
|
98
|
+
status?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function useNcpConversationSeedLoader<T extends NcpSeedSessionSummary>(
|
|
102
|
+
sessionSummariesRef: MutableRefObject<readonly T[]>,
|
|
103
|
+
): NcpConversationSeedLoader {
|
|
104
|
+
return useCallback(
|
|
105
|
+
async (
|
|
106
|
+
sessionId: string,
|
|
107
|
+
signal: AbortSignal,
|
|
108
|
+
): Promise<NcpConversationSeed> => {
|
|
109
|
+
signal.throwIfAborted();
|
|
110
|
+
let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null =
|
|
111
|
+
null;
|
|
112
|
+
try {
|
|
113
|
+
history = await fetchNcpSessionMessages(sessionId, 300);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!isMissingNcpSessionError(error)) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
signal.throwIfAborted();
|
|
120
|
+
|
|
121
|
+
const sessionSummary =
|
|
122
|
+
sessionSummariesRef.current.find(
|
|
123
|
+
(item) => item.sessionId === sessionId,
|
|
124
|
+
) ?? null;
|
|
125
|
+
return {
|
|
126
|
+
messages: history?.messages ?? [],
|
|
127
|
+
status: sessionSummary?.status === "running" ? "running" : "idle",
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
[sessionSummariesRef],
|
|
131
|
+
);
|
|
71
132
|
}
|
|
72
133
|
|
|
73
134
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
74
135
|
const [presenter] = useState(() => new NcpChatPresenter());
|
|
75
|
-
const [draftSessionId, setDraftSessionId] = useState(() =>
|
|
136
|
+
const [draftSessionId, setDraftSessionId] = useState(() =>
|
|
137
|
+
createNcpSessionId(),
|
|
138
|
+
);
|
|
76
139
|
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
77
|
-
const selectedSessionKey = useChatSessionListStore(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
const selectedSessionKey = useChatSessionListStore(
|
|
141
|
+
(state) => state.snapshot.selectedSessionKey,
|
|
142
|
+
);
|
|
143
|
+
const selectedAgentId = useChatSessionListStore(
|
|
144
|
+
(state) => state.snapshot.selectedAgentId,
|
|
145
|
+
);
|
|
146
|
+
const pendingSessionType = useChatInputStore(
|
|
147
|
+
(state) => state.snapshot.pendingSessionType,
|
|
148
|
+
);
|
|
149
|
+
const pendingProjectRoot = useChatInputStore(
|
|
150
|
+
(state) => state.snapshot.pendingProjectRoot,
|
|
151
|
+
);
|
|
152
|
+
const pendingProjectRootSessionKey = useChatInputStore(
|
|
153
|
+
(state) => state.snapshot.pendingProjectRootSessionKey,
|
|
154
|
+
);
|
|
155
|
+
const currentSelectedModel = useChatInputStore(
|
|
156
|
+
(state) => state.snapshot.selectedModel,
|
|
157
|
+
);
|
|
83
158
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
84
159
|
const location = useLocation();
|
|
85
160
|
const navigate = useNavigate();
|
|
86
|
-
const { sessionId: routeSessionIdParam } = useParams<{
|
|
161
|
+
const { sessionId: routeSessionIdParam } = useParams<{
|
|
162
|
+
sessionId?: string;
|
|
163
|
+
}>();
|
|
87
164
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
88
165
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
89
|
-
const sessionStreamAttachInFlightRef = useRef(false);
|
|
90
166
|
const routeSessionKey = useMemo(
|
|
91
167
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
92
|
-
[routeSessionIdParam]
|
|
168
|
+
[routeSessionIdParam],
|
|
93
169
|
);
|
|
94
170
|
const sessionKey = selectedSessionKey ?? draftSessionId;
|
|
95
|
-
const hasSessionProjectRootOverride =
|
|
96
|
-
|
|
171
|
+
const hasSessionProjectRootOverride =
|
|
172
|
+
pendingProjectRootSessionKey === sessionKey;
|
|
173
|
+
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
174
|
+
? pendingProjectRoot
|
|
175
|
+
: undefined;
|
|
97
176
|
const {
|
|
98
177
|
sessionSkillsQuery,
|
|
99
178
|
isProviderStateResolved,
|
|
@@ -106,7 +185,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
106
185
|
selectedSessionType,
|
|
107
186
|
canEditSessionType,
|
|
108
187
|
sessionTypeUnavailable,
|
|
109
|
-
sessionTypeUnavailableMessage
|
|
188
|
+
sessionTypeUnavailableMessage,
|
|
110
189
|
} = useNcpChatPageData({
|
|
111
190
|
query,
|
|
112
191
|
sessionKey,
|
|
@@ -115,7 +194,8 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
115
194
|
pendingSessionType,
|
|
116
195
|
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
117
196
|
setSelectedModel: presenter.chatInputManager.setSelectedModel,
|
|
118
|
-
setSelectedThinkingLevel:
|
|
197
|
+
setSelectedThinkingLevel:
|
|
198
|
+
presenter.chatInputManager.setSelectedThinkingLevel,
|
|
119
199
|
});
|
|
120
200
|
|
|
121
201
|
const sessionSummariesRef = useRef(sessionSummaries);
|
|
@@ -126,35 +206,18 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
126
206
|
const [ncpClient] = useState(
|
|
127
207
|
() =>
|
|
128
208
|
new NcpHttpAgentClientEndpoint({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
})
|
|
209
|
+
baseUrl: API_BASE,
|
|
210
|
+
basePath: "/api/ncp/agent",
|
|
211
|
+
fetchImpl: createNcpAppClientFetch(),
|
|
212
|
+
}),
|
|
133
213
|
);
|
|
134
214
|
|
|
135
|
-
const loadSeed =
|
|
136
|
-
signal.throwIfAborted();
|
|
137
|
-
let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
|
|
138
|
-
try {
|
|
139
|
-
history = await fetchNcpSessionMessages(sessionId, 300);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if (!isMissingNcpSessionError(error)) {
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
signal.throwIfAborted();
|
|
146
|
-
|
|
147
|
-
const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
|
|
148
|
-
return {
|
|
149
|
-
messages: history?.messages ?? [],
|
|
150
|
-
status: sessionSummary?.status === 'running' ? 'running' : 'idle'
|
|
151
|
-
};
|
|
152
|
-
}, []);
|
|
215
|
+
const loadSeed = useNcpConversationSeedLoader(sessionSummariesRef);
|
|
153
216
|
|
|
154
217
|
const agent = useHydratedNcpAgent({
|
|
155
218
|
sessionId: sessionKey,
|
|
156
219
|
client: ncpClient,
|
|
157
|
-
loadSeed
|
|
220
|
+
loadSeed,
|
|
158
221
|
});
|
|
159
222
|
|
|
160
223
|
useEffect(() => {
|
|
@@ -169,43 +232,21 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
169
232
|
}
|
|
170
233
|
}, [presenter, selectedSessionKey]);
|
|
171
234
|
|
|
172
|
-
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
173
|
-
|
|
235
|
+
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
236
|
+
? pendingProjectRoot
|
|
237
|
+
: (selectedSession?.projectRoot ?? null);
|
|
238
|
+
const effectiveSessionProjectName = hasSessionProjectRootOverride
|
|
239
|
+
? getSessionProjectName(effectiveSessionProjectRoot)
|
|
240
|
+
: (selectedSession?.projectName ??
|
|
241
|
+
getSessionProjectName(effectiveSessionProjectRoot));
|
|
242
|
+
const parentSessionId = selectedSession?.parentSessionId ?? null;
|
|
174
243
|
|
|
175
244
|
const isSending = agent.isSending || agent.isRunning;
|
|
176
245
|
const isAwaitingAssistantOutput = agent.isRunning;
|
|
177
246
|
const canStopCurrentRun = agent.isRunning;
|
|
178
|
-
const stopDisabledReason = agent.isRunning ? null :
|
|
179
|
-
const lastSendError =
|
|
180
|
-
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
const attachRealtimeSessionStream = () => {
|
|
183
|
-
if (sessionStreamAttachInFlightRef.current) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (agent.isHydrating || agent.isRunning || agent.isSending) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
sessionStreamAttachInFlightRef.current = true;
|
|
191
|
-
void ncpClient
|
|
192
|
-
.stream({ sessionId: sessionKey })
|
|
193
|
-
.catch(() => undefined)
|
|
194
|
-
.finally(() => {
|
|
195
|
-
sessionStreamAttachInFlightRef.current = false;
|
|
196
|
-
});
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
return appClient.subscribe((event) => {
|
|
200
|
-
if (
|
|
201
|
-
event.type === 'session.run-status' &&
|
|
202
|
-
event.payload.sessionKey === sessionKey &&
|
|
203
|
-
event.payload.status === 'running'
|
|
204
|
-
) {
|
|
205
|
-
attachRealtimeSessionStream();
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}, [agent.isHydrating, agent.isRunning, agent.isSending, ncpClient, sessionKey]);
|
|
247
|
+
const stopDisabledReason = agent.isRunning ? null : "__preparing__";
|
|
248
|
+
const lastSendError =
|
|
249
|
+
agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
209
250
|
|
|
210
251
|
useEffect(() => {
|
|
211
252
|
presenter.chatStreamActionsManager.bind({
|
|
@@ -220,16 +261,16 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
220
261
|
projectRoot:
|
|
221
262
|
payload.sessionKey === pendingProjectRootSessionKey
|
|
222
263
|
? pendingProjectRoot
|
|
223
|
-
: selectedSession?.projectRoot ?? null,
|
|
264
|
+
: (selectedSession?.projectRoot ?? null),
|
|
224
265
|
requestedSkills: payload.requestedSkills,
|
|
225
|
-
composerNodes: payload.composerNodes
|
|
266
|
+
composerNodes: payload.composerNodes,
|
|
226
267
|
});
|
|
227
268
|
const envelope = buildNcpRequestEnvelope({
|
|
228
269
|
sessionId: payload.sessionKey,
|
|
229
270
|
text: payload.message,
|
|
230
271
|
attachments: payload.attachments,
|
|
231
272
|
parts: payload.parts,
|
|
232
|
-
metadata
|
|
273
|
+
metadata,
|
|
233
274
|
});
|
|
234
275
|
if (!envelope) {
|
|
235
276
|
return;
|
|
@@ -241,11 +282,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
241
282
|
if (payload.composerNodes && payload.composerNodes.length > 0) {
|
|
242
283
|
presenter.chatInputManager.restoreComposerState?.(
|
|
243
284
|
payload.composerNodes,
|
|
244
|
-
payload.attachments ?? []
|
|
285
|
+
payload.attachments ?? [],
|
|
245
286
|
);
|
|
246
287
|
} else {
|
|
247
288
|
presenter.chatInputManager.setDraft((currentDraft) =>
|
|
248
|
-
currentDraft.trim().length === 0
|
|
289
|
+
currentDraft.trim().length === 0
|
|
290
|
+
? payload.message
|
|
291
|
+
: currentDraft,
|
|
249
292
|
);
|
|
250
293
|
}
|
|
251
294
|
}
|
|
@@ -264,7 +307,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
264
307
|
resetStreamState: () => {
|
|
265
308
|
selectedSessionKeyRef.current = null;
|
|
266
309
|
},
|
|
267
|
-
applyHistoryMessages: () => {}
|
|
310
|
+
applyHistoryMessages: () => {},
|
|
268
311
|
});
|
|
269
312
|
}, [
|
|
270
313
|
agent,
|
|
@@ -272,16 +315,20 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
272
315
|
pendingProjectRootSessionKey,
|
|
273
316
|
presenter,
|
|
274
317
|
selectedSession?.projectRoot,
|
|
275
|
-
sessionKey
|
|
318
|
+
sessionKey,
|
|
276
319
|
]);
|
|
277
320
|
|
|
278
321
|
useEffect(() => {
|
|
279
|
-
if (
|
|
322
|
+
if (
|
|
323
|
+
!selectedSession ||
|
|
324
|
+
pendingProjectRootSessionKey !== selectedSession.key ||
|
|
325
|
+
(selectedSession.projectRoot ?? null) !== pendingProjectRoot
|
|
326
|
+
) {
|
|
280
327
|
return;
|
|
281
328
|
}
|
|
282
329
|
useChatInputStore.getState().setSnapshot({
|
|
283
330
|
pendingProjectRoot: null,
|
|
284
|
-
pendingProjectRootSessionKey: null
|
|
331
|
+
pendingProjectRootSessionKey: null,
|
|
285
332
|
});
|
|
286
333
|
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession]);
|
|
287
334
|
|
|
@@ -290,27 +337,40 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
290
337
|
routeSessionKey,
|
|
291
338
|
selectedSessionKey,
|
|
292
339
|
selectedAgentId,
|
|
293
|
-
setSelectedSessionKey:
|
|
340
|
+
setSelectedSessionKey:
|
|
341
|
+
presenter.chatSessionListManager.setSelectedSessionKey,
|
|
294
342
|
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
295
343
|
selectedSessionKeyRef,
|
|
296
344
|
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
297
|
-
resolveAgentIdFromSessionKey
|
|
345
|
+
resolveAgentIdFromSessionKey,
|
|
298
346
|
});
|
|
299
347
|
|
|
300
348
|
useEffect(() => {
|
|
301
349
|
presenter.chatUiManager.syncState({
|
|
302
|
-
pathname: location.pathname
|
|
350
|
+
pathname: location.pathname,
|
|
303
351
|
});
|
|
304
352
|
presenter.chatUiManager.bindActions({
|
|
305
353
|
navigate,
|
|
306
|
-
confirm
|
|
354
|
+
confirm,
|
|
307
355
|
});
|
|
308
356
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
309
357
|
|
|
310
|
-
const currentSessionDisplayName = selectedSession
|
|
358
|
+
const currentSessionDisplayName = selectedSession
|
|
359
|
+
? sessionDisplayName(selectedSession)
|
|
360
|
+
: undefined;
|
|
361
|
+
const parentSession = useMemo(() => {
|
|
362
|
+
if (!parentSessionId) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const parentSummary =
|
|
366
|
+
sessionSummaries.find(
|
|
367
|
+
(summary) => summary.sessionId === parentSessionId,
|
|
368
|
+
) ?? null;
|
|
369
|
+
return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
|
|
370
|
+
}, [parentSessionId, sessionSummaries]);
|
|
311
371
|
const currentSessionTypeLabel =
|
|
312
|
-
sessionTypeOptions.find((option) => option.value === selectedSessionType)
|
|
313
|
-
|
|
372
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)
|
|
373
|
+
?.label ?? resolveSessionTypeLabel(selectedSessionType);
|
|
314
374
|
|
|
315
375
|
useEffect(() => {
|
|
316
376
|
presenter.chatInputManager.syncSnapshot({
|
|
@@ -328,7 +388,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
328
388
|
canEditSessionType,
|
|
329
389
|
sessionTypeUnavailable,
|
|
330
390
|
skillRecords,
|
|
331
|
-
isSkillsLoading: sessionSkillsQuery.isLoading
|
|
391
|
+
isSkillsLoading: sessionSkillsQuery.isLoading,
|
|
332
392
|
});
|
|
333
393
|
presenter.chatThreadManager.syncSnapshot({
|
|
334
394
|
isProviderStateResolved,
|
|
@@ -345,7 +405,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
345
405
|
isHistoryLoading: agent.isHydrating,
|
|
346
406
|
messages: agent.visibleMessages,
|
|
347
407
|
isSending,
|
|
348
|
-
isAwaitingAssistantOutput
|
|
408
|
+
isAwaitingAssistantOutput,
|
|
409
|
+
parentSessionKey: parentSession?.key ?? null,
|
|
410
|
+
parentSessionLabel: parentSession
|
|
411
|
+
? sessionDisplayName(parentSession)
|
|
412
|
+
: null,
|
|
349
413
|
});
|
|
350
414
|
}, [
|
|
351
415
|
agent.isHydrating,
|
|
@@ -360,6 +424,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
360
424
|
isSending,
|
|
361
425
|
lastSendError,
|
|
362
426
|
modelOptions,
|
|
427
|
+
parentSession,
|
|
363
428
|
presenter,
|
|
364
429
|
effectiveSessionProjectName,
|
|
365
430
|
effectiveSessionProjectRoot,
|
|
@@ -372,7 +437,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
372
437
|
skillRecords,
|
|
373
438
|
stopDisabledReason,
|
|
374
439
|
threadRef,
|
|
375
|
-
agent.visibleMessages
|
|
440
|
+
agent.visibleMessages,
|
|
376
441
|
]);
|
|
377
442
|
|
|
378
443
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appQueryClient } from '@/app-query-client';
|
|
2
2
|
import { deleteNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
|
|
3
3
|
import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
|
|
4
|
+
import type { ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
|
|
4
5
|
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
5
6
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
6
7
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
@@ -45,6 +46,54 @@ export class NcpChatThreadManager {
|
|
|
45
46
|
this.uiManager.goToProviders();
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
openSessionFromToolAction = (action: ChatToolActionViewModel) => {
|
|
50
|
+
if (action.kind !== 'open-session') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (action.sessionKind === 'child' && !this.isCompactViewport()) {
|
|
54
|
+
const parentSessionKey =
|
|
55
|
+
action.parentSessionId?.trim() ||
|
|
56
|
+
useChatSessionListStore.getState().snapshot.selectedSessionKey ||
|
|
57
|
+
null;
|
|
58
|
+
useChatThreadStore.getState().setSnapshot({
|
|
59
|
+
childSessionDetailSessionKey: action.sessionId,
|
|
60
|
+
childSessionDetailParentSessionKey: parentSessionKey,
|
|
61
|
+
childSessionDetailLabel: action.label?.trim() || null,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.uiManager.goToSession(action.sessionId);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
closeChildSessionDetail = () => {
|
|
69
|
+
useChatThreadStore.getState().setSnapshot({
|
|
70
|
+
childSessionDetailSessionKey: null,
|
|
71
|
+
childSessionDetailParentSessionKey: null,
|
|
72
|
+
childSessionDetailLabel: null,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
goToParentSession = () => {
|
|
77
|
+
const {
|
|
78
|
+
parentSessionKey,
|
|
79
|
+
childSessionDetailParentSessionKey,
|
|
80
|
+
} = useChatThreadStore.getState().snapshot;
|
|
81
|
+
const resolvedParentSessionKey =
|
|
82
|
+
parentSessionKey ?? childSessionDetailParentSessionKey;
|
|
83
|
+
if (!resolvedParentSessionKey) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.closeChildSessionDetail();
|
|
87
|
+
this.uiManager.goToSession(resolvedParentSessionKey);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
private isCompactViewport = (): boolean => {
|
|
91
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return window.matchMedia('(max-width: 767px)').matches;
|
|
95
|
+
};
|
|
96
|
+
|
|
48
97
|
private deleteCurrentSession = async () => {
|
|
49
98
|
const {
|
|
50
99
|
snapshot: { selectedSessionKey }
|