@nextclaw/ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
  3. package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
  4. package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-Dsd8Dlq8.js} +1 -1
  5. package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-2ChEc_oz.js} +1 -1
  6. package/dist/assets/MarketplacePage-BXck6-X3.js +49 -0
  7. package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-CgHRSD0b.js} +1 -1
  8. package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
  9. package/dist/assets/RuntimeConfig-ClLEKNTN.js +1 -0
  10. package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-CuXVCbrf.js} +1 -1
  11. package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-udJz6Ake.js} +2 -2
  12. package/dist/assets/SessionsConfig-C1XnFfiC.js +2 -0
  13. package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-BETwXLD4.js} +3 -3
  14. package/dist/assets/{index-uMsNsQX6.js → index-COJdlL0e.js} +1 -1
  15. package/dist/assets/index-CsvP4CER.js +8 -0
  16. package/dist/assets/index-D-bXl7qL.css +1 -0
  17. package/dist/assets/{label-D8ly4a2P.js → label-BGL-ztxh.js} +1 -1
  18. package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-aw88k7tG.js} +1 -1
  19. package/dist/assets/popover-DyEvzhmV.js +1 -0
  20. package/dist/assets/security-config-BuPAQn82.js +1 -0
  21. package/dist/assets/skeleton-drzO_tdU.js +1 -0
  22. package/dist/assets/{switch-Ce_g9lpN.js → switch-BK8jIzto.js} +1 -1
  23. package/dist/assets/{tabs-custom-Cf5azvT5.js → tabs-custom-Da3cEOji.js} +1 -1
  24. package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-z0CE92iS.js} +2 -2
  25. package/dist/assets/{vendor-B7ozqnFC.js → vendor-CkJHmX1g.js} +65 -70
  26. package/dist/index.html +3 -3
  27. package/package.json +5 -2
  28. package/src/api/config.ts +9 -0
  29. package/src/api/ncp-session.ts +50 -0
  30. package/src/api/types.ts +20 -0
  31. package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
  32. package/src/components/chat/ChatConversationPanel.tsx +21 -12
  33. package/src/components/chat/ChatPage.tsx +10 -324
  34. package/src/components/chat/ChatSidebar.test.tsx +203 -0
  35. package/src/components/chat/ChatSidebar.tsx +97 -7
  36. package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -81
  37. package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
  38. package/src/components/chat/chat-chain.test.ts +22 -0
  39. package/src/components/chat/chat-chain.ts +23 -0
  40. package/src/components/chat/chat-page-data.ts +30 -1
  41. package/src/components/chat/chat-page-runtime.test.ts +181 -0
  42. package/src/components/chat/chat-page-runtime.ts +101 -15
  43. package/src/components/chat/chat-page-shell.tsx +103 -0
  44. package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
  45. package/src/components/chat/chat-session-preference-sync.ts +75 -0
  46. package/src/components/chat/containers/chat-input-bar.container.tsx +0 -22
  47. package/src/components/chat/containers/chat-message-list.container.tsx +34 -26
  48. package/src/components/chat/legacy/LegacyChatPage.tsx +252 -0
  49. package/src/components/chat/managers/chat-input.manager.ts +5 -0
  50. package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
  51. package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
  52. package/src/components/chat/ncp/NcpChatPage.tsx +381 -0
  53. package/src/components/chat/ncp/ncp-chat-input.manager.ts +179 -0
  54. package/src/components/chat/ncp/ncp-chat-page-data.ts +166 -0
  55. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  56. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  57. package/src/components/chat/ncp/ncp-session-adapter.test.ts +75 -0
  58. package/src/components/chat/ncp/ncp-session-adapter.ts +214 -0
  59. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  60. package/src/components/chat/stores/chat-thread.store.ts +2 -0
  61. package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
  62. package/src/components/chat/useChatSessionTypeState.ts +25 -8
  63. package/src/hooks/use-ncp-chat-session-types.ts +11 -0
  64. package/src/hooks/useConfig.ts +41 -1
  65. package/src/hooks/useMarketplace.ts +7 -4
  66. package/src/hooks/useWebSocket.ts +23 -2
  67. package/src/lib/i18n.ts +1 -1
  68. package/tailwind.config.js +8 -3
  69. package/tsconfig.json +4 -1
  70. package/dist/assets/ChannelsList-DF2U-LY1.js +0 -1
  71. package/dist/assets/ChatPage-BX39y0U5.js +0 -36
  72. package/dist/assets/MarketplacePage-DG5mHWJ8.js +0 -49
  73. package/dist/assets/ProvidersList-CH5z00YT.js +0 -1
  74. package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
  75. package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
  76. package/dist/assets/index-BLeJkJ0o.css +0 -1
  77. package/dist/assets/index-DK4TS5ev.js +0 -8
  78. package/dist/assets/index-X5J6Mm--.js +0 -1
  79. package/dist/assets/security-config-DlKEYHNN.js +0 -1
  80. package/dist/assets/skeleton-CWbsNx2h.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-DK4TS5ev.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-B7ozqnFC.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-BLeJkJ0o.css">
