@nextclaw/ui 0.11.8 → 0.11.10

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 (57) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/assets/{ChannelsList-dsxeZk5v.js → ChannelsList-C63gOoYI.js} +3 -3
  3. package/dist/assets/ChatPage-Ci3Gz0qh.js +37 -0
  4. package/dist/assets/{DocBrowser-B9RfxWIh.js → DocBrowser-CI4jOzJY.js} +1 -1
  5. package/dist/assets/{LogoBadge-iVDzhkNu.js → LogoBadge-DImV63-L.js} +1 -1
  6. package/dist/assets/{MarketplacePage-4MZIcD0K.js → MarketplacePage-B360oSAV.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-cwLMty-D.js → McpMarketplacePage-KIQgx_7h.js} +2 -2
  8. package/dist/assets/{ModelConfig-CNICBWzw.js → ModelConfig-Ben3tQoX.js} +1 -1
  9. package/dist/assets/{ProvidersList-CEHGsRSL.js → ProvidersList-DE-S9mq0.js} +1 -1
  10. package/dist/assets/RemoteAccessPage-DxUia6R-.js +1 -0
  11. package/dist/assets/RuntimeConfig-CQcGfNZT.js +1 -0
  12. package/dist/assets/{SearchConfig-CaFAgBMN.js → SearchConfig-DeOa-M6j.js} +1 -1
  13. package/dist/assets/{SecretsConfig-DzWq8hGZ.js → SecretsConfig-Ci8pJmzd.js} +2 -2
  14. package/dist/assets/{SessionsConfig-CJJTcxyQ.js → SessionsConfig-B6zq55yu.js} +2 -2
  15. package/dist/assets/chat-session-display--oo5yuIw.js +1 -0
  16. package/dist/assets/{index-BlrweCCh.js → index-LhlkB00c.js} +4 -4
  17. package/dist/assets/{label-DtssWSI4.js → label-3TKt0PoZ.js} +1 -1
  18. package/dist/assets/{page-layout-DnRqSldv.js → page-layout-CopkIM3Q.js} +1 -1
  19. package/dist/assets/{popover-Un2VFGcS.js → popover-CUx8uRJw.js} +1 -1
  20. package/dist/assets/security-config-BL29kTzz.js +1 -0
  21. package/dist/assets/{skeleton-DTFzTqqO.js → skeleton-Bs4zvcql.js} +1 -1
  22. package/dist/assets/{status-dot-DOJX6vii.js → status-dot-D6vJMwD7.js} +1 -1
  23. package/dist/assets/{switch-utBdpBRv.js → switch-A3-ClT1P.js} +1 -1
  24. package/dist/assets/{tabs-custom-ExyfvfgG.js → tabs-custom-BVSd5urq.js} +1 -1
  25. package/dist/assets/{useConfirmDialog-DjNtKs4n.js → useConfirmDialog-ChPriea6.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/package.json +5 -5
  28. package/src/api/ncp-session-query-cache.test.ts +89 -0
  29. package/src/api/ncp-session-query-cache.ts +85 -0
  30. package/src/api/types.ts +2 -0
  31. package/src/components/chat/ChatConversationPanel.test.tsx +1 -1
  32. package/src/components/chat/ChatConversationPanel.tsx +62 -32
  33. package/src/components/chat/ChatSidebar.test.tsx +87 -92
  34. package/src/components/chat/ChatSidebar.tsx +21 -36
  35. package/src/components/chat/adapters/chat-message.adapter.ts +17 -5
  36. package/src/components/chat/chat-session-label.service.ts +3 -3
  37. package/src/components/chat/containers/chat-message-list.container.test.tsx +101 -0
  38. package/src/components/chat/containers/chat-message-list.container.tsx +93 -54
  39. package/src/components/chat/managers/chat-session-list.manager.ts +0 -18
  40. package/src/components/chat/ncp/NcpChatPage.tsx +4 -52
  41. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +4 -18
  42. package/src/components/chat/ncp/ncp-chat.presenter.ts +0 -2
  43. package/src/components/chat/ncp/ncp-session-adapter.test.ts +0 -23
  44. package/src/components/chat/ncp/ncp-session-adapter.ts +0 -19
  45. package/src/components/chat/ncp/use-ncp-session-list-view.ts +42 -0
  46. package/src/components/chat/presenter/chat-presenter-context.tsx +0 -3
  47. package/src/components/chat/stores/chat-session-list.store.ts +1 -7
  48. package/src/components/chat/stores/chat-thread.store.ts +3 -3
  49. package/src/hooks/use-realtime-query-bridge.ts +14 -19
  50. package/src/hooks/useConfig.ts +10 -11
  51. package/dist/assets/ChatPage-CWK4Bckz.js +0 -37
  52. package/dist/assets/RemoteAccessPage-uYxoaQ8V.js +0 -1
  53. package/dist/assets/RuntimeConfig-CYQq4S_m.js +0 -1
  54. package/dist/assets/ncp-session-adapter-C-jqQqcV.js +0 -1
  55. package/dist/assets/security-config-B7Bkebpm.js +0 -1
  56. package/src/components/chat/managers/chat-run-status.manager.ts +0 -32
  57. package/src/components/chat/stores/chat-run-status.store.ts +0 -30
@@ -1,82 +1,121 @@
1
1
  import { useMemo } from "react";
2
- import { type UiMessage } from "@nextclaw/agent-chat";
3
- import { ChatMessageList } from "@nextclaw/agent-chat-ui";
2
+ import type { NcpMessage } from "@nextclaw/ncp";
4
3
  import {
5
- adaptChatMessages,
4
+ type ChatMessageViewModel,
5
+ ChatMessageList,
6
+ } from "@nextclaw/agent-chat-ui";
7
+ import {
8
+ adaptChatMessage,
9
+ type ChatMessageAdapterTexts,
6
10
  type ChatMessageSource,
7
11
  } from "@/components/chat/adapters/chat-message.adapter";
12
+ import { adaptNcpMessageToUiMessage } from "@/components/chat/ncp/ncp-session-adapter";
8
13
  import { useI18n } from "@/components/providers/I18nProvider";
9
14
  import { formatDateTime, t } from "@/lib/i18n";
10
15
 
11
16
  type ChatMessageListContainerProps = {
12
- uiMessages: UiMessage[];
17
+ messages: readonly NcpMessage[];
13
18
  isSending: boolean;
14
19
  className?: string;
15
20
  };
16
21
 
22
+ const messageViewModelCache = new WeakMap<
23
+ NcpMessage,
24
+ { language: string; viewModel: ChatMessageViewModel }
25
+ >();
26
+
27
+ function buildChatMessageAdapterTexts(
28
+ language: string,
29
+ ): ChatMessageAdapterTexts {
30
+ void language;
31
+ return {
32
+ roleLabels: {
33
+ user: t("chatRoleUser"),
34
+ assistant: t("chatRoleAssistant"),
35
+ tool: t("chatRoleTool"),
36
+ system: t("chatRoleSystem"),
37
+ fallback: t("chatRoleMessage"),
38
+ },
39
+ reasoningLabel: t("chatReasoning"),
40
+ toolCallLabel: t("chatToolCall"),
41
+ toolResultLabel: t("chatToolResult"),
42
+ toolNoOutputLabel: t("chatToolNoOutput"),
43
+ toolOutputLabel: t("chatToolOutput"),
44
+ toolStatusPreparingLabel: t("chatToolStatusPreparing"),
45
+ toolStatusRunningLabel: t("chatToolStatusRunning"),
46
+ toolStatusCompletedLabel: t("chatToolStatusCompleted"),
47
+ toolStatusFailedLabel: t("chatToolStatusFailed"),
48
+ toolStatusCancelledLabel: t("chatToolStatusCancelled"),
49
+ imageAttachmentLabel: t("chatImageAttachment"),
50
+ fileAttachmentLabel: t("chatFileAttachment"),
51
+ unknownPartLabel: t("chatUnknownPart"),
52
+ };
53
+ }
54
+
55
+ function buildChatMessageTexts(language: string) {
56
+ void language;
57
+ return {
58
+ copyCodeLabel: t("chatCodeCopy"),
59
+ copiedCodeLabel: t("chatCodeCopied"),
60
+ typingLabel: t("chatTyping"),
61
+ };
62
+ }
63
+
17
64
  export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
18
65
  const { language } = useI18n();
19
- const sourceMessages = useMemo<ChatMessageSource[]>(
20
- () =>
21
- props.uiMessages.map((message) => ({
22
- id: message.id,
23
- role: message.role,
66
+ const texts = useMemo<ChatMessageAdapterTexts>(
67
+ () => buildChatMessageAdapterTexts(language),
68
+ [language],
69
+ );
70
+
71
+ const messages = useMemo(() => {
72
+ return props.messages.map((message) => {
73
+ const cached = messageViewModelCache.get(message);
74
+ if (cached && cached.language === language) {
75
+ return cached.viewModel;
76
+ }
77
+
78
+ const uiMessage = adaptNcpMessageToUiMessage(message);
79
+ const sourceMessage: ChatMessageSource = {
80
+ id: uiMessage.id,
81
+ role: uiMessage.role,
24
82
  meta: {
25
- timestamp: message.meta?.timestamp,
26
- status: message.meta?.status,
83
+ timestamp: uiMessage.meta?.timestamp,
84
+ status: uiMessage.meta?.status,
27
85
  },
28
- parts: message.parts as unknown as ChatMessageSource["parts"],
29
- })),
30
- [props.uiMessages],
31
- );
86
+ parts: uiMessage.parts as unknown as ChatMessageSource["parts"],
87
+ };
88
+ const viewModel = adaptChatMessage(sourceMessage, {
89
+ formatTimestamp: (value) => formatDateTime(value, language),
90
+ texts,
91
+ });
92
+
93
+ messageViewModelCache.set(message, { language, viewModel });
94
+ return viewModel;
95
+ });
96
+ }, [language, props.messages, texts]);
32
97
 
33
- const messages = useMemo(
98
+ const hasAssistantDraft = useMemo(
34
99
  () =>
35
- adaptChatMessages({
36
- uiMessages: sourceMessages,
37
- formatTimestamp: (value) => formatDateTime(value, language),
38
- texts: {
39
- roleLabels: {
40
- user: t("chatRoleUser"),
41
- assistant: t("chatRoleAssistant"),
42
- tool: t("chatRoleTool"),
43
- system: t("chatRoleSystem"),
44
- fallback: t("chatRoleMessage"),
45
- },
46
- reasoningLabel: t("chatReasoning"),
47
- toolCallLabel: t("chatToolCall"),
48
- toolResultLabel: t("chatToolResult"),
49
- toolNoOutputLabel: t("chatToolNoOutput"),
50
- toolOutputLabel: t("chatToolOutput"),
51
- toolStatusPreparingLabel: t("chatToolStatusPreparing"),
52
- toolStatusRunningLabel: t("chatToolStatusRunning"),
53
- toolStatusCompletedLabel: t("chatToolStatusCompleted"),
54
- toolStatusFailedLabel: t("chatToolStatusFailed"),
55
- toolStatusCancelledLabel: t("chatToolStatusCancelled"),
56
- imageAttachmentLabel: t("chatImageAttachment"),
57
- fileAttachmentLabel: t("chatFileAttachment"),
58
- unknownPartLabel: t("chatUnknownPart"),
59
- },
60
- }),
61
- [language, sourceMessages],
100
+ messages.some(
101
+ (message) =>
102
+ message.role === "assistant" &&
103
+ (message.status === "streaming" || message.status === "pending"),
104
+ ),
105
+ [messages],
106
+ );
107
+ const messageTexts = useMemo(
108
+ () => buildChatMessageTexts(language),
109
+ [language],
62
110
  );
63
111
 
64
112
  return (
65
113
  <ChatMessageList
66
114
  messages={messages}
67
115
  isSending={props.isSending}
68
- hasAssistantDraft={props.uiMessages.some(
69
- (message) =>
70
- message.role === "assistant" &&
71
- (message.meta?.status === "streaming" ||
72
- message.meta?.status === "pending"),
73
- )}
116
+ hasAssistantDraft={hasAssistantDraft}
74
117
  className={props.className}
75
- texts={{
76
- copyCodeLabel: t("chatCodeCopy"),
77
- copiedCodeLabel: t("chatCodeCopied"),
78
- typingLabel: t("chatTyping"),
79
- }}
118
+ texts={messageTexts}
80
119
  />
81
120
  );
82
121
  }
@@ -1,7 +1,6 @@
1
1
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
2
2
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
3
3
  import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
4
- import type { ChatSessionListSnapshot } from '@/components/chat/stores/chat-session-list.store';
5
4
  import type { SetStateAction } from 'react';
6
5
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
7
6
 
@@ -11,16 +10,6 @@ export class ChatSessionListManager {
11
10
  private streamActionsManager: ChatStreamActionsManager
12
11
  ) {}
13
12
 
14
- private hasSnapshotChanges = (patch: Partial<ChatSessionListSnapshot>): boolean => {
15
- const current = useChatSessionListStore.getState().snapshot;
16
- for (const [key, value] of Object.entries(patch) as Array<[keyof ChatSessionListSnapshot, ChatSessionListSnapshot[keyof ChatSessionListSnapshot]]>) {
17
- if (!Object.is(current[key], value)) {
18
- return true;
19
- }
20
- }
21
- return false;
22
- };
23
-
24
13
  private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
25
14
  if (typeof next === 'function') {
26
15
  return (next as (value: T) => T)(prev);
@@ -28,13 +17,6 @@ export class ChatSessionListManager {
28
17
  return next;
29
18
  };
30
19
 
31
- syncSnapshot = (patch: Partial<ChatSessionListSnapshot>) => {
32
- if (!this.hasSnapshotChanges(patch)) {
33
- return;
34
- }
35
- useChatSessionListStore.getState().setSnapshot(patch);
36
- };
37
-
38
20
  setSelectedAgentId = (next: SetStateAction<string>) => {
39
21
  const prev = useChatSessionListStore.getState().snapshot.selectedAgentId;
40
22
  const value = this.resolveUpdateValue(prev, next);
@@ -14,7 +14,7 @@ import { createNcpAppClientFetch } from '@/components/chat/ncp/ncp-app-client-fe
14
14
  import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
15
15
  import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
16
16
  import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
17
- import { adaptNcpMessagesToUiMessages, buildNcpSessionRunStatusByKey, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
17
+ import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
18
18
  import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
19
19
  import type { ResumeRunParams } from '@/components/chat/chat-stream/types';
20
20
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
@@ -74,12 +74,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
74
74
  [routeSessionIdParam]
75
75
  );
76
76
  const {
77
- sessionsQuery,
78
77
  installedSkillsQuery,
79
78
  isProviderStateResolved,
80
79
  modelOptions,
81
80
  sessionSummaries,
82
- sessions,
83
81
  skillRecords,
84
82
  selectedSession,
85
83
  sessionTypeOptions,
@@ -97,7 +95,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
97
95
  setSelectedModel: presenter.chatInputManager.setSelectedModel,
98
96
  setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
99
97
  });
100
- const refetchSessions = sessionsQuery.refetch;
101
98
 
102
99
  const activeSessionId = selectedSessionKey ?? draftSessionId;
103
100
  const sessionSummariesRef = useRef(sessionSummaries);
@@ -151,32 +148,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
151
148
  }
152
149
  }, [presenter, selectedSessionKey]);
