@nextclaw/ui 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/assets/api-DLtsiBKm.js +13 -0
  3. package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
  4. package/dist/assets/book-open-BQwWBDKb.js +1 -0
  5. package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
  6. package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
  8. package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
  9. package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
  12. package/dist/assets/doc-browser-B76XOXmW.js +1 -0
  13. package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
  15. package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
  16. package/dist/assets/es2015-DhQo_oS6.js +41 -0
  17. package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
  18. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  19. package/dist/assets/i18n-provider-di1waTGX.js +1 -0
  20. package/dist/assets/index-CBRP-7-a.css +1 -0
  21. package/dist/assets/index-Z1fYpbNd.js +104 -0
  22. package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
  24. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
  25. package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
  26. package/dist/assets/plus-3ztRZD_l.js +1 -0
  27. package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
  28. package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
  29. package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
  30. package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
  31. package/dist/assets/remote-CKWZfVVB.js +1 -0
  32. package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
  33. package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
  34. package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
  35. package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
  36. package/dist/assets/search-config-page-Dm8daYny.js +1 -0
  37. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
  38. package/dist/assets/security-config-Ds0LsJmo.js +1 -0
  39. package/dist/assets/select-D7GcNP0z.js +1 -0
  40. package/dist/assets/setting-row-CuozlnJd.js +1 -0
  41. package/dist/assets/side-dock-BcLau5S_.js +3 -0
  42. package/dist/assets/skeleton-shfmXuYp.js +1 -0
  43. package/dist/assets/switch-C1BWMU5c.js +1 -0
  44. package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
  45. package/dist/assets/tooltip-DfAHHA76.js +1 -0
  46. package/dist/assets/use-config-DAdbT5QJ.js +1 -0
  47. package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
  48. package/dist/assets/x-DqL1mYLC.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  66. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  68. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  69. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  70. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
  71. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  72. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  73. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  74. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  75. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  76. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  77. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  78. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  79. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  80. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  81. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  82. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  83. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
  84. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  85. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  86. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  87. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  88. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  89. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  90. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  91. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  92. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  93. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
  94. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  95. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  96. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  97. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
  102. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  103. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
  104. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  105. package/src/features/chat/managers/chat-thread.manager.ts +9 -3
  106. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  107. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  108. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  109. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  110. package/src/features/chat/types/chat-message.types.ts +4 -0
  111. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  112. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  113. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  114. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  116. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  117. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  118. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  119. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  120. package/src/features/marketplace/index.ts +3 -0
  121. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  122. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  123. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  124. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  125. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  126. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  127. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  128. package/src/features/side-dock/components/side-dock.tsx +100 -13
  129. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  130. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  131. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  132. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  133. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  134. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  135. package/src/index.css +501 -27
  136. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  137. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  138. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  139. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  140. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  141. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  142. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  143. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  144. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  145. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  146. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  147. package/src/shared/components/ui/dialog.tsx +2 -2
  148. package/src/shared/components/ui/popover.tsx +1 -1
  149. package/src/shared/components/ui/select.tsx +1 -1
  150. package/src/shared/components/ui/switch.tsx +4 -3
  151. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  152. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  153. package/src/shared/components/ui/tooltip.tsx +1 -1
  154. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  155. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
  156. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  157. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  158. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  159. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
  160. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  163. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  164. package/src/shared/lib/session-project/index.ts +1 -1
  165. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  166. package/src/shared/lib/theme/index.ts +51 -13
  167. package/dist/assets/api-BP_8IaxB.js +0 -13
  168. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  169. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  170. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  171. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  172. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  173. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  174. package/dist/assets/index-BiE-K8-q.css +0 -1
  175. package/dist/assets/index-D9BAE_L_.js +0 -105
  176. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  177. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  178. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  179. package/dist/assets/plus-BbZqquJe.js +0 -1
  180. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  181. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  182. package/dist/assets/remote-B8TiUfwW.js +0 -1
  183. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  184. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  185. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  186. package/dist/assets/select-hoj8dK4K.js +0 -41
  187. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  188. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  189. package/dist/assets/x-D9PXs_xP.js +0 -1
  190. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  191. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  192. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  193. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  194. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -1,4 +1,13 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type Dispatch,
