@nextclaw/ui 0.12.4 → 0.12.6
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 +66 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +155 -59
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
- package/src/components/chat/stores/chat-session-list.store.ts +99 -5
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +19 -14
- package/src/components/layout/sidebar.layout.test.tsx +33 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
- package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
- package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
- package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
- package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
- package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
- package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
- package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
- package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
- package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
- package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
- package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
- package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
- package/dist/assets/config-CMiW0yaK.js +0 -1
- package/dist/assets/dist-BFc_H-lY.js +0 -15
- package/dist/assets/i18n-C_2dKw6w.js +0 -1
- package/dist/assets/index-ChUXhq0G.css +0 -1
- package/dist/assets/index-DAE8Srx-.js +0 -6
- package/dist/assets/label-D8yyejJS.js +0 -1
- package/dist/assets/loader-circle-B0sKKO29.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CYXs3JtZ.js +0 -1
- package/dist/assets/react-8EIEQjMP.js +0 -1
- package/dist/assets/search-DOsLw-P9.js +0 -1
- package/dist/assets/security-config-CM_tQRXQ.js +0 -1
- package/dist/assets/skeleton-GbHLjPC0.js +0 -1
- package/dist/assets/useMutation-DSinpgEq.js +0 -1
- package/dist/assets/x-Bnco_K8b.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -4,6 +4,7 @@ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
|
4
4
|
import type { SetStateAction } from 'react';
|
|
5
5
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
6
6
|
import { normalizeSessionProjectRootValue } from '@/lib/session-project/session-project.utils';
|
|
7
|
+
import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
7
8
|
|
|
8
9
|
export class ChatSessionListManager {
|
|
9
10
|
constructor(
|
|
@@ -45,8 +46,22 @@ export class ChatSessionListManager {
|
|
|
45
46
|
useChatSessionListStore.getState().setSnapshot({ listMode: value });
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
markSessionRead = (sessionKey: string | null | undefined, updatedAt: string | null | undefined) => {
|
|
50
|
+
if (!sessionKey) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
useChatSessionListStore.getState().markSessionRead(sessionKey, updatedAt);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
hydrateReadWatermarks = (
|
|
57
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
58
|
+
) => {
|
|
59
|
+
useChatSessionListStore.getState().hydrateReadWatermarks(entries);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
createSession = (sessionType?: string, projectRoot?: string | null): string => {
|
|
49
63
|
const { snapshot } = useChatInputStore.getState();
|
|
64
|
+
const { snapshot: sessionListSnapshot } = useChatSessionListStore.getState();
|
|
50
65
|
const { defaultSessionType: configuredDefaultSessionType } = snapshot;
|
|
51
66
|
const defaultSessionType = configuredDefaultSessionType || 'native';
|
|
52
67
|
const nextSessionType =
|
|
@@ -54,14 +69,26 @@ export class ChatSessionListManager {
|
|
|
54
69
|
? sessionType.trim()
|
|
55
70
|
: defaultSessionType;
|
|
56
71
|
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
72
|
+
const nextSessionKey = sessionListSnapshot.draftSessionKey;
|
|
57
73
|
this.streamActionsManager.resetStreamState();
|
|
58
|
-
useChatSessionListStore.getState().setSnapshot({
|
|
74
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
75
|
+
draftSessionKey: createNcpSessionId()
|
|
76
|
+
});
|
|
59
77
|
useChatInputStore.getState().setSnapshot({
|
|
60
78
|
pendingSessionType: nextSessionType,
|
|
61
79
|
pendingProjectRoot: normalizedProjectRoot,
|
|
62
|
-
pendingProjectRootSessionKey: null
|
|
80
|
+
pendingProjectRootSessionKey: normalizedProjectRoot ? nextSessionKey : null
|
|
63
81
|
});
|
|
64
|
-
this.uiManager.
|
|
82
|
+
this.uiManager.goToSession(nextSessionKey);
|
|
83
|
+
return nextSessionKey;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
ensureDraftSession = (sessionType?: string): string => {
|
|
87
|
+
const { snapshot } = useChatSessionListStore.getState();
|
|
88
|
+
if (snapshot.selectedSessionKey) {
|
|
89
|
+
return snapshot.selectedSessionKey;
|
|
90
|
+
}
|
|
91
|
+
return this.createSession(sessionType);
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
selectSession = (sessionKey: string) => {
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
useEffect,
|
|
3
3
|
useMemo,
|
|
4
4
|
useRef,
|
|
5
|
-
useState,
|
|
6
5
|
} from "react";
|
|
7
6
|
import {
|
|
8
7
|
buildNcpRequestEnvelope,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
} from "@/components/chat/chat-session-route";
|
|
23
22
|
import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
|
|
24
23
|
import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
|
|
25
|
-
import { createNcpSessionId } from "@/components/chat/ncp/ncp-session-adapter";
|
|
26
24
|
import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
|
|
27
25
|
import { useNcpChatDerivedState, useNcpChatSnapshotSync } from "@/components/chat/ncp/page/ncp-chat-derived-state";
|
|
28
26
|
import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
|
|
@@ -79,26 +77,39 @@ export function buildNcpSendMetadata(payload: {
|
|
|
79
77
|
return metadata;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
export function shouldClearPendingProjectRootOverride(params: {
|
|
81
|
+
pendingProjectRoot: string | null;
|
|
82
|
+
pendingProjectRootSessionKey: string | null;
|
|
83
|
+
sessionKey: string | null | undefined;
|
|
84
|
+
selectedSessionProjectRoot: string | null | undefined;
|
|
85
85
|
}): boolean {
|
|
86
|
+
const {
|
|
87
|
+
pendingProjectRoot,
|
|
88
|
+
pendingProjectRootSessionKey,
|
|
89
|
+
sessionKey,
|
|
90
|
+
selectedSessionProjectRoot,
|
|
91
|
+
} = params;
|
|
86
92
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
pendingProjectRoot !== null &&
|
|
94
|
+
pendingProjectRootSessionKey !== null &&
|
|
95
|
+
sessionKey === pendingProjectRootSessionKey &&
|
|
96
|
+
(selectedSessionProjectRoot ?? null) === pendingProjectRoot
|
|
90
97
|
);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const presenterRef = useRef<NcpChatPresenter | null>(null);
|
|
102
|
+
if (!presenterRef.current) {
|
|
103
|
+
presenterRef.current = new NcpChatPresenter();
|
|
104
|
+
}
|
|
105
|
+
const presenter = presenterRef.current;
|
|
98
106
|
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
99
107
|
const selectedSessionKey = useChatSessionListStore(
|
|
100
108
|
(state) => state.snapshot.selectedSessionKey,
|
|
101
109
|
);
|
|
110
|
+
const draftSessionKey = useChatSessionListStore(
|
|
111
|
+
(state) => state.snapshot.draftSessionKey,
|
|
112
|
+
);
|
|
102
113
|
const selectedAgentId = useChatSessionListStore(
|
|
103
114
|
(state) => state.snapshot.selectedAgentId,
|
|
104
115
|
);
|
|
@@ -123,21 +134,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
123
134
|
}>();
|
|
124
135
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
125
136
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
126
|
-
const previousSelectedSessionKeyRef = useRef<string | null | undefined>(
|
|
127
|
-
undefined,
|
|
128
|
-
);
|
|
129
137
|
const routeSessionKey = useMemo(
|
|
130
138
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
131
139
|
[routeSessionIdParam],
|
|
132
140
|
);
|
|
133
|
-
const sessionKey = selectedSessionKey ??
|
|
134
|
-
const hasDraftProjectRootOverride =
|
|
135
|
-
pendingProjectRoot !== null &&
|
|
136
|
-
pendingProjectRootSessionKey === null &&
|
|
137
|
-
selectedSessionKey === null;
|
|
141
|
+
const sessionKey = routeSessionKey ?? selectedSessionKey ?? draftSessionKey;
|
|
138
142
|
const hasSessionProjectRootOverride =
|
|
139
143
|
pendingProjectRoot !== null &&
|
|
140
|
-
|
|
144
|
+
pendingProjectRootSessionKey === sessionKey;
|
|
141
145
|
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
142
146
|
? pendingProjectRoot
|
|
143
147
|
: undefined;
|
|
@@ -168,25 +172,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
168
172
|
|
|
169
173
|
const agent = useNcpSessionConversation(sessionKey);
|
|
170
174
|
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
presenter.setDraftSessionId(draftSessionId);
|
|
173
|
-
}, [draftSessionId, presenter]);
|
|
174
|
-
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
if (
|
|
177
|
-
shouldRefreshDraftSessionId({
|
|
178
|
-
previousSelectedSessionKey:
|
|
179
|
-
previousSelectedSessionKeyRef.current,
|
|
180
|
-
nextSelectedSessionKey: selectedSessionKey,
|
|
181
|
-
})
|
|
182
|
-
) {
|
|
183
|
-
const nextDraftSessionId = createNcpSessionId();
|
|
184
|
-
setDraftSessionId(nextDraftSessionId);
|
|
185
|
-
presenter.setDraftSessionId(nextDraftSessionId);
|
|
186
|
-
}
|
|
187
|
-
previousSelectedSessionKeyRef.current = selectedSessionKey;
|
|
188
|
-
}, [presenter, selectedSessionKey]);
|
|
189
|
-
|
|
190
175
|
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
191
176
|
? pendingProjectRoot
|
|
192
177
|
: (selectedSession?.projectRoot ?? null);
|
|
@@ -215,10 +200,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
215
200
|
thinkingLevel: payload.thinkingLevel,
|
|
216
201
|
sessionType: payload.sessionType,
|
|
217
202
|
projectRoot:
|
|
218
|
-
payload.sessionKey === pendingProjectRootSessionKey
|
|
219
|
-
(pendingProjectRoot !== null &&
|
|
220
|
-
pendingProjectRootSessionKey === null &&
|
|
221
|
-
selectedSessionKey === null)
|
|
203
|
+
payload.sessionKey === pendingProjectRootSessionKey
|
|
222
204
|
? pendingProjectRoot
|
|
223
205
|
: (selectedSession?.projectRoot ?? null),
|
|
224
206
|
requestedSkills: payload.requestedSkills,
|
|
@@ -279,15 +261,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
279
261
|
]);
|
|
280
262
|
|
|
281
263
|
useEffect(() => {
|
|
282
|
-
const matchesPendingProjectSession =
|
|
283
|
-
pendingProjectRootSessionKey === null
|
|
284
|
-
? selectedSessionKey !== null
|
|
285
|
-
: pendingProjectRootSessionKey === selectedSession?.key;
|
|
286
264
|
if (
|
|
287
265
|
!selectedSession ||
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
266
|
+
!shouldClearPendingProjectRootOverride({
|
|
267
|
+
pendingProjectRoot,
|
|
268
|
+
pendingProjectRootSessionKey,
|
|
269
|
+
sessionKey: selectedSession.key,
|
|
270
|
+
selectedSessionProjectRoot: selectedSession.projectRoot ?? null,
|
|
271
|
+
})
|
|
291
272
|
) {
|
|
292
273
|
return;
|
|
293
274
|
}
|
|
@@ -55,7 +55,7 @@ describe('ncp-app-client-fetch', () => {
|
|
|
55
55
|
'content-type': 'application/json'
|
|
56
56
|
},
|
|
57
57
|
body: JSON.stringify({ sessionId: 's1' })
|
|
58
|
-
})).rejects.toThrow('Failed to fetch');
|
|
58
|
+
})).rejects.toThrow('NCP fetch failed for POST http://127.0.0.1:55667/api/ncp/agent/abort: Error: Failed to fetch');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('preserves native SSE request headers', async () => {
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
type FetchLike = typeof fetch;
|
|
2
2
|
|
|
3
|
+
function formatFetchTarget(input: RequestInfo | URL): string {
|
|
4
|
+
if (typeof input === 'string') {
|
|
5
|
+
return input;
|
|
6
|
+
}
|
|
7
|
+
if (input instanceof URL) {
|
|
8
|
+
return input.toString();
|
|
9
|
+
}
|
|
10
|
+
return input.url;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatUnknownFetchError(error: unknown): string {
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
const name = error.name?.trim();
|
|
16
|
+
const message = error.message?.trim();
|
|
17
|
+
if (name && message) {
|
|
18
|
+
return `${name}: ${message}`;
|
|
19
|
+
}
|
|
20
|
+
return message || name || 'Unknown Error';
|
|
21
|
+
}
|
|
22
|
+
return String(error ?? 'Unknown error');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createErrorWithCause(message: string, cause: unknown): Error {
|
|
26
|
+
const error = new Error(message) as Error & { cause?: unknown };
|
|
27
|
+
if (cause !== undefined) {
|
|
28
|
+
error.cause = cause;
|
|
29
|
+
}
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
export function createNcpAppClientFetch(): FetchLike {
|
|
4
|
-
return (input, init) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
34
|
+
return async (input, init) => {
|
|
35
|
+
try {
|
|
36
|
+
return await fetch(input, {
|
|
37
|
+
credentials: 'include',
|
|
38
|
+
...init
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const method = (init?.method || 'GET').toUpperCase();
|
|
42
|
+
const target = formatFetchTarget(input);
|
|
43
|
+
throw createErrorWithCause(
|
|
44
|
+
`NCP fetch failed for ${method} ${target}: ${formatUnknownFetchError(error)}`,
|
|
45
|
+
error
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
9
49
|
}
|
|
@@ -18,6 +18,7 @@ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-l
|
|
|
18
18
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
19
19
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
20
20
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
21
|
+
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
21
22
|
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
22
23
|
import { chatRecentModelsManager } from '@/components/chat/chat-recent-models.manager';
|
|
23
24
|
import { chatRecentSkillsManager } from '@/components/chat/chat-recent-skills.manager';
|
|
@@ -40,7 +41,7 @@ export class NcpChatInputManager {
|
|
|
40
41
|
constructor(
|
|
41
42
|
private uiManager: ChatUiManager,
|
|
42
43
|
private streamActionsManager: ChatStreamActionsManager,
|
|
43
|
-
private
|
|
44
|
+
private sessionListManager: ChatSessionListManager
|
|
44
45
|
) {}
|
|
45
46
|
|
|
46
47
|
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
@@ -179,10 +180,7 @@ export class NcpChatInputManager {
|
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
182
|
-
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.
|
|
183
|
-
if (!sessionSnapshot.selectedSessionKey) {
|
|
184
|
-
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
185
|
-
}
|
|
183
|
+
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.sessionListManager.ensureDraftSession(inputSnapshot.selectedSessionType);
|
|
186
184
|
this.setComposerNodes(createInitialChatComposerNodes());
|
|
187
185
|
await this.streamActionsManager.sendMessage({
|
|
188
186
|
message,
|
|
@@ -116,7 +116,6 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
116
116
|
);
|
|
117
117
|
const sessionTypeState = useChatSessionTypeState({
|
|
118
118
|
selectedSession,
|
|
119
|
-
selectedSessionKey: params.sessionKey,
|
|
120
119
|
pendingSessionType: params.pendingSessionType,
|
|
121
120
|
setPendingSessionType: params.setPendingSessionType,
|
|
122
121
|
sessionTypesData: sessionTypesQuery.data
|
|
@@ -11,21 +11,11 @@ export class NcpChatPresenter {
|
|
|
11
11
|
chatInputManager = new NcpChatInputManager(
|
|
12
12
|
this.chatUiManager,
|
|
13
13
|
this.chatStreamActionsManager,
|
|
14
|
-
|
|
14
|
+
this.chatSessionListManager
|
|
15
15
|
);
|
|
16
16
|
chatThreadManager = new NcpChatThreadManager(
|
|
17
17
|
this.chatUiManager,
|
|
18
18
|
this.chatSessionListManager,
|
|
19
19
|
this.chatStreamActionsManager
|
|
20
20
|
);
|
|
21
|
-
|
|
22
|
-
private draftSessionId = '';
|
|
23
|
-
|
|
24
|
-
setDraftSessionId = (sessionId: string) => {
|
|
25
|
-
this.draftSessionId = sessionId;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
private getDraftSessionId(): string {
|
|
29
|
-
return this.draftSessionId;
|
|
30
|
-
}
|
|
31
21
|
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { useMemo } from
|
|
2
|
-
import type { SessionEntryView } from
|
|
3
|
-
import { sessionDisplayName } from
|
|
4
|
-
import { adaptNcpSessionSummaries } from
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { SessionEntryView } from "@/api/types";
|
|
3
|
+
import { sessionDisplayName } from "@/components/chat/chat-session-display";
|
|
4
|
+
import { adaptNcpSessionSummaries } from "@/components/chat/ncp/ncp-session-adapter";
|
|
5
|
+
import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
|
|
6
|
+
import type { ChatChildSessionTab } from "@/components/chat/stores/chat-thread.store";
|
|
7
|
+
import { useNcpSessions } from "@/hooks/useConfig";
|
|
8
|
+
import type { SessionRunStatus } from "@/lib/session-run-status";
|
|
7
9
|
|
|
8
10
|
export type ResolvedChildSessionTab = {
|
|
9
11
|
sessionKey: string;
|
|
10
12
|
parentSessionKey: string | null;
|
|
11
13
|
title: string;
|
|
12
14
|
agentId: string | null;
|
|
15
|
+
updatedAt: string | null;
|
|
16
|
+
runStatus?: SessionRunStatus;
|
|
17
|
+
sessionTypeLabel: string | null;
|
|
18
|
+
preferredModel: string | null;
|
|
19
|
+
projectName: string | null;
|
|
20
|
+
projectRoot: string | null;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
function resolveChildSessionTitle(
|
|
@@ -29,24 +37,42 @@ export function useNcpChildSessionTabsView(
|
|
|
29
37
|
tabs: readonly ChatChildSessionTab[],
|
|
30
38
|
): ResolvedChildSessionTab[] {
|
|
31
39
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
40
|
+
const summaries = useMemo(
|
|
41
|
+
() => sessionsQuery.data?.sessions ?? [],
|
|
42
|
+
[sessionsQuery.data?.sessions],
|
|
43
|
+
);
|
|
32
44
|
|
|
33
45
|
const sessionByKey = useMemo(() => {
|
|
34
|
-
const sessions = adaptNcpSessionSummaries(
|
|
46
|
+
const sessions = adaptNcpSessionSummaries(summaries);
|
|
35
47
|
return new Map(sessions.map((session) => [session.key, session]));
|
|
36
|
-
}, [
|
|
48
|
+
}, [summaries]);
|
|
49
|
+
|
|
50
|
+
const summaryByKey = useMemo(
|
|
51
|
+
() => new Map(summaries.map((summary) => [summary.sessionId, summary])),
|
|
52
|
+
[summaries],
|
|
53
|
+
);
|
|
37
54
|
|
|
38
55
|
return useMemo(
|
|
39
56
|
() =>
|
|
40
57
|
tabs.map((tab) => {
|
|
41
58
|
const session = sessionByKey.get(tab.sessionKey) ?? null;
|
|
59
|
+
const summary = summaryByKey.get(tab.sessionKey) ?? null;
|
|
42
60
|
const agentId = tab.agentId?.trim() || session?.agentId || null;
|
|
43
61
|
return {
|
|
44
62
|
sessionKey: tab.sessionKey,
|
|
45
63
|
parentSessionKey: tab.parentSessionKey,
|
|
46
64
|
title: resolveChildSessionTitle(tab, session),
|
|
47
65
|
agentId,
|
|
66
|
+
updatedAt: session?.updatedAt ?? null,
|
|
67
|
+
runStatus: summary?.status === "running" ? "running" : undefined,
|
|
68
|
+
sessionTypeLabel: session?.sessionType
|
|
69
|
+
? resolveSessionTypeLabel(session.sessionType)
|
|
70
|
+
: null,
|
|
71
|
+
preferredModel: session?.preferredModel?.trim() || null,
|
|
72
|
+
projectName: session?.projectName?.trim() || null,
|
|
73
|
+
projectRoot: session?.projectRoot?.trim() || null,
|
|
48
74
|
};
|
|
49
75
|
}),
|
|
50
|
-
[sessionByKey, tabs],
|
|
76
|
+
[sessionByKey, summaryByKey, tabs],
|
|
51
77
|
);
|
|
52
78
|
}
|
|
@@ -1,33 +1,127 @@
|
|
|
1
|
-
import { create } from 'zustand';
|
|
1
|
+
import { create, type StateCreator } from 'zustand';
|
|
2
|
+
import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
3
|
+
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
2
4
|
|
|
3
5
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
4
6
|
|
|
5
7
|
export type ChatSessionListSnapshot = {
|
|
6
8
|
selectedSessionKey: string | null;
|
|
9
|
+
draftSessionKey: string;
|
|
7
10
|
selectedAgentId: string;
|
|
8
11
|
query: string;
|
|
9
12
|
listMode: ChatSessionListMode;
|
|
10
13
|
};
|
|
11
14
|
|
|
15
|
+
export function hasUnreadSessionUpdate(
|
|
16
|
+
updatedAt: string | null | undefined,
|
|
17
|
+
readUpdatedAt: string | undefined,
|
|
18
|
+
): boolean {
|
|
19
|
+
const normalizedUpdatedAt = updatedAt?.trim();
|
|
20
|
+
if (!normalizedUpdatedAt) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const normalizedReadUpdatedAt = readUpdatedAt?.trim();
|
|
24
|
+
if (!normalizedReadUpdatedAt) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return normalizedUpdatedAt.localeCompare(normalizedReadUpdatedAt) > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldShowUnreadSessionIndicator(params: {
|
|
31
|
+
active: boolean;
|
|
32
|
+
updatedAt: string | null | undefined;
|
|
33
|
+
readUpdatedAt: string | undefined;
|
|
34
|
+
runStatus?: SessionRunStatus;
|
|
35
|
+
}): boolean {
|
|
36
|
+
const { active, readUpdatedAt, runStatus, updatedAt } = params;
|
|
37
|
+
if (active || runStatus === 'running') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return hasUnreadSessionUpdate(updatedAt, readUpdatedAt);
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
type ChatSessionListStore = {
|
|
13
44
|
snapshot: ChatSessionListSnapshot;
|
|
45
|
+
readUpdatedAtBySessionKey: Record<string, string>;
|
|
46
|
+
hasHydratedReadWatermarks: boolean;
|
|
14
47
|
setSnapshot: (patch: Partial<ChatSessionListSnapshot>) => void;
|
|
48
|
+
markSessionRead: (sessionKey: string, updatedAt: string | null | undefined) => void;
|
|
49
|
+
hydrateReadWatermarks: (
|
|
50
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
51
|
+
) => void;
|
|
15
52
|
};
|
|
16
53
|
|
|
54
|
+
type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0];
|
|
55
|
+
|
|
17
56
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
18
57
|
selectedSessionKey: null,
|
|
58
|
+
draftSessionKey: createNcpSessionId(),
|
|
19
59
|
selectedAgentId: 'main',
|
|
20
60
|
query: '',
|
|
21
61
|
listMode: 'time-first'
|
|
22
62
|
};
|
|
23
63
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setSnapshot: (patch) =>
|
|
64
|
+
function createSetSnapshotAction(set: ChatSessionListStoreSet) {
|
|
65
|
+
return (patch: Partial<ChatSessionListSnapshot>) =>
|
|
27
66
|
set((state) => ({
|
|
28
67
|
snapshot: {
|
|
29
68
|
...state.snapshot,
|
|
30
69
|
...patch
|
|
31
70
|
}
|
|
32
|
-
}))
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createMarkSessionReadAction(set: ChatSessionListStoreSet) {
|
|
75
|
+
return (sessionKey: string, updatedAt: string | null | undefined) =>
|
|
76
|
+
set((state) => {
|
|
77
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
78
|
+
const normalizedUpdatedAt = updatedAt?.trim();
|
|
79
|
+
if (!normalizedSessionKey || !normalizedUpdatedAt) {
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
if (state.readUpdatedAtBySessionKey[normalizedSessionKey] === normalizedUpdatedAt) {
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
readUpdatedAtBySessionKey: {
|
|
88
|
+
...state.readUpdatedAtBySessionKey,
|
|
89
|
+
[normalizedSessionKey]: normalizedUpdatedAt
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createHydrateReadWatermarksAction(set: ChatSessionListStoreSet) {
|
|
96
|
+
return (
|
|
97
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
98
|
+
) =>
|
|
99
|
+
set((state) => {
|
|
100
|
+
if (state.hasHydratedReadWatermarks) {
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
const nextReadUpdatedAtBySessionKey = { ...state.readUpdatedAtBySessionKey };
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const normalizedSessionKey = entry.sessionKey.trim();
|
|
106
|
+
const normalizedUpdatedAt = entry.updatedAt?.trim();
|
|
107
|
+
if (!normalizedSessionKey || !normalizedUpdatedAt || nextReadUpdatedAtBySessionKey[normalizedSessionKey]) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
nextReadUpdatedAtBySessionKey[normalizedSessionKey] = normalizedUpdatedAt;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
hasHydratedReadWatermarks: true,
|
|
115
|
+
readUpdatedAtBySessionKey: nextReadUpdatedAtBySessionKey
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
|
|
121
|
+
snapshot: initialSnapshot,
|
|
122
|
+
readUpdatedAtBySessionKey: {},
|
|
123
|
+
hasHydratedReadWatermarks: false,
|
|
124
|
+
setSnapshot: createSetSnapshotAction(set),
|
|
125
|
+
markSessionRead: createMarkSessionReadAction(set),
|
|
126
|
+
hydrateReadWatermarks: createHydrateReadWatermarksAction(set)
|
|
33
127
|
}));
|
|
@@ -17,7 +17,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
17
17
|
const { result } = renderHook(() =>
|
|
18
18
|
useChatSessionTypeState({
|
|
19
19
|
selectedSession: null,
|
|
20
|
-
selectedSessionKey: null,
|
|
21
20
|
pendingSessionType: 'codex-sdk',
|
|
22
21
|
setPendingSessionType,
|
|
23
22
|
sessionTypesData: {
|
|
@@ -40,7 +39,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
40
39
|
renderHook(() =>
|
|
41
40
|
useChatSessionTypeState({
|
|
42
41
|
selectedSession: null,
|
|
43
|
-
selectedSessionKey: null,
|
|
44
42
|
pendingSessionType: '',
|
|
45
43
|
setPendingSessionType,
|
|
46
44
|
sessionTypesData: {
|
|
@@ -62,7 +60,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
62
60
|
const { result } = renderHook(() =>
|
|
63
61
|
useChatSessionTypeState({
|
|
64
62
|
selectedSession: null,
|
|
65
|
-
selectedSessionKey: null,
|
|
66
63
|
pendingSessionType: 'claude',
|
|
67
64
|
setPendingSessionType,
|
|
68
65
|
sessionTypesData: {
|
|
@@ -22,7 +22,6 @@ export type ChatSessionTypeOption = {
|
|
|
22
22
|
|
|
23
23
|
type UseChatSessionTypeStateParams = {
|
|
24
24
|
selectedSession: SessionEntryView | null;
|
|
25
|
-
selectedSessionKey: string | null;
|
|
26
25
|
pendingSessionType: string;
|
|
27
26
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
28
27
|
sessionTypesData?: {
|
|
@@ -114,7 +113,6 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
114
113
|
} {
|
|
115
114
|
const {
|
|
116
115
|
selectedSession,
|
|
117
|
-
selectedSessionKey,
|
|
118
116
|
pendingSessionType,
|
|
119
117
|
setPendingSessionType,
|
|
120
118
|
sessionTypesData
|
|
@@ -164,7 +162,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
164
162
|
);
|
|
165
163
|
|
|
166
164
|
useEffect(() => {
|
|
167
|
-
if (
|
|
165
|
+
if (selectedSession) {
|
|
168
166
|
return;
|
|
169
167
|
}
|
|
170
168
|
const rawPending = typeof pendingSessionType === 'string' ? pendingSessionType.trim() : '';
|
|
@@ -181,9 +179,9 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
181
179
|
return;
|
|
182
180
|
}
|
|
183
181
|
setPendingSessionType(defaultSessionType);
|
|
184
|
-
}, [defaultSessionType, pendingSessionType,
|
|
182
|
+
}, [defaultSessionType, pendingSessionType, selectedSession, setPendingSessionType]);
|
|
185
183
|
|
|
186
|
-
const canEditSessionType = !
|
|
184
|
+
const canEditSessionType = !selectedSession || Boolean(selectedSession.sessionTypeMutable);
|
|
187
185
|
const availableSessionTypeSet = useMemo(
|
|
188
186
|
() => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
|
|
189
187
|
[runtimeSessionTypeOptions]
|