@assistkick/create 1.10.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 +390 -22
  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 +129 -8
  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,229 @@
1
+ /**
2
+ * PermissionModeSelector — UI control for selecting Chat v2 permission mode.
3
+ * Allows the user to choose between skip, allowed_tools, and prompt modes,
4
+ * and configure the allowed tools list when in allowed_tools mode.
5
+ */
6
+
7
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
8
+
9
+ export type PermissionMode = 'skip' | 'allowed_tools';
10
+
11
+ /** Known Claude Code built-in tools for autocomplete. */
12
+ const KNOWN_TOOLS = [
13
+ 'Bash',
14
+ 'Read',
15
+ 'Write',
16
+ 'Edit',
17
+ 'MultiEdit',
18
+ 'Glob',
19
+ 'Grep',
20
+ 'LS',
21
+ 'Agent',
22
+ 'NotebookEdit',
23
+ 'WebFetch',
24
+ 'WebSearch',
25
+ 'TodoRead',
26
+ 'TodoWrite',
27
+ ];
28
+
29
+ interface PermissionModeSelectorProps {
30
+ mode: PermissionMode;
31
+ allowedTools: string[];
32
+ onModeChange: (mode: PermissionMode) => void;
33
+ onAllowedToolsChange: (tools: string[]) => void;
34
+ }
35
+
36
+ const MODE_LABELS: Record<PermissionMode, { label: string; description: string }> = {
37
+ skip: {
38
+ label: 'Skip all permissions',
39
+ description: 'Claude can use any tool without asking',
40
+ },
41
+ allowed_tools: {
42
+ label: 'Selective auto-approve',
43
+ description: 'Only tools in the allowed list run without prompting',
44
+ },
45
+ };
46
+
47
+ export function PermissionModeSelector({
48
+ mode,
49
+ allowedTools,
50
+ onModeChange,
51
+ onAllowedToolsChange,
52
+ }: PermissionModeSelectorProps) {
53
+ const [newTool, setNewTool] = useState('');
54
+ const [showSuggestions, setShowSuggestions] = useState(false);
55
+ const [highlightIdx, setHighlightIdx] = useState(-1);
56
+ const inputRef = useRef<HTMLInputElement>(null);
57
+ const suggestionsRef = useRef<HTMLDivElement>(null);
58
+
59
+ const suggestions = KNOWN_TOOLS.filter(
60
+ (t) =>
61
+ !allowedTools.includes(t) &&
62
+ (newTool.trim() === '' || t.toLowerCase().includes(newTool.trim().toLowerCase()))
63
+ );
64
+
65
+ const handleAddTool = useCallback((toolName?: string) => {
66
+ const trimmed = (toolName ?? newTool).trim();
67
+ if (trimmed && !allowedTools.includes(trimmed)) {
68
+ onAllowedToolsChange([...allowedTools, trimmed]);
69
+ setNewTool('');
70
+ setShowSuggestions(false);
71
+ setHighlightIdx(-1);
72
+ }
73
+ }, [newTool, allowedTools, onAllowedToolsChange]);
74
+
75
+ const handleRemoveTool = useCallback((tool: string) => {
76
+ onAllowedToolsChange(allowedTools.filter(t => t !== tool));
77
+ }, [allowedTools, onAllowedToolsChange]);
78
+
79
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
80
+ if (e.key === 'Enter') {
81
+ e.preventDefault();
82
+ if (highlightIdx >= 0 && highlightIdx < suggestions.length) {
83
+ handleAddTool(suggestions[highlightIdx]);
84
+ } else {
85
+ handleAddTool();
86
+ }
87
+ } else if (e.key === 'ArrowDown') {
88
+ e.preventDefault();
89
+ setHighlightIdx((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0));
90
+ } else if (e.key === 'ArrowUp') {
91
+ e.preventDefault();
92
+ setHighlightIdx((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1));
93
+ } else if (e.key === 'Escape') {
94
+ setShowSuggestions(false);
95
+ setHighlightIdx(-1);
96
+ }
97
+ }, [handleAddTool, highlightIdx, suggestions]);
98
+
99
+ // Close suggestions on outside click
100
+ useEffect(() => {
101
+ const handler = (e: MouseEvent) => {
102
+ if (
103
+ suggestionsRef.current &&
104
+ !suggestionsRef.current.contains(e.target as Node) &&
105
+ inputRef.current &&
106
+ !inputRef.current.contains(e.target as Node)
107
+ ) {
108
+ setShowSuggestions(false);
109
+ setHighlightIdx(-1);
110
+ }
111
+ };
112
+ document.addEventListener('mousedown', handler);
113
+ return () => document.removeEventListener('mousedown', handler);
114
+ }, []);
115
+
116
+ return (
117
+ <div className="flex flex-col gap-3">
118
+ <div className="font-mono text-[11px] font-semibold text-content-secondary uppercase tracking-wider">
119
+ Permission Mode
120
+ </div>
121
+
122
+ <div className="flex flex-col gap-1.5">
123
+ {(Object.keys(MODE_LABELS) as PermissionMode[]).map((m) => (
124
+ <label
125
+ key={m}
126
+ className={`flex items-start gap-2 px-3 py-2 rounded-lg cursor-pointer border transition-colors duration-150 ${
127
+ mode === m
128
+ ? 'border-accent bg-[rgba(var(--accent-rgb),0.05)]'
129
+ : 'border-transparent hover:bg-surface-raised'
130
+ }`}
131
+ >
132
+ <input
133
+ type="radio"
134
+ name="permissionMode"
135
+ value={m}
136
+ checked={mode === m}
137
+ onChange={() => onModeChange(m)}
138
+ className="mt-0.5 accent-accent"
139
+ />
140
+ <div className="flex flex-col">
141
+ <span className="font-mono text-[13px] text-content">{MODE_LABELS[m].label}</span>
142
+ <span className="font-mono text-[11px] text-content-secondary">{MODE_LABELS[m].description}</span>
143
+ </div>
144
+ </label>
145
+ ))}
146
+ </div>
147
+
148
+ {mode === 'allowed_tools' && (
149
+ <div className="flex flex-col gap-2 mt-1">
150
+ <div className="font-mono text-[11px] font-semibold text-content-secondary uppercase tracking-wider">
151
+ Allowed Tools
152
+ </div>
153
+
154
+ {allowedTools.length > 0 && (
155
+ <div className="flex flex-wrap gap-1.5">
156
+ {allowedTools.map(tool => (
157
+ <span
158
+ key={tool}
159
+ className="inline-flex items-center gap-1 px-2 py-1 bg-surface-raised border border-edge rounded text-xs font-mono text-content"
160
+ >
161
+ {tool}
162
+ <button
163
+ className="bg-none border-none text-content-secondary cursor-pointer px-0.5 text-sm leading-none hover:text-error"
164
+ onClick={() => handleRemoveTool(tool)}
165
+ type="button"
166
+ >
167
+ &times;
168
+ </button>
169
+ </span>
170
+ ))}
171
+ </div>
172
+ )}
173
+
174
+ <div className="relative flex gap-2">
175
+ <div className="relative flex-1">
176
+ <input
177
+ ref={inputRef}
178
+ className="w-full h-8 px-2.5 bg-surface-raised border border-edge rounded text-content font-mono text-[12px] outline-none transition-[border-color] duration-150 focus:border-accent placeholder:text-content-muted"
179
+ type="text"
180
+ value={newTool}
181
+ onChange={e => {
182
+ setNewTool(e.target.value);
183
+ setShowSuggestions(true);
184
+ setHighlightIdx(-1);
185
+ }}
186
+ onFocus={() => setShowSuggestions(true)}
187
+ onKeyDown={handleKeyDown}
188
+ placeholder="Tool name (e.g. Read, Glob)"
189
+ />
190
+ {showSuggestions && suggestions.length > 0 && (
191
+ <div
192
+ ref={suggestionsRef}
193
+ className="absolute left-0 right-0 bottom-full mb-1 max-h-48 overflow-y-auto bg-surface-raised border border-edge rounded shadow-lg z-50"
194
+ >
195
+ {suggestions.map((tool, i) => (
196
+ <button
197
+ key={tool}
198
+ type="button"
199
+ className={`w-full text-left px-2.5 py-1.5 font-mono text-[12px] cursor-pointer border-none ${
200
+ i === highlightIdx
201
+ ? 'bg-accent text-white'
202
+ : 'bg-transparent text-content hover:bg-surface-alt'
203
+ }`}
204
+ onMouseDown={(e) => {
205
+ e.preventDefault();
206
+ handleAddTool(tool);
207
+ }}
208
+ onMouseEnter={() => setHighlightIdx(i)}
209
+ >
210
+ {tool}
211
+ </button>
212
+ ))}
213
+ </div>
214
+ )}
215
+ </div>
216
+ <button
217
+ className="h-8 px-3 bg-transparent border border-accent rounded text-accent font-mono text-[12px] cursor-pointer transition-[background,color] duration-150 hover:bg-accent hover:text-white disabled:opacity-60 disabled:cursor-not-allowed"
218
+ onClick={() => handleAddTool()}
219
+ disabled={!newTool.trim()}
220
+ type="button"
221
+ >
222
+ Add
223
+ </button>
224
+ </div>
225
+ </div>
226
+ )}
227
+ </div>
228
+ );
229
+ }
@@ -1,5 +1,9 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
2
  import type { Project } from '../hooks/useProjects';
