@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.
- package/dist/bin/create.js +0 -0
- package/package.json +9 -7
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +202 -171
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-developer/SKILL.md +3 -0
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- 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,
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 [
|
|
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 [
|
|
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
|
|
108
|
+
const kanbanNodes = new Map<string, any>();
|
|
115
109
|
graphData.nodes
|
|
116
|
-
.filter((n: any) => n.type === 'feature')
|
|
117
|
-
.forEach((n: any) =>
|
|
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 &&
|
|
125
|
+
.filter(([id, entry]: [string, any]) => entry.column === columnId && kanbanNodes.has(id))
|
|
121
126
|
.map(([id, entry]: [string, any]) => ({
|
|
122
127
|
id,
|
|
123
|
-
name:
|
|
124
|
-
completeness:
|
|
125
|
-
kind:
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
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
|
|
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.
|
|
156
|
-
|
|
157
|
-
if (['idle', 'completed', '
|
|
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
|
|
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.
|
|
208
|
+
const status: WorkflowStatus = await apiClient.getWorkflowStatus(id);
|
|
177
209
|
if (status.status !== 'idle') {
|
|
178
|
-
|
|
179
|
-
if (!['completed', 'failed'
|
|
180
|
-
|
|
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,
|
|
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
|
|
259
|
+
const handleStartWorkflow = async (featureId: string, workflowId?: string) => {
|
|
241
260
|
try {
|
|
242
|
-
await apiClient.
|
|
243
|
-
|
|
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
|
|
247
|
-
showToast(err?.message || 'Failed to start
|
|
265
|
+
console.error('Failed to start workflow:', err);
|
|
266
|
+
showToast(err?.message || 'Failed to start workflow');
|
|
248
267
|
}
|
|
249
268
|
};
|
|
250
269
|
|
|
251
|
-
const
|
|
270
|
+
const handleResumeWorkflow = async (featureId: string) => {
|
|
252
271
|
try {
|
|
253
|
-
await apiClient.
|
|
254
|
-
|
|
272
|
+
await apiClient.resumeWorkflow(featureId);
|
|
273
|
+
startWorkflowPolling(featureId);
|
|
255
274
|
await fetchKanban();
|
|
256
275
|
} catch (err: any) {
|
|
257
|
-
console.error('Failed to resume
|
|
258
|
-
showToast(err?.message || 'Failed to resume
|
|
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
|
|
400
|
-
const
|
|
401
|
-
const
|
|
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
|
-
&&
|
|
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
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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="
|
|
58
|
-
<div className="
|
|
59
|
-
<div className="
|
|
60
|
-
<span className="
|
|
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="
|
|
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="
|
|
71
|
-
<label className="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
96
|
+
{error && <div className="font-mono text-[11px] text-error mt-1">{error}</div>}
|
|
97
97
|
|
|
98
98
|
<button
|
|
99
|
-
className="
|
|
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="
|
|
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="
|
|
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>
|