@assistkick/create 1.7.0 → 1.9.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/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
  62. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
  63. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  64. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  65. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  66. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  67. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  68. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  69. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  109. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  113. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  114. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  123. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  124. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  133. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  134. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
  157. package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  159. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  160. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  161. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
  163. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  165. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  166. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  167. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  168. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  171. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  172. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  173. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  174. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  175. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  176. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  177. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  178. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  179. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  180. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  181. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  182. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  183. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  184. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  185. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  186. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  187. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  188. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  189. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  190. package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
  191. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  192. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  193. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  194. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  195. package/templates/skills/product-system/graph.json +1890 -0
  196. package/templates/skills/product-system/kanban.json +304 -0
  197. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  198. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  199. package/templates/skills/product-system/nodes/data_001.md +51 -0
  200. package/templates/skills/product-system/nodes/data_002.md +40 -0
  201. package/templates/skills/product-system/nodes/data_004.md +38 -0
  202. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  203. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  204. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  205. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  206. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,404 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import Editor, { type OnMount } from '@monaco-editor/react';
3
+ import { FileTree } from './FileTree';
4
+ import { EditorTabBar } from './EditorTabBar';
5
+ import { useTheme } from '../hooks/useTheme';
6
+ import { useAutoSave } from '../hooks/useAutoSave';
7
+ import { apiClient } from '../api/client';
8
+ import { getLanguageFromPath, getFileName, buildNewPath, updatePathPrefix } from '../utils/file_utils';
9
+ import type { TreeEntry, FileTreeCallbacks } from './FileTree';
10
+ import type { EditorTab } from './EditorTabBar';
11
+
12
+ const MIN_TREE_WIDTH = 180;
13
+ const MAX_TREE_WIDTH = 600;
14
+ const DEFAULT_TREE_WIDTH = 260;
15
+
16
+ interface FilesViewProps {
17
+ projectId: string | null;
18
+ }
19
+
20
+ const collectExpandedPaths = (tree: TreeEntry[], depth: number): Set<string> => {
21
+ const paths = new Set<string>();
22
+ for (const entry of tree) {
23
+ if (entry.type === 'directory' && depth === 0) {
24
+ paths.add(entry.path);
25
+ }
26
+ if (entry.children) {
27
+ for (const p of collectExpandedPaths(entry.children, depth + 1)) {
28
+ paths.add(p);
29
+ }
30
+ }
31
+ }
32
+ return paths;
33
+ };
34
+
35
+ export const FilesView = ({ projectId }: FilesViewProps) => {
36
+ const { theme } = useTheme();
37
+ const autoSave = useAutoSave({ projectId });
38
+ const autoSaveRef = useRef(autoSave);
39
+ autoSaveRef.current = autoSave;
40
+ const [tree, setTree] = useState<TreeEntry[]>([]);
41
+ const [tabs, setTabs] = useState<EditorTab[]>([]);
42
+ const [activeTabPath, setActiveTabPath] = useState<string | null>(null);
43
+ const [fileContents, setFileContents] = useState<Record<string, string>>({});
44
+ const [loading, setLoading] = useState(false);
45
+ const [treeError, setTreeError] = useState<string | null>(null);
46
+ const [treeWidth, setTreeWidth] = useState(DEFAULT_TREE_WIDTH);
47
+ const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());
48
+ const resizingRef = useRef(false);
49
+ const containerRef = useRef<HTMLDivElement>(null);
50
+ const initialExpandDone = useRef(false);
51
+
52
+ const loadTree = useCallback(async () => {
53
+ if (!projectId) {
54
+ setTree([]);
55
+ return;
56
+ }
57
+ try {
58
+ const data = await apiClient.fetchFileTree(projectId);
59
+ const newTree = data.tree || [];
60
+ setTree(newTree);
61
+ // On first load, expand root-level directories
62
+ if (!initialExpandDone.current) {
63
+ setExpandedPaths(collectExpandedPaths(newTree, 0));
64
+ initialExpandDone.current = true;
65
+ }
66
+ setTreeError(null);
67
+ } catch (err: any) {
68
+ if (err.message?.includes('404')) {
69
+ setTreeError('No workspace found. Connect a git repository first.');
70
+ } else {
71
+ setTreeError(err.message || 'Failed to load file tree');
72
+ }
73
+ }
74
+ }, [projectId]);
75
+
76
+ // Fetch file tree when project changes
77
+ useEffect(() => {
78
+ initialExpandDone.current = false;
79
+ loadTree();
80
+ }, [loadTree]);
81
+
82
+ const handleToggleExpand = useCallback((path: string) => {
83
+ setExpandedPaths((prev) => {
84
+ const next = new Set(prev);
85
+ if (next.has(path)) {
86
+ next.delete(path);
87
+ } else {
88
+ next.add(path);
89
+ }
90
+ return next;
91
+ });
92
+ }, []);
93
+
94
+ // Open a file in a new tab or switch to existing tab
95
+ const handleFileSelect = useCallback(async (path: string, name: string) => {
96
+ if (!projectId) return;
97
+
98
+ // If already open, just switch to it
99
+ const existing = tabs.find((t) => t.path === path);
100
+ if (existing) {
101
+ setActiveTabPath(path);
102
+ return;
103
+ }
104
+
105
+ // Load the file content
106
+ setLoading(true);
107
+ try {
108
+ const data = await apiClient.fetchFileContent(projectId, path);
109
+
110
+ if (data.binary) {
111
+ setFileContents((prev) => ({ ...prev, [path]: `[Binary file — ${data.size} bytes]` }));
112
+ } else {
113
+ setFileContents((prev) => ({ ...prev, [path]: data.content }));
114
+ }
115
+
116
+ const language = getLanguageFromPath(path);
117
+ const newTab: EditorTab = { path, name, language };
118
+ setTabs((prev) => [...prev, newTab]);
119
+ setActiveTabPath(path);
120
+ } catch (err: any) {
121
+ console.error('Failed to load file:', err);
122
+ } finally {
123
+ setLoading(false);
124
+ }
125
+ }, [projectId, tabs]);
126
+
127
+ const handleSelectTab = useCallback((path: string) => {
128
+ setActiveTabPath(path);
129
+ }, []);
130
+
131
+ const handleCloseTab = useCallback((path: string) => {
132
+ // Fire-and-forget save for unsaved changes before closing
133
+ autoSaveRef.current.saveAndDiscard(path);
134
+
135
+ setTabs((prev) => {
136
+ const idx = prev.findIndex((t) => t.path === path);
137
+ const next = prev.filter((t) => t.path !== path);
138
+
139
+ if (path === activeTabPath) {
140
+ if (next.length === 0) {
141
+ setActiveTabPath(null);
142
+ } else {
143
+ const newIdx = Math.min(idx, next.length - 1);
144
+ setActiveTabPath(next[newIdx].path);
145
+ }
146
+ }
147
+
148
+ return next;
149
+ });
150
+
151
+ setFileContents((prev) => {
152
+ const copy = { ...prev };
153
+ delete copy[path];
154
+ return copy;
155
+ });
156
+ }, [activeTabPath]);
157
+
158
+ // CRUD operations
159
+ const handleCreateFile = useCallback(async (parentPath: string, name: string, type: 'file' | 'directory') => {
160
+ if (!projectId) return;
161
+ const fullPath = parentPath ? `${parentPath}/${name}` : name;
162
+ try {
163
+ await apiClient.createFile(projectId, fullPath, type);
164
+ await loadTree();
165
+ // If it's a file, open it in the editor
166
+ if (type === 'file') {
167
+ handleFileSelect(fullPath, name);
168
+ }
169
+ } catch (err: any) {
170
+ console.error(`Failed to create ${type}:`, err);
171
+ }
172
+ }, [projectId, loadTree, handleFileSelect]);
173
+
174
+ const handleRename = useCallback(async (oldPath: string, newName: string) => {
175
+ if (!projectId) return;
176
+ const newPath = buildNewPath(oldPath, newName);
177
+ if (newPath === oldPath) return;
178
+
179
+ try {
180
+ await apiClient.renameFile(projectId, oldPath, newPath);
181
+ await loadTree();
182
+
183
+ // Update any open tabs that reference the old path
184
+ setTabs((prev) => prev.map((tab) => {
185
+ const updatedPath = updatePathPrefix(tab.path, oldPath, newPath);
186
+ if (updatedPath !== tab.path) {
187
+ return { ...tab, path: updatedPath, name: getFileName(updatedPath), language: getLanguageFromPath(updatedPath) };
188
+ }
189
+ return tab;
190
+ }));
191
+
192
+ // Update active tab path
193
+ if (activeTabPath) {
194
+ const updatedActive = updatePathPrefix(activeTabPath, oldPath, newPath);
195
+ if (updatedActive !== activeTabPath) {
196
+ setActiveTabPath(updatedActive);
197
+ }
198
+ }
199
+
200
+ // Update file contents cache
201
+ setFileContents((prev) => {
202
+ const next: Record<string, string> = {};
203
+ for (const [key, val] of Object.entries(prev)) {
204
+ next[updatePathPrefix(key, oldPath, newPath)] = val;
205
+ }
206
+ return next;
207
+ });
208
+
209
+ // Update expanded paths
210
+ setExpandedPaths((prev) => {
211
+ const next = new Set<string>();
212
+ for (const p of prev) {
213
+ next.add(updatePathPrefix(p, oldPath, newPath));
214
+ }
215
+ return next;
216
+ });
217
+ } catch (err: any) {
218
+ console.error('Failed to rename:', err);
219
+ }
220
+ }, [projectId, loadTree, activeTabPath]);
221
+
222
+ const handleDelete = useCallback(async (path: string, type: 'file' | 'directory') => {
223
+ if (!projectId) return;
224
+ try {
225
+ await apiClient.deleteFile(projectId, path);
226
+ await loadTree();
227
+
228
+ // Close any tabs for deleted files
229
+ setTabs((prev) => {
230
+ const next = prev.filter((tab) => tab.path !== path && !tab.path.startsWith(path + '/'));
231
+ if (activeTabPath === path || activeTabPath?.startsWith(path + '/')) {
232
+ if (next.length === 0) {
233
+ setActiveTabPath(null);
234
+ } else {
235
+ setActiveTabPath(next[next.length - 1].path);
236
+ }
237
+ }
238
+ return next;
239
+ });
240
+
241
+ // Clean up content cache
242
+ setFileContents((prev) => {
243
+ const copy = { ...prev };
244
+ for (const key of Object.keys(copy)) {
245
+ if (key === path || key.startsWith(path + '/')) {
246
+ delete copy[key];
247
+ }
248
+ }
249
+ return copy;
250
+ });
251
+ } catch (err: any) {
252
+ console.error('Failed to delete:', err);
253
+ }
254
+ }, [projectId, loadTree, activeTabPath]);
255
+
256
+ const treeCallbacks: FileTreeCallbacks = {
257
+ onFileSelect: handleFileSelect,
258
+ onCreateFile: handleCreateFile,
259
+ onRename: handleRename,
260
+ onDelete: handleDelete,
261
+ };
262
+
263
+ // Resizable split pane drag handling
264
+ const handleResizeStart = useCallback((e: React.MouseEvent) => {
265
+ e.preventDefault();
266
+ resizingRef.current = true;
267
+ const startX = e.clientX;
268
+ const startWidth = treeWidth;
269
+
270
+ const onMouseMove = (ev: MouseEvent) => {
271
+ if (!resizingRef.current) return;
272
+ const delta = ev.clientX - startX;
273
+ const newWidth = Math.min(MAX_TREE_WIDTH, Math.max(MIN_TREE_WIDTH, startWidth + delta));
274
+ setTreeWidth(newWidth);
275
+ };
276
+
277
+ const onMouseUp = () => {
278
+ resizingRef.current = false;
279
+ document.removeEventListener('mousemove', onMouseMove);
280
+ document.removeEventListener('mouseup', onMouseUp);
281
+ document.body.style.cursor = '';
282
+ document.body.style.userSelect = '';
283
+ };
284
+
285
+ document.body.style.cursor = 'col-resize';
286
+ document.body.style.userSelect = 'none';
287
+ document.addEventListener('mousemove', onMouseMove);
288
+ document.addEventListener('mouseup', onMouseUp);
289
+ }, [treeWidth]);
290
+
291
+ const activeTabPathRef = useRef(activeTabPath);
292
+ activeTabPathRef.current = activeTabPath;
293
+
294
+ const handleEditorMount: OnMount = useCallback((editor, monaco) => {
295
+ // Intercept Ctrl/Cmd+S to trigger immediate save
296
+ editor.addAction({
297
+ id: 'auto-save-now',
298
+ label: 'Save File',
299
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
300
+ run: () => {
301
+ const path = activeTabPathRef.current;
302
+ if (path) {
303
+ autoSaveRef.current.saveNow(path);
304
+ }
305
+ },
306
+ });
307
+ }, []);
308
+
309
+ const handleEditorChange = useCallback((value: string | undefined) => {
310
+ if (activeTabPath && value !== undefined) {
311
+ setFileContents((prev) => ({ ...prev, [activeTabPath]: value }));
312
+ autoSaveRef.current.handleChange(activeTabPath, value);
313
+ }
314
+ }, [activeTabPath]);
315
+
316
+ // No project selected
317
+ if (!projectId) {
318
+ return (
319
+ <div className="files-view__empty">
320
+ <p className="text-content-muted">Select a project to browse files</p>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ // Workspace error (no git repo connected)
326
+ if (treeError) {
327
+ return (
328
+ <div className="files-view__empty">
329
+ <p className="text-content-muted">{treeError}</p>
330
+ </div>
331
+ );
332
+ }
333
+
334
+ const activeTab = tabs.find((t) => t.path === activeTabPath) || null;
335
+ const activeContent = activeTabPath ? (fileContents[activeTabPath] ?? '') : '';
336
+
337
+ return (
338
+ <div className="files-view" ref={containerRef}>
339
+ {/* File tree sidebar */}
340
+ <div className="files-view__tree" style={{ width: `${treeWidth}px`, minWidth: `${treeWidth}px` }}>
341
+ <div className="files-view__tree-header">
342
+ <span className="text-[11px] uppercase tracking-wider font-medium text-content-muted">Explorer</span>
343
+ </div>
344
+ <div className="files-view__tree-content">
345
+ {tree.length === 0 ? (
346
+ <p className="text-content-muted text-xs p-3">No files found</p>
347
+ ) : (
348
+ <FileTree
349
+ tree={tree}
350
+ callbacks={treeCallbacks}
351
+ selectedPath={activeTabPath}
352
+ expandedPaths={expandedPaths}
353
+ onToggleExpand={handleToggleExpand}
354
+ />
355
+ )}
356
+ </div>
357
+ </div>
358
+
359
+ {/* Resize handle */}
360
+ <div className="files-view__resize-handle" onMouseDown={handleResizeStart} />
361
+
362
+ {/* Editor area */}
363
+ <div className="files-view__editor">
364
+ <EditorTabBar
365
+ tabs={tabs}
366
+ activeTabPath={activeTabPath}
367
+ dirtyPaths={autoSave.dirtyPaths}
368
+ onSelectTab={handleSelectTab}
369
+ onCloseTab={handleCloseTab}
370
+ />
371
+ {activeTab ? (
372
+ <div className="files-view__editor-container">
373
+ {loading && (
374
+ <div className="files-view__loading">
375
+ <span className="text-content-muted text-sm">Loading...</span>
376
+ </div>
377
+ )}
378
+ <Editor
379
+ language={activeTab.language}
380
+ value={activeContent}
381
+ theme={theme === 'dark' ? 'vs-dark' : 'vs'}
382
+ path={activeTab.path}
383
+ onChange={handleEditorChange}
384
+ onMount={handleEditorMount}
385
+ options={{
386
+ minimap: { enabled: true },
387
+ fontSize: 13,
388
+ lineNumbers: 'on',
389
+ scrollBeyondLastLine: false,
390
+ automaticLayout: true,
391
+ tabSize: 2,
392
+ wordWrap: 'off',
393
+ }}
394
+ />
395
+ </div>
396
+ ) : (
397
+ <div className="files-view__empty-editor">
398
+ <p className="text-content-muted text-sm">Open a file from the explorer to start editing</p>
399
+ </div>
400
+ )}
401
+ </div>
402
+ </div>
403
+ );
404
+ };