@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
  3. package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
  4. package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
  5. package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
  6. package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
  7. package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
  8. package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
  9. package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
  11. package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
  12. package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
  13. package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
  14. package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
  15. package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
  16. package/dist/assets/index-bZ8cqQIS.css +1 -0
  17. package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
  18. package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
  19. package/dist/index.html +2 -2
  20. package/package.json +6 -6
  21. package/src/api/types.ts +4 -0
  22. package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
  23. package/src/components/chat/ChatConversationPanel.tsx +114 -77
  24. package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
  25. package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
  26. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
  27. package/src/components/chat/chat-child-session-panel.tsx +100 -0
  28. package/src/components/chat/chat-page-runtime.test.ts +1 -0
  29. package/src/components/chat/chat-session-display.test.ts +1 -0
  30. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  31. package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
  32. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
  33. package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
  34. package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
  35. package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
  36. package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
  37. package/src/components/chat/stores/chat-thread.store.ts +11 -1
  38. package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
  39. package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
  40. package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
  41. package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
  42. package/dist/assets/index-BlH4-cBw.css +0 -1
  43. 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 } : {}),
@@ -9,6 +9,7 @@ function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryV
9
9
  updatedAt: '2026-03-31T10:00:00.000Z',
10
10
  sessionType: 'native',
11
11
  sessionTypeMutable: false,
12
+ isChildSession: false,
12
13
  messageCount: 3,
13
14
  ...overrides
14
15
  };
@@ -1,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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { NcpHttpAgentClientEndpoint } from '@nextclaw/ncp-http-agent-client';
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
- } from '@nextclaw/ncp-react';
8
- import { useLocation, useNavigate, useParams } from 'react-router-dom';
9
- import { API_BASE } from '@/api/api-base';
10
- import { fetchNcpSessionMessages } from '@/api/ncp-session';
11
- import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
12
- import { sessionDisplayName } from '@/components/chat/chat-session-display';
13
- import { buildInlineSkillTokensFromComposer, CHAT_UI_INLINE_TOKENS_METADATA_KEY } from '@/components/chat/chat-inline-token.utils';
14
- import { createNcpAppClientFetch } from '@/components/chat/ncp/ncp-app-client-fetch';
15
- import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
16
- import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
17
- import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
18
- import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
19
- import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
20
- import type { ResumeRunParams } from '@/components/chat/chat-stream/types';
21
- import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
22
- import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
23
- import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
24
- import { useConfirmDialog } from '@/hooks/useConfirmDialog';
25
- import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
26
- import { getSessionProjectName, normalizeSessionProjectRootValue } from '@/lib/session-project/session-project.utils';
27
- import { appClient } from '@/transport';
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('ncp session not found:');
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(() => createNcpSessionId());
136
+ const [draftSessionId, setDraftSessionId] = useState(() =>
137
+ createNcpSessionId(),
138
+ );
76
139
  const query = useChatSessionListStore((state) => state.snapshot.query);
77
- const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
78
- const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
79
- const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
80
- const pendingProjectRoot = useChatInputStore((state) => state.snapshot.pendingProjectRoot);
81
- const pendingProjectRootSessionKey = useChatInputStore((state) => state.snapshot.pendingProjectRootSessionKey);
82
- const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
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<{ sessionId?: string }>();
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 = pendingProjectRootSessionKey === sessionKey;
96
- const sessionProjectRootOverride = hasSessionProjectRootOverride ? pendingProjectRoot : undefined;
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: presenter.chatInputManager.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
- baseUrl: API_BASE,
130
- basePath: '/api/ncp/agent',
131
- fetchImpl: createNcpAppClientFetch()
132
- })
209
+ baseUrl: API_BASE,
210
+ basePath: "/api/ncp/agent",
211
+ fetchImpl: createNcpAppClientFetch(),
212
+ }),
133
213
  );
134
214
 
135
- const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
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 ? pendingProjectRoot : selectedSession?.projectRoot ?? null;
173
- const effectiveSessionProjectName = hasSessionProjectRootOverride ? getSessionProjectName(effectiveSessionProjectRoot) : selectedSession?.projectName ?? getSessionProjectName(effectiveSessionProjectRoot);
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 : '__preparing__';
179
- const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
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 ? payload.message : currentDraft
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 (!selectedSession || pendingProjectRootSessionKey !== selectedSession.key || (selectedSession.projectRoot ?? null) !== pendingProjectRoot) {
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: presenter.chatSessionListManager.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 ? sessionDisplayName(selectedSession) : undefined;
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)?.label ??
313
- resolveSessionTypeLabel(selectedSessionType);
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 }