153
150
 
154
- const uiMessages = useMemo(
155
- () => adaptNcpMessagesToUiMessages(agent.visibleMessages),
156
- [agent.visibleMessages]
157
- );
158
151
  const isSending = agent.isSending || agent.isRunning;
159
152
  const isAwaitingAssistantOutput = agent.isRunning;
160
153
  const canStopCurrentRun = agent.isRunning;
161
154
  const stopDisabledReason = agent.isRunning ? null : '__preparing__';
162
155
  const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
163
- const activeBackendRunId = agent.activeRunId;
164
- const sessionRunStatusByKey = useMemo(
165
- () =>
166
- buildNcpSessionRunStatusByKey({
167
- summaries: sessionSummaries,
168
- activeSessionId,
169
- isLocallyRunning: isSending || Boolean(activeBackendRunId)
170
- }),
171
- [activeBackendRunId, activeSessionId, isSending, sessionSummaries]
172
- );
173
-
174
- useEffect(() => {
175
- if (!isSending && !activeBackendRunId) {
176
- return;
177
- }
178
- void refetchSessions();
179
- }, [activeBackendRunId, isSending, refetchSessions]);
180
156
 
181
157
  useEffect(() => {
182
158
  presenter.chatStreamActionsManager.bind({
@@ -201,9 +177,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
201
177
  return;
202
178
  }
203
179
  try {
204
- void sessionsQuery.refetch();
205
180
  await agent.send(envelope);
206
- await sessionsQuery.refetch();
207
181
  } catch (error) {
208
182
  if (payload.restoreDraftOnError) {
209
183
  if (payload.composerNodes && payload.composerNodes.length > 0) {
@@ -222,7 +196,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
222
196
  },
223
197
  stopCurrentRun: async () => {
224
198
  await agent.abort();
225
- await sessionsQuery.refetch();
226
199
  },
227
200
  resumeRun: async (run: ResumeRunParams) => {
228
201
  if (run.sessionKey !== activeSessionId) {
@@ -235,7 +208,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
235
208
  },
236
209
  applyHistoryMessages: () => {}
237
210
  });
238
- }, [activeSessionId, agent, presenter, sessionsQuery]);
211
+ }, [activeSessionId, agent, presenter]);
239
212
 
