orbital-command 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/bin/orbital.js +362 -0
  4. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
  5. package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
  6. package/dist/assets/charts-D__PA1zp.js +72 -0
  7. package/dist/assets/index-D1G6i0nS.css +1 -0
  8. package/dist/assets/index-DpItvKpf.js +419 -0
  9. package/dist/assets/ui-BvF022GT.js +53 -0
  10. package/dist/assets/vendor-Dzv9lrRc.js +59 -0
  11. package/dist/index.html +19 -0
  12. package/dist/scanner-sweep.png +0 -0
  13. package/dist/server/server/adapters/index.js +34 -0
  14. package/dist/server/server/adapters/iterm2-adapter.js +29 -0
  15. package/dist/server/server/adapters/subprocess-adapter.js +21 -0
  16. package/dist/server/server/adapters/terminal-adapter.js +1 -0
  17. package/dist/server/server/config.js +156 -0
  18. package/dist/server/server/database.js +90 -0
  19. package/dist/server/server/index.js +372 -0
  20. package/dist/server/server/init.js +811 -0
  21. package/dist/server/server/parsers/event-parser.js +64 -0
  22. package/dist/server/server/parsers/scope-parser.js +188 -0
  23. package/dist/server/server/routes/config-routes.js +163 -0
  24. package/dist/server/server/routes/data-routes.js +461 -0
  25. package/dist/server/server/routes/dispatch-routes.js +215 -0
  26. package/dist/server/server/routes/git-routes.js +92 -0
  27. package/dist/server/server/routes/scope-routes.js +215 -0
  28. package/dist/server/server/routes/sprint-routes.js +116 -0
  29. package/dist/server/server/routes/version-routes.js +130 -0
  30. package/dist/server/server/routes/workflow-routes.js +185 -0
  31. package/dist/server/server/schema.js +90 -0
  32. package/dist/server/server/services/batch-orchestrator.js +253 -0
  33. package/dist/server/server/services/claude-session-service.js +352 -0
  34. package/dist/server/server/services/config-service.js +132 -0
  35. package/dist/server/server/services/deploy-service.js +51 -0
  36. package/dist/server/server/services/event-service.js +63 -0
  37. package/dist/server/server/services/gate-service.js +83 -0
  38. package/dist/server/server/services/git-service.js +309 -0
  39. package/dist/server/server/services/github-service.js +145 -0
  40. package/dist/server/server/services/readiness-service.js +184 -0
  41. package/dist/server/server/services/scope-cache.js +72 -0
  42. package/dist/server/server/services/scope-service.js +424 -0
  43. package/dist/server/server/services/sprint-orchestrator.js +312 -0
  44. package/dist/server/server/services/sprint-service.js +293 -0
  45. package/dist/server/server/services/workflow-service.js +397 -0
  46. package/dist/server/server/utils/cc-hooks-parser.js +49 -0
  47. package/dist/server/server/utils/dispatch-utils.js +305 -0
  48. package/dist/server/server/utils/logger.js +86 -0
  49. package/dist/server/server/utils/terminal-launcher.js +388 -0
  50. package/dist/server/server/utils/worktree-manager.js +98 -0
  51. package/dist/server/server/watchers/event-watcher.js +81 -0
  52. package/dist/server/server/watchers/scope-watcher.js +33 -0
  53. package/dist/server/shared/api-types.js +5 -0
  54. package/dist/server/shared/default-workflow.json +616 -0
  55. package/dist/server/shared/workflow-config.js +44 -0
  56. package/dist/server/shared/workflow-engine.js +353 -0
  57. package/index.html +15 -0
  58. package/package.json +110 -0
  59. package/postcss.config.js +6 -0
  60. package/schemas/orbital.config.schema.json +83 -0
  61. package/scripts/postinstall.js +24 -0
  62. package/scripts/start.sh +20 -0
  63. package/server/adapters/index.ts +41 -0
  64. package/server/adapters/iterm2-adapter.ts +37 -0
  65. package/server/adapters/subprocess-adapter.ts +25 -0
  66. package/server/adapters/terminal-adapter.ts +24 -0
  67. package/server/config.ts +234 -0
  68. package/server/database.ts +107 -0
  69. package/server/index.ts +452 -0
  70. package/server/init.ts +891 -0
  71. package/server/parsers/event-parser.ts +74 -0
  72. package/server/parsers/scope-parser.ts +240 -0
  73. package/server/routes/config-routes.ts +182 -0
  74. package/server/routes/data-routes.ts +548 -0
  75. package/server/routes/dispatch-routes.ts +275 -0
  76. package/server/routes/git-routes.ts +112 -0
  77. package/server/routes/scope-routes.ts +262 -0
  78. package/server/routes/sprint-routes.ts +142 -0
  79. package/server/routes/version-routes.ts +156 -0
  80. package/server/routes/workflow-routes.ts +198 -0
  81. package/server/schema.ts +90 -0
  82. package/server/services/batch-orchestrator.ts +286 -0
  83. package/server/services/claude-session-service.ts +441 -0
  84. package/server/services/config-service.ts +151 -0
  85. package/server/services/deploy-service.ts +98 -0
  86. package/server/services/event-service.ts +98 -0
  87. package/server/services/gate-service.ts +126 -0
  88. package/server/services/git-service.ts +391 -0
  89. package/server/services/github-service.ts +183 -0
  90. package/server/services/readiness-service.ts +250 -0
  91. package/server/services/scope-cache.ts +81 -0
  92. package/server/services/scope-service.ts +476 -0
  93. package/server/services/sprint-orchestrator.ts +361 -0
  94. package/server/services/sprint-service.ts +415 -0
  95. package/server/services/workflow-service.ts +461 -0
  96. package/server/utils/cc-hooks-parser.ts +70 -0
  97. package/server/utils/dispatch-utils.ts +395 -0
  98. package/server/utils/logger.ts +109 -0
  99. package/server/utils/terminal-launcher.ts +462 -0
  100. package/server/utils/worktree-manager.ts +104 -0
  101. package/server/watchers/event-watcher.ts +100 -0
  102. package/server/watchers/scope-watcher.ts +38 -0
  103. package/shared/api-types.ts +20 -0
  104. package/shared/default-workflow.json +616 -0
  105. package/shared/workflow-config.ts +170 -0
  106. package/shared/workflow-engine.ts +427 -0
  107. package/src/App.tsx +33 -0
  108. package/src/components/AgentBadge.tsx +40 -0
  109. package/src/components/BatchPreflightModal.tsx +115 -0
  110. package/src/components/CardDisplayToggle.tsx +74 -0
  111. package/src/components/ColumnHeaderActions.tsx +55 -0
  112. package/src/components/ColumnMenu.tsx +99 -0
  113. package/src/components/DeployHistory.tsx +141 -0
  114. package/src/components/DispatchModal.tsx +164 -0
  115. package/src/components/DispatchPopover.tsx +139 -0
  116. package/src/components/DragOverlay.tsx +25 -0
  117. package/src/components/DriftSidebar.tsx +140 -0
  118. package/src/components/EnvironmentStrip.tsx +88 -0
  119. package/src/components/ErrorBoundary.tsx +62 -0
  120. package/src/components/FilterChip.tsx +105 -0
  121. package/src/components/GateIndicator.tsx +33 -0
  122. package/src/components/IdeaDetailModal.tsx +190 -0
  123. package/src/components/IdeaFormDialog.tsx +113 -0
  124. package/src/components/KanbanColumn.tsx +201 -0
  125. package/src/components/MarkdownRenderer.tsx +114 -0
  126. package/src/components/NeonGrid.tsx +128 -0
  127. package/src/components/PromotionQueue.tsx +89 -0
  128. package/src/components/ScopeCard.tsx +234 -0
  129. package/src/components/ScopeDetailModal.tsx +255 -0
  130. package/src/components/ScopeFilterBar.tsx +152 -0
  131. package/src/components/SearchInput.tsx +102 -0
  132. package/src/components/SessionPanel.tsx +335 -0
  133. package/src/components/SprintContainer.tsx +303 -0
  134. package/src/components/SprintDependencyDialog.tsx +78 -0
  135. package/src/components/SprintPreflightModal.tsx +138 -0
  136. package/src/components/StatusBar.tsx +168 -0
  137. package/src/components/SwimCell.tsx +67 -0
  138. package/src/components/SwimLaneRow.tsx +94 -0
  139. package/src/components/SwimlaneBoardView.tsx +108 -0
  140. package/src/components/VersionBadge.tsx +139 -0
  141. package/src/components/ViewModeSelector.tsx +114 -0
  142. package/src/components/config/AgentChip.tsx +53 -0
  143. package/src/components/config/AgentCreateDialog.tsx +321 -0
  144. package/src/components/config/AgentEditor.tsx +175 -0
  145. package/src/components/config/DirectoryTree.tsx +582 -0
  146. package/src/components/config/FileEditor.tsx +550 -0
  147. package/src/components/config/HookChip.tsx +50 -0
  148. package/src/components/config/StageCard.tsx +198 -0
  149. package/src/components/config/TransitionZone.tsx +173 -0
  150. package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
  151. package/src/components/config/WorkflowPipeline.tsx +161 -0
  152. package/src/components/source-control/BranchList.tsx +93 -0
  153. package/src/components/source-control/BranchPanel.tsx +105 -0
  154. package/src/components/source-control/CommitLog.tsx +100 -0
  155. package/src/components/source-control/CommitRow.tsx +47 -0
  156. package/src/components/source-control/GitHubPanel.tsx +110 -0
  157. package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
  158. package/src/components/source-control/GitOverviewBar.tsx +101 -0
  159. package/src/components/source-control/PullRequestList.tsx +69 -0
  160. package/src/components/source-control/WorktreeList.tsx +80 -0
  161. package/src/components/ui/badge.tsx +41 -0
  162. package/src/components/ui/button.tsx +55 -0
  163. package/src/components/ui/card.tsx +78 -0
  164. package/src/components/ui/dialog.tsx +94 -0
  165. package/src/components/ui/popover.tsx +33 -0
  166. package/src/components/ui/scroll-area.tsx +54 -0
  167. package/src/components/ui/separator.tsx +28 -0
  168. package/src/components/ui/tabs.tsx +52 -0
  169. package/src/components/ui/toggle-switch.tsx +35 -0
  170. package/src/components/ui/tooltip.tsx +27 -0
  171. package/src/components/workflow/AddEdgeDialog.tsx +217 -0
  172. package/src/components/workflow/AddListDialog.tsx +201 -0
  173. package/src/components/workflow/ChecklistEditor.tsx +239 -0
  174. package/src/components/workflow/CommandPrefixManager.tsx +118 -0
  175. package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
  176. package/src/components/workflow/DirectionSelector.tsx +133 -0
  177. package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
  178. package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
  179. package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
  180. package/src/components/workflow/EditToolbar.tsx +138 -0
  181. package/src/components/workflow/HookDetailPanel.tsx +250 -0
  182. package/src/components/workflow/HookExecutionLog.tsx +24 -0
  183. package/src/components/workflow/HookSourceModal.tsx +129 -0
  184. package/src/components/workflow/HooksDashboard.tsx +363 -0
  185. package/src/components/workflow/ListPropertyEditor.tsx +251 -0
  186. package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
  187. package/src/components/workflow/MovementRulesPanel.tsx +188 -0
  188. package/src/components/workflow/NodeDetailPanel.tsx +245 -0
  189. package/src/components/workflow/PresetSelector.tsx +414 -0
  190. package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
  191. package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
  192. package/src/components/workflow/WorkflowNode.tsx +147 -0
  193. package/src/components/workflow/graphLayout.ts +186 -0
  194. package/src/components/workflow/mergeHooks.ts +85 -0
  195. package/src/components/workflow/useEditHistory.ts +88 -0
  196. package/src/components/workflow/useWorkflowEditor.ts +262 -0
  197. package/src/components/workflow/validateConfig.ts +70 -0
  198. package/src/hooks/useActiveDispatches.ts +198 -0
  199. package/src/hooks/useBoardSettings.ts +170 -0
  200. package/src/hooks/useCardDisplay.ts +57 -0
  201. package/src/hooks/useCcHooks.ts +24 -0
  202. package/src/hooks/useConfigTree.ts +51 -0
  203. package/src/hooks/useEnforcementRules.ts +46 -0
  204. package/src/hooks/useEvents.ts +59 -0
  205. package/src/hooks/useFileEditor.ts +165 -0
  206. package/src/hooks/useGates.ts +57 -0
  207. package/src/hooks/useIdeaActions.ts +53 -0
  208. package/src/hooks/useKanbanDnd.ts +410 -0
  209. package/src/hooks/useOrbitalConfig.ts +54 -0
  210. package/src/hooks/usePipeline.ts +47 -0
  211. package/src/hooks/usePipelineData.ts +338 -0
  212. package/src/hooks/useReconnect.ts +25 -0
  213. package/src/hooks/useScopeFilters.ts +125 -0
  214. package/src/hooks/useScopeSessions.ts +44 -0
  215. package/src/hooks/useScopes.ts +67 -0
  216. package/src/hooks/useSearch.ts +67 -0
  217. package/src/hooks/useSettings.tsx +187 -0
  218. package/src/hooks/useSocket.ts +25 -0
  219. package/src/hooks/useSourceControl.ts +105 -0
  220. package/src/hooks/useSprintPreflight.ts +55 -0
  221. package/src/hooks/useSprints.ts +154 -0
  222. package/src/hooks/useStatusBarHighlight.ts +18 -0
  223. package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
  224. package/src/hooks/useTheme.ts +9 -0
  225. package/src/hooks/useTransitionReadiness.ts +53 -0
  226. package/src/hooks/useVersion.ts +155 -0
  227. package/src/hooks/useViolations.ts +65 -0
  228. package/src/hooks/useWorkflow.tsx +125 -0
  229. package/src/hooks/useZoomModifier.ts +19 -0
  230. package/src/index.css +797 -0
  231. package/src/layouts/DashboardLayout.tsx +113 -0
  232. package/src/lib/collisionDetection.ts +20 -0
  233. package/src/lib/scope-fields.ts +61 -0
  234. package/src/lib/swimlane.ts +146 -0
  235. package/src/lib/utils.ts +15 -0
  236. package/src/main.tsx +19 -0
  237. package/src/socket.ts +11 -0
  238. package/src/types/index.ts +497 -0
  239. package/src/views/AgentFeed.tsx +339 -0
  240. package/src/views/DeployPipeline.tsx +59 -0
  241. package/src/views/EnforcementView.tsx +378 -0
  242. package/src/views/PrimitivesConfig.tsx +500 -0
  243. package/src/views/QualityGates.tsx +1012 -0
  244. package/src/views/ScopeBoard.tsx +454 -0
  245. package/src/views/SessionTimeline.tsx +516 -0
  246. package/src/views/Settings.tsx +183 -0
  247. package/src/views/SourceControl.tsx +95 -0
  248. package/src/views/WorkflowVisualizer.tsx +382 -0
  249. package/tailwind.config.js +161 -0
  250. package/templates/agents/AUTO-INVOKE.md +180 -0
  251. package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
  252. package/templates/agents/QUICK-REFERENCE.md +122 -0
  253. package/templates/agents/README.md +188 -0
  254. package/templates/agents/SKILL-TRIGGERS.md +100 -0
  255. package/templates/agents/blue-team/frontend-designer.md +424 -0
  256. package/templates/agents/green-team/architect.md +526 -0
  257. package/templates/agents/green-team/rules-enforcer.md +131 -0
  258. package/templates/agents/red-team/attacker-learned.md +24 -0
  259. package/templates/agents/red-team/attacker.md +486 -0
  260. package/templates/agents/red-team/chaos.md +548 -0
  261. package/templates/agents/reference/component-registry.md +82 -0
  262. package/templates/agents/workflows/full-mode.md +218 -0
  263. package/templates/agents/workflows/quick-mode.md +118 -0
  264. package/templates/agents/workflows/security-mode.md +283 -0
  265. package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
  266. package/templates/config/agent-triggers.json +92 -0
  267. package/templates/hooks/agent-team-gate.sh +31 -0
  268. package/templates/hooks/agent-trigger.sh +97 -0
  269. package/templates/hooks/block-push.sh +66 -0
  270. package/templates/hooks/block-workarounds.sh +61 -0
  271. package/templates/hooks/blocker-check.sh +28 -0
  272. package/templates/hooks/completion-checklist.sh +28 -0
  273. package/templates/hooks/decision-capture.sh +15 -0
  274. package/templates/hooks/dependency-check.sh +27 -0
  275. package/templates/hooks/end-session.sh +31 -0
  276. package/templates/hooks/exploration-logger.sh +37 -0
  277. package/templates/hooks/files-changed-summary.sh +37 -0
  278. package/templates/hooks/get-session-id.sh +49 -0
  279. package/templates/hooks/git-commit-guard.sh +34 -0
  280. package/templates/hooks/init-session.sh +93 -0
  281. package/templates/hooks/orbital-emit.sh +79 -0
  282. package/templates/hooks/orbital-report-deploy.sh +78 -0
  283. package/templates/hooks/orbital-report-gates.sh +40 -0
  284. package/templates/hooks/orbital-report-violation.sh +36 -0
  285. package/templates/hooks/orbital-scope-update.sh +53 -0
  286. package/templates/hooks/phase-verify-reminder.sh +26 -0
  287. package/templates/hooks/review-gate-check.sh +82 -0
  288. package/templates/hooks/scope-commit-logger.sh +37 -0
  289. package/templates/hooks/scope-create-cleanup.sh +36 -0
  290. package/templates/hooks/scope-create-gate.sh +80 -0
  291. package/templates/hooks/scope-create-tracker.sh +17 -0
  292. package/templates/hooks/scope-file-sync.sh +53 -0
  293. package/templates/hooks/scope-gate.sh +35 -0
  294. package/templates/hooks/scope-helpers.sh +188 -0
  295. package/templates/hooks/scope-lifecycle-gate.sh +139 -0
  296. package/templates/hooks/scope-prepare.sh +244 -0
  297. package/templates/hooks/scope-transition.sh +172 -0
  298. package/templates/hooks/session-enforcer.sh +143 -0
  299. package/templates/hooks/time-tracker.sh +33 -0
  300. package/templates/lessons-learned.md +15 -0
  301. package/templates/orbital.config.json +35 -0
  302. package/templates/presets/development.json +42 -0
  303. package/templates/presets/gitflow.json +712 -0
  304. package/templates/presets/minimal.json +23 -0
  305. package/templates/quick/rules.md +218 -0
  306. package/templates/scopes/_template.md +255 -0
  307. package/templates/settings-hooks.json +98 -0
  308. package/templates/skills/git-commit/SKILL.md +85 -0
  309. package/templates/skills/git-dev/SKILL.md +99 -0
  310. package/templates/skills/git-hotfix/SKILL.md +223 -0
  311. package/templates/skills/git-main/SKILL.md +84 -0
  312. package/templates/skills/git-production/SKILL.md +165 -0
  313. package/templates/skills/git-staging/SKILL.md +112 -0
  314. package/templates/skills/scope-create/SKILL.md +81 -0
  315. package/templates/skills/scope-fix-review/SKILL.md +168 -0
  316. package/templates/skills/scope-implement/SKILL.md +110 -0
  317. package/templates/skills/scope-post-review/SKILL.md +144 -0
  318. package/templates/skills/scope-pre-review/SKILL.md +211 -0
  319. package/templates/skills/scope-verify/SKILL.md +201 -0
  320. package/templates/skills/session-init/SKILL.md +62 -0
  321. package/templates/skills/session-resume/SKILL.md +201 -0
  322. package/templates/skills/test-checks/SKILL.md +171 -0
  323. package/templates/skills/test-code-review/SKILL.md +252 -0
  324. package/tsconfig.json +25 -0
  325. package/vite.config.ts +38 -0
