@nocobase/portal-template-default 1.0.1 → 1.0.3

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 (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,463 @@
1
+ import type { InferUIMessageChunk } from "ai";
2
+ import { parseNocoBaseSSE, type NocoBaseStreamEvent } from "./stream-parser";
3
+ import { StreamCoalescer } from "./stream-coalescer";
4
+ import {
5
+ getToolCallState,
6
+ getToolProviderMetadata,
7
+ isRecord,
8
+ parseToolInput,
9
+ toolCallsFromEvent,
10
+ type NocoBaseToolCall,
11
+ } from "./stream-event-utils";
12
+ import { SubAgentStreamAccumulator } from "./sub-agent-stream";
13
+ import type { AIChatMessage } from "./types";
14
+
15
+ const VISUAL_DELTA_FLUSH_INTERVAL = 50;
16
+ const VISUAL_DELTA_FLUSH_SIZE = 768;
17
+ const TOOL_INPUT_FLUSH_INTERVAL = 50;
18
+ const TOOL_INPUT_FLUSH_SIZE = 2048;
19
+
20
+ type AIChatChunk = InferUIMessageChunk<AIChatMessage>;
21
+
22
+ type BufferedVisualDelta = {
23
+ type: "text-delta" | "reasoning-delta";
24
+ id: string;
25
+ delta: string;
26
+ };
27
+
28
+ type BufferedSubAgentEvent = {
29
+ event: NocoBaseStreamEvent;
30
+ content: string;
31
+ };
32
+
33
+ type BufferedSubAgentToolEvent = {
34
+ event: NocoBaseStreamEvent;
35
+ chunks: NocoBaseToolCall[];
36
+ size: number;
37
+ };
38
+
39
+ const reasoningContent = (event: NocoBaseStreamEvent) => {
40
+ if (typeof event.body === "object" && event.body) {
41
+ const content = (event.body as { content?: unknown }).content;
42
+ return typeof content === "string" ? content : "";
43
+ }
44
+ return "";
45
+ };
46
+
47
+ const mergeToolCallChunks = (
48
+ current: BufferedSubAgentToolEvent,
49
+ incoming: BufferedSubAgentToolEvent
50
+ ) => {
51
+ const chunks = current.chunks.map((chunk) => ({ ...chunk }));
52
+ for (const chunk of incoming.chunks) {
53
+ const last = chunks.at(-1);
54
+ if (
55
+ last &&
56
+ !chunk.name &&
57
+ (chunk.id === undefined || chunk.id === last.id) &&
58
+ (chunk.index === undefined || chunk.index === last.index) &&
59
+ typeof chunk.args === "string"
60
+ ) {
61
+ last.args = `${typeof last.args === "string" ? last.args : ""}${
62
+ chunk.args
63
+ }`;
64
+ } else {
65
+ chunks.push({ ...chunk });
66
+ }
67
+ }
68
+ return {
69
+ event: incoming.event,
70
+ chunks,
71
+ size: current.size + incoming.size,
72
+ };
73
+ };
74
+
75
+ export function createNocoBaseUIMessageStream(
76
+ stream: ReadableStream<Uint8Array>,
77
+ messageId: string | null = `assistant-${crypto.randomUUID()}`,
78
+ options: { waitForNewMessage?: boolean; seedMessage?: AIChatMessage } = {}
79
+ ): ReadableStream<AIChatChunk> {
80
+ return new ReadableStream<AIChatChunk>({
81
+ async start(controller) {
82
+ let textId = `text-${crypto.randomUUID()}`;
83
+ let reasoningId = `reasoning-${crypto.randomUUID()}`;
84
+ let textStarted = false;
85
+ let reasoningStarted = false;
86
+ let currentToolCallId: string | undefined;
87
+ let responseStarted = options.waitForNewMessage !== true;
88
+ const announcedToolCalls = new Set<string>();
89
+ const toolCallNames = new Map<string, string>();
90
+ const toolCallInputs = new Map<string, unknown>();
91
+ const subAgents = new SubAgentStreamAccumulator(options.seedMessage);
92
+ controller.enqueue(
93
+ messageId === null ? { type: "start" } : { type: "start", messageId }
94
+ );
95
+
96
+ const visualDeltas = new StreamCoalescer<string, BufferedVisualDelta>({
97
+ interval: VISUAL_DELTA_FLUSH_INTERVAL,
98
+ maxSize: VISUAL_DELTA_FLUSH_SIZE,
99
+ getSize: (value) => value.delta.length,
100
+ merge: (current, incoming) => ({
101
+ ...incoming,
102
+ delta: `${current.delta}${incoming.delta}`,
103
+ }),
104
+ onFlush: (_, value) => controller.enqueue(value),
105
+ });
106
+
107
+ const toolInputDeltas = new StreamCoalescer<string, string>({
108
+ interval: TOOL_INPUT_FLUSH_INTERVAL,
109
+ maxSize: TOOL_INPUT_FLUSH_SIZE,
110
+ getSize: (value) => value.length,
111
+ merge: (current, incoming) => `${current}${incoming}`,
112
+ onFlush: (toolCallId, inputTextDelta) => {
113
+ controller.enqueue({
114
+ type: "tool-input-delta",
115
+ toolCallId,
116
+ inputTextDelta,
117
+ });
118
+ },
119
+ });
120
+
121
+ const subAgentToolEvents = new StreamCoalescer<
122
+ string,
123
+ BufferedSubAgentToolEvent
124
+ >({
125
+ interval: TOOL_INPUT_FLUSH_INTERVAL,
126
+ maxSize: TOOL_INPUT_FLUSH_SIZE,
127
+ getSize: (value) => value.size,
128
+ merge: mergeToolCallChunks,
129
+ onFlush: (_, value) => {
130
+ for (const chunk of subAgents.process({
131
+ ...value.event,
132
+ body: value.chunks,
133
+ })) {
134
+ controller.enqueue(chunk);
135
+ }
136
+ },
137
+ });
138
+
139
+ const enqueueSubAgentToolEvent = (event: NocoBaseStreamEvent) => {
140
+ const sessionId = event.sessionId;
141
+ if (!sessionId) return;
142
+ const chunks = toolCallsFromEvent(event);
143
+ subAgentToolEvents.push(sessionId, {
144
+ event,
145
+ chunks,
146
+ size: chunks.reduce(
147
+ (size, chunk) =>
148
+ size + (typeof chunk.args === "string" ? chunk.args.length : 0),
149
+ 0
150
+ ),
151
+ });
152
+ };
153
+
154
+ const enqueueVisualDelta = (chunk: BufferedVisualDelta) => {
155
+ visualDeltas.push(`${chunk.type}:${chunk.id}`, chunk);
156
+ };
157
+
158
+ const subAgentNarrativeEvents = new StreamCoalescer<
159
+ string,
160
+ BufferedSubAgentEvent
161
+ >({
162
+ interval: VISUAL_DELTA_FLUSH_INTERVAL,
163
+ maxSize: VISUAL_DELTA_FLUSH_SIZE,
164
+ getSize: (value) => value.content.length,
165
+ merge: (current, incoming) => ({
166
+ event: incoming.event,
167
+ content: `${current.content}${incoming.content}`,
168
+ }),
169
+ onFlush: (_, pending) => {
170
+ const body =
171
+ pending.event.type === "reasoning" && isRecord(pending.event.body)
172
+ ? { ...pending.event.body, content: pending.content }
173
+ : pending.content;
174
+ for (const chunk of subAgents.process({ ...pending.event, body })) {
175
+ controller.enqueue(chunk);
176
+ }
177
+ },
178
+ });
179
+
180
+ const subAgentNarrativeKey = (sessionId: string, type: string) =>
181
+ `${sessionId}:${type}`;
182
+
183
+ const flushSubAgentNarrativeEvents = (sessionId?: string) => {
184
+ if (!sessionId) {
185
+ subAgentNarrativeEvents.flushAll();
186
+ return;
187
+ }
188
+ subAgentNarrativeEvents.flush(
189
+ subAgentNarrativeKey(sessionId, "reasoning")
190
+ );
191
+ subAgentNarrativeEvents.flush(
192
+ subAgentNarrativeKey(sessionId, "content")
193
+ );
194
+ };
195
+
196
+ const enqueueSubAgentNarrativeEvent = (event: NocoBaseStreamEvent) => {
197
+ const sessionId = event.sessionId;
198
+ if (!sessionId) return;
199
+ const content =
200
+ event.type === "content" && typeof event.body === "string"
201
+ ? event.body
202
+ : event.type === "reasoning" && isRecord(event.body)
203
+ ? typeof event.body.content === "string"
204
+ ? event.body.content
205
+ : ""
206
+ : "";
207
+ if (!content) return;
208
+ const otherType = event.type === "content" ? "reasoning" : "content";
209
+ subAgentNarrativeEvents.flush(
210
+ subAgentNarrativeKey(sessionId, otherType)
211
+ );
212
+ subAgentNarrativeEvents.push(
213
+ subAgentNarrativeKey(sessionId, event.type),
214
+ { event, content }
215
+ );
216
+ };
217
+
218
+ const finishActiveText = () => {
219
+ if (!textStarted) return;
220
+ visualDeltas.flush(`text-delta:${textId}`);
221
+ controller.enqueue({ type: "text-end", id: textId });
222
+ textStarted = false;
223
+ textId = `text-${crypto.randomUUID()}`;
224
+ };
225
+
226
+ const finishActiveReasoning = () => {
227
+ if (!reasoningStarted) return;
228
+ visualDeltas.flush(`reasoning-delta:${reasoningId}`);
229
+ controller.enqueue({ type: "reasoning-end", id: reasoningId });
230
+ reasoningStarted = false;
231
+ reasoningId = `reasoning-${crypto.randomUUID()}`;
232
+ };
233
+
234
+ const finishActiveNarrative = () => {
235
+ finishActiveReasoning();
236
+ finishActiveText();
237
+ };
238
+
239
+ try {
240
+ for await (const event of parseNocoBaseSSE(stream)) {
241
+ if (event.type !== "tool_call_chunks") {
242
+ toolInputDeltas.flushAll();
243
+ }
244
+ if (event.from === "sub-agent") {
245
+ finishActiveNarrative();
246
+ if (event.type === "content" || event.type === "reasoning") {
247
+ if (event.sessionId) subAgentToolEvents.flush(event.sessionId);
248
+ enqueueSubAgentNarrativeEvent(event);
249
+ continue;
250
+ }
251
+ flushSubAgentNarrativeEvents(event.sessionId);
252
+ if (event.type === "tool_call_chunks") {
253
+ enqueueSubAgentToolEvent(event);
254
+ continue;
255
+ }
256
+ if (event.sessionId) subAgentToolEvents.flush(event.sessionId);
257
+ for (const chunk of subAgents.process(event)) {
258
+ controller.enqueue(chunk);
259
+ }
260
+ continue;
261
+ }
262
+
263
+ flushSubAgentNarrativeEvents();
264
+ subAgentToolEvents.flushAll();
265
+
266
+ if (event.type === "new_message") {
267
+ responseStarted = true;
268
+ continue;
269
+ }
270
+
271
+ if (!responseStarted && event.type !== "error") {
272
+ continue;
273
+ }
274
+
275
+ if (event.type === "content" && typeof event.body === "string") {
276
+ finishActiveReasoning();
277
+ if (!textStarted) {
278
+ controller.enqueue({ type: "text-start", id: textId });
279
+ textStarted = true;
280
+ }
281
+ enqueueVisualDelta({
282
+ type: "text-delta",
283
+ id: textId,
284
+ delta: event.body,
285
+ });
286
+ }
287
+
288
+ if (event.type === "reasoning") {
289
+ finishActiveText();
290
+ const delta = reasoningContent(event);
291
+ if (delta && !reasoningStarted) {
292
+ controller.enqueue({
293
+ type: "reasoning-start",
294
+ id: reasoningId,
295
+ });
296
+ reasoningStarted = true;
297
+ }
298
+ if (delta) {
299
+ enqueueVisualDelta({
300
+ type: "reasoning-delta",
301
+ id: reasoningId,
302
+ delta,
303
+ });
304
+ }
305
+ }
306
+
307
+ if (event.type === "tool_call_chunks") {
308
+ finishActiveNarrative();
309
+ for (const chunk of toolCallsFromEvent(event)) {
310
+ const toolCallId = chunk.id ?? currentToolCallId;
311
+ if (!toolCallId) continue;
312
+ currentToolCallId = toolCallId;
313
+ if (chunk.name && !announcedToolCalls.has(toolCallId)) {
314
+ toolCallNames.set(toolCallId, chunk.name);
315
+ controller.enqueue({
316
+ type: "tool-input-start",
317
+ toolCallId,
318
+ toolName: chunk.name,
319
+ dynamic: true,
320
+ });
321
+ announcedToolCalls.add(toolCallId);
322
+ }
323
+ if (typeof chunk.args === "string" && chunk.args) {
324
+ const previousInput = toolCallInputs.get(toolCallId);
325
+ toolCallInputs.set(
326
+ toolCallId,
327
+ `${typeof previousInput === "string" ? previousInput : ""}${
328
+ chunk.args
329
+ }`
330
+ );
331
+ toolInputDeltas.push(toolCallId, chunk.args);
332
+ }
333
+ }
334
+ }
335
+
336
+ if (event.type === "tool_calls") {
337
+ finishActiveNarrative();
338
+ for (const toolCall of toolCallsFromEvent(event)) {
339
+ const toolCallId = toolCall.id ?? `tool-${crypto.randomUUID()}`;
340
+ const toolName = toolCall.name ?? "tool";
341
+ const toolState = getToolCallState(toolCall);
342
+ const { invokeStatus, resultStatus } = toolState;
343
+ const toolInput = parseToolInput(
344
+ toolCall.args ?? toolCall.input ?? {}
345
+ );
346
+ toolCallNames.set(toolCallId, toolName);
347
+ toolCallInputs.set(toolCallId, toolInput);
348
+ controller.enqueue({
349
+ type: "tool-input-available",
350
+ toolCallId,
351
+ toolName,
352
+ input: toolInput,
353
+ providerMetadata: getToolProviderMetadata(toolCall),
354
+ dynamic: true,
355
+ });
356
+ announcedToolCalls.add(toolCallId);
357
+
358
+ if (toolState.failed) {
359
+ controller.enqueue({
360
+ type: "tool-output-error",
361
+ toolCallId,
362
+ errorText: String(
363
+ toolCall.content ?? toolCall.output ?? "Tool call failed"
364
+ ),
365
+ dynamic: true,
366
+ });
367
+ } else if (toolState.completed) {
368
+ controller.enqueue({
369
+ type: "tool-output-available",
370
+ toolCallId,
371
+ output: toolCall.output ??
372
+ toolCall.content ?? {
373
+ status: invokeStatus || resultStatus,
374
+ },
375
+ dynamic: true,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ if (event.type === "tool_call_status" && isRecord(event.body)) {
382
+ finishActiveNarrative();
383
+ const toolCall = isRecord(event.body.toolCall)
384
+ ? (event.body.toolCall as NocoBaseToolCall)
385
+ : undefined;
386
+ const toolCallId = toolCall?.id;
387
+ if (toolCallId) {
388
+ const mergedToolCall = {
389
+ ...toolCall,
390
+ invokeStatus:
391
+ typeof event.body.invokeStatus === "string"
392
+ ? event.body.invokeStatus
393
+ : toolCall.invokeStatus,
394
+ status:
395
+ typeof event.body.status === "string"
396
+ ? event.body.status
397
+ : toolCall.status,
398
+ content: event.body.content ?? toolCall.content,
399
+ } satisfies NocoBaseToolCall;
400
+ const toolState = getToolCallState(mergedToolCall);
401
+ const { invokeStatus, resultStatus } = toolState;
402
+ const toolName =
403
+ toolCall.name ?? toolCallNames.get(toolCallId) ?? "tool";
404
+ const toolInput =
405
+ toolCall.args !== undefined || toolCall.input !== undefined
406
+ ? parseToolInput(toolCall.args ?? toolCall.input)
407
+ : parseToolInput(toolCallInputs.get(toolCallId) ?? {});
408
+ toolCallNames.set(toolCallId, toolName);
409
+ toolCallInputs.set(toolCallId, toolInput);
410
+ controller.enqueue({
411
+ type: "tool-input-available",
412
+ toolCallId,
413
+ toolName,
414
+ input: toolInput,
415
+ providerMetadata: getToolProviderMetadata(mergedToolCall),
416
+ dynamic: true,
417
+ });
418
+ announcedToolCalls.add(toolCallId);
419
+ if (toolState.failed) {
420
+ controller.enqueue({
421
+ type: "tool-output-error",
422
+ toolCallId,
423
+ errorText: String(event.body.content ?? "Tool call failed"),
424
+ dynamic: true,
425
+ });
426
+ } else if (toolState.completed) {
427
+ controller.enqueue({
428
+ type: "tool-output-available",
429
+ toolCallId,
430
+ output: event.body.content ?? {
431
+ status: invokeStatus || resultStatus,
432
+ },
433
+ dynamic: true,
434
+ });
435
+ }
436
+ }
437
+ }
438
+
439
+ if (event.type === "error") {
440
+ controller.enqueue({
441
+ type: "error",
442
+ errorText: String(event.body ?? "AI response failed"),
443
+ });
444
+ }
445
+ }
446
+
447
+ toolInputDeltas.flushAll();
448
+ visualDeltas.flushAll();
449
+ flushSubAgentNarrativeEvents();
450
+ subAgentToolEvents.flushAll();
451
+ finishActiveNarrative();
452
+ controller.enqueue({ type: "finish" });
453
+ controller.close();
454
+ } catch (error) {
455
+ toolInputDeltas.clear();
456
+ visualDeltas.clear();
457
+ subAgentNarrativeEvents.clear();
458
+ subAgentToolEvents.clear();
459
+ controller.error(error);
460
+ }
461
+ },
462
+ });
463
+ }
@@ -0,0 +1,94 @@
1
+ import type { Chat } from "@ai-sdk/react";
2
+ import { useCallback, useRef } from "react";
3
+ import { getChatToolParts, getNocoBaseToolMetadata } from "./chat-message-utils";
4
+ import type { AIChatMessage, AIToolCallDecision } from "./types";
5
+
6
+ const isChatRunning = (chat: Chat<AIChatMessage>) =>
7
+ chat.status === "streaming" || chat.status === "submitted";
8
+
9
+ export function useAutomaticToolApproval({
10
+ enabled,
11
+ decide,
12
+ }: {
13
+ enabled: boolean;
14
+ decide: (
15
+ conversationId: string,
16
+ targetChat: Chat<AIChatMessage>,
17
+ decision: AIToolCallDecision,
18
+ options?: { automatic?: boolean }
19
+ ) => Promise<void>;
20
+ }) {
21
+ const approvedRef = useRef(new Set<string>());
22
+ const runningRef = useRef(new Set<string>());
23
+ const rerunRef = useRef(new Set<string>());
24
+
25
+ const process = useCallback(
26
+ async (conversationId: string, targetChat: Chat<AIChatMessage>) => {
27
+ if (!enabled || isChatRunning(targetChat)) return;
28
+ if (runningRef.current.has(conversationId)) {
29
+ rerunRef.current.add(conversationId);
30
+ return;
31
+ }
32
+
33
+ runningRef.current.add(conversationId);
34
+ try {
35
+ do {
36
+ rerunRef.current.delete(conversationId);
37
+ const pending = getChatToolParts(targetChat.messages)
38
+ .filter(
39
+ ({ part }) => getNocoBaseToolMetadata(part)?.autoApprove === true
40
+ )
41
+ .flatMap(({ message, part }) => {
42
+ const key = `${conversationId}:${message.id}:${part.toolCallId}`;
43
+ return approvedRef.current.has(key)
44
+ ? []
45
+ : [{ key, message, part }];
46
+ });
47
+
48
+ for (const { key, message, part } of pending) {
49
+ approvedRef.current.add(key);
50
+ try {
51
+ await decide(
52
+ conversationId,
53
+ targetChat,
54
+ {
55
+ messageId: message.id,
56
+ toolCallId: part.toolCallId,
57
+ toolName:
58
+ part.type === "dynamic-tool"
59
+ ? part.toolName
60
+ : part.type.slice(5),
61
+ decision: "approve",
62
+ },
63
+ { automatic: true }
64
+ );
65
+ } catch (error) {
66
+ approvedRef.current.delete(key);
67
+ throw error;
68
+ }
69
+ }
70
+ } while (rerunRef.current.has(conversationId));
71
+ } finally {
72
+ runningRef.current.delete(conversationId);
73
+ rerunRef.current.delete(conversationId);
74
+ }
75
+ },
76
+ [decide, enabled]
77
+ );
78
+
79
+ const clearConversation = useCallback((conversationId: string) => {
80
+ for (const key of approvedRef.current) {
81
+ if (key.startsWith(`${conversationId}:`)) approvedRef.current.delete(key);
82
+ }
83
+ runningRef.current.delete(conversationId);
84
+ rerunRef.current.delete(conversationId);
85
+ }, []);
86
+
87
+ const reset = useCallback(() => {
88
+ approvedRef.current.clear();
89
+ runningRef.current.clear();
90
+ rerunRef.current.clear();
91
+ }, []);
92
+
93
+ return { clearConversation, process, reset };
94
+ }