240
213
  useChatSessionSync({
241
214
  view,
@@ -264,12 +237,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
264
237
  sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
265
238
  resolveSessionTypeLabel(selectedSessionType);
266
239
 
267
- useEffect(() => {
268
- presenter.chatThreadManager.bindActions({
269
- refetchSessions: sessionsQuery.refetch
270
- });
271
- }, [presenter, sessionsQuery.refetch]);
272
-
273
240
  useEffect(() => {
274
241
  presenter.chatInputManager.syncSnapshot({
275
242
  isProviderStateResolved,
@@ -288,16 +255,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
288
255
  skillRecords,
289
256
  isSkillsLoading: installedSkillsQuery.isLoading
290
257
  });
291
- presenter.chatSessionListManager.syncSnapshot({
292
- sessions,
293
- query,
294
- isLoading: sessionsQuery.isLoading
295
- });
296
- presenter.chatRunStatusManager.syncSnapshot({
297
- sessionRunStatusByKey,
298
- isLocallyRunning: isSending || Boolean(activeBackendRunId),
299
- activeBackendRunId
300
- });
301
258
  presenter.chatThreadManager.syncSnapshot({
302
259
  isProviderStateResolved,
303
260
  modelOptions,
@@ -309,12 +266,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
309
266
  canDeleteSession: Boolean(selectedSession),
310
267
  threadRef,
311
268
  isHistoryLoading: agent.isHydrating,
312
- uiMessages,
269
+ messages: agent.visibleMessages,
313
270
  isSending,
314
271
  isAwaitingAssistantOutput
315
272
  });
316
273
  }, [
317
- activeBackendRunId,
318
274
  agent.isHydrating,
319
275
  canEditSessionType,
320
276
  canStopCurrentRun,
@@ -329,20 +285,16 @@ export function NcpChatPage({ view }: ChatPageProps) {
329
285
  modelOptions.length,
330
286
  modelOptions,
331
287
  presenter,
332
- query,
333
288
  selectedSession,
334
289
  selectedSessionKey,
335
290
  selectedSessionType,
336
- sessionRunStatusByKey,
337
291
  sessionTypeOptions,
338
292
  sessionTypeUnavailable,
339
293
  sessionTypeUnavailableMessage,
340
- sessions,
341
- sessionsQuery.isLoading,
342
294
  skillRecords,
343
295
  stopDisabledReason,
344
296
  threadRef,
345
- uiMessages
297
+ agent.visibleMessages
346
298
  ]);
347
299
 
348
300
  return (
@@ -1,3 +1,5 @@
1
+ import { appQueryClient } from '@/app-query-client';
2
+ import { deleteNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
1
3
  import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
2
4
  import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
5
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
@@ -7,30 +9,13 @@ import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.st
7
9
  import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
8
10
  import { t } from '@/lib/i18n';
9
11
 
10
- export type NcpChatThreadManagerActions = {
11
- refetchSessions: () => Promise<unknown>;
12
- };
13
-
14
- const noopAsync = async () => {};
15
-
16
12
  export class NcpChatThreadManager {
17
- private actions: NcpChatThreadManagerActions = {
18
- refetchSessions: noopAsync
19
- };
20
-
21
13
  constructor(
22
14
  private uiManager: ChatUiManager,
23
15
  private sessionListManager: ChatSessionListManager,
24
16
  private streamActionsManager: ChatStreamActionsManager
25
17
  ) {}
26
18
 
27
- bindActions = (patch: Partial<NcpChatThreadManagerActions>) => {
28
- this.actions = {
29
- ...this.actions,
30
- ...patch
31
- };
32
- };
33
-
34
19
  private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
35
20
  const current = useChatThreadStore.getState().snapshot;
36
21
  for (const [key, value] of Object.entries(patch) as Array<[keyof ChatThreadSnapshot, ChatThreadSnapshot[keyof ChatThreadSnapshot]]>) {
@@ -78,9 +63,10 @@ export class NcpChatThreadManager {
78
63
  useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
79
64
  try {
80
65
  await deleteNcpSessionApi(selectedSessionKey);
66
+ deleteNcpSessionSummaryInQueryClient(appQueryClient, selectedSessionKey);
67
+ appQueryClient.removeQueries({ queryKey: ['ncp-session-messages', selectedSessionKey] });
81
68
  this.streamActionsManager.resetStreamState();
82
69
  this.uiManager.goToChatRoot({ replace: true });
83
- await this.actions.refetchSessions();
84
70
  } finally {
85
71
  useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
86
72
  }
@@ -1,4 +1,3 @@
1
- import { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
2
1
  import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
2
  import { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
4
3
  import { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
@@ -14,7 +13,6 @@ export class NcpChatPresenter {
14
13
  this.chatStreamActionsManager,
15
14
  () => this.getDraftSessionId()
16
15
  );
17
- chatRunStatusManager = new ChatRunStatusManager();
18
16
  chatThreadManager = new NcpChatThreadManager(
19
17
  this.chatUiManager,
20
18
  this.chatSessionListManager,
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  adaptNcpMessageToUiMessage,
3
3
  adaptNcpSessionSummary,
4
- buildNcpSessionRunStatusByKey,
5
4
  readNcpSessionPreferredThinking
6
5
  } from '@/components/chat/ncp/ncp-session-adapter';
7
6
  import type { NcpSessionSummaryView } from '@/api/types';
@@ -95,25 +94,3 @@ describe('readNcpSessionPreferredThinking', () => {
95
94
  expect(thinking).toBe('high');
96
95
  });
97
96
  });
98
-
99
- describe('buildNcpSessionRunStatusByKey', () => {
100
- it('marks the active local session as running before the server summary catches up', () => {
101
- const statuses = buildNcpSessionRunStatusByKey({
102
- summaries: [createSummary({ sessionId: 'ncp-session-1', status: 'idle' })],
103
- activeSessionId: 'ncp-session-1',
104
- isLocallyRunning: true
105
- });
106
-
107
- expect(statuses.get('ncp-session-1')).toBe('running');
108
- });
109
-
110
- it('keeps persisted running sessions marked as running', () => {
111
- const statuses = buildNcpSessionRunStatusByKey({
112
- summaries: [createSummary({ sessionId: 'ncp-session-2', status: 'running' })],
113
- activeSessionId: null,
114
- isLocallyRunning: false
115
- });
116
-
117
- expect(statuses.get('ncp-session-2')).toBe('running');
118
- });
119
- });
@@ -1,7 +1,6 @@
1
1
  import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
4
- import type { SessionRunStatus } from '@/lib/session-run-status';
5
4
 
6
5
  const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
7
6
 
@@ -206,24 +205,6 @@ export function adaptNcpSessionSummaries(summaries: NcpSessionSummaryView[]): Se
206
205
  return summaries.map(adaptNcpSessionSummary);
207
206
  }
208
207
 
209
- export function buildNcpSessionRunStatusByKey(params: {
210
- summaries: readonly NcpSessionSummaryView[];
211
- activeSessionId?: string | null;
212
- isLocallyRunning?: boolean;
213
- }): Map<string, SessionRunStatus> {
214
- const map = new Map<string, SessionRunStatus>();
215
- for (const summary of params.summaries) {
216
- if (summary.status === 'running') {
217
- map.set(summary.sessionId, 'running');
218
- }
219
- }
220
- const activeSessionId = readOptionalString(params.activeSessionId);
221
- if (params.isLocallyRunning && activeSessionId) {
222
- map.set(activeSessionId, 'running');
223
- }
224
- return map;
225
- }
226
-
227
208
  export function createNcpSessionId(): string {
228
209
  return `ncp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
229
210
  }
@@ -0,0 +1,42 @@
1
+ import { useMemo } from 'react';
2
+ import type { SessionEntryView } from '@/api/types';
3
+ import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
4
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
+ import { useNcpSessions } from '@/hooks/useConfig';
6
+ import type { SessionRunStatus } from '@/lib/session-run-status';
7
+
8
+ export type NcpSessionListItemView = {
9
+ session: SessionEntryView;
10
+ runStatus?: SessionRunStatus;
11
+ };
12
+
13
+ function filterSessionsByQuery(sessions: readonly SessionEntryView[], query: string): SessionEntryView[] {
14
+ const normalizedQuery = query.trim().toLowerCase();
15
+ if (!normalizedQuery) {
16
+ return [...sessions];
17
+ }
18
+
19
+ return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
20
+ }
21
+
22
+ export function useNcpSessionListView(params: { limit?: number } = {}) {
23
+ const query = useChatSessionListStore((state) => state.snapshot.query);
24
+ const sessionsQuery = useNcpSessions({ limit: params.limit ?? 200 });
25
+
26
+ const items = useMemo<NcpSessionListItemView[]>(() => {
27
+ const summaries = sessionsQuery.data?.sessions ?? [];
28
+ const sessions = adaptNcpSessionSummaries(summaries);
29
+ const filteredSessions = filterSessionsByQuery(sessions, query);
30
+ const summaryBySessionId = new Map(summaries.map((summary) => [summary.sessionId, summary]));
31
+
32
+ return filteredSessions.map((session) => ({
33
+ session,
34
+ runStatus: summaryBySessionId.get(session.key)?.status === 'running' ? 'running' : undefined
35
+ }));
36
+ }, [query, sessionsQuery.data?.sessions]);
37
+
38
+ return {
39
+ isLoading: sessionsQuery.isLoading,
40
+ items
41
+ };
42
+ }
@@ -3,7 +3,6 @@ import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
3
3
  import { createContext, useContext } from 'react';
4
4
  import type { ReactNode } from 'react';
5
5
  import type { SetStateAction } from 'react';
6
- import type { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
7
6
  import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
8
7
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
9
8
  import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
@@ -33,7 +32,6 @@ export type ChatInputManagerLike = {
33
32
  };
34
33
 
35
34
  export type ChatThreadManagerLike = {
36
- bindActions: (patch: { refetchSessions?: () => Promise<unknown> }) => void;
37
35
  syncSnapshot: (patch: Partial<ChatThreadSnapshot>) => void;
38
36
  deleteSession: () => void;
39
37
  createSession: () => void;
@@ -45,7 +43,6 @@ export type ChatPresenterLike = {
45
43
  chatStreamActionsManager: ChatStreamActionsManager;
46
44
  chatInputManager: ChatInputManagerLike;
47
45
  chatSessionListManager: ChatSessionListManager;
48
- chatRunStatusManager: ChatRunStatusManager;
49
46
  chatThreadManager: ChatThreadManagerLike;
50
47
  };
51
48
 
@@ -1,12 +1,8 @@
1
1
  import { create } from 'zustand';
2
- import type { SessionEntryView } from '@/api/types';
3
-
4
2
  export type ChatSessionListSnapshot = {
5
- sessions: SessionEntryView[];
6
3
  selectedSessionKey: string | null;
7
4
  selectedAgentId: string;
8
5
  query: string;
9
- isLoading: boolean;
10
6
  };
11
7
 
12
8
  type ChatSessionListStore = {
@@ -15,11 +11,9 @@ type ChatSessionListStore = {
15
11
  };
16
12
 
17
13
  const initialSnapshot: ChatSessionListSnapshot = {
18
- sessions: [],
19
14
  selectedSessionKey: null,
20
15
  selectedAgentId: 'main',
21
- query: '',
22
- isLoading: false
16
+ query: ''
23
17
  };
24
18
 
25
19
  export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
@@ -1,6 +1,6 @@
1
1
  import { create } from 'zustand';
2
2
  import type { MutableRefObject } from 'react';
3
- import type { UiMessage } from '@nextclaw/agent-chat';
3
+ import type { NcpMessage } from '@nextclaw/ncp';
4
4
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
5
 
6
6
  export type ChatThreadSnapshot = {
@@ -15,7 +15,7 @@ export type ChatThreadSnapshot = {
15
15
  isDeletePending: boolean;
16
16
  threadRef: MutableRefObject<HTMLDivElement | null> | null;
17
17
  isHistoryLoading: boolean;
18
- uiMessages: UiMessage[];
18
+ messages: readonly NcpMessage[];
19
19
  isSending: boolean;
20
20
  isAwaitingAssistantOutput: boolean;
21
21
  };
@@ -37,7 +37,7 @@ const initialSnapshot: ChatThreadSnapshot = {
37
37
  isDeletePending: false,
38
38
  threadRef: null,
39
39
  isHistoryLoading: false,
40
- uiMessages: [],
40
+ messages: [],
41
41
  isSending: false,
42
42
  isAwaitingAssistantOutput: false
43
43
  };