@nextclaw/ui 0.15.14 → 0.15.15
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 +19 -0
- package/dist/assets/{api-DGG69ZcJ.js → api-D5UCLVE_.js} +3 -3
- package/dist/assets/app-presenter-provider-DcpcRgLn.js +106 -0
- package/dist/assets/{appearance-settings-page-Byr55LGb.js → appearance-settings-page-yJR6Kt9c.js} +1 -1
- package/dist/assets/{book-open-BhaS5BJv.js → book-open-WsEnuW9P.js} +1 -1
- package/dist/assets/{card-Bcr5YM8F.js → card-xVrS1b1T.js} +1 -1
- package/dist/assets/{channels-list-page-C_x83ZCT.js → channels-list-page-CAWuTHSM.js} +1 -1
- package/dist/assets/{chat-page-CEavSS6B.js → chat-page-CXYXMIQQ.js} +1 -1
- package/dist/assets/{config-split-page-RXAoLeNv.js → config-split-page-xj0UtMow.js} +1 -1
- package/dist/assets/{confirm-dialog-o_VPKvVD.js → confirm-dialog-e2YCUzt1.js} +1 -1
- package/dist/assets/{desktop-update-config-DEOMOsDN.js → desktop-update-config-D5NIaimo.js} +1 -1
- package/dist/assets/{dist-C87P4QWA.js → dist-Ca-jiFrK.js} +1 -1
- package/dist/assets/doc-browser-B2tNEKrJ.js +1 -0
- package/dist/assets/{doc-browser-DQ75iUkA.js → doc-browser-CZX6I6CU.js} +1 -1
- package/dist/assets/{doc-browser-context-tE0--wSa.js → doc-browser-context-CGz0LULR.js} +1 -1
- package/dist/assets/{eye-DL9QkrKg.js → eye-B30J471A.js} +1 -1
- package/dist/assets/{form-actions-J0tIaUdK.js → form-actions-CnSzu3WD.js} +1 -1
- package/dist/assets/{index-BFIZUZP3.js → index-6Esxu-Pp.js} +2 -2
- package/dist/assets/index-BzSgA8tR.css +1 -0
- package/dist/assets/loader-circle-DjwSRQHW.js +1 -0
- package/dist/assets/{logo-badge-BYTbZbOF.js → logo-badge-CISsltAo.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +1 -0
- package/dist/assets/{mcp-marketplace-page-COWlzeP1.js → mcp-marketplace-page-CPcPzMUQ.js} +1 -1
- package/dist/assets/{model-config-page-oNqnMXNY.js → model-config-page-D6ex3_NM.js} +1 -1
- package/dist/assets/{navigation-link-Dk6VdDkc.js → navigation-link-qPPUiLt0.js} +1 -1
- package/dist/assets/plus-C45awBb1.js +1 -0
- package/dist/assets/{popover-Txwrum65.js → popover-DHx55dbc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-B7rQM4b6.js → provider-scoped-model-input-1TJxAhx6.js} +1 -1
- package/dist/assets/{providers-config-page-CP-OnvLK.js → providers-config-page-BwHtHIPe.js} +1 -1
- package/dist/assets/{react-B0LbRnAE.js → react-DerwD2EA.js} +1 -1
- package/dist/assets/{refresh-cw-C84mMku5.js → refresh-cw-C7bynrWX.js} +1 -1
- package/dist/assets/remote-CPpTKDgX.js +1 -0
- package/dist/assets/rotate-cw-Bhx0XkQP.js +1 -0
- package/dist/assets/{runtime-config-page-CIGtssWi.js → runtime-config-page-CMLVC5UV.js} +1 -1
- package/dist/assets/{save-R_qyrHpa.js → save-CsRsFtHS.js} +1 -1
- package/dist/assets/{search-UKxiouNi.js → search-D0HtJIL-.js} +1 -1
- package/dist/assets/{search-config-page-DzkpSGFH.js → search-config-page-n1GElgij.js} +1 -1
- package/dist/assets/{secrets-config-page-DhoLYm7Z.js → secrets-config-page-DbUc3Ggi.js} +1 -1
- package/dist/assets/security-config-n2WHhHXV.js +1 -0
- package/dist/assets/{select-CYHT8P5S.js → select-C1zfp9zM.js} +1 -1
- package/dist/assets/{settings-2-DGh4bGSi.js → settings-2-Ba3cLSN2.js} +1 -1
- package/dist/assets/skeleton-DL5npsVL.js +1 -0
- package/dist/assets/{tabs-BCihm1zK.js → tabs-DmKgMKzb.js} +1 -1
- package/dist/assets/{tag-chip-De0iz4o2.js → tag-chip-2vwlVUwM.js} +1 -1
- package/dist/assets/{trash-2-Bo3DWiju.js → trash-2-CUpWjNHu.js} +1 -1
- package/dist/assets/{use-config-DCzwgChy.js → use-config-BvxDWV-M.js} +1 -1
- package/dist/assets/{use-confirm-dialog-WZ94CehU.js → use-confirm-dialog-CeUEE4p5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CZ-3pMne.js → use-viewport-layout-DaKJ-kap.js} +1 -1
- package/dist/assets/x-DGdYT7rH.js +1 -0
- package/dist/index.html +28 -28
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +2 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +11 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +4 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +3 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +64 -72
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +85 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +108 -257
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +52 -0
- package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +63 -0
- package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +134 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +5 -5
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +113 -32
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +3 -2
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +118 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list-skill-preview.container.test.tsx +170 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +18 -9
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +36 -23
- package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +90 -0
- package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +3 -5
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +51 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +9 -3
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +42 -8
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/dist/assets/app-presenter-provider-Dp90n1Va.js +0 -106
- package/dist/assets/doc-browser-BuGh646I.js +0 -1
- package/dist/assets/index-B2YF-7Yw.css +0 -1
- package/dist/assets/loader-circle-CAnHHSrY.js +0 -1
- package/dist/assets/mcp-marketplace-page-CaUFv37E.js +0 -1
- package/dist/assets/plus-DjdTWPjQ.js +0 -1
- package/dist/assets/remote-_L4pTEaW.js +0 -1
- package/dist/assets/rotate-cw-DQJhDnjg.js +0 -1
- package/dist/assets/security-config-yraYoy9s.js +0 -1
- package/dist/assets/skeleton-BQuylYFO.js +0 -1
- package/dist/assets/x-NZxXyqW1.js +0 -1
package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type Dispatch,
|
|
8
|
-
type MutableRefObject,
|
|
9
|
-
type SetStateAction,
|
|
10
|
-
} from 'react';
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
11
2
|
import { buildNcpRequestEnvelope } from '@nextclaw/ncp-react';
|
|
3
|
+
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
12
4
|
import type { NcpAgentSendEnvelope, NcpMessage, NcpRunHandle } from '@nextclaw/ncp';
|
|
13
5
|
|
|
14
6
|
import { deriveNcpMessagePartsFromComposer } from '@/features/chat/features/input/utils/chat-composer-state.utils';
|
|
15
7
|
import { isNcpChatSendDisabled } from '@/features/chat/features/input/utils/ncp-chat-input-availability.utils';
|
|
16
8
|
import { buildChatRunMetadata } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
|
|
9
|
+
import {
|
|
10
|
+
buildSessionQueuedInputComposerSnapshot,
|
|
11
|
+
buildSessionQueuedInputPreview,
|
|
12
|
+
} from '@/features/chat/features/conversation/utils/session-queued-input.utils';
|
|
17
13
|
import type { SessionConversationInputSnapshot } from './use-session-conversation-input-state';
|
|
18
14
|
import type { useSessionConversationInputQuery } from './use-session-conversation-input-query';
|
|
19
15
|
|
|
@@ -44,9 +40,11 @@ export type SessionConversationQueuedInput = {
|
|
|
44
40
|
readonly preview: string;
|
|
45
41
|
};
|
|
46
42
|
|
|
47
|
-
type
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
43
|
+
type SessionRunQueue = {
|
|
44
|
+
readonly inputs: readonly UiNcpSessionQueuedInputView[];
|
|
45
|
+
readonly removeQueuedInput: (
|
|
46
|
+
queuedInputId: string,
|
|
47
|
+
) => Promise<UiNcpSessionQueuedInputView | null>;
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
export type SessionConversationMaterializationContext = {
|
|
@@ -61,6 +59,7 @@ type UseSessionConversationControllerParams = {
|
|
|
61
59
|
readonly inputQuery: SessionConversationInputQuery;
|
|
62
60
|
readonly isRuntimeBlocked: boolean;
|
|
63
61
|
readonly materializationContext?: SessionConversationMaterializationContext | null;
|
|
62
|
+
readonly runQueue: SessionRunQueue;
|
|
64
63
|
readonly selectedAgentId: string;
|
|
65
64
|
readonly sessionKey: string | null;
|
|
66
65
|
readonly onSessionMaterialized?: (sessionKey: string) => void;
|
|
@@ -69,7 +68,7 @@ type UseSessionConversationControllerParams = {
|
|
|
69
68
|
readonly setSendError: (message: string | null) => void;
|
|
70
69
|
};
|
|
71
70
|
|
|
72
|
-
type
|
|
71
|
+
type BuildSubmissionDraftParams = {
|
|
73
72
|
readonly agentIsSending: boolean;
|
|
74
73
|
readonly inputSnapshot: SessionConversationInputSnapshot;
|
|
75
74
|
readonly inputQuery: SessionConversationInputQuery;
|
|
@@ -79,36 +78,9 @@ type BuildQueuedInputDraftParams = {
|
|
|
79
78
|
readonly sessionKey: string | null;
|
|
80
79
|
};
|
|
81
80
|
|
|
82
|
-
type
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
type SendQueuedDraft = (
|
|
87
|
-
draft: QueuedInputDraft,
|
|
88
|
-
) => Promise<NcpRunHandle | null>;
|
|
89
|
-
|
|
90
|
-
type UseQueuedDraftAutoSendParams = {
|
|
91
|
-
readonly agentIsRunning: boolean;
|
|
92
|
-
readonly agentIsSending: boolean;
|
|
93
|
-
readonly queuedDrafts: readonly QueuedInputDraft[];
|
|
94
|
-
readonly sendQueuedDraft: SendQueuedDraft;
|
|
95
|
-
readonly setQueuedDrafts: Dispatch<SetStateAction<QueuedInputDraft[]>>;
|
|
96
|
-
readonly setSendError: (message: string | null) => void;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
type StartQueuedDraftAutoSendParams = {
|
|
100
|
-
readonly autoSendingQueuedDraftIdRef: MutableRefObject<string | null>;
|
|
101
|
-
readonly draft: QueuedInputDraft;
|
|
102
|
-
readonly sendQueuedDraft: SendQueuedDraft;
|
|
103
|
-
readonly setQueuedDrafts: Dispatch<SetStateAction<QueuedInputDraft[]>>;
|
|
104
|
-
readonly setSendError: (message: string | null) => void;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
type UseQueuedDraftActionsParams = {
|
|
108
|
-
readonly queuedDrafts: readonly QueuedInputDraft[];
|
|
109
|
-
readonly restoreComposer: (snapshot: ComposerDraftSnapshot) => void;
|
|
110
|
-
readonly setQueuedDrafts: Dispatch<SetStateAction<QueuedInputDraft[]>>;
|
|
111
|
-
readonly setSendError: (message: string | null) => void;
|
|
81
|
+
type SubmissionDraft = {
|
|
82
|
+
readonly composerSnapshot: ComposerDraftSnapshot;
|
|
83
|
+
readonly metadata: Record<string, unknown>;
|
|
112
84
|
};
|
|
113
85
|
|
|
114
86
|
const hasSendableMessagePart = (parts: ReturnType<typeof deriveNcpMessagePartsFromComposer>): boolean =>
|
|
@@ -119,17 +91,6 @@ const resolveModelForSend = (value: string | null | undefined): string | undefin
|
|
|
119
91
|
return trimmed || undefined;
|
|
120
92
|
};
|
|
121
93
|
|
|
122
|
-
function buildQueuedInputPreview(snapshot: Pick<
|
|
123
|
-
SessionConversationInputSnapshot,
|
|
124
|
-
'attachments' | 'text'
|
|
125
|
-
>): string {
|
|
126
|
-
const compactText = snapshot.text.replace(/\s+/g, ' ').trim();
|
|
127
|
-
if (compactText) {
|
|
128
|
-
return compactText;
|
|
129
|
-
}
|
|
130
|
-
return snapshot.attachments.map((attachment) => attachment.name).filter(Boolean).join(', ');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
94
|
function buildInputAvailabilitySnapshot(inputQuery: SessionConversationInputQuery) {
|
|
134
95
|
return {
|
|
135
96
|
isProviderStateResolved: inputQuery.isProviderStateResolved,
|
|
@@ -138,7 +99,7 @@ function buildInputAvailabilitySnapshot(inputQuery: SessionConversationInputQuer
|
|
|
138
99
|
};
|
|
139
100
|
}
|
|
140
101
|
|
|
141
|
-
function
|
|
102
|
+
function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDraft | null {
|
|
142
103
|
const {
|
|
143
104
|
agentIsSending,
|
|
144
105
|
inputQuery,
|
|
@@ -169,46 +130,46 @@ function buildQueuedInputDraft(params: BuildQueuedInputDraftParams): QueuedInput
|
|
|
169
130
|
skillRecords: [...inputSnapshot.skillRecords],
|
|
170
131
|
attachments: [...inputSnapshot.attachments],
|
|
171
132
|
};
|
|
172
|
-
const metadata = buildChatRunMetadata({
|
|
173
|
-
agentId: materializationContext ? undefined : selectedAgentId,
|
|
174
|
-
model: materializationContext
|
|
175
|
-
? resolveModelForSend(inputSnapshot.selectedModel)
|
|
176
|
-
: resolveModelForSend(
|
|
177
|
-
inputSnapshot.selectedModel ??
|
|
178
|
-
inputQuery.fallbackPreferredModel ??
|
|
179
|
-
inputQuery.defaultModel,
|
|
180
|
-
),
|
|
181
|
-
thinkingLevel: materializationContext
|
|
182
|
-
? inputSnapshot.selectedThinkingLevel ?? undefined
|
|
183
|
-
: inputSnapshot.selectedThinkingLevel ?? inputQuery.fallbackPreferredThinking ?? undefined,
|
|
184
|
-
sessionType: materializationContext
|
|
185
|
-
? undefined
|
|
186
|
-
: inputQuery.sessionTypeState.selectedSessionType,
|
|
187
|
-
projectRoot: materializationContext
|
|
188
|
-
? inputSnapshot.pendingProjectRoot
|
|
189
|
-
: sessionKey
|
|
190
|
-
? inputQuery.selectedSession?.projectRoot ?? null
|
|
191
|
-
: inputSnapshot.pendingProjectRoot,
|
|
192
|
-
requestedSkills: [...inputSnapshot.selectedSkills],
|
|
193
|
-
composerNodes: [...inputSnapshot.nodes],
|
|
194
|
-
sessionMaterialization: materializationContext
|
|
195
|
-
? {
|
|
196
|
-
kind: materializationContext.kind,
|
|
197
|
-
parentSessionId: materializationContext.parentSessionKey,
|
|
198
|
-
inheritContext: materializationContext.inheritContext,
|
|
199
|
-
}
|
|
200
|
-
: null,
|
|
201
|
-
});
|
|
202
133
|
return {
|
|
203
134
|
composerSnapshot,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
135
|
+
metadata: buildChatRunMetadata({
|
|
136
|
+
agentId: materializationContext ? undefined : selectedAgentId,
|
|
137
|
+
model: materializationContext
|
|
138
|
+
? resolveModelForSend(inputSnapshot.selectedModel)
|
|
139
|
+
: resolveModelForSend(
|
|
140
|
+
inputSnapshot.selectedModel ??
|
|
141
|
+
inputQuery.fallbackPreferredModel ??
|
|
142
|
+
inputQuery.defaultModel,
|
|
143
|
+
),
|
|
144
|
+
thinkingLevel: materializationContext
|
|
145
|
+
? inputSnapshot.selectedThinkingLevel ?? undefined
|
|
146
|
+
: inputSnapshot.selectedThinkingLevel ?? inputQuery.fallbackPreferredThinking ?? undefined,
|
|
147
|
+
sessionType: materializationContext
|
|
148
|
+
? undefined
|
|
149
|
+
: inputQuery.sessionTypeState.selectedSessionType,
|
|
150
|
+
projectRoot: materializationContext
|
|
151
|
+
? inputSnapshot.pendingProjectRoot
|
|
152
|
+
: sessionKey
|
|
153
|
+
? inputQuery.selectedSession?.projectRoot ?? null
|
|
154
|
+
: inputSnapshot.pendingProjectRoot,
|
|
155
|
+
requestedSkills: [...inputSnapshot.selectedSkills],
|
|
156
|
+
skillRecords: inputQuery.skillRecords.filter((record) =>
|
|
157
|
+
inputSnapshot.selectedSkills.includes(record.ref)
|
|
158
|
+
),
|
|
159
|
+
composerNodes: [...inputSnapshot.nodes],
|
|
160
|
+
sessionMaterialization: materializationContext
|
|
161
|
+
? {
|
|
162
|
+
kind: materializationContext.kind,
|
|
163
|
+
parentSessionId: materializationContext.parentSessionKey,
|
|
164
|
+
inheritContext: materializationContext.inheritContext,
|
|
165
|
+
}
|
|
166
|
+
: null,
|
|
167
|
+
}),
|
|
207
168
|
};
|
|
208
169
|
}
|
|
209
170
|
|
|
210
|
-
function
|
|
211
|
-
draft:
|
|
171
|
+
function buildSubmissionEnvelope(
|
|
172
|
+
draft: SubmissionDraft,
|
|
212
173
|
sessionKey: string | null,
|
|
213
174
|
): NcpAgentSendEnvelope | null {
|
|
214
175
|
return buildNcpRequestEnvelope({
|
|
@@ -223,102 +184,6 @@ function buildQueuedDraftEnvelope(
|
|
|
223
184
|
});
|
|
224
185
|
}
|
|
225
186
|
|
|
226
|
-
function startQueuedDraftAutoSend(params: StartQueuedDraftAutoSendParams): void {
|
|
227
|
-
const {
|
|
228
|
-
autoSendingQueuedDraftIdRef,
|
|
229
|
-
draft,
|
|
230
|
-
sendQueuedDraft,
|
|
231
|
-
setQueuedDrafts,
|
|
232
|
-
setSendError,
|
|
233
|
-
} = params;
|
|
234
|
-
autoSendingQueuedDraftIdRef.current = draft.id;
|
|
235
|
-
setQueuedDrafts((current) => current[0]?.id === draft.id ? current.slice(1) : current);
|
|
236
|
-
setSendError(null);
|
|
237
|
-
void sendQueuedDraft(draft)
|
|
238
|
-
.catch((error) => {
|
|
239
|
-
setQueuedDrafts((current) => [draft, ...current]);
|
|
240
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
-
setSendError(message);
|
|
242
|
-
})
|
|
243
|
-
.finally(() => {
|
|
244
|
-
autoSendingQueuedDraftIdRef.current = null;
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function useQueuedDraftAutoSend(params: UseQueuedDraftAutoSendParams) {
|
|
249
|
-
const {
|
|
250
|
-
agentIsRunning,
|
|
251
|
-
agentIsSending,
|
|
252
|
-
queuedDrafts,
|
|
253
|
-
sendQueuedDraft,
|
|
254
|
-
setQueuedDrafts,
|
|
255
|
-
setSendError,
|
|
256
|
-
} = params;
|
|
257
|
-
const autoSendingQueuedDraftIdRef = useRef<string | null>(null);
|
|
258
|
-
|
|
259
|
-
useEffect(() => {
|
|
260
|
-
if (
|
|
261
|
-
agentIsRunning ||
|
|
262
|
-
agentIsSending ||
|
|
263
|
-
autoSendingQueuedDraftIdRef.current ||
|
|
264
|
-
queuedDrafts.length === 0
|
|
265
|
-
) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
startQueuedDraftAutoSend({
|
|
269
|
-
autoSendingQueuedDraftIdRef,
|
|
270
|
-
draft: queuedDrafts[0],
|
|
271
|
-
sendQueuedDraft,
|
|
272
|
-
setQueuedDrafts,
|
|
273
|
-
setSendError,
|
|
274
|
-
});
|
|
275
|
-
}, [
|
|
276
|
-
agentIsRunning,
|
|
277
|
-
agentIsSending,
|
|
278
|
-
queuedDrafts,
|
|
279
|
-
sendQueuedDraft,
|
|
280
|
-
setQueuedDrafts,
|
|
281
|
-
setSendError,
|
|
282
|
-
]);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function useQueuedDraftActions(params: UseQueuedDraftActionsParams) {
|
|
286
|
-
const {
|
|
287
|
-
queuedDrafts,
|
|
288
|
-
restoreComposer,
|
|
289
|
-
setQueuedDrafts,
|
|
290
|
-
setSendError,
|
|
291
|
-
} = params;
|
|
292
|
-
|
|
293
|
-
const editQueuedInput = useCallback((id: string) => {
|
|
294
|
-
const draft = queuedDrafts.find((item) => item.id === id);
|
|
295
|
-
if (!draft) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
setQueuedDrafts((current) => current.filter((item) => item.id !== id));
|
|
299
|
-
restoreComposer(draft.composerSnapshot);
|
|
300
|
-
setSendError(null);
|
|
301
|
-
}, [
|
|
302
|
-
queuedDrafts,
|
|
303
|
-
restoreComposer,
|
|
304
|
-
setQueuedDrafts,
|
|
305
|
-
setSendError,
|
|
306
|
-
]);
|
|
307
|
-
|
|
308
|
-
const deleteQueuedInput = useCallback((id: string) => {
|
|
309
|
-
setQueuedDrafts((current) => current.filter((item) => item.id !== id));
|
|
310
|
-
setSendError(null);
|
|
311
|
-
}, [
|
|
312
|
-
setQueuedDrafts,
|
|
313
|
-
setSendError,
|
|
314
|
-
]);
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
deleteQueuedInput,
|
|
318
|
-
editQueuedInput,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
187
|
export function useSessionConversationController(params: UseSessionConversationControllerParams) {
|
|
323
188
|
const {
|
|
324
189
|
agent,
|
|
@@ -326,6 +191,7 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
326
191
|
inputQuery,
|
|
327
192
|
isRuntimeBlocked,
|
|
328
193
|
materializationContext,
|
|
194
|
+
runQueue,
|
|
329
195
|
selectedAgentId,
|
|
330
196
|
sessionKey,
|
|
331
197
|
onSessionMaterialized,
|
|
@@ -339,16 +205,14 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
339
205
|
);
|
|
340
206
|
const hasSendableDraft = hasSendableMessagePart(parts);
|
|
341
207
|
const isSending = agent.isSending || agent.isRunning;
|
|
342
|
-
const
|
|
208
|
+
const sendDisabled = isNcpChatSendDisabled({
|
|
343
209
|
snapshot: buildInputAvailabilitySnapshot(inputQuery),
|
|
344
210
|
hasSendableDraft,
|
|
345
211
|
isRuntimeBlocked,
|
|
346
|
-
});
|
|
347
|
-
const sendDisabled = baseSendDisabled || agent.isSending;
|
|
348
|
-
const [queuedDrafts, setQueuedDrafts] = useState<QueuedInputDraft[]>([]);
|
|
212
|
+
}) || agent.isSending;
|
|
349
213
|
|
|
350
|
-
const
|
|
351
|
-
|
|
214
|
+
const send = useCallback(async () => {
|
|
215
|
+
const draft = buildSubmissionDraft({
|
|
352
216
|
agentIsSending: agent.isSending,
|
|
353
217
|
inputSnapshot,
|
|
354
218
|
inputQuery,
|
|
@@ -357,86 +221,69 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
357
221
|
selectedAgentId,
|
|
358
222
|
sessionKey,
|
|
359
223
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
inputQuery,
|
|
363
|
-
inputSnapshot,
|
|
364
|
-
isRuntimeBlocked,
|
|
365
|
-
materializationContext,
|
|
366
|
-
selectedAgentId,
|
|
367
|
-
sessionKey,
|
|
368
|
-
]);
|
|
369
|
-
|
|
370
|
-
const sendEnvelope = useCallback<SendEnvelope>(async (envelope) => {
|
|
371
|
-
const handle = await agent.send(envelope);
|
|
372
|
-
const materializedSessionKey =
|
|
373
|
-
handle?.sessionId?.trim() ||
|
|
374
|
-
agent.snapshot.activeRun?.sessionId?.trim() ||
|
|
375
|
-
null;
|
|
376
|
-
if (!sessionKey && materializedSessionKey) {
|
|
377
|
-
onSessionMaterialized?.(materializedSessionKey);
|
|
224
|
+
if (!draft) {
|
|
225
|
+
return;
|
|
378
226
|
}
|
|
379
|
-
|
|
380
|
-
}, [
|
|
381
|
-
agent,
|
|
382
|
-
onSessionMaterialized,
|
|
383
|
-
sessionKey,
|
|
384
|
-
]);
|
|
385
|
-
|
|
386
|
-
const sendQueuedDraft = useCallback<SendQueuedDraft>(async (draft) => {
|
|
387
|
-
const envelope = buildQueuedDraftEnvelope(draft, sessionKey);
|
|
227
|
+
const envelope = buildSubmissionEnvelope(draft, sessionKey);
|
|
388
228
|
if (!envelope) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
return sendEnvelope(envelope);
|
|
392
|
-
}, [
|
|
393
|
-
sendEnvelope,
|
|
394
|
-
sessionKey,
|
|
395
|
-
]);
|
|
396
|
-
|
|
397
|
-
const send = useCallback(async () => {
|
|
398
|
-
const draft = buildDraftSubmission();
|
|
399
|
-
if (!draft) {
|
|
400
229
|
return;
|
|
401
230
|
}
|
|
402
231
|
resetComposer();
|
|
403
232
|
setSendError(null);
|
|
404
|
-
if (agent.isRunning) {
|
|
405
|
-
setQueuedDrafts((current) => [...current, draft]);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
233
|
try {
|
|
409
|
-
await
|
|
234
|
+
const handle = await agent.send(envelope);
|
|
235
|
+
const materializedSessionKey =
|
|
236
|
+
handle?.sessionId?.trim() ||
|
|
237
|
+
agent.snapshot.activeRun?.sessionId?.trim() ||
|
|
238
|
+
null;
|
|
239
|
+
if (!sessionKey && materializedSessionKey) {
|
|
240
|
+
onSessionMaterialized?.(materializedSessionKey);
|
|
241
|
+
}
|
|
410
242
|
} catch (error) {
|
|
411
243
|
restoreComposer(draft.composerSnapshot);
|
|
412
|
-
|
|
413
|
-
setSendError(message);
|
|
244
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
414
245
|
throw error;
|
|
415
246
|
}
|
|
416
247
|
}, [
|
|
417
|
-
agent
|
|
418
|
-
|
|
248
|
+
agent,
|
|
249
|
+
inputQuery,
|
|
250
|
+
inputSnapshot,
|
|
251
|
+
isRuntimeBlocked,
|
|
252
|
+
materializationContext,
|
|
253
|
+
onSessionMaterialized,
|
|
419
254
|
resetComposer,
|
|
420
255
|
restoreComposer,
|
|
421
|
-
|
|
256
|
+
selectedAgentId,
|
|
257
|
+
sessionKey,
|
|
422
258
|
setSendError,
|
|
423
259
|
]);
|
|
424
260
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
261
|
+
const editQueuedInput = useCallback((id: string) => {
|
|
262
|
+
const input = runQueue.inputs.find((item) => item.id === id);
|
|
263
|
+
if (!input) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
void runQueue.removeQueuedInput(id).then((removed) => {
|
|
267
|
+
if (!removed) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
restoreComposer(buildSessionQueuedInputComposerSnapshot(
|
|
271
|
+
removed,
|
|
272
|
+
inputQuery.skillRecords,
|
|
273
|
+
));
|
|
274
|
+
setSendError(null);
|
|
275
|
+
}).catch((error) => {
|
|
276
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
277
|
+
});
|
|
278
|
+
}, [inputQuery.skillRecords, restoreComposer, runQueue, setSendError]);
|
|
431
279
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
});
|
|
280
|
+
const deleteQueuedInput = useCallback((id: string) => {
|
|
281
|
+
void runQueue.removeQueuedInput(id).then(() => {
|
|
282
|
+
setSendError(null);
|
|
283
|
+
}).catch((error) => {
|
|
284
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
285
|
+
});
|
|
286
|
+
}, [runQueue, setSendError]);
|
|
440
287
|
|
|
441
288
|
const stop = useCallback(async () => {
|
|
442
289
|
await agent.abort();
|
|
@@ -444,10 +291,14 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
444
291
|
|
|
445
292
|
return {
|
|
446
293
|
canStopGeneration: agent.isRunning,
|
|
447
|
-
|
|
294
|
+
deleteQueuedInput,
|
|
295
|
+
editQueuedInput,
|
|
448
296
|
hasSendableDraft,
|
|
449
297
|
isSending,
|
|
450
|
-
queuedInputs:
|
|
298
|
+
queuedInputs: runQueue.inputs.map((input) => ({
|
|
299
|
+
id: input.id,
|
|
300
|
+
preview: buildSessionQueuedInputPreview(input),
|
|
301
|
+
})),
|
|
451
302
|
send,
|
|
452
303
|
sendDisabled,
|
|
453
304
|
stop,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { eventKeys, type UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
4
|
+
|
|
5
|
+
import { nextclawClient } from '@/shared/lib/api';
|
|
6
|
+
|
|
7
|
+
const SESSION_RUN_QUEUE_QUERY_KEY = 'ncp-session-run-queue';
|
|
8
|
+
|
|
9
|
+
export function useSessionRunQueue(sessionKey: string | null) {
|
|
10
|
+
const queryClient = useQueryClient();
|
|
11
|
+
const normalizedSessionKey = sessionKey?.trim() || null;
|
|
12
|
+
const query = useQuery({
|
|
13
|
+
queryKey: [SESSION_RUN_QUEUE_QUERY_KEY, normalizedSessionKey],
|
|
14
|
+
queryFn: () => nextclawClient.sessions.listQueuedInputs(normalizedSessionKey as string),
|
|
15
|
+
enabled: Boolean(normalizedSessionKey),
|
|
16
|
+
retry: false,
|
|
17
|
+
staleTime: 5_000,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!normalizedSessionKey) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return nextclawClient.eventBus.on(eventKeys.sessionRunQueueUpdated, ({ sessionKey: updatedSessionKey }) => {
|
|
25
|
+
if (updatedSessionKey !== normalizedSessionKey) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
void queryClient.invalidateQueries({
|
|
29
|
+
queryKey: [SESSION_RUN_QUEUE_QUERY_KEY, normalizedSessionKey],
|
|
30
|
+
exact: true,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}, [normalizedSessionKey, queryClient]);
|
|
34
|
+
|
|
35
|
+
const removeQueuedInput = useCallback(async (
|
|
36
|
+
queuedInputId: string,
|
|
37
|
+
): Promise<UiNcpSessionQueuedInputView | null> => {
|
|
38
|
+
if (!normalizedSessionKey) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return await nextclawClient.sessions.deleteQueuedInput(
|
|
42
|
+
normalizedSessionKey,
|
|
43
|
+
queuedInputId,
|
|
44
|
+
);
|
|
45
|
+
}, [normalizedSessionKey]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
inputs: query.data?.inputs ?? [],
|
|
49
|
+
isLoading: query.isLoading,
|
|
50
|
+
removeQueuedInput,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildSessionQueuedInputComposerSnapshot,
|
|
6
|
+
buildSessionQueuedInputPreview,
|
|
7
|
+
} from '@/features/chat/features/conversation/utils/session-queued-input.utils';
|
|
8
|
+
|
|
9
|
+
describe('session queued input utils', () => {
|
|
10
|
+
it('restores text, inline skills, and attachments from the backend message', () => {
|
|
11
|
+
const input: UiNcpSessionQueuedInputView = {
|
|
12
|
+
id: 'queued-1',
|
|
13
|
+
sessionId: 'session-1',
|
|
14
|
+
enqueuedAt: '2026-07-22T10:00:00.000Z',
|
|
15
|
+
metadata: {
|
|
16
|
+
requested_skill_refs: ['project:review'],
|
|
17
|
+
ui_inline_tokens: [{
|
|
18
|
+
kind: 'skill',
|
|
19
|
+
key: 'project:review',
|
|
20
|
+
label: 'Review',
|
|
21
|
+
rawText: '$review',
|
|
22
|
+
}],
|
|
23
|
+
},
|
|
24
|
+
message: {
|
|
25
|
+
id: 'message-1',
|
|
26
|
+
sessionId: 'session-1',
|
|
27
|
+
role: 'user',
|
|
28
|
+
status: 'final',
|
|
29
|
+
timestamp: '2026-07-22T10:00:00.000Z',
|
|
30
|
+
parts: [
|
|
31
|
+
{ type: 'text', text: 'please $review ' },
|
|
32
|
+
{
|
|
33
|
+
type: 'file',
|
|
34
|
+
name: 'spec.md',
|
|
35
|
+
mimeType: 'text/markdown',
|
|
36
|
+
sizeBytes: 12,
|
|
37
|
+
assetUri: 'asset://spec',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const snapshot = buildSessionQueuedInputComposerSnapshot(input, [
|
|
44
|
+
{ ref: 'project:review', name: 'Code Review' },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
expect(snapshot.text).toBe('please ');
|
|
48
|
+
expect(snapshot.nodes).toMatchObject([
|
|
49
|
+
{ type: 'text', text: 'please ' },
|
|
50
|
+
{ type: 'token', tokenKind: 'skill', tokenKey: 'project:review', label: 'Review' },
|
|
51
|
+
{ type: 'text', text: ' ' },
|
|
52
|
+
{ type: 'token', tokenKind: 'file', label: 'spec.md' },
|
|
53
|
+
]);
|
|
54
|
+
expect(snapshot.selectedSkills).toEqual(['project:review']);
|
|
55
|
+
expect(snapshot.skillRecords).toEqual([{ ref: 'project:review', name: 'Code Review' }]);
|
|
56
|
+
expect(snapshot.attachments).toMatchObject([{
|
|
57
|
+
name: 'spec.md',
|
|
58
|
+
mimeType: 'text/markdown',
|
|
59
|
+
assetUri: 'asset://spec',
|
|
60
|
+
}]);
|
|
61
|
+
expect(buildSessionQueuedInputPreview(input)).toBe('please');
|
|
62
|
+
});
|
|
63
|
+
});
|