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,45 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Terminal Context - Share terminal commands across components
5
- */
6
-
7
- import { createContext, useContext, useRef, useCallback, useState, ReactNode } from 'react'
8
-
9
- interface TerminalContextType {
10
- isConnected: boolean
11
- setIsConnected: (connected: boolean) => void
12
- sendCommand: (command: string) => void
13
- registerSendInput: (fn: (data: string) => void) => void
14
- }
15
-
16
- const TerminalContext = createContext<TerminalContextType | null>(null)
17
-
18
- export function TerminalProvider({ children }: { children: ReactNode }) {
19
- const [isConnected, setIsConnected] = useState(false)
20
- const sendInputRef = useRef<((data: string) => void) | null>(null)
21
-
22
- const registerSendInput = useCallback((fn: (data: string) => void) => {
23
- sendInputRef.current = fn
24
- }, [])
25
-
26
- const sendCommand = useCallback((command: string) => {
27
- if (sendInputRef.current && isConnected) {
28
- sendInputRef.current(command + '\n')
29
- }
30
- }, [isConnected])
31
-
32
- return (
33
- <TerminalContext.Provider value={{ isConnected, setIsConnected, sendCommand, registerSendInput }}>
34
- {children}
35
- </TerminalContext.Provider>
36
- )
37
- }
38
-
39
- export function useTerminalContext() {
40
- const context = useContext(TerminalContext)
41
- if (!context) {
42
- throw new Error('useTerminalContext must be used within TerminalProvider')
43
- }
44
- return context
45
- }
@@ -1,181 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * Terminal Tabs Context - Manage multiple terminal sessions
5
- */
6
-
7
- import { createContext, useContext, useCallback, useState, useRef, useEffect, ReactNode } from 'react'
8
-
9
- export interface TerminalSession {
10
- id: string
11
- createdAt: Date
12
- isConnected: boolean
13
- isLoading: boolean
14
- label: string
15
- }
16
-
17
- interface TerminalTabsContextType {
18
- sessions: TerminalSession[]
19
- activeSessionId: string | null
20
- createSession: () => string
21
- closeSession: (sessionId: string) => void
22
- setActiveSession: (sessionId: string) => void
23
- updateSession: (sessionId: string, updates: Partial<TerminalSession>) => void
24
- getActiveSession: () => TerminalSession | null
25
- sendCommandToActive: (command: string) => void
26
- registerSendInput: (sessionId: string, fn: (data: string) => void) => void
27
- registerFocusTerminal: (sessionId: string, fn: () => void) => void
28
- focusActiveTerminal: () => void
29
- }
30
-
31
- const TerminalTabsContext = createContext<TerminalTabsContextType | null>(null)
32
-
33
- let sessionCounter = 0
34
-
35
- export function TerminalTabsProvider({ children, projectId }: { children: ReactNode; projectId: string }) {
36
- const [sessions, setSessions] = useState<TerminalSession[]>([])
37
- const [activeSessionId, setActiveSessionId] = useState<string | null>(null)
38
- const sendInputRefs = useRef<Map<string, (data: string) => void>>(new Map())
39
- const focusTerminalRefs = useRef<Map<string, () => void>>(new Map())
40
-
41
- const createSession = useCallback(() => {
42
- sessionCounter++
43
- const newSession: TerminalSession = {
44
- id: `pty_${projectId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
45
- createdAt: new Date(),
46
- isConnected: false,
47
- isLoading: true,
48
- label: `Terminal ${sessionCounter}`
49
- }
50
-
51
- setSessions(prev => [...prev, newSession])
52
- setActiveSessionId(newSession.id)
53
- return newSession.id
54
- }, [projectId])
55
-
56
- const closeSession = useCallback((sessionId: string) => {
57
- setSessions(prev => {
58
- const filtered = prev.filter(s => s.id !== sessionId)
59
- // If closing active session, switch to last remaining
60
- if (sessionId === activeSessionId && filtered.length > 0) {
61
- setActiveSessionId(filtered[filtered.length - 1].id)
62
- } else if (filtered.length === 0) {
63
- setActiveSessionId(null)
64
- }
65
- return filtered
66
- })
67
- sendInputRefs.current.delete(sessionId)
68
- focusTerminalRefs.current.delete(sessionId)
69
- }, [activeSessionId])
70
-
71
- const setActiveSession = useCallback((sessionId: string) => {
72
- setActiveSessionId(sessionId)
73
- }, [])
74
-
75
- const updateSession = useCallback((sessionId: string, updates: Partial<TerminalSession>) => {
76
- setSessions(prev => prev.map(s =>
77
- s.id === sessionId ? { ...s, ...updates } : s
78
- ))
79
- }, [])
80
-
81
- const getActiveSession = useCallback(() => {
82
- return sessions.find(s => s.id === activeSessionId) || null
83
- }, [sessions, activeSessionId])
84
-
85
- const registerSendInput = useCallback((sessionId: string, fn: (data: string) => void) => {
86
- sendInputRefs.current.set(sessionId, fn)
87
- }, [])
88
-
89
- const registerFocusTerminal = useCallback((sessionId: string, fn: () => void) => {
90
- focusTerminalRefs.current.set(sessionId, fn)
91
- }, [])
92
-
93
- const focusActiveTerminal = useCallback(() => {
94
- if (!activeSessionId) return
95
- const focusFn = focusTerminalRefs.current.get(activeSessionId)
96
- if (focusFn) focusFn()
97
- }, [activeSessionId])
98
-
99
- const sendCommandToActive = useCallback((command: string) => {
100
- if (!activeSessionId) return
101
- const sendFn = sendInputRefs.current.get(activeSessionId)
102
- const session = sessions.find(s => s.id === activeSessionId)
103
- if (sendFn && session?.isConnected) {
104
- sendFn(command + '\n')
105
- // Auto-focus terminal after sending command
106
- const focusFn = focusTerminalRefs.current.get(activeSessionId)
107
- if (focusFn) focusFn()
108
- }
109
- }, [activeSessionId, sessions])
110
-
111
- // Check if any session is connected
112
- const hasConnectedSessions = sessions.some(s => s.isConnected)
113
-
114
- // Prevent page unload when terminal sessions are active
115
- useEffect(() => {
116
- if (!hasConnectedSessions) return
117
-
118
- const handleBeforeUnload = (e: BeforeUnloadEvent) => {
119
- e.preventDefault()
120
- // Modern browsers require returnValue to be set
121
- e.returnValue = 'You have active terminal sessions. Are you sure you want to leave?'
122
- return e.returnValue
123
- }
124
-
125
- window.addEventListener('beforeunload', handleBeforeUnload)
126
- return () => window.removeEventListener('beforeunload', handleBeforeUnload)
127
- }, [hasConnectedSessions])
128
-
129
- // Prevent back/forward navigation when terminal sessions are active
130
- useEffect(() => {
131
- if (!hasConnectedSessions) return
132
-
133
- // Push a state to history so we can intercept back navigation
134
- const currentPath = window.location.pathname + window.location.search
135
- window.history.pushState({ terminalActive: true }, '', currentPath)
136
-
137
- const handlePopState = (e: PopStateEvent) => {
138
- // User pressed back/forward
139
- const confirmLeave = window.confirm(
140
- 'You have active terminal sessions. Leaving will terminate them. Are you sure?'
141
- )
142
-
143
- if (!confirmLeave) {
144
- // Cancel navigation by pushing state back
145
- window.history.pushState({ terminalActive: true }, '', currentPath)
146
- }
147
- // If confirmed, let the navigation happen naturally
148
- }
149
-
150
- window.addEventListener('popstate', handlePopState)
151
- return () => {
152
- window.removeEventListener('popstate', handlePopState)
153
- }
154
- }, [hasConnectedSessions])
155
-
156
- return (
157
- <TerminalTabsContext.Provider value={{
158
- sessions,
159
- activeSessionId,
160
- createSession,
161
- closeSession,
162
- setActiveSession,
163
- updateSession,
164
- getActiveSession,
165
- sendCommandToActive,
166
- registerSendInput,
167
- registerFocusTerminal,
168
- focusActiveTerminal
169
- }}>
170
- {children}
171
- </TerminalTabsContext.Provider>
172
- )
173
- }
174
-
175
- export function useTerminalTabs() {
176
- const context = useContext(TerminalTabsContext)
177
- if (!context) {
178
- throw new Error('useTerminalTabs must be used within TerminalTabsProvider')
179
- }
180
- return context
181
- }
@@ -1,18 +0,0 @@
1
- import { defineConfig, globalIgnores } from "eslint/config";
2
- import nextVitals from "eslint-config-next/core-web-vitals";
3
- import nextTs from "eslint-config-next/typescript";
4
-
5
- const eslintConfig = defineConfig([
6
- ...nextVitals,
7
- ...nextTs,
8
- // Override default ignores of eslint-config-next.
9
- globalIgnores([
10
- // Default ignores of eslint-config-next:
11
- ".next/**",
12
- "out/**",
13
- "build/**",
14
- "next-env.d.ts",
15
- ]),
16
- ]);
17
-
18
- export default eslintConfig;
@@ -1,425 +0,0 @@
1
- 'use client'
2
-
3
- /**
4
- * useClaudeTerminal - WebSocket connection to Claude Code CLI via PTY
5
- *
6
- * Features:
7
- * - Auto-reconnect with exponential backoff
8
- * - Session persistence across server restarts
9
- * - NO API costs - uses your existing Claude Max subscription!
10
- * - Light/Dark theme support
11
- */
12
-
13
- import { useEffect, useRef, useCallback, useState } from 'react'
14
- import { useTheme } from 'next-themes'
15
- import type { Terminal, ITheme } from '@xterm/xterm'
16
- import type { FitAddon } from '@xterm/addon-fit'
17
-
18
- // Terminal themes
19
- const darkTheme: ITheme = {
20
- background: '#0a0a0f',
21
- foreground: '#e4e4e7',
22
- cursor: '#e4e4e7',
23
- cursorAccent: '#0a0a0f',
24
- selectionBackground: '#3f3f46',
25
- black: '#18181b',
26
- red: '#ef4444',
27
- green: '#22c55e',
28
- yellow: '#eab308',
29
- blue: '#3b82f6',
30
- magenta: '#a855f7',
31
- cyan: '#06b6d4',
32
- white: '#e4e4e7',
33
- brightBlack: '#52525b',
34
- brightRed: '#f87171',
35
- brightGreen: '#4ade80',
36
- brightYellow: '#facc15',
37
- brightBlue: '#60a5fa',
38
- brightMagenta: '#c084fc',
39
- brightCyan: '#22d3ee',
40
- brightWhite: '#fafafa'
41
- }
42
-
43
- const lightTheme: ITheme = {
44
- background: '#ffffff',
45
- foreground: '#18181b',
46
- cursor: '#18181b',
47
- cursorAccent: '#ffffff',
48
- selectionBackground: '#d4d4d8',
49
- black: '#18181b',
50
- red: '#dc2626',
51
- green: '#16a34a',
52
- yellow: '#ca8a04',
53
- blue: '#2563eb',
54
- magenta: '#9333ea',
55
- cyan: '#0891b2',
56
- white: '#f4f4f5',
57
- brightBlack: '#71717a',
58
- brightRed: '#ef4444',
59
- brightGreen: '#22c55e',
60
- brightYellow: '#eab308',
61
- brightBlue: '#3b82f6',
62
- brightMagenta: '#a855f7',
63
- brightCyan: '#06b6d4',
64
- brightWhite: '#fafafa'
65
- }
66
-
67
- interface UseClaudeTerminalOptions {
68
- sessionId: string
69
- projectDir: string
70
- onConnect?: () => void
71
- onDisconnect?: () => void
72
- onError?: (error: string) => void
73
- onReconnecting?: (attempt: number, maxAttempts: number) => void
74
- }
75
-
76
- const MAX_RECONNECT_ATTEMPTS = 10
77
- const BASE_RECONNECT_DELAY = 1000 // 1 second
78
- const MAX_RECONNECT_DELAY = 30000 // 30 seconds
79
-
80
- export function useClaudeTerminal(options: UseClaudeTerminalOptions) {
81
- const { sessionId, projectDir, onConnect, onDisconnect, onError, onReconnecting } = options
82
- const { resolvedTheme } = useTheme()
83
-
84
- const terminalRef = useRef<Terminal | null>(null)
85
- const fitAddonRef = useRef<FitAddon | null>(null)
86
- const wsRef = useRef<WebSocket | null>(null)
87
- const containerRef = useRef<HTMLDivElement | null>(null)
88
- const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
89
- const reconnectAttemptsRef = useRef(0)
90
- const intentionalDisconnectRef = useRef(false)
91
- const currentSessionIdRef = useRef<string | null>(null)
92
- const resizeObserverRef = useRef<ResizeObserver | null>(null)
93
- const outputBufferRef = useRef<string[]>([])
94
- const flushScheduledRef = useRef(false)
95
-
96
- const [isConnected, setIsConnected] = useState(false)
97
- const [isLoading, setIsLoading] = useState(false)
98
- const [isReconnecting, setIsReconnecting] = useState(false)
99
-
100
- // Update terminal theme when resolvedTheme changes
101
- useEffect(() => {
102
- if (terminalRef.current) {
103
- const newTheme = resolvedTheme === 'light' ? lightTheme : darkTheme
104
- terminalRef.current.options.theme = newTheme
105
- }
106
- }, [resolvedTheme])
107
-
108
- // Initialize terminal - returns Promise<void>
109
- const initTerminal = useCallback(async (container: HTMLDivElement): Promise<void> => {
110
- if (terminalRef.current) return
111
-
112
- // Dynamic imports for client-side only
113
- const { Terminal } = await import('@xterm/xterm')
114
- const { FitAddon } = await import('@xterm/addon-fit')
115
- const { WebLinksAddon } = await import('@xterm/addon-web-links')
116
- // CSS is loaded globally via globals.css
117
-
118
- // Determine initial theme based on document class
119
- const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
120
- const initialTheme = isDark ? darkTheme : lightTheme
121
-
122
- const term = new Terminal({
123
- cursorBlink: true,
124
- cursorStyle: 'block',
125
- fontFamily: '"JetBrains Mono", "Fira Code", monospace',
126
- fontSize: 14,
127
- lineHeight: 1.2,
128
- theme: initialTheme,
129
- // Performance optimizations
130
- scrollback: 5000,
131
- smoothScrollDuration: 0, // Instant scroll for better performance
132
- })
133
-
134
- const fitAddon = new FitAddon()
135
- const webLinksAddon = new WebLinksAddon()
136
-
137
- term.loadAddon(fitAddon)
138
- term.loadAddon(webLinksAddon)
139
-
140
- term.open(container)
141
- fitAddon.fit()
142
-
143
- terminalRef.current = term
144
- fitAddonRef.current = fitAddon
145
- containerRef.current = container
146
-
147
- // Use ResizeObserver for better resize handling (works with container, not just window)
148
- const resizeObserver = new ResizeObserver(() => {
149
- if (fitAddonRef.current && containerRef.current) {
150
- // Only fit if container has dimensions (not hidden)
151
- if (containerRef.current.offsetWidth > 0 && containerRef.current.offsetHeight > 0) {
152
- fitAddonRef.current.fit()
153
-
154
- // Send resize to server
155
- if (wsRef.current?.readyState === WebSocket.OPEN && terminalRef.current) {
156
- wsRef.current.send(JSON.stringify({
157
- type: 'resize',
158
- cols: terminalRef.current.cols,
159
- rows: terminalRef.current.rows
160
- }))
161
- }
162
- }
163
- }
164
- })
165
-
166
- resizeObserver.observe(container)
167
- resizeObserverRef.current = resizeObserver
168
- }, [])
169
-
170
- // Clear reconnect timeout
171
- const clearReconnectTimeout = useCallback(() => {
172
- if (reconnectTimeoutRef.current) {
173
- clearTimeout(reconnectTimeoutRef.current)
174
- reconnectTimeoutRef.current = null
175
- }
176
- }, [])
177
-
178
- // Schedule reconnect with exponential backoff
179
- const scheduleReconnect = useCallback(() => {
180
- if (intentionalDisconnectRef.current) return
181
- if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
182
- setIsReconnecting(false)
183
- onError?.(`Failed to reconnect after ${MAX_RECONNECT_ATTEMPTS} attempts`)
184
- return
185
- }
186
-
187
- const delay = Math.min(
188
- BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttemptsRef.current),
189
- MAX_RECONNECT_DELAY
190
- )
191
-
192
- reconnectAttemptsRef.current++
193
- setIsReconnecting(true)
194
- onReconnecting?.(reconnectAttemptsRef.current, MAX_RECONNECT_ATTEMPTS)
195
-
196
- console.log(`[Terminal] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`)
197
-
198
- reconnectTimeoutRef.current = setTimeout(() => {
199
- connectWebSocket()
200
- }, delay)
201
- }, [onError, onReconnecting])
202
-
203
- // Connect WebSocket (internal)
204
- const connectWebSocket = useCallback(async () => {
205
- // Close existing connection if any
206
- if (wsRef.current) {
207
- wsRef.current.close()
208
- wsRef.current = null
209
- }
210
-
211
- const activeSessionId = currentSessionIdRef.current || sessionId
212
-
213
- try {
214
- // Create PTY session on server
215
- const response = await fetch('/api/claude/sessions', {
216
- method: 'POST',
217
- headers: { 'Content-Type': 'application/json' },
218
- body: JSON.stringify({ sessionId: activeSessionId, projectDir })
219
- })
220
-
221
- if (!response.ok) {
222
- throw new Error('Failed to create PTY session')
223
- }
224
-
225
- // Connect WebSocket
226
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
227
- const wsUrl = `${protocol}//${window.location.host}/ws/claude/${activeSessionId}`
228
- const ws = new WebSocket(wsUrl)
229
-
230
- ws.onopen = () => {
231
- console.log('[Terminal] WebSocket connected')
232
- reconnectAttemptsRef.current = 0
233
- setIsConnected(true)
234
- setIsLoading(false)
235
- setIsReconnecting(false)
236
- onConnect?.()
237
-
238
- // Send initial resize
239
- if (terminalRef.current) {
240
- ws.send(JSON.stringify({
241
- type: 'resize',
242
- cols: terminalRef.current.cols,
243
- rows: terminalRef.current.rows
244
- }))
245
- }
246
- }
247
-
248
- // Flush buffered output using requestAnimationFrame for smooth rendering
249
- const flushOutput = () => {
250
- if (outputBufferRef.current.length > 0 && terminalRef.current) {
251
- const combined = outputBufferRef.current.join('')
252
- terminalRef.current.write(combined)
253
- outputBufferRef.current = []
254
- }
255
- flushScheduledRef.current = false
256
- }
257
-
258
- ws.onmessage = (event) => {
259
- try {
260
- const message = JSON.parse(event.data)
261
-
262
- switch (message.type) {
263
- case 'output':
264
- // Buffer output and flush on next animation frame for smooth rendering
265
- outputBufferRef.current.push(message.data)
266
- if (!flushScheduledRef.current) {
267
- flushScheduledRef.current = true
268
- requestAnimationFrame(flushOutput)
269
- }
270
- break
271
-
272
- case 'connected':
273
- console.log('[Terminal] Connected to Claude Code CLI')
274
- break
275
-
276
- case 'exit':
277
- console.log('[Terminal] Claude Code exited with code:', message.code)
278
- terminalRef.current?.write('\r\n[Session ended]\r\n')
279
- break
280
-
281
- case 'error':
282
- onError?.(message.message)
283
- break
284
- }
285
- } catch {
286
- // Raw data, write directly (also buffered)
287
- outputBufferRef.current.push(event.data)
288
- if (!flushScheduledRef.current) {
289
- flushScheduledRef.current = true
290
- requestAnimationFrame(flushOutput)
291
- }
292
- }
293
- }
294
-
295
- ws.onclose = (event) => {
296
- console.log(`[Terminal] WebSocket closed (code: ${event.code})`)
297
- setIsConnected(false)
298
- wsRef.current = null
299
-
300
- // Only trigger reconnect if not intentional
301
- if (!intentionalDisconnectRef.current) {
302
- terminalRef.current?.write('\r\n\x1b[33m[Disconnected - Reconnecting...]\x1b[0m\r\n')
303
- scheduleReconnect()
304
- } else {
305
- onDisconnect?.()
306
- }
307
- }
308
-
309
- ws.onerror = (error) => {
310
- console.error('[Terminal] WebSocket error:', error)
311
- // Don't call onError here, onclose will handle reconnect
312
- }
313
-
314
- wsRef.current = ws
315
-
316
- // Forward terminal input to WebSocket
317
- terminalRef.current?.onData((data) => {
318
- if (ws.readyState === WebSocket.OPEN) {
319
- ws.send(JSON.stringify({ type: 'input', data }))
320
- }
321
- })
322
-
323
- } catch (error) {
324
- console.error('[Terminal] Connection error:', error)
325
- setIsLoading(false)
326
-
327
- // Schedule reconnect on connection failure
328
- if (!intentionalDisconnectRef.current) {
329
- scheduleReconnect()
330
- } else {
331
- onError?.(error instanceof Error ? error.message : 'Connection failed')
332
- }
333
- }
334
- }, [sessionId, projectDir, onConnect, onDisconnect, onError, scheduleReconnect])
335
-
336
- // Public connect function
337
- const connect = useCallback(async () => {
338
- if (wsRef.current?.readyState === WebSocket.OPEN) return
339
-
340
- intentionalDisconnectRef.current = false
341
- reconnectAttemptsRef.current = 0
342
- currentSessionIdRef.current = sessionId
343
- setIsLoading(true)
344
-
345
- await connectWebSocket()
346
- }, [sessionId, connectWebSocket])
347
-
348
- // Disconnect (intentional)
349
- const disconnect = useCallback(() => {
350
- intentionalDisconnectRef.current = true
351
- clearReconnectTimeout()
352
- setIsReconnecting(false)
353
-
354
- if (wsRef.current) {
355
- wsRef.current.close()
356
- wsRef.current = null
357
- }
358
- setIsConnected(false)
359
- onDisconnect?.()
360
- }, [clearReconnectTimeout, onDisconnect])
361
-
362
- // Send input
363
- const sendInput = useCallback((data: string) => {
364
- if (wsRef.current?.readyState === WebSocket.OPEN) {
365
- wsRef.current.send(JSON.stringify({ type: 'input', data }))
366
- }
367
- }, [])
368
-
369
- // Focus terminal
370
- const focusTerminal = useCallback(() => {
371
- if (terminalRef.current) {
372
- terminalRef.current.focus()
373
- }
374
- }, [])
375
-
376
- // Fit terminal to container (useful when tab becomes visible)
377
- const fit = useCallback(() => {
378
- if (fitAddonRef.current && containerRef.current) {
379
- // Only fit if container has dimensions (not hidden)
380
- if (containerRef.current.offsetWidth > 0 && containerRef.current.offsetHeight > 0) {
381
- fitAddonRef.current.fit()
382
-
383
- // Send resize to server
384
- if (wsRef.current?.readyState === WebSocket.OPEN && terminalRef.current) {
385
- wsRef.current.send(JSON.stringify({
386
- type: 'resize',
387
- cols: terminalRef.current.cols,
388
- rows: terminalRef.current.rows
389
- }))
390
- }
391
- }
392
- }
393
- }, [])
394
-
395
- // Cleanup on unmount - empty deps to run only on unmount
396
- useEffect(() => {
397
- return () => {
398
- intentionalDisconnectRef.current = true
399
- if (reconnectTimeoutRef.current) {
400
- clearTimeout(reconnectTimeoutRef.current)
401
- }
402
- if (wsRef.current) {
403
- wsRef.current.close()
404
- }
405
- // Cleanup ResizeObserver
406
- if (resizeObserverRef.current) {
407
- resizeObserverRef.current.disconnect()
408
- }
409
- }
410
- }, []) // Empty deps - run cleanup only on unmount
411
-
412
- return {
413
- initTerminal,
414
- connect,
415
- disconnect,
416
- sendInput,
417
- focusTerminal,
418
- fit,
419
- isConnected,
420
- isLoading,
421
- isReconnecting,
422
- terminalRef,
423
- containerRef
424
- }
425
- }