@nextclaw/ui 0.11.0 → 0.11.1

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 (82) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/dist/assets/{ChannelsList-BqsOYnXz.js → ChannelsList-CVPqrxns.js} +4 -4
  3. package/dist/assets/ChatPage-BO1VUrAY.js +37 -0
  4. package/dist/assets/{DocBrowser-BmL0QXBZ.js → DocBrowser-FBwg8iji.js} +1 -1
  5. package/dist/assets/{LogoBadge-C1HiPZPf.js → LogoBadge-BCmJfRT8.js} +1 -1
  6. package/dist/assets/MarketplacePage-DWxXUOCx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-CLHFnNBd.js → McpMarketplacePage-Bth9X_hu.js} +2 -2
  8. package/dist/assets/{ModelConfig-LQSR58tc.js → ModelConfig-PkSp_ioc.js} +1 -1
  9. package/dist/assets/ProvidersList-DVDge8wa.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BVkzfEaL.js +1 -0
  11. package/dist/assets/RuntimeConfig-ByJs3khh.js +1 -0
  12. package/dist/assets/{SearchConfig-Chzo_JGs.js → SearchConfig-KZUAqYJN.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CEIbjZYA.js → SecretsConfig-qwB_Y_Ka.js} +2 -2
  14. package/dist/assets/SessionsConfig-CGCl4UTr.js +2 -0
  15. package/dist/assets/index-CrilScMo.css +1 -0
  16. package/dist/assets/{index-j6A_-1b6.js → index-D41ntvb7.js} +6 -6
  17. package/dist/assets/{label-GACO2RzW.js → label-7JEFhkur.js} +1 -1
  18. package/dist/assets/ncp-session-adapter-BOqhkrc-.js +1 -0
  19. package/dist/assets/{page-layout-DjXaK3A3.js → page-layout-B7q511TE.js} +1 -1
  20. package/dist/assets/popover-CywJGmPr.js +1 -0
  21. package/dist/assets/security-config-zi2UxN5r.js +1 -0
  22. package/dist/assets/skeleton-qUJZQ03S.js +1 -0
  23. package/dist/assets/{status-dot-IWEBezqb.js → status-dot-BilwNdTT.js} +1 -1
  24. package/dist/assets/{switch-DCHAJSrA.js → switch-BLp2Pno1.js} +1 -1
  25. package/dist/assets/tabs-custom-CgIdQMGC.js +1 -0
  26. package/dist/assets/useConfirmDialog-BitswAkv.js +1 -0
  27. package/dist/assets/{vendor-CNhxtHCf.js → vendor-D_JxmsLV.js} +87 -87
  28. package/dist/index.html +3 -3
  29. package/package.json +4 -4
  30. package/src/App.test.tsx +42 -10
  31. package/src/App.tsx +5 -40
  32. package/src/api/api-base.test.ts +37 -0
  33. package/src/api/api-base.ts +0 -4
  34. package/src/api/config.ts +2 -270
  35. package/src/api/types.ts +0 -117
  36. package/src/components/chat/ChatPage.tsx +1 -11
  37. package/src/components/chat/ChatSidebar.test.tsx +1 -50
  38. package/src/components/chat/ChatSidebar.tsx +0 -5
  39. package/src/components/chat/README.md +2 -0
  40. package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
  41. package/src/components/chat/chat-session-display.ts +9 -0
  42. package/src/components/chat/chat-session-label.service.ts +3 -12
  43. package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
  44. package/src/components/chat/chat-stream/types.ts +4 -57
  45. package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
  46. package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
  47. package/src/components/config/README.md +2 -0
  48. package/src/components/config/SessionsConfig.tsx +152 -132
  49. package/src/hooks/use-auth.test.ts +3 -3
  50. package/src/hooks/use-auth.ts +16 -4
  51. package/src/hooks/use-realtime-query-bridge.ts +0 -24
  52. package/src/hooks/useConfig.ts +10 -137
  53. package/src/lib/session-run-status.ts +1 -63
  54. package/src/vite-env.d.ts +1 -0
  55. package/vite.config.ts +4 -4
  56. package/dist/assets/ChatPage-CJBYKR-Y.js +0 -38
  57. package/dist/assets/MarketplacePage-BIRP0NRS.js +0 -49
  58. package/dist/assets/ProvidersList-CwI-mxah.js +0 -1
  59. package/dist/assets/RemoteAccessPage-Cw5BqZb6.js +0 -1
  60. package/dist/assets/RuntimeConfig-DbowSRAb.js +0 -1
  61. package/dist/assets/SessionsConfig-BR8GfGWL.js +0 -2
  62. package/dist/assets/chat-message-CPG7zxRR.js +0 -3
  63. package/dist/assets/index-kaPUhd-8.css +0 -1
  64. package/dist/assets/popover-DTaFiTmU.js +0 -1
  65. package/dist/assets/security-config-Dk-yoKvK.js +0 -1
  66. package/dist/assets/skeleton-Dm2xOBSA.js +0 -1
  67. package/dist/assets/tabs-custom-DKSbDSB9.js +0 -1
  68. package/dist/assets/useConfirmDialog-ByJ8A8n7.js +0 -1
  69. package/src/api/config.stream.test.ts +0 -115
  70. package/src/components/chat/chat-chain.test.ts +0 -22
  71. package/src/components/chat/chat-chain.ts +0 -23
  72. package/src/components/chat/chat-page-data.ts +0 -171
  73. package/src/components/chat/chat-page-runtime.ts +0 -190
  74. package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
  75. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
  76. package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
  77. package/src/components/chat/chat-stream/transport.ts +0 -253
  78. package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
  79. package/src/components/chat/managers/chat-input.manager.ts +0 -228
  80. package/src/components/chat/managers/chat-thread.manager.ts +0 -87
  81. package/src/components/chat/presenter/chat.presenter.ts +0 -32
  82. package/src/components/chat/useChatRuntimeController.ts +0 -134
