@assistkick/create 1.7.0 → 1.8.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 (200) 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 +43 -4
  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 +154 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -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 +28 -78
  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 +222 -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 +456 -16
  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/KanbanView.tsx +202 -171
  62. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  63. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  64. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  65. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  66. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  67. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  68. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  69. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  105. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  107. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  108. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  109. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  110. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  111. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  112. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  113. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  114. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  119. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  120. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  121. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  129. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  130. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  131. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  132. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  133. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  134. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  151. package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
  152. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  153. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  154. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  155. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  156. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
  157. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  159. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  160. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  161. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  162. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  163. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  164. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  165. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  166. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  167. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  168. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  169. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  170. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  171. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  172. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  173. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  174. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  175. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  176. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  177. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  178. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  179. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  180. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  181. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  182. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  183. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  184. package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
  185. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  186. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  187. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  188. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  189. package/templates/skills/product-system/graph.json +1890 -0
  190. package/templates/skills/product-system/kanban.json +304 -0
  191. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  192. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  193. package/templates/skills/product-system/nodes/data_001.md +51 -0
  194. package/templates/skills/product-system/nodes/data_002.md +40 -0
  195. package/templates/skills/product-system/nodes/data_004.md +38 -0
  196. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  197. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  198. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  199. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  200. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from 'react';
2
2
  import { apiClient } from '../api/client';
3
- import { COLUMNS, PIPELINE_STATUS_LABELS } from '../constants/graph';
3
+ import { COLUMNS, WORKFLOW_STATUS_LABELS } from '../constants/graph';
4
4
  import { useToast } from '../hooks/useToast';
5
5
  import { KanbanCard } from './ds/KanbanCard';
6
+ import { WorkflowMonitorModal } from './workflow/WorkflowMonitorModal';
6
7
 
7
8
 
8
9
  interface KanbanViewProps {
@@ -17,6 +18,10 @@ interface CardData {
17
18
  name: string;
18
19
  completeness: number;
19
20
  kind: string;
21
+ nodeType: string;
22
+ featureType: string | null;
23
+ isEpicChild: boolean;
24
+ epicParentId: string | null;
20
25
  rejectionCount: number;
21
26
  notes: any[];
22
27
  reviews: any[];
@@ -25,56 +30,39 @@ interface CardData {
25
30
  movedAt: string | null;
26
31
  }
27
32
 
28
- const ALL_COLUMNS = COLUMNS.map(c => c.id);
29
-
30
- const STAGE_TABS = [
31
- { key: 'in_progress', label: 'Dev' },
32
- { key: 'in_review', label: 'Review' },
33
- { key: 'qa', label: 'QA' },
34
- ] as const;
35
-
36
- const hasStageData = (stage: any): boolean => {
37
- if (!stage) return false;
38
- const tc = stage.toolCalls;
39
- return (tc && tc.total > 0)
40
- || stage.numTurns != null
41
- || stage.costUsd != null
42
- || stage.usage != null
43
- || stage.stopReason != null;
44
- };
45
-
46
- const formatContextFill = (lastTurnUsage: any, contextWindow: number | null): string => {
47
- if (!lastTurnUsage) return '';
48
- const fill = (lastTurnUsage.input_tokens || 0)
49
- + (lastTurnUsage.cache_creation_input_tokens || 0)
50
- + (lastTurnUsage.cache_read_input_tokens || 0);
51
- const denom = contextWindow || 200_000;
52
- const pct = Math.round((fill / denom) * 100);
53
- return `${pct}%`;
54
- };
55
-
56
- const formatCost = (costUsd: number | null): string => {
57
- if (costUsd == null) return '';
58
- return `$${costUsd.toFixed(4)}`;
59
- };
60
-
61
- const formatModel = (model: string | null): string => {
62
- if (!model) return '';
63
- // Shorten long model names: "claude-sonnet-4-20250514" → "sonnet-4"
64
- const match = model.match(/(opus|sonnet|haiku)-(\d[\d.]*)/);
65
- return match ? `${match[1]}-${match[2]}` : model;
66
- };
33
+ interface WorkflowStatus {
34
+ executionId?: string;
35
+ status: string;
36
+ featureId: string;
37
+ currentNodeId?: string | null;
38
+ currentNodeType?: string | null;
39
+ currentAgentName?: string | null;
40
+ error?: string | null;
41
+ completedNodes?: string[];
42
+ failedNodes?: string[];
43
+ pendingNodes?: string[];
44
+ }
45
+
46
+ interface WorkflowListItem {
47
+ id: string;
48
+ name: string;
49
+ featureType: string | null;
50
+ isDefault: number;
51
+ }
67
52
 
68
53
  export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
69
54
  const [kanbanData, setKanbanData] = useState<any>(null);
70
55
  const [error, setError] = useState<string | null>(null);
71
56
  const [draggedCard, setDraggedCard] = useState<string | null>(null);
72
57
  const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
73
- const [pipelineStatuses, setPipelineStatuses] = useState<Record<string, any>>({});
58
+ const [workflowStatuses, setWorkflowStatuses] = useState<Record<string, WorkflowStatus>>({});
74
59
  const [playAllRunning, setPlayAllRunning] = useState(false);
75
60
  const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
61
+ const [playAllEpicId, setPlayAllEpicId] = useState<string | null>(null);
76
62
  const [copiedId, setCopiedId] = useState<string | null>(null);
77
- const [activeStageTab, setActiveStageTab] = useState<Record<string, string>>({});
63
+ const [availableWorkflows, setAvailableWorkflows] = useState<WorkflowListItem[]>([]);
64
+ const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);
65
+ const [monitorTarget, setMonitorTarget] = useState<{ featureId: string; featureName: string } | null>(null);
78
66
  const { showToast } = useToast();
79
67
 
80
68
  const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
@@ -94,10 +82,16 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
94
82
  }
