@nextclaw/ui 0.12.10 → 0.12.11
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 +51 -10
- package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
- package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
- package/dist/assets/{DocBrowser-DMfr0Oow.js → DocBrowser-rDOjI3ga.js} +1 -1
- package/dist/assets/{DocBrowserContext-BXydqby-.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
- package/dist/assets/{LogoBadge-hO7tY7hE.js → LogoBadge-DP8Ye7wJ.js} +1 -1
- package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
- package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
- package/dist/assets/ProvidersList-BCupBayq.js +1 -0
- package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
- package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
- package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
- package/dist/assets/SessionsConfig-CvjxU40H.js +2 -0
- package/dist/assets/{book-open-DzdUViDm.js → book-open-BE8M56IM.js} +1 -1
- package/dist/assets/chat-page-JKC6ln-y.js +58 -0
- package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-C5dEc8hV.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
- package/dist/assets/client-CszWMVKi.js +7 -0
- package/dist/assets/{config-split-page-BUout_Ak.js → config-split-page-BAGSzUR3.js} +1 -1
- package/dist/assets/{createLucideIcon-dy5ie7Ox.js → createLucideIcon-CCiTGX8L.js} +1 -1
- package/dist/assets/desktop-DfkLlkG2.js +1 -0
- package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
- package/dist/assets/dialog-BghZFPch.js +5 -0
- package/dist/assets/{dist-Cy7_j6hA.js → dist-Dd9cr-kz.js} +1 -1
- package/dist/assets/dist-ZwoAXs46.js +9 -0
- package/dist/assets/{download-BD0ETkB-.js → download-D7LOizcW.js} +1 -1
- package/dist/assets/es2015-CEAreese.js +41 -0
- package/dist/assets/{external-link-kZSAO8nT.js → external-link-qsnCMhw1.js} +1 -1
- package/dist/assets/{hash-BHJC2Ovu.js → hash-0zjWsNl-.js} +1 -1
- package/dist/assets/{i18n-CpTZLchQ.js → i18n-DvzXOGQX.js} +1 -1
- package/dist/assets/index-DvVTC9FF.css +1 -0
- package/dist/assets/index-lr6rQUSd.js +2 -0
- package/dist/assets/key-round-BLe9D8ND.js +1 -0
- package/dist/assets/loader-circle-wj7kARHv.js +1 -0
- package/dist/assets/{logos-B7gRObP8.js → logos-_v5b2SdG.js} +1 -1
- package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
- package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
- package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
- package/dist/assets/play-o6NmwGTi.js +1 -0
- package/dist/assets/plus-I9pBS4Fl.js +1 -0
- package/dist/assets/{refresh-cw-Bcv40SXy.js → refresh-cw-MNqgR3LZ.js} +1 -1
- package/dist/assets/remote-C9fXm4V5.js +1 -0
- package/dist/assets/{save-EqJPOF0G.js → save-D4bObrmH.js} +1 -1
- package/dist/assets/search-DxmL3IWE.js +1 -0
- package/dist/assets/security-config-BUm6FFfl.js +1 -0
- package/dist/assets/select-BILPf7zs.js +1 -0
- package/dist/assets/setting-row-BATDgg4r.js +1 -0
- package/dist/assets/skeleton-COKMAnJy.js +1 -0
- package/dist/assets/{switch-CM29eCAR.js → switch-CBOzecWS.js} +1 -1
- package/dist/assets/{tabs-custom-YcZUWn3o.js → tabs-custom-Bx3cNhD-.js} +1 -1
- package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
- package/dist/assets/{trash-2-mJT6oWa2.js → trash-2-CQUgYyRn.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
- package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
- package/dist/assets/{useMutation-CNcz2fgt.js → useMutation-__AYv-Pz.js} +1 -1
- package/dist/assets/x-BHUGQIUv.js +1 -0
- package/dist/index.html +22 -22
- package/module-structure.config.json +7 -0
- package/package.json +5 -5
- package/src/api/config.ts +10 -0
- package/src/api/raw-client.test.ts +1 -1
- package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
- package/src/api/types.ts +40 -0
- package/src/app/components/app-manager-provider.tsx +20 -0
- package/src/app/managers/app.manager.ts +12 -0
- package/src/app.tsx +8 -8
- package/src/components/chat/chat-conversation-panel.test.tsx +10 -0
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
- package/src/components/chat/chat-page-shell.tsx +1 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
- package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
- package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +13 -37
- package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
- package/src/components/chat/ncp/ncp-chat-page.tsx +21 -2
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
- package/src/components/config/desktop-update-config.test.tsx +10 -4
- package/src/components/config/desktop-update-config.tsx +5 -3
- package/src/components/config/runtime-control-card.test.tsx +119 -197
- package/src/components/config/runtime-control-card.tsx +20 -70
- package/src/components/config/runtime-presence-card.test.tsx +10 -14
- package/src/components/config/runtime-presence-card.tsx +7 -5
- package/src/components/layout/Sidebar.tsx +4 -4
- package/src/components/layout/runtime-status-entry.test.tsx +45 -101
- package/src/components/layout/runtime-status-entry.tsx +15 -63
- package/src/components/layout/sidebar.layout.test.tsx +11 -5
- package/src/{account → features/account}/components/account-panel.tsx +13 -13
- package/src/features/account/index.ts +6 -0
- package/src/{account → features/account}/managers/account.manager.ts +3 -3
- package/src/{components/remote → features/remote/components}/remote-access-page.test.tsx +4 -5
- package/src/{components/remote → features/remote/components}/remote-access-page.tsx +15 -13
- package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
- package/src/features/remote/index.ts +27 -0
- package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
- package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
- package/src/features/system-status/hooks/use-system-status.ts +104 -0
- package/src/features/system-status/index.ts +12 -0
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
- package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
- package/src/features/system-status/managers/system-status.manager.ts +511 -0
- package/src/features/system-status/stores/system-status.store.ts +32 -0
- package/src/features/system-status/types/system-status.types.ts +73 -0
- package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
- package/src/features/system-status/utils/system-status.utils.ts +202 -0
- package/src/hooks/use-realtime-query-bridge.ts +34 -18
- package/src/lib/i18n.chat.ts +8 -0
- package/src/platforms/desktop/index.ts +20 -0
- package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
- package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
- package/src/stores/ui.store.ts +0 -9
- package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
- package/src/transport/app-client.test.ts +9 -5
- package/src/transport/index.ts +1 -1
- package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
- package/dist/assets/ChannelsList-M9FTK1Ak.js +0 -8
- package/dist/assets/DocBrowser-CH7-GxlL.js +0 -1
- package/dist/assets/ModelConfig-CNIgLf0e.js +0 -1
- package/dist/assets/ProviderScopedModelInput-B3HWP4oz.js +0 -1
- package/dist/assets/ProvidersList-CHjMnRhX.js +0 -1
- package/dist/assets/RuntimeConfig-psp8nMSG.js +0 -1
- package/dist/assets/SearchConfig-CSoKip1f.js +0 -1
- package/dist/assets/SecretsConfig-MEt6MjuD.js +0 -3
- package/dist/assets/SessionsConfig-DifCiXwR.js +0 -2
- package/dist/assets/app-query-client-9jNewezV.js +0 -1
- package/dist/assets/chat-page-CLp0UV0Y.js +0 -58
- package/dist/assets/chat-session-display-DsYHx0RZ.js +0 -1
- package/dist/assets/client-C-8fH7-c.js +0 -7
- package/dist/assets/config-CBScxsdV.js +0 -1
- package/dist/assets/desktop-update-config-2BS6BMkW.js +0 -1
- package/dist/assets/dist-BruyLa92.js +0 -9
- package/dist/assets/index-mW8W2FUu.css +0 -1
- package/dist/assets/index-zDZfXoI4.js +0 -6
- package/dist/assets/infiniteQueryBehavior-CyER9hv0.js +0 -1
- package/dist/assets/loader-circle-Bc2gCU33.js +0 -1
- package/dist/assets/marketplace-page-3qVMnF3d.js +0 -1
- package/dist/assets/marketplace-page-BhFIeQzI.js +0 -49
- package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +0 -40
- package/dist/assets/page-layout-0UcO9H9Z.js +0 -1
- package/dist/assets/play-CKDjSQFL.js +0 -1
- package/dist/assets/plus-CG0QrVY_.js +0 -1
- package/dist/assets/refresh-ccw-COVhNHtN.js +0 -1
- package/dist/assets/remote-access-page-CWHG-sug.js +0 -1
- package/dist/assets/rotate-cw-oHMKJMC8.js +0 -1
- package/dist/assets/search-BCAlB8nz.js +0 -1
- package/dist/assets/security-config-Slh0Mayz.js +0 -1
- package/dist/assets/select-CVz0t7MF.js +0 -41
- package/dist/assets/setting-row-CbVHAuQt.js +0 -1
- package/dist/assets/skeleton-D5rdKvzy.js +0 -1
- package/dist/assets/status-dot-DpPtVzQT.js +0 -1
- package/dist/assets/tag-chip-DMXdnLcj.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-DJ1L81Dz.js +0 -1
- package/dist/assets/useConfirmDialog-BsVuqu1x.js +0 -1
- package/dist/assets/x-Czwxm82I.js +0 -1
- package/src/hooks/use-runtime-control.ts +0 -24
- package/src/presenter/app-presenter-context.tsx +0 -20
- package/src/presenter/app.presenter.ts +0 -12
- package/src/runtime-control/runtime-control.manager.ts +0 -118
- /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
- /package/src/{account → features/account}/stores/account.store.ts +0 -0
- /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
- /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
- /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
- /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
2
|
+
|
|
3
|
+
type NcpChatInputAvailabilitySnapshot = Pick<
|
|
4
|
+
ChatInputSnapshot,
|
|
5
|
+
'isProviderStateResolved' | 'modelOptions' | 'sessionTypeUnavailable'
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function hasNcpChatModelOptions(
|
|
9
|
+
snapshot: NcpChatInputAvailabilitySnapshot
|
|
10
|
+
): boolean {
|
|
11
|
+
return snapshot.modelOptions.length > 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isNcpChatModelOptionsLoading(
|
|
15
|
+
snapshot: NcpChatInputAvailabilitySnapshot
|
|
16
|
+
): boolean {
|
|
17
|
+
return !snapshot.isProviderStateResolved && !hasNcpChatModelOptions(snapshot);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isNcpChatModelOptionsEmpty(
|
|
21
|
+
snapshot: NcpChatInputAvailabilitySnapshot
|
|
22
|
+
): boolean {
|
|
23
|
+
return snapshot.isProviderStateResolved && !hasNcpChatModelOptions(snapshot);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isNcpChatComposerDisabled(
|
|
27
|
+
snapshot: NcpChatInputAvailabilitySnapshot
|
|
28
|
+
): boolean {
|
|
29
|
+
return snapshot.sessionTypeUnavailable;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isNcpChatSendDisabled(params: {
|
|
33
|
+
hasSendableDraft: boolean;
|
|
34
|
+
snapshot: NcpChatInputAvailabilitySnapshot;
|
|
35
|
+
isRuntimeBlocked: boolean;
|
|
36
|
+
}): boolean {
|
|
37
|
+
const { hasSendableDraft, isRuntimeBlocked, snapshot } = params;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
isRuntimeBlocked ||
|
|
41
|
+
!hasSendableDraft ||
|
|
42
|
+
!hasNcpChatModelOptions(snapshot) ||
|
|
43
|
+
snapshot.sessionTypeUnavailable
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
3
|
-
import { ChatSidebar } from "@/components/chat/
|
|
3
|
+
import { ChatSidebar } from "@/components/chat/containers/chat-sidebar";
|
|
4
4
|
import { ChatConversationPanel } from "@/components/chat/chat-conversation-panel";
|
|
5
5
|
import { AgentsPage } from "@/components/agents/agents-page";
|
|
6
6
|
import { CronConfig } from "@/components/config/CronConfig";
|
|
@@ -16,6 +16,13 @@ import {
|
|
|
16
16
|
type ChatThinkingLevel
|
|
17
17
|
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
18
18
|
import { deriveSelectedSkillsFromComposer } from '@/components/chat/chat-composer-state';
|
|
19
|
+
import {
|
|
20
|
+
hasNcpChatModelOptions,
|
|
21
|
+
isNcpChatComposerDisabled,
|
|
22
|
+
isNcpChatModelOptionsEmpty,
|
|
23
|
+
isNcpChatModelOptionsLoading,
|
|
24
|
+
isNcpChatSendDisabled,
|
|
25
|
+
} from '@/components/chat/chat-input/ncp-chat-input-availability.utils';
|
|
19
26
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
20
27
|
import {
|
|
21
28
|
CHAT_RECENT_MODELS_MIN_OPTIONS,
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
} from '@/components/chat/chat-recent-skills.manager';
|
|
28
35
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
29
36
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
37
|
+
import { useChatRuntimeAvailability } from '@/features/system-status';
|
|
30
38
|
import type { SessionSkillEntryView } from '@/api/types';
|
|
31
39
|
import { t } from '@/lib/i18n';
|
|
32
40
|
import { toast } from 'sonner';
|
|
@@ -83,11 +91,13 @@ export function ChatInputBarContainer() {
|
|
|
83
91
|
const presenter = usePresenter();
|
|
84
92
|
const { language } = useI18n();
|
|
85
93
|
const snapshot = useChatInputStore((state) => state.snapshot);
|
|
94
|
+
const runtimeAvailability = useChatRuntimeAvailability();
|
|
86
95
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
|
87
96
|
const inputBarRef = useRef<ChatInputBarHandle | null>(null);
|
|
88
97
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
89
98
|
|
|
90
99
|
const skillScopeLabels = useMemo<Record<'builtin' | 'project' | 'workspace', string>>(() => {
|
|
100
|
+
void language;
|
|
91
101
|
return {
|
|
92
102
|
builtin: t('chatSkillScopeBuiltin'),
|
|
93
103
|
project: t('chatSkillScopeProject'),
|
|
@@ -96,6 +106,7 @@ export function ChatInputBarContainer() {
|
|
|
96
106
|
}, [language]);
|
|
97
107
|
const slashTexts = useMemo(
|
|
98
108
|
() => {
|
|
109
|
+
void language;
|
|
99
110
|
return {
|
|
100
111
|
slashSkillSubtitle: t('chatSlashTypeSkill'),
|
|
101
112
|
slashSkillSpecLabel: t('chatSlashSkillSpec'),
|
|
@@ -124,17 +135,14 @@ export function ChatInputBarContainer() {
|
|
|
124
135
|
minAvailableCount: CHAT_RECENT_SKILLS_MIN_OPTIONS
|
|
125
136
|
});
|
|
126
137
|
|
|
127
|
-
const hasModelOptions =
|
|
128
|
-
const isModelOptionsLoading =
|
|
129
|
-
const isModelOptionsEmpty = snapshot
|
|
130
|
-
const inputDisabled =
|
|
131
|
-
((isModelOptionsLoading || isModelOptionsEmpty) && !snapshot.isSending) || snapshot.sessionTypeUnavailable;
|
|
138
|
+
const hasModelOptions = hasNcpChatModelOptions(snapshot);
|
|
139
|
+
const isModelOptionsLoading = isNcpChatModelOptionsLoading(snapshot);
|
|
140
|
+
const isModelOptionsEmpty = isNcpChatModelOptionsEmpty(snapshot);
|
|
141
|
+
const inputDisabled = isNcpChatComposerDisabled(snapshot);
|
|
132
142
|
const attachmentSupported = typeof presenter.chatInputManager.addAttachments === 'function';
|
|
133
|
-
const textareaPlaceholder =
|
|
134
|
-
? ''
|
|
135
|
-
:
|
|
136
|
-
? t('chatInputPlaceholder')
|
|
137
|
-
: t('chatModelNoOptions');
|
|
143
|
+
const textareaPlaceholder = isModelOptionsEmpty
|
|
144
|
+
? t('chatModelNoOptions')
|
|
145
|
+
: t('chatInputPlaceholder');
|
|
138
146
|
const recentModelsLabel = t('chatPickerRecentModels');
|
|
139
147
|
const allModelsLabel = t('chatPickerAllModels');
|
|
140
148
|
const recentSkillsLabel = t('chatPickerRecent');
|
|
@@ -298,10 +306,14 @@ export function ChatInputBarContainer() {
|
|
|
298
306
|
],
|
|
299
307
|
skillPicker,
|
|
300
308
|
actions: {
|
|
301
|
-
sendError: snapshot.sendError,
|
|
309
|
+
sendError: runtimeAvailability.isBlocked ? null : snapshot.sendError,
|
|
302
310
|
isSending: snapshot.isSending,
|
|
303
311
|
canStopGeneration: snapshot.canStopGeneration,
|
|
304
|
-
sendDisabled:
|
|
312
|
+
sendDisabled: isNcpChatSendDisabled({
|
|
313
|
+
snapshot,
|
|
314
|
+
hasSendableDraft,
|
|
315
|
+
isRuntimeBlocked: runtimeAvailability.isBlocked,
|
|
316
|
+
}),
|
|
305
317
|
stopDisabled: !snapshot.canStopGeneration,
|
|
306
318
|
stopHint: resolvedStopHint,
|
|
307
319
|
sendButtonLabel: t('chatSend'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { ChatSidebar } from '@/components/chat/
|
|
4
|
+
import { ChatSidebar } from '@/components/chat/containers/chat-sidebar';
|
|
5
5
|
import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
6
6
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
7
7
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
@@ -107,9 +107,10 @@ vi.mock('@/components/providers/ThemeProvider', () => ({
|
|
|
107
107
|
})
|
|
108
108
|
}));
|
|
109
109
|
|
|
110
|
-
vi.mock('@/
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
vi.mock('@/features/system-status', () => ({
|
|
111
|
+
useSystemStatus: () => ({
|
|
112
|
+
connectionStatus: 'connected'
|
|
113
|
+
})
|
|
113
114
|
}));
|
|
114
115
|
|
|
115
116
|
function resetSidebarTestState() {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type ChatSidebarProjectGroup
|
|
15
15
|
} from '@/components/chat/chat-sidebar-project-groups';
|
|
16
16
|
import { resolveSessionContextView } from '@/lib/session-context.utils';
|
|
17
|
-
import {
|
|
17
|
+
import { useChatSidebarSessionLabelEditor } from '@/components/chat/hooks/use-chat-sidebar-session-label-editor';
|
|
18
18
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
19
19
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
20
20
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
@@ -31,7 +31,7 @@ import { useI18n } from '@/components/providers/I18nProvider';
|
|
|
31
31
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
32
32
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
33
33
|
import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
|
|
34
|
-
import {
|
|
34
|
+
import { useSystemStatus } from '@/features/system-status';
|
|
35
35
|
import {
|
|
36
36
|
AlarmClock,
|
|
37
37
|
Bot,
|
|
@@ -169,22 +169,17 @@ function useChatSessionUnreadState(
|
|
|
169
169
|
|
|
170
170
|
return optimisticReadAtBySessionKey;
|
|
171
171
|
}
|
|
172
|
-
|
|
173
172
|
export function ChatSidebar() {
|
|
174
173
|
const presenter = usePresenter();
|
|
175
174
|
const docBrowser = useDocBrowser();
|
|
176
175
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
177
|
-
const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
|
|
178
|
-
const [draftLabel, setDraftLabel] = useState('');
|
|
179
|
-
const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
|
|
180
176
|
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
181
177
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
182
|
-
const
|
|
178
|
+
const systemStatus = useSystemStatus();
|
|
183
179
|
const agentsQuery = useAgents();
|
|
184
180
|
const { isLoading, items } = useNcpSessionListView();
|
|
185
181
|
const { language, setLanguage } = useI18n();
|
|
186
182
|
const { theme, setTheme } = useTheme();
|
|
187
|
-
const updateSessionLabel = useChatSessionLabel();
|
|
188
183
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
189
184
|
const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
190
185
|
const agentsById = useMemo(
|
|
@@ -221,39 +216,20 @@ export function ChatSidebar() {
|
|
|
221
216
|
listSnapshot.selectedSessionKey,
|
|
222
217
|
presenter.chatSessionListManager.markSessionRead,
|
|
223
218
|
);
|
|
219
|
+
const {
|
|
220
|
+
editingSessionKey,
|
|
221
|
+
draftLabel,
|
|
222
|
+
savingSessionKey,
|
|
223
|
+
setDraftLabel,
|
|
224
|
+
startEditingSessionLabel,
|
|
225
|
+
cancelEditingSessionLabel,
|
|
226
|
+
saveSessionLabel,
|
|
227
|
+
} = useChatSidebarSessionLabelEditor();
|
|
224
228
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
225
229
|
if (language === nextLang) return;
|
|
226
230
|
setLanguage(nextLang);
|
|
227
231
|
window.location.reload();
|
|
228
232
|
};
|
|
229
|
-
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
230
|
-
setEditingSessionKey(session.key);
|
|
231
|
-
setDraftLabel(session.label?.trim() ?? '');
|
|
232
|
-
};
|
|
233
|
-
const cancelEditingSessionLabel = () => {
|
|
234
|
-
setEditingSessionKey(null);
|
|
235
|
-
setDraftLabel('');
|
|
236
|
-
setSavingSessionKey(null);
|
|
237
|
-
};
|
|
238
|
-
const saveSessionLabel = async (session: SessionEntryView) => {
|
|
239
|
-
const normalizedLabel = draftLabel.trim();
|
|
240
|
-
const currentLabel = session.label?.trim() ?? '';
|
|
241
|
-
if (normalizedLabel === currentLabel) {
|
|
242
|
-
cancelEditingSessionLabel();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
setSavingSessionKey(session.key);
|
|
247
|
-
try {
|
|
248
|
-
await updateSessionLabel({
|
|
249
|
-
sessionKey: session.key,
|
|
250
|
-
label: normalizedLabel || null
|
|
251
|
-
});
|
|
252
|
-
cancelEditingSessionLabel();
|
|
253
|
-
} catch {
|
|
254
|
-
setSavingSessionKey(null);
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
233
|
const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
|
|
258
234
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
259
235
|
const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
|
|
@@ -306,7 +282,7 @@ export function ChatSidebar() {
|
|
|
306
282
|
<div className="px-5 pt-5 pb-3">
|
|
307
283
|
<BrandHeader
|
|
308
284
|
className="flex items-center gap-2.5 min-w-0"
|
|
309
|
-
suffix={<StatusBadge status={connectionStatus} />}
|
|
285
|
+
suffix={<StatusBadge status={systemStatus.connectionStatus} />}
|
|
310
286
|
/>
|
|
311
287
|
</div>
|
|
312
288
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
import { useChatSessionLabel } from '@/components/chat/hooks/use-chat-session-label';
|
|
4
|
+
|
|
5
|
+
export function useChatSidebarSessionLabelEditor() {
|
|
6
|
+
const updateSessionLabel = useChatSessionLabel();
|
|
7
|
+
const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
|
|
8
|
+
const [draftLabel, setDraftLabel] = useState('');
|
|
9
|
+
const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
12
|
+
setEditingSessionKey(session.key);
|
|
13
|
+
setDraftLabel(session.label?.trim() ?? '');
|
|
14
|
+
};
|
|
15
|
+
const cancelEditingSessionLabel = () => {
|
|
16
|
+
setEditingSessionKey(null);
|
|
17
|
+
setDraftLabel('');
|
|
18
|
+
setSavingSessionKey(null);
|
|
19
|
+
};
|
|
20
|
+
const saveSessionLabel = async (session: SessionEntryView) => {
|
|
21
|
+
const normalizedLabel = draftLabel.trim();
|
|
22
|
+
const currentLabel = session.label?.trim() ?? '';
|
|
23
|
+
if (normalizedLabel === currentLabel) {
|
|
24
|
+
cancelEditingSessionLabel();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSavingSessionKey(session.key);
|
|
29
|
+
try {
|
|
30
|
+
await updateSessionLabel({
|
|
31
|
+
sessionKey: session.key,
|
|
32
|
+
label: normalizedLabel || null,
|
|
33
|
+
});
|
|
34
|
+
cancelEditingSessionLabel();
|
|
35
|
+
} catch {
|
|
36
|
+
setSavingSessionKey(null);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
editingSessionKey,
|
|
42
|
+
draftLabel,
|
|
43
|
+
savingSessionKey,
|
|
44
|
+
setDraftLabel,
|
|
45
|
+
startEditingSessionLabel,
|
|
46
|
+
cancelEditingSessionLabel,
|
|
47
|
+
saveSessionLabel,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { systemStatusManager } from '@/features/system-status';
|
|
2
|
+
|
|
1
3
|
type FetchLike = typeof fetch;
|
|
2
4
|
|
|
3
5
|
function formatFetchTarget(input: RequestInfo | URL): string {
|
|
@@ -38,6 +40,7 @@ export function createNcpAppClientFetch(): FetchLike {
|
|
|
38
40
|
...init
|
|
39
41
|
});
|
|
40
42
|
} catch (error) {
|
|
43
|
+
systemStatusManager.reportTransportFailure(formatUnknownFetchError(error));
|
|
41
44
|
const method = (init?.method || 'GET').toUpperCase();
|
|
42
45
|
const target = formatFetchTarget(input);
|
|
43
46
|
throw createErrorWithCause(
|
|
@@ -21,10 +21,12 @@ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-s
|
|
|
21
21
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
22
22
|
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
23
23
|
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
24
|
+
import { isNcpChatSendDisabled } from '@/components/chat/chat-input/ncp-chat-input-availability.utils';
|
|
24
25
|
import { chatRecentModelsManager } from '@/components/chat/chat-recent-models.manager';
|
|
25
26
|
import { chatRecentSkillsManager } from '@/components/chat/chat-recent-skills.manager';
|
|
26
27
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
27
28
|
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
29
|
+
import { systemStatusManager } from '@/features/system-status';
|
|
28
30
|
|
|
29
31
|
export class NcpChatInputManager {
|
|
30
32
|
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
@@ -139,7 +141,7 @@ export class NcpChatInputManager {
|
|
|
139
141
|
if (attachments.length === 0) {
|
|
140
142
|
return [];
|
|
141
143
|
}
|
|
142
|
-
const snapshot = useChatInputStore.getState()
|
|
144
|
+
const { snapshot } = useChatInputStore.getState();
|
|
143
145
|
const existingSignatures = new Set(snapshot.attachments.map(this.buildAttachmentSignature));
|
|
144
146
|
const nextAttachments = this.dedupeAttachments([...snapshot.attachments, ...attachments]);
|
|
145
147
|
const insertedAttachments = nextAttachments.filter(
|
|
@@ -173,12 +175,18 @@ export class NcpChatInputManager {
|
|
|
173
175
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
174
176
|
const threadSnapshot = useChatThreadStore.getState().snapshot;
|
|
175
177
|
const message = inputSnapshot.draft.trim();
|
|
176
|
-
const attachments = inputSnapshot
|
|
178
|
+
const { attachments } = inputSnapshot;
|
|
177
179
|
const parts = deriveNcpMessagePartsFromComposer(inputSnapshot.composerNodes, attachments);
|
|
178
180
|
const hasSendableContent = parts.some(
|
|
179
181
|
(part) => part.type !== 'text' || part.text.trim().length > 0
|
|
180
182
|
);
|
|
181
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
isNcpChatSendDisabled({
|
|
185
|
+
snapshot: inputSnapshot,
|
|
186
|
+
hasSendableDraft: hasSendableContent,
|
|
187
|
+
isRuntimeBlocked: systemStatusManager.isChatInteractionBlocked(),
|
|
188
|
+
})
|
|
189
|
+
) {
|
|
182
190
|
return;
|
|
183
191
|
}
|
|
184
192
|
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
@@ -237,7 +245,7 @@ export class NcpChatInputManager {
|
|
|
237
245
|
};
|
|
238
246
|
|
|
239
247
|
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
240
|
-
const snapshot = useChatInputStore.getState()
|
|
248
|
+
const { snapshot } = useChatInputStore.getState();
|
|
241
249
|
const { selectedSkills: prev } = snapshot;
|
|
242
250
|
const value = this.resolveUpdateValue(prev, next);
|
|
243
251
|
if (this.isSameStringArray(value, prev)) {
|
|
@@ -292,7 +300,7 @@ export class NcpChatInputManager {
|
|
|
292
300
|
};
|
|
293
301
|
|
|
294
302
|
private reconcileThinkingForModel = (model: string): void => {
|
|
295
|
-
const snapshot = useChatInputStore.getState()
|
|
303
|
+
const { snapshot } = useChatInputStore.getState();
|
|
296
304
|
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
297
305
|
const { selectedThinkingLevel } = snapshot;
|
|
298
306
|
const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
} from "@/components/chat/chat-session-route";
|
|
23
23
|
import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
|
|
24
24
|
import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
isNcpAgentStartupUnavailableErrorMessage,
|
|
27
|
+
useNcpSessionConversation,
|
|
28
|
+
} from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
|
|
26
29
|
import { useNcpChatDerivedState, useNcpChatSnapshotSync } from "@/components/chat/ncp/page/ncp-chat-derived-state";
|
|
27
30
|
import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
|
|
28
31
|
import type { ResumeRunParams } from "@/components/chat/chat-stream/types";
|
|
@@ -31,6 +34,10 @@ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-l
|
|
|
31
34
|
import { useConfirmDialog } from "@/hooks/useConfirmDialog";
|
|
32
35
|
import { useAgents } from "@/hooks/agents/useAgents";
|
|
33
36
|
import { normalizeRequestedSkills } from "@/lib/chat-runtime-utils";
|
|
37
|
+
import {
|
|
38
|
+
systemStatusManager,
|
|
39
|
+
useChatRuntimeAvailability,
|
|
40
|
+
} from "@/features/system-status";
|
|
34
41
|
import {
|
|
35
42
|
getSessionProjectName,
|
|
36
43
|
normalizeSessionProjectRootValue,
|
|
@@ -119,6 +126,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
119
126
|
const pendingProjectRootSessionKey = useChatInputStore(
|
|
120
127
|
(state) => state.snapshot.pendingProjectRootSessionKey,
|
|
121
128
|
);
|
|
129
|
+
const runtimeAvailability = useChatRuntimeAvailability();
|
|
122
130
|
const agentsQuery = useAgents();
|
|
123
131
|
const currentSelectedModel = useChatInputStore(
|
|
124
132
|
(state) => state.snapshot.selectedModel,
|
|
@@ -182,8 +190,19 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
182
190
|
const isAwaitingAssistantOutput = agent.isRunning;
|
|
183
191
|
const canStopCurrentRun = agent.isRunning;
|
|
184
192
|
const stopDisabledReason = agent.isRunning ? null : "__preparing__";
|
|
185
|
-
const
|
|
193
|
+
const rawLastSendError =
|
|
186
194
|
agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
195
|
+
const filteredLastSendError =
|
|
196
|
+
runtimeAvailability.phase === "ready" &&
|
|
197
|
+
isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
|
|
198
|
+
? null
|
|
199
|
+
: rawLastSendError;
|
|
200
|
+
const lastSendError =
|
|
201
|
+
runtimeAvailability.isBlocked
|
|
202
|
+
? null
|
|
203
|
+
: runtimeAvailability.phase === "ready"
|
|
204
|
+
? filteredLastSendError
|
|
205
|
+
: systemStatusManager.getDisplayMessage(filteredLastSendError);
|
|
187
206
|
|
|
188
207
|
useEffect(() => {
|
|
189
208
|
presenter.chatStreamActionsManager.bind({
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { renderHook } from "@testing-library/react";
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { fetchNcpSessionConversationSeed, useNcpSessionConversation } from "./use-ncp-session-conversation";
|
|
4
4
|
|
|
5
5
|
const mocks = vi.hoisted(() => ({
|
|
6
6
|
fetchNcpSessionMessages: vi.fn(),
|
|
7
|
-
hydratedCalls: [] as Array<{ client: unknown }>,
|
|
7
|
+
hydratedCalls: [] as Array<{ client: unknown; loadSeed: unknown }>,
|
|
8
|
+
runtimeAvailability: {
|
|
9
|
+
phase: "cold-starting" as "cold-starting" | "ready",
|
|
10
|
+
lastReadyAt: null as number | null,
|
|
11
|
+
},
|
|
8
12
|
useHydratedNcpAgent: vi.fn(() => ({
|
|
9
13
|
snapshot: {
|
|
10
14
|
messages: [],
|
|
11
15
|
streamingMessage: null,
|
|
12
16
|
activeRun: null,
|
|
13
|
-
error: null,
|
|
17
|
+
error: null as Error | null,
|
|
14
18
|
},
|
|
15
19
|
visibleMessages: [],
|
|
16
20
|
activeRunId: null,
|
|
@@ -30,7 +34,7 @@ vi.mock("@/api/ncp-session", () => ({
|
|
|
30
34
|
}));
|
|
31
35
|
|
|
32
36
|
vi.mock("@nextclaw/ncp-react", () => ({
|
|
33
|
-
useHydratedNcpAgent: vi.fn((params: { client: unknown }) => {
|
|
37
|
+
useHydratedNcpAgent: vi.fn((params: { client: unknown; loadSeed: unknown }) => {
|
|
34
38
|
mocks.hydratedCalls.push(params);
|
|
35
39
|
return mocks.useHydratedNcpAgent();
|
|
36
40
|
}),
|
|
@@ -42,12 +46,18 @@ vi.mock("@nextclaw/ncp-http-agent-client", () => ({
|
|
|
42
46
|
}),
|
|
43
47
|
}));
|
|
44
48
|
|
|
49
|
+
vi.mock("@/features/system-status", () => ({
|
|
50
|
+
useChatRuntimeAvailability: vi.fn(() => mocks.runtimeAvailability),
|
|
51
|
+
}));
|
|
52
|
+
|
|
45
53
|
describe("useNcpSessionConversation", () => {
|
|
46
54
|
beforeEach(() => {
|
|
47
55
|
mocks.fetchNcpSessionMessages.mockReset();
|
|
48
56
|
mocks.useHydratedNcpAgent.mockClear();
|
|
49
57
|
mocks.hydratedCalls.length = 0;
|
|
50
58
|
mocks.clientInstances.length = 0;
|
|
59
|
+
mocks.runtimeAvailability.phase = "cold-starting";
|
|
60
|
+
mocks.runtimeAvailability.lastReadyAt = null;
|
|
51
61
|
});
|
|
52
62
|
|
|
53
63
|
it("hydrates seed from the shared session messages endpoint payload", async () => {
|
|
@@ -98,4 +108,38 @@ describe("useNcpSessionConversation", () => {
|
|
|
98
108
|
expect(mocks.hydratedCalls[0]?.client).toBe(mocks.clientInstances[0]);
|
|
99
109
|
expect(mocks.hydratedCalls[1]?.client).toBe(mocks.clientInstances[1]);
|
|
100
110
|
});
|
|
111
|
+
|
|
112
|
+
it("retries hydration once the runtime becomes ready after a startup placeholder error", async () => {
|
|
113
|
+
mocks.useHydratedNcpAgent.mockImplementation(() => ({
|
|
114
|
+
snapshot: {
|
|
115
|
+
messages: [],
|
|
116
|
+
streamingMessage: null,
|
|
117
|
+
activeRun: null,
|
|
118
|
+
error: new Error("ncp agent unavailable during startup") as Error | null,
|
|
119
|
+
},
|
|
120
|
+
visibleMessages: [],
|
|
121
|
+
activeRunId: null,
|
|
122
|
+
isRunning: false,
|
|
123
|
+
isSending: false,
|
|
124
|
+
send: vi.fn(),
|
|
125
|
+
abort: vi.fn(),
|
|
126
|
+
streamRun: vi.fn(),
|
|
127
|
+
isHydrating: false,
|
|
128
|
+
hydrateError: null,
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const { rerender } = renderHook(() => useNcpSessionConversation("session-a"));
|
|
132
|
+
const initialLoadSeed = mocks.hydratedCalls[0]?.loadSeed;
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
mocks.runtimeAvailability.phase = "ready";
|
|
136
|
+
mocks.runtimeAvailability.lastReadyAt = 123;
|
|
137
|
+
});
|
|
138
|
+
rerender();
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(mocks.hydratedCalls.length).toBeGreaterThan(2);
|
|
142
|
+
});
|
|
143
|
+
expect(mocks.hydratedCalls[mocks.hydratedCalls.length - 1]?.loadSeed).not.toBe(initialLoadSeed);
|
|
144
|
+
});
|
|
101
145
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
|
|
3
3
|
import { useHydratedNcpAgent, type NcpConversationSeed } from "@nextclaw/ncp-react";
|
|
4
4
|
import { API_BASE } from "@/api/api-base";
|
|
5
5
|
import { fetchNcpSessionMessages } from "@/api/ncp-session";
|
|
6
6
|
import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
|
|
7
|
+
import { useChatRuntimeAvailability } from "@/features/system-status";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_MESSAGE_LIMIT = 300;
|
|
10
|
+
const NCP_AGENT_UNAVAILABLE_DURING_STARTUP = "ncp agent unavailable during startup";
|
|
9
11
|
|
|
10
12
|
type UseNcpSessionConversationOptions = {
|
|
11
13
|
messageLimit?: number;
|
|
@@ -18,6 +20,15 @@ function isMissingNcpSessionError(error: unknown): boolean {
|
|
|
18
20
|
return error.message.includes("ncp session not found:");
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
export function isNcpAgentStartupUnavailableErrorMessage(
|
|
24
|
+
message: string | null | undefined,
|
|
25
|
+
): boolean {
|
|
26
|
+
return (
|
|
27
|
+
message?.trim().toLowerCase().includes(NCP_AGENT_UNAVAILABLE_DURING_STARTUP) ??
|
|
28
|
+
false
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export function createNcpSessionConversationClient(): NcpHttpAgentClientEndpoint {
|
|
22
33
|
return new NcpHttpAgentClientEndpoint({
|
|
23
34
|
baseUrl: API_BASE,
|
|
@@ -57,16 +68,43 @@ export function useNcpSessionConversation(
|
|
|
57
68
|
options: UseNcpSessionConversationOptions = {},
|
|
58
69
|
) {
|
|
59
70
|
const [client] = useState(() => createNcpSessionConversationClient());
|
|
71
|
+
const runtimeAvailability = useChatRuntimeAvailability();
|
|
72
|
+
const [hydrationRetryNonce, setHydrationRetryNonce] = useState(0);
|
|
73
|
+
const retriedReadySignatureRef = useRef<string | null>(null);
|
|
60
74
|
const messageLimit = options.messageLimit ?? DEFAULT_MESSAGE_LIMIT;
|
|
61
75
|
const loadSeed = useCallback(
|
|
62
|
-
(targetSessionId: string, signal: AbortSignal) =>
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
(targetSessionId: string, signal: AbortSignal) => {
|
|
77
|
+
void hydrationRetryNonce;
|
|
78
|
+
return fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit);
|
|
79
|
+
},
|
|
80
|
+
[hydrationRetryNonce, messageLimit],
|
|
65
81
|
);
|
|
66
82
|
|
|
67
|
-
|
|
83
|
+
const agent = useHydratedNcpAgent({
|
|
68
84
|
sessionId,
|
|
69
85
|
client,
|
|
70
86
|
loadSeed,
|
|
71
87
|
});
|
|
88
|
+
|
|
89
|
+
const currentAgentError =
|
|
90
|
+
agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
91
|
+
const readyRetrySignature =
|
|
92
|
+
runtimeAvailability.phase === "ready" &&
|
|
93
|
+
isNcpAgentStartupUnavailableErrorMessage(currentAgentError)
|
|
94
|
+
? `${sessionId}:${runtimeAvailability.lastReadyAt ?? 0}`
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!readyRetrySignature) {
|
|
99
|
+
retriedReadySignatureRef.current = null;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (retriedReadySignatureRef.current === readyRetrySignature) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
retriedReadySignatureRef.current = readyRetrySignature;
|
|
106
|
+
setHydrationRetryNonce((current) => current + 1);
|
|
107
|
+
}, [readyRetrySignature]);
|
|
108
|
+
|
|
109
|
+
return agent;
|
|
72
110
|
}
|