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