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.
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +74 -211
- package/core/agentic/prompt-builder.ts +3 -7
- package/core/command-registry/optional-commands.ts +0 -20
- package/core/infrastructure/command-installer/command-installer.ts +8 -1
- package/core/infrastructure/command-installer/global-config.ts +31 -1
- package/core/infrastructure/command-installer/index.ts +1 -1
- package/core/infrastructure/setup.ts +3 -0
- package/package.json +3 -17
- package/templates/commands/done.md +57 -258
- package/templates/commands/now.md +72 -277
- package/templates/commands/ship.md +55 -261
- package/templates/commands/test.md +328 -21
- package/templates/global/CLAUDE.md +40 -205
- package/templates/global/docs/agents.md +88 -0
- package/templates/global/docs/architecture.md +103 -0
- package/templates/global/docs/commands.md +98 -0
- package/templates/global/docs/validation.md +95 -0
- package/templates/mcp-config.json +36 -0
- package/bin/dev.js +0 -216
- package/bin/serve.js +0 -361
- package/packages/web/README.md +0 -36
- package/packages/web/app/api/claude/sessions/route.ts +0 -44
- package/packages/web/app/api/claude/status/route.ts +0 -34
- package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
- package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
- package/packages/web/app/api/projects/[id]/route.ts +0 -29
- package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
- package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
- package/packages/web/app/api/projects/route.ts +0 -16
- package/packages/web/app/api/sessions/current/route.ts +0 -132
- package/packages/web/app/api/sessions/history/route.ts +0 -204
- package/packages/web/app/error.tsx +0 -34
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +0 -198
- package/packages/web/app/layout.tsx +0 -53
- package/packages/web/app/loading.tsx +0 -7
- package/packages/web/app/not-found.tsx +0 -25
- package/packages/web/app/page.tsx +0 -12
- package/packages/web/app/project/[id]/code/layout.tsx +0 -18
- package/packages/web/app/project/[id]/code/page.tsx +0 -408
- package/packages/web/app/project/[id]/error.tsx +0 -41
- package/packages/web/app/project/[id]/loading.tsx +0 -9
- package/packages/web/app/project/[id]/not-found.tsx +0 -27
- package/packages/web/app/project/[id]/page.tsx +0 -384
- package/packages/web/app/project/[id]/reports/page.tsx +0 -59
- package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
- package/packages/web/app/sessions/page.tsx +0 -165
- package/packages/web/app/settings/page.tsx +0 -151
- package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
- package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
- package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
- package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
- package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
- package/packages/web/components/ActivityTimeline/index.ts +0 -2
- package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
- package/packages/web/components/AgentsCard/index.ts +0 -2
- package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
- package/packages/web/components/AppSidebar/index.ts +0 -1
- package/packages/web/components/BackLink/BackLink.tsx +0 -18
- package/packages/web/components/BackLink/BackLink.types.ts +0 -5
- package/packages/web/components/BackLink/index.ts +0 -2
- package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
- package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
- package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
- package/packages/web/components/BentoCard/index.ts +0 -2
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
- package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
- package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
- package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
- package/packages/web/components/BentoGrid/index.ts +0 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
- package/packages/web/components/BlockersCard/index.ts +0 -2
- package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
- package/packages/web/components/CommandBar/index.ts +0 -1
- package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
- package/packages/web/components/CommandButton/index.ts +0 -1
- package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
- package/packages/web/components/ConnectionStatus/index.ts +0 -1
- package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
- package/packages/web/components/DashboardContent/index.ts +0 -1
- package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
- package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
- package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
- package/packages/web/components/DateGroup/index.ts +0 -2
- package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
- package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
- package/packages/web/components/EmptyState/index.ts +0 -2
- package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
- package/packages/web/components/EventRow/EventRow.tsx +0 -49
- package/packages/web/components/EventRow/EventRow.types.ts +0 -7
- package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
- package/packages/web/components/EventRow/index.ts +0 -2
- package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
- package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
- package/packages/web/components/ExpandButton/index.ts +0 -2
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
- package/packages/web/components/HealthGradientBackground/index.ts +0 -2
- package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
- package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
- package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
- package/packages/web/components/HeroSection/hooks/index.ts +0 -2
- package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
- package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
- package/packages/web/components/HeroSection/index.ts +0 -2
- package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
- package/packages/web/components/IdeasCard/index.ts +0 -2
- package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
- package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
- package/packages/web/components/InsightMessage/index.ts +0 -2
- package/packages/web/components/Logo/Logo.tsx +0 -65
- package/packages/web/components/Logo/index.ts +0 -1
- package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
- package/packages/web/components/MarkdownContent/index.ts +0 -1
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
- package/packages/web/components/MasonryGrid/index.ts +0 -1
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
- package/packages/web/components/MomentumWidget/index.ts +0 -2
- package/packages/web/components/NowCard/NowCard.tsx +0 -118
- package/packages/web/components/NowCard/NowCard.types.ts +0 -16
- package/packages/web/components/NowCard/index.ts +0 -2
- package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
- package/packages/web/components/PageHeader/index.ts +0 -1
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
- package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
- package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
- package/packages/web/components/ProgressRing/index.ts +0 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
- package/packages/web/components/ProjectAvatar/index.ts +0 -1
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
- package/packages/web/components/ProjectColorDot/index.ts +0 -1
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
- package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
- package/packages/web/components/Providers/Providers.tsx +0 -48
- package/packages/web/components/Providers/index.ts +0 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
- package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
- package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
- package/packages/web/components/QueueCard/index.ts +0 -2
- package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
- package/packages/web/components/RecoverCard/index.ts +0 -2
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
- package/packages/web/components/RoadmapCard/index.ts +0 -2
- package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
- package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
- package/packages/web/components/ShipsCard/index.ts +0 -2
- package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
- package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
- package/packages/web/components/SparklineChart/index.ts +0 -2
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
- package/packages/web/components/StatsMasonry/index.ts +0 -1
- package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
- package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
- package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
- package/packages/web/components/StreakCard/index.ts +0 -2
- package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
- package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
- package/packages/web/components/TasksCounter/index.ts +0 -2
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
- package/packages/web/components/TechStackBadges/index.ts +0 -1
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
- package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
- package/packages/web/components/TerminalDock/index.ts +0 -2
- package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
- package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
- package/packages/web/components/TerminalTabs/index.ts +0 -1
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
- package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
- package/packages/web/components/VelocityBadge/index.ts +0 -2
- package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
- package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
- package/packages/web/components/VelocityCard/index.ts +0 -2
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
- package/packages/web/components/WeeklyReports/index.ts +0 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
- package/packages/web/components/WeeklySparkline/index.ts +0 -2
- package/packages/web/components/charts/SessionsChart.tsx +0 -175
- package/packages/web/components/ui/alert-dialog.tsx +0 -157
- package/packages/web/components/ui/badge.tsx +0 -46
- package/packages/web/components/ui/button.tsx +0 -60
- package/packages/web/components/ui/card.tsx +0 -92
- package/packages/web/components/ui/chart.tsx +0 -385
- package/packages/web/components/ui/dialog.tsx +0 -143
- package/packages/web/components/ui/drawer.tsx +0 -135
- package/packages/web/components/ui/dropdown-menu.tsx +0 -257
- package/packages/web/components/ui/input.tsx +0 -21
- package/packages/web/components/ui/scroll-area.tsx +0 -58
- package/packages/web/components/ui/select.tsx +0 -187
- package/packages/web/components/ui/sheet.tsx +0 -139
- package/packages/web/components/ui/tabs.tsx +0 -66
- package/packages/web/components/ui/tooltip.tsx +0 -61
- package/packages/web/components.json +0 -22
- package/packages/web/context/GlobalTerminalContext.tsx +0 -538
- package/packages/web/context/TerminalContext.tsx +0 -45
- package/packages/web/context/TerminalTabsContext.tsx +0 -181
- package/packages/web/eslint.config.mjs +0 -18
- package/packages/web/hooks/useClaudeTerminal.ts +0 -425
- package/packages/web/hooks/useProjectStats.ts +0 -93
- package/packages/web/hooks/useProjects.ts +0 -73
- package/packages/web/lib/actions/projects.ts +0 -15
- package/packages/web/lib/commands.ts +0 -81
- package/packages/web/lib/format.ts +0 -23
- package/packages/web/lib/generate-week-report.ts +0 -285
- package/packages/web/lib/parse-prjct-files.ts +0 -1123
- package/packages/web/lib/project-colors.ts +0 -58
- package/packages/web/lib/projects.ts +0 -506
- package/packages/web/lib/pty.ts +0 -101
- package/packages/web/lib/query-config.ts +0 -44
- package/packages/web/lib/services/index.ts +0 -9
- package/packages/web/lib/services/projects.server.ts +0 -66
- package/packages/web/lib/services/stats.server.ts +0 -562
- package/packages/web/lib/unified-loader.ts +0 -396
- package/packages/web/lib/utils.ts +0 -6
- package/packages/web/next-env.d.ts +0 -6
- package/packages/web/next.config.ts +0 -7
- package/packages/web/package.json +0 -57
- package/packages/web/postcss.config.mjs +0 -7
- package/packages/web/public/file.svg +0 -1
- package/packages/web/public/globe.svg +0 -1
- package/packages/web/public/next.svg +0 -1
- package/packages/web/public/vercel.svg +0 -1
- package/packages/web/public/window.svg +0 -1
- package/packages/web/server.ts +0 -312
- package/packages/web/tsconfig.json +0 -34
- 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
|
-
}
|