@antelopejs/dms-ai 0.1.3 → 0.1.5

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 (245) hide show
  1. package/README.md +48 -29
  2. package/dist/channels/bridge.d.ts +2 -8
  3. package/dist/channels/bridge.js +26 -42
  4. package/dist/channels/bridge.js.map +1 -1
  5. package/dist/channels/channel-service.d.ts +2 -2
  6. package/dist/channels/channel-service.js +12 -31
  7. package/dist/channels/channel-service.js.map +1 -1
  8. package/dist/channels/registry.d.ts +2 -2
  9. package/dist/channels/registry.js +7 -7
  10. package/dist/channels/registry.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/constants/channels.d.ts +5 -10
  15. package/dist/constants/channels.js +6 -10
  16. package/dist/constants/channels.js.map +1 -1
  17. package/dist/constants/sidecar.d.ts +2 -1
  18. package/dist/constants/sidecar.js +3 -2
  19. package/dist/constants/sidecar.js.map +1 -1
  20. package/dist/index.js +8 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/lifecycle/build-id.d.ts +1 -0
  23. package/dist/lifecycle/build-id.js +32 -0
  24. package/dist/lifecycle/build-id.js.map +1 -0
  25. package/dist/lifecycle/sidecar-socket.d.ts +1 -1
  26. package/dist/lifecycle/sidecar-socket.js +2 -2
  27. package/dist/lifecycle/sidecar-socket.js.map +1 -1
  28. package/dist/lifecycle/spawn-sidecar.d.ts +0 -1
  29. package/dist/lifecycle/spawn-sidecar.js +5 -27
  30. package/dist/lifecycle/spawn-sidecar.js.map +1 -1
  31. package/dist/pages/settings/page.js.map +1 -1
  32. package/dist/routes/channels.d.ts +3 -3
  33. package/dist/routes/channels.js +11 -12
  34. package/dist/routes/channels.js.map +1 -1
  35. package/dist/routes/index.d.ts +0 -1
  36. package/dist/routes/index.js +0 -1
  37. package/dist/routes/index.js.map +1 -1
  38. package/frontend-vue/.prettierignore +3 -0
  39. package/frontend-vue/.prettierrc +10 -0
  40. package/frontend-vue/app/chat/components/ChatView.vue +582 -0
  41. package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
  42. package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
  43. package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
  44. package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
  45. package/frontend-vue/app/chat/components/MessageList.vue +474 -0
  46. package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
  47. package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
  48. package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
  49. package/frontend-vue/app/chat/components/TodoList.vue +291 -0
  50. package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
  51. package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
  52. package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
  53. package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
  54. package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
  55. package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
  56. package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
  57. package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
  58. package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
  59. package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
  60. package/frontend-vue/app/chat/constants/attachments.ts +16 -0
  61. package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
  62. package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
  63. package/frontend-vue/app/chat/constants/conversation.ts +75 -0
  64. package/frontend-vue/app/chat/constants/permissions.ts +50 -0
  65. package/frontend-vue/app/chat/constants/protocol.ts +48 -0
  66. package/frontend-vue/app/chat/constants/run-status.ts +50 -0
  67. package/frontend-vue/app/chat/constants/settings.ts +60 -0
  68. package/frontend-vue/app/chat/types/conversation.ts +87 -0
  69. package/frontend-vue/app/chat/types/permission.ts +7 -0
  70. package/frontend-vue/app/chat/types/question.ts +17 -0
  71. package/frontend-vue/app/chat/types/settings.ts +27 -0
  72. package/frontend-vue/app/chat/utils/attachments.ts +62 -0
  73. package/frontend-vue/app/chat/utils/format.ts +14 -0
  74. package/frontend-vue/app/chat/utils/ids.ts +34 -0
  75. package/frontend-vue/app/chat/utils/markdown.ts +188 -0
  76. package/frontend-vue/app/chat/utils/run-status.ts +90 -0
  77. package/frontend-vue/app/chat/utils/todos.ts +64 -0
  78. package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
  79. package/frontend-vue/app/components/ActivityView.vue +34 -34
  80. package/frontend-vue/app/components/ChatPanel.vue +276 -0
  81. package/frontend-vue/app/components/SettingsView.vue +127 -130
  82. package/frontend-vue/app/components/SkillsView.vue +55 -53
  83. package/frontend-vue/app/plugins/ai.client.ts +145 -129
  84. package/frontend-vue/app/runtime/assistant-session.ts +21 -0
  85. package/frontend-vue/app/runtime/backoff.ts +11 -14
  86. package/frontend-vue/app/runtime/channel-client.ts +130 -141
  87. package/frontend-vue/app/runtime/chat-transport.ts +25 -55
  88. package/frontend-vue/app/runtime/constants.ts +70 -60
  89. package/frontend-vue/app/runtime/current-page.ts +49 -15
  90. package/frontend-vue/app/runtime/event-stream.ts +35 -35
  91. package/frontend-vue/app/runtime/frame-router.ts +30 -0
  92. package/frontend-vue/app/runtime/header-action.ts +18 -18
  93. package/frontend-vue/app/runtime/host-commands.ts +57 -56
  94. package/frontend-vue/app/runtime/listeners.ts +31 -0
  95. package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
  96. package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
  97. package/frontend-vue/app/runtime/panel-state.ts +88 -0
  98. package/frontend-vue/app/runtime/session-gate.ts +8 -8
  99. package/frontend-vue/app/runtime/shortcuts.ts +12 -12
  100. package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
  101. package/frontend-vue/app/runtime/typed-message.ts +11 -0
  102. package/frontend-vue/dms.frontend.ts +12 -12
  103. package/frontend-vue/eslint.config.mjs +13 -13
  104. package/frontend-vue/package.json +11 -4
  105. package/frontend-vue/pnpm-lock.yaml +298 -96
  106. package/frontend-vue/tests/backoff.test.ts +26 -30
  107. package/frontend-vue/tests/channel-client.test.ts +187 -188
  108. package/frontend-vue/tests/chat/markdown.test.ts +271 -0
  109. package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
  110. package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
  111. package/frontend-vue/tests/chat-panel.test.ts +354 -0
  112. package/frontend-vue/tests/chat-transport.test.ts +84 -57
  113. package/frontend-vue/tests/host-commands.test.ts +126 -87
  114. package/frontend-vue/tests/navigation.test.ts +65 -34
  115. package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
  116. package/frontend-vue/tests/panel-state.test.ts +58 -0
  117. package/frontend-vue/tests/session-gate.test.ts +33 -33
  118. package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
  119. package/frontend-vue/tests/sidecar-status.test.ts +29 -29
  120. package/frontend-vue/tsconfig.chat.json +16 -0
  121. package/frontend-vue/vitest.config.ts +6 -4
  122. package/package.json +5 -5
  123. package/sidecar/dist/agent/effective-mode.d.ts +2 -2
  124. package/sidecar/dist/agent/effective-mode.js +1 -1
  125. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  126. package/sidecar/dist/agent/host-context.js +7 -9
  127. package/sidecar/dist/agent/host-context.js.map +1 -1
  128. package/sidecar/dist/agent/permission-bus.d.ts +1 -1
  129. package/sidecar/dist/agent/permission-bus.js +2 -2
  130. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  131. package/sidecar/dist/agent/provider.d.ts +2 -2
  132. package/sidecar/dist/agent/question-bus.d.ts +1 -1
  133. package/sidecar/dist/agent/question-bus.js +2 -2
  134. package/sidecar/dist/agent/question-bus.js.map +1 -1
  135. package/sidecar/dist/agent/runner.d.ts +2 -2
  136. package/sidecar/dist/agent/runner.js +24 -8
  137. package/sidecar/dist/agent/runner.js.map +1 -1
  138. package/sidecar/dist/agent/session.d.ts +2 -2
  139. package/sidecar/dist/agent/session.js +49 -9
  140. package/sidecar/dist/agent/session.js.map +1 -1
  141. package/sidecar/dist/agent/switching-runner.js +10 -5
  142. package/sidecar/dist/agent/switching-runner.js.map +1 -1
  143. package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
  144. package/sidecar/dist/agent/turn-end-reasons.js +6 -1
  145. package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
  146. package/sidecar/dist/constants/agent.d.ts +4 -1
  147. package/sidecar/dist/constants/agent.js +4 -1
  148. package/sidecar/dist/constants/agent.js.map +1 -1
  149. package/sidecar/dist/constants/cli.d.ts +0 -1
  150. package/sidecar/dist/constants/cli.js +0 -1
  151. package/sidecar/dist/constants/cli.js.map +1 -1
  152. package/sidecar/dist/constants/codex.d.ts +5 -2
  153. package/sidecar/dist/constants/codex.js +5 -2
  154. package/sidecar/dist/constants/codex.js.map +1 -1
  155. package/sidecar/dist/constants/http.d.ts +0 -9
  156. package/sidecar/dist/constants/http.js +0 -19
  157. package/sidecar/dist/constants/http.js.map +1 -1
  158. package/sidecar/dist/constants/mcp.d.ts +1 -1
  159. package/sidecar/dist/constants/mcp.js +1 -1
  160. package/sidecar/dist/constants/mcp.js.map +1 -1
  161. package/sidecar/dist/constants/paths.d.ts +0 -1
  162. package/sidecar/dist/constants/paths.js +0 -1
  163. package/sidecar/dist/constants/paths.js.map +1 -1
  164. package/sidecar/dist/constants/ws.d.ts +3 -4
  165. package/sidecar/dist/constants/ws.js +3 -4
  166. package/sidecar/dist/constants/ws.js.map +1 -1
  167. package/sidecar/dist/index.d.ts +0 -1
  168. package/sidecar/dist/index.js +6 -10
  169. package/sidecar/dist/index.js.map +1 -1
  170. package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
  171. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  172. package/sidecar/dist/mcp/types.d.ts +1 -1
  173. package/sidecar/dist/pages/page-filepath.d.ts +4 -0
  174. package/sidecar/dist/pages/page-filepath.js +28 -0
  175. package/sidecar/dist/pages/page-filepath.js.map +1 -0
  176. package/sidecar/dist/prompts/agent.d.ts +1 -1
  177. package/sidecar/dist/prompts/agent.js +1 -1
  178. package/sidecar/dist/prompts/agent.js.map +1 -1
  179. package/sidecar/dist/protocol/events.js +2 -2
  180. package/sidecar/dist/protocol/events.js.map +1 -1
  181. package/sidecar/dist/protocol/messages.d.ts +4 -15
  182. package/sidecar/dist/protocol/messages.js +4 -11
  183. package/sidecar/dist/protocol/messages.js.map +1 -1
  184. package/sidecar/dist/providers/claude/config.d.ts +2 -2
  185. package/sidecar/dist/providers/claude/config.js +2 -2
  186. package/sidecar/dist/providers/claude/config.js.map +1 -1
  187. package/sidecar/dist/providers/claude/provider.js +1 -1
  188. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  189. package/sidecar/dist/providers/codex/config.d.ts +3 -2
  190. package/sidecar/dist/providers/codex/config.js +3 -3
  191. package/sidecar/dist/providers/codex/config.js.map +1 -1
  192. package/sidecar/dist/providers/codex/process.js +107 -74
  193. package/sidecar/dist/providers/codex/process.js.map +1 -1
  194. package/sidecar/dist/providers/codex/provider.js +68 -31
  195. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  196. package/sidecar/dist/providers/codex/skills.d.ts +1 -0
  197. package/sidecar/dist/providers/codex/skills.js +4 -1
  198. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  199. package/sidecar/dist/providers/codex/stop.d.ts +4 -0
  200. package/sidecar/dist/providers/codex/stop.js +71 -0
  201. package/sidecar/dist/providers/codex/stop.js.map +1 -0
  202. package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
  203. package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
  204. package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
  205. package/sidecar/dist/server/client-auth.js +2 -14
  206. package/sidecar/dist/server/client-auth.js.map +1 -1
  207. package/sidecar/dist/server/host-command-router.d.ts +5 -0
  208. package/sidecar/dist/server/host-command-router.js +4 -0
  209. package/sidecar/dist/server/host-command-router.js.map +1 -0
  210. package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
  211. package/sidecar/dist/server/host-socket-registry.js +16 -13
  212. package/sidecar/dist/server/host-socket-registry.js.map +1 -1
  213. package/sidecar/dist/server/http.d.ts +0 -1
  214. package/sidecar/dist/server/http.js +1 -44
  215. package/sidecar/dist/server/http.js.map +1 -1
  216. package/sidecar/dist/server/routing.d.ts +5 -6
  217. package/sidecar/dist/server/routing.js +18 -28
  218. package/sidecar/dist/server/routing.js.map +1 -1
  219. package/sidecar/dist/server/ws.d.ts +2 -2
  220. package/sidecar/dist/server/ws.js +41 -39
  221. package/sidecar/dist/server/ws.js.map +1 -1
  222. package/sidecar/dist/state/host-state.d.ts +10 -2
  223. package/sidecar/dist/state/host-state.js +13 -3
  224. package/sidecar/dist/state/host-state.js.map +1 -1
  225. package/sidecar/dist/state/settings-store.js +2 -2
  226. package/sidecar/dist/state/settings-store.js.map +1 -1
  227. package/sidecar/dist/state/settings-types.d.ts +3 -3
  228. package/sidecar/dist/state/settings-types.js +1 -1
  229. package/sidecar/dist/state/settings-types.js.map +1 -1
  230. package/sidecar/package.json +3 -5
  231. package/dist/chatbox/passthrough.d.ts +0 -3
  232. package/dist/chatbox/passthrough.js +0 -53
  233. package/dist/chatbox/passthrough.js.map +0 -1
  234. package/dist/constants/chatbox.d.ts +0 -10
  235. package/dist/constants/chatbox.js +0 -35
  236. package/dist/constants/chatbox.js.map +0 -1
  237. package/dist/routes/chatbox.d.ts +0 -7
  238. package/dist/routes/chatbox.js +0 -47
  239. package/dist/routes/chatbox.js.map +0 -1
  240. package/frontend-vue/app/runtime/overlay.ts +0 -400
  241. package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
  242. package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
  243. package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
  244. package/sidecar/chatbox/dist/index.html +0 -13
  245. package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
