@mrpatronz/nexusflow 0.2.13 → 0.2.17

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 (186) hide show
  1. package/.github/dependabot.yml +52 -52
  2. package/.github/workflows/dependabot-merge.yml +53 -0
  3. package/.github/workflows/release.yml +112 -100
  4. package/.vscode/launch.json +17 -17
  5. package/.vscode/tasks.json +17 -17
  6. package/GETTING_STARTED.md +114 -109
  7. package/README.md +336 -311
  8. package/desktop/build-installer.js +228 -0
  9. package/desktop/build.js +118 -0
  10. package/desktop/installer/nexusflow.iss +35 -0
  11. package/desktop/neutralino.config.json +39 -0
  12. package/desktop/package-lock.json +1060 -0
  13. package/desktop/package.json +14 -0
  14. package/dist/commands/desktop.d.ts +12 -0
  15. package/dist/commands/desktop.d.ts.map +1 -0
  16. package/dist/commands/desktop.js +119 -0
  17. package/dist/commands/desktop.js.map +1 -0
  18. package/dist/commands/sync.d.ts.map +1 -1
  19. package/dist/commands/sync.js +1 -3
  20. package/dist/commands/sync.js.map +1 -1
  21. package/dist/commands/tui.d.ts +8 -0
  22. package/dist/commands/tui.d.ts.map +1 -0
  23. package/dist/commands/tui.js +386 -0
  24. package/dist/commands/tui.js.map +1 -0
  25. package/dist/commands/ui.d.ts +3 -1
  26. package/dist/commands/ui.d.ts.map +1 -1
  27. package/dist/commands/ui.js +76 -19
  28. package/dist/commands/ui.js.map +1 -1
  29. package/dist/generators/base.d.ts.map +1 -1
  30. package/dist/generators/base.js +78 -66
  31. package/dist/generators/base.js.map +1 -1
  32. package/dist/generators/codex.js +11 -11
  33. package/dist/generators/copilot.js +17 -17
  34. package/dist/generators/cursor.js +5 -5
  35. package/dist/generators/index.js +50 -50
  36. package/dist/generators/map-generator.js +14 -14
  37. package/dist/generators/skills-generator.js +28 -28
  38. package/dist/gui/app_logo.png +0 -0
  39. package/dist/gui/assets/index-Ct3qq-X4.js +25 -0
  40. package/dist/gui/assets/index-P_ZrgHXg.css +1 -0
  41. package/dist/gui/icons.svg +24 -24
  42. package/dist/gui/index.html +18 -18
  43. package/dist/index.js +43 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +182 -1
  47. package/dist/server.js.map +1 -1
  48. package/dist/server.test.js +78 -0
  49. package/dist/server.test.js.map +1 -1
  50. package/dist/types.d.ts +4 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/utils/git.d.ts +16 -0
  53. package/dist/utils/git.d.ts.map +1 -1
  54. package/dist/utils/git.js +52 -0
  55. package/dist/utils/git.js.map +1 -1
  56. package/dist/utils/multi-git.d.ts +2 -0
  57. package/dist/utils/multi-git.d.ts.map +1 -1
  58. package/dist/utils/multi-git.js +5 -2
  59. package/dist/utils/multi-git.js.map +1 -1
  60. package/dist/utils/update-check.d.ts +3 -1
  61. package/dist/utils/update-check.d.ts.map +1 -1
  62. package/dist/utils/update-check.js +23 -6
  63. package/dist/utils/update-check.js.map +1 -1
  64. package/dist/utils/workflows.d.ts +11 -0
  65. package/dist/utils/workflows.d.ts.map +1 -0
  66. package/dist/utils/workflows.js +156 -0
  67. package/dist/utils/workflows.js.map +1 -0
  68. package/dist/utils/workflows.test.d.ts +2 -0
  69. package/dist/utils/workflows.test.d.ts.map +1 -0
  70. package/dist/utils/workflows.test.js +86 -0
  71. package/dist/utils/workflows.test.js.map +1 -0
  72. package/extension/media/icon.svg +1 -0
  73. package/extension/package-lock.json +2673 -2811
  74. package/extension/package.json +108 -70
  75. package/extension/src/extension.ts +631 -204
  76. package/extension/tsconfig.json +21 -19
  77. package/gui/README.md +73 -73
  78. package/gui/e2e/wizard.spec.ts +346 -327
  79. package/gui/eslint.config.js +26 -26
  80. package/gui/index.html +17 -17
  81. package/gui/package-lock.json +3079 -3118
  82. package/gui/package.json +36 -36
  83. package/gui/playwright.config.ts +41 -41
  84. package/gui/public/app_logo.png +0 -0
  85. package/gui/public/icons.svg +24 -24
  86. package/gui/src/App.css +1 -1
  87. package/gui/src/App.tsx +3407 -2722
  88. package/gui/src/assets/vite.svg +1 -1
  89. package/gui/src/features/changes/ChangesViewer.tsx +388 -388
  90. package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
  91. package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
  92. package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
  93. package/gui/src/features/services/ServiceConsole.tsx +250 -250
  94. package/gui/src/features/sessions/SessionHistory.tsx +195 -195
  95. package/gui/src/features/workspace/WorkspaceList.tsx +666 -666
  96. package/gui/src/index.css +152 -110
  97. package/gui/src/main.tsx +94 -10
  98. package/gui/src/types.ts +62 -62
  99. package/gui/tsconfig.app.json +25 -25
  100. package/gui/tsconfig.json +7 -7
  101. package/gui/tsconfig.node.json +24 -24
  102. package/gui/vite.config.ts +12 -12
  103. package/package.json +56 -56
  104. package/resources/workflows/plan-implement-review.md +8 -0
  105. package/resources/workflows/research-verify.md +6 -0
  106. package/resources/workflows/solo-developer.md +3 -0
  107. package/scripts/simulate-workspaces.ts +55 -55
  108. package/src/analyzers/detect-apis.ts +290 -290
  109. package/src/analyzers/detect-deps.ts +315 -315
  110. package/src/analyzers/detect-existing.ts +74 -74
  111. package/src/analyzers/detect-ports.ts +110 -110
  112. package/src/analyzers/index.ts +97 -97
  113. package/src/analyzers/messaging-analyzer.ts +254 -254
  114. package/src/analyzers/readme-summarizer.ts +102 -102
  115. package/src/analyzers/run-analyzer.ts +269 -269
  116. package/src/analyzers/tech-stack.ts +283 -283
  117. package/src/commands/add-repo.ts +157 -157
  118. package/src/commands/commands.test.ts +155 -155
  119. package/src/commands/commit.ts +131 -131
  120. package/src/commands/create.ts +207 -207
  121. package/src/commands/desktop.ts +128 -0
  122. package/src/commands/diff.ts +125 -125
  123. package/src/commands/doctor.ts +357 -357
  124. package/src/commands/handoff.ts +266 -266
  125. package/src/commands/init.ts +134 -134
  126. package/src/commands/list.ts +46 -46
  127. package/src/commands/logs.ts +63 -63
  128. package/src/commands/mcp.ts +92 -92
  129. package/src/commands/open.ts +129 -129
  130. package/src/commands/pack.ts +73 -73
  131. package/src/commands/refresh.ts +147 -147
  132. package/src/commands/remove.ts +98 -98
  133. package/src/commands/start.ts +117 -117
  134. package/src/commands/status.ts +54 -54
  135. package/src/commands/stop.ts +55 -55
  136. package/src/commands/sync.ts +151 -153
  137. package/src/commands/tui.ts +424 -0
  138. package/src/commands/ui.ts +111 -47
  139. package/src/core/config.test.ts +96 -96
  140. package/src/core/config.ts +115 -115
  141. package/src/core/graph.ts +344 -344
  142. package/src/core/packer.test.ts +99 -99
  143. package/src/core/packer.ts +107 -107
  144. package/src/core/scanner.test.ts +61 -61
  145. package/src/core/scanner.ts +91 -91
  146. package/src/core/workspace.ts +380 -380
  147. package/src/core/worktree.ts +120 -120
  148. package/src/generators/antigravity.ts +32 -32
  149. package/src/generators/base.ts +240 -227
  150. package/src/generators/claude.ts +34 -34
  151. package/src/generators/codex.ts +48 -48
  152. package/src/generators/copilot.ts +56 -56
  153. package/src/generators/cursor.ts +44 -44
  154. package/src/generators/index.ts +250 -250
  155. package/src/generators/map-generator.test.ts +159 -159
  156. package/src/generators/map-generator.ts +542 -542
  157. package/src/generators/plan-generator.ts +481 -481
  158. package/src/generators/skills-generator.ts +259 -259
  159. package/src/index.ts +426 -382
  160. package/src/mcp/server.ts +294 -294
  161. package/src/orchestration/detect.ts +313 -313
  162. package/src/orchestration/index.ts +7 -7
  163. package/src/orchestration/runner.ts +283 -283
  164. package/src/server.test.ts +555 -464
  165. package/src/server.ts +1308 -1104
  166. package/src/types.ts +408 -403
  167. package/src/utils/detect-ai.test.ts +44 -44
  168. package/src/utils/detect-ai.ts +84 -84
  169. package/src/utils/detect-editors.test.ts +48 -48
  170. package/src/utils/detect-editors.ts +57 -57
  171. package/src/utils/git.test.ts +74 -74
  172. package/src/utils/git.ts +117 -61
  173. package/src/utils/local-ai.test.ts +130 -130
  174. package/src/utils/local-ai.ts +111 -111
  175. package/src/utils/multi-git.ts +313 -306
  176. package/src/utils/prompts.ts +209 -209
  177. package/src/utils/session-finder.ts +483 -483
  178. package/src/utils/system-scanner.test.ts +89 -89
  179. package/src/utils/system-scanner.ts +96 -96
  180. package/src/utils/update-check.ts +309 -275
  181. package/src/utils/workflows.test.ts +114 -0
  182. package/src/utils/workflows.ts +178 -0
  183. package/tsconfig.json +19 -19
  184. package/vitest.config.ts +14 -14
  185. package/dist/gui/assets/index-CFWwqFux.js +0 -23
  186. package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