3
+ import type { PreviewApp } from '../stores/usePreviewStore';
4
+ import { toAppName } from '../utils/preview_utils';
5
+ import { IconButton } from './ds/IconButton';
6
+ import { Settings, Play, Square, ExternalLink, Pencil, X } from 'lucide-react';
3
7
 
4
8
  interface ProjectSelectorProps {
5
9
  projects: Project[];
@@ -7,25 +11,43 @@ interface ProjectSelectorProps {
7
11
  onSelect: (id: string) => void;
8
12
  onCreate: (name: string, type?: string) => Promise<any>;
9
13
  onRename: (id: string, name: string) => Promise<void>;
14
+ onUpdatePreviewCommand: (id: string, previewCommand: string | null) => Promise<void>;
10
15
  onArchive: (id: string) => Promise<void>;
11
- onOpenGitModal?: (project: Project) => void;
16
+ previewApps: PreviewApp[];
17
+ onPreviewStart: (appName: string, projectId: string) => Promise<PreviewApp>;
18
+ onPreviewStop: (appName: string) => Promise<void>;
19
+ previewLoading: boolean;
12
20
  }
13
21
 
22
+ type InlineEditMode = 'rename' | 'command';
23
+
14
24
  export function ProjectSelector({
15
- projects, selectedProjectId, onSelect, onCreate, onRename, onArchive, onOpenGitModal,
25
+ projects, selectedProjectId, onSelect, onCreate, onRename, onUpdatePreviewCommand, onArchive,
26
+ previewApps, onPreviewStart, onPreviewStop, previewLoading,
16
27
  }: ProjectSelectorProps) {
17
28
  const [open, setOpen] = useState(false);
18
29
  const [creating, setCreating] = useState(false);
19
30
  const [newName, setNewName] = useState('');
20
31
  const [newType, setNewType] = useState<'software' | 'video'>('software');
21
- const [renamingId, setRenamingId] = useState<string | null>(null);
22
- const [renameValue, setRenameValue] = useState('');
32
+ const [editingId, setEditingId] = useState<string | null>(null);
33
+ const [editMode, setEditMode] = useState<InlineEditMode>('rename');
34
+ const [editValue, setEditValue] = useState('');
35
+ const [startingAppName, setStartingAppName] = useState<string | null>(null);
36
+ // When user clicks start but project has no command and no package.json fallback
37
+ const [promptCommandForId, setPromptCommandForId] = useState<string | null>(null);
23
38
  const dropdownRef = useRef<HTMLDivElement>(null);
24
39
  const createInputRef = useRef<HTMLInputElement>(null);
25
- const renameInputRef = useRef<HTMLInputElement>(null);
40
+ const editInputRef = useRef<HTMLInputElement>(null);
26
41
 
27
42
  const selectedProject = projects.find(p => p.id === selectedProjectId);
28
43
 
44
+ // Build a lookup of running apps by appName
45
+ const runningApps = new Map(previewApps.map(a => [a.appName, a]));
46
+
47
+ // Check if the selected project has a running preview
48
+ const selectedAppName = selectedProject ? toAppName(selectedProject.name) : null;
49
+ const selectedIsRunning = selectedAppName ? runningApps.has(selectedAppName) : false;
50
+
29
51
  // Close dropdown on outside click
30
52
  useEffect(() => {
31
53
  if (!open) return;
@@ -33,7 +55,8 @@ export function ProjectSelector({
33
55
  if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
34
56
  setOpen(false);
35
57
  setCreating(false);
36
- setRenamingId(null);
58
+ setEditingId(null);
59
+ setPromptCommandForId(null);
37
60
  }
38
61
  };
39
62
  document.addEventListener('mousedown', handler);
@@ -45,10 +68,10 @@ export function ProjectSelector({
45
68
  if (creating) createInputRef.current?.focus();
46
69
  }, [creating]);
47
70
 
48
- // Focus rename input when shown
71
+ // Focus edit input when shown
49
72
  useEffect(() => {
50
- if (renamingId) renameInputRef.current?.focus();
51
- }, [renamingId]);
73
+ if (editingId || promptCommandForId) editInputRef.current?.focus();
74
+ }, [editingId, promptCommandForId]);
52
75
 
53
76
  const handleCreate = async () => {
54
77
  const trimmed = newName.trim();
@@ -64,16 +87,28 @@ export function ProjectSelector({
64
87
  }
65
88
  };
66
89
 
67
- const handleRename = async () => {
68
- const trimmed = renameValue.trim();
69
- if (!trimmed || !renamingId) return;
90
+ const handleEditSave = async () => {
91
+ const trimmed = editValue.trim();
92
+ if (!editingId) return;
93
+
70
94
  try {
71
- await onRename(renamingId, trimmed);
72
- setRenamingId(null);
73
- setRenameValue('');
95
+ if (editMode === 'rename') {
96
+ if (!trimmed) return;
97
+ await onRename(editingId, trimmed);
98
+ } else {
99
+ // command mode — empty string means clear
100
+ await onUpdatePreviewCommand(editingId, trimmed || null);
101
+ }
74
102
  } catch {
75
103
  // Error is handled by the caller
76
104
  }
105
+ setEditingId(null);
106
+ setEditValue('');
107
+ };
108
+
109
+ const handleEditCancel = () => {
110
+ setEditingId(null);
111
+ setEditValue('');
77
112
  };
78
113
 
79
114
  const handleArchive = async (id: string) => {
@@ -84,6 +119,81 @@ export function ProjectSelector({
84
119
  }
85
120
  };
86
121
 
122
+ const startRename = (e: React.MouseEvent, project: Project) => {
123
+ e.stopPropagation();
124
+ setEditingId(project.id);
125
+ setEditMode('rename');
126
+ setEditValue(project.name);
127
+ setPromptCommandForId(null);
128
+ };
129
+
130
+ const startCommandEdit = (e: React.MouseEvent, project: Project) => {
131
+ e.stopPropagation();
132
+ setEditingId(project.id);
133
+ setEditMode('command');
134
+ setEditValue(project.previewCommand || '');
135
+ setPromptCommandForId(null);
136
+ };
137
+
138
+ const handleCommandPromptSave = async () => {
139
+ if (!promptCommandForId) return;
140
+ const trimmed = editValue.trim();
141
+ if (!trimmed) return;
142
+
143
+ try {
144
+ await onUpdatePreviewCommand(promptCommandForId, trimmed);
145
+ // Now start the preview
146
+ const project = projects.find(p => p.id === promptCommandForId);
147
+ if (project) {
148
+ const appName = toAppName(project.name);
149
+ setStartingAppName(appName);
150
+ try {
151
+ await onPreviewStart(appName, project.id);
152
+ window.open(`/apps/${appName}/`, '_blank');
153
+ } finally {
154
+ setStartingAppName(null);
155
+ }
156
+ }
157
+ } catch {
158
+ // Error handled by caller
159
+ }
160
+ setPromptCommandForId(null);
161
+ setEditValue('');
162
+ };
163
+
164
+ const handlePreviewToggle = async (e: React.MouseEvent, project: Project) => {
165
+ e.stopPropagation();
166
+ const appName = toAppName(project.name);
167
+ const isRunning = runningApps.has(appName);
168
+
169
+ if (isRunning) {
170
+ await onPreviewStop(appName);
171
+ } else {
172
+ setStartingAppName(appName);
173
+ try {
174
+ // Attempt to start via backend — it will use previewCommand if set,
175
+ // otherwise auto-detect from package.json
176
+ await onPreviewStart(appName, project.id);
177
+ window.open(`/apps/${appName}/`, '_blank');
178
+ } catch (err: any) {
179
+ // If backend couldn't detect a start command, prompt the user to configure one
180
+ const message = err?.message || '';
181
+ if (message.includes('No dev server script found')) {
182
+ setPromptCommandForId(project.id);
183
+ setEditValue('');
184
+ }
185
+ // Other errors are handled by the caller
186
+ } finally {
187
+ setStartingAppName(null);
188
+ }
189
+ }
190
+ };
191
+
192
+ const handleOpenPreview = (e: React.MouseEvent, appName: string) => {
193
+ e.stopPropagation();
194
+ window.open(`/apps/${appName}/`, '_blank');
195
+ };
196
+
87
197
  return (
88
198
  <div className="relative" ref={dropdownRef}>
89
199
  <button
@@ -91,6 +201,9 @@ export function ProjectSelector({
91
201
  onClick={() => setOpen(v => !v)}
92
202
  title="Select project"
93
203
  >
204
+ {selectedIsRunning && (
205
+ <span className="shrink-0 w-2 h-2 rounded-full bg-emerald-500" title="Preview server running" />
206
+ )}
94
207
  <span className="overflow-hidden text-ellipsis whitespace-nowrap">
95
208
  {selectedProject?.name || 'No project'}
96
209
  </span>
@@ -98,80 +211,133 @@ export function ProjectSelector({
98
211
  </button>
99
212
 
100
213
  {open && (
101
- <div className="absolute top-[calc(100%+4px)] left-0 min-w-[220px] max-w-[300px] bg-panel border border-edge rounded-md shadow-[0_4px_12px_var(--panel-shadow)] z-[200] overflow-hidden">
214
+ <div className="absolute top-[calc(100%+4px)] left-0 min-w-[280px] max-w-[360px] bg-panel border border-edge rounded-md shadow-[0_4px_12px_var(--panel-shadow)] z-[200] overflow-hidden">
102
215
  <div className="max-h-60 overflow-y-auto py-1">
103
- {projects.map(project => (
104
- <div
105
- key={project.id}
106
- className={`group flex items-center pr-1 hover:bg-tab-hover${project.id === selectedProjectId ? ' bg-tab-active' : ''}`}
107
- >
108
- {renamingId === project.id ? (
109
- <input
110
- ref={renameInputRef}
111
- className="flex-1 py-[5px] px-2 bg-surface border border-accent rounded-sm text-content font-mono text-xs outline-none mx-1 my-0.5"
112
- value={renameValue}
113
- onChange={e => setRenameValue(e.target.value)}
114
- onKeyDown={e => {
115
- if (e.key === 'Enter') handleRename();
116
- if (e.key === 'Escape') { setRenamingId(null); setRenameValue(''); }
117
- }}
118
- onBlur={handleRename}
119
- />
120
- ) : (
121
- <>
122
- <button
123
- className="flex-1 flex items-center gap-1.5 text-left py-1.5 px-2 bg-none border-none text-content font-mono text-xs cursor-pointer overflow-hidden"
124
- onClick={() => { onSelect(project.id); setOpen(false); }}
125
- >
126
- <span className="overflow-hidden text-ellipsis whitespace-nowrap">{project.name}</span>
127
- {project.type === 'video' && (
128
- <span className="shrink-0 rounded-full bg-purple-500/15 px-1.5 py-0.5 text-[9px] font-bold text-purple-400">Video</span>
129
- )}
130
- </button>
131
- <div className="flex gap-0.5 opacity-0 transition-opacity duration-150 group-hover:opacity-100">
132
- {onOpenGitModal && (
133
- <button
134
- className={`bg-none border-none text-xs cursor-pointer px-1 py-0.5 rounded-sm transition-[color,background] duration-150 hover:text-content hover:bg-surface-raised ${project.repoUrl ? 'text-accent' : 'text-content-secondary'}`}
135
- title="Git Repository"
136
- onClick={(e) => {
137
- e.stopPropagation();
138
- setOpen(false);
139
- onOpenGitModal(project);
140
- }}
141
- >
142
- <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
143
- <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" />
144
- </svg>
145
- </button>
146
- )}
147
- <button
148
- className="bg-none border-none text-content-muted text-xs cursor-pointer px-1 py-0.5 rounded-sm transition-[color,background] duration-150 hover:text-content hover:bg-surface-raised"
149
- title="Rename"
150
- onClick={(e) => {
151
- e.stopPropagation();
152
- setRenamingId(project.id);
153
- setRenameValue(project.name);
216
+ {projects.map(project => {
217
+ const appName = toAppName(project.name);
218
+ const isRunning = runningApps.has(appName);
219
+ const isStarting = startingAppName === appName;
220
+ const isPromptingCommand = promptCommandForId === project.id;
221
+
222
+ return (
223
+ <div
224
+ key={project.id}
225
+ className={`group flex flex-col hover:bg-tab-hover${project.id === selectedProjectId ? ' bg-tab-active' : ''}`}
226
+ >
227
+ {editingId === project.id ? (
228
+ <div className="flex items-center px-1 py-0.5">
229
+ <input
230
+ ref={editInputRef}
231
+ className="flex-1 py-[5px] px-2 bg-surface border border-accent rounded-sm text-content font-mono text-xs outline-none mx-1 my-0.5"
232
+ placeholder={editMode === 'rename' ? 'Project name...' : 'e.g. npm run dev, node server.js'}
233
+ value={editValue}
234
+ onChange={e => setEditValue(e.target.value)}
235
+ onKeyDown={e => {
236
+ if (e.key === 'Enter') handleEditSave();
237
+ if (e.key === 'Escape') handleEditCancel();
154
238
  }}
155
- >
156
- &#9998;
157
- </button>
158
- {!project.isDefault && (
239
+ onBlur={handleEditSave}
240
+ />
241
+ </div>
242
+ ) : (
243
+ <>
244
+ <div className="flex items-center pr-1">
159
245
  <button
160
- className="bg-none border-none text-content-muted text-xs cursor-pointer px-1 py-0.5 rounded-sm transition-[color,background] duration-150 hover:text-content hover:bg-surface-raised hover:!text-error"
161
- title="Archive"
162
- onClick={(e) => {
163
- e.stopPropagation();
164
- handleArchive(project.id);
165
- }}
246
+ className="flex-1 flex items-center gap-1.5 text-left py-1.5 px-2 bg-none border-none text-content font-mono text-xs cursor-pointer overflow-hidden"
247
+ onClick={() => { onSelect(project.id); setOpen(false); }}
166
248
  >
167
- &#10005;
249
+ {isRunning && (
250
+ <span className="shrink-0 w-1.5 h-1.5 rounded-full bg-emerald-500" />
251
+ )}
252
+ <span className="overflow-hidden text-ellipsis whitespace-nowrap">{project.name}</span>
253
+ {project.type === 'video' && (
254
+ <span className="shrink-0 rounded-full bg-purple-500/15 px-1.5 py-0.5 text-[9px] font-bold text-purple-400">Video</span>
255
+ )}
168
256
  </button>
257
+ <div className="flex items-center gap-0.5 opacity-0 transition-opacity duration-150 group-hover:opacity-100">
258
+ {/* Preview start/stop toggle */}
259
+ <IconButton
260
+ variant={isRunning ? 'accent' : 'ghost'}
261
+ size="sm"
262
+ label={isRunning ? 'Stop preview server' : 'Start preview server'}
263
+ disabled={previewLoading || isStarting}
264
+ onClick={(e) => handlePreviewToggle(e, project)}
265
+ >
266
+ {isStarting ? (
267
+ <span className="inline-block w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
268
+ ) : isRunning ? (
269
+ <Square className="w-3 h-3" />
270
+ ) : (
271
+ <Play className="w-3 h-3" />
272
+ )}
273
+ </IconButton>
274
+ {/* Open Preview link for running servers */}
275
+ {isRunning && (
276
+ <IconButton
277
+ variant="ghost"
278
+ size="sm"
279
+ label="Open preview in new tab"
280
+ onClick={(e) => handleOpenPreview(e, appName)}
281
+ >
282
+ <ExternalLink className="w-3 h-3" />
283
+ </IconButton>
284
+ )}
285
+ {/* Settings/gear icon — edit preview command */}
286
+ <IconButton
287
+ variant="ghost"
288
+ size="sm"
289
+ label="Configure preview command"
290
+ onClick={(e) => startCommandEdit(e, project)}
291
+ >
292
+ <Settings className="w-3 h-3" />
293
+ </IconButton>
294
+ {/* Rename */}
295
+ <IconButton
296
+ variant="ghost"
297
+ size="sm"
298
+ label="Rename"
299
+ onClick={(e) => startRename(e, project)}
300
+ >
301
+ <Pencil className="w-3 h-3" />
302
+ </IconButton>
303
+ {/* Archive */}
304
+ {!project.isDefault && (
305
+ <IconButton
306
+ variant="danger"
307
+ size="sm"
308
+ label="Archive"
309
+ onClick={(e) => {
310
+ e.stopPropagation();
311
+ handleArchive(project.id);
312
+ }}
313
+ >
314
+ <X className="w-3 h-3" />
315
+ </IconButton>
316
+ )}
317
+ </div>
318
+ </div>
319
+ {/* Command prompt — shown when user tries to start without a configured command */}
320
+ {isPromptingCommand && (
321
+ <div className="flex items-center px-1 pb-1">
322
+ <input
323
+ ref={editInputRef}
324
+ className="flex-1 py-[5px] px-2 bg-surface border border-accent rounded-sm text-content font-mono text-xs outline-none mx-1"
325
+ placeholder="Start command (e.g. npm run dev)..."
326
+ value={editValue}
327
+ onChange={e => setEditValue(e.target.value)}
328
+ onKeyDown={e => {
329
+ if (e.key === 'Enter') handleCommandPromptSave();
330
+ if (e.key === 'Escape') { setPromptCommandForId(null); setEditValue(''); }
331
+ }}
332
+ onBlur={() => { setPromptCommandForId(null); setEditValue(''); }}
333
+ />
334
+ </div>
169
335
  )}
170
- </div>
171
- </>
172
- )}
173
- </div>
174
- ))}
336
+ </>
337
+ )}
338
+ </div>
339
+ );
340
+ })}
175
341
  </div>
176
342
 
177
343
  <div className="border-t border-edge p-1">
@@ -0,0 +1,38 @@
1
+ /**
2
+ * QueuedMessageBubble — displays a single queued user message with a
3
+ * pending/queued visual state and a remove button.
4
+ *
5
+ * Queued messages appear as user bubbles in the chat UI but with reduced
6
+ * opacity and a "queued" indicator to distinguish them from sent messages.
7
+ */
8
+
9
+ interface Props {
10
+ id: string;
11
+ text: string;
12
+ onRemove: (id: string) => void;
13
+ }
14
+
15
+ export const QueuedMessageBubble = ({ id, text, onRemove }: Props) => {
16
+ return (
17
+ <div className="flex justify-end items-start gap-2 group opacity-60">
18
+ <div className="flex flex-col items-end gap-1 max-w-[80%]">
19
+ <div className="relative bg-accent/10 border border-accent/30 rounded-xl rounded-tr-sm px-4 py-2.5">
20
+ <pre className="m-0 text-[13px] font-mono text-content whitespace-pre-wrap break-words">
21
+ {text}
22
+ </pre>
23
+ <button
24
+ type="button"
25
+ onClick={() => onRemove(id)}
26
+ className="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-surface border border-edge flex items-center justify-center text-content-secondary text-xs cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity duration-150 hover:bg-error hover:text-white hover:border-error"
27
+ aria-label="Remove queued message"
28
+ >
29
+ &times;
30
+ </button>
31
+ </div>
32
+ <span className="text-[10px] font-mono text-content-muted uppercase tracking-wider">
33
+ queued
34
+ </span>
35
+ </div>
36
+ </div>
37
+ );
38
+ };