@assistkick/create 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  179. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  180. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  181. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  182. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  183. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  185. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  186. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  187. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  188. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  189. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  190. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  191. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  192. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  193. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  194. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  195. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  196. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  197. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  198. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  199. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  200. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  201. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  202. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  203. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  204. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  205. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  206. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,826 @@
1
+ /**
2
+ * useChatStream — React hook for real-time Chat v2 message streaming.
3
+ *
4
+ * Manages a WebSocket connection to /api/chat, sends user messages,
5
+ * and processes stream-json events as they arrive from the backend.
6
+ * Text tokens are accumulated in real time for a streaming typing effect.
7
+ *
8
+ * Also handles permission requests from the backend: when Claude CLI needs
9
+ * permission to use a tool, the backend sends a permission_request message.
10
+ * The hook exposes the pending request and a method to respond.
11
+ *
12
+ * Supports message queuing: when streaming is active, consumers can register
13
+ * an onStreamEnd callback to auto-drain queued messages. The sendMergedMessages
14
+ * method adds each message as a separate user bubble but sends the merged
15
+ * text as a single CLI invocation.
16
+ *
17
+ * Usage:
18
+ * const { connect, disconnect, sendMessage, sendMergedMessages, stopStream,
19
+ * setOnStreamEnd, messages, streaming, connected, error,
20
+ * cancelledUserMessage, clearCancelledUserMessage,
21
+ * clearMessages, pendingPermission, respondToPermission } = useChatStream();
22
+ */
23
+
24
+ import {useRef, useState, useCallback} from 'react';
25
+ import type {PermissionDecision} from '../components/PermissionDialog';
26
+ import {apiClient} from '../api/client';
27
+ import {extractContextUsage} from '../lib/context_usage_helpers';
28
+ import {markOrCreateStreamingAssistant} from '../lib/chat_message_helpers';
29
+ import {useChatSessionStore} from '../stores/useChatSessionStore';
30
+
31
+ // --- Server → Client message types ---
32
+
33
+ interface StreamStartMessage {
34
+ type: 'stream_start';
35
+ claudeSessionId: string;
36
+ }
37
+
38
+ interface StreamEventMessage {
39
+ type: 'stream_event';
40
+ event: Record<string, unknown>;
41
+ }
42
+
43
+ interface StreamEndMessage {
44
+ type: 'stream_end';
45
+ exitCode: number | null;
46
+ claudeSessionId: string;
47
+ error?: string;
48
+ }
49
+
50
+ interface StreamCancelledMessage {
51
+ type: 'stream_cancelled';
52
+ claudeSessionId: string;
53
+ }
54
+
55
+ interface StreamResumedMessage {
56
+ type: 'stream_resumed';
57
+ claudeSessionId: string;
58
+ }
59
+
60
+ interface StreamErrorMessage {
61
+ type: 'stream_error';
62
+ error: string;
63
+ }
64
+
65
+ interface PermissionRequestMessage {
66
+ type: 'permission_request';
67
+ requestId: string;
68
+ toolName: string;
69
+ input: Record<string, unknown>;
70
+ }
71
+
72
+ interface SystemPromptMessage {
73
+ type: 'system_prompt';
74
+ prompt: string;
75
+ }
76
+
77
+ interface SessionRenamedMessage {
78
+ type: 'session_renamed';
79
+ sessionId: string;
80
+ name: string;
81
+ }
82
+
83
+ interface ErrorMessage {
84
+ type: 'error';
85
+ message: string;
86
+ }
87
+
88
+ type ChatServerMessage =
89
+ | StreamStartMessage
90
+ | StreamEventMessage
91
+ | StreamEndMessage
92
+ | StreamCancelledMessage
93
+ | StreamResumedMessage
94
+ | StreamErrorMessage
95
+ | PermissionRequestMessage
96
+ | SystemPromptMessage
97
+ | SessionRenamedMessage
98
+ | ErrorMessage;
99
+
100
+ // --- Client → Server message types ---
101
+
102
+ interface SendMessagePayload {
103
+ type: 'send_message';
104
+ message: string;
105
+ projectId: string;
106
+ /** The chat session ID (csess_xxx) for message persistence. */
107
+ sessionId?: string;
108
+ claudeSessionId: string;
109
+ isNewSession: boolean;
110
+ permissionMode?: 'skip' | 'allowed_tools';
111
+ allowedTools?: string[];
112
+ /** Relative workspace paths of attached files to include in the prompt */
113
+ attachments?: string[];
114
+ /** Attachment content blocks for DB persistence alongside the user message */
115
+ attachmentBlocks?: Array<{ type: 'image' | 'file'; path: string; name: string; mimeType: string }>;
116
+ }
117
+
118
+ interface PermissionResponsePayload {
119
+ type: 'permission_response';
120
+ requestId: string;
121
+ projectId: string;
122
+ decision: PermissionDecision;
123
+ }
124
+
125
+ interface StopStreamPayload {
126
+ type: 'stop_stream';
127
+ claudeSessionId?: string;
128
+ }
129
+
130
+ // --- Chat message model ---
131
+
132
+ export interface TextBlock {
133
+ type: 'text';
134
+ text: string;
135
+ }
136
+
137
+ export interface ToolUseBlock {
138
+ type: 'tool_use';
139
+ id: string;
140
+ name: string;
141
+ input: Record<string, unknown>;
142
+ }
143
+
144
+ export interface ToolResultBlock {
145
+ type: 'tool_result';
146
+ toolUseId: string;
147
+ content: string;
148
+ isError: boolean;
149
+ }
150
+
151
+ export interface ImageBlock {
152
+ type: 'image';
153
+ path: string;
154
+ name: string;
155
+ mimeType: string;
156
+ }
157
+
158
+ export interface FileBlock {
159
+ type: 'file';
160
+ path: string;
161
+ name: string;
162
+ mimeType: string;
163
+ }
164
+
165
+ export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | FileBlock;
166
+
167
+ export interface ChatMessage {
168
+ id: string;
169
+ role: 'user' | 'assistant';
170
+ content: ContentBlock[];
171
+ isStreaming: boolean;
172
+ }
173
+
174
+ export interface PendingPermission {
175
+ requestId: string;
176
+ toolName: string;
177
+ input: Record<string, unknown>;
178
+ }
179
+
180
+ export interface ContextUsage {
181
+ inputTokens: number;
182
+ outputTokens: number;
183
+ cacheCreationTokens: number;
184
+ cacheReadTokens: number;
185
+ totalTokens: number;
186
+ /** Context window limit from the model (null until result event provides it). */
187
+ contextWindow: number | null;
188
+ }
189
+
190
+ export interface UseChatStreamOptions {
191
+ projectId: string;
192
+ /** The chat session ID (csess_xxx) for message persistence. */
193
+ sessionId?: string;
194
+ claudeSessionId: string;
195
+ isNewSession: boolean;
196
+ permissionMode?: 'skip' | 'allowed_tools';
197
+ allowedTools?: string[];
198
+ /** Relative workspace paths of attached files to include in the prompt */
199
+ attachments?: string[];
200
+ /** Image/file content blocks for persisting attachment metadata in the user message */
201
+ attachmentBlocks?: (ImageBlock | FileBlock)[];
202
+ }
203
+
204
+ export interface UseChatStreamReturn {
205
+ connect: () => void;
206
+ disconnect: () => void;
207
+ sendMessage: (message: string, options: UseChatStreamOptions) => void;
208
+ sendMergedMessages: (texts: string[], options: UseChatStreamOptions) => void;
209
+ stopStream: (claudeSessionId?: string) => void;
210
+ setOnStreamEnd: (cb: (() => void) | null) => void;
211
+ /** Load persisted messages from the database for a session. Returns the number of messages loaded. */
212
+ loadMessages: (sessionId: string) => Promise<number>;
213
+ /** Attempt to re-attach to an in-flight stream after page reload. */
214
+ attachSession: (claudeSessionId: string) => void;
215
+ messages: ChatMessage[];
216
+ streaming: boolean;
217
+ connected: boolean;
218
+ error: string | null;
219
+ /** Text from the user message that was removed after a stop/cancel, for prepopulating the input field */
220
+ cancelledUserMessage: string | null;
221
+ clearCancelledUserMessage: () => void;
222
+ clearMessages: () => void;
223
+ /** Restore a previously cached set of messages (e.g. when switching back to a session). */
224
+ restoreMessages: (msgs: ChatMessage[]) => void;
225
+ pendingPermission: PendingPermission | null;
226
+ respondToPermission: (requestId: string, projectId: string, decision: PermissionDecision) => void;
227
+ /** Current context window usage (updated from stream events) */
228
+ contextUsage: ContextUsage | null;
229
+ /** Initialize context usage from persisted session data */
230
+ initContextUsage: (usage: ContextUsage | null) => void;
231
+ /** The composed system prompt for the current session */
232
+ systemPrompt: string | null;
233
+ /** Initialize system prompt from persisted session data */
234
+ initSystemPrompt: (prompt: string | null) => void;
235
+ }
236
+
237
+ let messageIdCounter = 0;
238
+ const nextMessageId = (): string => `msg_${++messageIdCounter}_${Date.now()}`;
239
+
240
+ /**
241
+ * Build the WebSocket URL for the chat endpoint.
242
+ * Uses wss:// for HTTPS, ws:// for HTTP, relative to the current host.
243
+ */
244
+ const buildWsUrl = (): string => {
245
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
246
+ return `${protocol}//${window.location.host}/api/chat`;
247
+ };
248
+
249
+ /**
250
+ * Extract content blocks from a stream-json assistant event.
251
+ * Returns text and tool_use blocks found in the message content array.
252
+ */
253
+ const extractContentBlocks = (event: Record<string, unknown>): ContentBlock[] => {
254
+ const blocks: ContentBlock[] = [];
255
+ const message = event.message as Record<string, unknown> | undefined;
256
+ if (!message) return blocks;
257
+
258
+ const content = message.content as Array<Record<string, unknown>> | undefined;
259
+ if (!Array.isArray(content)) return blocks;
260
+
261
+ for (const block of content) {
262
+ if (block.type === 'text' && typeof block.text === 'string') {
263
+ blocks.push({type: 'text', text: block.text});
264
+ } else if (block.type === 'tool_use' && typeof block.name === 'string') {
265
+ blocks.push({
266
+ type: 'tool_use',
267
+ id: (block.id as string) || '',
268
+ name: block.name,
269
+ input: (block.input as Record<string, unknown>) || {},
270
+ });
271
+ } else if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
272
+ // tool_result blocks carry the output of a tool execution.
273
+ // content can be a string or an array of {type:'text', text:...} blocks.
274
+ let resultText = '';
275
+ if (typeof block.content === 'string') {
276
+ resultText = block.content;
277
+ } else if (Array.isArray(block.content)) {
278
+ resultText = (block.content as Array<Record<string, unknown>>)
279
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
280
+ .map(c => c.text as string)
281
+ .join('\n');
282
+ }
283
+ blocks.push({
284
+ type: 'tool_result',
285
+ toolUseId: block.tool_use_id as string,
286
+ content: resultText,
287
+ isError: block.is_error === true,
288
+ });
289
+ }
290
+ }
291
+
292
+ return blocks;
293
+ };
294
+
295
+ /**
296
+ * Normalize a raw content block from the database (snake_case keys from Claude API)
297
+ * into the frontend's camelCase ContentBlock format.
298
+ */
299
+ const normalizeContentBlock = (block: Record<string, unknown>): ContentBlock | null => {
300
+ if (block.type === 'text' && typeof block.text === 'string') {
301
+ return { type: 'text', text: block.text };
302
+ }
303
+ if (block.type === 'tool_use' && typeof block.name === 'string') {
304
+ return {
305
+ type: 'tool_use',
306
+ id: (block.id as string) || '',
307
+ name: block.name as string,
308
+ input: (block.input as Record<string, unknown>) || {},
309
+ };
310
+ }
311
+ if (block.type === 'tool_result') {
312
+ const toolUseId = (block.toolUseId ?? block.tool_use_id) as string | undefined;
313
+ if (!toolUseId) return null;
314
+ let resultText = '';
315
+ if (typeof block.content === 'string') {
316
+ resultText = block.content;
317
+ } else if (Array.isArray(block.content)) {
318
+ resultText = (block.content as Array<Record<string, unknown>>)
319
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
320
+ .map(c => c.text as string)
321
+ .join('\n');
322
+ }
323
+ return {
324
+ type: 'tool_result',
325
+ toolUseId,
326
+ content: resultText,
327
+ isError: block.isError === true || block.is_error === true,
328
+ };
329
+ }
330
+ if (block.type === 'image' && typeof block.path === 'string') {
331
+ return {
332
+ type: 'image',
333
+ path: block.path as string,
334
+ name: (block.name as string) || '',
335
+ mimeType: (block.mimeType as string) || 'image/png',
336
+ };
337
+ }
338
+ if (block.type === 'file' && typeof block.path === 'string') {
339
+ return {
340
+ type: 'file',
341
+ path: block.path as string,
342
+ name: (block.name as string) || '',
343
+ mimeType: (block.mimeType as string) || 'application/octet-stream',
344
+ };
345
+ }
346
+ return null;
347
+ };
348
+
349
+ export const useChatStream = (): UseChatStreamReturn => {
350
+ const wsRef = useRef<WebSocket | null>(null);
351
+ const streamEndCallbackRef = useRef<(() => void) | null>(null);
352
+ const [connected, setConnected] = useState(false);
353
+ const [streaming, setStreaming] = useState(false);
354
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
355
+ const [error, setError] = useState<string | null>(null);
356
+ const [pendingPermission, setPendingPermission] = useState<PendingPermission | null>(null);
357
+ const [cancelledUserMessage, setCancelledUserMessage] = useState<string | null>(null);
358
+ const [contextUsage, setContextUsage] = useState<ContextUsage | null>(null);
359
+ const [systemPrompt, setSystemPrompt] = useState<string | null>(null);
360
+
361
+ const connect = useCallback(() => {
362
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return;
363
+
364
+ const url = buildWsUrl();
365
+ const ws = new WebSocket(url);
366
+ wsRef.current = ws;
367
+
368
+ ws.onopen = () => {
369
+ setConnected(true);
370
+ setError(null);
371
+ };
372
+
373
+ ws.onclose = () => {
374
+ setConnected(false);
375
+ setStreaming(false);
376
+ wsRef.current = null;
377
+ };
378
+
379
+ ws.onerror = () => {
380
+ setError('WebSocket connection error');
381
+ setConnected(false);
382
+ setStreaming(false);
383
+ };
384
+
385
+ ws.onmessage = (event: MessageEvent) => {
386
+ let msg: ChatServerMessage;
387
+ try {
388
+ msg = JSON.parse(event.data);
389
+ } catch {
390
+ return;
391
+ }
392
+
393
+ switch (msg.type) {
394
+ case 'stream_start':
395
+ setStreaming(true);
396
+ setError(null);
397
+ // Create an empty assistant message that will accumulate content
398
+ setMessages(prev => [
399
+ ...prev,
400
+ {id: nextMessageId(), role: 'assistant', content: [], isStreaming: true},
401
+ ]);
402
+ break;
403
+
404
+ case 'stream_event':
405
+ handleStreamEvent(msg.event);
406
+ break;
407
+
408
+ case 'stream_end':
409
+ setStreaming(false);
410
+ // Surface error details from non-zero exit codes
411
+ if (msg.error) {
412
+ setError(msg.error);
413
+ }
414
+ // Mark the last assistant message as no longer streaming
415
+ setMessages(prev => {
416
+ const updated = [...prev];
417
+ for (let i = updated.length - 1; i >= 0; i--) {
418
+ if (updated[i].role === 'assistant' && updated[i].isStreaming) {
419
+ updated[i] = {...updated[i], isStreaming: false};
420
+ break;
421
+ }
422
+ }
423
+ return updated;
424
+ });
425
+ // Notify consumers (e.g. message queue) that streaming has ended
426
+ streamEndCallbackRef.current?.();
427
+ break;
428
+
429
+ case 'stream_cancelled':
430
+ setStreaming(false);
431
+ // Discard the partial assistant response and the triggering user message.
432
+ // Prepopulate the cancelled user message text for the input field.
433
+ setMessages(prev => {
434
+ const updated = [...prev];
435
+
436
+ // Remove the last streaming assistant message (partial response)
437
+ for (let i = updated.length - 1; i >= 0; i--) {
438
+ if (updated[i].role === 'assistant' && updated[i].isStreaming) {
439
+ updated.splice(i, 1);
440
+ break;
441
+ }
442
+ }
443
+
444
+ // Remove the last user message and capture its text
445
+ for (let i = updated.length - 1; i >= 0; i--) {
446
+ if (updated[i].role === 'user') {
447
+ const userContent = updated[i].content;
448
+ const textBlock = userContent.find(b => b.type === 'text') as TextBlock | undefined;
449
+ if (textBlock) {
450
+ setCancelledUserMessage(textBlock.text);
451
+ }
452
+ updated.splice(i, 1);
453
+ break;
454
+ }
455
+ }
456
+
457
+ return updated;
458
+ });
459
+ break;
460
+
461
+ case 'stream_resumed':
462
+ // Re-attached to an in-flight stream after page reload or session switch.
463
+ // Mark the last assistant message as streaming so new events update it.
464
+ // If no assistant message exists (e.g. messages not yet persisted to DB),
465
+ // create an empty streaming assistant message for catch-up content.
466
+ setStreaming(true);
467
+ setError(null);
468
+ setMessages(prev => markOrCreateStreamingAssistant(prev, nextMessageId()));
469
+ break;
470
+
471
+ case 'stream_error':
472
+ setStreaming(false);
473
+ setError(msg.error);
474
+ // Mark streaming message as done
475
+ setMessages(prev => {
476
+ const updated = [...prev];
477
+ for (let i = updated.length - 1; i >= 0; i--) {
478
+ if (updated[i].role === 'assistant' && updated[i].isStreaming) {
479
+ updated[i] = {...updated[i], isStreaming: false};
480
+ break;
481
+ }
482
+ }
483
+ return updated;
484
+ });
485
+ break;
486
+
487
+ case 'permission_request':
488
+ setPendingPermission({
489
+ requestId: msg.requestId,
490
+ toolName: msg.toolName,
491
+ input: msg.input,
492
+ });
493
+ break;
494
+
495
+ case 'system_prompt':
496
+ setSystemPrompt(msg.prompt);
497
+ break;
498
+
499
+ case 'session_renamed':
500
+ // Update the session name in the sidebar store in real time
501
+ useChatSessionStore.getState().updateSessionName(msg.sessionId, msg.name);
502
+ break;
503
+
504
+ case 'error':
505
+ setError(msg.message);
506
+ break;
507
+ }
508
+ };
509
+ }, []);
510
+
511
+ const handleStreamEvent = (event: Record<string, unknown>) => {
512
+ // Extract per-call context usage from assistant events only.
513
+ // The result event's usage is CUMULATIVE across all turns — not the context window size.
514
+ if (event.type === 'assistant') {
515
+ const message = event.message as Record<string, unknown> | undefined;
516
+ const usage = message?.usage as Record<string, unknown> | undefined;
517
+ if (usage) {
518
+ setContextUsage(prev => ({
519
+ ...extractContextUsage(usage),
520
+ contextWindow: prev?.contextWindow ?? null,
521
+ }));
522
+ }
523
+
524
+ const blocks = extractContentBlocks(event);
525
+ if (blocks.length === 0) return;
526
+
527
+ setMessages(prev => {
528
+ const updated = [...prev];
529
+ // Find the last streaming assistant message
530
+ for (let i = updated.length - 1; i >= 0; i--) {
531
+ if (updated[i].role === 'assistant' && updated[i].isStreaming) {
532
+ // Each assistant event is cumulative within its turn, but a new
533
+ // turn (after tool use) starts fresh. Detect turn boundaries by
534
+ // comparing tool_use IDs: if existing content has tool_use blocks
535
+ // not present in the new blocks, we've moved to a new turn.
536
+ const existing = updated[i].content;
537
+
538
+ const existingToolUseIds = new Set(
539
+ existing
540
+ .filter((b): b is ToolUseBlock => b.type === 'tool_use')
541
+ .map(b => b.id),
542
+ );
543
+ const newToolUseIds = new Set(
544
+ blocks
545
+ .filter((b): b is ToolUseBlock => b.type === 'tool_use')
546
+ .map(b => b.id),
547
+ );
548
+
549
+ const isNewTurn = existingToolUseIds.size > 0 &&
550
+ ![...existingToolUseIds].some(id => newToolUseIds.has(id));
551
+
552
+ if (isNewTurn) {
553
+ // New turn: preserve all existing content, append new blocks
554
+ updated[i] = {...updated[i], content: [...existing, ...blocks]};
555
+ } else {
556
+ // Same turn: replace with cumulative snapshot, preserving tool_result blocks
557
+ const existingToolResults = existing.filter(
558
+ (b): b is ToolResultBlock => b.type === 'tool_result',
559
+ );
560
+ updated[i] = {...updated[i], content: [...blocks, ...existingToolResults]};
561
+ }
562
+ break;
563
+ }
564
+ }
565
+ return updated;
566
+ });
567
+ return;
568
+ }
569
+
570
+ // Process tool result events — merge tool_result blocks into the last
571
+ // streaming assistant message so ToolUseCard can show output.
572
+ if (event.type === 'tool_result' || event.type === 'human' || event.type === 'user') {
573
+ const blocks = extractContentBlocks(event);
574
+ const resultBlocks = blocks.filter(
575
+ (b): b is ToolResultBlock => b.type === 'tool_result',
576
+ );
577
+ if (resultBlocks.length === 0) return;
578
+
579
+ setMessages(prev => {
580
+ const updated = [...prev];
581
+ for (let i = updated.length - 1; i >= 0; i--) {
582
+ if (updated[i].role === 'assistant' && updated[i].isStreaming) {
583
+ // Append tool_result blocks, deduplicating by toolUseId
584
+ const existingToolResultIds = new Set(
585
+ updated[i].content
586
+ .filter((b): b is ToolResultBlock => b.type === 'tool_result')
587
+ .map(b => b.toolUseId),
588
+ );
589
+ const newResults = resultBlocks.filter(
590
+ b => !existingToolResultIds.has(b.toolUseId),
591
+ );
592
+ if (newResults.length > 0) {
593
+ updated[i] = {
594
+ ...updated[i],
595
+ content: [...updated[i].content, ...newResults],
596
+ };
597
+ }
598
+ break;
599
+ }
600
+ }
601
+ return updated;
602
+ });
603
+ }
604
+
605
+ // Extract contextWindow from result event's modelUsage
606
+ if (event.type === 'result') {
607
+ const modelUsage = event.modelUsage as Record<string, Record<string, unknown>> | undefined;
608
+ if (modelUsage) {
609
+ for (const info of Object.values(modelUsage)) {
610
+ if (typeof info.contextWindow === 'number') {
611
+ setContextUsage(prev => prev ? { ...prev, contextWindow: info.contextWindow as number } : prev);
612
+ break;
613
+ }
614
+ }
615
+ }
616
+ }
617
+ };
618
+
619
+ const disconnect = useCallback(() => {
620
+ if (wsRef.current) {
621
+ wsRef.current.close();
622
+ wsRef.current = null;
623
+ }
624
+ setConnected(false);
625
+ setStreaming(false);
626
+ }, []);
627
+
628
+ const sendMessage = useCallback((message: string, options: UseChatStreamOptions) => {
629
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
630
+ setError('Not connected');
631
+ return;
632
+ }
633
+
634
+ // Build user message content blocks: text + any attachment blocks
635
+ const userContent: ContentBlock[] = [{type: 'text', text: message}];
636
+ if (options.attachmentBlocks && options.attachmentBlocks.length > 0) {
637
+ userContent.push(...options.attachmentBlocks);
638
+ }
639
+
640
+ // Add user message to the list
641
+ setMessages(prev => [
642
+ ...prev,
643
+ {id: nextMessageId(), role: 'user', content: userContent, isStreaming: false},
644
+ ]);
645
+
646
+ const payload: SendMessagePayload = {
647
+ type: 'send_message',
648
+ message,
649
+ projectId: options.projectId,
650
+ sessionId: options.sessionId,
651
+ claudeSessionId: options.claudeSessionId,
652
+ isNewSession: options.isNewSession,
653
+ permissionMode: options.permissionMode,
654
+ allowedTools: options.allowedTools,
655
+ attachments: options.attachments,
656
+ attachmentBlocks: options.attachmentBlocks,
657
+ };
658
+
659
+ wsRef.current.send(JSON.stringify(payload));
660
+ }, []);
661
+
662
+ const respondToPermission = useCallback((requestId: string, projectId: string, decision: PermissionDecision) => {
663
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
664
+ setError('Not connected');
665
+ return;
666
+ }
667
+
668
+ const payload: PermissionResponsePayload = {
669
+ type: 'permission_response',
670
+ requestId,
671
+ projectId,
672
+ decision,
673
+ };
674
+
675
+ wsRef.current.send(JSON.stringify(payload));
676
+ setPendingPermission(null);
677
+ }, []);
678
+
679
+ /**
680
+ * Send multiple queued messages as a single CLI invocation.
681
+ * Each text appears as a separate user bubble in the chat, but they are
682
+ * concatenated into one prompt for the 'claude -p --resume' call.
683
+ */
684
+ const sendMergedMessages = useCallback((texts: string[], options: UseChatStreamOptions) => {
685
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
686
+ setError('Not connected');
687
+ return;
688
+ }
689
+
690
+ if (texts.length === 0) return;
691
+
692
+ // Add each queued message as a separate user bubble
693
+ setMessages(prev => [
694
+ ...prev,
695
+ ...texts.map(text => ({
696
+ id: nextMessageId(),
697
+ role: 'user' as const,
698
+ content: [{type: 'text' as const, text}],
699
+ isStreaming: false,
700
+ })),
701
+ ]);
702
+
703
+ // Merge all texts into a single prompt
704
+ const merged = texts.join('\n\n');
705
+
706
+ const payload: SendMessagePayload = {
707
+ type: 'send_message',
708
+ message: merged,
709
+ projectId: options.projectId,
710
+ sessionId: options.sessionId,
711
+ claudeSessionId: options.claudeSessionId,
712
+ isNewSession: false, // queued messages always resume an existing session
713
+ permissionMode: options.permissionMode,
714
+ allowedTools: options.allowedTools,
715
+ };
716
+
717
+ wsRef.current.send(JSON.stringify(payload));
718
+ }, []);
719
+
720
+ const stopStream = useCallback((claudeSessionId?: string) => {
721
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
722
+
723
+ const payload: StopStreamPayload = {type: 'stop_stream', claudeSessionId};
724
+ wsRef.current.send(JSON.stringify(payload));
725
+ }, []);
726
+
727
+ const clearCancelledUserMessage = useCallback(() => {
728
+ setCancelledUserMessage(null);
729
+ }, []);
730
+
731
+ const setOnStreamEnd = useCallback((cb: (() => void) | null) => {
732
+ streamEndCallbackRef.current = cb;
733
+ }, []);
734
+
735
+ const clearMessages = useCallback(() => {
736
+ setMessages([]);
737
+ setContextUsage(null);
738
+ }, []);
739
+
740
+ const restoreMessages = useCallback((msgs: ChatMessage[]) => {
741
+ setMessages(msgs);
742
+ }, []);
743
+
744
+ const initContextUsage = useCallback((usage: ContextUsage | null) => {
745
+ setContextUsage(usage);
746
+ }, []);
747
+
748
+ const initSystemPrompt = useCallback((prompt: string | null) => {
749
+ setSystemPrompt(prompt);
750
+ }, []);
751
+
752
+ /**
753
+ * Attempt to re-attach to an in-flight stream after page reload.
754
+ * If the backend has an active CLI process for this session, it will send
755
+ * stream_resumed + catch-up events. If not, nothing happens and the loaded
756
+ * DB messages are the final state.
757
+ */
758
+ const attachSession = useCallback((claudeSessionId: string) => {
759
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
760
+
761
+ wsRef.current.send(JSON.stringify({
762
+ type: 'attach_session',
763
+ claudeSessionId,
764
+ }));
765
+ }, []);
766
+
767
+ /**
768
+ * Load persisted messages from the database for a session.
769
+ * Replaces the current in-memory messages with the stored history.
770
+ */
771
+ const loadMessages = useCallback(async (sessionId: string): Promise<number> => {
772
+ try {
773
+ const data = await apiClient.listChatMessages(sessionId);
774
+ const rows = data.messages as Array<{
775
+ id: string;
776
+ role: string;
777
+ content: string;
778
+ orderIndex: number;
779
+ createdAt: string;
780
+ }>;
781
+ const loaded: ChatMessage[] = rows.map((row) => {
782
+ const rawBlocks = JSON.parse(row.content) as Array<Record<string, unknown>>;
783
+ const content = rawBlocks
784
+ .map(normalizeContentBlock)
785
+ .filter((b): b is ContentBlock => b !== null);
786
+ return {
787
+ id: row.id,
788
+ role: row.role as 'user' | 'assistant',
789
+ content,
790
+ isStreaming: false,
791
+ };
792
+ });
793
+ setMessages(loaded);
794
+ return loaded.length;
795
+ } catch {
796
+ // If loading fails (e.g. no messages yet), start with empty messages
797
+ setMessages([]);
798
+ return 0;
799
+ }
800
+ }, []);
801
+
802
+ return {
803
+ connect,
804
+ disconnect,
805
+ sendMessage,
806
+ sendMergedMessages,
807
+ stopStream,
808
+ setOnStreamEnd,
809
+ loadMessages,
810
+ attachSession,
811
+ messages,
812
+ streaming,
813
+ connected,
814
+ error,
815
+ cancelledUserMessage,
816
+ clearCancelledUserMessage,
817
+ clearMessages,
818
+ restoreMessages,
819
+ pendingPermission,
820
+ respondToPermission,
821
+ contextUsage,
822
+ initContextUsage,
823
+ systemPrompt,
824
+ initSystemPrompt,
825
+ };
826
+ };