@@ -0,0 +1,335 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { ArrowLeft, Terminal, ChevronRight } from 'lucide-react';
3
+ import { format } from 'date-fns';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent } from '@/components/ui/card';
7
+ import { ScrollArea } from '@/components/ui/scroll-area';
8
+ import { Separator } from '@/components/ui/separator';
9
+ import { cn } from '@/lib/utils';
10
+ import type { EnrichedSession } from '@/hooks/useScopeSessions';
11
+
12
+ const ACTION_LABELS: Record<string, string> = {
13
+ createScope: 'Created',
14
+ reviewScope: 'Reviewed',
15
+ implementScope: 'Implemented',
16
+ verifyScope: 'Verified',
17
+ commit: 'Committed',
18
+ pushToMain: 'Pushed to Main',
19
+ pushToDev: 'Pushed to Dev',
20
+ pushToStaging: 'PR to Staging',
21
+ pushToProduction: 'PR to Production',
22
+ };
23
+
24
+ function actionLabel(action: string | null): string | null {
25
+ if (!action) return null;
26
+ return ACTION_LABELS[action] ?? action;
27
+ }
28
+
29
+ interface SessionPanelProps {
30
+ sessions: EnrichedSession[];
31
+ loading: boolean;
32
+ }
33
+
34
+ interface SessionMeta {
35
+ slug: string;
36
+ branch: string;
37
+ fileSize: number;
38
+ summary: string | null;
39
+ startedAt: string;
40
+ lastActiveAt: string;
41
+ }
42
+
43
+ interface SessionContent {
44
+ id: string;
45
+ content: string;
46
+ claude_session_id: string | null;
47
+ meta: SessionMeta | null;
48
+ }
49
+
50
+ export function SessionPanel({ sessions, loading }: SessionPanelProps) {
51
+ const [selected, setSelected] = useState<EnrichedSession | null>(null);
52
+ const [content, setContent] = useState<SessionContent | null>(null);
53
+ const [contentLoading, setContentLoading] = useState(false);
54
+ const [resuming, setResuming] = useState(false);
55
+
56
+ const selectSession = useCallback(async (session: EnrichedSession) => {
57
+ setSelected(session);
58
+ setContentLoading(true);
59
+ try {
60
+ const res = await fetch(`/api/orbital/sessions/${session.id}/content`);
61
+ if (res.ok) setContent(await res.json());
62
+ } catch {
63
+ // silent
64
+ } finally {
65
+ setContentLoading(false);
66
+ }
67
+ }, []);
68
+
69
+ const handleResume = useCallback(async () => {
70
+ const sessionId = selected?.claude_session_id;
71
+ if (!sessionId) return;
72
+
73
+ setResuming(true);
74
+ try {
75
+ await fetch(`/api/orbital/sessions/${selected.id}/resume`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ claude_session_id: sessionId }),
79
+ });
80
+ } catch {
81
+ // silent
82
+ } finally {
83
+ setTimeout(() => setResuming(false), 2000);
84
+ }
85
+ }, [selected]);
86
+
87
+ if (loading) {
88
+ return (
89
+ <div className="flex h-32 items-center justify-center">
90
+ <div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // ─── Detail view ───────────────────────────────────────────
96
+ if (selected) {
97
+ const discoveries = Array.isArray(selected.discoveries) ? selected.discoveries : [];
98
+ const nextSteps = Array.isArray(selected.next_steps) ? selected.next_steps : [];
99
+ const canResume = !!selected.claude_session_id;
100
+ const meta = content?.meta ?? null;
101
+ // Prefer meta summary (from JSONL), then DB summary
102
+ const rawDisplayName = meta?.summary ?? selected.summary ?? null;
103
+ const displayName = rawDisplayName ? truncateText(rawDisplayName, 100) : null;
104
+
105
+ return (
106
+ <div className="flex h-full flex-col">
107
+ {/* Back button + header */}
108
+ <div className="flex items-center gap-2 pb-3">
109
+ <Button
110
+ variant="ghost"
111
+ size="icon"
112
+ className="h-7 w-7"
113
+ onClick={() => { setSelected(null); setContent(null); }}
114
+ >
115
+ <ArrowLeft className="h-4 w-4" />
116
+ </Button>
117
+ <div className="min-w-0 flex-1">
118
+ <p className="text-xxs text-muted-foreground">
119
+ {selected.started_at && format(new Date(selected.started_at), 'MMM d, yyyy')}
120
+ </p>
121
+ <p className="truncate text-sm font-light">
122
+ {displayName || 'Untitled Session'}
123
+ </p>
124
+ </div>
125
+ </div>
126
+
127
+ <Separator className="mb-3" />
128
+
129
+ {/* Content */}
130
+ <ScrollArea className="flex-1 -mr-2 pr-2">
131
+ {contentLoading ? (
132
+ <div className="flex h-20 items-center justify-center">
133
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
134
+ </div>
135
+ ) : (
136
+ <table className="w-full text-xs">
137
+ <tbody className="[&_td]:border-b [&_td]:border-border/30 [&_td]:py-2 [&_td]:align-top [&_td:first-child]:pr-3 [&_td:first-child]:text-muted-foreground [&_td:first-child]:whitespace-nowrap">
138
+ {/* DB fields */}
139
+ <tr>
140
+ <td>Scope</td>
141
+ <td>{selected.scope_id}</td>
142
+ </tr>
143
+ {actionLabel(selected.action) && (
144
+ <tr>
145
+ <td>Action</td>
146
+ <td>{actionLabel(selected.action)}</td>
147
+ </tr>
148
+ )}
149
+ <tr>
150
+ <td>Summary</td>
151
+ <td>{selected.summary ? truncateText(selected.summary, 200) : '—'}</td>
152
+ </tr>
153
+ <tr>
154
+ <td>Started</td>
155
+ <td>{selected.started_at ? format(new Date(selected.started_at), 'MMM d, h:mm a') : '—'}</td>
156
+ </tr>
157
+ <tr>
158
+ <td>Ended</td>
159
+ <td>{selected.ended_at ? format(new Date(selected.ended_at), 'MMM d, h:mm a') : '—'}</td>
160
+ </tr>
161
+
162
+ {/* JSONL metadata (when available) */}
163
+ {meta && (
164
+ <>
165
+ {meta.branch && meta.branch !== 'unknown' && (
166
+ <tr>
167
+ <td>Branch</td>
168
+ <td className="font-mono text-xxs">{meta.branch}</td>
169
+ </tr>
170
+ )}
171
+ {meta.fileSize > 0 && (
172
+ <tr>
173
+ <td>File size</td>
174
+ <td>{formatFileSize(meta.fileSize)}</td>
175
+ </tr>
176
+ )}
177
+ <tr>
178
+ <td>Plan</td>
179
+ <td className="text-muted-foreground">{meta.slug}</td>
180
+ </tr>
181
+ </>
182
+ )}
183
+
184
+ {/* Handoff file */}
185
+ {selected.handoff_file && (
186
+ <tr>
187
+ <td>Handoff</td>
188
+ <td className="font-mono text-xxs">{selected.handoff_file}</td>
189
+ </tr>
190
+ )}
191
+
192
+ {/* Discoveries */}
193
+ {discoveries.length > 0 && (
194
+ <tr>
195
+ <td>Completed</td>
196
+ <td>
197
+ <ul className="space-y-0.5">
198
+ {discoveries.map((item, idx) => (
199
+ <li key={idx} className="text-muted-foreground">
200
+ <span className="text-bid-green mr-1">{'•'}</span>{item}
201
+ </li>
202
+ ))}
203
+ </ul>
204
+ </td>
205
+ </tr>
206
+ )}
207
+
208
+ {/* Next steps */}
209
+ {nextSteps.length > 0 && (
210
+ <tr>
211
+ <td>Next steps</td>
212
+ <td>
213
+ <ul className="space-y-0.5">
214
+ {nextSteps.map((item, idx) => (
215
+ <li key={idx} className="text-muted-foreground">
216
+ <span className="text-accent-blue mr-1">{'•'}</span>{item}
217
+ </li>
218
+ ))}
219
+ </ul>
220
+ </td>
221
+ </tr>
222
+ )}
223
+
224
+ {/* Session ID */}
225
+ {selected.claude_session_id && (
226
+ <tr>
227
+ <td>Session ID</td>
228
+ <td className="font-mono text-xxs text-muted-foreground">{selected.claude_session_id}</td>
229
+ </tr>
230
+ )}
231
+ </tbody>
232
+ </table>
233
+ )}
234
+ </ScrollArea>
235
+
236
+ {/* Resume button */}
237
+ <div className="pt-3">
238
+ <Button
239
+ className="w-full"
240
+ disabled={!canResume || resuming}
241
+ onClick={handleResume}
242
+ title={canResume ? 'Open in iTerm' : 'No Claude Code session found'}
243
+ >
244
+ <Terminal className="mr-2 h-4 w-4" />
245
+ {resuming ? 'Opening iTerm...' : 'Resume Session'}
246
+ </Button>
247
+ {!canResume && (
248
+ <p className="mt-1.5 text-center text-xs text-muted-foreground">
249
+ No matching Claude Code session found
250
+ </p>
251
+ )}
252
+ </div>
253
+ </div>
254
+ );
255
+ }
256
+
257
+ // ─── List view ─────────────────────────────────────────────
258
+ return (
259
+ <div className="flex h-full flex-col">
260
+ <div className="flex items-center gap-2 pb-3">
261
+ <h3 className="text-xs font-normal">Sessions</h3>
262
+ <Badge variant="secondary" className="text-xxs">
263
+ {sessions.length}
264
+ </Badge>
265
+ </div>
266
+
267
+ {sessions.length === 0 ? (
268
+ <div className="flex flex-1 items-center justify-center">
269
+ <p className="text-xs text-muted-foreground">No sessions recorded yet</p>
270
+ </div>
271
+ ) : (
272
+ <ScrollArea className="flex-1 -mr-2 pr-2">
273
+ <div className="space-y-2">
274
+ {sessions.map((session) => {
275
+ const discoveries = Array.isArray(session.discoveries) ? session.discoveries : [];
276
+ const nextSteps = Array.isArray(session.next_steps) ? session.next_steps : [];
277
+
278
+ return (
279
+ <Card
280
+ key={session.id}
281
+ className={cn(
282
+ 'cursor-pointer transition-colors hover:border-primary/30',
283
+ session.claude_session_id && 'border-l-2 border-l-primary/50',
284
+ 'glow-blue-sm',
285
+ )}
286
+ onClick={() => selectSession(session)}
287
+ >
288
+ <CardContent className="p-2.5">
289
+ <div className="flex items-center justify-between">
290
+ <div className="flex items-center gap-1.5">
291
+ <span className="text-xxs text-muted-foreground">
292
+ {session.started_at && format(new Date(session.started_at), 'MMM d')}
293
+ </span>
294
+ {actionLabel(session.action) && (
295
+ <Badge variant="outline" className="text-xxs px-1 py-0 font-light">
296
+ {actionLabel(session.action)}
297
+ </Badge>
298
+ )}
299
+ </div>
300
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/50" />
301
+ </div>
302
+ <p className="mt-1 truncate text-xs font-normal">
303
+ {session.summary || 'Untitled Session'}
304
+ </p>
305
+ <div className="mt-1.5 flex items-center gap-3 text-xxs text-muted-foreground">
306
+ {discoveries.length > 0 && (
307
+ <span className="text-bid-green">{discoveries.length} completed</span>
308
+ )}
309
+ {nextSteps.length > 0 && (
310
+ <span className="text-accent-blue">{nextSteps.length} next</span>
311
+ )}
312
+ {session.claude_session_id && (
313
+ <span className="text-primary/70">resumable</span>
314
+ )}
315
+ </div>
316
+ </CardContent>
317
+ </Card>
318
+ );
319
+ })}
320
+ </div>
321
+ </ScrollArea>
322
+ )}
323
+ </div>
324
+ );
325
+ }
326
+
327
+ function truncateText(text: string, max: number): string {
328
+ return text.length > max ? text.slice(0, max) + '...' : text;
329
+ }
330
+
331
+ function formatFileSize(bytes: number): string {
332
+ if (bytes < 1024) return `${bytes} B`;
333
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
334
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
335
+ }
@@ -0,0 +1,303 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { useDraggable, useDroppable } from '@dnd-kit/core';
3
+ import { CSS } from '@dnd-kit/utilities';
4
+ import { X, Layers, Package, Play } from 'lucide-react';
5
+ import type { Sprint, Scope, CardDisplayConfig } from '@/types';
6
+ import { ScopeCard } from './ScopeCard';
7
+ import { cn, formatScopeId } from '@/lib/utils';
8
+ import { useWorkflow } from '@/hooks/useWorkflow';
9
+
10
+ interface SprintContainerProps {
11
+ sprint: Sprint;
12
+ /** Full scope objects for scopes in the sprint (for rendering cards) */
13
+ scopeLookup: Map<number, Scope>;
14
+ onDelete?: (id: number) => void;
15
+ onDispatch?: (id: number) => void;
16
+ onRename?: (id: number, name: string) => void;
17
+ onScopeClick?: (scope: Scope) => void;
18
+ cardDisplay?: CardDisplayConfig;
19
+ dimmedIds?: Set<number>;
20
+ /** Number of loose (non-batched) scopes in the column — drives "Add all" button visibility */
21
+ looseCount?: number;
22
+ /** Bulk-add all loose column scopes into this batch */
23
+ onAddAll?: (sprintId: number) => void;
24
+ /** Whether this sprint was just created and should start with name editing */
25
+ editingName?: boolean;
26
+ /** Called when name editing finishes (committed or cancelled) */
27
+ onEditingDone?: () => void;
28
+ }
29
+
30
+ const STATUS_STYLE: Record<string, string> = {
31
+ assembling: 'border-dashed border-cyan-500/40',
32
+ dispatched: 'border-solid border-amber-500/50 batch-group-dispatched',
33
+ in_progress: 'border-solid border-amber-500/40 batch-group-dispatched',
34
+ completed: 'border-solid border-green-500/40 opacity-60',
35
+ failed: 'border-solid border-red-500/40',
36
+ cancelled: 'border-solid border-muted-foreground/30 opacity-50',
37
+ };
38
+
39
+ const STATUS_LABEL: Record<string, string> = {
40
+ assembling: 'Assembling',
41
+ dispatched: 'Dispatched',
42
+ in_progress: 'Running',
43
+ completed: 'Complete',
44
+ failed: 'Failed',
45
+ cancelled: 'Cancelled',
46
+ };
47
+
48
+ function totalEffortHours(sprint: Sprint): string {
49
+ let total = 0;
50
+ for (const ss of sprint.scopes) {
51
+ if (!ss.effort_estimate) continue;
52
+ const match = ss.effort_estimate.toLowerCase().match(/(\d+(?:\.\d+)?)\s*hour/);
53
+ if (match) total += parseFloat(match[1]);
54
+ const minMatch = ss.effort_estimate.toLowerCase().match(/(\d+)\s*min/);
55
+ if (minMatch) total += parseInt(minMatch[1]) / 60;
56
+ }
57
+ if (total === 0) return 'TBD';
58
+ return total < 1 ? `${Math.round(total * 60)}M` : `~${total.toFixed(0)}H`;
59
+ }
60
+
61
+ export function SprintContainer({ sprint, scopeLookup, onDelete, onDispatch, onRename, onScopeClick, cardDisplay, dimmedIds, looseCount, onAddAll, editingName, onEditingDone }: SprintContainerProps) {
62
+ const { engine } = useWorkflow();
63
+ const isAssembling = sprint.status === 'assembling';
64
+ const [isEditing, setIsEditing] = useState(editingName ?? false);
65
+ const [draftName, setDraftName] = useState(sprint.name);
66
+ const inputRef = useRef<HTMLInputElement>(null);
67
+
68
+ useEffect(() => {
69
+ if (editingName) {
70
+ setIsEditing(true);
71
+ setDraftName('');
72
+ }
73
+ }, [editingName]);
74
+
75
+ useEffect(() => {
76
+ if (isEditing) inputRef.current?.focus();
77
+ }, [isEditing]);
78
+
79
+ const commitName = () => {
80
+ const trimmed = draftName.trim();
81
+ if (trimmed && trimmed !== sprint.name && onRename) {
82
+ onRename(sprint.id, trimmed);
83
+ }
84
+ setIsEditing(false);
85
+ setDraftName(trimmed || sprint.name);
86
+ onEditingDone?.();
87
+ };
88
+ const isBatch = sprint.group_type === 'batch';
89
+ const batchActionLabel = isBatch
90
+ ? (() => {
91
+ const target = engine.getBatchTargetStatus(sprint.target_column);
92
+ return target ? engine.findEdge(sprint.target_column, target)?.label ?? 'Dispatch' : 'Dispatch';
93
+ })()
94
+ : undefined;
95
+
96
+ // Only sprints are draggable (batches dispatch via header button)
97
+ const {
98
+ attributes: dragAttrs,
99
+ listeners: dragListeners,
100
+ setNodeRef: setDragRef,
101
+ transform,
102
+ isDragging,
103
+ } = useDraggable({
104
+ id: `sprint-${sprint.id}`,
105
+ disabled: isBatch || !isAssembling || sprint.scope_ids.length === 0,
106
+ });
107
+
108
+ const { setNodeRef: setDropRef, isOver } = useDroppable({
109
+ id: `sprint-drop-${sprint.id}`,
110
+ disabled: !isAssembling,
111
+ });
112
+
113
+ const dragStyle = transform ? { transform: CSS.Translate.toString(transform) } : undefined;
114
+
115
+ const totalScopes = sprint.scope_ids.length;
116
+ const { progress } = sprint;
117
+ const canDispatch = isBatch && isAssembling && totalScopes > 0 && onDispatch;
118
+
119
+ // Icon and border vary by group_type
120
+ const Icon = isBatch ? Package : Layers;
121
+ const iconColor = isBatch ? 'text-amber-400' : 'text-cyan-400';
122
+ const borderStyle = isBatch && isAssembling
123
+ ? 'border-muted-foreground/30'
124
+ : STATUS_STYLE[sprint.status] ?? 'border-muted-foreground/30';
125
+
126
+ return (
127
+ <div
128
+ ref={setDragRef}
129
+ style={{
130
+ ...dragStyle,
131
+ ...(isBatch && isAssembling ? { borderColor: (engine.getList(sprint.target_column)?.hex ?? '') + '80' } : undefined),
132
+ }}
133
+ className={cn(
134
+ 'rounded-lg border bg-card/30 transition-all duration-200',
135
+ borderStyle,
136
+ isDragging && 'opacity-30',
137
+ !isBatch && isAssembling && 'cursor-grab active:cursor-grabbing',
138
+ )}
139
+ {...(isBatch ? {} : dragAttrs)}
140
+ {...(isBatch ? {} : dragListeners)}
141
+ >
142
+ {/* Header */}
143
+ <div className="flex items-center gap-2 px-2.5 py-1.5 border-b border-inherit">
144
+ <Icon className={cn('h-3 w-3 shrink-0', iconColor)} />
145
+ {isEditing ? (
146
+ <input
147
+ ref={inputRef}
148
+ className="min-w-0 flex-1 h-5 rounded bg-muted/50 px-1.5 text-xs font-medium text-foreground border border-cyan-500/30 focus:outline-none focus:ring-1 focus:ring-cyan-500/50"
149
+ placeholder={isBatch ? 'Batch name...' : 'Sprint name...'}
150
+ value={draftName}
151
+ onChange={(e) => setDraftName(e.target.value)}
152
+ onKeyDown={(e) => {
153
+ if (e.key === 'Enter') commitName();
154
+ if (e.key === 'Escape') { setIsEditing(false); setDraftName(sprint.name); onEditingDone?.(); }
155
+ }}
156
+ onBlur={commitName}
157
+ onClick={(e) => e.stopPropagation()}
158
+ />
159
+ ) : (
160
+ <span
161
+ className={cn('text-xs font-medium text-foreground truncate flex-1', isAssembling && 'cursor-text')}
162
+ onDoubleClick={() => { if (isAssembling) { setIsEditing(true); setDraftName(sprint.name); } }}
163
+ >
164
+ {sprint.name}
165
+ </span>
166
+ )}
167
+ <span className={cn(
168
+ 'rounded px-1 py-0.5 text-[10px] uppercase',
169
+ sprint.status === 'dispatched' || sprint.status === 'in_progress'
170
+ ? 'bg-amber-500/20 text-amber-400'
171
+ : sprint.status === 'completed'
172
+ ? 'bg-green-500/20 text-green-400'
173
+ : sprint.status === 'failed'
174
+ ? 'bg-red-500/20 text-red-400'
175
+ : 'text-muted-foreground',
176
+ )}>
177
+ {STATUS_LABEL[sprint.status]}
178
+ </span>
179
+ {isAssembling && (looseCount ?? 0) > 0 && onAddAll && (
180
+ <button
181
+ onClick={(e) => { e.stopPropagation(); onAddAll(sprint.id); }}
182
+ className="shrink-0 flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
183
+ title={`Add all ${looseCount} remaining scopes`}
184
+ >
185
+ + All ({looseCount})
186
+ </button>
187
+ )}
188
+ {canDispatch && (
189
+ <button
190
+ onClick={(e) => { e.stopPropagation(); onDispatch(sprint.id); }}
191
+ className="shrink-0 flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] bg-cyan-600/80 text-black hover:bg-cyan-500/80 transition-colors"
192
+ title={batchActionLabel ?? 'Dispatch'}
193
+ >
194
+ <Play className="h-2.5 w-2.5" />
195
+ {batchActionLabel ?? 'Dispatch'}
196
+ </button>
197
+ )}
198
+ {isAssembling && onDelete && (
199
+ <button
200
+ onClick={(e) => { e.stopPropagation(); onDelete(sprint.id); }}
201
+ className="shrink-0 text-muted-foreground hover:text-red-400 transition-colors"
202
+ >
203
+ <X className="h-3 w-3" />
204
+ </button>
205
+ )}
206
+ </div>
207
+
208
+ {/* Scope Cards (inside the sprint) */}
209
+ <div
210
+ ref={setDropRef}
211
+ className={cn(
212
+ 'p-1.5 space-y-1 min-h-[40px] transition-colors duration-150',
213
+ isOver && isAssembling && 'bg-cyan-500/5 ring-1 ring-inset ring-cyan-500/30 rounded-b-lg',
214
+ )}
215
+ >
216
+ {sprint.scope_ids.map((scopeId) => {
217
+ const scope = scopeLookup.get(scopeId);
218
+ if (!scope) {
219
+ // Fallback: show minimal info from sprint scope data
220
+ const ss = sprint.scopes.find((s) => s.scope_id === scopeId);
221
+ return (
222
+ <div key={scopeId} className="rounded border border-muted-foreground/20 bg-card/50 px-2 py-1 text-xs text-muted-foreground">
223
+ <span className="font-mono">{formatScopeId(scopeId)}</span>
224
+ {ss && <span className="ml-2">{ss.title}</span>}
225
+ </div>
226
+ );
227
+ }
228
+ return (
229
+ <ScopeCard key={scopeId} scope={scope} onClick={onScopeClick} cardDisplay={cardDisplay} dimmed={dimmedIds?.has(scopeId)} />
230
+ );
231
+ })}
232
+ {totalScopes === 0 && isAssembling && isOver && (
233
+ <p className="py-3 text-center text-[10px] text-muted-foreground/50">
234
+ Drop to add
235
+ </p>
236
+ )}
237
+ </div>
238
+
239
+ {/* Footer: effort + scope count + progress + dispatch result */}
240
+ <div className="flex items-center justify-between border-t border-inherit px-2.5 py-1">
241
+ <span className="text-[10px] text-muted-foreground">
242
+ {isBatch ? batchActionLabel ?? 'Batch' : `Effort: ${totalEffortHours(sprint)}`}
243
+ </span>
244
+ <span className="text-[10px] text-muted-foreground">
245
+ {totalScopes} scope{totalScopes !== 1 ? 's' : ''}
246
+ </span>
247
+ {sprint.status !== 'assembling' && totalScopes > 0 && (
248
+ <div className="flex items-center gap-1">
249
+ {progress.completed > 0 && (
250
+ <span className="text-[10px] text-green-400">{progress.completed} done</span>
251
+ )}
252
+ {progress.failed > 0 && (
253
+ <span className="text-[10px] text-red-400">{progress.failed} fail</span>
254
+ )}
255
+ {progress.in_progress > 0 && (
256
+ <span className="text-[10px] text-amber-400">{progress.in_progress} active</span>
257
+ )}
258
+ </div>
259
+ )}
260
+ </div>
261
+ {/* Dispatch result (batch only — commit SHA / PR link) */}
262
+ {isBatch && sprint.dispatch_result && (
263
+ <div className="border-t border-inherit px-2.5 py-1 text-[10px] text-muted-foreground space-y-0.5">
264
+ {sprint.dispatch_result.commit_sha && (
265
+ <span className="font-mono">{sprint.dispatch_result.commit_sha.slice(0, 7)}</span>
266
+ )}
267
+ {sprint.dispatch_result.pr_url && (
268
+ <a
269
+ href={sprint.dispatch_result.pr_url}
270
+ target="_blank"
271
+ rel="noopener noreferrer"
272
+ className="text-cyan-400 hover:underline ml-1"
273
+ >
274
+ PR #{sprint.dispatch_result.pr_number ?? ''}
275
+ </a>
276
+ )}
277
+ </div>
278
+ )}
279
+ </div>
280
+ );
281
+ }
282
+
283
+ /** Compact sprint preview for drag overlay */
284
+ export function SprintDragPreview({ sprint }: { sprint: Sprint }) {
285
+ return (
286
+ <div className="w-72 rotate-1 opacity-90 shadow-xl shadow-black/40 rounded-lg border border-cyan-500/40 bg-card/80 p-2">
287
+ <div className="flex items-center gap-2 mb-1">
288
+ <Layers className="h-3 w-3 text-cyan-400" />
289
+ <span className="text-xs font-medium">{sprint.name}</span>
290
+ </div>
291
+ <div className="space-y-0.5">
292
+ {sprint.scopes.slice(0, 3).map((ss) => (
293
+ <div key={ss.scope_id} className="rounded bg-muted/30 px-1.5 py-0.5 text-[10px] text-muted-foreground truncate">
294
+ {formatScopeId(ss.scope_id)} {ss.title}
295
+ </div>
296
+ ))}
297
+ {sprint.scopes.length > 3 && (
298
+ <p className="text-[10px] text-muted-foreground text-center">+{sprint.scopes.length - 3} more</p>
299
+ )}
300
+ </div>
301
+ </div>
302
+ );
303
+ }