@nextclaw/ui 0.6.15 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/dist/assets/ChannelsList-DBcoVJRW.js +1 -0
- package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
- package/dist/assets/DocBrowser-DDX2HMXW.js +1 -0
- package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-J53F_3JA.js} +1 -1
- package/dist/assets/MarketplacePage-0BZ4bza0.js +49 -0
- package/dist/assets/ModelConfig-Wzq9wGHV.js +1 -0
- package/dist/assets/ProvidersList-kwzRS8_M.js +1 -0
- package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
- package/dist/assets/SearchConfig-DVt5QVa_.js +1 -0
- package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CkwauPa8.js} +2 -2
- package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
- package/dist/assets/{session-run-status-tZ4ISNj-.js → chat-message-pxr79GDs.js} +3 -3
- package/dist/assets/index-BIvFMkN4.js +1 -0
- package/dist/assets/index-CzkY1reu.js +8 -0
- package/dist/assets/{index-CkqvHQAt.js → index-GdpEEKnz.js} +1 -1
- package/dist/assets/index-RZ0kHHRI.css +1 -0
- package/dist/assets/{label-DkL14Jvl.js → label-CmksBHgc.js} +1 -1
- package/dist/assets/page-layout-Db0GbnhS.js +1 -0
- package/dist/assets/security-config-CjLFME5Q.js +1 -0
- package/dist/assets/skeleton-CkpQeVWN.js +1 -0
- package/dist/assets/{switch-CgbPbIX3.js → switch-C24d-UJU.js} +1 -1
- package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
- package/dist/assets/useConfirmDialog-BeP35LcG.js +5 -0
- package/dist/assets/vendor-psXJBy9u.js +407 -0
- package/dist/index.html +3 -3
- package/package.json +12 -5
- package/src/App.tsx +49 -27
- package/src/api/client.ts +1 -0
- package/src/api/config.ts +98 -0
- package/src/api/types.ts +45 -0
- package/src/components/auth/login-page.tsx +69 -0
- package/src/components/chat/ChatConversationPanel.tsx +12 -54
- package/src/components/chat/ChatPage.tsx +10 -324
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +138 -0
- package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
- package/src/components/chat/chat-chain.test.ts +22 -0
- package/src/components/chat/chat-chain.ts +23 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
- package/src/components/chat/chat-page-shell.tsx +103 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +71 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
- package/src/components/chat/managers/chat-thread.manager.ts +3 -1
- package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
- package/src/components/chat/nextclaw/index.ts +23 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
- package/src/components/config/runtime-security-card.tsx +276 -0
- package/src/components/config/security-config.tsx +12 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
- package/src/components/marketplace/MarketplacePage.tsx +77 -28
- package/src/hooks/use-auth.ts +111 -0
- package/src/hooks/useConfig.ts +42 -0
- package/src/hooks/useMarketplace.ts +9 -0
- package/src/lib/i18n.ts +73 -1
- package/src/test/setup.ts +16 -0
- package/tailwind.config.js +8 -3
- package/tsconfig.json +6 -2
- package/vite.config.ts +2 -1
- package/vitest.config.ts +16 -0
- package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
- package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
- package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
- package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
- package/dist/assets/ModelConfig-AchYxLft.js +0 -1
- package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
- package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
- package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
- package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
- package/dist/assets/card-BAM7vbMg.js +0 -1
- package/dist/assets/index-D9rRqOi8.css +0 -1
- package/dist/assets/index-DJZ5y7t1.js +0 -8
- package/dist/assets/input-BoelTiYL.js +0 -1
- package/dist/assets/page-layout-CERNdqzB.js +0 -1
- package/dist/assets/popover-uwYz3Chm.js +0 -1
- package/dist/assets/tabs-custom-pDyl95el.js +0 -1
- package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
- package/dist/assets/vendor-BKtTvQYU.js +0 -407
- package/src/components/chat/ChatThread.tsx +0 -402
- package/src/components/chat/SkillsPicker.tsx +0 -137
- package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
- package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { SetStateAction } from 'react';
|
|
2
|
+
import type { ThinkingLevel } from '@/api/types';
|
|
3
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
4
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
|
+
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
6
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
7
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
8
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
9
|
+
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
10
|
+
|
|
11
|
+
export class NcpChatInputManager {
|
|
12
|
+
constructor(
|
|
13
|
+
private uiManager: ChatUiManager,
|
|
14
|
+
private streamActionsManager: ChatStreamActionsManager,
|
|
15
|
+
private getDraftSessionId: () => string
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
19
|
+
const current = useChatInputStore.getState().snapshot;
|
|
20
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
21
|
+
if (!Object.is(current[key], value)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
29
|
+
if (typeof next === 'function') {
|
|
30
|
+
return (next as (value: T) => T)(prev);
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
36
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
useChatInputStore.getState().setSnapshot(patch);
|
|
40
|
+
if (
|
|
41
|
+
Object.prototype.hasOwnProperty.call(patch, 'modelOptions') ||
|
|
42
|
+
Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
|
|
43
|
+
Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
|
|
44
|
+
) {
|
|
45
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
46
|
+
this.reconcileThinkingForModel(snapshot.selectedModel);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
setDraft = (next: SetStateAction<string>) => {
|
|
51
|
+
const prev = useChatInputStore.getState().snapshot.draft;
|
|
52
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
53
|
+
if (value === prev) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
useChatInputStore.getState().setSnapshot({ draft: value });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
60
|
+
const prev = useChatInputStore.getState().snapshot.pendingSessionType;
|
|
61
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
62
|
+
if (value === prev) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
send = async () => {
|
|
69
|
+
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
70
|
+
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
71
|
+
const message = inputSnapshot.draft.trim();
|
|
72
|
+
if (!message) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const requestedSkills = inputSnapshot.selectedSkills;
|
|
76
|
+
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.getDraftSessionId();
|
|
77
|
+
if (!sessionSnapshot.selectedSessionKey) {
|
|
78
|
+
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
79
|
+
}
|
|
80
|
+
this.setDraft('');
|
|
81
|
+
this.setSelectedSkills([]);
|
|
82
|
+
await this.streamActionsManager.sendMessage({
|
|
83
|
+
message,
|
|
84
|
+
sessionKey,
|
|
85
|
+
agentId: sessionSnapshot.selectedAgentId,
|
|
86
|
+
sessionType: inputSnapshot.selectedSessionType,
|
|
87
|
+
model: inputSnapshot.selectedModel || undefined,
|
|
88
|
+
thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
|
|
89
|
+
stopSupported: true,
|
|
90
|
+
requestedSkills,
|
|
91
|
+
restoreDraftOnError: true
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
stop = async () => {
|
|
96
|
+
await this.streamActionsManager.stopCurrentRun();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
goToProviders = () => {
|
|
100
|
+
this.uiManager.goToProviders();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
setSelectedModel = (next: SetStateAction<string>) => {
|
|
104
|
+
const prev = useChatInputStore.getState().snapshot.selectedModel;
|
|
105
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
106
|
+
if (value === prev) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
useChatInputStore.getState().setSnapshot({ selectedModel: value });
|
|
110
|
+
this.reconcileThinkingForModel(value);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
setSelectedThinkingLevel = (next: SetStateAction<ThinkingLevel | null>) => {
|
|
114
|
+
const prev = useChatInputStore.getState().snapshot.selectedThinkingLevel;
|
|
115
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
116
|
+
if (value === prev) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: value });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
selectSessionType = (value: string) => {
|
|
123
|
+
const normalized = normalizeSessionType(value);
|
|
124
|
+
useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
128
|
+
const prev = useChatInputStore.getState().snapshot.selectedSkills;
|
|
129
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
130
|
+
if (Object.is(value, prev)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
useChatInputStore.getState().setSnapshot({ selectedSkills: value });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
selectModel = (value: string) => {
|
|
137
|
+
this.setSelectedModel(value);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
141
|
+
this.setSelectedThinkingLevel(value);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
selectSkills = (next: string[]) => {
|
|
145
|
+
this.setSelectedSkills(next);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
private resolveThinkingForModel(modelOption: ChatModelOption | undefined, current: ThinkingLevel | null): ThinkingLevel | null {
|
|
149
|
+
const capability = modelOption?.thinkingCapability;
|
|
150
|
+
if (!capability || capability.supported.length === 0) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (current === 'off') {
|
|
154
|
+
return 'off';
|
|
155
|
+
}
|
|
156
|
+
if (current && capability.supported.includes(current)) {
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
if (capability.default && capability.supported.includes(capability.default)) {
|
|
160
|
+
return capability.default;
|
|
161
|
+
}
|
|
162
|
+
return 'off';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private reconcileThinkingForModel(model: string): void {
|
|
166
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
167
|
+
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
168
|
+
const nextThinking = this.resolveThinkingForModel(modelOption, snapshot.selectedThinkingLevel);
|
|
169
|
+
if (nextThinking !== snapshot.selectedThinkingLevel) {
|
|
170
|
+
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import type { SessionEntryView } from '@/api/types';
|
|
4
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
|
+
import {
|
|
6
|
+
adaptNcpSessionSummaries,
|
|
7
|
+
readNcpSessionPreferredThinking
|
|
8
|
+
} from '@/components/chat/ncp/ncp-session-adapter';
|
|
9
|
+
import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
10
|
+
import { useSyncSelectedModel } from '@/components/chat/chat-page-runtime';
|
|
11
|
+
import {
|
|
12
|
+
useConfig,
|
|
13
|
+
useConfigMeta,
|
|
14
|
+
useNcpSessions
|
|
15
|
+
} from '@/hooks/useConfig';
|
|
16
|
+
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
17
|
+
import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
|
|
18
|
+
|
|
19
|
+
type UseNcpChatPageDataParams = {
|
|
20
|
+
query: string;
|
|
21
|
+
selectedSessionKey: string | null;
|
|
22
|
+
pendingSessionType: string;
|
|
23
|
+
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
24
|
+
setSelectedModel: Dispatch<SetStateAction<string>>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
|
|
28
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
29
|
+
if (!normalizedQuery) {
|
|
30
|
+
return sessions;
|
|
31
|
+
}
|
|
32
|
+
return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
36
|
+
const configQuery = useConfig();
|
|
37
|
+
const configMetaQuery = useConfigMeta();
|
|
38
|
+
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
39
|
+
const installedSkillsQuery = useMarketplaceInstalled('skill');
|
|
40
|
+
const isProviderStateResolved =
|
|
41
|
+
(configQuery.isFetched || configQuery.isSuccess) &&
|
|
42
|
+
(configMetaQuery.isFetched || configMetaQuery.isSuccess);
|
|
43
|
+
|
|
44
|
+
const modelOptions = useMemo<ChatModelOption[]>(() => {
|
|
45
|
+
const providers = buildProviderModelCatalog({
|
|
46
|
+
meta: configMetaQuery.data,
|
|
47
|
+
config: configQuery.data,
|
|
48
|
+
onlyConfigured: true
|
|
49
|
+
});
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
const options: ChatModelOption[] = [];
|
|
52
|
+
for (const provider of providers) {
|
|
53
|
+
for (const localModel of provider.models) {
|
|
54
|
+
const value = composeProviderModel(provider.prefix, localModel);
|
|
55
|
+
if (!value || seen.has(value)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
seen.add(value);
|
|
59
|
+
options.push({
|
|
60
|
+
value,
|
|
61
|
+
modelLabel: localModel,
|
|
62
|
+
providerLabel: provider.displayName,
|
|
63
|
+
thinkingCapability: resolveModelThinkingCapability(provider.modelThinking, localModel, provider.aliases)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return options.sort((left, right) => {
|
|
68
|
+
const providerCompare = left.providerLabel.localeCompare(right.providerLabel);
|
|
69
|
+
if (providerCompare !== 0) {
|
|
70
|
+
return providerCompare;
|
|
71
|
+
}
|
|
72
|
+
return left.modelLabel.localeCompare(right.modelLabel);
|
|
73
|
+
});
|
|
74
|
+
}, [configMetaQuery.data, configQuery.data]);
|
|
75
|
+
|
|
76
|
+
const sessionSummaries = useMemo(
|
|
77
|
+
() => sessionsQuery.data?.sessions ?? [],
|
|
78
|
+
[sessionsQuery.data?.sessions]
|
|
79
|
+
);
|
|
80
|
+
const allSessions = useMemo(
|
|
81
|
+
() => adaptNcpSessionSummaries(sessionSummaries),
|
|
82
|
+
[sessionSummaries]
|
|
83
|
+
);
|
|
84
|
+
const sessions = useMemo(
|
|
85
|
+
() => filterSessionsByQuery(allSessions, params.query),
|
|
86
|
+
[allSessions, params.query]
|
|
87
|
+
);
|
|
88
|
+
const selectedSession = useMemo(
|
|
89
|
+
() => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
|
|
90
|
+
[allSessions, params.selectedSessionKey]
|
|
91
|
+
);
|
|
92
|
+
const selectedSessionSummary = useMemo(
|
|
93
|
+
() => sessionSummaries.find((session) => session.sessionId === params.selectedSessionKey) ?? null,
|
|
94
|
+
[params.selectedSessionKey, sessionSummaries]
|
|
95
|
+
);
|
|
96
|
+
const skillRecords = useMemo(
|
|
97
|
+
() => installedSkillsQuery.data?.records ?? [],
|
|
98
|
+
[installedSkillsQuery.data?.records]
|
|
99
|
+
);
|
|
100
|
+
const selectedSessionThinkingLevel = useMemo(
|
|
101
|
+
() => (selectedSessionSummary ? readNcpSessionPreferredThinking(selectedSessionSummary) : null),
|
|
102
|
+
[selectedSessionSummary]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const sessionTypeState = useChatSessionTypeState({
|
|
106
|
+
selectedSession,
|
|
107
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
108
|
+
pendingSessionType: params.pendingSessionType,
|
|
109
|
+
setPendingSessionType: params.setPendingSessionType,
|
|
110
|
+
sessionTypesData: null
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
useSyncSelectedModel({
|
|
114
|
+
modelOptions,
|
|
115
|
+
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
116
|
+
defaultModel: configQuery.data?.agents.defaults.model,
|
|
117
|
+
setSelectedModel: params.setSelectedModel
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
configQuery,
|
|
122
|
+
configMetaQuery,
|
|
123
|
+
sessionsQuery,
|
|
124
|
+
installedSkillsQuery,
|
|
125
|
+
isProviderStateResolved,
|
|
126
|
+
modelOptions,
|
|
127
|
+
sessionSummaries,
|
|
128
|
+
sessions,
|
|
129
|
+
skillRecords,
|
|
130
|
+
selectedSession,
|
|
131
|
+
selectedSessionThinkingLevel,
|
|
132
|
+
...sessionTypeState
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { deleteNcpSession as deleteNcpSessionApi } from '@/api/config';
|
|
2
|
+
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
3
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
4
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
5
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
|
+
import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.store';
|
|
7
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
8
|
+
import { t } from '@/lib/i18n';
|
|
9
|
+
|
|
10
|
+
export type NcpChatThreadManagerActions = {
|
|
11
|
+
refetchSessions: () => Promise<unknown>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const noopAsync = async () => {};
|
|
15
|
+
|
|
16
|
+
export class NcpChatThreadManager {
|
|
17
|
+
private actions: NcpChatThreadManagerActions = {
|
|
18
|
+
refetchSessions: noopAsync
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private uiManager: ChatUiManager,
|
|
23
|
+
private sessionListManager: ChatSessionListManager,
|
|
24
|
+
private streamActionsManager: ChatStreamActionsManager
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
bindActions = (patch: Partial<NcpChatThreadManagerActions>) => {
|
|
28
|
+
this.actions = {
|
|
29
|
+
...this.actions,
|
|
30
|
+
...patch
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
|
|
35
|
+
const current = useChatThreadStore.getState().snapshot;
|
|
36
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatThreadSnapshot, ChatThreadSnapshot[keyof ChatThreadSnapshot]]>) {
|
|
37
|
+
if (!Object.is(current[key], value)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
syncSnapshot = (patch: Partial<ChatThreadSnapshot>) => {
|
|
45
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
useChatThreadStore.getState().setSnapshot(patch);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
deleteSession = () => {
|
|
52
|
+
void this.deleteCurrentSession();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
createSession = () => {
|
|
56
|
+
this.sessionListManager.createSession();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
goToProviders = () => {
|
|
60
|
+
this.uiManager.goToProviders();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
private deleteCurrentSession = async () => {
|
|
64
|
+
const {
|
|
65
|
+
snapshot: { selectedSessionKey }
|
|
66
|
+
} = useChatSessionListStore.getState();
|
|
67
|
+
if (!selectedSessionKey) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const confirmed = await this.uiManager.confirm({
|
|
71
|
+
title: t('chatDeleteSessionConfirm'),
|
|
72
|
+
variant: 'destructive',
|
|
73
|
+
confirmLabel: t('delete')
|
|
74
|
+
});
|
|
75
|
+
if (!confirmed) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
|
|
79
|
+
try {
|
|
80
|
+
await deleteNcpSessionApi(selectedSessionKey);
|
|
81
|
+
this.streamActionsManager.resetStreamState();
|
|
82
|
+
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
|
|
83
|
+
this.uiManager.goToChatRoot({ replace: true });
|
|
84
|
+
await this.actions.refetchSessions();
|
|
85
|
+
} finally {
|
|
86
|
+
useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
|
|
2
|
+
import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
3
|
+
import { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
4
|
+
import { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
5
|
+
import { NcpChatInputManager } from '@/components/chat/ncp/ncp-chat-input.manager';
|
|
6
|
+
import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.manager';
|
|
7
|
+
|
|
8
|
+
export class NcpChatPresenter {
|
|
9
|
+
chatUiManager = new ChatUiManager();
|
|
10
|
+
chatStreamActionsManager = new ChatStreamActionsManager();
|
|
11
|
+
chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
|
|
12
|
+
chatInputManager = new NcpChatInputManager(
|
|
13
|
+
this.chatUiManager,
|
|
14
|
+
this.chatStreamActionsManager,
|
|
15
|
+
() => this.getDraftSessionId()
|
|
16
|
+
);
|
|
17
|
+
chatRunStatusManager = new ChatRunStatusManager();
|
|
18
|
+
chatThreadManager = new NcpChatThreadManager(
|
|
19
|
+
this.chatUiManager,
|
|
20
|
+
this.chatSessionListManager,
|
|
21
|
+
this.chatStreamActionsManager
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
private draftSessionId = '';
|
|
25
|
+
|
|
26
|
+
setDraftSessionId = (sessionId: string) => {
|
|
27
|
+
this.draftSessionId = sessionId;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
private getDraftSessionId(): string {
|
|
31
|
+
return this.draftSessionId;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { adaptNcpSessionSummary, readNcpSessionPreferredThinking } from '@/components/chat/ncp/ncp-session-adapter';
|
|
2
|
+
import type { NcpSessionSummaryView } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
function createSummary(partial: Partial<NcpSessionSummaryView> = {}): NcpSessionSummaryView {
|
|
5
|
+
return {
|
|
6
|
+
sessionId: 'ncp-session-1',
|
|
7
|
+
messageCount: 3,
|
|
8
|
+
updatedAt: '2026-03-18T00:00:00.000Z',
|
|
9
|
+
status: 'idle',
|
|
10
|
+
...partial
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('adaptNcpSessionSummary', () => {
|
|
15
|
+
it('maps session metadata into shared session entry fields', () => {
|
|
16
|
+
const adapted = adaptNcpSessionSummary(
|
|
17
|
+
createSummary({
|
|
18
|
+
metadata: {
|
|
19
|
+
label: 'NCP Planning Thread',
|
|
20
|
+
model: 'openai/gpt-5',
|
|
21
|
+
session_type: 'native'
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(adapted).toMatchObject({
|
|
27
|
+
key: 'ncp-session-1',
|
|
28
|
+
label: 'NCP Planning Thread',
|
|
29
|
+
preferredModel: 'openai/gpt-5',
|
|
30
|
+
sessionType: 'native',
|
|
31
|
+
sessionTypeMutable: false,
|
|
32
|
+
messageCount: 3
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('readNcpSessionPreferredThinking', () => {
|
|
38
|
+
it('normalizes persisted thinking metadata for UI hydration', () => {
|
|
39
|
+
const thinking = readNcpSessionPreferredThinking(
|
|
40
|
+
createSummary({
|
|
41
|
+
metadata: {
|
|
42
|
+
preferred_thinking: 'HIGH'
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(thinking).toBe('high');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
|
|
2
|
+
import type { NcpMessagePart } from '@nextclaw/ncp';
|
|
3
|
+
import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
|
|
4
|
+
|
|
5
|
+
const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
|
|
6
|
+
|
|
7
|
+
function stringifyUnknown(value: unknown): string {
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.stringify(value ?? {});
|
|
13
|
+
} catch {
|
|
14
|
+
return String(value ?? '');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readOptionalString(value: unknown): string | null {
|
|
19
|
+
if (typeof value !== 'string') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readMetadata(summary: NcpSessionSummaryView): Record<string, unknown> | null {
|
|
27
|
+
const { metadata } = summary;
|
|
28
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return metadata as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readNcpSessionPreferredModel(summary: NcpSessionSummaryView): string | null {
|
|
35
|
+
const metadata = readMetadata(summary);
|
|
36
|
+
if (!metadata) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
readOptionalString(metadata.preferred_model) ??
|
|
41
|
+
readOptionalString(metadata.preferredModel) ??
|
|
42
|
+
readOptionalString(metadata.model)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readNcpSessionPreferredThinking(summary: NcpSessionSummaryView): ThinkingLevel | null {
|
|
47
|
+
const metadata = readMetadata(summary);
|
|
48
|
+
if (!metadata) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const rawValue =
|
|
52
|
+
readOptionalString(metadata.preferred_thinking) ??
|
|
53
|
+
readOptionalString(metadata.thinking) ??
|
|
54
|
+
readOptionalString(metadata.thinking_level) ??
|
|
55
|
+
readOptionalString(metadata.thinkingLevel);
|
|
56
|
+
if (!rawValue) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const normalized = rawValue.toLowerCase();
|
|
60
|
+
return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readNcpSessionLabel(summary: NcpSessionSummaryView): string | null {
|
|
64
|
+
const metadata = readMetadata(summary);
|
|
65
|
+
if (!metadata) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return readOptionalString(metadata.label);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
72
|
+
const metadata = readMetadata(summary);
|
|
73
|
+
if (!metadata) {
|
|
74
|
+
return 'native';
|
|
75
|
+
}
|
|
76
|
+
return readOptionalString(metadata.session_type) ?? readOptionalString(metadata.sessionType) ?? 'native';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
|
|
80
|
+
if (part.state === 'result') {
|
|
81
|
+
return ToolInvocationStatus.RESULT;
|
|
82
|
+
}
|
|
83
|
+
if (part.state === 'partial-call') {
|
|
84
|
+
return ToolInvocationStatus.PARTIAL_CALL;
|
|
85
|
+
}
|
|
86
|
+
return ToolInvocationStatus.CALL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
|
|
90
|
+
const uiParts: UIMessage['parts'] = [];
|
|
91
|
+
for (const part of parts) {
|
|
92
|
+
if (part.type === 'text') {
|
|
93
|
+
uiParts.push({ type: 'text', text: part.text });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (part.type === 'rich-text') {
|
|
97
|
+
uiParts.push({ type: 'text', text: part.text });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (part.type === 'reasoning') {
|
|
101
|
+
uiParts.push({
|
|
102
|
+
type: 'reasoning',
|
|
103
|
+
reasoning: part.text,
|
|
104
|
+
details: []
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (part.type === 'source') {
|
|
109
|
+
uiParts.push({
|
|
110
|
+
type: 'source',
|
|
111
|
+
source: {
|
|
112
|
+
sourceType: 'url',
|
|
113
|
+
id: part.url ?? part.title ?? Math.random().toString(36).slice(2, 8),
|
|
114
|
+
url: part.url ?? '',
|
|
115
|
+
...(part.title ? { title: part.title } : {})
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (part.type === 'file' && part.contentBase64) {
|
|
121
|
+
uiParts.push({
|
|
122
|
+
type: 'file',
|
|
123
|
+
mimeType: part.mimeType ?? 'application/octet-stream',
|
|
124
|
+
data: part.contentBase64
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (part.type === 'step-start') {
|
|
129
|
+
uiParts.push({ type: 'step-start' });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (part.type === 'tool-invocation') {
|
|
133
|
+
uiParts.push({
|
|
134
|
+
type: 'tool-invocation',
|
|
135
|
+
toolInvocation: {
|
|
136
|
+
status: mapToolStatus(part),
|
|
137
|
+
toolCallId: part.toolCallId ?? `${part.toolName}-${Math.random().toString(36).slice(2, 8)}`,
|
|
138
|
+
toolName: part.toolName,
|
|
139
|
+
args: stringifyUnknown(part.args),
|
|
140
|
+
result: part.result
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return uiParts;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeRole(role: NcpMessageView['role']): UIMessage['role'] {
|
|
149
|
+
if (role === 'service') {
|
|
150
|
+
return 'system';
|
|
151
|
+
}
|
|
152
|
+
return role === 'tool' ? 'assistant' : role;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function adaptNcpMessageToUiMessage(message: NcpMessageView): UIMessage {
|
|
156
|
+
return {
|
|
157
|
+
id: message.id,
|
|
158
|
+
role: normalizeRole(message.role),
|
|
159
|
+
parts: toUiParts(message.parts),
|
|
160
|
+
meta: {
|
|
161
|
+
source: 'stream',
|
|
162
|
+
status: message.status,
|
|
163
|
+
sessionKey: message.sessionId,
|
|
164
|
+
timestamp: message.timestamp
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]): UIMessage[] {
|
|
170
|
+
return messages.map(adaptNcpMessageToUiMessage);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
|
|
174
|
+
const label = readNcpSessionLabel(summary);
|
|
175
|
+
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
176
|
+
return {
|
|
177
|
+
key: summary.sessionId,
|
|
178
|
+
createdAt: summary.updatedAt,
|
|
179
|
+
updatedAt: summary.updatedAt,
|
|
180
|
+
...(label ? { label } : {}),
|
|
181
|
+
...(preferredModel ? { preferredModel } : {}),
|
|
182
|
+
sessionType: readNcpSessionType(summary),
|
|
183
|
+
sessionTypeMutable: false,
|
|
184
|
+
messageCount: summary.messageCount
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function adaptNcpSessionSummaries(summaries: NcpSessionSummaryView[]): SessionEntryView[] {
|
|
189
|
+
return summaries.map(adaptNcpSessionSummary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createNcpSessionId(): string {
|
|
193
|
+
return `ncp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
194
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { ChatInputBarContainer } from '@/components/chat/containers/chat-input-bar.container';
|
|
2
|
+
export { ChatMessageListContainer } from '@/components/chat/containers/chat-message-list.container';
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
adaptChatMessages,
|
|
6
|
+
type ChatMessageAdapterTexts,
|
|
7
|
+
type ChatMessageSource,
|
|
8
|
+
type ChatMessagePartSource
|
|
9
|
+
} from '@/components/chat/adapters/chat-message.adapter';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
buildChatSlashItems,
|
|
13
|
+
buildSelectedSkillItems,
|
|
14
|
+
buildSkillPickerModel,
|
|
15
|
+
buildModelStateHint,
|
|
16
|
+
buildModelToolbarSelect,
|
|
17
|
+
buildSessionTypeToolbarSelect,
|
|
18
|
+
buildThinkingToolbarSelect,
|
|
19
|
+
resolveSlashQuery,
|
|
20
|
+
type ChatSkillRecord,
|
|
21
|
+
type ChatModelRecord,
|
|
22
|
+
type ChatThinkingLevel
|
|
23
|
+
} from '@/components/chat/adapters/chat-input-bar.adapter';
|