prjct-cli 0.13.2 → 0.15.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 (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. package/templates/commands/workflow.md +0 -12
@@ -0,0 +1,538 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Global Terminal Context - Manage terminal sessions across ALL projects
5
+ *
6
+ * Features:
7
+ * - Multi-project session management (Map<projectId, sessions[]>)
8
+ * - Bottom dock panel UI state (height, open/closed)
9
+ * - Full-screen mode for code page
10
+ * - Persistence to localStorage
11
+ * - Cross-page navigation support
12
+ */
13
+
14
+ import { createContext, useContext, useCallback, useState, useRef, useEffect, ReactNode } from 'react'
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export interface GlobalTerminalSession {
21
+ id: string
22
+ projectId: string
23
+ projectName: string
24
+ projectPath: string
25
+ createdAt: Date
26
+ isConnected: boolean
27
+ isLoading: boolean
28
+ label: string
29
+ panel: 'left' | 'right' // Which panel this session belongs to
30
+ }
31
+
32
+ export interface ProjectTerminals {
33
+ projectId: string
34
+ projectName: string
35
+ projectPath: string
36
+ sessions: GlobalTerminalSession[]
37
+ }
38
+
39
+ interface GlobalTerminalContextType {
40
+ // Session management
41
+ projectSessions: Map<string, ProjectTerminals>
42
+ activeProjectId: string | null
43
+ activeSessionId: string | null
44
+
45
+ // Dock UI state
46
+ isDockOpen: boolean
47
+ dockHeight: number
48
+ isFullScreen: boolean // When true, terminal takes full main area (code page)
49
+ isSplitEnabled: boolean // Split view mode
50
+ secondActiveSessionId: string | null // Second panel session (for split)
51
+
52
+ // Session actions
53
+ createSessionForProject: (projectId: string, projectName: string, projectPath: string, panel?: 'left' | 'right') => string
54
+ closeSession: (sessionId: string) => void
55
+ switchProject: (projectId: string) => void
56
+ switchSession: (sessionId: string, panel?: 'left' | 'right') => void
57
+ updateSession: (sessionId: string, updates: Partial<GlobalTerminalSession>) => void
58
+
59
+ // Dock UI actions
60
+ openDock: () => void
61
+ closeDock: () => void
62
+ toggleDock: () => void
63
+ setDockHeight: (height: number) => void
64
+ setFullScreen: (isFullScreen: boolean) => void
65
+ setSplitEnabled: (enabled: boolean) => void
66
+
67
+ // Helpers
68
+ getActiveSession: () => GlobalTerminalSession | null
69
+ getSecondActiveSession: () => GlobalTerminalSession | null
70
+ getProjectSessions: (projectId: string) => GlobalTerminalSession[]
71
+ getTotalSessionCount: () => number
72
+ getConnectedSessionCount: () => number
73
+ getAllSessions: () => GlobalTerminalSession[]
74
+ getLeftPanelSessions: () => GlobalTerminalSession[]
75
+ getRightPanelSessions: () => GlobalTerminalSession[]
76
+ moveSessionToPanel: (sessionId: string, panel: 'left' | 'right') => void
77
+
78
+ // Input/focus refs
79
+ sendCommandToActive: (command: string) => void
80
+ registerSendInput: (sessionId: string, fn: (data: string) => void) => void
81
+ registerFocusTerminal: (sessionId: string, fn: () => void) => void
82
+ focusActiveTerminal: () => void
83
+ }
84
+
85
+ // ============================================================================
86
+ // Constants
87
+ // ============================================================================
88
+
89
+ const STORAGE_KEYS = {
90
+ dockHeight: 'prjct-terminal-dock-height',
91
+ dockOpen: 'prjct-terminal-dock-open',
92
+ activeProject: 'prjct-terminal-active-project',
93
+ splitEnabled: 'prjct-terminal-split-enabled',
94
+ }
95
+
96
+ const DEFAULT_DOCK_HEIGHT = 300
97
+ const MIN_DOCK_HEIGHT = 150
98
+ // Dynamic max height - 90% of viewport
99
+ const getMaxDockHeight = () => typeof window !== 'undefined' ? window.innerHeight * 0.9 : 800
100
+
101
+ // ============================================================================
102
+ // Context
103
+ // ============================================================================
104
+
105
+ const GlobalTerminalContext = createContext<GlobalTerminalContextType | null>(null)
106
+
107
+ let sessionCounter = 0
108
+
109
+ // ============================================================================
110
+ // Provider
111
+ // ============================================================================
112
+
113
+ export function GlobalTerminalProvider({ children }: { children: ReactNode }) {
114
+ // Session state
115
+ const [projectSessions, setProjectSessions] = useState<Map<string, ProjectTerminals>>(new Map())
116
+ const [activeProjectId, setActiveProjectId] = useState<string | null>(null)
117
+ const [activeSessionId, setActiveSessionId] = useState<string | null>(null)
118
+
119
+ // Dock UI state
120
+ const [isDockOpen, setIsDockOpen] = useState(false)
121
+ const [dockHeight, setDockHeightState] = useState(DEFAULT_DOCK_HEIGHT)
122
+ const [isFullScreen, setIsFullScreen] = useState(false)
123
+ const [isSplitEnabled, setIsSplitEnabledState] = useState(false)
124
+ const [secondActiveSessionId, setSecondActiveSessionId] = useState<string | null>(null)
125
+
126
+ // Refs for input/focus functions per session
127
+ const sendInputRefs = useRef<Map<string, (data: string) => void>>(new Map())
128
+ const focusTerminalRefs = useRef<Map<string, () => void>>(new Map())
129
+
130
+ // -------------------------------------------------------------------------
131
+ // Load persisted state from localStorage
132
+ // -------------------------------------------------------------------------
133
+ useEffect(() => {
134
+ if (typeof window === 'undefined') return
135
+
136
+ try {
137
+ const savedDockHeight = localStorage.getItem(STORAGE_KEYS.dockHeight)
138
+ if (savedDockHeight) {
139
+ const parsed = parseInt(savedDockHeight, 10)
140
+ if (!isNaN(parsed)) {
141
+ setDockHeightState(Math.max(MIN_DOCK_HEIGHT, Math.min(parsed, getMaxDockHeight())))
142
+ }
143
+ }
144
+
145
+ const savedDockOpen = localStorage.getItem(STORAGE_KEYS.dockOpen)
146
+ if (savedDockOpen) {
147
+ setIsDockOpen(JSON.parse(savedDockOpen))
148
+ }
149
+
150
+ const savedActiveProject = localStorage.getItem(STORAGE_KEYS.activeProject)
151
+ if (savedActiveProject) {
152
+ setActiveProjectId(savedActiveProject)
153
+ }
154
+ } catch (e) {
155
+ console.warn('Failed to load terminal state from localStorage:', e)
156
+ }
157
+ }, [])
158
+
159
+ // -------------------------------------------------------------------------
160
+ // Persist state to localStorage
161
+ // -------------------------------------------------------------------------
162
+ const setDockHeight = useCallback((newHeight: number) => {
163
+ const maxHeight = getMaxDockHeight()
164
+ const clampedHeight = Math.max(MIN_DOCK_HEIGHT, Math.min(newHeight, maxHeight))
165
+ setDockHeightState(clampedHeight)
166
+ if (typeof window !== 'undefined') {
167
+ localStorage.setItem(STORAGE_KEYS.dockHeight, String(clampedHeight))
168
+ }
169
+ }, [])
170
+
171
+ const setSplitEnabled = useCallback((enabled: boolean) => {
172
+ setIsSplitEnabledState(enabled)
173
+ if (typeof window !== 'undefined') {
174
+ localStorage.setItem(STORAGE_KEYS.splitEnabled, String(enabled))
175
+ }
176
+ // If disabling split, clear second panel
177
+ if (!enabled) {
178
+ setSecondActiveSessionId(null)
179
+ }
180
+ }, [])
181
+
182
+ // -------------------------------------------------------------------------
183
+ // Session management
184
+ // -------------------------------------------------------------------------
185
+ const createSessionForProject = useCallback((projectId: string, projectName: string, projectPath: string, panel: 'left' | 'right' = 'left') => {
186
+ sessionCounter++
187
+ const newSession: GlobalTerminalSession = {
188
+ id: `pty_${projectId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
189
+ projectId,
190
+ projectName,
191
+ projectPath,
192
+ createdAt: new Date(),
193
+ isConnected: false,
194
+ isLoading: true,
195
+ label: `Terminal ${sessionCounter}`,
196
+ panel,
197
+ }
198
+
199
+ setProjectSessions(prev => {
200
+ const newMap = new Map(prev)
201
+ const existing = newMap.get(projectId)
202
+
203
+ if (existing) {
204
+ newMap.set(projectId, {
205
+ ...existing,
206
+ sessions: [...existing.sessions, newSession],
207
+ })
208
+ } else {
209
+ newMap.set(projectId, {
210
+ projectId,
211
+ projectName,
212
+ projectPath,
213
+ sessions: [newSession],
214
+ })
215
+ }
216
+
217
+ return newMap
218
+ })
219
+
220
+ // Set as active based on panel
221
+ setActiveProjectId(projectId)
222
+ if (panel === 'right') {
223
+ setSecondActiveSessionId(newSession.id)
224
+ } else {
225
+ setActiveSessionId(newSession.id)
226
+ }
227
+ localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
228
+
229
+ // Open dock if not open
230
+ setIsDockOpen(true)
231
+ localStorage.setItem(STORAGE_KEYS.dockOpen, 'true')
232
+
233
+ return newSession.id
234
+ }, [])
235
+
236
+ const closeSession = useCallback((sessionId: string) => {
237
+ setProjectSessions(prev => {
238
+ const newMap = new Map(prev)
239
+
240
+ // Find which project this session belongs to
241
+ for (const [projectId, project] of newMap) {
242
+ const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
243
+ if (sessionIndex !== -1) {
244
+ const newSessions = project.sessions.filter(s => s.id !== sessionId)
245
+
246
+ if (newSessions.length === 0) {
247
+ // Remove project entirely if no sessions left
248
+ newMap.delete(projectId)
249
+ } else {
250
+ newMap.set(projectId, { ...project, sessions: newSessions })
251
+ }
252
+
253
+ // Update active session if needed
254
+ if (sessionId === activeSessionId) {
255
+ if (newSessions.length > 0) {
256
+ setActiveSessionId(newSessions[newSessions.length - 1].id)
257
+ } else {
258
+ // Switch to another project's session or null
259
+ const remainingProjects = Array.from(newMap.values())
260
+ if (remainingProjects.length > 0) {
261
+ const nextProject = remainingProjects[0]
262
+ setActiveProjectId(nextProject.projectId)
263
+ setActiveSessionId(nextProject.sessions[nextProject.sessions.length - 1]?.id || null)
264
+ } else {
265
+ setActiveProjectId(null)
266
+ setActiveSessionId(null)
267
+ }
268
+ }
269
+ }
270
+
271
+ break
272
+ }
273
+ }
274
+
275
+ return newMap
276
+ })
277
+
278
+ // Cleanup refs
279
+ sendInputRefs.current.delete(sessionId)
280
+ focusTerminalRefs.current.delete(sessionId)
281
+ }, [activeSessionId])
282
+
283
+ const switchProject = useCallback((projectId: string) => {
284
+ setActiveProjectId(projectId)
285
+ localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
286
+
287
+ // Switch to first session of this project
288
+ const project = projectSessions.get(projectId)
289
+ if (project && project.sessions.length > 0) {
290
+ setActiveSessionId(project.sessions[0].id)
291
+ }
292
+ }, [projectSessions])
293
+
294
+ const switchSession = useCallback((sessionId: string, panel: 'left' | 'right' = 'left') => {
295
+ if (panel === 'right' && isSplitEnabled) {
296
+ setSecondActiveSessionId(sessionId)
297
+ } else {
298
+ setActiveSessionId(sessionId)
299
+
300
+ // Also update active project if session is from different project
301
+ for (const [projectId, project] of projectSessions) {
302
+ if (project.sessions.some(s => s.id === sessionId)) {
303
+ if (projectId !== activeProjectId) {
304
+ setActiveProjectId(projectId)
305
+ localStorage.setItem(STORAGE_KEYS.activeProject, projectId)
306
+ }
307
+ break
308
+ }
309
+ }
310
+ }
311
+ }, [projectSessions, activeProjectId, isSplitEnabled])
312
+
313
+ const updateSession = useCallback((sessionId: string, updates: Partial<GlobalTerminalSession>) => {
314
+ setProjectSessions(prev => {
315
+ const newMap = new Map(prev)
316
+
317
+ for (const [projectId, project] of newMap) {
318
+ const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
319
+ if (sessionIndex !== -1) {
320
+ const newSessions = [...project.sessions]
321
+ newSessions[sessionIndex] = { ...newSessions[sessionIndex], ...updates }
322
+ newMap.set(projectId, { ...project, sessions: newSessions })
323
+ break
324
+ }
325
+ }
326
+
327
+ return newMap
328
+ })
329
+ }, [])
330
+
331
+ // -------------------------------------------------------------------------
332
+ // Dock UI actions
333
+ // -------------------------------------------------------------------------
334
+ const openDock = useCallback(() => {
335
+ setIsDockOpen(true)
336
+ localStorage.setItem(STORAGE_KEYS.dockOpen, 'true')
337
+ }, [])
338
+
339
+ const closeDock = useCallback(() => {
340
+ setIsDockOpen(false)
341
+ localStorage.setItem(STORAGE_KEYS.dockOpen, 'false')
342
+ }, [])
343
+
344
+ const toggleDock = useCallback(() => {
345
+ const newValue = !isDockOpen
346
+ setIsDockOpen(newValue)
347
+ localStorage.setItem(STORAGE_KEYS.dockOpen, String(newValue))
348
+ }, [isDockOpen])
349
+
350
+ const setFullScreen = useCallback((value: boolean) => {
351
+ setIsFullScreen(value)
352
+ }, [])
353
+
354
+ // -------------------------------------------------------------------------
355
+ // Helpers
356
+ // -------------------------------------------------------------------------
357
+ const getActiveSession = useCallback((): GlobalTerminalSession | null => {
358
+ if (!activeSessionId) return null
359
+ for (const project of projectSessions.values()) {
360
+ const session = project.sessions.find(s => s.id === activeSessionId)
361
+ if (session) return session
362
+ }
363
+ return null
364
+ }, [projectSessions, activeSessionId])
365
+
366
+ const getSecondActiveSession = useCallback((): GlobalTerminalSession | null => {
367
+ if (!secondActiveSessionId) return null
368
+ for (const project of projectSessions.values()) {
369
+ const session = project.sessions.find(s => s.id === secondActiveSessionId)
370
+ if (session) return session
371
+ }
372
+ return null
373
+ }, [projectSessions, secondActiveSessionId])
374
+
375
+ const getProjectSessions = useCallback((projectId: string): GlobalTerminalSession[] => {
376
+ return projectSessions.get(projectId)?.sessions || []
377
+ }, [projectSessions])
378
+
379
+ const getAllSessions = useCallback((): GlobalTerminalSession[] => {
380
+ const allSessions: GlobalTerminalSession[] = []
381
+ for (const project of projectSessions.values()) {
382
+ allSessions.push(...project.sessions)
383
+ }
384
+ return allSessions
385
+ }, [projectSessions])
386
+
387
+ const getTotalSessionCount = useCallback((): number => {
388
+ let count = 0
389
+ for (const project of projectSessions.values()) {
390
+ count += project.sessions.length
391
+ }
392
+ return count
393
+ }, [projectSessions])
394
+
395
+ const getConnectedSessionCount = useCallback((): number => {
396
+ let count = 0
397
+ for (const project of projectSessions.values()) {
398
+ count += project.sessions.filter(s => s.isConnected).length
399
+ }
400
+ return count
401
+ }, [projectSessions])
402
+
403
+ const getLeftPanelSessions = useCallback((): GlobalTerminalSession[] => {
404
+ return getAllSessions().filter(s => s.panel === 'left')
405
+ }, [getAllSessions])
406
+
407
+ const getRightPanelSessions = useCallback((): GlobalTerminalSession[] => {
408
+ return getAllSessions().filter(s => s.panel === 'right')
409
+ }, [getAllSessions])
410
+
411
+ const moveSessionToPanel = useCallback((sessionId: string, panel: 'left' | 'right') => {
412
+ setProjectSessions(prev => {
413
+ const newMap = new Map(prev)
414
+ for (const [projectId, project] of newMap) {
415
+ const sessionIndex = project.sessions.findIndex(s => s.id === sessionId)
416
+ if (sessionIndex !== -1) {
417
+ const newSessions = [...project.sessions]
418
+ newSessions[sessionIndex] = { ...newSessions[sessionIndex], panel }
419
+ newMap.set(projectId, { ...project, sessions: newSessions })
420
+ break
421
+ }
422
+ }
423
+ return newMap
424
+ })
425
+ }, [])
426
+
427
+ // -------------------------------------------------------------------------
428
+ // Input/Focus management
429
+ // -------------------------------------------------------------------------
430
+ const registerSendInput = useCallback((sessionId: string, fn: (data: string) => void) => {
431
+ sendInputRefs.current.set(sessionId, fn)
432
+ }, [])
433
+
434
+ const registerFocusTerminal = useCallback((sessionId: string, fn: () => void) => {
435
+ focusTerminalRefs.current.set(sessionId, fn)
436
+ }, [])
437
+
438
+ const focusActiveTerminal = useCallback(() => {
439
+ if (!activeSessionId) return
440
+ const focusFn = focusTerminalRefs.current.get(activeSessionId)
441
+ if (focusFn) focusFn()
442
+ }, [activeSessionId])
443
+
444
+ const sendCommandToActive = useCallback((command: string) => {
445
+ if (!activeSessionId) return
446
+ const sendFn = sendInputRefs.current.get(activeSessionId)
447
+ const session = getActiveSession()
448
+ if (sendFn && session?.isConnected) {
449
+ sendFn(command + '\n')
450
+ // Auto-focus terminal after sending command
451
+ const focusFn = focusTerminalRefs.current.get(activeSessionId)
452
+ if (focusFn) focusFn()
453
+ }
454
+ }, [activeSessionId, getActiveSession])
455
+
456
+ // -------------------------------------------------------------------------
457
+ // Page unload protection
458
+ // -------------------------------------------------------------------------
459
+ const hasConnectedSessions = getConnectedSessionCount() > 0
460
+
461
+ useEffect(() => {
462
+ if (!hasConnectedSessions) return
463
+
464
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
465
+ e.preventDefault()
466
+ e.returnValue = 'You have active terminal sessions. Are you sure you want to leave?'
467
+ return e.returnValue
468
+ }
469
+
470
+ window.addEventListener('beforeunload', handleBeforeUnload)
471
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload)
472
+ }, [hasConnectedSessions])
473
+
474
+ // -------------------------------------------------------------------------
475
+ // Render
476
+ // -------------------------------------------------------------------------
477
+ return (
478
+ <GlobalTerminalContext.Provider value={{
479
+ // Session state
480
+ projectSessions,
481
+ activeProjectId,
482
+ activeSessionId,
483
+
484
+ // Dock UI state
485
+ isDockOpen,
486
+ dockHeight,
487
+ isFullScreen,
488
+ isSplitEnabled,
489
+ secondActiveSessionId,
490
+
491
+ // Session actions
492
+ createSessionForProject,
493
+ closeSession,
494
+ switchProject,
495
+ switchSession,
496
+ updateSession,
497
+
498
+ // Dock UI actions
499
+ openDock,
500
+ closeDock,
501
+ toggleDock,
502
+ setDockHeight,
503
+ setFullScreen,
504
+ setSplitEnabled,
505
+
506
+ // Helpers
507
+ getActiveSession,
508
+ getSecondActiveSession,
509
+ getProjectSessions,
510
+ getTotalSessionCount,
511
+ getConnectedSessionCount,
512
+ getAllSessions,
513
+ getLeftPanelSessions,
514
+ getRightPanelSessions,
515
+ moveSessionToPanel,
516
+
517
+ // Input/focus
518
+ sendCommandToActive,
519
+ registerSendInput,
520
+ registerFocusTerminal,
521
+ focusActiveTerminal,
522
+ }}>
523
+ {children}
524
+ </GlobalTerminalContext.Provider>
525
+ )
526
+ }
527
+
528
+ // ============================================================================
529
+ // Hook
530
+ // ============================================================================
531
+
532
+ export function useGlobalTerminal() {
533
+ const context = useContext(GlobalTerminalContext)
534
+ if (!context) {
535
+ throw new Error('useGlobalTerminal must be used within GlobalTerminalProvider')
536
+ }
537
+ return context
538
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ Play,
3
+ Target,
4
+ Lightbulb,
5
+ ListTodo,
6
+ Rocket,
7
+ Sparkles,
8
+ CheckCircle2,
9
+ Pause,
10
+ BarChart3,
11
+ TrendingUp,
12
+ Activity,
13
+ History,
14
+ Undo2,
15
+ Redo2,
16
+ RefreshCw,
17
+ type LucideIcon,
18
+ } from 'lucide-react'
19
+
20
+ export interface WorkflowCommand {
21
+ cmd: string
22
+ icon: LucideIcon
23
+ tip: string
24
+ group: CommandGroup
25
+ }
26
+
27
+ export type CommandGroup = 'work' | 'session' | 'plan' | 'ship' | 'status' | 'recovery'
28
+
29
+ // Commands ordered by real developer workflow
30
+ export const WORKFLOW_COMMANDS: readonly WorkflowCommand[] = [
31
+ { cmd: 'p. now', icon: Target, tip: 'Set task', group: 'work' },
32
+ { cmd: 'p. done', icon: CheckCircle2, tip: 'Complete', group: 'work' },
33
+ { cmd: 'p. pause', icon: Pause, tip: 'Pause', group: 'session' },
34
+ { cmd: 'p. resume', icon: Play, tip: 'Resume', group: 'session' },
35
+ { cmd: 'p. feature', icon: Sparkles, tip: 'Feature', group: 'plan' },
36
+ { cmd: 'p. idea', icon: Lightbulb, tip: 'Idea', group: 'plan' },
37
+ { cmd: 'p. next', icon: ListTodo, tip: 'Queue', group: 'plan' },
38
+ { cmd: 'p. ship', icon: Rocket, tip: 'Ship', group: 'ship' },
39
+ { cmd: 'p. recap', icon: BarChart3, tip: 'Recap', group: 'status' },
40
+ { cmd: 'p. progress', icon: TrendingUp, tip: 'Progress', group: 'status' },
41
+ { cmd: 'p. status', icon: Activity, tip: 'Status', group: 'status' },
42
+ { cmd: 'p. history', icon: History, tip: 'History', group: 'status' },
43
+ { cmd: 'p. undo', icon: Undo2, tip: 'Undo', group: 'recovery' },
44
+ { cmd: 'p. redo', icon: Redo2, tip: 'Redo', group: 'recovery' },
45
+ ] as const
46
+
47
+ export const COMMAND_GROUPS: readonly CommandGroup[] = ['work', 'session', 'plan', 'ship', 'status', 'recovery']
48
+
49
+ // Sync command - always first/prominent
50
+ export const SYNC_COMMAND: WorkflowCommand = {
51
+ cmd: 'p. sync',
52
+ icon: RefreshCw,
53
+ tip: 'Sync',
54
+ group: 'status',
55
+ }
56
+
57
+ // Get commands by group
58
+ export function getCommandsByGroup(group: CommandGroup): WorkflowCommand[] {
59
+ return WORKFLOW_COMMANDS.filter(c => c.group === group)
60
+ }
61
+
62
+ // Project colors for tabs (based on project ID hash)
63
+ export const PROJECT_COLORS = [
64
+ 'bg-orange-500/20 border-orange-500/50 text-orange-400',
65
+ 'bg-blue-500/20 border-blue-500/50 text-blue-400',
66
+ 'bg-green-500/20 border-green-500/50 text-green-400',
67
+ 'bg-purple-500/20 border-purple-500/50 text-purple-400',
68
+ 'bg-pink-500/20 border-pink-500/50 text-pink-400',
69
+ 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400',
70
+ 'bg-yellow-500/20 border-yellow-500/50 text-yellow-400',
71
+ 'bg-red-500/20 border-red-500/50 text-red-400',
72
+ ]
73
+
74
+ export function getProjectColor(projectId: string): string {
75
+ let hash = 0
76
+ for (let i = 0; i < projectId.length; i++) {
77
+ hash = ((hash << 5) - hash) + projectId.charCodeAt(i)
78
+ hash = hash & hash
79
+ }
80
+ return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]
81
+ }