@@ -1,666 +1,666 @@
1
- import React from 'react';
2
- import { Play, RefreshCw, FolderGit2, Sparkles, ExternalLink, Trash2, Terminal, Cpu, Layers, Copy, Check } from 'lucide-react';
3
- import type { Feature, RunningService, ServiceConfig, OrchestrationDetection, RepoInfo, DetectedAI } from '../../types.js';
4
-
5
- // Feature subcomponents
6
- import { SessionHistory } from '../sessions/SessionHistory.js';
7
- import { ServiceConsole } from '../services/ServiceConsole.js';
8
- import { ChangesViewer } from '../changes/ChangesViewer.js';
9
- import { KnowledgeBase } from '../knowledge/KnowledgeBase.js';
10
- import { ImplementationPlan } from '../plan/ImplementationPlan.js';
11
-
12
- interface WorkspaceListProps {
13
- workspacesLoading: boolean;
14
- workspaces: Feature[];
15
- activeWsId: string | null;
16
- setActiveWsId: (id: string | null) => void;
17
- resumingWs: string | null;
18
- handleResumeSession: (ws: Feature, sessionId?: string, assistant?: string) => Promise<void>;
19
- handleCopyPrompt: (ws: Feature) => void;
20
- handleOpenInEditor: (workspacePath: string) => Promise<void>;
21
- fetchWorkspaces: () => Promise<void>;
22
-
23
- // Deletion and repo addition props
24
- repos: RepoInfo[];
25
- deleteWsLoading: string | null;
26
- addRepoLoading: boolean;
27
- handleDeleteWorkspace: (wsName: string) => Promise<void>;
28
- handleAddRepo: (wsName: string, repoPath: string) => Promise<void>;
29
-
30
- // ServiceConsole props
31
- services: ServiceConfig[];
32
- runningServices: RunningService[];
33
- selectedLogService: string | null;
34
- serviceLogs: string;
35
- logsEndRef: React.RefObject<HTMLDivElement | null>;
36
- setSelectedLogService: (val: string | null) => void;
37
- handleStartServices: (wsId: string) => Promise<void>;
38
- handleStopServices: (wsId: string) => Promise<void>;
39
- orchTools: OrchestrationDetection[];
40
- servicesLoading: boolean;
41
-
42
-
43
- // ChangesViewer props
44
- gitChanges: any[];
45
- gitChangesLoading: boolean;
46
- syncLoading: boolean;
47
- syncResults: any[] | null;
48
- commitMessage: string;
49
- showCommitModal: boolean;
50
- commitLoading: boolean;
51
- commitResults: any[] | null;
52
- setSyncResults: (val: any[] | null) => void;
53
- setCommitResults: (val: any[] | null) => void;
54
- setCommitMessage: (val: string) => void;
55
- setShowCommitModal: (val: boolean) => void;
56
- fetchGitChanges: (wsId: string) => Promise<void>;
57
- handleSyncAll: (wsId: string) => Promise<void>;
58
- handleCommitAll: (wsId: string) => Promise<void>;
59
-
60
- // KnowledgeBase props
61
- knowledgeContent: string;
62
- knowledgeLoading: boolean;
63
- isEditingKnowledge: boolean;
64
- editedKnowledge: string;
65
- saveKnowledgeLoading: boolean;
66
- setEditedKnowledge: (val: string) => void;
67
- setIsEditingKnowledge: (val: boolean) => void;
68
- handleSaveKnowledge: (wsId: string) => Promise<void>;
69
-
70
- // ImplementationPlan props
71
- planContent: string;
72
- planLoading: boolean;
73
-
74
- // SessionHistory props
75
- sessions: any[];
76
- sessionsLoading: boolean;
77
- activeSession: any | null;
78
- transcript: any[];
79
- transcriptLoading: boolean;
80
- setActiveSession: (val: any | null) => void;
81
- setTranscript: (val: any[]) => void;
82
- fetchSessionTranscript: (assistant: string, sessionId: string) => Promise<void>;
83
-
84
- // Navigation tab states
85
- subTab: 'overview' | 'sessions' | 'services' | 'changes' | 'knowledge' | 'plan';
86
- setSubTab: (tab: 'overview' | 'sessions' | 'services' | 'changes' | 'knowledge' | 'plan') => void;
87
- aiAssistants: DetectedAI[];
88
- }
89
-
90
- const parseInlineStyles = (text: string): React.ReactNode => {
91
- const tokenRegex = /(\*\*.*?\*\*|`.*?`|\*.*?\*)/g;
92
- const parts = text.split(tokenRegex);
93
-
94
- return parts.map((part, index) => {
95
- if (part.startsWith('**') && part.endsWith('**')) {
96
- return <strong key={index} className="font-bold text-gray-200">{part.slice(2, -2)}</strong>;
97
- }
98
- if (part.startsWith('`') && part.endsWith('`')) {
99
- return <code key={index} className="px-1.5 py-0.5 bg-gray-900 border border-gray-800 text-indigo-300 rounded font-mono text-[10px]">{part.slice(1, -1)}</code>;
100
- }
101
- if (part.startsWith('*') && part.endsWith('*')) {
102
- return <em key={index} className="italic text-gray-300">{part.slice(1, -1)}</em>;
103
- }
104
- return part;
105
- });
106
- };
107
-
108
- const renderFormattedDescription = (text: string) => {
109
- if (!text) return null;
110
-
111
- const lines = text.split('\n');
112
- const elements: React.ReactNode[] = [];
113
- let currentList: { type: 'ul' | 'ol'; items: string[] } | null = null;
114
-
115
- const flushList = (key: number) => {
116
- if (currentList) {
117
- const ListTag = currentList.type;
118
- elements.push(
119
- <ListTag key={`list-${key}`} className={currentList.type === 'ul' ? 'list-disc pl-5 my-1.5 space-y-1' : 'list-decimal pl-5 my-1.5 space-y-1'}>
120
- {currentList.items.map((item, idx) => (
121
- <li key={`item-${idx}`} className="leading-relaxed">{parseInlineStyles(item)}</li>
122
- ))}
123
- </ListTag>
124
- );
125
- currentList = null;
126
- }
127
- };
128
-
129
- lines.forEach((line, idx) => {
130
- const trimmed = line.trim();
131
-
132
- // Check for bullet list item
133
- const bulletMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
134
- if (bulletMatch) {
135
- if (!currentList || currentList.type !== 'ul') {
136
- flushList(idx);
137
- currentList = { type: 'ul', items: [] };
138
- }
139
- currentList.items.push(bulletMatch[2]);
140
- return;
141
- }
142
-
143
- // Check for numbered list item
144
- const numberMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
145
- if (numberMatch) {
146
- if (!currentList || currentList.type !== 'ol') {
147
- flushList(idx);
148
- currentList = { type: 'ol', items: [] };
149
- }
150
- currentList.items.push(numberMatch[2]);
151
- return;
152
- }
153
-
154
- // If not a list item, flush any active list first
155
- flushList(idx);
156
-
157
- if (trimmed === '') {
158
- elements.push(<div key={`empty-${idx}`} className="h-2" />);
159
- } else {
160
- elements.push(
161
- <div key={`line-${idx}`} className="leading-relaxed">
162
- {parseInlineStyles(line)}
163
- </div>
164
- );
165
- }
166
- });
167
-
168
- flushList(lines.length);
169
- return <div className="space-y-1">{elements}</div>;
170
- };
171
-
172
- export const WorkspaceList: React.FC<WorkspaceListProps> = ({
173
- workspacesLoading,
174
- workspaces,
175
- activeWsId,
176
- setActiveWsId,
177
- resumingWs,
178
- handleResumeSession,
179
- handleCopyPrompt,
180
- handleOpenInEditor,
181
- fetchWorkspaces,
182
- repos,
183
- deleteWsLoading,
184
- addRepoLoading,
185
- handleDeleteWorkspace,
186
- handleAddRepo,
187
-
188
- // ServiceConsole
189
- services,
190
- runningServices,
191
- selectedLogService,
192
- serviceLogs,
193
- logsEndRef,
194
- setSelectedLogService,
195
- handleStartServices,
196
- handleStopServices,
197
- orchTools,
198
- servicesLoading,
199
-
200
-
201
- // ChangesViewer
202
- gitChanges,
203
- gitChangesLoading,
204
- syncLoading,
205
- syncResults,
206
- commitMessage,
207
- showCommitModal,
208
- commitLoading,
209
- commitResults,
210
- setSyncResults,
211
- setCommitResults,
212
- setCommitMessage,
213
- setShowCommitModal,
214
- fetchGitChanges,
215
- handleSyncAll,
216
- handleCommitAll,
217
-
218
- // KnowledgeBase
219
- knowledgeContent,
220
- knowledgeLoading,
221
- isEditingKnowledge,
222
- editedKnowledge,
223
- saveKnowledgeLoading,
224
- setEditedKnowledge,
225
- setIsEditingKnowledge,
226
- handleSaveKnowledge,
227
-
228
- // ImplementationPlan
229
- planContent,
230
- planLoading,
231
-
232
- // SessionHistory
233
- sessions,
234
- sessionsLoading,
235
- activeSession,
236
- transcript,
237
- transcriptLoading,
238
- setActiveSession,
239
- setTranscript,
240
- fetchSessionTranscript,
241
-
242
- // Tabs
243
- subTab,
244
- setSubTab,
245
- aiAssistants,
246
- }) => {
247
- const [copiedText, setCopiedText] = React.useState<string | null>(null);
248
- const handleCopy = (text: string) => {
249
- navigator.clipboard.writeText(text);
250
- setCopiedText(text);
251
- setTimeout(() => setCopiedText(null), 2000);
252
- };
253
-
254
- return (
255
- <div className="max-w-5xl mx-auto">
256
- <header className="flex justify-between items-center mb-8">
257
- <div>
258
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">Active Workspaces</h1>
259
- <p className="text-sm text-gray-400">Monitor running servers, libraries, and processes on your development branches.</p>
260
- </div>
261
- <button
262
- className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
263
- onClick={fetchWorkspaces}
264
- disabled={workspacesLoading}
265
- >
266
- <RefreshCw size={14} className={workspacesLoading ? 'animate-spin text-indigo-400' : ''} /> Refresh
267
- </button>
268
- </header>
269
-
270
- {workspacesLoading ? (
271
- <div className="flex flex-col items-center py-40 gap-4 text-gray-400">
272
- <RefreshCw className="animate-spin text-indigo-400" size={32} />
273
- <span className="text-sm">Fetching workspace details...</span>
274
- </div>
275
- ) : workspaces.length === 0 ? (
276
- <div className="flex flex-col items-center justify-center py-24 bg-[#111827]/20 border border-gray-800/80 rounded-xl text-center">
277
- <FolderGit2 size={44} className="text-gray-600 mb-4" />
278
- <h3 className="text-lg font-bold text-white">No Active Workspaces</h3>
279
- <p className="text-xs text-gray-505 max-w-sm mt-1">
280
- No feature workspaces were detected in your development folder. Create a workspace to start.
281
- </p>
282
- </div>
283
- ) : (
284
- <div className="flex flex-col gap-6">
285
- {workspaces.map((ws) => {
286
- const isExpanded = activeWsId === ws.branchName;
287
- return (
288
- <div key={ws.id} className="glass-card p-8 rounded-2xl shadow-xl hover:shadow-2xl border border-slate-800/40 relative overflow-hidden transition-all duration-300 before:absolute before:inset-x-0 before:top-0 before:h-[1px] before:bg-gradient-to-r before:from-indigo-500/10 via-purple-500/10 to-transparent">
289
- <div className="flex justify-between items-start gap-4 mb-4">
290
- <div>
291
- <h3 className="text-lg font-bold text-white hover:text-indigo-400 transition-colors">{ws.branchName}</h3>
292
- <div className="flex items-center gap-3 text-xs text-slate-500 mt-1">
293
- <span>Created: {new Date(ws.createdAt).toLocaleDateString()}</span>
294
- <span className="h-1 w-1 rounded-full bg-slate-850"></span>
295
- <span>{ws.repos.length} repos</span>
296
- </div>
297
- </div>
298
- <div className="flex gap-2">
299
- <button
300
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-emerald-650 hover:bg-emerald-600 text-white rounded-xl text-xs font-semibold transition-all cursor-pointer shadow-md shadow-emerald-600/10 disabled:opacity-40 disabled:cursor-not-allowed"
301
- onClick={() => handleResumeSession(ws)}
302
- disabled={resumingWs === ws.branchName}
303
- >
304
- <Play size={12} className={resumingWs === ws.branchName ? 'animate-spin' : ''} />
305
- {resumingWs === ws.branchName ? 'Resuming...' : 'Resume Session'}
306
- </button>
307
- <button
308
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-slate-800 hover:border-slate-700 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-350 hover:text-white"
309
- onClick={() => handleCopyPrompt(ws)}
310
- >
311
- <Sparkles size={12} className="text-cyan-400" /> Copy Prompt
312
- </button>
313
- <button
314
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-slate-800 hover:border-slate-700 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-350 hover:text-white"
315
- onClick={() => handleOpenInEditor(ws.workspacePath)}
316
- >
317
- <ExternalLink size={12} /> Open
318
- </button>
319
- <button
320
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-indigo-500 hover:bg-indigo-600 text-white rounded-xl text-xs font-semibold transition-all cursor-pointer shadow-md shadow-indigo-500/10"
321
- onClick={() => {
322
- if (isExpanded) {
323
- setActiveWsId(null);
324
- } else {
325
- setActiveWsId(ws.branchName);
326
- }
327
- }}
328
- >
329
- {isExpanded ? 'Hide Runner' : 'Orchestrate'}
330
- </button>
331
- <button
332
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-red-950/40 border border-red-900/60 hover:bg-red-900/60 hover:border-red-800 rounded-xl text-xs font-semibold text-red-200 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed animate-fade-in"
333
- onClick={() => handleDeleteWorkspace(ws.branchName)}
334
- disabled={deleteWsLoading === ws.branchName}
335
- >
336
- <Trash2 size={12} className={deleteWsLoading === ws.branchName ? 'animate-spin' : ''} />
337
- {deleteWsLoading === ws.branchName ? 'Deleting...' : 'Delete'}
338
- </button>
339
- </div>
340
- </div>
341
-
342
- <div className="text-xs text-slate-350 bg-slate-950/60 border-l-2 border-indigo-500/80 rounded-r-xl px-5 py-3.5 mb-5 font-normal leading-relaxed relative overflow-hidden before:absolute before:inset-y-0 before:right-0 before:w-12 before:bg-gradient-to-r before:from-transparent before:to-indigo-500/2">
343
- {renderFormattedDescription(ws.description)}
344
- </div>
345
-
346
- <div className="flex items-center gap-3 mb-4">
347
- <div className="flex flex-wrap gap-2">
348
- {ws.repos.map((repoPath) => (
349
- <span key={repoPath} className="text-[10px] px-2.5 py-1.5 bg-slate-950/60 border border-slate-850 text-slate-300 rounded-lg font-semibold select-all">
350
- {repoPath.split(/[\\/]/).pop()}
351
- </span>
352
- ))}
353
- </div>
354
-
355
- {repos.filter((r) => !ws.repos.includes(r.path)).length > 0 && (
356
- <div className="flex items-center gap-1.5 ml-auto">
357
- <select
358
- id={`add-repo-select-feature-${ws.branchName}`}
359
- className="bg-slate-905 border border-slate-800 text-slate-300 rounded-lg text-[10px] px-3 py-1.5 font-semibold outline-none focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-all hover:border-slate-700"
360
- defaultValue=""
361
- onChange={async (e) => {
362
- const val = e.target.value;
363
- if (val) {
364
- if (window.confirm(`Are you sure you want to add repository "${val.split(/[\\/]/).pop()}" to this workspace?\nThis will create a new git worktree and re-run analysis.`)) {
365
- await handleAddRepo(ws.branchName, val);
366
- }
367
- e.target.value = "";
368
- }
369
- }}
370
- disabled={addRepoLoading}
371
- >
372
- <option value="" disabled>+ Add Repository</option>
373
- {repos
374
- .filter((r) => !ws.repos.includes(r.path))
375
- .map((r) => (
376
- <option key={r.path} value={r.path}>{r.name}</option>
377
- ))
378
- }
379
- </select>
380
- </div>
381
- )}
382
- </div>
383
-
384
- {/* Expansion: Process management console */}
385
- {isExpanded && (
386
- <div className="mt-6 pt-6 border-t border-slate-850/80">
387
- {/* Sub-tab Navigation */}
388
- <div className="flex flex-wrap items-center gap-1.5 p-1 bg-slate-950/60 rounded-xl border border-slate-900/60 mb-6 w-max max-w-full overflow-x-auto">
389
- <button
390
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
391
- subTab === 'overview'
392
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
393
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
394
- }`}
395
- onClick={() => setSubTab('overview')}
396
- >
397
- Harness & Capabilities
398
- </button>
399
- <button
400
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
401
- subTab === 'sessions'
402
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
403
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
404
- }`}
405
- onClick={() => setSubTab('sessions')}
406
- >
407
- AI Session History
408
- </button>
409
- <button
410
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
411
- subTab === 'services'
412
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
413
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
414
- }`}
415
- onClick={() => setSubTab('services')}
416
- >
417
- Orchestrated Services
418
- </button>
419
- <button
420
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
421
- subTab === 'changes'
422
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
423
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
424
- }`}
425
- onClick={() => setSubTab('changes')}
426
- >
427
- Active Changes (AI Diffs)
428
- </button>
429
- <button
430
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
431
- subTab === 'knowledge'
432
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
433
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
434
- }`}
435
- onClick={() => setSubTab('knowledge')}
436
- >
437
- Knowledge Base
438
- </button>
439
- <button
440
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
441
- subTab === 'plan'
442
- ? 'bg-indigo-500/15 border border-indigo-500/35 text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.05)]'
443
- : 'text-slate-500 hover:text-slate-350 hover:bg-slate-900/30 border border-transparent'
444
- }`}
445
- onClick={() => setSubTab('plan')}
446
- >
447
- Implementation Plan
448
- </button>
449
- </div>
450
-
451
- {subTab === 'overview' && (
452
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-gray-300 animate-fade-in">
453
- {/* Section 1: AI Assistant Harnesses */}
454
- <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
455
- <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
456
- <Cpu size={18} className="text-indigo-400" />
457
- <h4 className="font-bold text-white">AI Assistant Harnesses</h4>
458
- </div>
459
- <p className="text-xs text-gray-400 mb-4">
460
- These coding assistants are configured to run contextually inside this workspace. Select an assistant to see its setup.
461
- </p>
462
- <div className="flex flex-col gap-3">
463
- {['antigravity', 'claude', 'copilot', 'cursor', 'codex'].map((name) => {
464
- const isConfigured = ws.assistants.includes(name as any);
465
- const isDetected = aiAssistants.find((a) => a.name === name)?.detected;
466
- let displayName = name.charAt(0).toUpperCase() + name.slice(1);
467
- if (name === 'antigravity') displayName = 'Antigravity (Ollama/Gemini)';
468
- else if (name === 'codex') displayName = 'OpenAI Codex';
469
- else if (name === 'copilot') displayName = 'GitHub Copilot';
470
- else if (name === 'claude') displayName = 'Claude Code';
471
-
472
- let resumeCmd = '';
473
- if (name === 'antigravity') resumeCmd = 'agy --continue';
474
- else if (name === 'claude') resumeCmd = 'claude --resume';
475
- else if (name === 'codex') resumeCmd = 'codex resume';
476
- else if (name === 'copilot') resumeCmd = 'copilot --resume';
477
-
478
- return (
479
- <div key={name} className="flex flex-col p-3 bg-gray-900/40 border border-gray-800/60 rounded-lg">
480
- <div className="flex items-center justify-between gap-2">
481
- <div className="flex items-center gap-2">
482
- <span className={`h-2 w-2 rounded-full ${isConfigured ? 'bg-indigo-500 shadow-sm' : 'bg-gray-700'}`}></span>
483
- <span className="font-semibold text-xs text-gray-200">{displayName}</span>
484
- </div>
485
- <div className="flex items-center gap-1.5 text-[10px] font-medium">
486
- {isConfigured && <span className="text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20 text-[9px] font-bold uppercase tracking-wider">Active</span>}
487
- {isDetected ? (
488
- <span className="text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 text-[9px] font-bold uppercase tracking-wider">Installed</span>
489
- ) : (
490
- <span className="text-gray-500 bg-gray-950 px-2 py-0.5 rounded border border-gray-800/60 text-[9px] font-bold uppercase tracking-wider">Not Probed</span>
491
- )}
492
- </div>
493
- </div>
494
- {isConfigured && resumeCmd && (
495
- <div className="mt-2.5 flex items-center justify-between gap-2 bg-gray-950/80 px-2.5 py-1.5 rounded border border-gray-800/60 font-mono text-[10px]">
496
- <span className="text-indigo-300 truncate">{resumeCmd}</span>
497
- <button
498
- onClick={() => handleCopy(resumeCmd)}
499
- className="text-gray-400 hover:text-white transition-colors cursor-pointer"
500
- title="Copy Resume Command"
501
- >
502
- {copiedText === resumeCmd ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}
503
- </button>
504
- </div>
505
- )}
506
- </div>
507
- );
508
- })}
509
- </div>
510
- </div>
511
-
512
- {/* Section 2: Model Context Protocol (MCP) */}
513
- <div className="flex flex-col gap-6">
514
- <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
515
- <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
516
- <Layers size={18} className="text-indigo-400" />
517
- <h4 className="font-bold text-white">Local MCP Server (Tools)</h4>
518
- </div>
519
- <p className="text-xs text-gray-400 mb-4">
520
- NexusFlow runs a Model Context Protocol (MCP) server that exposes local workspace-scoped tools to your AI agents.
521
- </p>
522
- <div className="flex flex-col gap-3.5">
523
- <div className="flex flex-col gap-1">
524
- <div className="flex items-center gap-2 text-xs font-semibold text-indigo-300">
525
- <code>search_workspace</code>
526
- </div>
527
- <span className="text-[11px] text-gray-400">
528
- Extremely fast query/regex search across all microservices and repos in the workspace.
529
- </span>
530
- </div>
531
- <div className="flex flex-col gap-1 border-t border-gray-800/40 pt-2.5">
532
- <div className="flex items-center gap-2 text-xs font-semibold text-indigo-300">
533
- <code>get_service_logs</code>
534
- </div>
535
- <span className="text-[11px] text-gray-400">
536
- Fetch and parse recent stdout/stderr output from any background service running in the workspace.
537
- </span>
538
- </div>
539
- <div className="flex flex-col gap-1 border-t border-gray-800/40 pt-2.5">
540
- <div className="flex items-center gap-2 text-xs font-semibold text-indigo-300">
541
- <code>delegate_to_local_agent</code>
542
- </div>
543
- <span className="text-[11px] text-gray-400">
544
- Delegate sub-tasks or log analysis to a lightweight LLM running locally to optimize remote token usage.
545
- </span>
546
- </div>
547
- </div>
548
- </div>
549
-
550
- {/* CLI shortcuts */}
551
- <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
552
- <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
553
- <Terminal size={18} className="text-indigo-400" />
554
- <h4 className="font-bold text-white">Workspace CLI Shortcuts</h4>
555
- </div>
556
- <div className="flex flex-col gap-2.5">
557
- {[
558
- { cmd: 'nexusflow start', desc: 'Spin up detected backend/frontend services' },
559
- { cmd: 'nexusflow sync', desc: 'Rebase worktrees with upstream defaults' },
560
- { cmd: 'nexusflow commit -m "update"', desc: 'Stage and commit changes across repos' },
561
- { cmd: 'nexusflow diff', desc: 'Unified codebase diff across repos' },
562
- { cmd: 'nexusflow handoff', desc: 'Generate resume context file' }
563
- ].map(({ cmd, desc }) => (
564
- <div key={cmd} className="flex flex-col gap-1">
565
- <div className="flex items-center justify-between gap-2">
566
- <code className="text-xs text-indigo-300">{cmd}</code>
567
- <button
568
- onClick={() => handleCopy(cmd)}
569
- className="text-gray-400 hover:text-white transition-colors cursor-pointer"
570
- title="Copy Command"
571
- >
572
- {copiedText === cmd ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}
573
- </button>
574
- </div>
575
- <span className="text-[11px] text-gray-550">{desc}</span>
576
- </div>
577
- ))}
578
- </div>
579
- </div>
580
- </div>
581
- </div>
582
- )}
583
-
584
- {subTab === 'sessions' && (
585
- <SessionHistory
586
- ws={ws}
587
- sessions={sessions}
588
- sessionsLoading={sessionsLoading}
589
- activeSession={activeSession}
590
- transcript={transcript}
591
- transcriptLoading={transcriptLoading}
592
- workspaces={workspaces}
593
- setActiveSession={setActiveSession}
594
- setTranscript={setTranscript}
595
- fetchSessionTranscript={fetchSessionTranscript}
596
- handleResumeSession={handleResumeSession}
597
- />
598
- )}
599
-
600
- {subTab === 'services' && (
601
- <ServiceConsole
602
- ws={ws}
603
- services={services}
604
- runningServices={runningServices}
605
- selectedLogService={selectedLogService}
606
- serviceLogs={serviceLogs}
607
- logsEndRef={logsEndRef}
608
- setSelectedLogService={setSelectedLogService}
609
- handleStartServices={handleStartServices}
610
- handleStopServices={handleStopServices}
611
- orchTools={orchTools}
612
- servicesLoading={servicesLoading}
613
- />
614
- )}
615
-
616
- {subTab === 'changes' && (
617
- <ChangesViewer
618
- ws={ws}
619
- gitChanges={gitChanges}
620
- gitChangesLoading={gitChangesLoading}
621
- syncLoading={syncLoading}
622
- syncResults={syncResults}
623
- commitMessage={commitMessage}
624
- showCommitModal={showCommitModal}
625
- commitLoading={commitLoading}
626
- commitResults={commitResults}
627
- setSyncResults={setSyncResults}
628
- setCommitResults={setCommitResults}
629
- setCommitMessage={setCommitMessage}
630
- setShowCommitModal={setShowCommitModal}
631
- fetchGitChanges={fetchGitChanges}
632
- handleSyncAll={handleSyncAll}
633
- handleCommitAll={handleCommitAll}
634
- />
635
- )}
636
-
637
- {subTab === 'knowledge' && (
638
- <KnowledgeBase
639
- ws={ws}
640
- knowledgeContent={knowledgeContent}
641
- knowledgeLoading={knowledgeLoading}
642
- isEditingKnowledge={isEditingKnowledge}
643
- editedKnowledge={editedKnowledge}
644
- saveKnowledgeLoading={saveKnowledgeLoading}
645
- setEditedKnowledge={setEditedKnowledge}
646
- setIsEditingKnowledge={setIsEditingKnowledge}
647
- handleSaveKnowledge={handleSaveKnowledge}
648
- />
649
- )}
650
-
651
- {subTab === 'plan' && (
652
- <ImplementationPlan
653
- planContent={planContent}
654
- planLoading={planLoading}
655
- />
656
- )}
657
- </div>
658
- )}
659
- </div>
660
- );
661
- })}
662
- </div>
663
- )}
664
- </div>
665
- );
666
- };
1
+ import React from 'react';
2
+ import { Play, RefreshCw, FolderGit2, Sparkles, ExternalLink, Trash2, Terminal, Cpu, Layers, Copy, Check } from 'lucide-react';
3
+ import type { Feature, RunningService, ServiceConfig, OrchestrationDetection, RepoInfo, DetectedAI } from '../../types.js';
4
+
5
+ // Feature subcomponents
6
+ import { SessionHistory } from '../sessions/SessionHistory.js';
7
+ import { ServiceConsole } from '../services/ServiceConsole.js';
8
+ import { ChangesViewer } from '../changes/ChangesViewer.js';
9
+ import { KnowledgeBase } from '../knowledge/KnowledgeBase.js';
10
+ import { ImplementationPlan } from '../plan/ImplementationPlan.js';
11
+
12
+ interface WorkspaceListProps {
13
+ workspacesLoading: boolean;
14
+ workspaces: Feature[];
15
+ activeWsId: string | null;
16
+ setActiveWsId: (id: string | null) => void;
17
+ resumingWs: string | null;
18
+ handleResumeSession: (ws: Feature, sessionId?: string, assistant?: string) => Promise<void>;
19
+ handleCopyPrompt: (ws: Feature) => void;
20
+ handleOpenInEditor: (workspacePath: string) => Promise<void>;
21
+ fetchWorkspaces: () => Promise<void>;
22
+
23
+ // Deletion and repo addition props
24
+ repos: RepoInfo[];
25
+ deleteWsLoading: string | null;
26
+ addRepoLoading: boolean;
27
+ handleDeleteWorkspace: (wsName: string) => Promise<void>;
28
+ handleAddRepo: (wsName: string, repoPath: string) => Promise<void>;
29
+
30
+ // ServiceConsole props
31
+ services: ServiceConfig[];
32
+ runningServices: RunningService[];
33
+ selectedLogService: string | null;
34
+ serviceLogs: string;
35
+ logsEndRef: React.RefObject<HTMLDivElement | null>;
36
+ setSelectedLogService: (val: string | null) => void;
37
+ handleStartServices: (wsId: string) => Promise<void>;
38
+ handleStopServices: (wsId: string) => Promise<void>;
39
+ orchTools: OrchestrationDetection[];
40
+ servicesLoading: boolean;
41
+
42
+
43
+ // ChangesViewer props
44
+ gitChanges: any[];
45
+ gitChangesLoading: boolean;
46
+ syncLoading: boolean;
47
+ syncResults: any[] | null;
48
+ commitMessage: string;
49
+ showCommitModal: boolean;
50
+ commitLoading: boolean;
51
+ commitResults: any[] | null;
52
+ setSyncResults: (val: any[] | null) => void;
53
+ setCommitResults: (val: any[] | null) => void;
54
+ setCommitMessage: (val: string) => void;
55
+ setShowCommitModal: (val: boolean) => void;
56
+ fetchGitChanges: (wsId: string) => Promise<void>;
57
+ handleSyncAll: (wsId: string) => Promise<void>;
58
+ handleCommitAll: (wsId: string) => Promise<void>;
59
+
60
+ // KnowledgeBase props
61
+ knowledgeContent: string;
62
+ knowledgeLoading: boolean;
63
+ isEditingKnowledge: boolean;
64
+ editedKnowledge: string;
65
+ saveKnowledgeLoading: boolean;
66
+ setEditedKnowledge: (val: string) => void;
67
+ setIsEditingKnowledge: (val: boolean) => void;
68
+ handleSaveKnowledge: (wsId: string) => Promise<void>;
69
+
70
+ // ImplementationPlan props
71
+ planContent: string;
72
+ planLoading: boolean;
73
+
74
+ // SessionHistory props
75
+ sessions: any[];
76
+ sessionsLoading: boolean;
77
+ activeSession: any | null;
78
+ transcript: any[];
79
+ transcriptLoading: boolean;
80
+ setActiveSession: (val: any | null) => void;
81
+ setTranscript: (val: any[]) => void;
82
+ fetchSessionTranscript: (assistant: string, sessionId: string) => Promise<void>;
83
+
84
+ // Navigation tab states
85
+ subTab: 'overview' | 'sessions' | 'services' | 'changes' | 'knowledge' | 'plan';
86
+ setSubTab: (tab: 'overview' | 'sessions' | 'services' | 'changes' | 'knowledge' | 'plan') => void;
87
+ aiAssistants: DetectedAI[];
88
+ }
89
+
90
+ const parseInlineStyles = (text: string): React.ReactNode => {
91
+ const tokenRegex = /(\*\*.*?\*\*|`.*?`|\*.*?\*)/g;
92
+ const parts = text.split(tokenRegex);
93
+
94
+ return parts.map((part, index) => {
95
+ if (part.startsWith('**') && part.endsWith('**')) {
96
+ return <strong key={index} className="font-bold text-gray-200">{part.slice(2, -2)}</strong>;
97
+ }
98
+ if (part.startsWith('`') && part.endsWith('`')) {
99
+ return <code key={index} className="px-1.5 py-0.5 bg-gray-900 border border-gray-800 text-cyan-400 rounded font-mono text-[10px]">{part.slice(1, -1)}</code>;
100
+ }
101
+ if (part.startsWith('*') && part.endsWith('*')) {
102
+ return <em key={index} className="italic text-gray-300">{part.slice(1, -1)}</em>;
103
+ }
104
+ return part;
105
+ });
106
+ };
107
+
108
+ const renderFormattedDescription = (text: string) => {
109
+ if (!text) return null;
110
+
111
+ const lines = text.split('\n');
112
+ const elements: React.ReactNode[] = [];
113
+ let currentList: { type: 'ul' | 'ol'; items: string[] } | null = null;
114
+
115
+ const flushList = (key: number) => {
116
+ if (currentList) {
117
+ const ListTag = currentList.type;
118
+ elements.push(
119
+ <ListTag key={`list-${key}`} className={currentList.type === 'ul' ? 'list-disc pl-5 my-1.5 space-y-1' : 'list-decimal pl-5 my-1.5 space-y-1'}>
120
+ {currentList.items.map((item, idx) => (
121
+ <li key={`item-${idx}`} className="leading-relaxed">{parseInlineStyles(item)}</li>
122
+ ))}
123
+ </ListTag>
124
+ );
125
+ currentList = null;
126
+ }
127
+ };
128
+
129
+ lines.forEach((line, idx) => {
130
+ const trimmed = line.trim();
131
+
132
+ // Check for bullet list item
133
+ const bulletMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
134
+ if (bulletMatch) {
135
+ if (!currentList || currentList.type !== 'ul') {
136
+ flushList(idx);
137
+ currentList = { type: 'ul', items: [] };
138
+ }
139
+ currentList.items.push(bulletMatch[2]);
140
+ return;
141
+ }
142
+
143
+ // Check for numbered list item
144
+ const numberMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
145
+ if (numberMatch) {
146
+ if (!currentList || currentList.type !== 'ol') {
147
+ flushList(idx);
148
+ currentList = { type: 'ol', items: [] };
149
+ }
150
+ currentList.items.push(numberMatch[2]);
151
+ return;
152
+ }
153
+
154
+ // If not a list item, flush any active list first
155
+ flushList(idx);
156
+
157
+ if (trimmed === '') {
158
+ elements.push(<div key={`empty-${idx}`} className="h-2" />);
159
+ } else {
160
+ elements.push(
161
+ <div key={`line-${idx}`} className="leading-relaxed">
162
+ {parseInlineStyles(line)}
163
+ </div>
164
+ );
165
+ }
166
+ });
167
+
168
+ flushList(lines.length);
169
+ return <div className="space-y-1">{elements}</div>;
170
+ };
171
+
172
+ export const WorkspaceList: React.FC<WorkspaceListProps> = ({
173
+ workspacesLoading,
174
+ workspaces,
175
+ activeWsId,
176
+ setActiveWsId,
177
+ resumingWs,
178
+ handleResumeSession,
179
+ handleCopyPrompt,
180
+ handleOpenInEditor,
181
+ fetchWorkspaces,
182
+ repos,
183
+ deleteWsLoading,
184
+ addRepoLoading,
185
+ handleDeleteWorkspace,
186
+ handleAddRepo,
187
+
188
+ // ServiceConsole
189
+ services,
190
+ runningServices,
191
+ selectedLogService,
192
+ serviceLogs,
193
+ logsEndRef,
194
+ setSelectedLogService,
195
+ handleStartServices,
196
+ handleStopServices,
197
+ orchTools,
198
+ servicesLoading,
199
+
200
+
201
+ // ChangesViewer
202
+ gitChanges,
203
+ gitChangesLoading,
204
+ syncLoading,
205
+ syncResults,
206
+ commitMessage,
207
+ showCommitModal,
208
+ commitLoading,
209
+ commitResults,
210
+ setSyncResults,
211
+ setCommitResults,
212
+ setCommitMessage,
213
+ setShowCommitModal,
214
+ fetchGitChanges,
215
+ handleSyncAll,
216
+ handleCommitAll,
217
+
218
+ // KnowledgeBase
219
+ knowledgeContent,
220
+ knowledgeLoading,
221
+ isEditingKnowledge,
222
+ editedKnowledge,
223
+ saveKnowledgeLoading,
224
+ setEditedKnowledge,
225
+ setIsEditingKnowledge,
226
+ handleSaveKnowledge,
227
+
228
+ // ImplementationPlan
229
+ planContent,
230
+ planLoading,
231
+
232
+ // SessionHistory
233
+ sessions,
234
+ sessionsLoading,
235
+ activeSession,
236
+ transcript,
237
+ transcriptLoading,
238
+ setActiveSession,
239
+ setTranscript,
240
+ fetchSessionTranscript,
241
+
242
+ // Tabs
243
+ subTab,
244
+ setSubTab,
245
+ aiAssistants,
246
+ }) => {
247
+ const [copiedText, setCopiedText] = React.useState<string | null>(null);
248
+ const handleCopy = (text: string) => {
249
+ navigator.clipboard.writeText(text);
250
+ setCopiedText(text);
251
+ setTimeout(() => setCopiedText(null), 2000);
252
+ };
253
+
254
+ return (
255
+ <div className="max-w-5xl mx-auto">
256
+ <header className="flex justify-between items-center mb-8">
257
+ <div>
258
+ <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">Active Workspaces</h1>
259
+ <p className="text-sm text-gray-400">Monitor running servers, libraries, and processes on your development branches.</p>
260
+ </div>
261
+ <button
262
+ className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
263
+ onClick={fetchWorkspaces}
264
+ disabled={workspacesLoading}
265
+ >
266
+ <RefreshCw size={14} className={workspacesLoading ? 'animate-spin text-cyan-400' : ''} /> Refresh
267
+ </button>
268
+ </header>
269
+
270
+ {workspacesLoading ? (
271
+ <div className="flex flex-col items-center py-40 gap-4 text-gray-400">
272
+ <RefreshCw className="animate-spin text-cyan-400" size={32} />
273
+ <span className="text-sm">Fetching workspace details...</span>
274
+ </div>
275
+ ) : workspaces.length === 0 ? (
276
+ <div className="flex flex-col items-center justify-center py-24 bg-[#111827]/20 border border-gray-800/80 rounded-xl text-center">
277
+ <FolderGit2 size={44} className="text-gray-600 mb-4" />
278
+ <h3 className="text-lg font-bold text-white">No Active Workspaces</h3>
279
+ <p className="text-xs text-gray-505 max-w-sm mt-1">
280
+ No feature workspaces were detected in your development folder. Create a workspace to start.
281
+ </p>
282
+ </div>
283
+ ) : (
284
+ <div className="flex flex-col gap-6">
285
+ {workspaces.map((ws) => {
286
+ const isExpanded = activeWsId === ws.branchName;
287
+ return (
288
+ <div key={ws.id} className="glass-card p-8 rounded-2xl shadow-xl hover:shadow-2xl border border-slate-800/40 relative overflow-hidden transition-all duration-300 before:absolute before:inset-x-0 before:top-0 before:h-[1px] before:bg-gradient-to-r before:from-cyan-500/10 via-violet-500/10 to-transparent">
289
+ <div className="flex justify-between items-start gap-4 mb-4">
290
+ <div>
291
+ <h3 className="text-lg font-bold text-white hover:text-cyan-400 transition-colors">{ws.branchName}</h3>
292
+ <div className="flex items-center gap-3 text-xs text-slate-500 mt-1">
293
+ <span>Created: {new Date(ws.createdAt).toLocaleDateString()}</span>
294
+ <span className="h-1 w-1 rounded-full bg-slate-850"></span>
295
+ <span>{ws.repos.length} repos</span>
296
+ </div>
297
+ </div>
298
+ <div className="flex gap-2">
299
+ <button
300
+ className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-emerald-650 hover:bg-emerald-600 text-white rounded-xl text-xs font-semibold transition-all cursor-pointer shadow-md shadow-emerald-600/10 disabled:opacity-40 disabled:cursor-not-allowed"
301
+ onClick={() => handleResumeSession(ws)}
302
+ disabled={resumingWs === ws.branchName}
303
+ >
304
+ <Play size={12} className={resumingWs === ws.branchName ? 'animate-spin' : ''} />
305
+ {resumingWs === ws.branchName ? 'Resuming...' : 'Resume Session'}
306
+ </button>
307
+ <button
308
+ className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-slate-800 hover:border-slate-700 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-350 hover:text-white"
309
+ onClick={() => handleCopyPrompt(ws)}
310
+ >
311
+ <Sparkles size={12} className="text-cyan-400" /> Copy Prompt
312
+ </button>
313
+ <button
314
+ className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-slate-800 hover:border-slate-700 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-350 hover:text-white"
315
+ onClick={() => handleOpenInEditor(ws.workspacePath)}
316
+ >
317
+ <ExternalLink size={12} /> Open
318
+ </button>
319
+ <button
320
+ className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl text-xs font-semibold transition-all cursor-pointer shadow-md shadow-cyan-650/10"
321
+ onClick={() => {
322
+ if (isExpanded) {
323
+ setActiveWsId(null);
324
+ } else {
325
+ setActiveWsId(ws.branchName);
326
+ }
327
+ }}
328
+ >
329
+ {isExpanded ? 'Hide Runner' : 'Orchestrate'}
330
+ </button>
331
+ <button
332
+ className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2.5 bg-red-950/40 border border-red-900/60 hover:bg-red-900/60 hover:border-red-800 rounded-xl text-xs font-semibold text-red-200 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed animate-fade-in"
333
+ onClick={() => handleDeleteWorkspace(ws.branchName)}
334
+ disabled={deleteWsLoading === ws.branchName}
335
+ >
336
+ <Trash2 size={12} className={deleteWsLoading === ws.branchName ? 'animate-spin' : ''} />
337
+ {deleteWsLoading === ws.branchName ? 'Deleting...' : 'Delete'}
338
+ </button>
339
+ </div>
340
+ </div>
341
+
342
+ <div className="text-xs text-slate-350 bg-slate-950/60 border-l-2 border-cyan-500/80 rounded-r-xl px-5 py-3.5 mb-5 font-normal leading-relaxed relative overflow-hidden before:absolute before:inset-y-0 before:right-0 before:w-12 before:bg-gradient-to-r before:from-transparent before:to-cyan-500/2">
343
+ {renderFormattedDescription(ws.description)}
344
+ </div>
345
+
346
+ <div className="flex items-center gap-3 mb-4">
347
+ <div className="flex flex-wrap gap-2">
348
+ {ws.repos.map((repoPath) => (
349
+ <span key={repoPath} className="text-[10px] px-2.5 py-1.5 bg-slate-950/60 border border-slate-850 text-slate-300 rounded-lg font-semibold select-all">
350
+ {repoPath.split(/[\\/]/).pop()}
351
+ </span>
352
+ ))}
353
+ </div>
354
+
355
+ {repos.filter((r) => !ws.repos.includes(r.path)).length > 0 && (
356
+ <div className="flex items-center gap-1.5 ml-auto">
357
+ <select
358
+ id={`add-repo-select-feature-${ws.branchName}`}
359
+ className="bg-slate-905 border border-slate-800 text-slate-300 rounded-lg text-[10px] px-3 py-1.5 font-semibold outline-none focus:border-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-all hover:border-slate-700"
360
+ defaultValue=""
361
+ onChange={async (e) => {
362
+ const val = e.target.value;
363
+ if (val) {
364
+ if (window.confirm(`Are you sure you want to add repository "${val.split(/[\\/]/).pop()}" to this workspace?\nThis will create a new git worktree and re-run analysis.`)) {
365
+ await handleAddRepo(ws.branchName, val);
366
+ }
367
+ e.target.value = "";
368
+ }
369
+ }}
370
+ disabled={addRepoLoading}
371
+ >
372
+ <option value="" disabled>+ Add Repository</option>
373
+ {repos
374
+ .filter((r) => !ws.repos.includes(r.path))
375
+ .map((r) => (
376
+ <option key={r.path} value={r.path}>{r.name}</option>
377
+ ))
378
+ }
379
+ </select>
380
+ </div>
381
+ )}
382
+ </div>
383
+
384
+ {/* Expansion: Process management console */}
385
+ {isExpanded && (
386
+ <div className="mt-6 pt-6 border-t border-slate-850/80">
387
+ {/* Sub-tab Navigation */}
388
+ <div className="flex flex-wrap items-center gap-1.5 p-1 bg-slate-950/60 rounded-xl border border-slate-900/60 mb-6 w-max max-w-full overflow-x-auto">
389
+ <button
390
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
391
+ subTab === 'overview'
392
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
393
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
394
+ }`}
395
+ onClick={() => setSubTab('overview')}
396
+ >
397
+ Harness & Capabilities
398
+ </button>
399
+ <button
400
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
401
+ subTab === 'sessions'
402
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
403
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
404
+ }`}
405
+ onClick={() => setSubTab('sessions')}
406
+ >
407
+ AI Session History
408
+ </button>
409
+ <button
410
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
411
+ subTab === 'services'
412
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
413
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
414
+ }`}
415
+ onClick={() => setSubTab('services')}
416
+ >
417
+ Orchestrated Services
418
+ </button>
419
+ <button
420
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
421
+ subTab === 'changes'
422
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
423
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
424
+ }`}
425
+ onClick={() => setSubTab('changes')}
426
+ >
427
+ Active Changes (AI Diffs)
428
+ </button>
429
+ <button
430
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
431
+ subTab === 'knowledge'
432
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
433
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
434
+ }`}
435
+ onClick={() => setSubTab('knowledge')}
436
+ >
437
+ Knowledge Base
438
+ </button>
439
+ <button
440
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer select-none ${
441
+ subTab === 'plan'
442
+ ? 'bg-cyan-950/30 border border-cyan-900/60 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
443
+ : 'text-slate-500 hover:text-slate-300 hover:bg-slate-900/40 border border-transparent'
444
+ }`}
445
+ onClick={() => setSubTab('plan')}
446
+ >
447
+ Implementation Plan
448
+ </button>
449
+ </div>
450
+
451
+ {subTab === 'overview' && (
452
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-gray-300 animate-fade-in">
453
+ {/* Section 1: AI Assistant Harnesses */}
454
+ <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
455
+ <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
456
+ <Cpu size={18} className="text-cyan-400" />
457
+ <h4 className="font-bold text-white">AI Assistant Harnesses</h4>
458
+ </div>
459
+ <p className="text-xs text-gray-400 mb-4">
460
+ These coding assistants are configured to run contextually inside this workspace. Select an assistant to see its setup.
461
+ </p>
462
+ <div className="flex flex-col gap-3">
463
+ {['antigravity', 'claude', 'copilot', 'cursor', 'codex'].map((name) => {
464
+ const isConfigured = ws.assistants.includes(name as any);
465
+ const isDetected = aiAssistants.find((a) => a.name === name)?.detected;
466
+ let displayName = name.charAt(0).toUpperCase() + name.slice(1);
467
+ if (name === 'antigravity') displayName = 'Antigravity (Ollama/Gemini)';
468
+ else if (name === 'codex') displayName = 'OpenAI Codex';
469
+ else if (name === 'copilot') displayName = 'GitHub Copilot';
470
+ else if (name === 'claude') displayName = 'Claude Code';
471
+
472
+ let resumeCmd = '';
473
+ if (name === 'antigravity') resumeCmd = 'agy --continue';
474
+ else if (name === 'claude') resumeCmd = 'claude --resume';
475
+ else if (name === 'codex') resumeCmd = 'codex resume';
476
+ else if (name === 'copilot') resumeCmd = 'copilot --resume';
477
+
478
+ return (
479
+ <div key={name} className="flex flex-col p-3 bg-gray-900/40 border border-gray-800/60 rounded-lg">
480
+ <div className="flex items-center justify-between gap-2">
481
+ <div className="flex items-center gap-2">
482
+ <span className={`h-2 w-2 rounded-full ${isConfigured ? 'bg-cyan-500 shadow-sm' : 'bg-gray-700'}`}></span>
483
+ <span className="font-semibold text-xs text-gray-200">{displayName}</span>
484
+ </div>
485
+ <div className="flex items-center gap-1.5 text-[10px] font-medium">
486
+ {isConfigured && <span className="text-cyan-400 bg-cyan-950/20 px-2 py-0.5 rounded border border-cyan-900/40 text-[9px] font-bold uppercase tracking-wider">Active</span>}
487
+ {isDetected ? (
488
+ <span className="text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 text-[9px] font-bold uppercase tracking-wider">Installed</span>
489
+ ) : (
490
+ <span className="text-gray-500 bg-gray-950 px-2 py-0.5 rounded border border-gray-800/60 text-[9px] font-bold uppercase tracking-wider">Not Probed</span>
491
+ )}
492
+ </div>
493
+ </div>
494
+ {isConfigured && resumeCmd && (
495
+ <div className="mt-2.5 flex items-center justify-between gap-2 bg-gray-950/80 px-2.5 py-1.5 rounded border border-gray-800/60 font-mono text-[10px]">
496
+ <span className="text-cyan-400 truncate">{resumeCmd}</span>
497
+ <button
498
+ onClick={() => handleCopy(resumeCmd)}
499
+ className="text-gray-400 hover:text-white transition-colors cursor-pointer"
500
+ title="Copy Resume Command"
501
+ >
502
+ {copiedText === resumeCmd ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}
503
+ </button>
504
+ </div>
505
+ )}
506
+ </div>
507
+ );
508
+ })}
509
+ </div>
510
+ </div>
511
+
512
+ {/* Section 2: Model Context Protocol (MCP) */}
513
+ <div className="flex flex-col gap-6">
514
+ <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
515
+ <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
516
+ <Layers size={18} className="text-cyan-400" />
517
+ <h4 className="font-bold text-white">Local MCP Server (Tools)</h4>
518
+ </div>
519
+ <p className="text-xs text-gray-400 mb-4">
520
+ NexusFlow runs a Model Context Protocol (MCP) server that exposes local workspace-scoped tools to your AI agents.
521
+ </p>
522
+ <div className="flex flex-col gap-3.5">
523
+ <div className="flex flex-col gap-1">
524
+ <div className="flex items-center gap-2 text-xs font-semibold text-cyan-455">
525
+ <code>search_workspace</code>
526
+ </div>
527
+ <span className="text-[11px] text-gray-400">
528
+ Extremely fast query/regex search across all microservices and repos in the workspace.
529
+ </span>
530
+ </div>
531
+ <div className="flex flex-col gap-1 border-t border-gray-800/40 pt-2.5">
532
+ <div className="flex items-center gap-2 text-xs font-semibold text-cyan-455">
533
+ <code>get_service_logs</code>
534
+ </div>
535
+ <span className="text-[11px] text-gray-400">
536
+ Fetch and parse recent stdout/stderr output from any background service running in the workspace.
537
+ </span>
538
+ </div>
539
+ <div className="flex flex-col gap-1 border-t border-gray-800/40 pt-2.5">
540
+ <div className="flex items-center gap-2 text-xs font-semibold text-cyan-455">
541
+ <code>delegate_to_local_agent</code>
542
+ </div>
543
+ <span className="text-[11px] text-gray-400">
544
+ Delegate sub-tasks or log analysis to a lightweight LLM running locally to optimize remote token usage.
545
+ </span>
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ {/* CLI shortcuts */}
551
+ <div className="bg-gray-950/20 border border-gray-800/80 rounded-xl p-5">
552
+ <div className="flex items-center gap-2 mb-4 border-b border-gray-800/80 pb-3">
553
+ <Terminal size={18} className="text-cyan-400" />
554
+ <h4 className="font-bold text-white">Workspace CLI Shortcuts</h4>
555
+ </div>
556
+ <div className="flex flex-col gap-2.5">
557
+ {[
558
+ { cmd: 'nexusflow start', desc: 'Spin up detected backend/frontend services' },
559
+ { cmd: 'nexusflow sync', desc: 'Rebase worktrees with upstream defaults' },
560
+ { cmd: 'nexusflow commit -m "update"', desc: 'Stage and commit changes across repos' },
561
+ { cmd: 'nexusflow diff', desc: 'Unified codebase diff across repos' },
562
+ { cmd: 'nexusflow handoff', desc: 'Generate resume context file' }
563
+ ].map(({ cmd, desc }) => (
564
+ <div key={cmd} className="flex flex-col gap-1">
565
+ <div className="flex items-center justify-between gap-2">
566
+ <code className="text-xs text-cyan-400">{cmd}</code>
567
+ <button
568
+ onClick={() => handleCopy(cmd)}
569
+ className="text-gray-400 hover:text-white transition-colors cursor-pointer"
570
+ title="Copy Command"
571
+ >
572
+ {copiedText === cmd ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}
573
+ </button>
574
+ </div>
575
+ <span className="text-[11px] text-gray-550">{desc}</span>
576
+ </div>
577
+ ))}
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ )}
583
+
584
+ {subTab === 'sessions' && (
585
+ <SessionHistory
586
+ ws={ws}
587
+ sessions={sessions}
588
+ sessionsLoading={sessionsLoading}
589
+ activeSession={activeSession}
590
+ transcript={transcript}
591
+ transcriptLoading={transcriptLoading}
592
+ workspaces={workspaces}
593
+ setActiveSession={setActiveSession}
594
+ setTranscript={setTranscript}
595
+ fetchSessionTranscript={fetchSessionTranscript}
596
+ handleResumeSession={handleResumeSession}
597
+ />
598
+ )}
599
+
600
+ {subTab === 'services' && (
601
+ <ServiceConsole
602
+ ws={ws}
603
+ services={services}
604
+ runningServices={runningServices}
605
+ selectedLogService={selectedLogService}
606
+ serviceLogs={serviceLogs}
607
+ logsEndRef={logsEndRef}
608
+ setSelectedLogService={setSelectedLogService}
609
+ handleStartServices={handleStartServices}
610
+ handleStopServices={handleStopServices}
611
+ orchTools={orchTools}
612
+ servicesLoading={servicesLoading}
613
+ />
614
+ )}
615
+
616
+ {subTab === 'changes' && (
617
+ <ChangesViewer
618
+ ws={ws}
619
+ gitChanges={gitChanges}
620
+ gitChangesLoading={gitChangesLoading}
621
+ syncLoading={syncLoading}
622
+ syncResults={syncResults}
623
+ commitMessage={commitMessage}
624
+ showCommitModal={showCommitModal}
625
+ commitLoading={commitLoading}
626
+ commitResults={commitResults}
627
+ setSyncResults={setSyncResults}
628
+ setCommitResults={setCommitResults}
629
+ setCommitMessage={setCommitMessage}
630
+ setShowCommitModal={setShowCommitModal}
631
+ fetchGitChanges={fetchGitChanges}
632
+ handleSyncAll={handleSyncAll}
633
+ handleCommitAll={handleCommitAll}
634
+ />
635
+ )}
636
+
637
+ {subTab === 'knowledge' && (
638
+ <KnowledgeBase
639
+ ws={ws}
640
+ knowledgeContent={knowledgeContent}
641
+ knowledgeLoading={knowledgeLoading}
642
+ isEditingKnowledge={isEditingKnowledge}
643
+ editedKnowledge={editedKnowledge}
644
+ saveKnowledgeLoading={saveKnowledgeLoading}
645
+ setEditedKnowledge={setEditedKnowledge}
646
+ setIsEditingKnowledge={setIsEditingKnowledge}
647
+ handleSaveKnowledge={handleSaveKnowledge}
648
+ />
649
+ )}
650
+
651
+ {subTab === 'plan' && (
652
+ <ImplementationPlan
653
+ planContent={planContent}
654
+ planLoading={planLoading}
655
+ />
656
+ )}
657
+ </div>
658
+ )}
659
+ </div>
660
+ );
661
+ })}
662
+ </div>
663
+ )}
664
+ </div>
665
+ );
666
+ };