8
+ type MutableRefObject,
9
+ type SetStateAction,
10
+ } from 'react';
2
11
  import { buildNcpRequestEnvelope } from '@nextclaw/ncp-react';
3
12
  import type { NcpAgentSendEnvelope, NcpMessage, NcpRunHandle } from '@nextclaw/ncp';
4
13
 
@@ -9,6 +18,10 @@ import type { SessionConversationInputSnapshot } from './use-session-conversatio
9
18
  import type { useSessionConversationInputQuery } from './use-session-conversation-input-query';
10
19
 
11
20
  type SessionConversationInputQuery = ReturnType<typeof useSessionConversationInputQuery>;
21
+ type ComposerDraftSnapshot = Pick<
22
+ SessionConversationInputSnapshot,
23
+ 'text' | 'nodes' | 'selectedSkills' | 'skillRecords' | 'attachments'
24
+ >;
12
25
 
13
26
  type SessionConversationAgent = {
14
27
  readonly isHydrating: boolean;
@@ -20,10 +33,22 @@ type SessionConversationAgent = {
20
33
  readonly sessionId?: string | null;
21
34
  } | null;
22
35
  };
23
- readonly send: (envelope: NcpAgentSendEnvelope) => Promise<NcpRunHandle | null>;
36
+ readonly send: (
37
+ envelope: NcpAgentSendEnvelope,
38
+ ) => Promise<NcpRunHandle | null>;
24
39
  readonly abort: () => Promise<void>;
25
40
  };
26
41
 
42
+ export type SessionConversationQueuedInput = {
43
+ readonly id: string;
44
+ readonly preview: string;
45
+ };
46
+
47
+ type QueuedInputDraft = SessionConversationQueuedInput & {
48
+ readonly composerSnapshot: ComposerDraftSnapshot;
49
+ readonly metadata: Record<string, unknown>;
50
+ };
51
+
27
52
  export type SessionConversationMaterializationContext = {
28
53
  readonly kind: 'child';
29
54
  readonly parentSessionKey: string;
@@ -40,10 +65,49 @@ type UseSessionConversationControllerParams = {
40
65
  readonly sessionKey: string | null;
41
66
  readonly onSessionMaterialized?: (sessionKey: string) => void;
42
67
  readonly resetComposer: () => void;
43
- readonly restoreComposer: (snapshot: Pick<
44
- SessionConversationInputSnapshot,
45
- 'text' | 'nodes' | 'selectedSkills' | 'skillRecords' | 'attachments'
46
- >) => void;
68
+ readonly restoreComposer: (snapshot: ComposerDraftSnapshot) => void;
69
+ readonly setSendError: (message: string | null) => void;
70
+ };
71
+
72
+ type BuildQueuedInputDraftParams = {
73
+ readonly agentIsSending: boolean;
74
+ readonly inputSnapshot: SessionConversationInputSnapshot;
75
+ readonly inputQuery: SessionConversationInputQuery;
76
+ readonly isRuntimeBlocked: boolean;
77
+ readonly materializationContext?: SessionConversationMaterializationContext | null;
78
+ readonly selectedAgentId: string;
79
+ readonly sessionKey: string | null;
80
+ };
81
+
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[]>>;
47
111
  readonly setSendError: (message: string | null) => void;
48
112
  };
49
113
 
@@ -55,6 +119,206 @@ const resolveModelForSend = (value: string | null | undefined): string | undefin
55
119
  return trimmed || undefined;
56
120
  };
