@assistkick/create 1.6.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/package.json +2 -2
- 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/index.html +3 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +800 -11
- package/templates/assistkick-product-system/packages/frontend/package.json +11 -1
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +24 -7
- 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 +383 -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 +193 -64
- 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 +226 -291
- 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 +40 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +55 -115
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +155 -77
- 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 +270 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +37 -0
- 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 +207 -0
- 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/useGraph.ts +6 -21
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
- 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 +19 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
- 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/useGitModalStore.ts +14 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +90 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +336 -3632
- 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 +7 -1
- 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 +16 -5
- 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-bootstrap/SKILL.md +3 -3
- package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
- package/templates/skills/assistkick-debugger/SKILL.md +2 -2
- package/templates/skills/assistkick-developer/SKILL.md +6 -3
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/assistkick-interview/SKILL.md +2 -2
- 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,7 +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
|
+
import { KanbanCard } from './ds/KanbanCard';
|
|
6
|
+
import { WorkflowMonitorModal } from './workflow/WorkflowMonitorModal';
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
interface KanbanViewProps {
|
|
@@ -16,6 +18,10 @@ interface CardData {
|
|
|
16
18
|
name: string;
|
|
17
19
|
completeness: number;
|
|
18
20
|
kind: string;
|
|
21
|
+
nodeType: string;
|
|
22
|
+
featureType: string | null;
|
|
23
|
+
isEpicChild: boolean;
|
|
24
|
+
epicParentId: string | null;
|
|
19
25
|
rejectionCount: number;
|
|
20
26
|
notes: any[];
|
|
21
27
|
reviews: any[];
|
|
@@ -24,56 +30,39 @@ interface CardData {
|
|
|
24
30
|
movedAt: string | null;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!lastTurnUsage) return '';
|
|
47
|
-
const fill = (lastTurnUsage.input_tokens || 0)
|
|
48
|
-
+ (lastTurnUsage.cache_creation_input_tokens || 0)
|
|
49
|
-
+ (lastTurnUsage.cache_read_input_tokens || 0);
|
|
50
|
-
const denom = contextWindow || 200_000;
|
|
51
|
-
const pct = Math.round((fill / denom) * 100);
|
|
52
|
-
return `${pct}%`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const formatCost = (costUsd: number | null): string => {
|
|
56
|
-
if (costUsd == null) return '';
|
|
57
|
-
return `$${costUsd.toFixed(4)}`;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const formatModel = (model: string | null): string => {
|
|
61
|
-
if (!model) return '';
|
|
62
|
-
// Shorten long model names: "claude-sonnet-4-20250514" → "sonnet-4"
|
|
63
|
-
const match = model.match(/(opus|sonnet|haiku)-(\d[\d.]*)/);
|
|
64
|
-
return match ? `${match[1]}-${match[2]}` : model;
|
|
65
|
-
};
|
|
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
|
+
}
|
|
66
52
|
|
|
67
53
|
export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
|
|
68
54
|
const [kanbanData, setKanbanData] = useState<any>(null);
|
|
69
55
|
const [error, setError] = useState<string | null>(null);
|
|
70
56
|
const [draggedCard, setDraggedCard] = useState<string | null>(null);
|
|
71
57
|
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
|
|
72
|
-
const [
|
|
58
|
+
const [workflowStatuses, setWorkflowStatuses] = useState<Record<string, WorkflowStatus>>({});
|
|
73
59
|
const [playAllRunning, setPlayAllRunning] = useState(false);
|
|
74
60
|
const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
|
|
61
|
+
const [playAllEpicId, setPlayAllEpicId] = useState<string | null>(null);
|
|
75
62
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
76
|
-
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);
|
|
77
66
|
const { showToast } = useToast();
|
|
78
67
|
|
|
79
68
|
const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
|
@@ -93,10 +82,16 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
93
82
|
}
|
|
94
83
|
}, [projectId]);
|
|
95
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
|
+
|
|
96
92
|
useEffect(() => {
|
|
97
93
|
fetchKanban();
|
|
98
94
|
return () => {
|
|
99
|
-
// Cleanup all pollers
|
|
100
95
|
for (const intervalId of pollersRef.current.values()) {
|
|
101
96
|
clearInterval(intervalId);
|
|
102
97
|
}
|
|
@@ -110,18 +105,33 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
110
105
|
|
|
111
106
|
const getCardsForColumn = useCallback((columnId: string): CardData[] => {
|
|
112
107
|
if (!kanbanData || !graphData) return [];
|
|
113
|
-
const
|
|
108
|
+
const kanbanNodes = new Map<string, any>();
|
|
114
109
|
graphData.nodes
|
|
115
|
-
.filter((n: any) => n.type === 'feature')
|
|
116
|
-
.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
|
+
}
|
|
117
123
|
|
|
118
124
|
const cards: CardData[] = Object.entries(kanbanData)
|
|
119
|
-
.filter(([id, entry]: [string, any]) => entry.column === columnId &&
|
|
125
|
+
.filter(([id, entry]: [string, any]) => entry.column === columnId && kanbanNodes.has(id))
|
|
120
126
|
.map(([id, entry]: [string, any]) => ({
|
|
121
127
|
id,
|
|
122
|
-
name:
|
|
123
|
-
completeness:
|
|
124
|
-
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,
|
|
125
135
|
rejectionCount: entry.rejection_count || 0,
|
|
126
136
|
notes: entry.notes || [],
|
|
127
137
|
reviews: entry.reviews || [],
|
|
@@ -138,22 +148,45 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
138
148
|
cards.sort((a, b) => (a.movedAt || '').localeCompare(b.movedAt || ''));
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
|
|
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;
|
|
142
177
|
}, [kanbanData, graphData]);
|
|
143
178
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (rejectionCount >= 1) return `Retry ${rejectionCount + 1}`;
|
|
147
|
-
return label;
|
|
179
|
+
const getStatusBadgeText = (status: string) => {
|
|
180
|
+
return (WORKFLOW_STATUS_LABELS as any)[status] || status;
|
|
148
181
|
};
|
|
149
182
|
|
|
150
|
-
const
|
|
183
|
+
const startWorkflowPolling = useCallback((featureId: string) => {
|
|
151
184
|
if (pollersRef.current.has(featureId)) return;
|
|
152
185
|
const intervalId = setInterval(async () => {
|
|
153
186
|
try {
|
|
154
|
-
const status = await apiClient.
|
|
155
|
-
|
|
156
|
-
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)) {
|
|
157
190
|
clearInterval(pollersRef.current.get(featureId)!);
|
|
158
191
|
pollersRef.current.delete(featureId);
|
|
159
192
|
await fetchKanban();
|
|
@@ -166,35 +199,22 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
166
199
|
pollersRef.current.set(featureId, intervalId);
|
|
167
200
|
}, [fetchKanban]);
|
|
168
201
|
|
|
169
|
-
// Fetch initial
|
|
202
|
+
// Fetch initial workflow statuses for all cards
|
|
170
203
|
useEffect(() => {
|
|
171
204
|
if (!kanbanData || !graphData) return;
|
|
172
205
|
const featureIds = Object.keys(kanbanData);
|
|
173
206
|
featureIds.forEach(async (id) => {
|
|
174
207
|
try {
|
|
175
|
-
const status = await apiClient.
|
|
208
|
+
const status: WorkflowStatus = await apiClient.getWorkflowStatus(id);
|
|
176
209
|
if (status.status !== 'idle') {
|
|
177
|
-
|
|
178
|
-
if (!['completed', 'failed'
|
|
179
|
-
|
|
210
|
+
setWorkflowStatuses(prev => ({ ...prev, [id]: status }));
|
|
211
|
+
if (!['completed', 'failed'].includes(status.status)) {
|
|
212
|
+
startWorkflowPolling(id);
|
|
180
213
|
}
|
|
181
214
|
}
|
|
182
215
|
} catch { /* ignore */ }
|
|
183
216
|
});
|
|
184
|
-
}, [kanbanData, graphData,
|
|
185
|
-
|
|
186
|
-
const checkDependencies = useCallback((featureId: string) => {
|
|
187
|
-
if (!graphData || !kanbanData) return { blocked: false, blockedBy: [] as string[] };
|
|
188
|
-
const deps = graphData.edges
|
|
189
|
-
.filter((e: any) => e.from === featureId && e.relation === 'depends_on')
|
|
190
|
-
.map((e: any) => e.to)
|
|
191
|
-
.filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
|
|
192
|
-
const blockedBy = deps.filter((depId: string) => {
|
|
193
|
-
const entry = kanbanData[depId];
|
|
194
|
-
return !entry || entry.column !== 'done';
|
|
195
|
-
});
|
|
196
|
-
return { blocked: blockedBy.length > 0, blockedBy };
|
|
197
|
-
}, [graphData, kanbanData]);
|
|
217
|
+
}, [kanbanData, graphData, startWorkflowPolling]);
|
|
198
218
|
|
|
199
219
|
// Drag handlers
|
|
200
220
|
const handleDragStart = (e: React.DragEvent, featureId: string) => {
|
|
@@ -236,34 +256,25 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
236
256
|
}
|
|
237
257
|
};
|
|
238
258
|
|
|
239
|
-
const
|
|
259
|
+
const handleStartWorkflow = async (featureId: string, workflowId?: string) => {
|
|
240
260
|
try {
|
|
241
|
-
await apiClient.
|
|
242
|
-
|
|
261
|
+
await apiClient.startWorkflow(featureId, workflowId, projectId ?? undefined);
|
|
262
|
+
startWorkflowPolling(featureId);
|
|
243
263
|
await fetchKanban();
|
|
244
264
|
} catch (err: any) {
|
|
245
|
-
console.error('Failed to start
|
|
246
|
-
showToast(err?.message || 'Failed to start
|
|
265
|
+
console.error('Failed to start workflow:', err);
|
|
266
|
+
showToast(err?.message || 'Failed to start workflow');
|
|
247
267
|
}
|
|
248
268
|
};
|
|
249
269
|
|
|
250
|
-
const
|
|
270
|
+
const handleResumeWorkflow = async (featureId: string) => {
|
|
251
271
|
try {
|
|
252
|
-
await apiClient.
|
|
253
|
-
|
|
272
|
+
await apiClient.resumeWorkflow(featureId);
|
|
273
|
+
startWorkflowPolling(featureId);
|
|
254
274
|
await fetchKanban();
|
|
255
275
|
} catch (err: any) {
|
|
256
|
-
console.error('Failed to resume
|
|
257
|
-
showToast(err?.message || 'Failed to resume
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const handleUnblock = async (featureId: string) => {
|
|
262
|
-
try {
|
|
263
|
-
await apiClient.unblockCard(featureId);
|
|
264
|
-
await fetchKanban();
|
|
265
|
-
} catch (err) {
|
|
266
|
-
console.error('Failed to unblock card:', err);
|
|
276
|
+
console.error('Failed to resume workflow:', err);
|
|
277
|
+
showToast(err?.message || 'Failed to resume workflow');
|
|
267
278
|
}
|
|
268
279
|
};
|
|
269
280
|
|
|
@@ -276,6 +287,18 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
276
287
|
} catch { /* ignore */ }
|
|
277
288
|
};
|
|
278
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
|
+
|
|
279
302
|
// Orchestrator status polling
|
|
280
303
|
const startOrchestratorPolling = useCallback(() => {
|
|
281
304
|
if (orchestratorPollerRef.current) return;
|
|
@@ -284,14 +307,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
284
307
|
const status = await apiClient.getOrchestratorStatus();
|
|
285
308
|
setPlayAllRunning(status.active);
|
|
286
309
|
setPlayAllCurrentFeature(status.currentFeatureId);
|
|
310
|
+
setPlayAllEpicId(status.epicId || null);
|
|
287
311
|
if (status.active) {
|
|
288
312
|
await fetchKanban();
|
|
289
313
|
} else {
|
|
290
|
-
// Orchestrator stopped — clean up poller
|
|
291
314
|
if (orchestratorPollerRef.current) {
|
|
292
315
|
clearInterval(orchestratorPollerRef.current);
|
|
293
316
|
orchestratorPollerRef.current = null;
|
|
294
317
|
}
|
|
318
|
+
setPlayAllEpicId(null);
|
|
295
319
|
await fetchKanban();
|
|
296
320
|
}
|
|
297
321
|
} catch {
|
|
@@ -301,6 +325,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
301
325
|
}
|
|
302
326
|
setPlayAllRunning(false);
|
|
303
327
|
setPlayAllCurrentFeature(null);
|
|
328
|
+
setPlayAllEpicId(null);
|
|
304
329
|
}
|
|
305
330
|
}, 5000);
|
|
306
331
|
}, [fetchKanban]);
|
|
@@ -310,6 +335,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
310
335
|
apiClient.getOrchestratorStatus().then(status => {
|
|
311
336
|
setPlayAllRunning(status.active);
|
|
312
337
|
setPlayAllCurrentFeature(status.currentFeatureId);
|
|
338
|
+
setPlayAllEpicId(status.epicId || null);
|
|
313
339
|
if (status.active) {
|
|
314
340
|
startOrchestratorPolling();
|
|
315
341
|
}
|
|
@@ -324,10 +350,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
324
350
|
}, [startOrchestratorPolling]);
|
|
325
351
|
|
|
326
352
|
// Play All — thin wrapper around backend orchestrator
|
|
327
|
-
const startPlayAll = async () => {
|
|
353
|
+
const startPlayAll = async (epicId?: string) => {
|
|
328
354
|
if (playAllRunning) return;
|
|
329
355
|
try {
|
|
330
|
-
await apiClient.startPlayAll(projectId ?? undefined);
|
|
356
|
+
await apiClient.startPlayAll(projectId ?? undefined, epicId);
|
|
331
357
|
setPlayAllRunning(true);
|
|
332
358
|
setPlayAllCurrentFeature(null);
|
|
333
359
|
startOrchestratorPolling();
|
|
@@ -344,38 +370,49 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
344
370
|
}
|
|
345
371
|
};
|
|
346
372
|
|
|
347
|
-
if (error) return <div className="error-
|
|
348
|
-
if (!kanbanData) return <div className="
|
|
373
|
+
if (error) return <div className="p-4 text-error text-[13px]">{error}</div>;
|
|
374
|
+
if (!kanbanData) return <div className="p-4 text-content-muted text-[13px]">Loading...</div>;
|
|
349
375
|
|
|
350
376
|
return (
|
|
351
|
-
<div className="
|
|
377
|
+
<div className="flex h-full w-full gap-2 overflow-x-auto">
|
|
352
378
|
{COLUMNS.map(col => {
|
|
353
379
|
const cards = getCardsForColumn(col.id);
|
|
354
|
-
const sourceColumn = draggedCard ? kanbanData?.[draggedCard]?.column : null;
|
|
355
|
-
const isDropTarget = draggedCard && col.id !== sourceColumn;
|
|
356
380
|
const isDragOver = dragOverColumn === col.id;
|
|
357
381
|
|
|
358
382
|
return (
|
|
359
|
-
<div key={col.id} className="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<span className="
|
|
383
|
+
<div key={col.id} className="flex shrink-0 basis-[calc(20%-0.4rem)] flex-col rounded-xl border border-edge bg-surface-alt overflow-hidden" data-column={col.id}>
|
|
384
|
+
{/* Column header */}
|
|
385
|
+
<div className="flex items-center justify-between px-3 py-2.5 border-b border-edge bg-surface-raised">
|
|
386
|
+
<div className="flex items-center gap-2">
|
|
387
|
+
<span className="text-[12px] font-bold uppercase tracking-wider text-content">{col.label}</span>
|
|
388
|
+
<span className="rounded-md bg-surface px-1.5 py-0.5 text-[11px] font-mono text-content-muted">{cards.length}</span>
|
|
364
389
|
</div>
|
|
365
390
|
{col.id === 'todo' && (
|
|
366
391
|
playAllRunning ? (
|
|
367
|
-
<button
|
|
392
|
+
<button
|
|
393
|
+
className="flex h-6 w-6 items-center justify-center rounded-full border border-error text-error text-[12px] hover:bg-error hover:text-surface transition-colors cursor-pointer"
|
|
394
|
+
title="Stop processing"
|
|
395
|
+
onClick={stopPlayAll}
|
|
396
|
+
>
|
|
368
397
|
{'\u25A0'}
|
|
369
398
|
</button>
|
|
370
399
|
) : (
|
|
371
|
-
<button
|
|
400
|
+
<button
|
|
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"
|
|
402
|
+
title="Start automated development for all TODO features"
|
|
403
|
+
onClick={() => startPlayAll()}
|
|
404
|
+
>
|
|
372
405
|
{'\u25B6\u25B6'}
|
|
373
406
|
</button>
|
|
374
407
|
)
|
|
375
408
|
)}
|
|
376
409
|
</div>
|
|
410
|
+
{/* Column body */}
|
|
377
411
|
<div
|
|
378
|
-
className={
|
|
412
|
+
className={[
|
|
413
|
+
'flex flex-1 flex-col gap-2 overflow-y-auto p-2',
|
|
414
|
+
isDragOver ? 'bg-accent/10' : '',
|
|
415
|
+
].join(' ')}
|
|
379
416
|
data-column={col.id}
|
|
380
417
|
onDragOver={(e) => handleDragOver(e, col.id)}
|
|
381
418
|
onDragLeave={handleDragLeave}
|
|
@@ -383,193 +420,82 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
383
420
|
>
|
|
384
421
|
{cards.map(card => {
|
|
385
422
|
const pct = Math.round((card.completeness || 0) * 100);
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const pipelineStatusValue = pStatus?.status ?? 'idle';
|
|
423
|
+
const wStatus = workflowStatuses[card.id];
|
|
424
|
+
const workflowStatusValue = wStatus?.status ?? 'idle';
|
|
425
|
+
const isActive = workflowStatusValue === 'running';
|
|
390
426
|
const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
|
|
391
427
|
&& !isActive
|
|
392
|
-
&&
|
|
428
|
+
&& workflowStatusValue === 'failed';
|
|
393
429
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
draggable
|
|
400
|
-
onDragStart={(e) => handleDragStart(e, card.id)}
|
|
401
|
-
onDragEnd={handleDragEnd}
|
|
402
|
-
onClick={(e) => {
|
|
403
|
-
if ((e.target as HTMLElement).closest('button, textarea, .kanban-note-form')) return;
|
|
404
|
-
onCardClick({ id: card.id, name: card.name });
|
|
405
|
-
}}
|
|
406
|
-
style={{ cursor: 'pointer' }}
|
|
407
|
-
>
|
|
408
|
-
<div className="kanban-card-header">
|
|
409
|
-
<span className="kanban-card-id">
|
|
410
|
-
{card.id}
|
|
411
|
-
{card.kind && card.kind !== 'new' && (
|
|
412
|
-
<> <span className={`kanban-card-kind kind-${card.kind}`}>{card.kind}</span></>
|
|
413
|
-
)}
|
|
414
|
-
</span>
|
|
415
|
-
<button
|
|
416
|
-
className={`kanban-copy-btn${copiedId === card.id ? ' copied' : ''}`}
|
|
417
|
-
title="Copy feature ID and name"
|
|
418
|
-
onClick={(e) => { e.stopPropagation(); handleCopy(card); }}
|
|
419
|
-
>
|
|
420
|
-
{copiedId === card.id ? '\u2713' : '\uD83D\uDCCB'}
|
|
421
|
-
</button>
|
|
422
|
-
<span className="kanban-card-header-right">
|
|
423
|
-
{card.rejectionCount > 0 && (
|
|
424
|
-
<span className={`kanban-card-rejections${card.rejectionCount >= 3 ? ' high' : ''}`}>
|
|
425
|
-
{card.rejectionCount}x rejected
|
|
426
|
-
</span>
|
|
427
|
-
)}
|
|
428
|
-
{card.column === 'todo' && !card.devBlocked && (
|
|
429
|
-
<button
|
|
430
|
-
className="kanban-play-btn"
|
|
431
|
-
title="Start automated development pipeline"
|
|
432
|
-
onClick={(e) => { e.stopPropagation(); handleStartPipeline(card.id); }}
|
|
433
|
-
>
|
|
434
|
-
{'\u25B6'}
|
|
435
|
-
</button>
|
|
436
|
-
)}
|
|
437
|
-
{showResumeBtn && (
|
|
438
|
-
<button
|
|
439
|
-
className="kanban-play-btn kanban-resume-btn"
|
|
440
|
-
title="Resume pipeline from last completed step"
|
|
441
|
-
onClick={(e) => { e.stopPropagation(); handleResumePipeline(card.id); }}
|
|
442
|
-
>
|
|
443
|
-
{'\u25B6'}
|
|
444
|
-
</button>
|
|
445
|
-
)}
|
|
446
|
-
{card.devBlocked && (
|
|
447
|
-
<span className="kanban-blocked-badge">Blocked</span>
|
|
448
|
-
)}
|
|
449
|
-
</span>
|
|
450
|
-
</div>
|
|
451
|
-
|
|
452
|
-
<div className="kanban-card-name">{card.name}</div>
|
|
453
|
-
|
|
454
|
-
<div className="kanban-card-completeness-row">
|
|
455
|
-
<span className="kanban-card-completeness-prefix">Spec</span>
|
|
456
|
-
<div className="kanban-card-completeness">
|
|
457
|
-
<div className="kanban-card-completeness-fill" style={{ width: `${pct}%` }} />
|
|
458
|
-
</div>
|
|
459
|
-
<span className="kanban-card-completeness-label">{pct}%</span>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
{pStatus && pStatus.status !== 'idle' && (
|
|
463
|
-
<div className={`kanban-pipeline-status${isActive ? ' pipeline-active' : ''}${isTerminal ? ` pipeline-${pStatus.status}` : ''}`}>
|
|
464
|
-
{getPipelineBadgeText(pStatus.status, card.rejectionCount)}
|
|
465
|
-
</div>
|
|
466
|
-
)}
|
|
467
|
-
|
|
468
|
-
{(() => {
|
|
469
|
-
const ss = pStatus?.stageStats;
|
|
470
|
-
const availableTabs = ss
|
|
471
|
-
? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
|
|
472
|
-
: [];
|
|
473
|
-
// Fall back to flat toolCalls for old data without stageStats
|
|
474
|
-
if (availableTabs.length === 0 && pStatus?.toolCalls?.total > 0) {
|
|
475
|
-
return (
|
|
476
|
-
<div className="kanban-tool-calls">
|
|
477
|
-
{[
|
|
478
|
-
{ label: 'Write', count: pStatus.toolCalls.write },
|
|
479
|
-
{ label: 'Edit', count: pStatus.toolCalls.edit },
|
|
480
|
-
{ label: 'Read', count: pStatus.toolCalls.read },
|
|
481
|
-
{ label: 'Bash', count: pStatus.toolCalls.bash },
|
|
482
|
-
].filter(i => i.count > 0).map(i => (
|
|
483
|
-
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
484
|
-
))}
|
|
485
|
-
</div>
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
if (availableTabs.length === 0) return null;
|
|
489
|
-
|
|
490
|
-
const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
|
|
491
|
-
const stage = ss[currentTab];
|
|
492
|
-
const tc = stage?.toolCalls;
|
|
430
|
+
const issueLabel = card.notes.length > 0
|
|
431
|
+
? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
|
|
432
|
+
: card.column === 'qa'
|
|
433
|
+
? '+ Report Issue'
|
|
434
|
+
: 'No issues';
|
|
493
435
|
|
|
436
|
+
return (
|
|
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
|
+
);
|
|
494
476
|
return (
|
|
495
|
-
<div
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
{tc && tc.total > 0 && (
|
|
512
|
-
<div className="kanban-tool-calls">
|
|
513
|
-
{[
|
|
514
|
-
{ label: 'Write', count: tc.write },
|
|
515
|
-
{ label: 'Edit', count: tc.edit },
|
|
516
|
-
{ label: 'Read', count: tc.read },
|
|
517
|
-
{ label: 'Bash', count: tc.bash },
|
|
518
|
-
].filter(i => i.count > 0).map(i => (
|
|
519
|
-
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
520
|
-
))}
|
|
521
|
-
</div>
|
|
522
|
-
)}
|
|
523
|
-
<div className="kanban-stage-meta">
|
|
524
|
-
{stage?.lastTurnUsage && (
|
|
525
|
-
<span className="kanban-stage-meta-item" title="Peak context window utilization">
|
|
526
|
-
Ctx: {formatContextFill(stage.lastTurnUsage, stage.contextWindow)}
|
|
527
|
-
</span>
|
|
528
|
-
)}
|
|
529
|
-
{stage?.numTurns != null && (
|
|
530
|
-
<span className="kanban-stage-meta-item" title="Agentic turns">
|
|
531
|
-
{stage.numTurns} turns
|
|
532
|
-
</span>
|
|
533
|
-
)}
|
|
534
|
-
{stage?.costUsd != null && (
|
|
535
|
-
<span className="kanban-stage-meta-item" title={`Model: ${stage.model || 'unknown'}`}>
|
|
536
|
-
{formatCost(stage.costUsd)}
|
|
537
|
-
{stage.model && <span className="kanban-stage-model"> {formatModel(stage.model)}</span>}
|
|
538
|
-
</span>
|
|
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>
|
|
539
493
|
)}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{stage.stopReason}
|
|
543
|
-
</span>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
494
|
+
</button>
|
|
495
|
+
))}
|
|
547
496
|
</div>
|
|
548
497
|
);
|
|
549
498
|
})()}
|
|
550
|
-
|
|
551
|
-
{card.devBlocked && (
|
|
552
|
-
<button
|
|
553
|
-
className="kanban-unblock-btn"
|
|
554
|
-
onClick={(e) => { e.stopPropagation(); handleUnblock(card.id); }}
|
|
555
|
-
>
|
|
556
|
-
Unblock
|
|
557
|
-
</button>
|
|
558
|
-
)}
|
|
559
|
-
|
|
560
|
-
<button
|
|
561
|
-
className={`kanban-issues-btn${card.notes.length > 0 ? ' has-issues' : ''}`}
|
|
562
|
-
onClick={(e) => {
|
|
563
|
-
e.stopPropagation();
|
|
564
|
-
onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews);
|
|
565
|
-
}}
|
|
566
|
-
>
|
|
567
|
-
{card.notes.length > 0
|
|
568
|
-
? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
|
|
569
|
-
: card.column === 'qa'
|
|
570
|
-
? '+ Report Issue'
|
|
571
|
-
: 'No issues'}
|
|
572
|
-
</button>
|
|
573
499
|
</div>
|
|
574
500
|
);
|
|
575
501
|
})}
|
|
@@ -578,6 +504,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
578
504
|
);
|
|
579
505
|
})}
|
|
580
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
|
+
)}
|
|
581
516
|
</div>
|
|
582
517
|
);
|
|
583
518
|
}
|