@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -0
  3. package/dist/assets/ChannelsList-DBcoVJRW.js +1 -0
  4. package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
  5. package/dist/assets/DocBrowser-DDX2HMXW.js +1 -0
  6. package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-J53F_3JA.js} +1 -1
  7. package/dist/assets/MarketplacePage-0BZ4bza0.js +49 -0
  8. package/dist/assets/ModelConfig-Wzq9wGHV.js +1 -0
  9. package/dist/assets/ProvidersList-kwzRS8_M.js +1 -0
  10. package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
  11. package/dist/assets/SearchConfig-DVt5QVa_.js +1 -0
  12. package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CkwauPa8.js} +2 -2
  13. package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
  14. package/dist/assets/{session-run-status-tZ4ISNj-.js → chat-message-pxr79GDs.js} +3 -3
  15. package/dist/assets/index-BIvFMkN4.js +1 -0
  16. package/dist/assets/index-CzkY1reu.js +8 -0
  17. package/dist/assets/{index-CkqvHQAt.js → index-GdpEEKnz.js} +1 -1
  18. package/dist/assets/index-RZ0kHHRI.css +1 -0
  19. package/dist/assets/{label-DkL14Jvl.js → label-CmksBHgc.js} +1 -1
  20. package/dist/assets/page-layout-Db0GbnhS.js +1 -0
  21. package/dist/assets/security-config-CjLFME5Q.js +1 -0
  22. package/dist/assets/skeleton-CkpQeVWN.js +1 -0
  23. package/dist/assets/{switch-CgbPbIX3.js → switch-C24d-UJU.js} +1 -1
  24. package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
  25. package/dist/assets/useConfirmDialog-BeP35LcG.js +5 -0
  26. package/dist/assets/vendor-psXJBy9u.js +407 -0
  27. package/dist/index.html +3 -3
  28. package/package.json +12 -5
  29. package/src/App.tsx +49 -27
  30. package/src/api/client.ts +1 -0
  31. package/src/api/config.ts +98 -0
  32. package/src/api/types.ts +45 -0
  33. package/src/components/auth/login-page.tsx +69 -0
  34. package/src/components/chat/ChatConversationPanel.tsx +12 -54
  35. package/src/components/chat/ChatPage.tsx +10 -324
  36. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
  37. package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
  38. package/src/components/chat/adapters/chat-message.adapter.test.ts +138 -0
  39. package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
  40. package/src/components/chat/chat-chain.test.ts +22 -0
  41. package/src/components/chat/chat-chain.ts +23 -0
  42. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
  43. package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
  44. package/src/components/chat/chat-page-shell.tsx +103 -0
  45. package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
  46. package/src/components/chat/containers/chat-message-list.container.tsx +71 -0
  47. package/src/components/chat/index.ts +1 -0
  48. package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
  49. package/src/components/chat/managers/chat-thread.manager.ts +3 -1
  50. package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
  51. package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
  52. package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -0
  53. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  54. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  55. package/src/components/chat/ncp/ncp-session-adapter.test.ts +49 -0
  56. package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
  57. package/src/components/chat/nextclaw/index.ts +23 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  59. package/src/components/config/runtime-security-card.tsx +276 -0
  60. package/src/components/config/security-config.tsx +12 -0
  61. package/src/components/layout/Sidebar.tsx +6 -1
  62. package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
  63. package/src/components/marketplace/MarketplacePage.tsx +77 -28
  64. package/src/hooks/use-auth.ts +111 -0
  65. package/src/hooks/useConfig.ts +42 -0
  66. package/src/hooks/useMarketplace.ts +9 -0
  67. package/src/lib/i18n.ts +73 -1
  68. package/src/test/setup.ts +16 -0
  69. package/tailwind.config.js +8 -3
  70. package/tsconfig.json +6 -2
  71. package/vite.config.ts +2 -1
  72. package/vitest.config.ts +16 -0
  73. package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
  74. package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
  75. package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
  76. package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
  77. package/dist/assets/ModelConfig-AchYxLft.js +0 -1
  78. package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
  79. package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
  80. package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
  81. package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
  82. package/dist/assets/card-BAM7vbMg.js +0 -1
  83. package/dist/assets/index-D9rRqOi8.css +0 -1
  84. package/dist/assets/index-DJZ5y7t1.js +0 -8
  85. package/dist/assets/input-BoelTiYL.js +0 -1
  86. package/dist/assets/page-layout-CERNdqzB.js +0 -1
  87. package/dist/assets/popover-uwYz3Chm.js +0 -1
  88. package/dist/assets/tabs-custom-pDyl95el.js +0 -1
  89. package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
  90. package/dist/assets/vendor-BKtTvQYU.js +0 -407
  91. package/src/components/chat/ChatThread.tsx +0 -402
  92. package/src/components/chat/SkillsPicker.tsx +0 -137
  93. package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
  94. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
  95. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
  96. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
  97. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
  98. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
  99. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
  100. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
  101. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
  102. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
  103. 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';