prjct-cli 0.18.2 → 0.20.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 (246) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CLAUDE.md +74 -211
  3. package/core/agentic/prompt-builder.ts +3 -7
  4. package/core/command-registry/optional-commands.ts +0 -20
  5. package/core/infrastructure/command-installer/command-installer.ts +8 -1
  6. package/core/infrastructure/command-installer/global-config.ts +31 -1
  7. package/core/infrastructure/command-installer/index.ts +1 -1
  8. package/core/infrastructure/setup.ts +3 -0
  9. package/package.json +3 -17
  10. package/templates/agentic/agents/uxui.md +210 -0
  11. package/templates/commands/bug.md +219 -41
  12. package/templates/commands/done.md +57 -258
  13. package/templates/commands/feature.md +368 -80
  14. package/templates/commands/now.md +72 -277
  15. package/templates/commands/ship.md +167 -246
  16. package/templates/commands/sync.md +62 -3
  17. package/templates/commands/test.md +160 -20
  18. package/templates/global/CLAUDE.md +40 -205
  19. package/templates/global/docs/agents.md +88 -0
  20. package/templates/global/docs/architecture.md +103 -0
  21. package/templates/global/docs/commands.md +98 -0
  22. package/templates/global/docs/validation.md +95 -0
  23. package/bin/dev.js +0 -216
  24. package/bin/serve.js +0 -361
  25. package/packages/web/README.md +0 -36
  26. package/packages/web/app/api/claude/sessions/route.ts +0 -44
  27. package/packages/web/app/api/claude/status/route.ts +0 -34
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
  29. package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
  30. package/packages/web/app/api/projects/[id]/route.ts +0 -29
  31. package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
  32. package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
  33. package/packages/web/app/api/projects/route.ts +0 -16
  34. package/packages/web/app/api/sessions/current/route.ts +0 -132
  35. package/packages/web/app/api/sessions/history/route.ts +0 -204
  36. package/packages/web/app/error.tsx +0 -34
  37. package/packages/web/app/favicon.ico +0 -0
  38. package/packages/web/app/globals.css +0 -198
  39. package/packages/web/app/layout.tsx +0 -53
  40. package/packages/web/app/loading.tsx +0 -7
  41. package/packages/web/app/not-found.tsx +0 -25
  42. package/packages/web/app/page.tsx +0 -12
  43. package/packages/web/app/project/[id]/code/layout.tsx +0 -18
  44. package/packages/web/app/project/[id]/code/page.tsx +0 -408
  45. package/packages/web/app/project/[id]/error.tsx +0 -41
  46. package/packages/web/app/project/[id]/loading.tsx +0 -9
  47. package/packages/web/app/project/[id]/not-found.tsx +0 -27
  48. package/packages/web/app/project/[id]/page.tsx +0 -384
  49. package/packages/web/app/project/[id]/reports/page.tsx +0 -59
  50. package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
  51. package/packages/web/app/sessions/page.tsx +0 -165
  52. package/packages/web/app/settings/page.tsx +0 -151
  53. package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
  54. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
  55. package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
  56. package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
  57. package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
  58. package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
  59. package/packages/web/components/ActivityTimeline/index.ts +0 -2
  60. package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
  61. package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
  62. package/packages/web/components/AgentsCard/index.ts +0 -2
  63. package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
  64. package/packages/web/components/AppSidebar/index.ts +0 -1
  65. package/packages/web/components/BackLink/BackLink.tsx +0 -18
  66. package/packages/web/components/BackLink/BackLink.types.ts +0 -5
  67. package/packages/web/components/BackLink/index.ts +0 -2
  68. package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
  69. package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
  70. package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
  71. package/packages/web/components/BentoCard/index.ts +0 -2
  72. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
  73. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
  74. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
  75. package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
  76. package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
  77. package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
  78. package/packages/web/components/BentoGrid/index.ts +0 -2
  79. package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
  80. package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
  81. package/packages/web/components/BlockersCard/index.ts +0 -2
  82. package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
  83. package/packages/web/components/CommandBar/index.ts +0 -1
  84. package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
  85. package/packages/web/components/CommandButton/index.ts +0 -1
  86. package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
  87. package/packages/web/components/ConnectionStatus/index.ts +0 -1
  88. package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
  89. package/packages/web/components/DashboardContent/index.ts +0 -1
  90. package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
  91. package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
  92. package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
  93. package/packages/web/components/DateGroup/index.ts +0 -2
  94. package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
  95. package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
  96. package/packages/web/components/EmptyState/index.ts +0 -2
  97. package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
  98. package/packages/web/components/EventRow/EventRow.tsx +0 -49
  99. package/packages/web/components/EventRow/EventRow.types.ts +0 -7
  100. package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
  101. package/packages/web/components/EventRow/index.ts +0 -2
  102. package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
  103. package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
  104. package/packages/web/components/ExpandButton/index.ts +0 -2
  105. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
  106. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
  107. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
  108. package/packages/web/components/HealthGradientBackground/index.ts +0 -2
  109. package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
  110. package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
  111. package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
  112. package/packages/web/components/HeroSection/hooks/index.ts +0 -2
  113. package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
  114. package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
  115. package/packages/web/components/HeroSection/index.ts +0 -2
  116. package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
  117. package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
  118. package/packages/web/components/IdeasCard/index.ts +0 -2
  119. package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
  120. package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
  121. package/packages/web/components/InsightMessage/index.ts +0 -2
  122. package/packages/web/components/Logo/Logo.tsx +0 -65
  123. package/packages/web/components/Logo/index.ts +0 -1
  124. package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
  125. package/packages/web/components/MarkdownContent/index.ts +0 -1
  126. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
  127. package/packages/web/components/MasonryGrid/index.ts +0 -1
  128. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
  129. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
  130. package/packages/web/components/MomentumWidget/index.ts +0 -2
  131. package/packages/web/components/NowCard/NowCard.tsx +0 -118
  132. package/packages/web/components/NowCard/NowCard.types.ts +0 -16
  133. package/packages/web/components/NowCard/index.ts +0 -2
  134. package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
  135. package/packages/web/components/PageHeader/index.ts +0 -1
  136. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
  137. package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
  138. package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
  139. package/packages/web/components/ProgressRing/index.ts +0 -2
  140. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
  141. package/packages/web/components/ProjectAvatar/index.ts +0 -1
  142. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
  143. package/packages/web/components/ProjectColorDot/index.ts +0 -1
  144. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
  145. package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
  146. package/packages/web/components/Providers/Providers.tsx +0 -48
  147. package/packages/web/components/Providers/index.ts +0 -1
  148. package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
  149. package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
  150. package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
  151. package/packages/web/components/QueueCard/index.ts +0 -2
  152. package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
  153. package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
  154. package/packages/web/components/RecoverCard/index.ts +0 -2
  155. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
  156. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
  157. package/packages/web/components/RoadmapCard/index.ts +0 -2
  158. package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
  159. package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
  160. package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
  161. package/packages/web/components/ShipsCard/index.ts +0 -2
  162. package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
  163. package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
  164. package/packages/web/components/SparklineChart/index.ts +0 -2
  165. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
  166. package/packages/web/components/StatsMasonry/index.ts +0 -1
  167. package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
  168. package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
  169. package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
  170. package/packages/web/components/StreakCard/index.ts +0 -2
  171. package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
  172. package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
  173. package/packages/web/components/TasksCounter/index.ts +0 -2
  174. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
  175. package/packages/web/components/TechStackBadges/index.ts +0 -1
  176. package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
  177. package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
  178. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
  179. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
  180. package/packages/web/components/TerminalDock/index.ts +0 -2
  181. package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
  182. package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
  183. package/packages/web/components/TerminalTabs/index.ts +0 -1
  184. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
  185. package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
  186. package/packages/web/components/VelocityBadge/index.ts +0 -2
  187. package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
  188. package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
  189. package/packages/web/components/VelocityCard/index.ts +0 -2
  190. package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
  191. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
  192. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
  193. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
  194. package/packages/web/components/WeeklyReports/index.ts +0 -4
  195. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
  196. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
  197. package/packages/web/components/WeeklySparkline/index.ts +0 -2
  198. package/packages/web/components/charts/SessionsChart.tsx +0 -175
  199. package/packages/web/components/ui/alert-dialog.tsx +0 -157
  200. package/packages/web/components/ui/badge.tsx +0 -46
  201. package/packages/web/components/ui/button.tsx +0 -60
  202. package/packages/web/components/ui/card.tsx +0 -92
  203. package/packages/web/components/ui/chart.tsx +0 -385
  204. package/packages/web/components/ui/dialog.tsx +0 -143
  205. package/packages/web/components/ui/drawer.tsx +0 -135
  206. package/packages/web/components/ui/dropdown-menu.tsx +0 -257
  207. package/packages/web/components/ui/input.tsx +0 -21
  208. package/packages/web/components/ui/scroll-area.tsx +0 -58
  209. package/packages/web/components/ui/select.tsx +0 -187
  210. package/packages/web/components/ui/sheet.tsx +0 -139
  211. package/packages/web/components/ui/tabs.tsx +0 -66
  212. package/packages/web/components/ui/tooltip.tsx +0 -61
  213. package/packages/web/components.json +0 -22
  214. package/packages/web/context/GlobalTerminalContext.tsx +0 -538
  215. package/packages/web/context/TerminalContext.tsx +0 -45
  216. package/packages/web/context/TerminalTabsContext.tsx +0 -181
  217. package/packages/web/eslint.config.mjs +0 -18
  218. package/packages/web/hooks/useClaudeTerminal.ts +0 -425
  219. package/packages/web/hooks/useProjectStats.ts +0 -93
  220. package/packages/web/hooks/useProjects.ts +0 -73
  221. package/packages/web/lib/actions/projects.ts +0 -15
  222. package/packages/web/lib/commands.ts +0 -81
  223. package/packages/web/lib/format.ts +0 -23
  224. package/packages/web/lib/generate-week-report.ts +0 -285
  225. package/packages/web/lib/parse-prjct-files.ts +0 -1123
  226. package/packages/web/lib/project-colors.ts +0 -58
  227. package/packages/web/lib/projects.ts +0 -506
  228. package/packages/web/lib/pty.ts +0 -101
  229. package/packages/web/lib/query-config.ts +0 -44
  230. package/packages/web/lib/services/index.ts +0 -9
  231. package/packages/web/lib/services/projects.server.ts +0 -66
  232. package/packages/web/lib/services/stats.server.ts +0 -562
  233. package/packages/web/lib/unified-loader.ts +0 -396
  234. package/packages/web/lib/utils.ts +0 -6
  235. package/packages/web/next-env.d.ts +0 -6
  236. package/packages/web/next.config.ts +0 -7
  237. package/packages/web/package.json +0 -57
  238. package/packages/web/postcss.config.mjs +0 -7
  239. package/packages/web/public/file.svg +0 -1
  240. package/packages/web/public/globe.svg +0 -1
  241. package/packages/web/public/next.svg +0 -1
  242. package/packages/web/public/vercel.svg +0 -1
  243. package/packages/web/public/window.svg +0 -1
  244. package/packages/web/server.ts +0 -312
  245. package/packages/web/tsconfig.json +0 -34
  246. package/templates/commands/serve.md +0 -121
