@nextclaw/ui 0.15.13 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/{api-DGG69ZcJ.js → api-D5UCLVE_.js} +3 -3
  3. package/dist/assets/app-presenter-provider-DcpcRgLn.js +106 -0
  4. package/dist/assets/{appearance-settings-page-BZSd1SrH.js → appearance-settings-page-yJR6Kt9c.js} +1 -1
  5. package/dist/assets/{book-open-BhaS5BJv.js → book-open-WsEnuW9P.js} +1 -1
  6. package/dist/assets/{card-Bcr5YM8F.js → card-xVrS1b1T.js} +1 -1
  7. package/dist/assets/{channels-list-page-C_x83ZCT.js → channels-list-page-CAWuTHSM.js} +1 -1
  8. package/dist/assets/{chat-page-D2xfR6TK.js → chat-page-CXYXMIQQ.js} +1 -1
  9. package/dist/assets/{config-split-page-RXAoLeNv.js → config-split-page-xj0UtMow.js} +1 -1
  10. package/dist/assets/{confirm-dialog-o_VPKvVD.js → confirm-dialog-e2YCUzt1.js} +1 -1
  11. package/dist/assets/{desktop-update-config-DEOMOsDN.js → desktop-update-config-D5NIaimo.js} +1 -1
  12. package/dist/assets/{dist-C87P4QWA.js → dist-Ca-jiFrK.js} +1 -1
  13. package/dist/assets/doc-browser-B2tNEKrJ.js +1 -0
  14. package/dist/assets/{doc-browser-DQ75iUkA.js → doc-browser-CZX6I6CU.js} +1 -1
  15. package/dist/assets/{doc-browser-context-tE0--wSa.js → doc-browser-context-CGz0LULR.js} +1 -1
  16. package/dist/assets/{eye-DL9QkrKg.js → eye-B30J471A.js} +1 -1
  17. package/dist/assets/{form-actions-J0tIaUdK.js → form-actions-CnSzu3WD.js} +1 -1
  18. package/dist/assets/{index-B9QEjnoY.js → index-6Esxu-Pp.js} +2 -2
  19. package/dist/assets/index-BzSgA8tR.css +1 -0
  20. package/dist/assets/loader-circle-DjwSRQHW.js +1 -0
  21. package/dist/assets/{logo-badge-BYTbZbOF.js → logo-badge-CISsltAo.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +1 -0
  23. package/dist/assets/{mcp-marketplace-page-COWlzeP1.js → mcp-marketplace-page-CPcPzMUQ.js} +1 -1
  24. package/dist/assets/{model-config-page-oNqnMXNY.js → model-config-page-D6ex3_NM.js} +1 -1
  25. package/dist/assets/{navigation-link-Dk6VdDkc.js → navigation-link-qPPUiLt0.js} +1 -1
  26. package/dist/assets/plus-C45awBb1.js +1 -0
  27. package/dist/assets/{popover-Txwrum65.js → popover-DHx55dbc.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-B7rQM4b6.js → provider-scoped-model-input-1TJxAhx6.js} +1 -1
  29. package/dist/assets/{providers-config-page-CP-OnvLK.js → providers-config-page-BwHtHIPe.js} +1 -1
  30. package/dist/assets/{react-B0LbRnAE.js → react-DerwD2EA.js} +1 -1
  31. package/dist/assets/{refresh-cw-C84mMku5.js → refresh-cw-C7bynrWX.js} +1 -1
  32. package/dist/assets/remote-CPpTKDgX.js +1 -0
  33. package/dist/assets/rotate-cw-Bhx0XkQP.js +1 -0
  34. package/dist/assets/{runtime-config-page-u_fuHxc8.js → runtime-config-page-CMLVC5UV.js} +1 -1
  35. package/dist/assets/{save-R_qyrHpa.js → save-CsRsFtHS.js} +1 -1
  36. package/dist/assets/{search-UKxiouNi.js → search-D0HtJIL-.js} +1 -1
  37. package/dist/assets/{search-config-page-DzkpSGFH.js → search-config-page-n1GElgij.js} +1 -1
  38. package/dist/assets/{secrets-config-page-DhoLYm7Z.js → secrets-config-page-DbUc3Ggi.js} +1 -1
  39. package/dist/assets/security-config-n2WHhHXV.js +1 -0
  40. package/dist/assets/{select-CYHT8P5S.js → select-C1zfp9zM.js} +1 -1
  41. package/dist/assets/{settings-2-DGh4bGSi.js → settings-2-Ba3cLSN2.js} +1 -1
  42. package/dist/assets/skeleton-DL5npsVL.js +1 -0
  43. package/dist/assets/{tabs-BCihm1zK.js → tabs-DmKgMKzb.js} +1 -1
  44. package/dist/assets/{tag-chip-De0iz4o2.js → tag-chip-2vwlVUwM.js} +1 -1
  45. package/dist/assets/{trash-2-Bo3DWiju.js → trash-2-CUpWjNHu.js} +1 -1
  46. package/dist/assets/{use-config-DCzwgChy.js → use-config-BvxDWV-M.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-WZ94CehU.js → use-confirm-dialog-CeUEE4p5.js} +1 -1
  48. package/dist/assets/{use-viewport-layout-CZ-3pMne.js → use-viewport-layout-DaKJ-kap.js} +1 -1
  49. package/dist/assets/x-DGdYT7rH.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -1
  53. package/src/features/chat/components/conversation/chat-conversation-content.tsx +2 -2
  54. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +11 -0
  55. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +4 -0
  56. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +3 -0
  57. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +64 -72
  58. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +85 -0
  59. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +108 -257
  60. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +52 -0
  61. package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +63 -0
  62. package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +134 -0
  63. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +5 -5
  64. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +113 -32
  65. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +3 -2
  66. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +118 -12
  67. package/src/features/chat/features/message/components/__tests__/chat-message-list-skill-preview.container.test.tsx +170 -0
  68. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +18 -9
  69. package/src/features/chat/features/message/components/chat-message-list.container.tsx +36 -23
  70. package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +90 -0
  71. package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +3 -5
  72. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +51 -0
  73. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +9 -3
  74. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +42 -8
  75. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
  76. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
  77. package/dist/assets/app-presenter-provider-B1RgR5ug.js +0 -106
  78. package/dist/assets/doc-browser-BuGh646I.js +0 -1
  79. package/dist/assets/index-B2YF-7Yw.css +0 -1
  80. package/dist/assets/loader-circle-CAnHHSrY.js +0 -1
  81. package/dist/assets/mcp-marketplace-page-CaUFv37E.js +0 -1
  82. package/dist/assets/plus-DjdTWPjQ.js +0 -1
  83. package/dist/assets/remote-2gZkJSX4.js +0 -1
  84. package/dist/assets/rotate-cw-DQJhDnjg.js +0 -1
  85. package/dist/assets/security-config-yraYoy9s.js +0 -1
  86. package/dist/assets/skeleton-BQuylYFO.js +0 -1
  87. package/dist/assets/x-NZxXyqW1.js +0 -1
@@ -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 QueuedInputDraft = SessionConversationQueuedInput & {
48
- readonly composerSnapshot: ComposerDraftSnapshot;
49
- readonly metadata: Record<string, unknown>;
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 BuildQueuedInputDraftParams = {
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 SendEnvelope = (
83
- envelope: NcpAgentSendEnvelope,
84
- ) => Promise<NcpRunHandle | null>;
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 buildQueuedInputDraft(params: BuildQueuedInputDraftParams): QueuedInputDraft | null {
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
- id: `queued-input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
205
- metadata,
206
- preview: buildQueuedInputPreview(composerSnapshot),
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 buildQueuedDraftEnvelope(
211
- draft: QueuedInputDraft,
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 baseSendDisabled = isNcpChatSendDisabled({
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 buildDraftSubmission = useCallback((): QueuedInputDraft | null => {
351
- return buildQueuedInputDraft({
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
- agent.isSending,
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
- return handle;
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 sendQueuedDraft(draft);
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
- const message = error instanceof Error ? error.message : String(error);
413
- setSendError(message);
244
+ setSendError(error instanceof Error ? error.message : String(error));
414
245
  throw error;
415
246
  }
416
247
  }, [
417
- agent.isRunning,
418
- buildDraftSubmission,
248
+ agent,
249
+ inputQuery,
250
+ inputSnapshot,
251
+ isRuntimeBlocked,
252
+ materializationContext,
253
+ onSessionMaterialized,
419
254
  resetComposer,
420
255
  restoreComposer,
421
- sendQueuedDraft,
256
+ selectedAgentId,
257
+ sessionKey,
422
258
  setSendError,
423
259
  ]);
424
260
 
425
- const queuedDraftActions = useQueuedDraftActions({
426
- queuedDrafts,
427
- restoreComposer,
428
- setQueuedDrafts,
429
- setSendError,
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
- useQueuedDraftAutoSend({
433
- agentIsRunning: agent.isRunning,
434
- agentIsSending: agent.isSending,
435
- queuedDrafts,
436
- sendQueuedDraft,
437
- setQueuedDrafts,
438
- setSendError,
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
- ...queuedDraftActions,
294
+ deleteQueuedInput,
295
+ editQueuedInput,
448
296
  hasSendableDraft,
449
297
  isSending,
450
- queuedInputs: queuedDrafts.map(({ id, preview }) => ({ id, preview })),
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
+ }
@@ -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
+ });