9
+ <script type="module" crossorigin src="/assets/index-CsvP4CER.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CkJHmX1g.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-D-bXl7qL.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,7 +27,10 @@
27
27
  "tailwind-merge": "^2.5.4",
28
28
  "zod": "^3.23.8",
29
29
  "zustand": "^5.0.2",
30
- "@nextclaw/agent-chat-ui": "0.1.1",
30
+ "@nextclaw/ncp-http-agent-client": "0.3.0",
31
+ "@nextclaw/agent-chat-ui": "0.2.0",
32
+ "@nextclaw/ncp-react": "0.3.1",
33
+ "@nextclaw/ncp": "0.3.0",
31
34
  "@nextclaw/agent-chat": "0.1.1"
32
35
  },
33
36
  "devDependencies": {
package/src/api/config.ts CHANGED
@@ -601,6 +601,15 @@ export async function fetchChatSessionTypes(): Promise<ChatSessionTypesView> {
601
601
  return response.data;
602
602
  }
603
603
 
604
+ // GET /api/ncp/session-types
605
+ export async function fetchNcpChatSessionTypes(): Promise<ChatSessionTypesView> {
606
+ const response = await api.get<ChatSessionTypesView>('/api/ncp/session-types');
607
+ if (!response.ok) {
608
+ throw new Error(response.error.message);
609
+ }
610
+ return response.data;
611
+ }
612
+
604
613
  // POST /api/chat/turn/stop
605
614
  export async function stopChatTurn(data: ChatTurnStopRequest): Promise<ChatTurnStopResult> {
606
615
  const response = await api.post<ChatTurnStopResult>('/api/chat/turn/stop', data);
@@ -0,0 +1,50 @@
1
+ import { api } from './client';
2
+ import type { NcpSessionMessagesView, NcpSessionsListView, NcpSessionSummaryView, SessionPatchUpdate } from './types';
3
+
4
+ // GET /api/ncp/sessions
5
+ export async function fetchNcpSessions(params?: { limit?: number }): Promise<NcpSessionsListView> {
6
+ const query = new URLSearchParams();
7
+ if (typeof params?.limit === 'number' && Number.isFinite(params.limit)) {
8
+ query.set('limit', String(Math.max(1, Math.trunc(params.limit))));
9
+ }
10
+ const suffix = query.toString();
11
+ const response = await api.get<NcpSessionsListView>(suffix ? `/api/ncp/sessions?${suffix}` : '/api/ncp/sessions');
12
+ if (!response.ok) {
13
+ throw new Error(response.error.message);
14
+ }
15
+ return response.data;
16
+ }
17
+
18
+ // GET /api/ncp/sessions/:sessionId/messages
19
+ export async function fetchNcpSessionMessages(sessionId: string, limit = 200): Promise<NcpSessionMessagesView> {
20
+ const response = await api.get<NcpSessionMessagesView>(
21
+ `/api/ncp/sessions/${encodeURIComponent(sessionId)}/messages?limit=${Math.max(1, Math.trunc(limit))}`
22
+ );
23
+ if (!response.ok) {
24
+ throw new Error(response.error.message);
25
+ }
26
+ return response.data;
27
+ }
28
+
29
+ // PUT /api/ncp/sessions/:sessionId
30
+ export async function updateNcpSession(
31
+ sessionId: string,
32
+ data: SessionPatchUpdate
33
+ ): Promise<NcpSessionSummaryView> {
34
+ const response = await api.put<NcpSessionSummaryView>(`/api/ncp/sessions/${encodeURIComponent(sessionId)}`, data);
35
+ if (!response.ok) {
36
+ throw new Error(response.error.message);
37
+ }
38
+ return response.data;
39
+ }
40
+
41
+ // DELETE /api/ncp/sessions/:sessionId
42
+ export async function deleteNcpSession(sessionId: string): Promise<{ deleted: boolean; sessionId: string }> {
43
+ const response = await api.delete<{ deleted: boolean; sessionId: string }>(
44
+ `/api/ncp/sessions/${encodeURIComponent(sessionId)}`
45
+ );
46
+ if (!response.ok) {
47
+ throw new Error(response.error.message);
48
+ }
49
+ return response.data;
50
+ }
package/src/api/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { NcpMessage, NcpSessionStatus, NcpSessionSummary } from '@nextclaw/ncp';
2
+
1
3
  // API Types - matching backend response format
2
4
  export type ApiError = {
3
5
  code: string;
@@ -261,9 +263,27 @@ export type SessionHistoryView = {
261
263
  events: SessionEventView[];
262
264
  };
263
265
 
266
+ export type NcpSessionSummaryView = NcpSessionSummary;
267
+
268
+ export type NcpSessionsListView = {
269
+ sessions: NcpSessionSummaryView[];
270
+ total: number;
271
+ };
272
+
273
+ export type NcpMessageView = NcpMessage;
274
+
275
+ export type NcpSessionMessagesView = {
276
+ sessionId: string;
277
+ messages: NcpMessageView[];
278
+ total: number;
279
+ };
280
+
281
+ export type NcpSessionStatusView = NcpSessionStatus;
282
+
264
283
  export type SessionPatchUpdate = {
265
284
  label?: string | null;
266
285
  preferredModel?: string | null;
286
+ preferredThinking?: ThinkingLevel | null;
267
287
  sessionType?: string | null;
268
288
  clearHistory?: boolean;
269
289
  };
@@ -0,0 +1,65 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
4
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ deleteSession: vi.fn(),
8
+ goToProviders: vi.fn()
9
+ }));
10
+
11
+ vi.mock('@nextclaw/agent-chat-ui', () => ({
12
+ useStickyBottomScroll: () => ({
13
+ onScroll: vi.fn()
14
+ })
15
+ }));
16
+
17
+ vi.mock('@/components/chat/nextclaw', () => ({
18
+ ChatInputBarContainer: () => <div data-testid="chat-input-bar" />,
19
+ ChatMessageListContainer: () => <div data-testid="chat-message-list" />
20
+ }));
21
+
22
+ vi.mock('@/components/chat/ChatWelcome', () => ({
23
+ ChatWelcome: () => <div data-testid="chat-welcome" />
24
+ }));
25
+
26
+ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
27
+ usePresenter: () => ({
28
+ chatThreadManager: {
29
+ deleteSession: mocks.deleteSession,
30
+ goToProviders: mocks.goToProviders,
31
+ createSession: vi.fn()
32
+ }
33
+ })
34
+ }));
35
+
36
+ describe('ChatConversationPanel', () => {
37
+ beforeEach(() => {
38
+ mocks.deleteSession.mockReset();
39
+ mocks.goToProviders.mockReset();
40
+ useChatThreadStore.setState({
41
+ snapshot: {
42
+ ...useChatThreadStore.getState().snapshot,
43
+ isProviderStateResolved: true,
44
+ modelOptions: [{ value: 'openai/gpt-5.1', modelLabel: 'gpt-5.1', providerLabel: 'OpenAI' } as never],
45
+ sessionTypeLabel: 'Codex',
46
+ selectedSessionKey: null,
47
+ sessionDisplayName: undefined,
48
+ canDeleteSession: false,
49
+ isDeletePending: false,
50
+ isHistoryLoading: false,
51
+ uiMessages: [],
52
+ isSending: false,
53
+ isAwaitingAssistantOutput: false
54
+ }
55
+ });
56
+ });
57
+
58
+ it('shows the draft session type in the conversation header', () => {
59
+ render(<ChatConversationPanel />);
60
+
61
+ expect(screen.getByText('New Task')).toBeTruthy();
62
+ expect(screen.getByText('Codex')).toBeTruthy();
63
+ expect(screen.queryByRole('button')).toBeNull();
64
+ });
65
+ });
@@ -42,6 +42,8 @@ export function ChatConversationPanel() {
42
42
  const snapshot = useChatThreadStore((state) => state.snapshot);
43
43
  const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
44
44
  const threadRef = snapshot.threadRef ?? fallbackThreadRef;
45
+ const shouldShowSessionHeader = Boolean(snapshot.selectedSessionKey || snapshot.sessionTypeLabel);
46
+ const sessionHeaderTitle = snapshot.sessionDisplayName || snapshot.selectedSessionKey || t('chatSidebarNewTask');
45
47
 
46
48
  const showWelcome = !snapshot.selectedSessionKey && snapshot.uiMessages.length === 0 && !snapshot.isSending;
47
49
  const hasConfiguredModel = snapshot.modelOptions.length > 0;
@@ -68,22 +70,29 @@ export function ChatConversationPanel() {
68
70
  <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
69
71
  <div className={cn(
70
72
  "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",
71
- snapshot.selectedSessionKey ? "py-3 opacity-100" : "h-0 py-0 opacity-0 border-b-0"
73
+ shouldShowSessionHeader ? "py-3 opacity-100" : "h-0 py-0 opacity-0 border-b-0"
72
74
  )}>
73
- <div className="min-w-0 flex-1">
75
+ <div className="min-w-0 flex-1 flex items-center gap-2">
74
76
  <span className="text-sm font-medium text-gray-700 truncate">
75
- {snapshot.sessionDisplayName || snapshot.selectedSessionKey}
77
+ {sessionHeaderTitle}
76
78
  </span>
79
+ {snapshot.sessionTypeLabel ? (
80
+ <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">
81
+ {snapshot.sessionTypeLabel}
82
+ </span>
83
+ ) : null}
77
84
  </div>
78
- <Button
79
- variant="ghost"
80
- size="icon"
81
- className="rounded-lg shrink-0 text-gray-400 hover:text-destructive"
82
- onClick={presenter.chatThreadManager.deleteSession}
83
- disabled={!snapshot.canDeleteSession || snapshot.isDeletePending}
84
- >
85
- <Trash2 className="h-4 w-4" />
86
- </Button>
85
+ {snapshot.selectedSessionKey ? (
86
+ <Button
87
+ variant="ghost"
88
+ size="icon"
89
+ className="rounded-lg shrink-0 text-gray-400 hover:text-destructive"
90
+ onClick={presenter.chatThreadManager.deleteSession}
91
+ disabled={!snapshot.canDeleteSession || snapshot.isDeletePending}
92
+ >
93
+ <Trash2 className="h-4 w-4" />
94
+ </Button>
95
+ ) : null}
87
96
  </div>
88
97
 
89
98
  {shouldShowProviderHint && (
@@ -1,330 +1,16 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
2
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
- import { useConfirmDialog } from '@/hooks/useConfirmDialog';
4
- import { ChatSidebar } from '@/components/chat/ChatSidebar';
5
- import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
6
- import { CronConfig } from '@/components/config/CronConfig';
7
- import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
8
- import { useSessionRunStatus } from '@/components/chat/chat-page-runtime';
9
- import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
10
- import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
11
- import { useChatPageData, sessionDisplayName } from '@/components/chat/chat-page-data';
12
- import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
13
- import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
14
- import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
15
- import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
16
- import { useLocation, useNavigate, useParams } from 'react-router-dom';
17
-
18
- type MainPanelView = 'chat' | 'cron' | 'skills';
19
-
20
- type ChatPageProps = {
21
- view: MainPanelView;
22
- };
23
-
24
- type UseSessionSyncParams = {
25
- view: MainPanelView;
26
- routeSessionKey: string | null;
27
- selectedSessionKey: string | null;
28
- selectedAgentId: string;
29
- setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
30
- setSelectedAgentId: Dispatch<SetStateAction<string>>;
31
- selectedSessionKeyRef: MutableRefObject<string | null>;
32
- resetStreamState: () => void;
33
- };
34
-
35
- function useChatSessionSync(params: UseSessionSyncParams): void {
36
- const {
37
- view,
38
- routeSessionKey,
39
- selectedSessionKey,
40
- selectedAgentId,
41
- setSelectedSessionKey,
42
- setSelectedAgentId,
43
- selectedSessionKeyRef,
44
- resetStreamState
45
- } = params;
46
-
47
- useEffect(() => {
48
- if (view !== 'chat') {
49
- return;
50
- }
51
- if (routeSessionKey) {
52
- if (selectedSessionKey !== routeSessionKey) {
53
- setSelectedSessionKey(routeSessionKey);
54
- }
55
- return;
56
- }
57
- if (selectedSessionKey !== null) {
58
- setSelectedSessionKey(null);
59
- resetStreamState();
60
- }
61
- }, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
62
-
63
- useEffect(() => {
64
- const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
65
- if (!inferred) {
66
- return;
67
- }
68
- if (selectedAgentId !== inferred) {
69
- setSelectedAgentId(inferred);
70
- }
71
- }, [selectedAgentId, selectedSessionKey, setSelectedAgentId]);
72
-
73
- useEffect(() => {
74
- selectedSessionKeyRef.current = selectedSessionKey;
75
- }, [selectedSessionKey, selectedSessionKeyRef]);
76
- }
77
-
78
- type ChatPageLayoutProps = {
79
- view: MainPanelView;
80
- confirmDialog: JSX.Element;
81
- };
82
-
83
- function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
84
- return (
85
- <div className="h-full flex">
86
- <ChatSidebar />
87
-
88
- {view === 'chat' ? (
89
- <ChatConversationPanel />
90
- ) : (
91
- <section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
92
- {view === 'cron' ? (
93
- <div className="h-full overflow-auto custom-scrollbar">
94
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
95
- <CronConfig />
96
- </div>
97
- </div>
98
- ) : (
99
- <div className="h-full overflow-hidden">
100
- <div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
101
- <MarketplacePage forcedType="skills" />
102
- </div>
103
- </div>
104
- )}
105
- </section>
106
- )}
107
-
108
- {confirmDialog}
109
- </div>
110
- );
111
- }
1
+ import { useLocation } from 'react-router-dom';
2
+ import { resolveChatChain } from '@/components/chat/chat-chain';
3
+ import type { ChatPageProps } from '@/components/chat/chat-page-shell';
4
+ import { LegacyChatPage } from '@/components/chat/legacy/LegacyChatPage';
5
+ import { NcpChatPage } from '@/components/chat/ncp/NcpChatPage';
112
6
 
113
7
  export function ChatPage({ view }: ChatPageProps) {
114
- const [presenter] = useState(() => new ChatPresenter());
115
- const query = useChatSessionListStore((state) => state.snapshot.query);
116
- const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
117
- const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
118
- const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
119
- const { confirm, ConfirmDialog } = useConfirmDialog();
120
8
  const location = useLocation();
121
- const navigate = useNavigate();
122
- const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
123
- const threadRef = useRef<HTMLDivElement | null>(null);
124
- const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
125
- const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
126
- const routeSessionKey = useMemo(
127
- () => parseSessionKeyFromRoute(routeSessionIdParam),
128
- [routeSessionIdParam]
129
- );
130
- const {
131
- sessionsQuery,
132
- installedSkillsQuery,
133
- chatCapabilitiesQuery,
134
- historyQuery,
135
- isProviderStateResolved,
136
- modelOptions,
137
- sessions,
138
- skillRecords,
139
- selectedSession,
140
- historyMessages,
141
- selectedSessionThinkingLevel,
142
- sessionTypeOptions,
143
- defaultSessionType,
144
- selectedSessionType,
145
- canEditSessionType,
146
- sessionTypeUnavailable,
147
- sessionTypeUnavailableMessage
148
- } = useChatPageData({
149
- query,
150
- selectedSessionKey,
151
- selectedAgentId,
152
- pendingSessionType,
153
- setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
154
- setSelectedModel: presenter.chatInputManager.setSelectedModel
155
- });
156
- const {
157
- uiMessages,
158
- isSending,
159
- isAwaitingAssistantOutput,
160
- canStopCurrentRun,
161
- stopDisabledReason,
162
- lastSendError,
163
- activeBackendRunId,
164
- sendMessage,
165
- stopCurrentRun,
166
- resumeRun,
167
- resetStreamState,
168
- applyHistoryMessages
169
- } = useChatRuntimeController(
170
- {
171
- selectedSessionKeyRef,
172
- setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
173
- setDraft: presenter.chatInputManager.setDraft,
174
- refetchSessions: sessionsQuery.refetch,
175
- refetchHistory: historyQuery.refetch
176
- },
177
- presenter.chatController
178
- );
179
-
180
- console.log('[ChatPage] uiMessages', { uiMessages, historyMessages });
181
- useEffect(() => {
182
- presenter.chatStreamActionsManager.bind({
183
- sendMessage,
184
- stopCurrentRun,
185
- resumeRun,
186
- resetStreamState,
187
- applyHistoryMessages
188
- });
189
- }, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
190
-
191
- const { sessionRunStatusByKey } = useSessionRunStatus({
192
- view,
193
- selectedSessionKey,
194
- activeBackendRunId,
195
- isLocallyRunning: isSending || Boolean(activeBackendRunId),
196
- resumeRun: presenter.chatStreamActionsManager.resumeRun
197
- });
198
-
199
- useChatSessionSync({
200
- view,
201
- routeSessionKey,
202
- selectedSessionKey,
203
- selectedAgentId,
204
- setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
205
- setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
206
- selectedSessionKeyRef,
207
- resetStreamState: presenter.chatStreamActionsManager.resetStreamState
208
- });
209
-
210
- useEffect(() => {
211
- presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
212
- isLoading: historyQuery.isLoading
213
- });
214
- }, [historyMessages, historyQuery.isLoading, presenter]);
215
-
216
- useEffect(() => {
217
- presenter.chatUiManager.syncState({
218
- pathname: location.pathname
219
- });
220
- presenter.chatUiManager.bindActions({
221
- navigate,
222
- confirm
223
- });
224
- }, [confirm, location.pathname, navigate, presenter]);
225
- const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
226
-
227
- useEffect(() => {
228
- presenter.chatThreadManager.bindActions({
229
- refetchSessions: sessionsQuery.refetch
230
- });
231
- }, [
232
- presenter,
233
- sessionsQuery.refetch,
234
- ]);
235
-
236
- useEffect(() => {
237
- const shouldHydrateThinkingFromHistory =
238
- !isSending &&
239
- !isAwaitingAssistantOutput &&
240
- !historyQuery.isLoading &&
241
- selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
9
+ const chatChain = resolveChatChain(location.search);
242
10
 
243
- presenter.chatInputManager.syncSnapshot({
244
- isProviderStateResolved,
245
- defaultSessionType,
246
- canStopGeneration: canStopCurrentRun,
247
- stopDisabledReason,
248
- stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
249
- stopReason: chatCapabilitiesQuery.data?.stopReason,
250
- sendError: lastSendError,
251
- isSending,
252
- modelOptions,
253
- sessionTypeOptions,
254
- selectedSessionType,
255
- ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
256
- canEditSessionType,
257
- sessionTypeUnavailable,
258
- skillRecords,
259
- isSkillsLoading: installedSkillsQuery.isLoading
260
- });
261
- if (shouldHydrateThinkingFromHistory) {
262
- thinkingHydratedSessionKeyRef.current = selectedSessionKey;
263
- }
264
- if (!selectedSessionKey) {
265
- thinkingHydratedSessionKeyRef.current = null;
266
- }
267
- presenter.chatSessionListManager.syncSnapshot({
268
- sessions,
269
- query,
270
- isLoading: sessionsQuery.isLoading
271
- });
272
- presenter.chatRunStatusManager.syncSnapshot({
273
- sessionRunStatusByKey,
274
- isLocallyRunning: isSending || Boolean(activeBackendRunId),
275
- activeBackendRunId
276
- });
277
- presenter.chatThreadManager.syncSnapshot({
278
- isProviderStateResolved,
279
- modelOptions,
280
- sessionTypeUnavailable,
281
- sessionTypeUnavailableMessage,
282
- selectedSessionKey,
283
- sessionDisplayName: currentSessionDisplayName,
284
- canDeleteSession: Boolean(selectedSession),
285
- threadRef,
286
- isHistoryLoading: historyQuery.isLoading,
287
- uiMessages,
288
- isSending,
289
- isAwaitingAssistantOutput
290
- });
291
- }, [
292
- activeBackendRunId,
293
- canEditSessionType,
294
- canStopCurrentRun,
295
- currentSessionDisplayName,
296
- chatCapabilitiesQuery.data?.stopReason,
297
- chatCapabilitiesQuery.data?.stopSupported,
298
- defaultSessionType,
299
- historyQuery.isLoading,
300
- installedSkillsQuery.isLoading,
301
- isAwaitingAssistantOutput,
302
- isProviderStateResolved,
303
- isSending,
304
- lastSendError,
305
- uiMessages,
306
- modelOptions,
307
- presenter,
308
- query,
309
- selectedSession,
310
- selectedSessionThinkingLevel,
311
- selectedSessionKey,
312
- selectedAgentId,
313
- selectedSessionType,
314
- sessionRunStatusByKey,
315
- sessionTypeOptions,
316
- sessionTypeUnavailable,
317
- sessionTypeUnavailableMessage,
318
- sessions,
319
- sessionsQuery.isLoading,
320
- stopDisabledReason,
321
- threadRef,
322
- skillRecords
323
- ]);
11
+ if (chatChain === 'ncp') {
12
+ return <NcpChatPage view={view} />;
13
+ }
324
14
 
325
- return (
326
- <ChatPresenterProvider presenter={presenter}>
327
- <ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
328
- </ChatPresenterProvider>
329
- );
15
+ return <LegacyChatPage view={view} />;
330
16
  }