@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/{ChannelsList-XGMfinnc.js → ChannelsList-C63gOoYI.js} +3 -3
- package/dist/assets/ChatPage-Ci3Gz0qh.js +37 -0
- package/dist/assets/{DocBrowser-DTRCNsSM.js → DocBrowser-CI4jOzJY.js} +1 -1
- package/dist/assets/{LogoBadge-CPMOwWdA.js → LogoBadge-DImV63-L.js} +1 -1
- package/dist/assets/{MarketplacePage-De2qZ9C0.js → MarketplacePage-B360oSAV.js} +1 -1
- package/dist/assets/{McpMarketplacePage-cjVKSQ2f.js → McpMarketplacePage-KIQgx_7h.js} +2 -2
- package/dist/assets/{ModelConfig-CMn3-VZk.js → ModelConfig-Ben3tQoX.js} +1 -1
- package/dist/assets/{ProvidersList-CArDOswN.js → ProvidersList-DE-S9mq0.js} +1 -1
- package/dist/assets/RemoteAccessPage-DxUia6R-.js +1 -0
- package/dist/assets/RuntimeConfig-CQcGfNZT.js +1 -0
- package/dist/assets/{SearchConfig-a38m8Ynx.js → SearchConfig-DeOa-M6j.js} +1 -1
- package/dist/assets/{SecretsConfig-B6mf4JY9.js → SecretsConfig-Ci8pJmzd.js} +2 -2
- package/dist/assets/{SessionsConfig-B_WQ1lVd.js → SessionsConfig-B6zq55yu.js} +2 -2
- package/dist/assets/chat-session-display--oo5yuIw.js +1 -0
- package/dist/assets/{index-D-wEIgPn.js → index-LhlkB00c.js} +4 -4
- package/dist/assets/{label-usOOP7mv.js → label-3TKt0PoZ.js} +1 -1
- package/dist/assets/{page-layout-CuIf20mx.js → page-layout-CopkIM3Q.js} +1 -1
- package/dist/assets/{popover-CTtTCP5d.js → popover-CUx8uRJw.js} +1 -1
- package/dist/assets/security-config-BL29kTzz.js +1 -0
- package/dist/assets/{skeleton-BNUaFYE7.js → skeleton-Bs4zvcql.js} +1 -1
- package/dist/assets/{status-dot-BeHTBy9k.js → status-dot-D6vJMwD7.js} +1 -1
- package/dist/assets/{switch-CtNnWZpa.js → switch-A3-ClT1P.js} +1 -1
- package/dist/assets/{tabs-custom-Dz_4tV62.js → tabs-custom-BVSd5urq.js} +1 -1
- package/dist/assets/{useConfirmDialog-C_n_JIEq.js → useConfirmDialog-ChPriea6.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -5
- package/src/api/ncp-session-query-cache.test.ts +89 -0
- package/src/api/ncp-session-query-cache.ts +85 -0
- package/src/api/types.ts +2 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +1 -1
- package/src/components/chat/ChatConversationPanel.tsx +6 -6
- package/src/components/chat/ChatSidebar.test.tsx +87 -92
- package/src/components/chat/ChatSidebar.tsx +21 -36
- package/src/components/chat/chat-session-label.service.ts +3 -3
- package/src/components/chat/containers/chat-message-list.container.test.tsx +53 -8
- package/src/components/chat/containers/chat-message-list.container.tsx +15 -14
- package/src/components/chat/managers/chat-session-list.manager.ts +0 -18
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -52
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +4 -18
- package/src/components/chat/ncp/ncp-chat.presenter.ts +0 -2
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +0 -23
- package/src/components/chat/ncp/ncp-session-adapter.ts +0 -19
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +42 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +0 -3
- package/src/components/chat/stores/chat-session-list.store.ts +1 -7
- package/src/components/chat/stores/chat-thread.store.ts +3 -3
- package/src/hooks/use-realtime-query-bridge.ts +14 -19
- package/src/hooks/useConfig.ts +10 -11
- package/dist/assets/ChatPage-DYTcCRPp.js +0 -37
- package/dist/assets/RemoteAccessPage-C0I4tHey.js +0 -1
- package/dist/assets/RuntimeConfig-B4o6uJq9.js +0 -1
- package/dist/assets/ncp-session-adapter-DSacECph.js +0 -1
- package/dist/assets/security-config-Bxrrv8Ac.js +0 -1
- package/src/components/chat/managers/chat-run-status.manager.ts +0 -32
- 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
|
-
|
|
38
|
+
items: NcpSessionListItemView[];
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
function groupSessionsByDate(
|
|
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:
|
|
48
|
-
const yesterday:
|
|
49
|
-
const previous7:
|
|
50
|
-
const older:
|
|
47
|
+
const today: NcpSessionListItemView[] = [];
|
|
48
|
+
const yesterday: NcpSessionListItemView[] = [];
|
|
49
|
+
const previous7: NcpSessionListItemView[] = [];
|
|
50
|
+
const older: NcpSessionListItemView[] = [];
|
|
51
51
|
|
|
52
|
-
for (const
|
|
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(
|
|
56
|
+
today.push(item);
|
|
56
57
|
} else if (ts >= yesterdayStart) {
|
|
57
|
-
yesterday.push(
|
|
58
|
+
yesterday.push(item);
|
|
58
59
|
} else if (ts >= sevenDaysStart) {
|
|
59
|
-
previous7.push(
|
|
60
|
+
previous7.push(item);
|
|
60
61
|
} else {
|
|
61
|
-
older.push(
|
|
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'),
|
|
67
|
-
if (yesterday.length > 0) groups.push({ label: t('chatSidebarYesterday'),
|
|
68
|
-
if (previous7.length > 0) groups.push({ label: t('chatSidebarPrevious7Days'),
|
|
69
|
-
if (older.length > 0) groups.push({ label: t('chatSidebarOlder'),
|
|
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(
|
|
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
|
-
{
|
|
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.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
status: "streaming",
|
|
37
|
-
},
|
|
35
|
+
status: "streaming",
|
|
36
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
38
37
|
parts: [{ type: "text", text: "hello" }],
|
|
39
|
-
} satisfies
|
|
38
|
+
} satisfies NcpMessage;
|
|
40
39
|
|
|
41
40
|
const { rerender } = render(
|
|
42
|
-
<ChatMessageListContainer
|
|
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
|
|
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 {
|
|
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
|
-
|
|
17
|
+
messages: readonly NcpMessage[];
|
|
17
18
|
isSending: boolean;
|
|
18
19
|
className?: string;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const messageViewModelCache = new WeakMap<
|
|
22
|
-
|
|
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.
|
|
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:
|
|
79
|
-
role:
|
|
80
|
+
id: uiMessage.id,
|
|
81
|
+
role: uiMessage.role,
|
|
80
82
|
meta: {
|
|
81
|
-
timestamp:
|
|
82
|
-
status:
|
|
83
|
+
timestamp: uiMessage.meta?.timestamp,
|
|
84
|
+
status: uiMessage.meta?.status,
|
|
83
85
|
},
|
|
84
|
-
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.
|
|
96
|
+
}, [language, props.messages, texts]);
|
|
95
97
|
|
|
96
98
|
const hasAssistantDraft = useMemo(
|
|
97
99
|
() =>
|
|
98
|
-
|
|
100
|
+
messages.some(
|
|
99
101
|
(message) =>
|
|
100
102
|
message.role === "assistant" &&
|
|
101
|
-
(message.
|
|
102
|
-
message.meta?.status === "pending"),
|
|
103
|
+
(message.status === "streaming" || message.status === "pending"),
|
|
103
104
|
),
|
|
104
|
-
[
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|