95
83
  }, [projectId]);
96
84
 
85
+ // Load available workflows for the dropdown
86
+ useEffect(() => {
87
+ apiClient.fetchWorkflows(projectId ?? undefined).then(data => {
88
+ setAvailableWorkflows(data.workflows || []);
89
+ }).catch(() => { /* ignore */ });
90
+ }, [projectId]);
91
+
97
92
  useEffect(() => {
98
93
  fetchKanban();
99
94
  return () => {
100
- // Cleanup all pollers
101
95
  for (const intervalId of pollersRef.current.values()) {
102
96
  clearInterval(intervalId);
103
97
  }
@@ -111,18 +105,33 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
111
105
 
112
106
  const getCardsForColumn = useCallback((columnId: string): CardData[] => {
113
107
  if (!kanbanData || !graphData) return [];
114
- const featureNodes = new Map<string, any>();
108
+ const kanbanNodes = new Map<string, any>();
115
109
  graphData.nodes
116
- .filter((n: any) => n.type === 'feature')
117
- .forEach((n: any) => featureNodes.set(n.id, n));
110
+ .filter((n: any) => n.type === 'feature' || n.type === 'epic')
111
+ .forEach((n: any) => kanbanNodes.set(n.id, n));
112
+
113
+ // Build epic → children map from backend-provided epic_parent_id
114
+ const epicChildrenMap = new Map<string, Set<string>>();
115
+ for (const [id, entry] of Object.entries(kanbanData) as [string, any][]) {
116
+ if (entry.epic_parent_id) {
117
+ if (!epicChildrenMap.has(entry.epic_parent_id)) {
118
+ epicChildrenMap.set(entry.epic_parent_id, new Set());
119
+ }
120
+ epicChildrenMap.get(entry.epic_parent_id)!.add(id);
121
+ }
122
+ }
118
123
 
119
124
  const cards: CardData[] = Object.entries(kanbanData)
120
- .filter(([id, entry]: [string, any]) => entry.column === columnId && featureNodes.has(id))
125
+ .filter(([id, entry]: [string, any]) => entry.column === columnId && kanbanNodes.has(id))
121
126
  .map(([id, entry]: [string, any]) => ({
122
127
  id,
123
- name: featureNodes.get(id).name,
124
- completeness: featureNodes.get(id).completeness,
125
- kind: featureNodes.get(id).kind || 'new',
128
+ name: kanbanNodes.get(id).name,
129
+ completeness: kanbanNodes.get(id).completeness,
130
+ kind: kanbanNodes.get(id).kind || 'new',
131
+ nodeType: kanbanNodes.get(id).type || 'feature',
132
+ featureType: kanbanNodes.get(id).feature_type || null,
133
+ isEpicChild: false,
134
+ epicParentId: entry.epic_parent_id || null,
126
135
  rejectionCount: entry.rejection_count || 0,
127
136
  notes: entry.notes || [],
128
137
  reviews: entry.reviews || [],
@@ -139,22 +148,45 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
139
148
  cards.sort((a, b) => (a.movedAt || '').localeCompare(b.movedAt || ''));
140
149
  }
141
150
 
142
- return cards;
151
+ // Sort-order-independent grouping: epics first, then their children, then ungrouped
152
+ const epicCards = cards.filter(c => c.nodeType === 'epic');
153
+ const childIds = new Set<string>();
154
+ const grouped: CardData[] = [];
155
+
156
+ for (const epic of epicCards) {
157
+ grouped.push(epic);
158
+ const children = epicChildrenMap.get(epic.id);
159
+ if (children) {
160
+ const childCards = cards.filter(c => children.has(c.id));
161
+ for (const child of childCards) {
162
+ child.isEpicChild = true;
163
+ grouped.push(child);
164
+ childIds.add(child.id);
165
+ }
166
+ }
167
+ }
168
+
169
+ // Add ungrouped cards (not an epic and not an epic child)
170
+ for (const card of cards) {
171
+ if (card.nodeType !== 'epic' && !childIds.has(card.id)) {
172
+ grouped.push(card);
173
+ }
174
+ }
175
+
176
+ return grouped;
143
177
  }, [kanbanData, graphData]);
144
178
 
145
- const getPipelineBadgeText = (status: string, rejectionCount: number) => {
146
- const label = (PIPELINE_STATUS_LABELS as any)[status] || status;
147
- if (rejectionCount >= 1) return `Retry ${rejectionCount + 1}`;
148
- return label;
179
+ const getStatusBadgeText = (status: string) => {
180
+ return (WORKFLOW_STATUS_LABELS as any)[status] || status;
149
181
  };
150
182
 
151
- const startPipelinePolling = useCallback((featureId: string) => {
183
+ const startWorkflowPolling = useCallback((featureId: string) => {
152
184
  if (pollersRef.current.has(featureId)) return;
153
185
  const intervalId = setInterval(async () => {
154
186
  try {
155
- const status = await apiClient.getPipelineStatus(featureId);
156
- setPipelineStatuses(prev => ({ ...prev, [featureId]: status }));
157
- if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
187
+ const status: WorkflowStatus = await apiClient.getWorkflowStatus(featureId);
188
+ setWorkflowStatuses(prev => ({ ...prev, [featureId]: status }));
189
+ if (['idle', 'completed', 'failed'].includes(status.status)) {
158
190
  clearInterval(pollersRef.current.get(featureId)!);
159
191
  pollersRef.current.delete(featureId);
160
192
  await fetchKanban();
@@ -167,35 +199,22 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
167
199
  pollersRef.current.set(featureId, intervalId);
168
200
  }, [fetchKanban]);
169
201
 
170
- // Fetch initial pipeline statuses for all cards
202
+ // Fetch initial workflow statuses for all cards
171
203
  useEffect(() => {
172
204
  if (!kanbanData || !graphData) return;
173
205
  const featureIds = Object.keys(kanbanData);
174
206
  featureIds.forEach(async (id) => {
175
207
  try {
176
- const status = await apiClient.getPipelineStatus(id);
208
+ const status: WorkflowStatus = await apiClient.getWorkflowStatus(id);
177
209
  if (status.status !== 'idle') {
178
- setPipelineStatuses(prev => ({ ...prev, [id]: status }));
179
- if (!['completed', 'failed', 'blocked', 'interrupted'].includes(status.status)) {
180
- startPipelinePolling(id);
210
+ setWorkflowStatuses(prev => ({ ...prev, [id]: status }));
211
+ if (!['completed', 'failed'].includes(status.status)) {
212
+ startWorkflowPolling(id);
181
213
  }
182
214
  }
183
215
  } catch { /* ignore */ }
184
216
  });
185
- }, [kanbanData, graphData, startPipelinePolling]);
186
-
187
- const checkDependencies = useCallback((featureId: string) => {
188
- if (!graphData || !kanbanData) return { blocked: false, blockedBy: [] as string[] };
189
- const deps = graphData.edges
190
- .filter((e: any) => e.from === featureId && e.relation === 'depends_on')
191
- .map((e: any) => e.to)
192
- .filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
193
- const blockedBy = deps.filter((depId: string) => {
194
- const entry = kanbanData[depId];
195
- return !entry || entry.column !== 'done';
196
- });
197
- return { blocked: blockedBy.length > 0, blockedBy };
198
- }, [graphData, kanbanData]);
217
+ }, [kanbanData, graphData, startWorkflowPolling]);
199
218
 
200
219
  // Drag handlers
201
220
  const handleDragStart = (e: React.DragEvent, featureId: string) => {
@@ -237,34 +256,25 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
237
256
  }
238
257
  };
239
258
 
240
- const handleStartPipeline = async (featureId: string) => {
259
+ const handleStartWorkflow = async (featureId: string, workflowId?: string) => {
241
260
  try {
242
- await apiClient.startPipeline(featureId);
243
- startPipelinePolling(featureId);
261
+ await apiClient.startWorkflow(featureId, workflowId, projectId ?? undefined);
262
+ startWorkflowPolling(featureId);
244
263
  await fetchKanban();
245
264
  } catch (err: any) {
246
- console.error('Failed to start pipeline:', err);
247
- showToast(err?.message || 'Failed to start pipeline');
265
+ console.error('Failed to start workflow:', err);
266
+ showToast(err?.message || 'Failed to start workflow');
248
267
  }
249
268
  };
250
269
 
251
- const handleResumePipeline = async (featureId: string) => {
270
+ const handleResumeWorkflow = async (featureId: string) => {
252
271
  try {
253
- await apiClient.resumePipeline(featureId);
254
- startPipelinePolling(featureId);
272
+ await apiClient.resumeWorkflow(featureId);
273
+ startWorkflowPolling(featureId);
255
274
  await fetchKanban();
256
275
  } catch (err: any) {
257
- console.error('Failed to resume pipeline:', err);
258
- showToast(err?.message || 'Failed to resume pipeline');
259
- }
260
- };
261
-
262
- const handleUnblock = async (featureId: string) => {
263
- try {
264
- await apiClient.unblockCard(featureId);
265
- await fetchKanban();
266
- } catch (err) {
267
- console.error('Failed to unblock card:', err);
276
+ console.error('Failed to resume workflow:', err);
277
+ showToast(err?.message || 'Failed to resume workflow');
268
278
  }
269
279
  };
270
280
 
@@ -277,6 +287,18 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
277
287
  } catch { /* ignore */ }
278
288
  };
279
289
 
290
+ const handlePlayDropdown = (featureId: string) => {
291
+ setDropdownOpen(prev => prev === featureId ? null : featureId);
292
+ };
293
+
294
+ // Close dropdown when clicking outside
295
+ useEffect(() => {
296
+ if (!dropdownOpen) return;
297
+ const handler = () => setDropdownOpen(null);
298
+ document.addEventListener('click', handler);
299
+ return () => document.removeEventListener('click', handler);
300
+ }, [dropdownOpen]);
301
+
280
302
  // Orchestrator status polling
281
303
  const startOrchestratorPolling = useCallback(() => {
282
304
  if (orchestratorPollerRef.current) return;
@@ -285,14 +307,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
285
307
  const status = await apiClient.getOrchestratorStatus();
286
308
  setPlayAllRunning(status.active);
287
309
  setPlayAllCurrentFeature(status.currentFeatureId);
310
+ setPlayAllEpicId(status.epicId || null);
288
311
  if (status.active) {
289
312
  await fetchKanban();
290
313
  } else {
291
- // Orchestrator stopped — clean up poller
292
314
  if (orchestratorPollerRef.current) {
293
315
  clearInterval(orchestratorPollerRef.current);
294
316
  orchestratorPollerRef.current = null;
295
317
  }
318
+ setPlayAllEpicId(null);
296
319
  await fetchKanban();
297
320
  }
298
321
  } catch {
@@ -302,6 +325,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
302
325
  }
303
326
  setPlayAllRunning(false);
304
327
  setPlayAllCurrentFeature(null);
328
+ setPlayAllEpicId(null);
305
329
  }
306
330
  }, 5000);
307
331
  }, [fetchKanban]);
@@ -311,6 +335,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
311
335
  apiClient.getOrchestratorStatus().then(status => {
312
336
  setPlayAllRunning(status.active);
313
337
  setPlayAllCurrentFeature(status.currentFeatureId);
338
+ setPlayAllEpicId(status.epicId || null);
314
339
  if (status.active) {
315
340
  startOrchestratorPolling();
316
341
  }
@@ -325,10 +350,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
325
350
  }, [startOrchestratorPolling]);
326
351
 
327
352
  // Play All — thin wrapper around backend orchestrator
328
- const startPlayAll = async () => {
353
+ const startPlayAll = async (epicId?: string) => {
329
354
  if (playAllRunning) return;
330
355
  try {
331
- await apiClient.startPlayAll(projectId ?? undefined);
356
+ await apiClient.startPlayAll(projectId ?? undefined, epicId);
332
357
  setPlayAllRunning(true);
333
358
  setPlayAllCurrentFeature(null);
334
359
  startOrchestratorPolling();
@@ -352,7 +377,6 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
352
377
  <div className="flex h-full w-full gap-2 overflow-x-auto">
353
378
  {COLUMNS.map(col => {
354
379
  const cards = getCardsForColumn(col.id);
355
- const sourceColumn = draggedCard ? kanbanData?.[draggedCard]?.column : null;
356
380
  const isDragOver = dragOverColumn === col.id;
357
381
 
358
382
  return (
@@ -376,7 +400,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
376
400
  <button
377
401
  className="flex h-6 w-6 items-center justify-center rounded-full border border-accent text-accent text-[9px] tracking-[-2px] pl-0.5 hover:bg-accent hover:text-surface transition-colors cursor-pointer"
378
402
  title="Start automated development for all TODO features"
379
- onClick={startPlayAll}
403
+ onClick={() => startPlayAll()}
380
404
  >
381
405
  {'\u25B6\u25B6'}
382
406
  </button>
@@ -396,48 +420,12 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
396
420
  >
397
421
  {cards.map(card => {
398
422
  const pct = Math.round((card.completeness || 0) * 100);
399
- const pStatus = pipelineStatuses[card.id];
400
- const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
401
- const pipelineStatusValue = pStatus?.status ?? 'idle';
423
+ const wStatus = workflowStatuses[card.id];
424
+ const workflowStatusValue = wStatus?.status ?? 'idle';
425
+ const isActive = workflowStatusValue === 'running';
402
426
  const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
403
427
  && !isActive
404
- && ['interrupted', 'failed', 'idle'].includes(pipelineStatusValue);
405
-
406
- // Build tool calls + meta from stage stats or flat toolCalls
407
- let toolCalls: Record<string, number> | undefined;
408
- let metaPills: string[] | undefined;
409
- let stopReason: string | undefined;
410
-
411
- const ss = pStatus?.stageStats;
412
- const availableTabs = ss
413
- ? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
414
- : [];
415
-
416
- if (availableTabs.length > 0) {
417
- const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
418
- const stage = ss[currentTab];
419
- const tc = stage?.toolCalls;
420
- if (tc && tc.total > 0) {
421
- toolCalls = {};
422
- if (tc.read > 0) toolCalls['Read'] = tc.read;
423
- if (tc.bash > 0) toolCalls['Tools'] = tc.bash;
424
- if (tc.write > 0) toolCalls['Write'] = tc.write;
425
- if (tc.edit > 0) toolCalls['Edit'] = tc.edit;
426
- }
427
- const pills: string[] = [];
428
- if (stage?.lastTurnUsage) pills.push(`Ctx ${formatContextFill(stage.lastTurnUsage, stage.contextWindow)}`);
429
- if (stage?.numTurns != null) pills.push(`${stage.numTurns}t`);
430
- if (stage?.costUsd != null) pills.push(formatCost(stage.costUsd));
431
- if (stage?.model) pills.push(formatModel(stage.model));
432
- if (pills.length > 0) metaPills = pills;
433
- if (stage?.stopReason) stopReason = stage.stopReason;
434
- } else if (pStatus?.toolCalls?.total > 0) {
435
- toolCalls = {};
436
- if (pStatus.toolCalls.read > 0) toolCalls['Read'] = pStatus.toolCalls.read;
437
- if (pStatus.toolCalls.bash > 0) toolCalls['Tools'] = pStatus.toolCalls.bash;
438
- if (pStatus.toolCalls.write > 0) toolCalls['Write'] = pStatus.toolCalls.write;
439
- if (pStatus.toolCalls.edit > 0) toolCalls['Edit'] = pStatus.toolCalls.edit;
440
- }
428
+ && workflowStatusValue === 'failed';
441
429
 
442
430
  const issueLabel = card.notes.length > 0
443
431
  ? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
@@ -446,35 +434,69 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
446
434
  : 'No issues';
447
435
 
448
436
  return (
449
- <KanbanCard
450
- key={card.id}
451
- id={card.id}
452
- name={card.name}
453
- pct={pct}
454
- kind={card.kind}
455
- rejectionCount={card.rejectionCount}
456
- blocked={card.devBlocked}
457
- pipeline={pipelineStatusValue}
458
- pipelineLabel={pStatus && pStatus.status !== 'idle' ? getPipelineBadgeText(pStatus.status, card.rejectionCount) : undefined}
459
- toolCalls={toolCalls}
460
- meta={metaPills}
461
- stopReason={stopReason}
462
- issueCount={card.notes.length}
463
- issueLabel={issueLabel}
464
- showPlay={card.column === 'todo' && !card.devBlocked}
465
- showResume={showResumeBtn}
466
- copied={copiedId === card.id}
467
- playAllActive={playAllCurrentFeature === card.id}
468
- onClick={() => onCardClick({ id: card.id, name: card.name })}
469
- onCopy={() => handleCopy(card)}
470
- onPlay={() => handleStartPipeline(card.id)}
471
- onResume={() => handleResumePipeline(card.id)}
472
- onUnblock={() => handleUnblock(card.id)}
473
- onIssuesClick={() => onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews)}
474
- draggable
475
- onDragStart={(e) => handleDragStart(e, card.id)}
476
- onDragEnd={handleDragEnd}
477
- />
437
+ <div key={card.id} className={['relative', card.isEpicChild ? 'ml-3' : ''].join(' ')}>
438
+ <KanbanCard
439
+ id={card.id}
440
+ name={card.name}
441
+ pct={pct}
442
+ kind={card.kind}
443
+ isEpic={card.nodeType === 'epic'}
444
+ rejectionCount={card.rejectionCount}
445
+ blocked={card.devBlocked}
446
+ pipeline={workflowStatusValue}
447
+ pipelineLabel={wStatus && wStatus.status !== 'idle' ? getStatusBadgeText(wStatus.status) : undefined}
448
+ issueCount={card.notes.length}
449
+ issueLabel={issueLabel}
450
+ showPlay={card.column === 'todo' && !card.devBlocked && !(card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id)}
451
+ showPlayDropdown={card.nodeType !== 'epic' && availableWorkflows.length > 1}
452
+ showStop={card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id}
453
+ showResume={showResumeBtn}
454
+ copied={copiedId === card.id}
455
+ playAllActive={playAllCurrentFeature === card.id || (card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id)}
456
+ agentName={isActive ? wStatus?.currentAgentName : null}
457
+ errorMessage={workflowStatusValue === 'failed' ? wStatus?.error : null}
458
+ onClick={() => onCardClick({ id: card.id, name: card.name })}
459
+ onCopy={() => handleCopy(card)}
460
+ onPlay={() => card.nodeType === 'epic' ? startPlayAll(card.id) : handleStartWorkflow(card.id)}
461
+ onPlayDropdown={() => handlePlayDropdown(card.id)}
462
+ onStop={stopPlayAll}
463
+ onResume={() => handleResumeWorkflow(card.id)}
464
+ onIssuesClick={() => onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews)}
465
+ onMonitorClick={() => setMonitorTarget({ featureId: card.id, featureName: card.name })}
466
+ draggable
467
+ onDragStart={(e) => handleDragStart(e, card.id)}
468
+ onDragEnd={handleDragEnd}
469
+ />
470
+ {/* Workflow selection dropdown — filtered by feature type */}
471
+ {dropdownOpen === card.id && (() => {
472
+ const cardFeatureType = card.featureType || 'code';
473
+ const filteredWorkflows = availableWorkflows.filter(wf =>
474
+ !wf.featureType || wf.featureType === cardFeatureType
475
+ );
476
+ return (
477
+ <div
478
+ className="absolute right-0 top-8 z-50 min-w-[180px] rounded-lg border border-edge bg-surface-raised shadow-xl shadow-black/20 py-1"
479
+ onClick={(e) => e.stopPropagation()}
480
+ >
481
+ {filteredWorkflows.map(wf => (
482
+ <button
483
+ key={wf.id}
484
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-content hover:bg-white/[0.08] transition-colors cursor-pointer"
485
+ onClick={() => {
486
+ setDropdownOpen(null);
487
+ handleStartWorkflow(card.id, wf.id);
488
+ }}
489
+ >
490
+ <span className="flex-1">{wf.name}</span>
491
+ {wf.isDefault === 1 && (
492
+ <span className="rounded-full bg-accent/15 px-1.5 py-0.5 text-[9px] font-bold text-accent">Default</span>
493
+ )}
494
+ </button>
495
+ ))}
496
+ </div>
497
+ );
498
+ })()}
499
+ </div>
478
500
  );
479
501
  })}
480
502
  </div>
@@ -482,6 +504,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
482
504
  );
483
505
  })}
484
506
 
507
+ {/* Workflow Monitor Modal */}
508
+ {monitorTarget && (
509
+ <WorkflowMonitorModal
510
+ featureId={monitorTarget.featureId}
511
+ featureName={monitorTarget.featureName}
512
+ isOpen={true}
513
+ onClose={() => setMonitorTarget(null)}
514
+ />
515
+ )}
485
516
  </div>
486
517
  );
487
518
  }
@@ -54,12 +54,12 @@ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
54
54
  }, [email, password, onLoginSuccess]);
55
55
 
56
56
  return (
57
- <div className="login-page">
58
- <div className="login-card">
59
- <div className="login-header">
60
- <span className="login-title">login</span>
57
+ <div className="flex items-center justify-center w-full h-screen bg-surface">
58
+ <div className="w-[360px] p-8 bg-surface-alt border border-edge rounded">
59
+ <div className="flex items-center justify-between mb-6">
60
+ <span className="font-mono text-base text-content tracking-wide">login</span>
61
61
  <button
62
- className="login-theme-toggle"
62
+ className="bg-transparent border border-edge rounded w-7 h-7 text-content-secondary text-sm cursor-pointer flex items-center justify-center transition-[border-color,color] duration-150 hover:border-content hover:text-content"
63
63
  onClick={toggleTheme}
64
64
  title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
65
65
  >
@@ -67,11 +67,11 @@ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
67
67
  </button>
68
68
  </div>
69
69
 
70
- <form className="login-form" onSubmit={handleSubmit}>
71
- <label className="login-label" htmlFor="login-email">email</label>
70
+ <form className="flex flex-col gap-2" onSubmit={handleSubmit}>
71
+ <label className="font-mono text-[11px] text-content-secondary mt-1" htmlFor="login-email">email</label>
72
72
  <input
73
73
  id="login-email"
74
- className="login-input"
74
+ className="bg-surface-raised border border-edge rounded text-content font-mono text-[13px] py-2 px-2.5 outline-none transition-[border-color] duration-150 focus:border-accent placeholder:text-content-muted disabled:opacity-60"
75
75
  type="email"
76
76
  value={email}
77
77
  onChange={e => setEmail(e.target.value)}
@@ -81,10 +81,10 @@ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
81
81
  disabled={submitting}
82
82
  />
83
83
 
84
- <label className="login-label" htmlFor="login-password">password</label>
84
+ <label className="font-mono text-[11px] text-content-secondary mt-1" htmlFor="login-password">password</label>
85
85
  <input
86
86
  id="login-password"
87
- className="login-input"
87
+ className="bg-surface-raised border border-edge rounded text-content font-mono text-[13px] py-2 px-2.5 outline-none transition-[border-color] duration-150 focus:border-accent placeholder:text-content-muted disabled:opacity-60"
88
88
  type="password"
89
89
  value={password}
90
90
  onChange={e => setPassword(e.target.value)}
@@ -93,10 +93,10 @@ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
93
93
  disabled={submitting}
94
94
  />
95
95
 
96
- {error && <div className="login-error">{error}</div>}
96
+ {error && <div className="font-mono text-[11px] text-error mt-1">{error}</div>}
97
97
 
98
98
  <button
99
- className="login-submit"
99
+ className="mt-3 bg-transparent border border-accent rounded text-accent font-mono text-[13px] py-2 px-3.5 cursor-pointer transition-[background,color] duration-150 hover:enabled:bg-accent hover:enabled:text-white disabled:opacity-50 disabled:cursor-not-allowed"
100
100
  type="submit"
101
101
  disabled={submitting}
102
102
  >
@@ -104,10 +104,10 @@ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
104
104
  </button>
105
105
  </form>
106
106
 
107
- <p className="login-register-link">
107
+ <p className="font-mono text-xs text-content-secondary mt-4 text-center">
108
108
  <Link to="/forgot-password">Forgot password?</Link>
109
109
  </p>
110
- <p className="login-register-link">
110
+ <p className="font-mono text-xs text-content-secondary mt-2 text-center">
111
111
  No account yet? <Link to="/register">Register</Link>
112
112
  </p>
113
113
  </div>