@@ -1,16 +1,6 @@
1
- import { useLocation } from 'react-router-dom';
2
- import { resolveChatChain } from '@/components/chat/chat-chain';
3
1
  import type { ChatPageProps } from '@/components/chat/chat-page-shell';
4
- import { LegacyChatPage } from '@/components/chat/legacy/LegacyChatPage';
5
2
  import { NcpChatPage } from '@/components/chat/ncp/NcpChatPage';
6
3
 
7
4
  export function ChatPage({ view }: ChatPageProps) {
8
- const location = useLocation();
9
- const chatChain = resolveChatChain(location.search);
10
-
11
- if (chatChain === 'ncp') {
12
- return <NcpChatPage view={view} />;
13
- }
14
-
15
- return <LegacyChatPage view={view} />;
5
+ return <NcpChatPage view={view} />;
16
6
  }
@@ -11,7 +11,6 @@ const mocks = vi.hoisted(() => ({
11
11
  setQuery: vi.fn(),
12
12
  selectSession: vi.fn(),
13
13
  docOpen: vi.fn(),
14
- updateSession: vi.fn(),
15
14
  updateNcpSession: vi.fn()
16
15
  }));
17
16
 
@@ -33,15 +32,9 @@ vi.mock('@/components/doc-browser', () => ({
33
32
 
34
33
  vi.mock('@/components/chat/chat-session-label.service', () => ({
35
34
  useChatSessionLabelService: () => async (params: {
36
- chatChain: 'legacy' | 'ncp';
37
35
  sessionKey: string;
38
36
  label: string | null;
39
- }) => {
40
- if (params.chatChain === 'ncp') {
41
- return mocks.updateNcpSession(params.sessionKey, { label: params.label });
42
- }
43
- return mocks.updateSession(params.sessionKey, { label: params.label });
44
- }
37
+ }) => mocks.updateNcpSession(params.sessionKey, { label: params.label })
45
38
  }));
46
39
 
47
40
  vi.mock('@/components/common/BrandHeader', () => ({
@@ -77,9 +70,7 @@ describe('ChatSidebar', () => {
77
70
  mocks.setQuery.mockReset();
78
71
  mocks.selectSession.mockReset();
79
72
  mocks.docOpen.mockReset();
80
- mocks.updateSession.mockReset();
81
73
  mocks.updateNcpSession.mockReset();
82
- mocks.updateSession.mockResolvedValue({});
83
74
  mocks.updateNcpSession.mockResolvedValue({});
84
75
 
85
76
  useChatInputStore.setState({
@@ -285,48 +276,9 @@ describe('ChatSidebar', () => {
285
276
  label: 'Renamed Label'
286
277
  });
287
278
  });
288
- expect(mocks.updateSession).not.toHaveBeenCalled();
289
279
  expect(screen.getByText('Renamed Label')).not.toBeNull();
290
280
  });
291
281
 
292
- it('routes inline session label edits to the legacy session api when chatChain=legacy', async () => {
293
- useChatSessionListStore.setState({
294
- snapshot: {
295
- ...useChatSessionListStore.getState().snapshot,
296
- sessions: [
297
- {
298
- key: 'session:legacy-1',
299
- createdAt: '2026-03-19T09:00:00.000Z',
300
- updatedAt: '2026-03-19T09:05:00.000Z',
301
- label: 'Legacy Label',
302
- sessionType: 'native',
303
- sessionTypeMutable: false,
304
- messageCount: 1
305
- }
306
- ]
307
- }
308
- });
309
-
310
- render(
311
- <MemoryRouter initialEntries={['/chat?chatChain=legacy']}>
312
- <ChatSidebar />
313
- </MemoryRouter>
314
- );
315
-
316
- fireEvent.click(screen.getByLabelText('Edit'));
317
- fireEvent.change(screen.getByPlaceholderText('Session label (optional)'), {
318
- target: { value: 'Legacy Renamed' }
319
- });
320
- fireEvent.click(screen.getByLabelText('Save'));
321
-
322
- await waitFor(() => {
323
- expect(mocks.updateSession).toHaveBeenCalledWith('session:legacy-1', {
324
- label: 'Legacy Renamed'
325
- });
326
- });
327
- expect(mocks.updateNcpSession).not.toHaveBeenCalled();
328
- });
329
-
330
282
  it('cancels inline session label editing without saving', () => {
331
283
  useChatSessionListStore.setState({
332
284
  snapshot: {
@@ -357,7 +309,6 @@ describe('ChatSidebar', () => {
357
309
  });
358
310
  fireEvent.click(screen.getByLabelText('Cancel'));
359
311
 
360
- expect(mocks.updateSession).not.toHaveBeenCalled();
361
312
  expect(mocks.updateNcpSession).not.toHaveBeenCalled();
362
313
  expect(screen.queryByDisplayValue('Should Not Persist')).toBeNull();
363
314
  expect(screen.getByText('Cancelable Label')).not.toBeNull();
@@ -12,7 +12,6 @@ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context
12
12
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
13
13
  import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
14
14
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
15
- import { resolveChatChain } from '@/components/chat/chat-chain';
16
15
  import { cn } from '@/lib/utils';
17
16
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
18
17
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
@@ -21,7 +20,6 @@ import { useTheme } from '@/components/providers/ThemeProvider';
21
20
  import { useDocBrowser } from '@/components/doc-browser';
22
21
  import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
23
22
  import { useUiStore } from '@/stores/ui.store';
24
- import { useLocation } from 'react-router-dom';
25
23
  import {
26
24
  AlarmClock,
27
25
  BookOpen,
@@ -117,7 +115,6 @@ const navItems = [
117
115
  export function ChatSidebar() {
118
116
  const presenter = usePresenter();
119
117
  const docBrowser = useDocBrowser();
120
- const location = useLocation();
121
118
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
122
119
  const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
123
120
  const [draftLabel, setDraftLabel] = useState('');
@@ -129,7 +126,6 @@ export function ChatSidebar() {
129
126
  const { language, setLanguage } = useI18n();
130
127
  const { theme, setTheme } = useTheme();
131
128
  const updateSessionLabel = useChatSessionLabelService();
132
- const chatChain = resolveChatChain(location.search);
133
129
  const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
134
130
  const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
135
131
 
@@ -182,7 +178,6 @@ export function ChatSidebar() {
182
178
  setSavingSessionKey(session.key);
183
179
  try {
184
180
  await updateSessionLabel({
185
- chatChain,
186
181
  sessionKey: session.key,
187
182
  label: normalizedLabel || null
188
183
  });
@@ -0,0 +1,2 @@
1
+ ## 目录预算豁免
2
+ - 原因:聊天入口层需要并列承载页面壳、会话侧栏、会话偏好同步、展示组件与配套测试,且已通过 `ncp/` 等子目录承接更细分的实现,短期内仍需保留这组扁平入口文件。
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ DEFAULT_NCP_ATTACHMENT_MAX_BYTES,
4
+ uploadFilesAsNcpDraftAttachments
5
+ } from '../../../../ncp-packages/nextclaw-ncp-react/src/attachments/ncp-attachments.ts';
6
+
7
+ describe('ncp attachment upload limit', () => {
8
+ it('accepts files larger than the previous 10MB cap', async () => {
9
+ expect(DEFAULT_NCP_ATTACHMENT_MAX_BYTES).toBe(200 * 1024 * 1024);
10
+
11
+ const file = new File([new Uint8Array(12 * 1024 * 1024)], 'large-image.png', {
12
+ type: 'image/png'
13
+ });
14
+ const uploadBatch = vi.fn(async (files: File[]) =>
15
+ files.map((entry) => ({
16
+ id: entry.name,
17
+ name: entry.name,
18
+ mimeType: entry.type,
19
+ sizeBytes: entry.size,
20
+ assetUri: `asset://store/${entry.name}`,
21
+ }))
22
+ );
23
+
24
+ const result = await uploadFilesAsNcpDraftAttachments([file], {
25
+ uploadBatch
26
+ });
27
+
28
+ expect(result.rejected).toEqual([]);
29
+ expect(uploadBatch).toHaveBeenCalledOnce();
30
+ expect(uploadBatch).toHaveBeenCalledWith([file]);
31
+ expect(result.attachments).toEqual([
32
+ {
33
+ id: 'large-image.png',
34
+ name: 'large-image.png',
35
+ mimeType: 'image/png',
36
+ sizeBytes: 12 * 1024 * 1024,
37
+ assetUri: 'asset://store/large-image.png',
38
+ }
39
+ ]);
40
+ });
41
+ });
@@ -0,0 +1,9 @@
1
+ import type { SessionEntryView } from '@/api/types';
2
+
3
+ export function sessionDisplayName(session: SessionEntryView): string {
4
+ if (session.label && session.label.trim()) {
5
+ return session.label.trim();
6
+ }
7
+ const chunks = session.key.split(':');
8
+ return chunks[chunks.length - 1] || session.key;
9
+ }
@@ -1,12 +1,9 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
2
  import { toast } from 'sonner';
3
- import { updateSession } from '@/api/config';
4
3
  import { updateNcpSession } from '@/api/ncp-session';
5
- import type { ChatChain } from '@/components/chat/chat-chain';
6
4
  import { t } from '@/lib/i18n';
7
5
 
8
6
  type UpdateChatSessionLabelParams = {
9
- chatChain: ChatChain;
10
7
  sessionKey: string;
11
8
  label: string | null;
12
9
  };
@@ -16,15 +13,9 @@ export function useChatSessionLabelService() {
16
13
 
17
14
  return async (params: UpdateChatSessionLabelParams): Promise<void> => {
18
15
  try {
19
- if (params.chatChain === 'ncp') {
20
- await updateNcpSession(params.sessionKey, { label: params.label });
21
- queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
22
- queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
23
- } else {
24
- await updateSession(params.sessionKey, { label: params.label });
25
- queryClient.invalidateQueries({ queryKey: ['sessions'] });
26
- queryClient.invalidateQueries({ queryKey: ['session-history', params.sessionKey] });
27
- }
16
+ await updateNcpSession(params.sessionKey, { label: params.label });
17
+ queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
18
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
28
19
  toast.success(t('configSavedApplied'));
29
20
  } catch (error) {
30
21
  toast.error(t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)));
@@ -1,19 +1,16 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { updateSession } from '@/api/config';
2
+ import { updateNcpSession } from '@/api/ncp-session';
3
3
  import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
4
4
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
6
 
7
- vi.mock('@/api/config', () => ({
8
- updateSession: vi.fn(async () => ({
9
- key: 'session-1',
10
- totalMessages: 0,
11
- totalEvents: 0,
12
- sessionType: 'native',
13
- sessionTypeMutable: false,
14
- metadata: {},
15
- messages: [],
16
- events: []
7
+ vi.mock('@/api/ncp-session', () => ({
8
+ updateNcpSession: vi.fn(async () => ({
9
+ sessionId: 'session-1',
10
+ messageCount: 0,
11
+ updatedAt: new Date().toISOString(),
12
+ status: 'idle',
13
+ metadata: {}
17
14
  }))
18
15
  }));
19
16
 
@@ -50,10 +47,10 @@ describe('ChatSessionPreferenceSync', () => {
50
47
  }
51
48
  }));
52
49
 
53
- const sync = new ChatSessionPreferenceSync(updateSession);
50
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
54
51
  sync.syncSelectedSessionPreferences();
55
52
  await vi.waitFor(() => {
56
- expect(updateSession).toHaveBeenCalledWith('session-1', {
53
+ expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
57
54
  preferredModel: 'openai/gpt-5',
58
55
  preferredThinking: 'high'
59
56
  });
@@ -1,18 +1,9 @@
1
1
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
5
- import type {
6
- ChatRunView,
7
- ChatTurnStreamDeltaEvent,
8
- ChatTurnStreamReadyEvent,
9
- ChatTurnStreamSessionEvent,
10
- SessionMessageView,
11
- ThinkingLevel
12
- } from '@/api/types';
4
+ import type { ThinkingLevel } from '@/api/types';
13
5
 
14
6
  export type SendMessageParams = {
15
- runId?: string;
16
7
  message: string;
17
8
  sessionKey: string;
18
9
  agentId: string;
@@ -28,58 +19,14 @@ export type SendMessageParams = {
28
19
  composerNodes?: ChatComposerNode[];
29
20
  };
30
21
 
31
- export type ActiveRunState = {
32
- localRunId: number;
22
+ export type ResumeRunParams = {
33
23
  sessionKey: string;
34
- agentId?: string;
35
- backendRunId?: string;
36
- backendStopSupported: boolean;
37
- backendStopReason?: string;
38
- };
39
-
40
- export type StreamReadyPayload = {
41
- sessionKey: string;
42
- runId?: string;
43
- stopSupported?: boolean;
44
- stopReason?: string;
45
- requestedAt?: string;
46
- };
47
-
48
- export type StreamReadyEvent = ChatTurnStreamReadyEvent;
49
- export type StreamDeltaEvent = ChatTurnStreamDeltaEvent;
50
- export type StreamSessionEvent = ChatTurnStreamSessionEvent;
51
-
52
- export type NextbotAgentRunMetadata =
53
- | {
54
- driver: 'nextbot-stream';
55
- mode: 'send';
56
- payload: SendMessageParams;
57
- requestedSkills: string[];
58
- }
59
- | {
60
- driver: 'nextbot-stream';
61
- mode: 'resume';
62
- runId: string;
63
- fromEventIndex?: number;
64
- sessionKey?: string;
65
- agentId?: string;
66
- stopSupported?: boolean;
67
- stopReason?: string;
68
- };
69
-
70
- export type UseChatStreamControllerParams = {
71
- selectedSessionKeyRef: MutableRefObject<string | null>;
72
- setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
73
- setDraft: Dispatch<SetStateAction<string>>;
74
- setComposerNodes: Dispatch<SetStateAction<ChatComposerNode[]>>;
75
- refetchSessions: () => Promise<unknown>;
76
- refetchHistory: () => Promise<unknown>;
77
24
  };
78
25
 
79
26
  export type ChatStreamActions = {
80
27
  sendMessage: (payload: SendMessageParams) => Promise<void>;
81
28
  stopCurrentRun: () => Promise<void>;
82
- resumeRun: (run: ChatRunView) => Promise<void>;
29
+ resumeRun: (run: ResumeRunParams) => Promise<void>;
83
30
  resetStreamState: () => void;
84
- applyHistoryMessages: (messages: SessionMessageView[], options?: { isLoading?: boolean }) => void;
31
+ applyHistoryMessages: (messages: unknown[], options?: { isLoading?: boolean }) => void;
85
32
  };
@@ -8,15 +8,15 @@ import {
8
8
  import { useLocation, useNavigate, useParams } from 'react-router-dom';
9
9
  import { API_BASE } from '@/api/api-base';
10
10
  import { fetchNcpSessionMessages } from '@/api/ncp-session';
11
- import type { ChatRunView } from '@/api/types';
12
- import { sessionDisplayName } from '@/components/chat/chat-page-data';
13
11
  import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
12
+ import { sessionDisplayName } from '@/components/chat/chat-session-display';
14
13
  import { createNcpAppClientFetch } from '@/components/chat/ncp/ncp-app-client-fetch';
15
14
  import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
16
15
  import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
17
16
  import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
18
17
  import { adaptNcpMessagesToUiMessages, buildNcpSessionRunStatusByKey, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
19
18
  import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
19
+ import type { ResumeRunParams } from '@/components/chat/chat-stream/types';
20
20
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
21
21
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
22
22
  import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
@@ -224,7 +224,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
224
224
  await agent.abort();
225
225
  await sessionsQuery.refetch();
226
226
  },
227
- resumeRun: async (run: ChatRunView) => {
227
+ resumeRun: async (run: ResumeRunParams) => {
228
228
  if (run.sessionKey !== activeSessionId) {
229
229
  return;
230
230
  }
@@ -0,0 +1,77 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { useHydratedNcpAgent } from '../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-hydrated-ncp-agent.ts';
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ manager: {
7
+ reset: vi.fn(),
8
+ hydrate: vi.fn()
9
+ },
10
+ runtime: {
11
+ snapshot: {
12
+ messages: [],
13
+ streamingMessage: null,
14
+ activeRun: null,
15
+ error: null
16
+ },
17
+ visibleMessages: [],
18
+ activeRunId: null,
19
+ isRunning: false,
20
+ isSending: false,
21
+ send: vi.fn(),
22
+ abort: vi.fn(),
23
+ streamRun: vi.fn()
24
+ }
25
+ }));
26
+
27
+ vi.mock('../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-ncp-agent-runtime.js', () => ({
28
+ useScopedAgentManager: () => mocks.manager,
29
+ useNcpAgentRuntime: () => mocks.runtime
30
+ }));
31
+
32
+ describe('useHydratedNcpAgent', () => {
33
+ beforeEach(() => {
34
+ mocks.manager.reset.mockReset();
35
+ mocks.manager.hydrate.mockReset();
36
+ mocks.runtime.send.mockReset();
37
+ mocks.runtime.abort.mockReset();
38
+ mocks.runtime.streamRun.mockReset();
39
+ });
40
+
41
+ it('treats a newly selected session as hydrating immediately on rerender', async () => {
42
+ const client = {
43
+ stop: vi.fn().mockResolvedValue(undefined),
44
+ stream: vi.fn().mockResolvedValue(undefined)
45
+ } as never;
46
+ const loadSeed = vi
47
+ .fn()
48
+ .mockResolvedValueOnce({ messages: [], status: 'idle' })
49
+ .mockResolvedValueOnce({ messages: [], status: 'idle' });
50
+
51
+ const { result, rerender } = renderHook(
52
+ ({ sessionId }: { sessionId: string }) =>
53
+ useHydratedNcpAgent({
54
+ sessionId,
55
+ client,
56
+ loadSeed
57
+ }),
58
+ {
59
+ initialProps: {
60
+ sessionId: 'session-a'
61
+ }
62
+ }
63
+ );
64
+
65
+ await waitFor(() => {
66
+ expect(result.current.isHydrating).toBe(false);
67
+ });
68
+
69
+ rerender({ sessionId: 'session-b' });
70
+
71
+ expect(result.current.isHydrating).toBe(true);
72
+
73
+ await waitFor(() => {
74
+ expect(result.current.isHydrating).toBe(false);
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,2 @@
1
+ ## 目录预算豁免
2
+ - 原因:配置中心目录按配置面板维度组织,每个面板都需要独立入口、局部测试与装配文件;当前结构受 UI 信息架构约束,需要保留超过 `20` 个直接代码文件的扁平集合。