@nextclaw/ui 0.11.9 → 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 (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{ChannelsList-XGMfinnc.js → ChannelsList-C63gOoYI.js} +3 -3
  3. package/dist/assets/ChatPage-Ci3Gz0qh.js +37 -0
  4. package/dist/assets/{DocBrowser-DTRCNsSM.js → DocBrowser-CI4jOzJY.js} +1 -1
  5. package/dist/assets/{LogoBadge-CPMOwWdA.js → LogoBadge-DImV63-L.js} +1 -1
  6. package/dist/assets/{MarketplacePage-De2qZ9C0.js → MarketplacePage-B360oSAV.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-cjVKSQ2f.js → McpMarketplacePage-KIQgx_7h.js} +2 -2
  8. package/dist/assets/{ModelConfig-CMn3-VZk.js → ModelConfig-Ben3tQoX.js} +1 -1
  9. package/dist/assets/{ProvidersList-CArDOswN.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-a38m8Ynx.js → SearchConfig-DeOa-M6j.js} +1 -1
  13. package/dist/assets/{SecretsConfig-B6mf4JY9.js → SecretsConfig-Ci8pJmzd.js} +2 -2
  14. package/dist/assets/{SessionsConfig-B_WQ1lVd.js → SessionsConfig-B6zq55yu.js} +2 -2
  15. package/dist/assets/chat-session-display--oo5yuIw.js +1 -0
  16. package/dist/assets/{index-D-wEIgPn.js → index-LhlkB00c.js} +4 -4
  17. package/dist/assets/{label-usOOP7mv.js → label-3TKt0PoZ.js} +1 -1
  18. package/dist/assets/{page-layout-CuIf20mx.js → page-layout-CopkIM3Q.js} +1 -1
  19. package/dist/assets/{popover-CTtTCP5d.js → popover-CUx8uRJw.js} +1 -1
  20. package/dist/assets/security-config-BL29kTzz.js +1 -0
  21. package/dist/assets/{skeleton-BNUaFYE7.js → skeleton-Bs4zvcql.js} +1 -1
  22. package/dist/assets/{status-dot-BeHTBy9k.js → status-dot-D6vJMwD7.js} +1 -1
  23. package/dist/assets/{switch-CtNnWZpa.js → switch-A3-ClT1P.js} +1 -1
  24. package/dist/assets/{tabs-custom-Dz_4tV62.js → tabs-custom-BVSd5urq.js} +1 -1
  25. package/dist/assets/{useConfirmDialog-C_n_JIEq.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 +6 -6
  33. package/src/components/chat/ChatSidebar.test.tsx +87 -92
  34. package/src/components/chat/ChatSidebar.tsx +21 -36
  35. package/src/components/chat/chat-session-label.service.ts +3 -3
  36. package/src/components/chat/containers/chat-message-list.container.test.tsx +53 -8
  37. package/src/components/chat/containers/chat-message-list.container.tsx +15 -14
  38. package/src/components/chat/managers/chat-session-list.manager.ts +0 -18
  39. package/src/components/chat/ncp/NcpChatPage.tsx +4 -52
  40. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +4 -18
  41. package/src/components/chat/ncp/ncp-chat.presenter.ts +0 -2
  42. package/src/components/chat/ncp/ncp-session-adapter.test.ts +0 -23
  43. package/src/components/chat/ncp/ncp-session-adapter.ts +0 -19
  44. package/src/components/chat/ncp/use-ncp-session-list-view.ts +42 -0
  45. package/src/components/chat/presenter/chat-presenter-context.tsx +0 -3
  46. package/src/components/chat/stores/chat-session-list.store.ts +1 -7
  47. package/src/components/chat/stores/chat-thread.store.ts +3 -3
  48. package/src/hooks/use-realtime-query-bridge.ts +14 -19
  49. package/src/hooks/useConfig.ts +10 -11
  50. package/dist/assets/ChatPage-DYTcCRPp.js +0 -37
  51. package/dist/assets/RemoteAccessPage-C0I4tHey.js +0 -1
  52. package/dist/assets/RuntimeConfig-B4o6uJq9.js +0 -1
  53. package/dist/assets/ncp-session-adapter-DSacECph.js +0 -1
  54. package/dist/assets/security-config-Bxrrv8Ac.js +0 -1
  55. package/src/components/chat/managers/chat-run-status.manager.ts +0 -32
  56. package/src/components/chat/stores/chat-run-status.store.ts +0 -30
@@ -8,9 +8,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
8
8
  import { SelectItem } from '@/components/ui/select';
9
9
  import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
10
10
  import { useChatSessionLabelService } from '@/components/chat/chat-session-label.service';
11
+ import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
11
12
  import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
12
13
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
13
- import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
14
14
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
15
15
  import { cn } from '@/lib/utils';
16
16
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
@@ -35,38 +35,39 @@ import {
35
35
 
36
36
  type DateGroup = {
37
37
  label: string;
38
- sessions: SessionEntryView[];
38
+ items: NcpSessionListItemView[];
39
39
  };
40
40
 
41
- function groupSessionsByDate(sessions: SessionEntryView[]): DateGroup[] {
41
+ function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
42
42
  const now = new Date();
43
43
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
44
44
  const yesterdayStart = todayStart - 86_400_000;
45
45
  const sevenDaysStart = todayStart - 7 * 86_400_000;
46
46
 
47
- const today: SessionEntryView[] = [];
48
- const yesterday: SessionEntryView[] = [];
49
- const previous7: SessionEntryView[] = [];
50
- const older: SessionEntryView[] = [];
47
+ const today: NcpSessionListItemView[] = [];
48
+ const yesterday: NcpSessionListItemView[] = [];
49
+ const previous7: NcpSessionListItemView[] = [];
50
+ const older: NcpSessionListItemView[] = [];
51
51
 
52
- for (const session of sessions) {
52
+ for (const item of items) {
53
+ const { session } = item;
53
54
  const ts = new Date(session.updatedAt).getTime();
54
55
  if (ts >= todayStart) {
55
- today.push(session);
56
+ today.push(item);
56
57
  } else if (ts >= yesterdayStart) {
57
- yesterday.push(session);
58
+ yesterday.push(item);
58
59
  } else if (ts >= sevenDaysStart) {
59
- previous7.push(session);
60
+ previous7.push(item);
60
61
  } else {
61
- older.push(session);
62
+ older.push(item);
62
63
  }
63
64
  }
64
65
 
65
66
  const groups: DateGroup[] = [];
66
- if (today.length > 0) groups.push({ label: t('chatSidebarToday'), sessions: today });
67
- if (yesterday.length > 0) groups.push({ label: t('chatSidebarYesterday'), sessions: yesterday });
68
- if (previous7.length > 0) groups.push({ label: t('chatSidebarPrevious7Days'), sessions: previous7 });
69
- if (older.length > 0) groups.push({ label: t('chatSidebarOlder'), sessions: older });
67
+ if (today.length > 0) groups.push({ label: t('chatSidebarToday'), items: today });
68
+ if (yesterday.length > 0) groups.push({ label: t('chatSidebarYesterday'), items: yesterday });
69
+ if (previous7.length > 0) groups.push({ label: t('chatSidebarPrevious7Days'), items: previous7 });
70
+ if (older.length > 0) groups.push({ label: t('chatSidebarOlder'), items: older });
70
71
  return groups;
71
72
  }
72
73
 
@@ -121,15 +122,15 @@ export function ChatSidebar() {
121
122
  const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
122
123
  const inputSnapshot = useChatInputStore((state) => state.snapshot);
123
124
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
124
- const runSnapshot = useChatRunStatusStore((state) => state.snapshot);
125
125
  const connectionStatus = useUiStore((state) => state.connectionStatus);
126
+ const { isLoading, items } = useNcpSessionListView();
126
127
  const { language, setLanguage } = useI18n();
127
128
  const { theme, setTheme } = useTheme();
128
129
  const updateSessionLabel = useChatSessionLabelService();
129
130
  const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
130
131
  const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
131
132
 
132
- const groups = useMemo(() => groupSessionsByDate(listSnapshot.sessions), [listSnapshot.sessions]);
133
+ const groups = useMemo(() => groupSessionsByDate(items), [items]);
133
134
  const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
134
135
  const nonDefaultSessionTypeOptions = useMemo(
135
136
  () => inputSnapshot.sessionTypeOptions.filter((option) => option.value !== defaultSessionType),
@@ -142,20 +143,6 @@ export function ChatSidebar() {
142
143
  window.location.reload();
143
144
  };
144
145
 
145
- const patchSessionLabelInStore = (sessionKey: string, label: string | undefined) => {
146
- const { sessions } = useChatSessionListStore.getState().snapshot;
147
- useChatSessionListStore.getState().setSnapshot({
148
- sessions: sessions.map((session) =>
149
- session.key === sessionKey
150
- ? {
151
- ...session,
152
- ...(label ? { label } : { label: undefined })
153
- }
154
- : session
155
- )
156
- });
157
- };
158
-
159
146
  const startEditingSessionLabel = (session: SessionEntryView) => {
160
147
  setEditingSessionKey(session.key);
161
148
  setDraftLabel(session.label?.trim() ?? '');
@@ -181,7 +168,6 @@ export function ChatSidebar() {
181
168
  sessionKey: session.key,
182
169
  label: normalizedLabel || null
183
170
  });
184
- patchSessionLabelInStore(session.key, normalizedLabel || undefined);
185
171
  cancelEditingSessionLabel();
186
172
  } catch {
187
173
  setSavingSessionKey(null);
@@ -297,7 +283,7 @@ export function ChatSidebar() {
297
283
  <div className="mx-4 border-t border-gray-200/60" />
298
284
 
299
285
  <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
300
- {listSnapshot.isLoading ? (
286
+ {isLoading ? (
301
287
  <div className="text-xs text-gray-500 p-3">{t('sessionsLoading')}</div>
302
288
  ) : groups.length === 0 ? (
303
289
  <div className="p-4 text-center">
@@ -312,9 +298,8 @@ export function ChatSidebar() {
312
298
  {group.label}
313
299
  </div>
314
300
  <div className="space-y-0.5">
315
- {group.sessions.map((session) => {
301
+ {group.items.map(({ session, runStatus }) => {
316
302
  const active = listSnapshot.selectedSessionKey === session.key;
317
- const runStatus = runSnapshot.sessionRunStatusByKey.get(session.key);
318
303
  const sessionTypeLabel = resolveSessionTypeLabel(session.sessionType, inputSnapshot.sessionTypeOptions);
319
304
  const isEditing = editingSessionKey === session.key;
320
305
  const isSaving = savingSessionKey === session.key;
@@ -1,5 +1,6 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
2
  import { toast } from 'sonner';
3
+ import { upsertNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
3
4
  import { updateNcpSession } from '@/api/ncp-session';
4
5
  import { t } from '@/lib/i18n';
5
6
 
@@ -13,9 +14,8 @@ export function useChatSessionLabelService() {
13
14
 
14
15
  return async (params: UpdateChatSessionLabelParams): Promise<void> => {
15
16
  try {
16
- await updateNcpSession(params.sessionKey, { label: params.label });
17
- queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
18
- queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
17
+ const updated = await updateNcpSession(params.sessionKey, { label: params.label });
18
+ upsertNcpSessionSummaryInQueryClient(queryClient, updated);
19
19
  toast.success(t('configSavedApplied'));
20
20
  } catch (error) {
21
21
  toast.error(t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)));
@@ -1,5 +1,5 @@
1
1
  import { render } from "@testing-library/react";
2
- import type { UiMessage } from "@nextclaw/agent-chat";
2
+ import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import { beforeEach, expect, it, vi } from "vitest";
4
4
  import { ChatMessageListContainer } from "./chat-message-list.container";
5
5
 
@@ -30,23 +30,22 @@ beforeEach(() => {
30
30
  it("reuses adapted message references when the source message object is unchanged", () => {
31
31
  const message = {
32
32
  id: "assistant-1",
33
+ sessionId: "session-1",
33
34
  role: "assistant",
34
- meta: {
35
- timestamp: "2026-03-17T10:00:00.000Z",
36
- status: "streaming",
37
- },
35
+ status: "streaming",
36
+ timestamp: "2026-03-17T10:00:00.000Z",
38
37
  parts: [{ type: "text", text: "hello" }],
39
- } satisfies UiMessage;
38
+ } satisfies NcpMessage;
40
39
 
41
40
  const { rerender } = render(
42
- <ChatMessageListContainer uiMessages={[message]} isSending={false} />,
41
+ <ChatMessageListContainer messages={[message]} isSending={false} />,
43
42
  );
44
43
 
45
44
  const firstMessages =
46
45
  captures.renders[captures.renders.length - 1]?.messages ?? [];
47
46
 
48
47
  rerender(
49
- <ChatMessageListContainer uiMessages={[message]} isSending={false} />,
48
+ <ChatMessageListContainer messages={[message]} isSending={false} />,
50
49
  );
51
50
 
52
51
  const secondMessages =
@@ -54,3 +53,49 @@ it("reuses adapted message references when the source message object is unchange
54
53
 
55
54
  expect(secondMessages[0]).toBe(firstMessages[0]);
56
55
  });
56
+
57
+ it("keeps historical adapted message references stable when only the streaming message changes", () => {
58
+ const historicalMessage = {
59
+ id: "assistant-1",
60
+ sessionId: "session-1",
61
+ role: "assistant",
62
+ status: "final",
63
+ timestamp: "2026-03-17T10:00:00.000Z",
64
+ parts: [{ type: "text", text: "history" }],
65
+ } satisfies NcpMessage;
66
+ const firstStreamingMessage = {
67
+ id: "assistant-2",
68
+ sessionId: "session-1",
69
+ role: "assistant",
70
+ status: "streaming",
71
+ timestamp: "2026-03-17T10:00:01.000Z",
72
+ parts: [{ type: "text", text: "hello" }],
73
+ } satisfies NcpMessage;
74
+
75
+ const { rerender } = render(
76
+ <ChatMessageListContainer
77
+ messages={[historicalMessage, firstStreamingMessage]}
78
+ isSending={false}
79
+ />,
80
+ );
81
+
82
+ const firstMessages =
83
+ captures.renders[captures.renders.length - 1]?.messages ?? [];
84
+ const nextStreamingMessage = {
85
+ ...firstStreamingMessage,
86
+ parts: [{ type: "text", text: "hello world" }],
87
+ } satisfies NcpMessage;
88
+
89
+ rerender(
90
+ <ChatMessageListContainer
91
+ messages={[historicalMessage, nextStreamingMessage]}
92
+ isSending={false}
93
+ />,
94
+ );
95
+
96
+ const secondMessages =
97
+ captures.renders[captures.renders.length - 1]?.messages ?? [];
98
+
99
+ expect(secondMessages[0]).toBe(firstMessages[0]);
100
+ expect(secondMessages[1]).not.toBe(firstMessages[1]);
101
+ });
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { type UiMessage } from "@nextclaw/agent-chat";
2
+ import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import {
4
4
  type ChatMessageViewModel,
5
5
  ChatMessageList,
@@ -9,17 +9,18 @@ import {
9
9
  type ChatMessageAdapterTexts,
10
10
  type ChatMessageSource,
11
11
  } from "@/components/chat/adapters/chat-message.adapter";
12
+ import { adaptNcpMessageToUiMessage } from "@/components/chat/ncp/ncp-session-adapter";
12
13
  import { useI18n } from "@/components/providers/I18nProvider";
13
14
  import { formatDateTime, t } from "@/lib/i18n";
14
15
 
15
16
  type ChatMessageListContainerProps = {
16
- uiMessages: UiMessage[];
17
+ messages: readonly NcpMessage[];
17
18
  isSending: boolean;
18
19
  className?: string;
19
20
  };
20
21
 
21
22
  const messageViewModelCache = new WeakMap<
22
- UiMessage,
23
+ NcpMessage,
23
24
  { language: string; viewModel: ChatMessageViewModel }
24
25
  >();
25
26
 
@@ -68,20 +69,21 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
68
69
  );
69
70
 
70
71
  const messages = useMemo(() => {
71
- return props.uiMessages.map((message) => {
72
+ return props.messages.map((message) => {
72
73
  const cached = messageViewModelCache.get(message);
73
74
  if (cached && cached.language === language) {
74
75
  return cached.viewModel;
75
76
  }
76
77
 
78
+ const uiMessage = adaptNcpMessageToUiMessage(message);
77
79
  const sourceMessage: ChatMessageSource = {
78
- id: message.id,
79
- role: message.role,
80
+ id: uiMessage.id,
81
+ role: uiMessage.role,
80
82
  meta: {
81
- timestamp: message.meta?.timestamp,
82
- status: message.meta?.status,
83
+ timestamp: uiMessage.meta?.timestamp,
84
+ status: uiMessage.meta?.status,
83
85
  },
84
- parts: message.parts as unknown as ChatMessageSource["parts"],
86
+ parts: uiMessage.parts as unknown as ChatMessageSource["parts"],
85
87
  };
86
88
  const viewModel = adaptChatMessage(sourceMessage, {
87
89
  formatTimestamp: (value) => formatDateTime(value, language),
@@ -91,17 +93,16 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
91
93
  messageViewModelCache.set(message, { language, viewModel });
92
94
  return viewModel;
93
95
  });
94
- }, [language, props.uiMessages, texts]);
96
+ }, [language, props.messages, texts]);
95
97
 
96
98
  const hasAssistantDraft = useMemo(
97
99
  () =>
98
- props.uiMessages.some(
100
+ messages.some(
99
101
  (message) =>
100
102
  message.role === "assistant" &&
101
- (message.meta?.status === "streaming" ||
102
- message.meta?.status === "pending"),
103
+ (message.status === "streaming" || message.status === "pending"),
103
104
  ),
104
- [props.uiMessages],
105
+ [messages],
105
106
  );
106
107
  const messageTexts = useMemo(
107
108
  () => buildChatMessageTexts(language),
@@ -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
  }