@@ -0,0 +1,838 @@
1
+ import { type Ref, ref } from "vue";
2
+ import {
3
+ MESSAGE_ROLES,
4
+ NOT_SENT_MESSAGE,
5
+ RETRY_NEEDS_FILES_MESSAGE,
6
+ STOP_NOT_DELIVERED_MESSAGE,
7
+ STORED_ERROR_ROLE,
8
+ STORED_TOOL_RESULT_ROLE,
9
+ STORED_TOOL_USE_ROLE,
10
+ TOOL_CUT_SHORT_RESULT,
11
+ TOOL_STATUS,
12
+ } from "../constants/conversation";
13
+ import {
14
+ CLIENT_MESSAGE_TYPES,
15
+ SERVER_EVENT_TYPES,
16
+ } from "../constants/protocol";
17
+ import type {
18
+ AssistantMessage,
19
+ ConversationMessage,
20
+ ErrorMessage,
21
+ MessageAttachment,
22
+ QueuedMessage,
23
+ RunProgress,
24
+ ToolCallMessage,
25
+ ToolStatus,
26
+ UserMessage,
27
+ } from "../types/conversation";
28
+ import type { PermissionRequestData } from "../types/permission";
29
+ import type { QuestionData, QuestionRequestData } from "../types/question";
30
+ import { type PendingAttachment, splitDataUrl } from "../utils/attachments";
31
+ import { newUuid } from "../utils/ids";
32
+ import { toActivityKind } from "../utils/run-status";
33
+
34
+ export interface UseConversationOptions {
35
+ activeId: Ref<string>;
36
+ /** Returns false when the message could not leave the chat. */
37
+ send: (msg: object) => boolean;
38
+ onMessage: (handler: (msg: unknown) => void) => () => void;
39
+ onPermissionRequest?: (req: PermissionRequestData) => void;
40
+ onQuestionRequest?: (req: QuestionRequestData) => void;
41
+ }
42
+
43
+ export interface UseConversationResult {
44
+ messages: Ref<ConversationMessage[]>;
45
+ isRunning: Ref<boolean>;
46
+ queued: Ref<QueuedMessage[]>;
47
+ /** What the running turn is doing, as last reported; null between turns. */
48
+ progress: Ref<RunProgress | null>;
49
+ /** Whether a turn is expected to report: sent, resumed, or reporting. */
50
+ isTurnInFlight: Ref<boolean>;
51
+ /** Local time of the last event the sidecar sent about this conversation. */
52
+ lastEventAtMs: Ref<number>;
53
+ sendUserMessage: (content: string, attachments?: PendingAttachment[]) => void;
54
+ /** Sends the last user message again, with its files when still at hand. */
55
+ retry: () => void;
56
+ cancelQueued: (id: string) => void;
57
+ interrupt: () => void;
58
+ reset: () => void;
59
+ }
60
+
61
+ interface SnapshotMessage {
62
+ role: string;
63
+ content: string;
64
+ toolName?: string;
65
+ callId?: string;
66
+ status?: string;
67
+ attachments?: MessageAttachment[];
68
+ isRetryable?: boolean;
69
+ timestampMs: number;
70
+ }
71
+
72
+ interface ChunkEvent {
73
+ type: typeof SERVER_EVENT_TYPES.ASSISTANT_MESSAGE_CHUNK;
74
+ conversationId: string;
75
+ text: string;
76
+ }
77
+
78
+ interface ToolStartEvent {
79
+ type: typeof SERVER_EVENT_TYPES.TOOL_CALL_START;
80
+ conversationId: string;
81
+ callId: string;
82
+ toolName: string;
83
+ args: unknown;
84
+ }
85
+
86
+ interface ToolEndEvent {
87
+ type: typeof SERVER_EVENT_TYPES.TOOL_CALL_END;
88
+ conversationId: string;
89
+ callId: string;
90
+ status: "success" | "error";
91
+ result: unknown;
92
+ }
93
+
94
+ interface RunDoneEvent {
95
+ type: typeof SERVER_EVENT_TYPES.RUN_DONE;
96
+ conversationId: string;
97
+ }
98
+
99
+ interface RunResumedEvent {
100
+ type: typeof SERVER_EVENT_TYPES.RUN_RESUMED;
101
+ conversationId: string;
102
+ }
103
+
104
+ interface RunErrorEvent {
105
+ type: typeof SERVER_EVENT_TYPES.RUN_ERROR;
106
+ conversationId: string;
107
+ error: string;
108
+ isRetryable?: boolean;
109
+ }
110
+
111
+ interface RunProgressEvent {
112
+ type: typeof SERVER_EVENT_TYPES.RUN_PROGRESS;
113
+ conversationId: string;
114
+ activity: string;
115
+ detail?: string;
116
+ elapsedMs: number;
117
+ idleMs: number;
118
+ }
119
+
120
+ interface SnapshotEvent {
121
+ type: typeof SERVER_EVENT_TYPES.CONVERSATION_SNAPSHOT;
122
+ conversationId: string;
123
+ messages: SnapshotMessage[];
124
+ }
125
+
126
+ interface PermissionRequestEvent {
127
+ type: typeof SERVER_EVENT_TYPES.PERMISSION_REQUEST;
128
+ conversationId: string;
129
+ requestId: string;
130
+ toolName: string;
131
+ args: unknown;
132
+ summary: string;
133
+ }
134
+
135
+ interface AskQuestionEvent {
136
+ type: typeof SERVER_EVENT_TYPES.ASK_QUESTION;
137
+ conversationId: string;
138
+ requestId: string;
139
+ questions: QuestionData[];
140
+ }
141
+
142
+ interface WireAttachment {
143
+ name: string;
144
+ mimeType: string;
145
+ size: number;
146
+ data: string;
147
+ }
148
+
149
+ interface AttachmentMeta {
150
+ name: string;
151
+ mimeType: string;
152
+ size: number;
153
+ }
154
+
155
+ interface QueuedItemWire {
156
+ id: string;
157
+ content: string;
158
+ attachments?: WireAttachment[];
159
+ }
160
+
161
+ interface QueueStateEvent {
162
+ type: typeof SERVER_EVENT_TYPES.QUEUE_STATE;
163
+ conversationId: string;
164
+ items: QueuedItemWire[];
165
+ }
166
+
167
+ interface UserMessageEchoEvent {
168
+ type: typeof SERVER_EVENT_TYPES.USER_MESSAGE_ECHO;
169
+ conversationId: string;
170
+ content: string;
171
+ attachments?: AttachmentMeta[];
172
+ timestampMs: number;
173
+ }
174
+
175
+ function newId(): string {
176
+ return newUuid();
177
+ }
178
+
179
+ function buildUserMessage(
180
+ content: string,
181
+ attachments?: MessageAttachment[],
182
+ ): UserMessage {
183
+ return {
184
+ id: newId(),
185
+ role: MESSAGE_ROLES.USER,
186
+ content,
187
+ attachments,
188
+ timestampMs: Date.now(),
189
+ };
190
+ }
191
+
192
+ function markUnsent(message: UserMessage, isSent: boolean): UserMessage {
193
+ return isSent ? message : { ...message, isUnsent: true };
194
+ }
195
+
196
+ function buildAssistantMessage(text: string): AssistantMessage {
197
+ return {
198
+ id: newId(),
199
+ role: MESSAGE_ROLES.ASSISTANT,
200
+ content: text,
201
+ timestampMs: Date.now(),
202
+ };
203
+ }
204
+
205
+ function buildToolCallMessage(event: ToolStartEvent): ToolCallMessage {
206
+ return {
207
+ id: newId(),
208
+ role: MESSAGE_ROLES.TOOL,
209
+ callId: event.callId,
210
+ toolName: event.toolName,
211
+ args: event.args,
212
+ status: TOOL_STATUS.PENDING,
213
+ timestampMs: Date.now(),
214
+ };
215
+ }
216
+
217
+ function appendChunk(
218
+ list: ConversationMessage[],
219
+ text: string,
220
+ ): ConversationMessage[] {
221
+ const last = list.at(-1);
222
+ if (last !== undefined && last.role === MESSAGE_ROLES.ASSISTANT) {
223
+ const updated: AssistantMessage = { ...last, content: last.content + text };
224
+ return [...list.slice(0, -1), updated];
225
+ }
226
+ return [...list, buildAssistantMessage(text)];
227
+ }
228
+
229
+ function upsertToolCall(
230
+ list: ConversationMessage[],
231
+ event: ToolStartEvent,
232
+ ): ConversationMessage[] {
233
+ const exists = list.some(
234
+ (msg) => msg.role === MESSAGE_ROLES.TOOL && msg.callId === event.callId,
235
+ );
236
+ if (!exists) return [...list, buildToolCallMessage(event)];
237
+ return list.map((msg) => {
238
+ if (msg.role !== MESSAGE_ROLES.TOOL || msg.callId !== event.callId) {
239
+ return msg;
240
+ }
241
+ return {
242
+ ...msg,
243
+ toolName: event.toolName,
244
+ args: event.args,
245
+ status: TOOL_STATUS.PENDING,
246
+ };
247
+ });
248
+ }
249
+
250
+ function applyToolEnd(
251
+ list: ConversationMessage[],
252
+ event: ToolEndEvent,
253
+ ): ConversationMessage[] {
254
+ return list.map((msg) => {
255
+ if (msg.role !== MESSAGE_ROLES.TOOL) return msg;
256
+ if (msg.callId !== event.callId) return msg;
257
+ return { ...msg, status: event.status, result: event.result };
258
+ });
259
+ }
260
+
261
+ function normalizeToolStatus(raw: string | undefined): ToolStatus {
262
+ if (raw === TOOL_STATUS.SUCCESS) return TOOL_STATUS.SUCCESS;
263
+ if (raw === TOOL_STATUS.ERROR) return TOOL_STATUS.ERROR;
264
+ return TOOL_STATUS.PENDING;
265
+ }
266
+
267
+ function safeParse(raw: string): unknown {
268
+ try {
269
+ return JSON.parse(raw);
270
+ } catch {
271
+ return raw;
272
+ }
273
+ }
274
+
275
+ function buildUserSnapshot(snap: SnapshotMessage): UserMessage {
276
+ return {
277
+ id: newId(),
278
+ role: MESSAGE_ROLES.USER,
279
+ content: snap.content,
280
+ attachments: snap.attachments,
281
+ timestampMs: snap.timestampMs,
282
+ };
283
+ }
284
+
285
+ function buildAssistantSnapshot(snap: SnapshotMessage): AssistantMessage {
286
+ return {
287
+ id: newId(),
288
+ role: MESSAGE_ROLES.ASSISTANT,
289
+ content: snap.content,
290
+ timestampMs: snap.timestampMs,
291
+ };
292
+ }
293
+
294
+ function buildToolUseSnapshot(snap: SnapshotMessage): ToolCallMessage | null {
295
+ if (snap.callId === undefined || snap.toolName === undefined) return null;
296
+ return {
297
+ id: newId(),
298
+ role: MESSAGE_ROLES.TOOL,
299
+ callId: snap.callId,
300
+ toolName: snap.toolName,
301
+ args: safeParse(snap.content),
302
+ status: TOOL_STATUS.PENDING,
303
+ timestampMs: snap.timestampMs,
304
+ };
305
+ }
306
+
307
+ function applyToolResultSnapshot(
308
+ out: ConversationMessage[],
309
+ toolByCallId: Map<string, ToolCallMessage>,
310
+ snap: SnapshotMessage,
311
+ ): void {
312
+ if (snap.callId === undefined) return;
313
+ const result = safeParse(snap.content);
314
+ const status = normalizeToolStatus(snap.status);
315
+ const existing = toolByCallId.get(snap.callId);
316
+ if (existing !== undefined) {
317
+ existing.result = result;
318
+ existing.status = status;
319
+ return;
320
+ }
321
+ out.push({
322
+ id: newId(),
323
+ role: MESSAGE_ROLES.TOOL,
324
+ callId: snap.callId,
325
+ toolName: "",
326
+ args: undefined,
327
+ result,
328
+ status,
329
+ timestampMs: snap.timestampMs,
330
+ });
331
+ }
332
+
333
+ function appendSnapshotItem(
334
+ out: ConversationMessage[],
335
+ toolByCallId: Map<string, ToolCallMessage>,
336
+ item: SnapshotMessage,
337
+ ): void {
338
+ if (item.role === MESSAGE_ROLES.USER) {
339
+ out.push(buildUserSnapshot(item));
340
+ return;
341
+ }
342
+ if (item.role === STORED_ERROR_ROLE) {
343
+ out.push(
344
+ buildErrorMessage(
345
+ item.content,
346
+ item.isRetryable ?? true,
347
+ item.timestampMs,
348
+ ),
349
+ );
350
+ return;
351
+ }
352
+ if (item.role === MESSAGE_ROLES.ASSISTANT) {
353
+ out.push(buildAssistantSnapshot(item));
354
+ return;
355
+ }
356
+ if (item.role === STORED_TOOL_USE_ROLE) {
357
+ const message = buildToolUseSnapshot(item);
358
+ if (message === null) return;
359
+ out.push(message);
360
+ toolByCallId.set(message.callId, message);
361
+ return;
362
+ }
363
+ if (item.role === STORED_TOOL_RESULT_ROLE) {
364
+ applyToolResultSnapshot(out, toolByCallId, item);
365
+ }
366
+ }
367
+
368
+ function snapshotToMessages(snap: SnapshotMessage[]): ConversationMessage[] {
369
+ const out: ConversationMessage[] = [];
370
+ const toolByCallId = new Map<string, ToolCallMessage>();
371
+ for (const item of snap) {
372
+ appendSnapshotItem(out, toolByCallId, item);
373
+ }
374
+ return settlePendingTools(out);
375
+ }
376
+
377
+ function buildErrorMessage(
378
+ error: string,
379
+ isRetryable: boolean,
380
+ timestampMs: number = Date.now(),
381
+ ): ErrorMessage {
382
+ return {
383
+ id: newId(),
384
+ role: MESSAGE_ROLES.ERROR,
385
+ content: error,
386
+ isRetryable,
387
+ timestampMs,
388
+ };
389
+ }
390
+
391
+ function isPendingTool(msg: ConversationMessage): msg is ToolCallMessage {
392
+ return msg.role === MESSAGE_ROLES.TOOL && msg.status === TOOL_STATUS.PENDING;
393
+ }
394
+
395
+ function settlePendingTools(
396
+ list: ConversationMessage[],
397
+ ): ConversationMessage[] {
398
+ if (!list.some(isPendingTool)) return list;
399
+ return list.map((msg) => {
400
+ if (!isPendingTool(msg)) return msg;
401
+ return { ...msg, status: TOOL_STATUS.ERROR, result: TOOL_CUT_SHORT_RESULT };
402
+ });
403
+ }
404
+
405
+ function toRunProgress(event: RunProgressEvent): RunProgress {
406
+ return {
407
+ activity: toActivityKind(event.activity),
408
+ detail: event.detail,
409
+ elapsedMs: event.elapsedMs,
410
+ idleMs: event.idleMs,
411
+ receivedAtMs: Date.now(),
412
+ };
413
+ }
414
+
415
+ function isUserMessage(msg: ConversationMessage): msg is UserMessage {
416
+ return msg.role === MESSAGE_ROLES.USER;
417
+ }
418
+
419
+ function lastUserMessage(list: ConversationMessage[]): UserMessage | undefined {
420
+ for (let index = list.length - 1; index >= 0; index -= 1) {
421
+ const message = list[index];
422
+ if (isUserMessage(message)) return message;
423
+ }
424
+ return undefined;
425
+ }
426
+
427
+ function toResendableAttachments(
428
+ attachments: MessageAttachment[] | undefined,
429
+ ): PendingAttachment[] | null {
430
+ const list = attachments ?? [];
431
+ if (list.some((att) => !att.dataUrl)) return null;
432
+ return list.map((att) => ({
433
+ id: newId(),
434
+ name: att.name,
435
+ mimeType: att.mimeType,
436
+ size: att.size,
437
+ data: splitDataUrl(att.dataUrl ?? ""),
438
+ dataUrl: att.dataUrl ?? "",
439
+ }));
440
+ }
441
+
442
+ function isObject(value: unknown): value is Record<string, unknown> {
443
+ return value !== null && typeof value === "object";
444
+ }
445
+
446
+ function getEventType(msg: unknown): string | null {
447
+ if (!isObject(msg)) return null;
448
+ const t = msg.type;
449
+ return typeof t === "string" ? t : null;
450
+ }
451
+
452
+ function getConversationId(msg: unknown): string | null {
453
+ if (!isObject(msg)) return null;
454
+ const id = msg.conversationId;
455
+ return typeof id === "string" ? id : null;
456
+ }
457
+
458
+ function toPermissionRequestData(
459
+ event: PermissionRequestEvent,
460
+ ): PermissionRequestData {
461
+ return {
462
+ requestId: event.requestId,
463
+ conversationId: event.conversationId,
464
+ toolName: event.toolName,
465
+ args: event.args,
466
+ summary: event.summary,
467
+ };
468
+ }
469
+
470
+ function toQuestionRequestData(event: AskQuestionEvent): QuestionRequestData {
471
+ return {
472
+ requestId: event.requestId,
473
+ conversationId: event.conversationId,
474
+ questions: event.questions,
475
+ };
476
+ }
477
+
478
+ interface ConversationState {
479
+ options: UseConversationOptions;
480
+ messages: Ref<ConversationMessage[]>;
481
+ isRunning: Ref<boolean>;
482
+ queued: Ref<QueuedMessage[]>;
483
+ progress: Ref<RunProgress | null>;
484
+ isTurnInFlight: Ref<boolean>;
485
+ lastEventAtMs: Ref<number>;
486
+ }
487
+
488
+ type ConversationEvent =
489
+ | ChunkEvent
490
+ | ToolStartEvent
491
+ | ToolEndEvent
492
+ | RunDoneEvent
493
+ | RunResumedEvent
494
+ | RunErrorEvent
495
+ | RunProgressEvent
496
+ | SnapshotEvent
497
+ | PermissionRequestEvent
498
+ | AskQuestionEvent
499
+ | QueueStateEvent
500
+ | UserMessageEchoEvent;
501
+
502
+ type ConversationEventType = ConversationEvent["type"];
503
+
504
+ interface EventTypeTag<K extends ConversationEventType> {
505
+ type: K;
506
+ }
507
+
508
+ type EventOfType<K extends ConversationEventType> = Extract<
509
+ ConversationEvent,
510
+ EventTypeTag<K>
511
+ >;
512
+
513
+ type EventHandlers = {
514
+ [K in ConversationEventType]: (
515
+ state: ConversationState,
516
+ event: EventOfType<K>,
517
+ ) => void;
518
+ };
519
+
520
+ function appendMessage(
521
+ state: ConversationState,
522
+ message: ConversationMessage,
523
+ ): void {
524
+ state.messages.value = [...state.messages.value, message];
525
+ }
526
+
527
+ function appendError(
528
+ state: ConversationState,
529
+ error: string,
530
+ isRetryable = true,
531
+ ): void {
532
+ appendMessage(state, buildErrorMessage(error, isRetryable));
533
+ }
534
+
535
+ function expectTurn(state: ConversationState): void {
536
+ state.isTurnInFlight.value = true;
537
+ state.lastEventAtMs.value = Date.now();
538
+ }
539
+
540
+ function endTurn(state: ConversationState): void {
541
+ state.messages.value = settlePendingTools(state.messages.value);
542
+ state.progress.value = null;
543
+ state.isTurnInFlight.value = false;
544
+ }
545
+
546
+ function settleOrKeepRunning(state: ConversationState): void {
547
+ state.isRunning.value = state.queued.value.length > 0;
548
+ }
549
+
550
+ function applySnapshot(state: ConversationState, event: SnapshotEvent): void {
551
+ state.messages.value = snapshotToMessages(event.messages);
552
+ state.isRunning.value = false;
553
+ state.progress.value = null;
554
+ state.isTurnInFlight.value = false;
555
+ }
556
+
557
+ function toPendingAttachment(wire: WireAttachment): PendingAttachment {
558
+ return {
559
+ id: newId(),
560
+ name: wire.name,
561
+ mimeType: wire.mimeType,
562
+ size: wire.size,
563
+ data: wire.data,
564
+ dataUrl: `data:${wire.mimeType};base64,${wire.data}`,
565
+ };
566
+ }
567
+
568
+ function applyQueueState(
569
+ state: ConversationState,
570
+ event: QueueStateEvent,
571
+ ): void {
572
+ state.queued.value = event.items.map((item) => ({
573
+ id: item.id,
574
+ content: item.content,
575
+ attachments: (item.attachments ?? []).map(toPendingAttachment),
576
+ }));
577
+ }
578
+
579
+ function withAttachments(
580
+ attachments: MessageAttachment[],
581
+ ): MessageAttachment[] | undefined {
582
+ return attachments.length > 0 ? attachments : undefined;
583
+ }
584
+
585
+ function applyUserEcho(
586
+ state: ConversationState,
587
+ event: UserMessageEchoEvent,
588
+ ): void {
589
+ const attachments = (event.attachments ?? []).map((a) => ({
590
+ ...a,
591
+ dataUrl: "",
592
+ }));
593
+ appendMessage(
594
+ state,
595
+ buildUserMessage(event.content, withAttachments(attachments)),
596
+ );
597
+ }
598
+
599
+ function forwardPermissionRequest(
600
+ state: ConversationState,
601
+ event: PermissionRequestEvent,
602
+ ): void {
603
+ state.options.onPermissionRequest?.(toPermissionRequestData(event));
604
+ }
605
+
606
+ function forwardQuestion(
607
+ state: ConversationState,
608
+ event: AskQuestionEvent,
609
+ ): void {
610
+ state.options.onQuestionRequest?.(toQuestionRequestData(event));
611
+ }
612
+
613
+ function applyProgress(
614
+ state: ConversationState,
615
+ event: RunProgressEvent,
616
+ ): void {
617
+ state.progress.value = toRunProgress(event);
618
+ state.isRunning.value = true;
619
+ state.isTurnInFlight.value = true;
620
+ }
621
+
622
+ const EVENT_HANDLERS: EventHandlers = {
623
+ [SERVER_EVENT_TYPES.ASSISTANT_MESSAGE_CHUNK]: (state, event) => {
624
+ state.messages.value = appendChunk(state.messages.value, event.text);
625
+ },
626
+ [SERVER_EVENT_TYPES.TOOL_CALL_START]: (state, event) => {
627
+ state.messages.value = upsertToolCall(state.messages.value, event);
628
+ },
629
+ [SERVER_EVENT_TYPES.TOOL_CALL_END]: (state, event) => {
630
+ state.messages.value = applyToolEnd(state.messages.value, event);
631
+ },
632
+ [SERVER_EVENT_TYPES.RUN_DONE]: (state) => {
633
+ endTurn(state);
634
+ settleOrKeepRunning(state);
635
+ },
636
+ [SERVER_EVENT_TYPES.RUN_RESUMED]: (state) => {
637
+ state.isRunning.value = true;
638
+ expectTurn(state);
639
+ },
640
+ [SERVER_EVENT_TYPES.RUN_ERROR]: (state, event) => {
641
+ endTurn(state);
642
+ appendError(state, event.error, event.isRetryable ?? true);
643
+ settleOrKeepRunning(state);
644
+ },
645
+ [SERVER_EVENT_TYPES.RUN_PROGRESS]: applyProgress,
646
+ [SERVER_EVENT_TYPES.CONVERSATION_SNAPSHOT]: applySnapshot,
647
+ [SERVER_EVENT_TYPES.PERMISSION_REQUEST]: forwardPermissionRequest,
648
+ [SERVER_EVENT_TYPES.ASK_QUESTION]: forwardQuestion,
649
+ [SERVER_EVENT_TYPES.QUEUE_STATE]: applyQueueState,
650
+ [SERVER_EVENT_TYPES.USER_MESSAGE_ECHO]: applyUserEcho,
651
+ };
652
+
653
+ function isForActiveConversation(
654
+ state: ConversationState,
655
+ msg: unknown,
656
+ ): boolean {
657
+ const eventId = getConversationId(msg);
658
+ return eventId === null || eventId === state.options.activeId.value;
659
+ }
660
+
661
+ function isHandledEventType(
662
+ type: string | null,
663
+ ): type is ConversationEventType {
664
+ return type !== null && Object.hasOwn(EVENT_HANDLERS, type);
665
+ }
666
+
667
+ function runHandler<K extends ConversationEventType>(
668
+ state: ConversationState,
669
+ type: K,
670
+ event: EventOfType<K>,
671
+ ): void {
672
+ const handler: EventHandlers[K] = EVENT_HANDLERS[type];
673
+ handler(state, event);
674
+ }
675
+
676
+ function dispatch(state: ConversationState, msg: unknown): void {
677
+ const type = getEventType(msg);
678
+ if (!isHandledEventType(type)) return;
679
+ if (!isForActiveConversation(state, msg)) return;
680
+ state.lastEventAtMs.value = Date.now();
681
+ runHandler(state, type, msg as EventOfType<typeof type>);
682
+ }
683
+
684
+ function toWireAttachments(attachments: PendingAttachment[]): WireAttachment[] {
685
+ return attachments.map((a) => ({
686
+ name: a.name,
687
+ mimeType: a.mimeType,
688
+ size: a.size,
689
+ data: a.data,
690
+ }));
691
+ }
692
+
693
+ function toLocalAttachments(
694
+ attachments: PendingAttachment[],
695
+ ): MessageAttachment[] {
696
+ return attachments.map((a) => ({
697
+ name: a.name,
698
+ mimeType: a.mimeType,
699
+ size: a.size,
700
+ dataUrl: a.dataUrl,
701
+ }));
702
+ }
703
+
704
+ function queueFollowUp(
705
+ state: ConversationState,
706
+ content: string,
707
+ attachments: PendingAttachment[],
708
+ ): void {
709
+ const wire = toWireAttachments(attachments);
710
+ const isQueued = state.options.send({
711
+ type: CLIENT_MESSAGE_TYPES.QUEUE_ENQUEUE,
712
+ conversationId: state.options.activeId.value,
713
+ item: {
714
+ id: newId(),
715
+ content,
716
+ ...(wire.length > 0 ? { attachments: wire } : {}),
717
+ },
718
+ });
719
+ if (!isQueued) appendError(state, NOT_SENT_MESSAGE);
720
+ }
721
+
722
+ function startTurn(
723
+ state: ConversationState,
724
+ content: string,
725
+ attachments: PendingAttachment[],
726
+ ): void {
727
+ const wire = toWireAttachments(attachments);
728
+ const isSent = state.options.send({
729
+ type: CLIENT_MESSAGE_TYPES.USER_MESSAGE,
730
+ conversationId: state.options.activeId.value,
731
+ content,
732
+ ...(wire.length > 0 ? { attachments: wire } : {}),
733
+ });
734
+ const message = buildUserMessage(
735
+ content,
736
+ withAttachments(toLocalAttachments(attachments)),
737
+ );
738
+ appendMessage(state, markUnsent(message, isSent));
739
+ if (!isSent) {
740
+ appendError(state, NOT_SENT_MESSAGE);
741
+ return;
742
+ }
743
+ state.isRunning.value = true;
744
+ expectTurn(state);
745
+ }
746
+
747
+ function sendUserMessage(
748
+ state: ConversationState,
749
+ content: string,
750
+ attachments: PendingAttachment[],
751
+ ): void {
752
+ const trimmed = content.trim();
753
+ if (trimmed.length === 0 && attachments.length === 0) return;
754
+ if (state.isRunning.value) {
755
+ queueFollowUp(state, trimmed, attachments);
756
+ return;
757
+ }
758
+ startTurn(state, trimmed, attachments);
759
+ }
760
+
761
+ function retry(state: ConversationState): void {
762
+ const last = lastUserMessage(state.messages.value);
763
+ if (last === undefined) return;
764
+ const attachments = toResendableAttachments(last.attachments);
765
+ if (attachments === null) {
766
+ appendError(state, RETRY_NEEDS_FILES_MESSAGE, false);
767
+ return;
768
+ }
769
+ if (last.isUnsent === true) {
770
+ state.messages.value = state.messages.value.filter(
771
+ (msg) => msg.id !== last.id,
772
+ );
773
+ }
774
+ sendUserMessage(state, last.content, attachments);
775
+ }
776
+
777
+ function cancelQueued(state: ConversationState, id: string): void {
778
+ state.options.send({
779
+ type: CLIENT_MESSAGE_TYPES.QUEUE_CANCEL,
780
+ conversationId: state.options.activeId.value,
781
+ id,
782
+ });
783
+ }
784
+
785
+ function interrupt(state: ConversationState): void {
786
+ if (!state.isRunning.value) return;
787
+ const isSent = state.options.send({
788
+ type: CLIENT_MESSAGE_TYPES.INTERRUPT_TURN,
789
+ conversationId: state.options.activeId.value,
790
+ });
791
+ if (isSent) return;
792
+ endTurn(state);
793
+ state.isRunning.value = false;
794
+ appendError(state, STOP_NOT_DELIVERED_MESSAGE);
795
+ }
796
+
797
+ function reset(state: ConversationState): void {
798
+ state.messages.value = [];
799
+ state.isRunning.value = false;
800
+ state.queued.value = [];
801
+ state.progress.value = null;
802
+ state.isTurnInFlight.value = false;
803
+ }
804
+
805
+ function createConversationState(
806
+ options: UseConversationOptions,
807
+ ): ConversationState {
808
+ return {
809
+ options,
810
+ messages: ref<ConversationMessage[]>([]),
811
+ isRunning: ref(false),
812
+ queued: ref<QueuedMessage[]>([]),
813
+ progress: ref<RunProgress | null>(null),
814
+ isTurnInFlight: ref(false),
815
+ lastEventAtMs: ref(Date.now()),
816
+ };
817
+ }
818
+
819
+ export function useConversation(
820
+ options: UseConversationOptions,
821
+ ): UseConversationResult {
822
+ const state = createConversationState(options);
823
+ options.onMessage((msg) => dispatch(state, msg));
824
+ return {
825
+ messages: state.messages,
826
+ isRunning: state.isRunning,
827
+ queued: state.queued,
828
+ progress: state.progress,
829
+ isTurnInFlight: state.isTurnInFlight,
830
+ lastEventAtMs: state.lastEventAtMs,
831
+ sendUserMessage: (content, attachments = []) =>
832
+ sendUserMessage(state, content, attachments),
833
+ retry: () => retry(state),
834
+ cancelQueued: (id) => cancelQueued(state, id),
835
+ interrupt: () => interrupt(state),
836
+ reset: () => reset(state),
837
+ };
838
+ }