@@ -1,66 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as TabsPrimitive from "@radix-ui/react-tabs"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- function Tabs({
9
- className,
10
- ...props
11
- }: React.ComponentProps<typeof TabsPrimitive.Root>) {
12
- return (
13
- <TabsPrimitive.Root
14
- data-slot="tabs"
15
- className={cn("flex flex-col gap-2", className)}
16
- {...props}
17
- />
18
- )
19
- }
20
-
21
- function TabsList({
22
- className,
23
- ...props
24
- }: React.ComponentProps<typeof TabsPrimitive.List>) {
25
- return (
26
- <TabsPrimitive.List
27
- data-slot="tabs-list"
28
- className={cn(
29
- "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
30
- className
31
- )}
32
- {...props}
33
- />
34
- )
35
- }
36
-
37
- function TabsTrigger({
38
- className,
39
- ...props
40
- }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
41
- return (
42
- <TabsPrimitive.Trigger
43
- data-slot="tabs-trigger"
44
- className={cn(
45
- "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
46
- className
47
- )}
48
- {...props}
49
- />
50
- )
51
- }
52
-
53
- function TabsContent({
54
- className,
55
- ...props
56
- }: React.ComponentProps<typeof TabsPrimitive.Content>) {
57
- return (
58
- <TabsPrimitive.Content
59
- data-slot="tabs-content"
60
- className={cn("flex-1 outline-none", className)}
61
- {...props}
62
- />
63
- )
64
- }
65
-
66
- export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -1,61 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- function TooltipProvider({
9
- delayDuration = 0,
10
- ...props
11
- }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
- return (
13
- <TooltipPrimitive.Provider
14
- data-slot="tooltip-provider"
15
- delayDuration={delayDuration}
16
- {...props}
17
- />
18
- )
19
- }
20
-
21
- function Tooltip({
22
- ...props
23
- }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
- return (
25
- <TooltipProvider>
26
- <TooltipPrimitive.Root data-slot="tooltip" {...props} />
27
- </TooltipProvider>
28
- )
29
- }
30
-
31
- function TooltipTrigger({
32
- ...props
33
- }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
34
- return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
35
- }
36
-
37
- function TooltipContent({
38
- className,
39
- sideOffset = 0,
40
- children,
41
- ...props
42
- }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
43
- return (
44
- <TooltipPrimitive.Portal>
45
- <TooltipPrimitive.Content
46
- data-slot="tooltip-content"
47
- sideOffset={sideOffset}
48
- className={cn(
49
- "bg-popover text-popover-foreground border shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
50
- className
51
- )}
52
- {...props}
53
- >
54
- {children}
55
- <TooltipPrimitive.Arrow className="bg-popover fill-popover z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
56
- </TooltipPrimitive.Content>
57
- </TooltipPrimitive.Portal>
58
- )
59
- }
60
-
61
- export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
4
- "rsc": true,
5
- "tsx": true,
6
- "tailwind": {
7
- "config": "",
8
- "css": "app/globals.css",
9
- "baseColor": "neutral",
10
- "cssVariables": true,
11
- "prefix": ""
12
- },
13
- "iconLibrary": "lucide",
14
- "aliases": {
15
- "components": "@/components",
16
- "utils": "@/lib/utils",
17
- "ui": "@/components/ui",
18
- "lib": "@/lib",
19
- "hooks": "@/hooks"
20
- },
21
- "registries": {}
22
- }
@@ -1,538 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Global Terminal Context - Manage terminal sessions across ALL projects
5
- *
6
- * Features:
7
- * - Multi-project session management (Map<projectId, sessions[]>)
8
- * - Bottom dock panel UI state (height, open/closed)
9
- * - Full-screen mode for code page
10
- * - Persistence to localStorage
11
- * - Cross-page navigation support
12
- */
13
-
14
- import { createContext, useContext, useCallback, useState, useRef, useEffect, ReactNode } from 'react'
15
-
16
- // ============================================================================
17
- // Types
18
- // ============================================================================
19
-
20
- export interface GlobalTerminalSession {
21
- id: string
22
- projectId: string
23
- projectName: string
24
- projectPath: string
25
- createdAt: Date
26
- isConnected: boolean
27
- isLoading: boolean
28
- label: string
29
- panel: 'left' | 'right' // Which panel this session belongs to
30
- }
31
-
32
- export interface ProjectTerminals {
33
- projectId: string
34
- projectName: string
35
- projectPath: string
36
- sessions: GlobalTerminalSession[]
37
- }
38
-
39
- interface GlobalTerminalContextType {
40
- // Session management
41
- projectSessions: Map<string, ProjectTerminals>
42
- activeProjectId: string | null
43
- activeSessionId: string | null
44
-
45
- // Dock UI state
46
- isDockOpen: boolean
47
- dockHeight: number
48
- isFullScreen: boolean // When true, terminal takes full main area (code page)
49
- isSplitEnabled: boolean // Split view mode
50
- secondActiveSessionId: string | null // Second panel session (for split)
51
-
52
- // Session actions
53
- createSessionForProject: (projectId: string, projectName: string, projectPath: string, panel?: 'left' | 'right') => string
54
- closeSession: (sessionId: string) => void
55
- switchProject: (projectId: string) => void
56
- switchSession: (sessionId: string, panel?: 'left' | 'right') => void
57
- updateSession: (sessionId: string, updates: Partial<GlobalTerminalSession>) => void
58
-
59
- // Dock UI actions
60
- openDock: () => void
61
- closeDock: () => void
62
- toggleDock: () => void
63
- setDockHeight: (height: number) => void
64
- setFullScreen: (isFullScreen: boolean) => void
65
- setSplitEnabled: (enabled: boolean) => void
66
-
67
- // Helpers
68
- getActiveSession: () => GlobalTerminalSession | null
69
- getSecondActiveSession: () => GlobalTerminalSession | null
70
- getProjectSessions: (projectId: string) => GlobalTerminalSession[]
71
- getTotalSessionCount: () => number
72
- getConnectedSessionCount: () => number
73
- getAllSessions: () => GlobalTerminalSession[]
74
- getLeftPanelSessions: () => GlobalTerminalSession[]
75
- getRightPanelSessions: () => GlobalTerminalSession[]
76
- moveSessionToPanel: (sessionId: string, panel: 'left' | 'right') => void
77
-
78
- // Input/focus refs
79
- sendCommandToActive: (command: string) => void
80
- registerSendInput: (sessionId: string, fn: (data: string) => void) => void
81
- registerFocusTerminal: (sessionId: string, fn: () => void) => void
82
- focusActiveTerminal: () => void
83
- }
84
-
85
- // ============================================================================
86
- // Constants
87
- // ============================================================================
88
-
89
- const STORAGE_KEYS = {
90
- dockHeight: 'prjct-terminal-dock-height',
91
- dockOpen: 'prjct-terminal-dock-open',
92
- activeProject: 'prjct-terminal-active-project',
93
- splitEnabled: 'prjct-terminal-split-enabled',
94
- }
95
-
96
- const DEFAULT_DOCK_HEIGHT = 300
97
- const MIN_DOCK_HEIGHT = 150
98
- // Dynamic max height - 90% of viewport
99
- const getMaxDockHeight = () => typeof window !== 'undefined' ? window.innerHeight * 0.9 : 800
100
-
101
- // ============================================================================
102
- // Context
103
- // ============================================================================
104
-
105
- const GlobalTerminalContext = createContext<GlobalTerminalContextType | null>(null)
106
-
107
- let sessionCounter = 0
108
-
109
- // ============================================================================
110
- // Provider
111
- // ============================================================================
112
-
113
- export function GlobalTerminalProvider({ children }: { children: ReactNode }) {
114
- // Session state
115
- const [projectSessions, setProjectSessions] = useState<Map<string, ProjectTerminals>>(new Map())
116
- const [activeProjectId, setActiveProjectId] = useState<string | null>(null)
117
- const [activeSessionId, setActiveSessionId] = useState<string | null>(null)
118
-
119
- // Dock UI state
120
- const [isDockOpen, setIsDockOpen] = useState(false)
121
- const [dockHeight, setDockHeightState] = useState(DEFAULT_DOCK_HEIGHT)
122
- const [isFullScreen, setIsFullScreen] = useState(false)
123
- const [isSplitEnabled, setIsSplitEnabledState] = useState(false)
124
- const [secondActiveSessionId, setSecondActiveSessionId] = useState<string | null>(null)
125
-
126
- // Refs for input/focus functions per session
127
- const sendInputRefs = useRef<Map<string, (data: string) => void>>(new Map())
128
- const focusTerminalRefs = useRef<Map<string, () => void>>(new Map())
129
-
130
- // -------------------------------------------------------------------------
131
- // Load persisted state from localStorage
132
- // -------------------------------------------------------------------------
133
- useEffect(() => {
134
- if (typeof window === 'undefined') return
135
-
136
- try {
137
- const savedDockHeight = localStorage.getItem(STORAGE_KEYS.dockHeight)
138
- if (savedDockHeight) {
139
- const parsed = parseInt(savedDockHeight, 10)
140
- if (!isNaN(parsed)) {
141
- setDockHeightState(Math.max(MIN_DOCK_HEIGHT, Math.min(parsed, getMaxDockHeight())))
142
- }
143
- }
144
-
145
- const savedDockOpen = localStorage.getItem(STORAGE_KEYS.dockOpen)
146
- if (savedDockOpen) {
147
- setIsDockOpen(JSON.parse(savedDockOpen))
148
- }
149
-
150
- const savedActiveProject = localStorage.getItem(STORAGE_KEYS.activeProject)
151
- if (savedActiveProject) {
152
- setActiveProjectId(savedActiveProject)
153
- }
154
- } catch (e) {
155
- console.warn('Failed to load terminal state from localStorage:', e)
156
- }
157
- }, [])
158
-
159
- // -------------------------------------------------------------------------
160
- // Persist state to localStorage
161
- // -------------------------------------------------------------------------
162
- const setDockHeight = useCallback((newHeight: number) => {
163
- const maxHeight = getMaxDockHeight()
164
- const clampedHeight = Math.max(MIN_DOCK_HEIGHT, Math.min(newHeight, maxHeight))
165
- setDockHeightState(clampedHeight)
166
- if (typeof window !== 'undefined') {
167
- localStorage.setItem(STORAGE_KEYS.dockHeight, String(clampedHeight))
168
- }
169
- }, [])
170
-
171
- const setSplitEnabled = useCallback((enabled: boolean) => {
172
- setIsSplitEnabledState(enabled)
173
- if (typeof window !== 'undefined') {
174
- localStorage.setItem(STORAGE_KEYS.splitEnabled, String(enabled))
175
- }
176
- // If disabling split, clear second panel
177
- if (!enabled) {
178
- setSecondActiveSessionId(null)
179
- }
180
- }, [])
181
-
182
- // -------------------------------------------------------------------------
183
- // Session management
184
- // -------------------------------------------------------------------------
185
- const createSessionForProject = useCallback((projectId: string, projectName: string, projectPath: string, panel: 'left' | 'right' = 'left') => {
186
- sessionCounter++
187
- const newSession: GlobalTerminalSession = {
188
- id: `pty_${projectId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
189
- projectId,
190
- projectName,
191
- projectPath,
192
- createdAt: new Date(),
193
- isConnected: false,
194
- isLoading: true,
195
- label: `Terminal ${sessionCounter}`,
196
- panel,
197
- }
198
-
199
- setProjectSessions(prev => {
200
- const newMap = new Map(prev)
201
- const existing = newMap.get(projectId)
202
-
203
- if (existing) {
204
- newMap.set(projectId, {
205
- ...existing,
206
- sessions: [...existing.sessions, newSession],
207
- })
208
- } else {
209
- newMap.set(projectId, {
210
- projectId,
211
- projectName,
212
- projectPath,
213
- sessions: [newSession],
214
- })
215
- }
216
-
217
- return newMap
218
- })
219
-
220
- // Set as active based on panel
221
- setActiveProjectId(projectId)
222
- if (panel === 'right') {
223
- setSecondActiveSessionId(newSession.id)
224
- } else {
225
- setActiveSessionId(newSession.id)
226
- }
227
- localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
228
-
229
- // Open dock if not open
230
- setIsDockOpen(true)
231
- localStorage.setItem(STORAGE_KEYS.dockOpen, 'true')
232
-
233
- return newSession.id
234
- }, [])
235
-
236
- const closeSession = useCallback((sessionId: string) => {
237
- setProjectSessions(prev => {
238
- const newMap = new Map(prev)
239
-
240
- // Find which project this session belongs to
241
- for (const [projectId, project] of newMap) {
242
- const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
243
- if (sessionIndex !== -1) {
244
- const newSessions = project.sessions.filter(s => s.id !== sessionId)
245
-
246
- if (newSessions.length === 0) {
247
- // Remove project entirely if no sessions left
248
- newMap.delete(projectId)
249
- } else {
250
- newMap.set(projectId, { ...project, sessions: newSessions })
251
- }
252
-
253
- // Update active session if needed
254
- if (sessionId === activeSessionId) {
255
- if (newSessions.length > 0) {
256
- setActiveSessionId(newSessions[newSessions.length - 1].id)
257
- } else {
258
- // Switch to another project's session or null
259
- const remainingProjects = Array.from(newMap.values())
260
- if (remainingProjects.length > 0) {
261
- const nextProject = remainingProjects[0]
262
- setActiveProjectId(nextProject.projectId)
263
- setActiveSessionId(nextProject.sessions[nextProject.sessions.length - 1]?.id || null)
264
- } else {
265
- setActiveProjectId(null)
266
- setActiveSessionId(null)
267
- }
268
- }
269
- }
270
-
271
- break
272
- }
273
- }
274
-
275
- return newMap
276
- })
277
-
278
- // Cleanup refs
279
- sendInputRefs.current.delete(sessionId)
280
- focusTerminalRefs.current.delete(sessionId)
281
- }, [activeSessionId])
282
-
283
- const switchProject = useCallback((projectId: string) => {
284
- setActiveProjectId(projectId)
285
- localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
286
-
287
- // Switch to first session of this project
288
- const project = projectSessions.get(projectId)
289
- if (project && project.sessions.length > 0) {
290
- setActiveSessionId(project.sessions[0].id)
291
- }
292
- }, [projectSessions])
293
-
294
- const switchSession = useCallback((sessionId: string, panel: 'left' | 'right' = 'left') => {
295
- if (panel === 'right' && isSplitEnabled) {
296
- setSecondActiveSessionId(sessionId)
297
- } else {
298
- setActiveSessionId(sessionId)
299
-
300
- // Also update active project if session is from different project
301
- for (const [projectId, project] of projectSessions) {
302
- if (project.sessions.some(s => s.id === sessionId)) {
303
- if (projectId !== activeProjectId) {
304
- setActiveProjectId(projectId)
305
- localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
306
- }
307
- break
308
- }
309
- }
310
- }
311
- }, [projectSessions, activeProjectId, isSplitEnabled])
312
-
313
- const updateSession = useCallback((sessionId: string, updates: Partial<GlobalTerminalSession>) => {
314
- setProjectSessions(prev => {
315
- const newMap = new Map(prev)
316
-
317
- for (const [projectId, project] of newMap) {
318
- const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
319
- if (sessionIndex !== -1) {
320
- const newSessions = [...project.sessions]
321
- newSessions[sessionIndex] = { ...newSessions[sessionIndex], ...updates }
322
- newMap.set(projectId, { ...project, sessions: newSessions })
323
- break
324
- }
325
- }
326
-
327
- return newMap
328
- })
329
- }, [])
330
-
331
- // -------------------------------------------------------------------------
332
- // Dock UI actions
333
- // -------------------------------------------------------------------------
334
- const openDock = useCallback(() => {
335
- setIsDockOpen(true)
336
- localStorage.setItem(STORAGE_KEYS.dockOpen, 'true')
337
- }, [])
338
-
339
- const closeDock = useCallback(() => {
340
- setIsDockOpen(false)
341
- localStorage.setItem(STORAGE_KEYS.dockOpen, 'false')
342
- }, [])
343
-
344
- const toggleDock = useCallback(() => {
345
- const newValue = !isDockOpen
346
- setIsDockOpen(newValue)
347
- localStorage.setItem(STORAGE_KEYS.dockOpen, String(newValue))
348
- }, [isDockOpen])
349
-
350
- const setFullScreen = useCallback((value: boolean) => {
351
- setIsFullScreen(value)
352
- }, [])
353
-
354
- // -------------------------------------------------------------------------
355
- // Helpers
356
- // -------------------------------------------------------------------------
357
- const getActiveSession = useCallback((): GlobalTerminalSession | null => {
358
- if (!activeSessionId) return null
359
- for (const project of projectSessions.values()) {
360
- const session = project.sessions.find(s => s.id === activeSessionId)
361
- if (session) return session
362
- }
363
- return null
364
- }, [projectSessions, activeSessionId])
365
-
366
- const getSecondActiveSession = useCallback((): GlobalTerminalSession | null => {
367
- if (!secondActiveSessionId) return null
368
- for (const project of projectSessions.values()) {
369
- const session = project.sessions.find(s => s.id === secondActiveSessionId)
370
- if (session) return session
371
- }
372
- return null
373
- }, [projectSessions, secondActiveSessionId])
374
-
375
- const getProjectSessions = useCallback((projectId: string): GlobalTerminalSession[] => {
376
- return projectSessions.get(projectId)?.sessions || []
377
- }, [projectSessions])
378
-
379
- const getAllSessions = useCallback((): GlobalTerminalSession[] => {
380
- const allSessions: GlobalTerminalSession[] = []
381
- for (const project of projectSessions.values()) {
382
- allSessions.push(...project.sessions)
383
- }
384
- return allSessions
385
- }, [projectSessions])
386
-
387
- const getTotalSessionCount = useCallback((): number => {
388
- let count = 0
389
- for (const project of projectSessions.values()) {
390
- count += project.sessions.length
391
- }
392
- return count
393
- }, [projectSessions])
394
-
395
- const getConnectedSessionCount = useCallback((): number => {
396
- let count = 0
397
- for (const project of projectSessions.values()) {
398
- count += project.sessions.filter(s => s.isConnected).length
399
- }
400
- return count
401
- }, [projectSessions])
402
-
403
- const getLeftPanelSessions = useCallback((): GlobalTerminalSession[] => {
404
- return getAllSessions().filter(s => s.panel === 'left')
405
- }, [getAllSessions])
406
-
407
- const getRightPanelSessions = useCallback((): GlobalTerminalSession[] => {
408
- return getAllSessions().filter(s => s.panel === 'right')
409
- }, [getAllSessions])
410
-
411
- const moveSessionToPanel = useCallback((sessionId: string, panel: 'left' | 'right') => {
412
- setProjectSessions(prev => {
413
- const newMap = new Map(prev)
414
- for (const [projectId, project] of newMap) {
415
- const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
416
- if (sessionIndex !== -1) {
417
- const newSessions = [...project.sessions]
418
- newSessions[sessionIndex] = { ...newSessions[sessionIndex], panel }
419
- newMap.set(projectId, { ...project, sessions: newSessions })
420
- break
421
- }
422
- }
423
- return newMap
424
- })
425
- }, [])
426
-
427
- // -------------------------------------------------------------------------
428
- // Input/Focus management
429
- // -------------------------------------------------------------------------
430
- const registerSendInput = useCallback((sessionId: string, fn: (data: string) => void) => {
431
- sendInputRefs.current.set(sessionId, fn)
432
- }, [])
433
-
434
- const registerFocusTerminal = useCallback((sessionId: string, fn: () => void) => {
435
- focusTerminalRefs.current.set(sessionId, fn)
436
- }, [])
437
-
438
- const focusActiveTerminal = useCallback(() => {
439
- if (!activeSessionId) return
440
- const focusFn = focusTerminalRefs.current.get(activeSessionId)
441
- if (focusFn) focusFn()
442
- }, [activeSessionId])
443
-
444
- const sendCommandToActive = useCallback((command: string) => {
445
- if (!activeSessionId) return
446
- const sendFn = sendInputRefs.current.get(activeSessionId)
447
- const session = getActiveSession()
448
- if (sendFn && session?.isConnected) {
449
- sendFn(command + '\n')
450
- // Auto-focus terminal after sending command
451
- const focusFn = focusTerminalRefs.current.get(activeSessionId)
452
- if (focusFn) focusFn()
453
- }
454
- }, [activeSessionId, getActiveSession])
455
-
456
- // -------------------------------------------------------------------------
457
- // Page unload protection
458
- // -------------------------------------------------------------------------
459
- const hasConnectedSessions = getConnectedSessionCount() > 0
460
-
461
- useEffect(() => {
462
- if (!hasConnectedSessions) return
463
-
464
- const handleBeforeUnload = (e: BeforeUnloadEvent) => {
465
- e.preventDefault()
466
- e.returnValue = 'You have active terminal sessions. Are you sure you want to leave?'
467
- return e.returnValue
468
- }
469
-
470
- window.addEventListener('beforeunload', handleBeforeUnload)
471
- return () => window.removeEventListener('beforeunload', handleBeforeUnload)
472
- }, [hasConnectedSessions])
473
-
474
- // -------------------------------------------------------------------------
475
- // Render
476
- // -------------------------------------------------------------------------
477
- return (
478
- <GlobalTerminalContext.Provider value={{
479
- // Session state
480
- projectSessions,
481
- activeProjectId,
482
- activeSessionId,
483
-
484
- // Dock UI state
485
- isDockOpen,
486
- dockHeight,
487
- isFullScreen,
488
- isSplitEnabled,
489
- secondActiveSessionId,
490
-
491
- // Session actions
492
- createSessionForProject,
493
- closeSession,
494
- switchProject,
495
- switchSession,
496
- updateSession,
497
-
498
- // Dock UI actions
499
- openDock,
500
- closeDock,
501
- toggleDock,
502
- setDockHeight,
503
- setFullScreen,
504
- setSplitEnabled,
505
-
506
- // Helpers
507
- getActiveSession,
508
- getSecondActiveSession,
509
- getProjectSessions,
510
- getTotalSessionCount,
511
- getConnectedSessionCount,
512
- getAllSessions,
513
- getLeftPanelSessions,
514
- getRightPanelSessions,
515
- moveSessionToPanel,
516
-
517
- // Input/focus
518
- sendCommandToActive,
519
- registerSendInput,
520
- registerFocusTerminal,
521
- focusActiveTerminal,
522
- }}>
523
- {children}
524
- </GlobalTerminalContext.Provider>
525
- )
526
- }
527
-
528
- // ============================================================================
529
- // Hook
530
- // ============================================================================
531
-
532
- export function useGlobalTerminal() {
533
- const context = useContext(GlobalTerminalContext)
534
- if (!context) {
535
- throw new Error('useGlobalTerminal must be used within GlobalTerminalProvider')
536
- }
537
- return context
538
- }