57
121
 
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
+ function buildInputAvailabilitySnapshot(inputQuery: SessionConversationInputQuery) {
134
+ return {
135
+ isProviderStateResolved: inputQuery.isProviderStateResolved,
136
+ modelOptions: inputQuery.modelOptions,
137
+ sessionTypeUnavailable: inputQuery.sessionTypeState.sessionTypeUnavailable,
138
+ };
139
+ }
140
+
141
+ function buildQueuedInputDraft(params: BuildQueuedInputDraftParams): QueuedInputDraft | null {
142
+ const {
143
+ agentIsSending,
144
+ inputQuery,
145
+ inputSnapshot,
146
+ isRuntimeBlocked,
147
+ materializationContext,
148
+ selectedAgentId,
149
+ sessionKey,
150
+ } = params;
151
+ const currentParts = deriveNcpMessagePartsFromComposer(
152
+ [...inputSnapshot.nodes],
153
+ inputSnapshot.attachments,
154
+ );
155
+ if (
156
+ isNcpChatSendDisabled({
157
+ snapshot: buildInputAvailabilitySnapshot(inputQuery),
158
+ hasSendableDraft: hasSendableMessagePart(currentParts),
159
+ isRuntimeBlocked,
160
+ }) ||
161
+ agentIsSending
162
+ ) {
163
+ return null;
164
+ }
165
+ const composerSnapshot: ComposerDraftSnapshot = {
166
+ text: inputSnapshot.text,
167
+ nodes: [...inputSnapshot.nodes],
168
+ selectedSkills: [...inputSnapshot.selectedSkills],
169
+ skillRecords: [...inputSnapshot.skillRecords],
170
+ attachments: [...inputSnapshot.attachments],
171
+ };
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
+ return {
203
+ composerSnapshot,
204
+ id: `queued-input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
205
+ metadata,
206
+ preview: buildQueuedInputPreview(composerSnapshot),
207
+ };
208
+ }
209
+
210
+ function buildQueuedDraftEnvelope(
211
+ draft: QueuedInputDraft,
212
+ sessionKey: string | null,
213
+ ): NcpAgentSendEnvelope | null {
214
+ return buildNcpRequestEnvelope({
215
+ sessionId: sessionKey ?? undefined,
216
+ text: draft.composerSnapshot.text.trim(),
217
+ attachments: [...draft.composerSnapshot.attachments],
218
+ parts: deriveNcpMessagePartsFromComposer(
219
+ [...draft.composerSnapshot.nodes],
220
+ draft.composerSnapshot.attachments,
221
+ ),
222
+ metadata: draft.metadata,
223
+ });
224
+ }
225
+
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
+
58
322
  export function useSessionConversationController(params: UseSessionConversationControllerParams) {
59
323
  const {
60
324
  agent,
@@ -75,117 +339,115 @@ export function useSessionConversationController(params: UseSessionConversationC
75
339
  );
76
340
  const hasSendableDraft = hasSendableMessagePart(parts);
77
341
  const isSending = agent.isSending || agent.isRunning;
78
- const sendDisabled = isNcpChatSendDisabled({
79
- snapshot: {
80
- isProviderStateResolved: inputQuery.isProviderStateResolved,
81
- modelOptions: inputQuery.modelOptions,
82
- sessionTypeUnavailable: inputQuery.sessionTypeState.sessionTypeUnavailable,
83
- },
342
+ const baseSendDisabled = isNcpChatSendDisabled({
343
+ snapshot: buildInputAvailabilitySnapshot(inputQuery),
84
344
  hasSendableDraft,
85
345
  isRuntimeBlocked,
86
- }) || isSending;
87
- const send = useCallback(async () => {
88
- const currentParts = deriveNcpMessagePartsFromComposer(
89
- [...inputSnapshot.nodes],
90
- inputSnapshot.attachments,
91
- );
92
- if (
93
- isNcpChatSendDisabled({
94
- snapshot: {
95
- isProviderStateResolved: inputQuery.isProviderStateResolved,
96
- modelOptions: inputQuery.modelOptions,
97
- sessionTypeUnavailable: inputQuery.sessionTypeState.sessionTypeUnavailable,
98
- },
99
- hasSendableDraft: hasSendableMessagePart(currentParts),
100
- isRuntimeBlocked,
101
- }) ||
102
- isSending
103
- ) {
104
- return;
105
- }
106
- const composerSnapshot = {
107
- text: inputSnapshot.text,
108
- nodes: inputSnapshot.nodes,
109
- selectedSkills: inputSnapshot.selectedSkills,
110
- skillRecords: inputSnapshot.skillRecords,
111
- attachments: inputSnapshot.attachments,
112
- };
113
- const metadata = buildChatRunMetadata({
114
- agentId: materializationContext ? undefined : selectedAgentId,
115
- model: materializationContext
116
- ? resolveModelForSend(inputSnapshot.selectedModel)
117
- : resolveModelForSend(inputSnapshot.selectedModel ?? inputQuery.fallbackPreferredModel ?? inputQuery.defaultModel),
118
- thinkingLevel: materializationContext
119
- ? inputSnapshot.selectedThinkingLevel ?? undefined
120
- : inputSnapshot.selectedThinkingLevel ?? inputQuery.fallbackPreferredThinking ?? undefined,
121
- sessionType: materializationContext
122
- ? undefined
123
- : inputQuery.sessionTypeState.selectedSessionType,
124
- projectRoot: materializationContext
125
- ? inputSnapshot.pendingProjectRoot
126
- : sessionKey
127
- ? inputQuery.selectedSession?.projectRoot ?? null
128
- : inputSnapshot.pendingProjectRoot,
129
- requestedSkills: [...inputSnapshot.selectedSkills],
130
- composerNodes: [...inputSnapshot.nodes],
131
- sessionMaterialization: materializationContext
132
- ? {
133
- kind: materializationContext.kind,
134
- parentSessionId: materializationContext.parentSessionKey,
135
- inheritContext: materializationContext.inheritContext,
136
- }
137
- : null,
138
- });
139
- const envelope = buildNcpRequestEnvelope({
140
- sessionId: sessionKey ?? undefined,
141
- text: inputSnapshot.text.trim(),
142
- attachments: [...inputSnapshot.attachments],
143
- parts: currentParts,
144
- metadata,
346
+ });
347
+ const sendDisabled = baseSendDisabled || agent.isSending;
348
+ const [queuedDrafts, setQueuedDrafts] = useState<QueuedInputDraft[]>([]);
349
+
350
+ const buildDraftSubmission = useCallback((): QueuedInputDraft | null => {
351
+ return buildQueuedInputDraft({
352
+ agentIsSending: agent.isSending,
353
+ inputSnapshot,
354
+ inputQuery,
355
+ isRuntimeBlocked,
356
+ materializationContext,
357
+ selectedAgentId,
358
+ sessionKey,
145
359
  });
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);
378
+ }
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);
146
388
  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) {
147
400
  return;
148
401
  }
149
402
  resetComposer();
150
403
  setSendError(null);
404
+ if (agent.isRunning) {
405
+ setQueuedDrafts((current) => [...current, draft]);
406
+ return;
407
+ }
151
408
  try {
152
- const handle = await agent.send(envelope);
153
- const materializedSessionKey =
154
- handle?.sessionId?.trim() ||
155
- agent.snapshot.activeRun?.sessionId?.trim() ||
156
- null;
157
- if (!sessionKey && materializedSessionKey) {
158
- onSessionMaterialized?.(materializedSessionKey);
159
- }
409
+ await sendQueuedDraft(draft);
160
410
  } catch (error) {
161
- restoreComposer(composerSnapshot);
411
+ restoreComposer(draft.composerSnapshot);
162
412
  const message = error instanceof Error ? error.message : String(error);
163
413
  setSendError(message);
164
414
  throw error;
165
415
  }
166
416
  }, [
167
- agent,
168
- inputQuery,
169
- inputSnapshot,
170
- isRuntimeBlocked,
171
- isSending,
172
- materializationContext,
173
- onSessionMaterialized,
417
+ agent.isRunning,
418
+ buildDraftSubmission,
174
419
  resetComposer,
175
420
  restoreComposer,
176
- selectedAgentId,
177
- sessionKey,
421
+ sendQueuedDraft,
178
422
  setSendError,
179
423
  ]);
180
424
 
425
+ const queuedDraftActions = useQueuedDraftActions({
426
+ queuedDrafts,
427
+ restoreComposer,
428
+ setQueuedDrafts,
429
+ setSendError,
430
+ });
431
+
432
+ useQueuedDraftAutoSend({
433
+ agentIsRunning: agent.isRunning,
434
+ agentIsSending: agent.isSending,
435
+ queuedDrafts,
436
+ sendQueuedDraft,
437
+ setQueuedDrafts,
438
+ setSendError,
439
+ });
440
+
181
441
  const stop = useCallback(async () => {
182
442
  await agent.abort();
183
443
  }, [agent]);
184
444
 
185
445
  return {
186
446
  canStopGeneration: agent.isRunning,
447
+ ...queuedDraftActions,
187
448
  hasSendableDraft,
188
449
  isSending,
450
+ queuedInputs: queuedDrafts.map(({ id, preview }) => ({ id, preview })),
189
451
  send,
190
452
  sendDisabled,
191
453
  stop,
@@ -1,13 +1,17 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
- import { expect, it, vi } from 'vitest';
2
+ import { beforeEach, expect, it, vi } from 'vitest';
3
3
  import { useChatInputSurfaceState } from '@/features/chat/features/input/hooks/use-chat-input-surface-state';
4
4
 
5
- vi.mock('@/features/panel-apps', () => ({
6
- usePanelApps: () => ({
5
+ const usePanelAppsMock = vi.hoisted(() =>
6
+ vi.fn(() => ({
7
7
  data: { entries: [] },
8
8
  isFetching: false,
9
9
  isLoading: false,
10
- }),
10
+ })),
11
+ );
12
+
13
+ vi.mock('@/features/panel-apps', () => ({
14
+ usePanelApps: usePanelAppsMock,
11
15
  }));
12
16
 
13
17
  function createHookParams() {
@@ -23,12 +27,17 @@ function createHookParams() {
23
27
  },
24
28
  },
25
29
  language: 'zh' as const,
30
+ onSelectPanelApp: vi.fn(),
26
31
  onSelectSkill: vi.fn(),
27
32
  recentSkillValues: [],
28
33
  skillRecords: [],
29
34
  };
30
35
  }
31
36
 
37
+ beforeEach(() => {
38
+ usePanelAppsMock.mockClear();
39
+ });
40
+
32
41
  it('keeps equivalent input surface trigger updates from rerendering the composer owner', () => {
33
42
  let renderCount = 0;
34
43
  const trigger = {
@@ -54,3 +63,19 @@ it('keeps equivalent input surface trigger updates from rerendering the composer
54
63
  act(() => result.current.setInputSurfaceTrigger({ ...trigger, end: 3, query: 'xx' }));
55
64
  expect(renderCount).toBe(3);
56
65
  });
66
+
67
+ it('loads panel apps for slash trigger panel actions', () => {
68
+ const { result } = renderHook(() => useChatInputSurfaceState(createHookParams()));
69
+
70
+ act(() =>
71
+ result.current.setInputSurfaceTrigger({
72
+ end: 5,
73
+ key: 'slash',
74
+ marker: '/',
75
+ query: 'task',
76
+ start: 0,
77
+ }),
78
+ );
79
+
80
+ expect(usePanelAppsMock).toHaveBeenLastCalledWith({ enabled: true });
81
+ });
@@ -1,11 +1,15 @@
1
1
  import { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import {
3
+ CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC,
3
4
  resolveChatInputSurfaceState,
4
5
  type ChatInputSurfaceTrigger,
5
6
  } from '@nextclaw/agent-chat-ui';
6
7
  import { usePanelApps } from '@/features/panel-apps';
7
8
  import { t, type I18nLanguage } from '@/shared/lib/i18n';
8
- import { createPanelAppReferenceInputSurfacePlugin, PANEL_APP_REFERENCE_TRIGGER_SPEC } from '@/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils';
9
+ import {
10
+ createPanelAppReferenceInputSurfacePlugin,
11
+ PANEL_APP_REFERENCE_TRIGGER_SPEC,
12
+ } from '@/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils';
9
13
  import {
10
14
  createSlashCommandInputSurfacePlugin,
11
15
  type ChatSlashCommandDescriptor,
@@ -22,6 +26,7 @@ export function useChatInputSurfaceState(params: {
22
26
  >;
23
27
  };
24
28
  language: I18nLanguage;
29
+ onSelectPanelApp: (appId: string) => void;
25
30
  onSelectSkill: (skillRef: string) => void;
26
31
  recentSkillValues: readonly string[];
27
32
  skillRecords: readonly ChatSkillRecord[];
@@ -30,6 +35,7 @@ export function useChatInputSurfaceState(params: {
30
35
  isSkillsLoading,
31
36
  itemTexts,
32
37
  language,
38
+ onSelectPanelApp,
33
39
  onSelectSkill,
34
40
  recentSkillValues,
35
41
  skillRecords,
@@ -43,13 +49,24 @@ export function useChatInputSurfaceState(params: {
43
49
  inputSurfaceTriggerSignatureRef.current = nextSignature;
44
50
  setInputSurfaceTriggerState(nextTrigger);
45
51
  }, []);
46
- const panelApps = usePanelApps({ enabled: inputSurfaceTrigger?.key === PANEL_APP_REFERENCE_TRIGGER_SPEC.key });
52
+ const shouldLoadPanelApps =
53
+ inputSurfaceTrigger?.key === PANEL_APP_REFERENCE_TRIGGER_SPEC.key ||
54
+ inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
55
+ const panelApps = usePanelApps({ enabled: shouldLoadPanelApps });
47
56
 
48
57
  const inputSurfacePlugins = useMemo(
49
58
  () => [
50
59
  createSlashCommandInputSurfacePlugin({
51
60
  commands,
52
- itemTexts: itemTexts.slashTexts,
61
+ itemTexts: {
62
+ panelAppTexts: {
63
+ appIdLabel: t('chatPanelAppReferenceAppId', language),
64
+ fileLabel: t('chatPanelAppReferenceFile', language),
65
+ noDescriptionLabel: t('chatPanelAppReferenceNoDescription', language),
66
+ subtitle: t('chatPanelAppReferenceType', language),
67
+ },
68
+ skillTexts: itemTexts.slashTexts,
69
+ },
53
70
  menuTexts: {
54
71
  loadingLabel: t('chatSlashLoading', language),
55
72
  sectionLabel: t('chatSlashSection', language),
@@ -61,9 +78,16 @@ export function useChatInputSurfaceState(params: {
61
78
  commandHintLabel: t('chatSlashCommandHint', language),
62
79
  commandSectionLabel: t('chatSlashSectionCommands', language),
63
80
  commandSubtitle: t('chatSlashTypeCommand', language),
81
+ filterAllLabel: t('chatSlashFilterAll', language),
82
+ filterCommandsLabel: t('chatSlashFilterCommands', language),
83
+ filterPanelAppsLabel: t('chatSlashFilterPanelApps', language),
84
+ filterSkillsLabel: t('chatSlashFilterSkills', language),
85
+ panelAppHintLabel: t('chatSlashPanelAppHint', language),
86
+ panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
64
87
  skillHintLabel: t('chatSlashSkillHint', language),
65
88
  skillSectionLabel: t('chatSlashSectionSkills', language)
66
89
  },
90
+ onSelectPanelApp,
67
91
  onSelectSkill
68
92
  }),
69
93
  createPanelAppReferenceInputSurfacePlugin({
@@ -86,6 +110,7 @@ export function useChatInputSurfaceState(params: {
86
110
  commands,
87
111
  itemTexts.slashTexts,
88
112
  language,
113
+ onSelectPanelApp,
89
114
  onSelectSkill
90
115
  ]
91
116
  );