prjct-cli 0.18.1 → 0.19.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 (245) hide show
  1. package/CHANGELOG.md +53 -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/subagent-generation.md +8 -6
  11. package/templates/commands/done.md +57 -258
  12. package/templates/commands/now.md +72 -277
  13. package/templates/commands/ship.md +55 -261
  14. package/templates/commands/sync.md +7 -7
  15. package/templates/commands/test.md +328 -21
  16. package/templates/global/CLAUDE.md +40 -205
  17. package/templates/global/docs/agents.md +88 -0
  18. package/templates/global/docs/architecture.md +103 -0
  19. package/templates/global/docs/commands.md +98 -0
  20. package/templates/global/docs/validation.md +95 -0
  21. package/templates/mcp-config.json +36 -0
  22. package/bin/dev.js +0 -216
  23. package/bin/serve.js +0 -361
  24. package/packages/web/README.md +0 -36
  25. package/packages/web/app/api/claude/sessions/route.ts +0 -44
  26. package/packages/web/app/api/claude/status/route.ts +0 -34
  27. package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
  28. package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
  29. package/packages/web/app/api/projects/[id]/route.ts +0 -29
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
  31. package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
  32. package/packages/web/app/api/projects/route.ts +0 -16
  33. package/packages/web/app/api/sessions/current/route.ts +0 -132
  34. package/packages/web/app/api/sessions/history/route.ts +0 -204
  35. package/packages/web/app/error.tsx +0 -34
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +0 -198
  38. package/packages/web/app/layout.tsx +0 -53
  39. package/packages/web/app/loading.tsx +0 -7
  40. package/packages/web/app/not-found.tsx +0 -25
  41. package/packages/web/app/page.tsx +0 -12
  42. package/packages/web/app/project/[id]/code/layout.tsx +0 -18
  43. package/packages/web/app/project/[id]/code/page.tsx +0 -408
  44. package/packages/web/app/project/[id]/error.tsx +0 -41
  45. package/packages/web/app/project/[id]/loading.tsx +0 -9
  46. package/packages/web/app/project/[id]/not-found.tsx +0 -27
  47. package/packages/web/app/project/[id]/page.tsx +0 -384
  48. package/packages/web/app/project/[id]/reports/page.tsx +0 -59
  49. package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
  50. package/packages/web/app/sessions/page.tsx +0 -165
  51. package/packages/web/app/settings/page.tsx +0 -151
  52. package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
  53. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
  54. package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
  55. package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
  56. package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
  57. package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
  58. package/packages/web/components/ActivityTimeline/index.ts +0 -2
  59. package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
  60. package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
  61. package/packages/web/components/AgentsCard/index.ts +0 -2
  62. package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
  63. package/packages/web/components/AppSidebar/index.ts +0 -1
  64. package/packages/web/components/BackLink/BackLink.tsx +0 -18
  65. package/packages/web/components/BackLink/BackLink.types.ts +0 -5
  66. package/packages/web/components/BackLink/index.ts +0 -2
  67. package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
  68. package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
  69. package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
  70. package/packages/web/components/BentoCard/index.ts +0 -2
  71. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
  72. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
  73. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
  74. package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
  75. package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
  76. package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
  77. package/packages/web/components/BentoGrid/index.ts +0 -2
  78. package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
  79. package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
  80. package/packages/web/components/BlockersCard/index.ts +0 -2
  81. package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
  82. package/packages/web/components/CommandBar/index.ts +0 -1
  83. package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
  84. package/packages/web/components/CommandButton/index.ts +0 -1
  85. package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
  86. package/packages/web/components/ConnectionStatus/index.ts +0 -1
  87. package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
  88. package/packages/web/components/DashboardContent/index.ts +0 -1
  89. package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
  90. package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
  91. package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
  92. package/packages/web/components/DateGroup/index.ts +0 -2
  93. package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
  94. package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
  95. package/packages/web/components/EmptyState/index.ts +0 -2
  96. package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
  97. package/packages/web/components/EventRow/EventRow.tsx +0 -49
  98. package/packages/web/components/EventRow/EventRow.types.ts +0 -7
  99. package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
  100. package/packages/web/components/EventRow/index.ts +0 -2
  101. package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
  102. package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
  103. package/packages/web/components/ExpandButton/index.ts +0 -2
  104. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
  105. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
  106. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
  107. package/packages/web/components/HealthGradientBackground/index.ts +0 -2
  108. package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
  109. package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
  110. package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
  111. package/packages/web/components/HeroSection/hooks/index.ts +0 -2
  112. package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
  113. package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
  114. package/packages/web/components/HeroSection/index.ts +0 -2
  115. package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
  116. package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
  117. package/packages/web/components/IdeasCard/index.ts +0 -2
  118. package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
  119. package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
  120. package/packages/web/components/InsightMessage/index.ts +0 -2
  121. package/packages/web/components/Logo/Logo.tsx +0 -65
  122. package/packages/web/components/Logo/index.ts +0 -1
  123. package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
  124. package/packages/web/components/MarkdownContent/index.ts +0 -1
  125. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
  126. package/packages/web/components/MasonryGrid/index.ts +0 -1
  127. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
  128. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
  129. package/packages/web/components/MomentumWidget/index.ts +0 -2
  130. package/packages/web/components/NowCard/NowCard.tsx +0 -118
  131. package/packages/web/components/NowCard/NowCard.types.ts +0 -16
  132. package/packages/web/components/NowCard/index.ts +0 -2
  133. package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
  134. package/packages/web/components/PageHeader/index.ts +0 -1
  135. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
  136. package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
  137. package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
  138. package/packages/web/components/ProgressRing/index.ts +0 -2
  139. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
  140. package/packages/web/components/ProjectAvatar/index.ts +0 -1
  141. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
  142. package/packages/web/components/ProjectColorDot/index.ts +0 -1
  143. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
  144. package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
  145. package/packages/web/components/Providers/Providers.tsx +0 -48
  146. package/packages/web/components/Providers/index.ts +0 -1
  147. package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
  148. package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
  149. package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
  150. package/packages/web/components/QueueCard/index.ts +0 -2
  151. package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
  152. package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
  153. package/packages/web/components/RecoverCard/index.ts +0 -2
  154. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
  155. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
  156. package/packages/web/components/RoadmapCard/index.ts +0 -2
  157. package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
  158. package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
  159. package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
  160. package/packages/web/components/ShipsCard/index.ts +0 -2
  161. package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
  162. package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
  163. package/packages/web/components/SparklineChart/index.ts +0 -2
  164. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
  165. package/packages/web/components/StatsMasonry/index.ts +0 -1
  166. package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
  167. package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
  168. package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
  169. package/packages/web/components/StreakCard/index.ts +0 -2
  170. package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
  171. package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
  172. package/packages/web/components/TasksCounter/index.ts +0 -2
  173. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
  174. package/packages/web/components/TechStackBadges/index.ts +0 -1
  175. package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
  176. package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
  177. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
  178. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
  179. package/packages/web/components/TerminalDock/index.ts +0 -2
  180. package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
  181. package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
  182. package/packages/web/components/TerminalTabs/index.ts +0 -1
  183. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
  184. package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
  185. package/packages/web/components/VelocityBadge/index.ts +0 -2
  186. package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
  187. package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
  188. package/packages/web/components/VelocityCard/index.ts +0 -2
  189. package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
  190. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
  191. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
  192. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
  193. package/packages/web/components/WeeklyReports/index.ts +0 -4
  194. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
  195. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
  196. package/packages/web/components/WeeklySparkline/index.ts +0 -2
  197. package/packages/web/components/charts/SessionsChart.tsx +0 -175
  198. package/packages/web/components/ui/alert-dialog.tsx +0 -157
  199. package/packages/web/components/ui/badge.tsx +0 -46
  200. package/packages/web/components/ui/button.tsx +0 -60
  201. package/packages/web/components/ui/card.tsx +0 -92
  202. package/packages/web/components/ui/chart.tsx +0 -385
  203. package/packages/web/components/ui/dialog.tsx +0 -143
  204. package/packages/web/components/ui/drawer.tsx +0 -135
  205. package/packages/web/components/ui/dropdown-menu.tsx +0 -257
  206. package/packages/web/components/ui/input.tsx +0 -21
  207. package/packages/web/components/ui/scroll-area.tsx +0 -58
  208. package/packages/web/components/ui/select.tsx +0 -187
  209. package/packages/web/components/ui/sheet.tsx +0 -139
  210. package/packages/web/components/ui/tabs.tsx +0 -66
  211. package/packages/web/components/ui/tooltip.tsx +0 -61
  212. package/packages/web/components.json +0 -22
  213. package/packages/web/context/GlobalTerminalContext.tsx +0 -538
  214. package/packages/web/context/TerminalContext.tsx +0 -45
  215. package/packages/web/context/TerminalTabsContext.tsx +0 -181
  216. package/packages/web/eslint.config.mjs +0 -18
  217. package/packages/web/hooks/useClaudeTerminal.ts +0 -425
  218. package/packages/web/hooks/useProjectStats.ts +0 -93
  219. package/packages/web/hooks/useProjects.ts +0 -73
  220. package/packages/web/lib/actions/projects.ts +0 -15
  221. package/packages/web/lib/commands.ts +0 -81
  222. package/packages/web/lib/format.ts +0 -23
  223. package/packages/web/lib/generate-week-report.ts +0 -285
  224. package/packages/web/lib/parse-prjct-files.ts +0 -1123
  225. package/packages/web/lib/project-colors.ts +0 -58
  226. package/packages/web/lib/projects.ts +0 -506
  227. package/packages/web/lib/pty.ts +0 -101
  228. package/packages/web/lib/query-config.ts +0 -44
  229. package/packages/web/lib/services/index.ts +0 -9
  230. package/packages/web/lib/services/projects.server.ts +0 -66
  231. package/packages/web/lib/services/stats.server.ts +0 -562
  232. package/packages/web/lib/unified-loader.ts +0 -396
  233. package/packages/web/lib/utils.ts +0 -6
  234. package/packages/web/next-env.d.ts +0 -6
  235. package/packages/web/next.config.ts +0 -7
  236. package/packages/web/package.json +0 -57
  237. package/packages/web/postcss.config.mjs +0 -7
  238. package/packages/web/public/file.svg +0 -1
  239. package/packages/web/public/globe.svg +0 -1
  240. package/packages/web/public/next.svg +0 -1
  241. package/packages/web/public/vercel.svg +0 -1
  242. package/packages/web/public/window.svg +0 -1
  243. package/packages/web/server.ts +0 -312
  244. package/packages/web/tsconfig.json +0 -34
  245. 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
- }