@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,155 @@
1
+ /**
2
+ * ChatMessageBubble — renders a single chat message as a user or assistant bubble.
3
+ *
4
+ * User bubbles: right-aligned, accent-tinted background, rounded corners.
5
+ * Includes inline image thumbnails and file chips for attachments.
6
+ * Assistant bubbles: left-aligned, surface background, full-width content area.
7
+ *
8
+ * Assistant messages delegate rendering to ChatMessageContent to handle
9
+ * text blocks, tool use cards, and tool result blocks.
10
+ */
11
+
12
+ import { useState, useCallback } from 'react';
13
+ import type { ChatMessage, TextBlock, ImageBlock, FileBlock } from '../hooks/use_chat_stream';
14
+ import { ChatMessageContent } from './ChatMessageContent';
15
+ import { HighlightedText } from './HighlightedText';
16
+ import { ImageLightbox } from './ImageLightbox';
17
+ import { useProjectStore } from '../stores/useProjectStore';
18
+ import { FileText, File as FileIcon } from 'lucide-react';
19
+
20
+ interface ChatMessageBubbleProps {
21
+ message: ChatMessage;
22
+ searchQuery?: string;
23
+ activeMatchIndex?: number;
24
+ /** Global match-index offset for this message (so highlights can be globally addressed). */
25
+ matchIndexOffset?: number;
26
+ }
27
+
28
+ /** Build the API URL for serving a chat attachment file.
29
+ * Paths are stored as `.chat-uploads/<filename>` — extract just the filename for the API. */
30
+ const buildFileUrl = (projectId: string, filePath: string): string => {
31
+ const fileName = filePath.replace(/^\.chat-uploads\//, '');
32
+ return `/api/chat-files/${encodeURIComponent(projectId)}/${encodeURIComponent(fileName)}`;
33
+ };
34
+
35
+ /** Map common MIME types to a friendly label for file chips. */
36
+ const getFileTypeLabel = (mimeType: string): string => {
37
+ if (mimeType.startsWith('text/')) return 'TXT';
38
+ if (mimeType === 'application/pdf') return 'PDF';
39
+ if (mimeType === 'application/json') return 'JSON';
40
+ if (mimeType.includes('spreadsheet') || mimeType === 'text/csv') return 'CSV';
41
+ return 'FILE';
42
+ };
43
+
44
+ export function ChatMessageBubble({ message, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageBubbleProps) {
45
+ if (message.role === 'user') {
46
+ return <UserBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
47
+ }
48
+ return <AssistantBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
49
+ }
50
+
51
+ function UserBubble({ message, searchQuery, activeMatchIndex, matchIndexOffset }: { message: ChatMessage; searchQuery: string; activeMatchIndex: number; matchIndexOffset: number }) {
52
+ const textBlock = message.content.find((b) => b.type === 'text') as TextBlock | undefined;
53
+ const text = textBlock?.text ?? '';
54
+ const imageBlocks = message.content.filter((b): b is ImageBlock => b.type === 'image');
55
+ const fileBlocks = message.content.filter((b): b is FileBlock => b.type === 'file');
56
+ const projectId = useProjectStore((s) => s.selectedProjectId);
57
+
58
+ const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
59
+
60
+ const handleImageClick = useCallback((index: number) => {
61
+ setLightboxIndex(index);
62
+ }, []);
63
+
64
+ const handleLightboxClose = useCallback(() => {
65
+ setLightboxIndex(null);
66
+ }, []);
67
+
68
+ return (
69
+ <div className="flex justify-end items-start gap-2">
70
+ <div className="max-w-[80%] overflow-hidden">
71
+ <div className="bg-accent/10 border border-accent/30 rounded-xl rounded-tr-sm px-4 py-2.5">
72
+ <pre className="m-0 text-[13px] font-mono text-content whitespace-pre-wrap break-words">
73
+ <HighlightedText text={text} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />
74
+ </pre>
75
+
76
+ {/* Image thumbnails */}
77
+ {imageBlocks.length > 0 && projectId && (
78
+ <div className="flex flex-wrap gap-2 mt-2">
79
+ {imageBlocks.map((img, idx) => (
80
+ <button
81
+ key={img.path}
82
+ type="button"
83
+ className="block w-20 h-20 rounded-md overflow-hidden border border-edge hover:border-accent transition-colors cursor-pointer bg-surface-alt shrink-0 p-0"
84
+ onClick={() => handleImageClick(idx)}
85
+ title={img.name}
86
+ >
87
+ <img
88
+ src={buildFileUrl(projectId, img.path)}
89
+ alt={img.name}
90
+ className="w-full h-full object-cover"
91
+ loading="lazy"
92
+ />
93
+ </button>
94
+ ))}
95
+ </div>
96
+ )}
97
+
98
+ {/* File chips */}
99
+ {fileBlocks.length > 0 && (
100
+ <div className="flex flex-wrap gap-1.5 mt-2">
101
+ {fileBlocks.map((file) => (
102
+ <span
103
+ key={file.path}
104
+ className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-alt border border-edge text-[11px] font-mono text-content-secondary"
105
+ title={file.name}
106
+ >
107
+ {file.mimeType === 'application/pdf' ? (
108
+ <FileText size={12} className="text-content-muted shrink-0" />
109
+ ) : (
110
+ <FileIcon size={12} className="text-content-muted shrink-0" />
111
+ )}
112
+ <span className="truncate max-w-32">{file.name}</span>
113
+ <span className="text-content-muted text-[10px]">{getFileTypeLabel(file.mimeType)}</span>
114
+ </span>
115
+ ))}
116
+ </div>
117
+ )}
118
+ </div>
119
+ </div>
120
+
121
+ {/* Lightbox overlay */}
122
+ {lightboxIndex !== null && projectId && (
123
+ <ImageLightbox
124
+ images={imageBlocks.map((img) => ({
125
+ src: buildFileUrl(projectId, img.path),
126
+ alt: img.name,
127
+ }))}
128
+ initialIndex={lightboxIndex}
129
+ onClose={handleLightboxClose}
130
+ />
131
+ )}
132
+ </div>
133
+ );
134
+ }
135
+
136
+ function AssistantBubble({ message, searchQuery, activeMatchIndex, matchIndexOffset }: { message: ChatMessage; searchQuery: string; activeMatchIndex: number; matchIndexOffset: number }) {
137
+ const isEmpty = message.content.length === 0;
138
+
139
+ return (
140
+ <div className="flex justify-start items-start gap-2">
141
+ <div className="max-w-[90%] min-w-0 overflow-hidden">
142
+ <div className="bg-surface-raised/50 border border-edge rounded-xl rounded-tl-sm px-4 py-2.5">
143
+ {isEmpty && message.isStreaming ? (
144
+ <span className="inline-flex gap-1 items-center text-[13px] font-mono text-content-muted">
145
+ <span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
146
+ Thinking…
147
+ </span>
148
+ ) : (
149
+ <ChatMessageContent content={message.content} isStreaming={message.isStreaming} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />
150
+ )}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ChatMessageContent — renders an assistant message's content blocks in order.
3
+ *
4
+ * Iterates over the ContentBlock array and renders:
5
+ * - TextBlock → markdown via react-markdown (safe, no dangerouslySetInnerHTML)
6
+ * - ToolUseBlock → collapsed ToolUseCard (shows tool input)
7
+ * - ToolResultBlock → collapsed ToolResultCard (shows tool output)
8
+ */
9
+
10
+ import ReactMarkdown from 'react-markdown';
11
+ import type { Components } from 'react-markdown';
12
+ import remarkGfm from 'remark-gfm';
13
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
14
+ import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
15
+ import type { ContentBlock, ToolResultBlock, ToolUseBlock } from '../hooks/use_chat_stream';
16
+ import { ToolUseCard } from './ToolUseCard';
17
+ import { HighlightedText, countMatches } from './HighlightedText';
18
+ import { MermaidBlock } from './MermaidBlock';
19
+
20
+ interface ChatMessageContentProps {
21
+ content: ContentBlock[];
22
+ isStreaming: boolean;
23
+ searchQuery?: string;
24
+ activeMatchIndex?: number;
25
+ matchIndexOffset?: number;
26
+ }
27
+
28
+ /**
29
+ * Build a lookup map from toolUseId → ToolResultBlock for quick access.
30
+ */
31
+ const buildResultMap = (blocks: ContentBlock[]): Map<string, ToolResultBlock> => {
32
+ const map = new Map<string, ToolResultBlock>();
33
+ for (const block of blocks) {
34
+ if (block.type === 'tool_result') {
35
+ map.set(block.toolUseId, block);
36
+ }
37
+ }
38
+ return map;
39
+ };
40
+
41
+
42
+ /** Tailwind classes for the markdown container (replaces .chat-markdown CSS) */
43
+ const chatMarkdownClass = [
44
+ 'text-[13px] font-mono text-content break-words min-w-0',
45
+ // first/last child spacing
46
+ '[&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
47
+ // headings
48
+ '[&_h1]:text-[16px] [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:leading-[1.3]',
49
+ '[&_h2]:text-[15px] [&_h2]:font-bold [&_h2]:mt-3.5 [&_h2]:mb-1.5 [&_h2]:leading-[1.3]',
50
+ '[&_h3]:text-[14px] [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:leading-[1.3]',
51
+ '[&_h4]:text-[13px] [&_h4]:font-semibold [&_h4]:mt-2.5 [&_h4]:mb-1 [&_h4]:leading-[1.3]',
52
+ // paragraphs
53
+ '[&_p]:my-1.5 [&_p]:leading-[1.6]',
54
+ // lists
55
+ '[&_ul]:list-disc [&_ol]:list-decimal [&_ul_ul]:list-circle [&_ol_ol]:list-[lower-alpha]',
56
+ '[&_ul]:pl-5 [&_ol]:pl-5 [&_ul]:my-1.5 [&_ol]:my-1.5',
57
+ '[&_li]:my-[3px] [&_li]:leading-normal [&_li>p]:my-0.5',
58
+ // blockquote
59
+ '[&_blockquote]:border-l-[3px] [&_blockquote]:border-edge [&_blockquote]:pl-3 [&_blockquote]:my-2 [&_blockquote]:text-content-secondary',
60
+ // pre / code
61
+ '[&_pre]:bg-surface-raised [&_pre]:py-2.5 [&_pre]:px-3 [&_pre]:rounded-md [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_pre]:my-2 [&_pre]:text-xs [&_pre]:leading-normal',
62
+ '[&_pre_code]:bg-transparent [&_pre_code]:p-0',
63
+ '[&_code]:bg-surface-raised [&_code]:px-[5px] [&_code]:py-px [&_code]:rounded-[3px] [&_code]:text-xs',
64
+ // inline
65
+ '[&_strong]:font-bold',
66
+ '[&_hr]:my-3 [&_hr]:border-edge',
67
+ // table
68
+ '[&_table]:my-2 [&_table]:border-collapse [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full',
69
+ '[&_th]:px-2.5 [&_th]:py-1 [&_th]:border [&_th]:border-edge [&_th]:text-xs [&_th]:font-semibold [&_th]:bg-surface-raised',
70
+ '[&_td]:px-2.5 [&_td]:py-1 [&_td]:border [&_td]:border-edge [&_td]:text-xs',
71
+ ].join(' ');
72
+
73
+ /**
74
+ * Custom react-markdown components for syntax-highlighted code blocks.
75
+ * Fenced blocks (```lang) get Prism highlighting; inline `code` stays as a styled <code>.
76
+ */
77
+ const markdownComponents: Components = {
78
+ code({ className, children, ...props }) {
79
+ const match = /language-(\w+)/.exec(className || '');
80
+ const codeString = String(children).replace(/\n$/, '');
81
+
82
+ // Fenced code block — has a language class set by react-markdown
83
+ if (match) {
84
+ // Mermaid code blocks render as interactive SVG diagrams
85
+ if (match[1] === 'mermaid') {
86
+ return <MermaidBlock code={codeString} />;
87
+ }
88
+
89
+ return (
90
+ <SyntaxHighlighter
91
+ style={oneDark}
92
+ language={match[1]}
93
+ PreTag="div"
94
+ customStyle={{ margin: 0, borderRadius: 6, fontSize: 12 }}
95
+ >
96
+ {codeString}
97
+ </SyntaxHighlighter>
98
+ );
99
+ }
100
+
101
+ // Inline code
102
+ return (
103
+ <code className={className} {...props}>
104
+ {children}
105
+ </code>
106
+ );
107
+ },
108
+ };
109
+
110
+ export const ChatMessageContent = ({ content, isStreaming, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageContentProps) => {
111
+ const resultMap = buildResultMap(content);
112
+
113
+ // Pre-compute per-block match offsets so each text block knows its global offset
114
+ const blockOffsets: number[] = [];
115
+ let runningOffset = matchIndexOffset;
116
+ for (const block of content) {
117
+ blockOffsets.push(runningOffset);
118
+ if (block.type === 'text' && searchQuery) {
119
+ runningOffset += countMatches(block.text, searchQuery);
120
+ }
121
+ }
122
+
123
+ return (
124
+ <div className="flex flex-col gap-1 min-w-0 overflow-hidden">
125
+ {content.map((block, index) => {
126
+ if (block.type === 'text') {
127
+ // When there's an active search, render plain text with highlights
128
+ // instead of markdown to ensure match positions are accurate
129
+ if (searchQuery) {
130
+ return (
131
+ <div
132
+ key={`text-${index}`}
133
+ className={`${chatMarkdownClass} whitespace-pre-wrap`}
134
+ >
135
+ <HighlightedText
136
+ text={block.text}
137
+ searchQuery={searchQuery}
138
+ activeMatchIndex={activeMatchIndex}
139
+ matchIndexOffset={blockOffsets[index]}
140
+ />
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <div
147
+ key={`text-${index}`}
148
+ className={chatMarkdownClass}
149
+ >
150
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
151
+ {block.text}
152
+ </ReactMarkdown>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ if (block.type === 'tool_use') {
158
+ const toolBlock = block as ToolUseBlock;
159
+ const matchedResult = toolBlock.id ? resultMap.get(toolBlock.id) : undefined;
160
+ const isLastBlock = index === content.length - 1;
161
+
162
+ return (
163
+ <ToolUseCard
164
+ key={`tool-${toolBlock.id || index}`}
165
+ name={toolBlock.name}
166
+ input={toolBlock.input}
167
+ isStreaming={isStreaming && isLastBlock && !matchedResult}
168
+ result={matchedResult}
169
+ />
170
+ );
171
+ }
172
+
173
+ // tool_result blocks are rendered inline within their ToolUseCard — skip standalone rendering
174
+ if (block.type === 'tool_result') {
175
+ return null;
176
+ }
177
+
178
+ return null;
179
+ })}
180
+ </div>
181
+ );
182
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * ChatMessageInput — multiline textarea with auto-expand, @ file and / skill
3
+ * mention autocomplete, and pill display for Chat v2.
4
+ *
5
+ * Features:
6
+ * - Auto-expands vertically as user types, up to a max height
7
+ * - Enter sends, Shift+Enter and Option+Enter insert a newline
8
+ * - Integrated attach button (left) and send button (right)
9
+ * - @ triggers file path autocomplete, / at start triggers skill autocomplete
10
+ * - Resolved mentions are highlighted inline with accent styling
11
+ * - Disabled state when no session is selected or while uploading
12
+ */
13
+
14
+ import { useRef, useCallback, useEffect, useMemo } from 'react';
15
+ import { SendHorizontal } from 'lucide-react';
16
+ import { IconButton } from './ds/IconButton';
17
+ import { ChatAttachButton } from './ChatAttachButton';
18
+ import { AutocompleteDropdown } from './AutocompleteDropdown';
19
+ import { useMentionAutocomplete } from '../hooks/use_mention_autocomplete';
20
+ import type { SkillInfo } from '../api/client';
21
+
22
+ interface ChatMessageInputProps {
23
+ value: string;
24
+ onChange: (value: string) => void;
25
+ onSend: () => void;
26
+ onFilesSelected: (files: File[]) => void;
27
+ disabled?: boolean;
28
+ placeholder?: string;
29
+ filePaths?: string[];
30
+ skills?: SkillInfo[];
31
+ }
32
+
33
+ const MAX_HEIGHT = 200;
34
+ const MIN_HEIGHT = 42;
35
+
36
+ export function ChatMessageInput({
37
+ value,
38
+ onChange,
39
+ onSend,
40
+ onFilesSelected,
41
+ disabled,
42
+ placeholder = 'Type a message\u2026',
43
+ filePaths = [],
44
+ skills = [],
45
+ }: ChatMessageInputProps) {
46
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
47
+
48
+ const {
49
+ showDropdown,
50
+ items,
51
+ selectedIndex,
52
+ mentions,
53
+ onTextChange,
54
+ onKeyDown: handleAutocompleteKeyDown,
55
+ selectItem,
56
+ } = useMentionAutocomplete({
57
+ filePaths,
58
+ skills,
59
+ value,
60
+ onChange,
61
+ textareaRef,
62
+ });
63
+
64
+ const adjustHeight = useCallback(() => {
65
+ const el = textareaRef.current;
66
+ if (!el) return;
67
+ el.style.height = 'auto';
68
+ el.style.height = `${Math.min(el.scrollHeight, MAX_HEIGHT)}px`;
69
+ }, []);
70
+
71
+ useEffect(() => {
72
+ adjustHeight();
73
+ }, [value, adjustHeight]);
74
+
75
+ const handleKeyDown = useCallback(
76
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
77
+ // Let autocomplete handle keys first
78
+ if (handleAutocompleteKeyDown(e)) return;
79
+
80
+ if (e.key === 'Enter') {
81
+ // Shift+Enter: default behavior (newline)
82
+ if (e.shiftKey) return;
83
+
84
+ // Option+Enter (Alt+Enter): insert newline manually
85
+ if (e.altKey) {
86
+ e.preventDefault();
87
+ const textarea = textareaRef.current;
88
+ if (textarea) {
89
+ const start = textarea.selectionStart;
90
+ const end = textarea.selectionEnd;
91
+ const newValue = value.slice(0, start) + '\n' + value.slice(end);
92
+ onChange(newValue);
93
+ requestAnimationFrame(() => {
94
+ textarea.selectionStart = start + 1;
95
+ textarea.selectionEnd = start + 1;
96
+ });
97
+ }
98
+ return;
99
+ }
100
+
101
+ // Plain Enter: send message
102
+ e.preventDefault();
103
+ if (!disabled && value.trim()) {
104
+ onSend();
105
+ }
106
+ }
107
+ },
108
+ [disabled, value, onSend, onChange, handleAutocompleteKeyDown],
109
+ );
110
+
111
+ const handleChange = useCallback(
112
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => {
113
+ const newValue = e.target.value;
114
+ const cursorPos = e.target.selectionStart;
115
+ onChange(newValue);
116
+ onTextChange(newValue, cursorPos);
117
+ },
118
+ [onChange, onTextChange],
119
+ );
120
+
121
+ // Detect trigger on cursor movement (click or arrow keys without value change)
122
+ const handleSelectionChange = useCallback(() => {
123
+ const textarea = textareaRef.current;
124
+ if (!textarea) return;
125
+ onTextChange(textarea.value, textarea.selectionStart);
126
+ }, [onTextChange]);
127
+
128
+ const overlayRef = useRef<HTMLDivElement>(null);
129
+ const hasMentions = mentions.length > 0;
130
+
131
+ // Sync overlay scroll with textarea
132
+ const handleScroll = useCallback(() => {
133
+ if (textareaRef.current && overlayRef.current) {
134
+ overlayRef.current.scrollTop = textareaRef.current.scrollTop;
135
+ }
136
+ }, []);
137
+
138
+ // Build overlay content: plain text + highlighted mention spans
139
+ const overlayContent = useMemo(() => {
140
+ if (!hasMentions) return null;
141
+
142
+ const sorted = [...mentions].sort((a, b) => a.startIndex - b.startIndex);
143
+ const parts: React.ReactNode[] = [];
144
+ let lastEnd = 0;
145
+
146
+ sorted.forEach((mention, i) => {
147
+ if (mention.startIndex > lastEnd) {
148
+ parts.push(value.slice(lastEnd, mention.startIndex));
149
+ }
150
+ parts.push(
151
+ <span
152
+ key={`m${i}`}
153
+ className="bg-accent/15 text-accent rounded-[3px] ring-1 ring-accent/25"
154
+ >
155
+ {value.slice(mention.startIndex, mention.endIndex)}
156
+ </span>,
157
+ );
158
+ lastEnd = mention.endIndex;
159
+ });
160
+
161
+ if (lastEnd < value.length) {
162
+ parts.push(value.slice(lastEnd));
163
+ }
164
+
165
+ return parts;
166
+ }, [value, mentions, hasMentions]);
167
+
168
+ return (
169
+ <div className="relative">
170
+ {/* Autocomplete dropdown positioned above */}
171
+ {showDropdown && (
172
+ <div className="absolute bottom-full left-0 right-0 mb-1 z-50">
173
+ <AutocompleteDropdown
174
+ items={items}
175
+ selectedIndex={selectedIndex}
176
+ onSelect={selectItem}
177
+ />
178
+ </div>
179
+ )}
180
+
181
+ <div className="flex flex-col bg-surface-alt border border-edge rounded-xl focus-within:border-accent transition-colors duration-150">
182
+ {/* Input row */}
183
+ <div className="flex items-end gap-2 px-3 py-2">
184
+ <ChatAttachButton onFilesSelected={onFilesSelected} disabled={disabled} />
185
+
186
+ <div className="relative flex-1">
187
+ {/* Overlay behind textarea for inline mention highlights */}
188
+ {hasMentions && (
189
+ <div
190
+ ref={overlayRef}
191
+ className="absolute inset-0 text-[13px] font-mono text-content leading-relaxed whitespace-pre-wrap break-words overflow-hidden pointer-events-none"
192
+ aria-hidden="true"
193
+ >
194
+ {overlayContent}
195
+ </div>
196
+ )}
197
+
198
+ <textarea
199
+ ref={textareaRef}
200
+ value={value}
201
+ onChange={handleChange}
202
+ onKeyDown={handleKeyDown}
203
+ onSelect={handleSelectionChange}
204
+ onClick={handleSelectionChange}
205
+ onScroll={handleScroll}
206
+ disabled={disabled}
207
+ placeholder={placeholder}
208
+ rows={1}
209
+ className="relative w-full bg-transparent text-[13px] font-mono leading-relaxed placeholder:text-content-muted outline-none resize-none"
210
+ style={{
211
+ minHeight: `${MIN_HEIGHT - 16}px`,
212
+ maxHeight: `${MAX_HEIGHT}px`,
213
+ ...(hasMentions
214
+ ? { color: 'transparent', caretColor: 'var(--color-content)' }
215
+ : { color: 'var(--color-content)' }),
216
+ }}
217
+ />
218
+ </div>
219
+
220
+ <IconButton
221
+ label="Send message"
222
+ variant="accent"
223
+ size="sm"
224
+ disabled={disabled || !value.trim()}
225
+ onClick={onSend}
226
+ >
227
+ <SendHorizontal size={14} strokeWidth={2} />
228
+ </IconButton>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ );
233
+ }