@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,109 @@
1
+ import { createContext, useContext } from "react";
2
+ import type {
3
+ AIChatAttachment,
4
+ AIChatMessage,
5
+ AIConversation,
6
+ AIEmployee,
7
+ AIEmployeeTask,
8
+ AIEmployeeTaskTrigger,
9
+ AIModel,
10
+ AIToolCallDecision,
11
+ AIWorkContextItem,
12
+ } from "./types";
13
+
14
+ export type AIChatContextValue = {
15
+ id: string;
16
+ messages: AIChatMessage[];
17
+ status: "submitted" | "streaming" | "ready" | "error";
18
+ error?: Error;
19
+ employees: AIEmployee[];
20
+ models: AIModel[];
21
+ currentEmployee: AIEmployee;
22
+ currentModel: AIModel;
23
+ activeConversation?: AIConversation;
24
+ activeConversationId: string;
25
+ conversations: AIConversation[];
26
+ conversationsLoading: boolean;
27
+ conversationSearch: string;
28
+ messagesLoading: boolean;
29
+ historyError?: Error;
30
+ interactionError?: Error;
31
+ conversationListOpen: boolean;
32
+ availableTasks: AIEmployeeTask[];
33
+ composerFocusRequest: number;
34
+ draft: string;
35
+ attachments: AIChatAttachment[];
36
+ uploadingAttachments: boolean;
37
+ workContext: AIWorkContextItem[];
38
+ editingMessageId?: string;
39
+ setDraft: (value: string) => void;
40
+ uploadFiles: (files: File[]) => Promise<void>;
41
+ removeAttachment: (uid: string) => void;
42
+ addWorkContext: (item: AIWorkContextItem) => void;
43
+ removeWorkContext: (item: AIWorkContextItem) => void;
44
+ send: () => Promise<void>;
45
+ stop: () => Promise<void>;
46
+ regenerate: () => Promise<void>;
47
+ retryMessage: (message: AIChatMessage) => Promise<void>;
48
+ decideToolCall: (decision: AIToolCallDecision) => Promise<void>;
49
+ startNewConversation: () => void;
50
+ selectConversation: (conversationId: string) => void;
51
+ renameConversation: (conversationId: string, title: string) => Promise<void>;
52
+ removeConversation: (conversationId: string) => Promise<void>;
53
+ searchConversations: (keyword: string) => Promise<void>;
54
+ setConversationListOpen: (open: boolean) => void;
55
+ selectEmployee: (username: string) => void;
56
+ selectModel: (model: string) => void;
57
+ startEditingMessage: (message: AIChatMessage) => Promise<void>;
58
+ cancelEditingMessage: () => void;
59
+ saveUserPrompt: (prompt: string) => Promise<void>;
60
+ triggerTask: (options: AIEmployeeTaskTrigger) => Promise<void>;
61
+ runTask: (task: AIEmployeeTask) => void;
62
+ focusComposer: () => void;
63
+ };
64
+
65
+ export type AIChatMessagesContextValue = Pick<AIChatContextValue, "messages">;
66
+ export type AIChatStatusContextValue = Pick<
67
+ AIChatContextValue,
68
+ "status" | "error"
69
+ >;
70
+ export type AIChatBaseContextValue = Omit<
71
+ AIChatContextValue,
72
+ keyof AIChatMessagesContextValue | keyof AIChatStatusContextValue
73
+ >;
74
+
75
+ export const AIChatContext = createContext<AIChatBaseContextValue | null>(null);
76
+ export const AIChatMessagesContext =
77
+ createContext<AIChatMessagesContextValue | null>(null);
78
+ export const AIChatStatusContext =
79
+ createContext<AIChatStatusContextValue | null>(null);
80
+
81
+ export function useAIChatBase() {
82
+ const value = useContext(AIChatContext);
83
+ if (!value) throw new Error("useAIChatBase must be used inside AIChatProvider");
84
+ return value;
85
+ }
86
+
87
+ export function useAIChatMessages() {
88
+ const value = useContext(AIChatMessagesContext);
89
+ if (!value) {
90
+ throw new Error("useAIChatMessages must be used inside AIChatProvider");
91
+ }
92
+ return value;
93
+ }
94
+
95
+ export function useAIChatStatus() {
96
+ const value = useContext(AIChatStatusContext);
97
+ if (!value) {
98
+ throw new Error("useAIChatStatus must be used inside AIChatProvider");
99
+ }
100
+ return value;
101
+ }
102
+
103
+ export function useAIChat() {
104
+ return {
105
+ ...useAIChatBase(),
106
+ ...useAIChatMessages(),
107
+ ...useAIChatStatus(),
108
+ };
109
+ }
@@ -0,0 +1,84 @@
1
+ import { useRef, useSyncExternalStore } from "react";
2
+ import type { AIEmployeeTaskTrigger } from "./types";
3
+
4
+ export type AIChatControllerSnapshot = {
5
+ open: boolean;
6
+ };
7
+
8
+ export type AIChatController = {
9
+ getSnapshot: () => AIChatControllerSnapshot;
10
+ subscribe: (listener: () => void) => () => void;
11
+ setOpen: (open: boolean) => void;
12
+ open: () => void;
13
+ close: () => void;
14
+ triggerTask: (options: AIEmployeeTaskTrigger) => void;
15
+ bindTaskHandler: (
16
+ handler: (options: AIEmployeeTaskTrigger) => void | Promise<void>
17
+ ) => () => void;
18
+ };
19
+
20
+ export function createAIChatController(): AIChatController {
21
+ let snapshot: AIChatControllerSnapshot = { open: false };
22
+ let taskHandler:
23
+ | ((options: AIEmployeeTaskTrigger) => void | Promise<void>)
24
+ | undefined;
25
+ const listeners = new Set<() => void>();
26
+ const pendingTasks: AIEmployeeTaskTrigger[] = [];
27
+
28
+ const setOpen = (open: boolean) => {
29
+ if (snapshot.open === open) return;
30
+ snapshot = { open };
31
+ listeners.forEach((listener) => listener());
32
+ };
33
+
34
+ return {
35
+ getSnapshot: () => snapshot,
36
+ subscribe: (listener) => {
37
+ listeners.add(listener);
38
+ return () => listeners.delete(listener);
39
+ },
40
+ setOpen,
41
+ open: () => setOpen(true),
42
+ close: () => setOpen(false),
43
+ triggerTask: (options) => {
44
+ if (taskHandler) {
45
+ void taskHandler(options);
46
+ return;
47
+ }
48
+ pendingTasks.push(options);
49
+ if (pendingTasks.length > 20) pendingTasks.shift();
50
+ },
51
+ bindTaskHandler: (handler) => {
52
+ if (taskHandler && taskHandler !== handler) {
53
+ console.warn(
54
+ "An AIChatController should be bound to only one AIChatProvider at a time."
55
+ );
56
+ }
57
+ taskHandler = handler;
58
+ pendingTasks.splice(0).forEach((task) => void handler(task));
59
+ return () => {
60
+ if (taskHandler === handler) taskHandler = undefined;
61
+ };
62
+ },
63
+ };
64
+ }
65
+
66
+ export function useAIChatController() {
67
+ const controllerRef = useRef<AIChatController | null>(null);
68
+ if (!controllerRef.current) {
69
+ controllerRef.current = createAIChatController();
70
+ }
71
+ return controllerRef.current;
72
+ }
73
+
74
+ const visibleControllerSnapshot: AIChatControllerSnapshot = { open: true };
75
+ const subscribeToVisibleController = () => () => undefined;
76
+ const getVisibleControllerSnapshot = () => visibleControllerSnapshot;
77
+
78
+ export function useAIChatControllerState(controller?: AIChatController) {
79
+ return useSyncExternalStore(
80
+ controller?.subscribe ?? subscribeToVisibleController,
81
+ controller?.getSnapshot ?? getVisibleControllerSnapshot,
82
+ controller?.getSnapshot ?? getVisibleControllerSnapshot
83
+ );
84
+ }
@@ -0,0 +1,198 @@
1
+ import type { AIChatMessage, AISubAgentConversation } from "./types";
2
+
3
+ export type AIToolPart = Extract<
4
+ AIChatMessage["parts"][number],
5
+ { type: `tool-${string}` | "dynamic-tool" }
6
+ >;
7
+
8
+ export type AISubAgentPart = Extract<
9
+ AIChatMessage["parts"][number],
10
+ { type: "data-subAgent" }
11
+ >;
12
+
13
+ export const isAIToolPart = (
14
+ part: AIChatMessage["parts"][number]
15
+ ): part is AIToolPart =>
16
+ part.type === "dynamic-tool" || part.type.startsWith("tool-");
17
+
18
+ export const isAISubAgentPart = (
19
+ part: AIChatMessage["parts"][number]
20
+ ): part is AISubAgentPart => part.type === "data-subAgent";
21
+
22
+ export const getNocoBaseToolMetadata = (part: AIToolPart) => {
23
+ if (!("callProviderMetadata" in part)) return undefined;
24
+ const metadata = part.callProviderMetadata?.nocobase;
25
+ return metadata && typeof metadata === "object"
26
+ ? (metadata as { autoApprove?: unknown })
27
+ : undefined;
28
+ };
29
+
30
+ const getMessageToolCallIds = (message: AIChatMessage) =>
31
+ new Set(
32
+ message.parts
33
+ .filter(isAIToolPart)
34
+ .map((part) => part.toolCallId)
35
+ .filter(Boolean)
36
+ );
37
+
38
+ const getMessageText = (message: AIChatMessage) =>
39
+ message.parts
40
+ .filter((part) => part.type === "text")
41
+ .map((part) => part.text)
42
+ .join("\n");
43
+
44
+ const getSubAgentSessionIds = (message: AIChatMessage) =>
45
+ new Set(
46
+ message.parts
47
+ .filter(isAISubAgentPart)
48
+ .map((part) => part.data.sessionId)
49
+ .filter(Boolean)
50
+ );
51
+
52
+ const reconcileConversation = (
53
+ current: AISubAgentConversation,
54
+ refreshed: AISubAgentConversation
55
+ ): AISubAgentConversation => ({
56
+ ...refreshed,
57
+ messages: reconcileRefreshedMessages(current.messages, refreshed.messages),
58
+ });
59
+
60
+ const reconcileSubAgents = (
61
+ currentMessage: AIChatMessage,
62
+ refreshedMessage: AIChatMessage
63
+ ) => {
64
+ const current = currentMessage.parts.filter(isAISubAgentPart);
65
+ return refreshedMessage.parts.map((part) => {
66
+ if (!isAISubAgentPart(part)) return part;
67
+ const currentPart = current.find(
68
+ (item) => item.data.sessionId === part.data.sessionId
69
+ );
70
+ return currentPart
71
+ ? {
72
+ ...part,
73
+ data: reconcileConversation(currentPart.data, part.data),
74
+ }
75
+ : part;
76
+ });
77
+ };
78
+
79
+ export const reconcileRefreshedMessages = (
80
+ current: AIChatMessage[],
81
+ refreshed: AIChatMessage[]
82
+ ) => {
83
+ const usedCurrentIds = new Set<string>();
84
+ const byServerMessageId = new Map<string, AIChatMessage>();
85
+ const bySubAgentSessionId = new Map<string, AIChatMessage[]>();
86
+ const byToolCallId = new Map<string, AIChatMessage[]>();
87
+ const byRoleAndText = new Map<string, AIChatMessage[]>();
88
+ const addToIndex = (
89
+ index: Map<string, AIChatMessage[]>,
90
+ key: string,
91
+ message: AIChatMessage
92
+ ) => {
93
+ const messages = index.get(key);
94
+ if (messages) messages.push(message);
95
+ else index.set(key, [message]);
96
+ };
97
+ for (const message of current) {
98
+ const serverMessageId = message.metadata?.serverMessageId;
99
+ if (serverMessageId) byServerMessageId.set(serverMessageId, message);
100
+ for (const sessionId of getSubAgentSessionIds(message)) {
101
+ addToIndex(bySubAgentSessionId, sessionId, message);
102
+ }
103
+ for (const toolCallId of getMessageToolCallIds(message)) {
104
+ addToIndex(byToolCallId, toolCallId, message);
105
+ }
106
+ const text = getMessageText(message);
107
+ if (text) addToIndex(byRoleAndText, `${message.role}\0${text}`, message);
108
+ }
109
+ const firstUnused = (messages: AIChatMessage[] | undefined) =>
110
+ messages?.find((message) => !usedCurrentIds.has(message.id));
111
+
112
+ return refreshed.map((serverMessage) => {
113
+ const serverToolCallIds = getMessageToolCallIds(serverMessage);
114
+ const serverMessageId = serverMessage.metadata?.serverMessageId;
115
+ let match = serverMessageId
116
+ ? byServerMessageId.get(serverMessageId)
117
+ : undefined;
118
+ if (match && usedCurrentIds.has(match.id)) match = undefined;
119
+ const serverSubAgentSessionIds = getSubAgentSessionIds(serverMessage);
120
+ if (!match && serverSubAgentSessionIds.size) {
121
+ for (const sessionId of serverSubAgentSessionIds) {
122
+ match = firstUnused(bySubAgentSessionId.get(sessionId));
123
+ if (match?.role === serverMessage.role) break;
124
+ match = undefined;
125
+ }
126
+ }
127
+ if (!match && !serverSubAgentSessionIds.size && serverToolCallIds.size) {
128
+ for (const toolCallId of serverToolCallIds) {
129
+ match = firstUnused(byToolCallId.get(toolCallId));
130
+ if (match?.role === serverMessage.role) break;
131
+ match = undefined;
132
+ }
133
+ }
134
+ if (!match && !serverSubAgentSessionIds.size && !serverToolCallIds.size) {
135
+ const serverText = getMessageText(serverMessage);
136
+ if (serverText) {
137
+ match = firstUnused(
138
+ byRoleAndText.get(`${serverMessage.role}\0${serverText}`)
139
+ );
140
+ }
141
+ }
142
+ if (!match) return serverMessage;
143
+ usedCurrentIds.add(match.id);
144
+ return {
145
+ ...serverMessage,
146
+ id: match.id,
147
+ parts: reconcileSubAgents(match, serverMessage),
148
+ };
149
+ });
150
+ };
151
+
152
+ export type ResolvedChatMessage = {
153
+ rootMessage: AIChatMessage;
154
+ targetMessage: AIChatMessage;
155
+ rootIndex: number;
156
+ };
157
+
158
+ export const findChatMessage = (
159
+ messages: AIChatMessage[],
160
+ messageId: string
161
+ ): ResolvedChatMessage | undefined => {
162
+ const findNestedMessage = (
163
+ message: AIChatMessage
164
+ ): AIChatMessage | undefined => {
165
+ if (message.id === messageId) return message;
166
+ for (const part of message.parts) {
167
+ if (!isAISubAgentPart(part)) continue;
168
+ for (const nestedMessage of part.data.messages) {
169
+ const match = findNestedMessage(nestedMessage);
170
+ if (match) return match;
171
+ }
172
+ }
173
+ return undefined;
174
+ };
175
+
176
+ for (const [rootIndex, rootMessage] of messages.entries()) {
177
+ const targetMessage = findNestedMessage(rootMessage);
178
+ if (targetMessage) return { rootMessage, targetMessage, rootIndex };
179
+ }
180
+ return undefined;
181
+ };
182
+
183
+ export const getChatToolParts = (messages: AIChatMessage[]) => {
184
+ const result: Array<{ message: AIChatMessage; part: AIToolPart }> = [];
185
+ const visit = (items: AIChatMessage[]) => {
186
+ for (const message of items) {
187
+ for (const part of message.parts) {
188
+ if (isAIToolPart(part)) {
189
+ result.push({ message, part });
190
+ } else if (isAISubAgentPart(part)) {
191
+ visit(part.data.messages);
192
+ }
193
+ }
194
+ }
195
+ };
196
+ visit(messages);
197
+ return result;
198
+ };