@mrpatronz/nexusflow 0.2.18 → 1.3.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 (236) hide show
  1. package/README.md +27 -3
  2. package/dist/analyzers/index.d.ts +23 -0
  3. package/dist/analyzers/index.d.ts.map +1 -1
  4. package/dist/analyzers/index.js +62 -0
  5. package/dist/analyzers/index.js.map +1 -1
  6. package/dist/commands/commands.test.js +20 -6
  7. package/dist/commands/commands.test.js.map +1 -1
  8. package/dist/commands/commit.d.ts +2 -0
  9. package/dist/commands/commit.d.ts.map +1 -1
  10. package/dist/commands/commit.js +27 -5
  11. package/dist/commands/commit.js.map +1 -1
  12. package/dist/commands/diff.d.ts +9 -2
  13. package/dist/commands/diff.d.ts.map +1 -1
  14. package/dist/commands/diff.js +36 -12
  15. package/dist/commands/diff.js.map +1 -1
  16. package/dist/commands/doctor.js +2 -2
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/handoff.js +2 -2
  19. package/dist/commands/handoff.js.map +1 -1
  20. package/dist/commands/refresh.d.ts +4 -2
  21. package/dist/commands/refresh.d.ts.map +1 -1
  22. package/dist/commands/refresh.js +20 -36
  23. package/dist/commands/refresh.js.map +1 -1
  24. package/dist/commands/schedule.d.ts +35 -0
  25. package/dist/commands/schedule.d.ts.map +1 -0
  26. package/dist/commands/schedule.js +151 -0
  27. package/dist/commands/schedule.js.map +1 -0
  28. package/dist/commands/sync.d.ts.map +1 -1
  29. package/dist/commands/sync.js +58 -50
  30. package/dist/commands/sync.js.map +1 -1
  31. package/dist/core/analysis-cache.d.ts +58 -0
  32. package/dist/core/analysis-cache.d.ts.map +1 -0
  33. package/dist/core/analysis-cache.js +116 -0
  34. package/dist/core/analysis-cache.js.map +1 -0
  35. package/dist/core/analysis-cache.test.d.ts +2 -0
  36. package/dist/core/analysis-cache.test.d.ts.map +1 -0
  37. package/dist/core/analysis-cache.test.js +108 -0
  38. package/dist/core/analysis-cache.test.js.map +1 -0
  39. package/dist/core/refresh.d.ts +41 -0
  40. package/dist/core/refresh.d.ts.map +1 -0
  41. package/dist/core/refresh.js +60 -0
  42. package/dist/core/refresh.js.map +1 -0
  43. package/dist/core/scheduler.d.ts +117 -0
  44. package/dist/core/scheduler.d.ts.map +1 -0
  45. package/dist/core/scheduler.js +247 -0
  46. package/dist/core/scheduler.js.map +1 -0
  47. package/dist/core/scheduler.test.d.ts +2 -0
  48. package/dist/core/scheduler.test.d.ts.map +1 -0
  49. package/dist/core/scheduler.test.js +111 -0
  50. package/dist/core/scheduler.test.js.map +1 -0
  51. package/dist/core/sync.d.ts +50 -0
  52. package/dist/core/sync.d.ts.map +1 -0
  53. package/dist/core/sync.js +92 -0
  54. package/dist/core/sync.js.map +1 -0
  55. package/dist/core/workspace-state.d.ts +51 -0
  56. package/dist/core/workspace-state.d.ts.map +1 -0
  57. package/dist/core/workspace-state.js +108 -0
  58. package/dist/core/workspace-state.js.map +1 -0
  59. package/dist/core/workspace-state.test.d.ts +2 -0
  60. package/dist/core/workspace-state.test.d.ts.map +1 -0
  61. package/dist/core/workspace-state.test.js +84 -0
  62. package/dist/core/workspace-state.test.js.map +1 -0
  63. package/dist/generators/index.d.ts +7 -1
  64. package/dist/generators/index.d.ts.map +1 -1
  65. package/dist/generators/index.js +14 -1
  66. package/dist/generators/index.js.map +1 -1
  67. package/dist/gui/assets/index-Bdbld6yY.css +1 -0
  68. package/dist/gui/assets/index-D8iJPvnk.js +26 -0
  69. package/dist/gui/index.html +2 -2
  70. package/dist/index.js +107 -4
  71. package/dist/index.js.map +1 -1
  72. package/dist/mcp/server.d.ts.map +1 -1
  73. package/dist/mcp/server.js +42 -0
  74. package/dist/mcp/server.js.map +1 -1
  75. package/dist/server.d.ts.map +1 -1
  76. package/dist/server.js +175 -13
  77. package/dist/server.js.map +1 -1
  78. package/dist/server.test.js +66 -0
  79. package/dist/server.test.js.map +1 -1
  80. package/dist/types.d.ts +31 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/utils/multi-git.d.ts +54 -8
  83. package/dist/utils/multi-git.d.ts.map +1 -1
  84. package/dist/utils/multi-git.js +129 -21
  85. package/dist/utils/multi-git.js.map +1 -1
  86. package/dist/utils/multi-git.test.d.ts +2 -0
  87. package/dist/utils/multi-git.test.d.ts.map +1 -0
  88. package/dist/utils/multi-git.test.js +125 -0
  89. package/dist/utils/multi-git.test.js.map +1 -0
  90. package/package.json +13 -1
  91. package/.agents/skills/ccc/SKILL.md +0 -71
  92. package/.agents/skills/ccc/references/management.md +0 -110
  93. package/.agents/skills/ccc/references/settings.md +0 -126
  94. package/.github/dependabot.yml +0 -52
  95. package/.github/workflows/ci.yml +0 -44
  96. package/.github/workflows/dependabot-merge.yml +0 -53
  97. package/.github/workflows/release-desktop.yml +0 -67
  98. package/.github/workflows/release-npm.yml +0 -65
  99. package/.github/workflows/release-vscode.yml +0 -61
  100. package/.vscode/launch.json +0 -17
  101. package/.vscode/tasks.json +0 -17
  102. package/GETTING_STARTED.md +0 -114
  103. package/desktop/build-installer.js +0 -228
  104. package/desktop/build.js +0 -118
  105. package/desktop/installer/nexusflow.iss +0 -35
  106. package/desktop/neutralino.config.json +0 -39
  107. package/desktop/package-lock.json +0 -1060
  108. package/desktop/package.json +0 -14
  109. package/dist/gui/assets/index-CB-jWded.css +0 -1
  110. package/dist/gui/assets/index-DDXpZZu3.js +0 -25
  111. package/extension/media/icon.svg +0 -1
  112. package/extension/package-lock.json +0 -6044
  113. package/extension/package.json +0 -117
  114. package/extension/src/extension.ts +0 -653
  115. package/extension/tsconfig.json +0 -21
  116. package/gui/README.md +0 -73
  117. package/gui/e2e/wizard.spec.ts +0 -346
  118. package/gui/eslint.config.js +0 -26
  119. package/gui/index.html +0 -17
  120. package/gui/package-lock.json +0 -3079
  121. package/gui/package.json +0 -36
  122. package/gui/playwright.config.ts +0 -41
  123. package/gui/public/app_logo.png +0 -0
  124. package/gui/public/favicon.svg +0 -1
  125. package/gui/public/icons.svg +0 -24
  126. package/gui/src/App.css +0 -1
  127. package/gui/src/App.tsx +0 -3632
  128. package/gui/src/assets/hero.png +0 -0
  129. package/gui/src/assets/react.svg +0 -1
  130. package/gui/src/assets/vite.svg +0 -1
  131. package/gui/src/features/changes/ChangesViewer.tsx +0 -388
  132. package/gui/src/features/knowledge/KnowledgeBase.tsx +0 -84
  133. package/gui/src/features/onboarding/OnboardingWizard.tsx +0 -230
  134. package/gui/src/features/plan/ImplementationPlan.tsx +0 -32
  135. package/gui/src/features/services/ServiceConsole.tsx +0 -250
  136. package/gui/src/features/sessions/SessionHistory.tsx +0 -195
  137. package/gui/src/features/workspace/WorkspaceList.tsx +0 -666
  138. package/gui/src/index.css +0 -152
  139. package/gui/src/main.tsx +0 -94
  140. package/gui/src/types.ts +0 -62
  141. package/gui/tsconfig.app.json +0 -25
  142. package/gui/tsconfig.json +0 -7
  143. package/gui/tsconfig.node.json +0 -24
  144. package/gui/vite.config.ts +0 -12
  145. package/resources/workflows/plan-implement-review.md +0 -8
  146. package/resources/workflows/research-verify.md +0 -6
  147. package/resources/workflows/solo-developer.md +0 -3
  148. package/scripts/simulate-workspaces.ts +0 -55
  149. package/skills-lock.json +0 -11
  150. package/src/analyzers/detect-apis.ts +0 -290
  151. package/src/analyzers/detect-deps.ts +0 -315
  152. package/src/analyzers/detect-existing.ts +0 -74
  153. package/src/analyzers/detect-ports.ts +0 -110
  154. package/src/analyzers/index.ts +0 -97
  155. package/src/analyzers/messaging-analyzer.ts +0 -254
  156. package/src/analyzers/readme-summarizer.ts +0 -102
  157. package/src/analyzers/run-analyzer.ts +0 -269
  158. package/src/analyzers/tech-stack.ts +0 -283
  159. package/src/commands/adapter.ts +0 -348
  160. package/src/commands/add-repo.ts +0 -156
  161. package/src/commands/commands.test.ts +0 -156
  162. package/src/commands/commit.ts +0 -131
  163. package/src/commands/config.ts +0 -52
  164. package/src/commands/create.ts +0 -204
  165. package/src/commands/desktop.ts +0 -128
  166. package/src/commands/diff.ts +0 -125
  167. package/src/commands/doctor.ts +0 -370
  168. package/src/commands/handoff.ts +0 -266
  169. package/src/commands/init.ts +0 -134
  170. package/src/commands/list.ts +0 -46
  171. package/src/commands/logs.ts +0 -63
  172. package/src/commands/mcp.ts +0 -94
  173. package/src/commands/open.ts +0 -129
  174. package/src/commands/refresh.ts +0 -137
  175. package/src/commands/remove.ts +0 -98
  176. package/src/commands/start.ts +0 -117
  177. package/src/commands/status.ts +0 -54
  178. package/src/commands/stop.ts +0 -55
  179. package/src/commands/sync.ts +0 -147
  180. package/src/commands/tui.ts +0 -484
  181. package/src/commands/ui.ts +0 -111
  182. package/src/core/adapters/local-storage.ts +0 -66
  183. package/src/core/adapters/obsidian-storage.ts +0 -151
  184. package/src/core/adapters/registry.ts +0 -44
  185. package/src/core/adapters/storage.test.ts +0 -174
  186. package/src/core/adapters/vault-storage.ts +0 -86
  187. package/src/core/config.test.ts +0 -96
  188. package/src/core/config.ts +0 -140
  189. package/src/core/graph.ts +0 -344
  190. package/src/core/plugins/index.ts +0 -17
  191. package/src/core/plugins/loader.ts +0 -36
  192. package/src/core/ports/storage.ts +0 -71
  193. package/src/core/scanner.test.ts +0 -61
  194. package/src/core/scanner.ts +0 -91
  195. package/src/core/storage.ts +0 -37
  196. package/src/core/workspace.ts +0 -487
  197. package/src/core/worktree.ts +0 -120
  198. package/src/generators/antigravity.ts +0 -32
  199. package/src/generators/base.ts +0 -243
  200. package/src/generators/claude.ts +0 -34
  201. package/src/generators/codex.ts +0 -48
  202. package/src/generators/copilot.ts +0 -56
  203. package/src/generators/cursor.ts +0 -44
  204. package/src/generators/diff-context.ts +0 -75
  205. package/src/generators/index.ts +0 -322
  206. package/src/generators/map-generator.test.ts +0 -159
  207. package/src/generators/map-generator.ts +0 -539
  208. package/src/generators/plan-generator.ts +0 -483
  209. package/src/generators/skills-generator.ts +0 -259
  210. package/src/index.ts +0 -539
  211. package/src/mcp/server.ts +0 -294
  212. package/src/orchestration/detect.ts +0 -313
  213. package/src/orchestration/index.ts +0 -7
  214. package/src/orchestration/runner.ts +0 -283
  215. package/src/server.test.ts +0 -618
  216. package/src/server.ts +0 -1314
  217. package/src/types.ts +0 -415
  218. package/src/utils/detect-ai.test.ts +0 -44
  219. package/src/utils/detect-ai.ts +0 -84
  220. package/src/utils/detect-editors.test.ts +0 -48
  221. package/src/utils/detect-editors.ts +0 -57
  222. package/src/utils/git.test.ts +0 -74
  223. package/src/utils/git.ts +0 -117
  224. package/src/utils/local-ai.test.ts +0 -130
  225. package/src/utils/local-ai.ts +0 -116
  226. package/src/utils/multi-git.ts +0 -313
  227. package/src/utils/prompts.ts +0 -209
  228. package/src/utils/session-finder.ts +0 -483
  229. package/src/utils/system-scanner.test.ts +0 -89
  230. package/src/utils/system-scanner.ts +0 -96
  231. package/src/utils/update-check.ts +0 -286
  232. package/src/utils/workflow-advisor.ts +0 -118
  233. package/src/utils/workflows.test.ts +0 -114
  234. package/src/utils/workflows.ts +0 -178
  235. package/tsconfig.json +0 -19
  236. package/vitest.config.ts +0 -14
package/gui/src/App.tsx DELETED
@@ -1,3632 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import {
3
- PlusCircle,
4
- FolderGit2,
5
- Settings as SettingsIcon,
6
- Terminal,
7
- Play,
8
- Check,
9
- AlertTriangle,
10
- AlertCircle,
11
- FolderOpen,
12
- ArrowRight,
13
- ArrowLeft,
14
- RefreshCw,
15
- Search,
16
- Sparkles,
17
- Copy,
18
- X,
19
- Cpu,
20
- Workflow,
21
- Trash2,
22
- CheckCircle,
23
- } from 'lucide-react';
24
- import './App.css';
25
- import { WorkspaceList } from './features/workspace/WorkspaceList.js';
26
-
27
-
28
- // Types matched with src/types.ts
29
- interface LocalLlmConfig {
30
- enabled: boolean;
31
- provider: 'ollama' | 'openai-compatible';
32
- endpoint: string;
33
- model: string;
34
- apiKey?: string;
35
- }
36
-
37
- interface NexusFlowConfig {
38
- version: string;
39
- devDir: string;
40
- workspacesDir: string;
41
- defaultAssistant: string | null;
42
- defaultEditor?: string | null;
43
- scanDepth: number;
44
- localLlm?: LocalLlmConfig;
45
- storageProvider?: string;
46
- adapterConfig?: Record<string, Record<string, any>>;
47
- plugins?: string[];
48
- }
49
-
50
- interface AdapterConfigField {
51
- key: string;
52
- label: string;
53
- type: 'string' | 'boolean' | 'number' | 'path';
54
- required?: boolean;
55
- default?: any;
56
- description?: string;
57
- }
58
-
59
- interface StorageAdapterMeta {
60
- name: string;
61
- displayName: string;
62
- description: string;
63
- configFields: AdapterConfigField[];
64
- }
65
-
66
- interface DetectedAI {
67
- name: string;
68
- displayName: string;
69
- detected: boolean;
70
- command?: string;
71
- }
72
-
73
- interface DetectedEditor {
74
- name: string;
75
- command: string;
76
- detected: boolean;
77
- }
78
-
79
- interface RepoInfo {
80
- name: string;
81
- path: string;
82
- defaultBranch: string;
83
- }
84
-
85
- interface Feature {
86
- id: string;
87
- branchName: string;
88
- description: string;
89
- repos: string[];
90
- assistants: string[];
91
- workspacePath: string;
92
- createdAt: string;
93
- }
94
-
95
- interface ServiceConfig {
96
- name: string;
97
- cwd: string;
98
- command: string;
99
- args: string[];
100
- port?: number;
101
- source: string;
102
- }
103
-
104
- interface OrchestrationDetection {
105
- tool: string;
106
- configPath: string;
107
- startCommand: string;
108
- stopCommand: string;
109
- }
110
-
111
- interface RunningService {
112
- name: string;
113
- pid: number;
114
- config: ServiceConfig;
115
- startedAt: string;
116
- }
117
-
118
- const API_BASE = (import.meta.env.DEV || (typeof window !== 'undefined' && (window as any).Neutralino)) ? 'http://localhost:3000' : '';
119
- const isVsCode = new URLSearchParams(window.location.search).get('env') === 'vscode';
120
- let toastIdCounter = 0;
121
-
122
- export default function App() {
123
- const [view, setView] = useState<'guide' | 'create' | 'workspaces' | 'settings' | 'workflows'>('guide');
124
-
125
- // Toast State
126
- interface Toast {
127
- id: string;
128
- message: string;
129
- type: 'success' | 'error' | 'info';
130
- duration?: number;
131
- }
132
- const [toasts, setToasts] = useState<Toast[]>([]);
133
- const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info', duration = 5000) => {
134
- const id = `toast-${++toastIdCounter}`;
135
- setToasts((prev) => [...prev, { id, message, type, duration }]);
136
- setTimeout(() => {
137
- setToasts((prev) => prev.filter((t) => t.id !== id));
138
- }, duration);
139
- };
140
-
141
- // App Config
142
- const [config, setConfig] = useState<NexusFlowConfig | null>(null);
143
- const [configLoading, setConfigLoading] = useState(true);
144
- const [configExists, setConfigExists] = useState<boolean>(true);
145
- const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
146
- const [adapters, setAdapters] = useState<StorageAdapterMeta[]>([]);
147
-
148
- // Update Check State
149
- const [updateStatus, setUpdateStatus] = useState<{
150
- currentVersion: string;
151
- latestVersion: string;
152
- updateAvailable: boolean;
153
- downloadUrl?: string | null;
154
- releaseNotes?: string;
155
- } | null>(null);
156
- const [appVersion, setAppVersion] = useState('0.1.5');
157
- const [defaultPaths, setDefaultPaths] = useState<{ devDir: string; workspacesDir: string } | null>(null);
158
-
159
- // Repos & Tools
160
- const [repos, setRepos] = useState<RepoInfo[]>([]);
161
- const [reposLoading, setReposLoading] = useState(false);
162
- const [repoSearch, setRepoSearch] = useState('');
163
- const [aiAssistants, setAiAssistants] = useState<DetectedAI[]>([]);
164
- const [editors, setEditors] = useState<DetectedEditor[]>([]);
165
-
166
- // Wizard State
167
- const [activeStep, setActiveStep] = useState(0);
168
- const [branchName, setBranchName] = useState('');
169
- const [description, setDescription] = useState('');
170
- const [selectedRepos, setSelectedRepos] = useState<RepoInfo[]>([]);
171
- const [selectedAI, setSelectedAI] = useState<string[]>([]);
172
- const [selectedEditor, setSelectedEditor] = useState<DetectedEditor | null>(null);
173
- const [localLlmEnabled, setLocalLlmEnabled] = useState(false);
174
- const [creating, setCreating] = useState(false);
175
- const [createdWorkspace, setCreatedWorkspace] = useState<{ path: string } | null>(null);
176
- const [creationError, setCreationError] = useState<string | null>(null);
177
- const [creationSteps, setCreationSteps] = useState<any[]>([
178
- { id: 'worktrees', name: 'Create Git Worktrees', status: 'pending', message: 'Waiting...' },
179
- { id: 'analysis', name: 'Analyze Repositories', status: 'pending', message: 'Waiting...' },
180
- { id: 'context', name: 'Generate AI Context Files', status: 'pending', message: 'Waiting...' },
181
- ]);
182
-
183
- const [toolsStatus, setToolsStatus] = useState<any[]>([]);
184
- const [toolsLoading, setToolsLoading] = useState(false);
185
- const [updatingToolId, setUpdatingToolId] = useState<string | null>(null);
186
-
187
- // App Autoupdate State
188
- const [updatingApp, setUpdatingApp] = useState(false);
189
- const [updateStep, setUpdateStep] = useState<'idle' | 'downloading' | 'applying' | 'error'>('idle');
190
-
191
- // Workflow Strategy State
192
- const [workflowTemplates, setWorkflowTemplates] = useState<any[]>([]);
193
- const [selectedWorkflowId, setSelectedWorkflowId] = useState<string>('plan-implement-review');
194
- const [customTeamworkInstructions, setCustomTeamworkInstructions] = useState<string>('');
195
- const [suggestingWorkflow, setSuggestingWorkflow] = useState(false);
196
- const [suggestedDifficulty, setSuggestedDifficulty] = useState<'simple' | 'moderate' | 'complex' | null>(null);
197
- const [suggestedRationale, setSuggestedRationale] = useState('');
198
-
199
- // Workflow Strategy Management State
200
- const [selectedMgtTemplateId, setSelectedMgtTemplateId] = useState<string | null>(null);
201
- const [isEditingTemplate, setIsEditingTemplate] = useState(false);
202
- const [mgtTemplateName, setMgtTemplateName] = useState('');
203
- const [mgtTemplateContent, setMgtTemplateContent] = useState('');
204
- const [analysisResult, setAnalysisResult] = useState<string | null>(null);
205
- const [analyzingTemplate, setAnalyzingTemplate] = useState(false);
206
- const [savingTemplate, setSavingTemplate] = useState(false);
207
- const [deletingTemplate, setDeletingTemplate] = useState(false);
208
- const [selectedInspectAssistant, setSelectedInspectAssistant] = useState<string>('antigravity');
209
- const [suggestedImprovement, setSuggestedImprovement] = useState<string | null>(null);
210
- const [mgtAnalysisComment, setMgtAnalysisComment] = useState('');
211
-
212
- // Resumption Commands State
213
- const [testCommand, setTestCommand] = useState('npm run test');
214
- const [mockCommand, setMockCommand] = useState('');
215
- const [startCommand, setStartCommand] = useState('');
216
- const [resumingWs, setResumingWs] = useState<string | null>(null);
217
-
218
- // Suggest defaults based on selected repos
219
- useEffect(() => {
220
- if (selectedRepos.length === 0) return;
221
- const isDotNet = selectedRepos.some(r => r.name.toLowerCase().includes('dotnet') || r.name.toLowerCase().includes('csharp') || r.name.toLowerCase().includes('microsoft'));
222
- const isPython = selectedRepos.some(r => r.name.toLowerCase().includes('django') || r.name.toLowerCase().includes('flask') || r.name.toLowerCase().includes('fastapi') || r.name.toLowerCase().includes('python'));
223
-
224
- if (isDotNet) {
225
- setTestCommand('dotnet test');
226
- } else if (isPython) {
227
- setTestCommand('pytest');
228
- } else {
229
- setTestCommand('npm run test');
230
- }
231
- }, [selectedRepos]);
232
-
233
- // Workspaces List
234
- const [workspaces, setWorkspaces] = useState<Feature[]>([]);
235
- const [workspacesLoading, setWorkspacesLoading] = useState(false);
236
-
237
- // Active Workspace Services / Orchestration Details
238
- const [activeWsId, setActiveWsId] = useState<string | null>(null);
239
- const [services, setServices] = useState<ServiceConfig[]>([]);
240
- const [orchTools, setOrchTools] = useState<OrchestrationDetection[]>([]);
241
- const [runningServices, setRunningServices] = useState<RunningService[]>([]);
242
- const [servicesLoading, setServicesLoading] = useState(false);
243
- const [subTab, setSubTab] = useState<'overview' | 'services' | 'changes' | 'sessions' | 'knowledge' | 'plan'>('overview');
244
- const [sessions, setSessions] = useState<any[]>([]);
245
- const [sessionsLoading, setSessionsLoading] = useState(false);
246
- const [activeSession, setActiveSession] = useState<any | null>(null);
247
- const [transcript, setTranscript] = useState<any[]>([]);
248
- const [transcriptLoading, setTranscriptLoading] = useState(false);
249
- const [gitChanges, setGitChanges] = useState<any[]>([]);
250
- const [gitChangesLoading, setGitChangesLoading] = useState(false);
251
- const [knowledgeContent, setKnowledgeContent] = useState<string>('');
252
- const [knowledgeLoading, setKnowledgeLoading] = useState<boolean>(false);
253
- const [isEditingKnowledge, setIsEditingKnowledge] = useState<boolean>(false);
254
- const [editedKnowledge, setEditedKnowledge] = useState<string>('');
255
- const [saveKnowledgeLoading, setSaveKnowledgeLoading] = useState<boolean>(false);
256
- const [planContent, setPlanContent] = useState<string>('');
257
- const [planLoading, setPlanLoading] = useState<boolean>(false);
258
- const [syncLoading, setSyncLoading] = useState<boolean>(false);
259
- const [syncResults, setSyncResults] = useState<any[] | null>(null);
260
- const [commitMessage, setCommitMessage] = useState<string>('');
261
- const [showCommitModal, setShowCommitModal] = useState<boolean>(false);
262
- const [commitLoading, setCommitLoading] = useState<boolean>(false);
263
- const [commitResults, setCommitResults] = useState<any[] | null>(null);
264
- const [deleteWsLoading, setDeleteWsLoading] = useState<string | null>(null);
265
- const [addRepoLoading, setAddRepoLoading] = useState<boolean>(false);
266
-
267
-
268
- // Log Viewer
269
- const [selectedLogService, setSelectedLogService] = useState<string | null>(null);
270
- const [serviceLogs, setServiceLogs] = useState<string>('');
271
- const logsEndRef = useRef<HTMLDivElement | null>(null);
272
-
273
- // Local LLM states
274
- const [recommendation, setRecommendation] = useState<{ totalRamGb: number; gpuName: string; recommendedModel: string } | null>(null);
275
- const [testStatus, setTestStatus] = useState<{ success: boolean; message: string } | null>(null);
276
- const [testingLlm, setTestingLlm] = useState(false);
277
-
278
- const isSettingsFormValid = (() => {
279
- if (!config) return false;
280
- if (!config.devDir || config.devDir.trim() === '' || !config.workspacesDir || config.workspacesDir.trim() === '') return false;
281
- if (config.localLlm?.enabled) {
282
- const endpoint = config.localLlm.endpoint?.trim() || '';
283
- if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) {
284
- return false;
285
- }
286
- }
287
- return true;
288
- })();
289
-
290
- // Effects moved below helper functions to resolve lexical declaration order
291
-
292
- // ─── API Fetches ────────────────────────────────────────────────────────
293
-
294
- const fetchUpdateStatus = async () => {
295
- try {
296
- const res = await fetch(`${API_BASE}/api/update-status`);
297
- if (res.ok) {
298
- const data = await res.json();
299
- setUpdateStatus(data);
300
- if (data.currentVersion) {
301
- setAppVersion(data.currentVersion);
302
- }
303
- }
304
- } catch (e) {
305
- console.error('Failed to fetch update status:', e);
306
- }
307
- };
308
-
309
- const fetchToolsStatus = async (force = false) => {
310
- setToolsLoading(true);
311
- try {
312
- const res = await fetch(`${API_BASE}/api/updates/tools${force ? '?force=true' : ''}`);
313
- if (res.ok) {
314
- const data = await res.json();
315
- setToolsStatus(data);
316
- }
317
- } catch (e) {
318
- console.error('Failed to fetch tools status:', e);
319
- } finally {
320
- setToolsLoading(false);
321
- }
322
- };
323
-
324
- const handleUpdateTool = async (toolId: string) => {
325
- setUpdatingToolId(toolId);
326
- try {
327
- const res = await fetch(`${API_BASE}/api/updates/install`, {
328
- method: 'POST',
329
- headers: { 'Content-Type': 'application/json' },
330
- body: JSON.stringify({ toolId }),
331
- });
332
- const data = await res.json();
333
- if (res.ok && data.success) {
334
- showToast(`${toolId} updated successfully!\n\nOutput:\n${data.output}`, 'success');
335
- fetchToolsStatus(true);
336
- fetchUpdateStatus();
337
- } else {
338
- showToast(`Error: ${data.error || 'Failed to update tool'}`, 'error');
339
- }
340
- } catch (e) {
341
- console.error(e);
342
- showToast('Network error when updating tool.', 'error');
343
- } finally {
344
- setUpdatingToolId(null);
345
- }
346
- };
347
-
348
- const handleAutoUpdate = async () => {
349
- if (!updateStatus || !updateStatus.downloadUrl) {
350
- showToast('No update download URL found.', 'error');
351
- return;
352
- }
353
-
354
- setUpdatingApp(true);
355
- setUpdateStep('downloading');
356
-
357
- try {
358
- // 1. Download the installer
359
- const downloadRes = await fetch(`${API_BASE}/api/updates/download`, {
360
- method: 'POST',
361
- headers: { 'Content-Type': 'application/json' },
362
- body: JSON.stringify({ downloadUrl: updateStatus.downloadUrl }),
363
- });
364
- const downloadData = await downloadRes.json();
365
-
366
- if (!downloadRes.ok || !downloadData.success) {
367
- throw new Error(downloadData.error || 'Failed to download installer binary');
368
- }
369
-
370
- // 2. Apply the installer
371
- setUpdateStep('applying');
372
- const applyRes = await fetch(`${API_BASE}/api/updates/apply`, {
373
- method: 'POST',
374
- });
375
- const applyData = await applyRes.json();
376
-
377
- if (!applyRes.ok || !applyData.success) {
378
- throw new Error(applyData.error || 'Failed to trigger application update');
379
- }
380
-
381
- showToast('Update downloaded! App is restarting...', 'success');
382
-
383
- // 3. Exit Neutralino client window to unlock files on disk
384
- if (typeof window !== 'undefined' && (window as any).Neutralino) {
385
- setTimeout(() => {
386
- (window as any).Neutralino.app.exit();
387
- }, 800);
388
- }
389
- } catch (err: any) {
390
- console.error('Update failed:', err);
391
- setUpdateStep('error');
392
- showToast(`Update Failed: ${err.message || 'Unknown error'}`, 'error');
393
- setUpdatingApp(false);
394
- }
395
- };
396
-
397
- const fetchLlmRecommendation = async () => {
398
- try {
399
- const res = await fetch(`${API_BASE}/api/local-llm/recommend`);
400
- if (res.ok) {
401
- const data = await res.json();
402
- setRecommendation(data);
403
- }
404
- } catch (e) {
405
- console.error('Error fetching LLM recommendation:', e);
406
- }
407
- };
408
-
409
- const testLlmConnection = async () => {
410
- if (!config?.localLlm) return;
411
- setTestingLlm(true);
412
- setTestStatus(null);
413
- try {
414
- const res = await fetch(`${API_BASE}/api/local-llm/test`, {
415
- method: 'POST',
416
- headers: { 'Content-Type': 'application/json' },
417
- body: JSON.stringify(config.localLlm),
418
- });
419
- const data = await res.json();
420
- if (data.success) {
421
- setTestStatus({ success: true, message: data.message });
422
- } else {
423
- setTestStatus({ success: false, message: data.error || 'Connection failed.' });
424
- }
425
- } catch (e: any) {
426
- setTestStatus({ success: false, message: e.message || 'Network error.' });
427
- } finally {
428
- setTestingLlm(false);
429
- }
430
- };
431
-
432
- const fetchAdapters = async () => {
433
- try {
434
- const res = await fetch(`${API_BASE}/api/adapters`);
435
- const data = await res.json();
436
- if (data.adapters) {
437
- setAdapters(data.adapters);
438
- }
439
- } catch (e) {
440
- console.error('Failed to fetch adapters:', e);
441
- }
442
- };
443
-
444
- const fetchConfig = async () => {
445
- fetchAdapters();
446
- try {
447
- const res = await fetch(`${API_BASE}/api/config`);
448
- const data = await res.json();
449
- setConfig(data.config);
450
- setConfigExists(data.exists);
451
- setLocalLlmEnabled(data.config?.localLlm?.enabled || false);
452
-
453
- fetchEditors(data.config?.defaultEditor);
454
-
455
- if (data.exists && data.config.devDir) {
456
- fetchRepos();
457
- } else {
458
- setDefaultPaths({
459
- devDir: data.config.devDir || '',
460
- workspacesDir: data.config.workspacesDir || '',
461
- });
462
- setConfig({
463
- version: '1.0.0',
464
- devDir: '',
465
- workspacesDir: '',
466
- defaultAssistant: null,
467
- scanDepth: 2,
468
- });
469
- }
470
- } catch (e) {
471
- console.error(e);
472
- } finally {
473
- setConfigLoading(false);
474
- }
475
- };
476
-
477
- const saveAppConfig = async (newConfig: NexusFlowConfig) => {
478
- try {
479
- const res = await fetch(`${API_BASE}/api/config`, {
480
- method: 'POST',
481
- headers: { 'Content-Type': 'application/json' },
482
- body: JSON.stringify(newConfig),
483
- });
484
- if (res.ok) {
485
- setSaveStatus('success');
486
- setConfig(newConfig);
487
- setConfigExists(true);
488
- setLocalLlmEnabled(newConfig.localLlm?.enabled || false);
489
- fetchRepos();
490
- fetchWorkspaces();
491
- } else {
492
- setSaveStatus('error');
493
- }
494
- } catch {
495
- setSaveStatus('error');
496
- }
497
- setTimeout(() => setSaveStatus(null), 3000);
498
- };
499
-
500
- const fetchRepos = async () => {
501
- setReposLoading(true);
502
- try {
503
- const res = await fetch(`${API_BASE}/api/repos`);
504
- const data = await res.json();
505
- setRepos(Array.isArray(data) ? data : []);
506
- } catch (e) {
507
- console.error(e);
508
- } finally {
509
- setReposLoading(false);
510
- }
511
- };
512
-
513
- const fetchAIAssistants = async () => {
514
- try {
515
- const res = await fetch(`${API_BASE}/api/ai-detect`);
516
- const data = await res.json();
517
- setAiAssistants(data);
518
- const firstHarness = data.find((ai: any) => ai.detected && ai.command);
519
- if (firstHarness) {
520
- setSelectedInspectAssistant(firstHarness.name);
521
- }
522
- setSelectedAI(data.filter((ai: DetectedAI) => ai.detected).map((ai: DetectedAI) => ai.name));
523
- } catch (e) {
524
- console.error(e);
525
- }
526
- };
527
-
528
- const fetchEditors = async (savedDefaultEditorCode?: string | null) => {
529
- try {
530
- const res = await fetch(`${API_BASE}/api/editor-detect`);
531
- const data = await res.json();
532
- setEditors(data);
533
-
534
- let initialEditor = null;
535
- if (savedDefaultEditorCode) {
536
- initialEditor = data.find((ed: DetectedEditor) => ed.command === savedDefaultEditorCode);
537
- }
538
- if (!initialEditor) {
539
- initialEditor = data.find((ed: DetectedEditor) => ed.detected);
540
- }
541
- if (initialEditor) setSelectedEditor(initialEditor);
542
- } catch (e) {
543
- console.error(e);
544
- }
545
- };
546
-
547
- const fetchWorkspaces = async () => {
548
- setWorkspacesLoading(true);
549
- try {
550
- const res = await fetch(`${API_BASE}/api/workspaces`);
551
- const data = await res.json();
552
- setWorkspaces(Array.isArray(data) ? data : []);
553
- } catch (e) {
554
- console.error(e);
555
- } finally {
556
- setWorkspacesLoading(false);
557
- }
558
- };
559
-
560
- const fetchWorkflowTemplates = async () => {
561
- try {
562
- const res = await fetch(`${API_BASE}/api/workflows/templates`);
563
- if (res.ok) {
564
- const data = await res.json();
565
- setWorkflowTemplates(data.templates || []);
566
-
567
- // Default to plan-implement-review template
568
- const pir = data.templates?.find((t: any) => t.id === 'plan-implement-review');
569
- if (pir) {
570
- setCustomTeamworkInstructions(pir.content);
571
- }
572
- }
573
- } catch (e) {
574
- console.error('Failed to fetch workflow templates:', e);
575
- }
576
- };
577
-
578
- const handleSaveTemplate = async () => {
579
- if (!mgtTemplateName.trim() || !mgtTemplateContent.trim()) {
580
- showToast('Name and content are required.', 'error');
581
- return;
582
- }
583
- setSavingTemplate(true);
584
- try {
585
- const res = await fetch(`${API_BASE}/api/workflows/templates`, {
586
- method: 'POST',
587
- headers: { 'Content-Type': 'application/json' },
588
- body: JSON.stringify({
589
- id: selectedMgtTemplateId,
590
- name: mgtTemplateName,
591
- content: mgtTemplateContent,
592
- }),
593
- });
594
- if (res.ok) {
595
- const data = await res.json();
596
- showToast('Strategy template saved successfully!', 'success');
597
- await fetchWorkflowTemplates();
598
- setSelectedMgtTemplateId(data.template.id);
599
- setIsEditingTemplate(false);
600
- setAnalysisResult(null);
601
- } else {
602
- const err = await res.json();
603
- showToast(err.error || 'Failed to save template', 'error');
604
- }
605
- } catch (e: any) {
606
- showToast(e.message || 'Error saving template', 'error');
607
- } finally {
608
- setSavingTemplate(false);
609
- }
610
- };
611
-
612
- const handleDeleteTemplate = async (id: string) => {
613
- if (!window.confirm('Are you sure you want to delete this custom strategy template?')) {
614
- return;
615
- }
616
- setDeletingTemplate(true);
617
- try {
618
- const res = await fetch(`${API_BASE}/api/workflows/templates/${encodeURIComponent(id)}`, {
619
- method: 'DELETE',
620
- });
621
- if (res.ok) {
622
- showToast('Strategy template deleted successfully!', 'success');
623
- await fetchWorkflowTemplates();
624
- setSelectedMgtTemplateId(null);
625
- setAnalysisResult(null);
626
- } else {
627
- const err = await res.json();
628
- showToast(err.error || 'Failed to delete template', 'error');
629
- }
630
- } catch (e: any) {
631
- showToast(e.message || 'Error deleting template', 'error');
632
- } finally {
633
- setDeletingTemplate(false);
634
- }
635
- };
636
-
637
- const handleAnalyzeTemplate = async (id: string, content: string, assistant: string) => {
638
- setAnalyzingTemplate(true);
639
- setAnalysisResult(null);
640
- setSuggestedImprovement(null);
641
- try {
642
- const res = await fetch(`${API_BASE}/api/workflows/templates/${encodeURIComponent(id)}/analyze`, {
643
- method: 'POST',
644
- headers: { 'Content-Type': 'application/json' },
645
- body: JSON.stringify({ content, assistant, comment: mgtAnalysisComment }),
646
- });
647
- if (res.ok) {
648
- const data = await res.json();
649
- setAnalysisResult(data.analysis);
650
- setSuggestedImprovement(data.suggestedImprovement || null);
651
- showToast('Strategy analysis completed successfully!', 'success');
652
- } else {
653
- const err = await res.json();
654
- showToast(err.error || 'Failed to analyze template using the selected assistant harness.', 'error');
655
- }
656
- } catch (e: any) {
657
- showToast(e.message || 'Error analyzing template', 'error');
658
- } finally {
659
- setAnalyzingTemplate(false);
660
- }
661
- };
662
-
663
- const fetchWorkspaceServices = async (wsId: string, silent = false) => {
664
- if (!silent) setServicesLoading(true);
665
- try {
666
- const encodedId = encodeURIComponent(wsId);
667
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/services`);
668
- const data = await res.json();
669
- setServices(data.services || []);
670
- setOrchTools(data.orchestrationTools || []);
671
- setRunningServices(data.runningState || []);
672
-
673
- if (data.services.length > 0 && !selectedLogService) {
674
- setSelectedLogService(data.services[0].name);
675
- }
676
- } catch (e) {
677
- console.error(e);
678
- } finally {
679
- if (!silent) setServicesLoading(false);
680
- }
681
- };
682
-
683
- const fetchLogs = async (wsId: string, serviceName: string) => {
684
- try {
685
- const encodedId = encodeURIComponent(wsId);
686
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/services/logs/${serviceName}`);
687
- const data = await res.json();
688
- setServiceLogs(data.logs || '');
689
- } catch (e) {
690
- console.error(e);
691
- }
692
- };
693
-
694
- const fetchGitChanges = async (wsId: string) => {
695
- setGitChangesLoading(true);
696
- try {
697
- const encodedId = encodeURIComponent(wsId);
698
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/changes`);
699
- const data = await res.json();
700
- setGitChanges(data.changes || []);
701
- } catch (e) {
702
- console.error(e);
703
- } finally {
704
- setGitChangesLoading(false);
705
- }
706
- };
707
-
708
- const fetchWorkspaceSessions = async (wsId: string) => {
709
- setSessionsLoading(true);
710
- try {
711
- const encodedId = encodeURIComponent(wsId);
712
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/sessions`);
713
- const data = await res.json();
714
- setSessions(data.sessions || []);
715
- } catch (e) {
716
- console.error(e);
717
- } finally {
718
- setSessionsLoading(false);
719
- }
720
- };
721
-
722
- const fetchSessionTranscript = async (assistant: string, sessionId: string) => {
723
- setTranscriptLoading(true);
724
- try {
725
- const res = await fetch(`${API_BASE}/api/session/${assistant}/${sessionId}/transcript`);
726
- const data = await res.json();
727
- setTranscript(data.messages || []);
728
- } catch (e) {
729
- console.error(e);
730
- } finally {
731
- setTranscriptLoading(false);
732
- }
733
- };
734
-
735
-
736
- const fetchKnowledge = async (wsId: string) => {
737
- setKnowledgeLoading(true);
738
- try {
739
- const encodedId = encodeURIComponent(wsId);
740
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/knowledge`);
741
- const data = await res.json();
742
- setKnowledgeContent(data.content || '');
743
- setEditedKnowledge(data.content || '');
744
- } catch (e) {
745
- console.error(e);
746
- } finally {
747
- setKnowledgeLoading(false);
748
- }
749
- };
750
-
751
- const handleSaveKnowledge = async (wsId: string) => {
752
- setSaveKnowledgeLoading(true);
753
- try {
754
- const encodedId = encodeURIComponent(wsId);
755
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/knowledge`, {
756
- method: 'PUT',
757
- headers: { 'Content-Type': 'application/json' },
758
- body: JSON.stringify({ content: editedKnowledge }),
759
- });
760
- if (res.ok) {
761
- setKnowledgeContent(editedKnowledge);
762
- setIsEditingKnowledge(false);
763
- }
764
- } catch (e) {
765
- console.error(e);
766
- } finally {
767
- setSaveKnowledgeLoading(false);
768
- }
769
- };
770
-
771
- const fetchPlan = async (wsId: string) => {
772
- setPlanLoading(true);
773
- try {
774
- const encodedId = encodeURIComponent(wsId);
775
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/plan`);
776
- const data = await res.json();
777
- setPlanContent(data.content || '');
778
- } catch (e) {
779
- console.error(e);
780
- } finally {
781
- setPlanLoading(false);
782
- }
783
- };
784
-
785
-
786
- const handleSyncAll = async (wsId: string) => {
787
- setSyncLoading(true);
788
- setSyncResults(null);
789
- try {
790
- const encodedId = encodeURIComponent(wsId);
791
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/sync`, { method: 'POST' });
792
- const data = await res.json();
793
- setSyncResults(data.results || []);
794
- fetchGitChanges(wsId);
795
- } catch (e) {
796
- console.error(e);
797
- } finally {
798
- setSyncLoading(false);
799
- }
800
- };
801
-
802
- const handleCommitAll = async (wsId: string) => {
803
- if (!commitMessage.trim()) return;
804
- setCommitLoading(true);
805
- setCommitResults(null);
806
- try {
807
- const encodedId = encodeURIComponent(wsId);
808
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/commit`, {
809
- method: 'POST',
810
- headers: { 'Content-Type': 'application/json' },
811
- body: JSON.stringify({ message: commitMessage }),
812
- });
813
- const data = await res.json();
814
- setCommitResults(data.results || []);
815
- setCommitMessage('');
816
- setShowCommitModal(false);
817
- fetchGitChanges(wsId);
818
- } catch (e) {
819
- console.error(e);
820
- } finally {
821
- setCommitLoading(false);
822
- }
823
- };
824
-
825
-
826
- // ─── Actions ────────────────────────────────────────────────────────────
827
-
828
- const handleToggleRepo = (repo: RepoInfo) => {
829
- if (selectedRepos.some((r) => r.path === repo.path)) {
830
- setSelectedRepos(selectedRepos.filter((r) => r.path !== repo.path));
831
- } else {
832
- setSelectedRepos([...selectedRepos, repo]);
833
- }
834
- };
835
-
836
- const handleToggleAI = (aiName: string) => {
837
- if (selectedAI.includes(aiName)) {
838
- setSelectedAI(selectedAI.filter((x) => x !== aiName));
839
- } else {
840
- setSelectedAI([...selectedAI, aiName]);
841
- }
842
- };
843
-
844
- const handleSuggestWorkflow = async () => {
845
- if (!description) {
846
- showToast('Please enter a description for the workspace details first.', 'info');
847
- return;
848
- }
849
- setSuggestingWorkflow(true);
850
- setSuggestedDifficulty(null);
851
- setSuggestedRationale('');
852
- try {
853
- const res = await fetch(`${API_BASE}/api/workspace/suggest-workflow`, {
854
- method: 'POST',
855
- headers: { 'Content-Type': 'application/json' },
856
- body: JSON.stringify({
857
- description,
858
- repos: selectedRepos,
859
- }),
860
- });
861
- const data = await res.json();
862
- if (res.ok && data.success) {
863
- setSuggestedDifficulty(data.difficulty);
864
- setSuggestedRationale(data.rationale);
865
- setSelectedWorkflowId(data.suggestedWorkflowId);
866
- setCustomTeamworkInstructions(data.customInstructions);
867
- showToast(`Suggested strategy populated successfully: ${data.difficulty.toUpperCase()} difficulty.`, 'success');
868
- } else {
869
- showToast(data.error || 'Failed to auto-suggest strategy.', 'error');
870
- }
871
- } catch (e) {
872
- console.error(e);
873
- showToast('Network error while requesting strategy suggestion.', 'error');
874
- } finally {
875
- setSuggestingWorkflow(false);
876
- }
877
- };
878
-
879
- const handleCreateWorkspace = async () => {
880
- if (!branchName || selectedRepos.length === 0) return;
881
- setCreating(true);
882
- setCreationError(null);
883
-
884
- // Reset steps to pending
885
- setCreationSteps([
886
- { id: 'worktrees', name: 'Create Git Worktrees', status: 'pending', message: 'Waiting...' },
887
- { id: 'analysis', name: 'Analyze Repositories', status: 'pending', message: 'Waiting...' },
888
- { id: 'context', name: 'Generate AI Context Files', status: 'pending', message: 'Waiting...' },
889
- ]);
890
-
891
- try {
892
- const res = await fetch(`${API_BASE}/api/workspace`, {
893
- method: 'POST',
894
- headers: { 'Content-Type': 'application/json' },
895
- body: JSON.stringify({
896
- branchName,
897
- description,
898
- repos: selectedRepos,
899
- assistants: selectedAI,
900
- localLlmEnabled,
901
- teamworkInstructions: customTeamworkInstructions || undefined,
902
- resumption: {
903
- testCommand,
904
- mockCommand: mockCommand || undefined,
905
- startCommand: startCommand || undefined,
906
- },
907
- }),
908
- });
909
- const data = await res.json();
910
- if (res.ok && data.success) {
911
- const jobId = data.jobId;
912
-
913
- // Establish EventSource SSE connection
914
- const eventSource = new EventSource(`${API_BASE}/api/workspace/create-stream/${encodeURIComponent(jobId)}`);
915
-
916
- eventSource.addEventListener('progress', (e) => {
917
- try {
918
- const job = JSON.parse(e.data);
919
- if (job.steps) {
920
- setCreationSteps(job.steps);
921
- }
922
- if (job.status === 'completed') {
923
- eventSource.close();
924
- setCreatedWorkspace({ path: job.workspacePath });
925
- setActiveStep(4);
926
- setCreating(false);
927
- fetchWorkspaces();
928
-
929
- if (selectedEditor) {
930
- if (isVsCode) {
931
- window.parent.postMessage({ type: 'openWorkspaceFolder', workspacePath: job.workspacePath }, '*');
932
- } else {
933
- fetch(`${API_BASE}/api/open-editor`, {
934
- method: 'POST',
935
- headers: { 'Content-Type': 'application/json' },
936
- body: JSON.stringify({
937
- workspacePath: job.workspacePath,
938
- command: selectedEditor.command,
939
- }),
940
- }).catch(console.error);
941
- }
942
- }
943
- } else if (job.status === 'failed') {
944
- eventSource.close();
945
- setCreating(false);
946
- setCreationError(job.error || 'Failed to create workspace');
947
- }
948
- } catch (err) {
949
- console.error('Failed to parse SSE data:', err);
950
- }
951
- });
952
-
953
- eventSource.onerror = (err) => {
954
- console.error('SSE Error:', err);
955
- eventSource.close();
956
- setCreating(false);
957
- setCreationError('Connection lost while building workspace.');
958
- };
959
- } else {
960
- setCreating(false);
961
- setCreationError(data.error || 'Failed to initialize workspace build');
962
- }
963
- } catch (e) {
964
- console.error(e);
965
- setCreating(false);
966
- setCreationError('Network error when creating workspace.');
967
- }
968
- };
969
-
970
- const handleStartServices = async (wsId: string) => {
971
- try {
972
- const encodedId = encodeURIComponent(wsId);
973
- await fetch(`${API_BASE}/api/workspace/${encodedId}/services/start`, {
974
- method: 'POST',
975
- headers: { 'Content-Type': 'application/json' },
976
- body: JSON.stringify({ services }),
977
- });
978
- fetchWorkspaceServices(wsId, true);
979
- } catch (e) {
980
- console.error(e);
981
- }
982
- };
983
-
984
- const handleStopServices = async (wsId: string) => {
985
- try {
986
- const encodedId = encodeURIComponent(wsId);
987
- await fetch(`${API_BASE}/api/workspace/${encodedId}/services/stop`, {
988
- method: 'POST',
989
- });
990
- fetchWorkspaceServices(wsId, true);
991
- setServiceLogs('Services stopped.');
992
- } catch (e) {
993
- console.error(e);
994
- }
995
- };
996
-
997
- const handleResumeSession = async (ws: Feature, sessionId?: string, assistant?: string) => {
998
- setResumingWs(ws.branchName);
999
- try {
1000
- const encodedId = encodeURIComponent(ws.branchName);
1001
- const editor = editors.find((e) => e.detected) || editors[0];
1002
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/resume`, {
1003
- method: 'POST',
1004
- headers: { 'Content-Type': 'application/json' },
1005
- body: JSON.stringify({
1006
- command: editor?.command,
1007
- sessionId,
1008
- assistant,
1009
- }),
1010
- });
1011
- const data = await res.json();
1012
- if (res.ok && data.success) {
1013
- if (isVsCode) {
1014
- window.parent.postMessage({
1015
- type: 'executeTerminalCommand',
1016
- command: data.resumeCommand,
1017
- cwd: ws.workspacePath
1018
- }, '*');
1019
- setActiveWsId(ws.branchName);
1020
- } else {
1021
- navigator.clipboard.writeText(data.resumeCommand);
1022
- showToast(`Session Resumed!\n\n1. Editor launched.\n2. Command "${data.resumeCommand}" copied to clipboard! Paste it into your terminal inside the workspace to continue.`, 'success');
1023
- setActiveWsId(ws.branchName);
1024
- }
1025
- } else {
1026
- showToast(`Error: ${data.error || 'Failed to resume session'}`, 'error');
1027
- }
1028
- } catch (e) {
1029
- console.error(e);
1030
- showToast('Network error when resuming session.', 'error');
1031
- } finally {
1032
- setResumingWs(null);
1033
- }
1034
- };
1035
-
1036
- const handleOpenInEditor = async (workspacePath: string) => {
1037
- if (isVsCode) {
1038
- window.parent.postMessage({ type: 'openWorkspaceFolder', workspacePath }, '*');
1039
- return;
1040
- }
1041
- if (config?.defaultEditor === 'none') {
1042
- showToast('Your preferred editor is set to "None" (skip opening). You can change this in Settings.', 'info');
1043
- return;
1044
- }
1045
- let editor = null;
1046
- if (config?.defaultEditor) {
1047
- editor = editors.find((e) => e.command === config.defaultEditor);
1048
- }
1049
- if (!editor) {
1050
- editor = editors.find((e) => e.detected) || editors[0];
1051
- }
1052
- if (!editor) {
1053
- showToast('No detected editors available.', 'error');
1054
- return;
1055
- }
1056
- try {
1057
- await fetch(`${API_BASE}/api/open-editor`, {
1058
- method: 'POST',
1059
- headers: { 'Content-Type': 'application/json' },
1060
- body: JSON.stringify({
1061
- workspacePath,
1062
- command: editor.command,
1063
- }),
1064
- });
1065
- } catch (e) {
1066
- console.error(e);
1067
- }
1068
- };
1069
-
1070
- const executeTerminal = (command: string) => {
1071
- if (!activeWsId) return;
1072
- const ws = workspaces.find(w => w.branchName === activeWsId);
1073
- if (ws) {
1074
- window.parent.postMessage({
1075
- type: 'executeTerminalCommand',
1076
- command,
1077
- cwd: ws.workspacePath
1078
- }, '*');
1079
- }
1080
- };
1081
-
1082
-
1083
-
1084
- // Initial loads
1085
- useEffect(() => {
1086
- fetchConfig();
1087
- fetchAIAssistants();
1088
- fetchWorkspaces();
1089
- fetchUpdateStatus();
1090
- fetchWorkflowTemplates();
1091
-
1092
- // Support auto-loading workspace from VS Code or URL params
1093
- const queryParams = new URLSearchParams(window.location.search);
1094
- const queryWsId = queryParams.get('workspaceId');
1095
- if (queryWsId) {
1096
- setActiveWsId(queryWsId);
1097
- setView('workspaces');
1098
- }
1099
- }, []);
1100
-
1101
- // Load tool updates status and LLM recommendations when settings view is open
1102
- useEffect(() => {
1103
- if (view === 'settings') {
1104
- fetchToolsStatus();
1105
- fetchLlmRecommendation();
1106
- }
1107
- }, [view]);
1108
-
1109
- // Poll logs and services status when active workspace is open
1110
- useEffect(() => {
1111
- let interval: any = null;
1112
- if (activeWsId) {
1113
- fetchWorkspaceServices(activeWsId);
1114
- interval = setInterval(() => {
1115
- fetchWorkspaceServices(activeWsId, true);
1116
- }, 3000);
1117
- }
1118
- return () => {
1119
- if (interval) clearInterval(interval);
1120
- };
1121
- }, [activeWsId]);
1122
-
1123
- // Load git changes when tab switches to 'changes' or active workspace changes
1124
- useEffect(() => {
1125
- if (activeWsId && subTab === 'changes') {
1126
- fetchGitChanges(activeWsId);
1127
- }
1128
- }, [activeWsId, subTab]);
1129
-
1130
- // Load sessions when active workspace changes or subTab switches to 'sessions'
1131
- useEffect(() => {
1132
- if (activeWsId && subTab === 'sessions') {
1133
- fetchWorkspaceSessions(activeWsId);
1134
- }
1135
- }, [activeWsId, subTab]);
1136
-
1137
- // Load knowledge when subTab switches to 'knowledge' or active workspace changes
1138
- useEffect(() => {
1139
- if (activeWsId && subTab === 'knowledge') {
1140
- fetchKnowledge(activeWsId);
1141
- }
1142
- }, [activeWsId, subTab]);
1143
-
1144
- // Load plan when subTab switches to 'plan' or active workspace changes
1145
- useEffect(() => {
1146
- if (activeWsId && subTab === 'plan') {
1147
- fetchPlan(activeWsId);
1148
- }
1149
- }, [activeWsId, subTab]);
1150
-
1151
- // Poll logs for active service logs
1152
- useEffect(() => {
1153
- let interval: any = null;
1154
- if (activeWsId && selectedLogService) {
1155
- fetchLogs(activeWsId, selectedLogService);
1156
- interval = setInterval(() => {
1157
- fetchLogs(activeWsId, selectedLogService);
1158
- }, 2000);
1159
- }
1160
- return () => {
1161
- if (interval) clearInterval(interval);
1162
- };
1163
- }, [activeWsId, selectedLogService]);
1164
-
1165
- // Scroll to bottom of logs when log content changes
1166
- useEffect(() => {
1167
- if (logsEndRef.current) {
1168
- logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
1169
- }
1170
- }, [serviceLogs]);
1171
-
1172
- const handleCopyPrompt = (ws: Feature) => {
1173
- const repoNames = ws.repos.map((r) => r.split(/[\\/]/).pop()).join(', ');
1174
- const prompt = `You are an AI assistant helping with feature development.
1175
- We are working in a multi-repository workspace.
1176
-
1177
- Workspace Metadata:
1178
- - Feature Branch: ${ws.branchName}
1179
- - Purpose & Context: ${ws.description}
1180
- - Mapped Repositories: ${repoNames}
1181
-
1182
- Core Instructions:
1183
- 1. Always inspect and update "session.md" (session state handover memo) and "plan.md" (tasks checklist) at the workspace root as you progress.
1184
- 2. Read "WORKSPACE.md" at the root for a detailed index of repository relationships, tech stacks, and listening ports.
1185
- 3. Follow all project-specific rules in "CLAUDE.md", ".cursorrules", or "AGENTS.md" in sub-repositories.
1186
- `;
1187
- navigator.clipboard.writeText(prompt);
1188
- showToast('Universal AI briefing prompt copied to clipboard!', 'success');
1189
- };
1190
-
1191
- const handleDeleteWorkspace = async (wsName: string) => {
1192
- if (!window.confirm(`Are you sure you want to delete the workspace "${wsName}"?\nThis will force remove all git worktrees inside it and delete all files in the folder.`)) {
1193
- return;
1194
- }
1195
- setDeleteWsLoading(wsName);
1196
- try {
1197
- const encodedId = encodeURIComponent(wsName);
1198
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}`, {
1199
- method: 'DELETE',
1200
- });
1201
- const data = await res.json();
1202
- if (res.ok && data.success) {
1203
- if (activeWsId === wsName) {
1204
- setActiveWsId(null);
1205
- }
1206
- await fetchWorkspaces();
1207
- showToast(`Workspace ${wsName} successfully deleted.`, 'success');
1208
- } else {
1209
- showToast(`Failed to delete workspace: ${data.error || 'Unknown error'}`, 'error');
1210
- }
1211
- } catch (e) {
1212
- console.error(e);
1213
- showToast('Network error while deleting workspace.', 'error');
1214
- } finally {
1215
- setDeleteWsLoading(null);
1216
- }
1217
- };
1218
-
1219
- const handleAddRepo = async (wsName: string, repoPath: string) => {
1220
- setAddRepoLoading(true);
1221
- try {
1222
- const encodedId = encodeURIComponent(wsName);
1223
- const res = await fetch(`${API_BASE}/api/workspace/${encodedId}/repo`, {
1224
- method: 'POST',
1225
- headers: { 'Content-Type': 'application/json' },
1226
- body: JSON.stringify({ repoPath }),
1227
- });
1228
- const data = await res.json();
1229
- if (res.ok && data.success) {
1230
- await fetchWorkspaces();
1231
- if (activeWsId === wsName) {
1232
- fetchWorkspaceServices(wsName, true);
1233
- fetchGitChanges(wsName);
1234
- fetchWorkspaceSessions(wsName);
1235
- }
1236
- showToast('Repository successfully added and configurations updated.', 'success');
1237
- } else {
1238
- showToast(`Failed to add repository: ${data.error || 'Unknown error'}`, 'error');
1239
- }
1240
- } catch (e) {
1241
- console.error(e);
1242
- showToast('Network error while adding repository.', 'error');
1243
- } finally {
1244
- setAddRepoLoading(false);
1245
- }
1246
- };
1247
-
1248
- const filteredRepos = repos.filter((r) =>
1249
- r.name.toLowerCase().includes(repoSearch.toLowerCase())
1250
- );
1251
-
1252
- if (!configExists && config) {
1253
- const isFormValid = config.devDir.trim() !== '' && config.workspacesDir.trim() !== '';
1254
-
1255
- return (
1256
- <div className="flex min-h-screen bg-[#060813] text-gray-100 font-sans items-center justify-center p-6 bg-gradient-to-br from-[#0b0e24] via-[#060813] to-[#030409]">
1257
- {/* Onboarding Box */}
1258
- <div className="max-w-6xl w-full glass-card border border-slate-800/60 rounded-3xl p-10 shadow-2xl glass-card-glow grid grid-cols-1 lg:grid-cols-12 gap-12 animate-slide-in">
1259
-
1260
- {/* Left Column: Onboarding Guide & Concepts */}
1261
- <div className="lg:col-span-7 flex flex-col justify-between">
1262
- <div>
1263
- <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 text-xs font-semibold uppercase tracking-wider mb-5">
1264
- <Sparkles size={12} className="text-cyan-400 animate-pulse" /> Onboarding Guide
1265
- </div>
1266
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2 bg-gradient-to-r from-white via-slate-200 to-indigo-300 bg-clip-text text-transparent">
1267
- Welcome to NexusFlow
1268
- </h1>
1269
- <p className="text-sm text-slate-400 mb-8 max-w-xl leading-relaxed">
1270
- NexusFlow orchestrates multi-repository developer environments. It combines isolated Git worktrees, automatic code analyzer sweeps, and background process running into a single dashboard.
1271
- </p>
1272
-
1273
- {/* Onboarding Steps */}
1274
- <div className="space-y-6">
1275
- <div className="flex gap-4 group">
1276
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0 group-hover:bg-indigo-500/20 group-hover:text-indigo-300 transition-colors">
1277
- 1
1278
- </div>
1279
- <div>
1280
- <h3 className="text-sm font-semibold text-white group-hover:text-indigo-300 transition-colors">Configure Development Folders</h3>
1281
- <p className="text-xs text-slate-400 mt-1 leading-relaxed">
1282
- Specify your local code path and target workspaces path. For the first setup, these paths start empty so you can explicitly configure them.
1283
- </p>
1284
- </div>
1285
- </div>
1286
-
1287
- <div className="flex gap-4 group">
1288
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0 group-hover:bg-indigo-500/20 group-hover:text-indigo-300 transition-colors">
1289
- 2
1290
- </div>
1291
- <div>
1292
- <h3 className="text-sm font-semibold text-white group-hover:text-indigo-300 transition-colors">Build Isolated Branch Workspaces</h3>
1293
- <p className="text-xs text-slate-400 mt-1 leading-relaxed">
1294
- Choose repositories and input your feature branch. NexusFlow runs <code>git worktree</code> to checkout dependencies under a unified folder structure, leaving your primary repository directories clean.
1295
- </p>
1296
- </div>
1297
- </div>
1298
-
1299
- <div className="flex gap-4 group">
1300
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0 group-hover:bg-indigo-500/20 group-hover:text-indigo-300 transition-colors">
1301
- 3
1302
- </div>
1303
- <div>
1304
- <h3 className="text-sm font-semibold text-white group-hover:text-indigo-300 transition-colors">Align AI Coding Contexts</h3>
1305
- <p className="text-xs text-slate-400 mt-1 leading-relaxed">
1306
- NexusFlow automatically generates configuration files (<code>CLAUDE.md</code>, <code>.cursorrules</code>, <code>AGENTS.md</code>) that instruct the AI assistant to analyze project inter-dependencies, document its assumptions in <code>nexusflow-overview.md</code>, and highlight clarifying questions.
1307
- </p>
1308
- </div>
1309
- </div>
1310
-
1311
- <div className="flex gap-4 group">
1312
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0 group-hover:bg-indigo-500/20 group-hover:text-indigo-300 transition-colors">
1313
- 4
1314
- </div>
1315
- <div>
1316
- <h3 className="text-sm font-semibold text-white group-hover:text-indigo-300 transition-colors">Orchestrate Background Services</h3>
1317
- <p className="text-xs text-slate-400 mt-1 leading-relaxed">
1318
- Run APIs, database scripts, and frontend watch tasks concurrently from the web portal. Monitor real-time logs inside a unified terminal pane.
1319
- </p>
1320
- </div>
1321
- </div>
1322
- </div>
1323
- </div>
1324
-
1325
- {/* Comparison Dashboard */}
1326
- <div className="mt-8 pt-6 border-t border-slate-800/80">
1327
- <h4 className="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3">Industry Comparison</h4>
1328
- <div className="overflow-x-auto font-sans">
1329
- <table className="w-full text-[10px] text-slate-400 text-left border-collapse">
1330
- <thead>
1331
- <tr className="border-b border-slate-800 text-slate-500">
1332
- <th className="py-2 pr-4 font-semibold">Orchestrator</th>
1333
- <th className="py-2 px-4 font-semibold">Multi-Repo</th>
1334
- <th className="py-2 px-4 font-semibold">AI Rules Integration</th>
1335
- <th className="py-2 pl-4 font-semibold">Weight</th>
1336
- </tr>
1337
- </thead>
1338
- <tbody>
1339
- <tr className="border-b border-slate-850">
1340
- <td className="py-2 pr-4 font-semibold text-white">Docker Compose</td>
1341
- <td className="py-2 px-4">Yes (Containers only)</td>
1342
- <td className="py-2 px-4">No</td>
1343
- <td className="py-2 pl-4">Medium (VM overhead)</td>
1344
- </tr>
1345
- <tr className="border-b border-slate-850">
1346
- <td className="py-2 pr-4 font-semibold text-white">Lerna / Turborepo</td>
1347
- <td className="py-2 px-4">Monorepo only</td>
1348
- <td className="py-2 px-4">No</td>
1349
- <td className="py-2 pl-4">Light</td>
1350
- </tr>
1351
- <tr className="border-b border-slate-850">
1352
- <td className="py-2 pr-4 font-semibold text-white">DevPod / Gitpod</td>
1353
- <td className="py-2 px-4">Yes (Complex config)</td>
1354
- <td className="py-2 px-4">No</td>
1355
- <td className="py-2 pl-4">Heavy (Full virtual VM)</td>
1356
- </tr>
1357
- <tr>
1358
- <td className="py-2 pr-4 font-semibold text-indigo-400 text-glow-indigo">NexusFlow</td>
1359
- <td className="py-2 px-4 text-indigo-300 font-medium">Yes (Native Worktrees)</td>
1360
- <td className="py-2 px-4 text-indigo-300 font-medium">Yes (CLAUDE.md/MDC rules)</td>
1361
- <td className="py-2 pl-4 text-indigo-300 font-medium">Extremely Light (Native processes)</td>
1362
- </tr>
1363
- </tbody>
1364
- </table>
1365
- </div>
1366
- </div>
1367
- </div>
1368
-
1369
- {/* Right Column: Configuration Form */}
1370
- <div className="lg:col-span-5 flex flex-col justify-center">
1371
- <div className="bg-slate-950/50 border border-slate-850 rounded-2xl p-8 shadow-xl relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-b before:from-indigo-500/5 before:to-transparent before:pointer-events-none">
1372
- <h2 className="text-xl font-bold text-white mb-1 flex items-center gap-2">
1373
- <SettingsIcon className="text-indigo-400 animate-spin-slow" size={20} /> Initialize Config
1374
- </h2>
1375
- <p className="text-xs text-slate-500 mb-6">
1376
- Define the directories on your machine. The fields are empty so you can provide your paths.
1377
- </p>
1378
-
1379
- {/* Form Input fields */}
1380
- <div className="space-y-5">
1381
- <div>
1382
- <label className="block text-[10px] font-semibold text-slate-400 uppercase tracking-wider mb-2">Development Directory</label>
1383
- <input
1384
- type="text"
1385
- className="w-full bg-slate-950/60 border border-slate-800 focus:border-indigo-500/80 focus:ring-1 focus:ring-indigo-500/35 rounded-xl px-4 py-3 text-white placeholder-slate-600 transition-all outline-none text-xs shadow-inner"
1386
- placeholder="e.g. C:\Users\username\dev"
1387
- value={config.devDir}
1388
- onChange={(e) => setConfig({ ...config, devDir: e.target.value })}
1389
- />
1390
- {defaultPaths && (
1391
- <div className="text-[10px] text-slate-500 mt-1.5 flex justify-between items-center">
1392
- <span>Suggested: <code className="text-indigo-300">{defaultPaths.devDir}</code></span>
1393
- </div>
1394
- )}
1395
- </div>
1396
-
1397
- <div>
1398
- <label className="block text-[10px] font-semibold text-slate-400 uppercase tracking-wider mb-2">Workspaces Directory</label>
1399
- <input
1400
- type="text"
1401
- className="w-full bg-slate-950/60 border border-slate-800 focus:border-indigo-500/80 focus:ring-1 focus:ring-indigo-500/35 rounded-xl px-4 py-3 text-white placeholder-slate-600 transition-all outline-none text-xs shadow-inner"
1402
- placeholder="e.g. C:\Users\username\dev\workspaces"
1403
- value={config.workspacesDir}
1404
- onChange={(e) => setConfig({ ...config, workspacesDir: e.target.value })}
1405
- />
1406
- {defaultPaths && (
1407
- <div className="text-[10px] text-slate-500 mt-1.5 flex justify-between items-center">
1408
- <span>Suggested: <code className="text-indigo-300">{defaultPaths.workspacesDir}</code></span>
1409
- </div>
1410
- )}
1411
- </div>
1412
-
1413
- <div className="pt-2">
1414
- <label htmlFor="onboardingStorageProvider" className="text-xs font-semibold text-slate-200 block mb-1">
1415
- Storage Provider
1416
- </label>
1417
- <select
1418
- id="onboardingStorageProvider"
1419
- className="w-full text-xs bg-slate-950/60 border border-slate-800 rounded-lg p-2 text-slate-200 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/50"
1420
- value={config.storageProvider || 'local'}
1421
- onChange={(e) => {
1422
- const val = e.target.value;
1423
- const newConf = { ...config, storageProvider: val };
1424
- const selectedAdapter = adapters.find(a => a.name === val);
1425
- if (selectedAdapter?.configFields?.length) {
1426
- if (!newConf.adapterConfig) newConf.adapterConfig = {};
1427
- if (!newConf.adapterConfig[val]) newConf.adapterConfig[val] = {};
1428
- selectedAdapter.configFields.forEach(f => {
1429
- if (newConf.adapterConfig![val][f.key] === undefined && f.default !== undefined) {
1430
- newConf.adapterConfig![val][f.key] = f.default;
1431
- }
1432
- });
1433
- }
1434
- setConfig(newConf);
1435
- }}
1436
- >
1437
- {adapters.length > 0 ? (
1438
- adapters.map((a) => (
1439
- <option key={a.name} value={a.name}>
1440
- {a.displayName}
1441
- </option>
1442
- ))
1443
- ) : (
1444
- <>
1445
- <option value="local">Local Workspace (Folders)</option>
1446
- <option value="central-vault">Obsidian Central Vault</option>
1447
- </>
1448
- )}
1449
- </select>
1450
- <span className="text-[10px] text-slate-500 mt-1 block leading-normal">
1451
- {adapters.find(a => a.name === (config.storageProvider || 'local'))?.description ||
1452
- 'Choose where to store maps, plans, and knowledge files. Centralized vault keeps repositories 100% clean and allows Obsidian integration.'}
1453
- </span>
1454
- </div>
1455
-
1456
- {/* Form Buttons */}
1457
- <div className="flex gap-2 pt-2">
1458
- <button
1459
- type="button"
1460
- className="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-indigo-500/5 hover:border-indigo-500/30 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-300 hover:text-white"
1461
- onClick={() => {
1462
- if (defaultPaths) {
1463
- setConfig({
1464
- ...config,
1465
- devDir: defaultPaths.devDir,
1466
- workspacesDir: defaultPaths.workspacesDir,
1467
- });
1468
- }
1469
- }}
1470
- >
1471
- Suggest Defaults
1472
- </button>
1473
- <button
1474
- type="button"
1475
- className="px-3 py-2.5 bg-slate-900 border border-slate-800/80 hover:bg-rose-500/5 hover:border-rose-500/30 hover:text-rose-400 rounded-xl text-xs font-semibold transition-all cursor-pointer text-slate-400"
1476
- onClick={() => {
1477
- setConfig({
1478
- ...config,
1479
- devDir: '',
1480
- workspacesDir: '',
1481
- });
1482
- }}
1483
- >
1484
- Clear
1485
- </button>
1486
- </div>
1487
-
1488
- {/* Save Button */}
1489
- <div className="pt-4">
1490
- <button
1491
- type="button"
1492
- className="w-full inline-flex items-center justify-center gap-2 px-5 py-3.5 rounded-xl text-sm font-bold bg-gradient-to-r from-indigo-500 via-indigo-600 to-purple-650 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/20 hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:translate-y-0 transition-all cursor-pointer"
1493
- disabled={!isFormValid}
1494
- onClick={() => saveAppConfig(config)}
1495
- >
1496
- Save & Get Started <ArrowRight size={16} />
1497
- </button>
1498
- </div>
1499
- </div>
1500
- </div>
1501
- </div>
1502
- </div>
1503
- </div>
1504
- );
1505
- }
1506
-
1507
- if (isVsCode) {
1508
- return (
1509
- <div className="flex flex-col h-screen w-full bg-[#060813] text-[#d1d5db] font-mono text-[11px] overflow-hidden select-none border-t border-gray-800">
1510
- {/* Terminal Header */}
1511
- <div className="flex items-center justify-between px-3 py-2 bg-[#0b0f19] border-b border-gray-800 shrink-0 text-[10px]">
1512
- <div className="flex items-center gap-2">
1513
- <span className="font-bold text-indigo-400">NEXUSFLOW_SHELL</span>
1514
- <span className="text-gray-700">|</span>
1515
- <span className="text-gray-300">ws: {activeWsId || 'none'}</span>
1516
- </div>
1517
- <div className="flex items-center gap-2">
1518
- {activeWsId && (
1519
- <button
1520
- onClick={() => {
1521
- setActiveWsId(null);
1522
- setSelectedLogService(null);
1523
- setServiceLogs('');
1524
- }}
1525
- className="text-indigo-400 hover:text-indigo-350 hover:underline transition-colors cursor-pointer bg-transparent border-none p-0 outline-none"
1526
- >
1527
- [Change WS]
1528
- </button>
1529
- )}
1530
- <span className="text-[10px] text-gray-500">v{appVersion}</span>
1531
- </div>
1532
- </div>
1533
-
1534
- {!activeWsId ? (
1535
- /* CLI Workspace Selection Menu */
1536
- <div className="flex-1 flex flex-col items-center justify-center p-6 text-center overflow-y-auto">
1537
- <div className="text-indigo-400 font-bold mb-4 text-[12px] uppercase">
1538
- === Select Active Workspace ===
1539
- </div>
1540
- <div className="flex flex-col gap-2 w-full max-w-xs">
1541
- {workspaces.map((ws) => (
1542
- <button
1543
- key={ws.branchName}
1544
- onClick={() => {
1545
- setActiveWsId(ws.branchName);
1546
- fetchWorkspaceServices(ws.branchName);
1547
- }}
1548
- className="w-full text-left p-2.5 bg-gray-950 border border-gray-800 hover:border-indigo-500 rounded hover:bg-indigo-500/5 text-gray-300 hover:text-white transition-all text-[11px] cursor-pointer"
1549
- >
1550
- &gt; {ws.branchName}
1551
- </button>
1552
- ))}
1553
- {workspaces.length === 0 && (
1554
- <div className="text-gray-550 italic">No workspaces found. Initialize one via the CLI.</div>
1555
- )}
1556
- </div>
1557
- </div>
1558
- ) : (
1559
- /* Active Workspace Panel */
1560
- <div className="flex-1 flex flex-col min-h-0">
1561
- {/* Top Config / Control Panel */}
1562
- <div className="p-3 border-b border-gray-800 shrink-0 bg-gray-950/30">
1563
- {/* Service Control Buttons */}
1564
- <div className="flex gap-2 mb-3">
1565
- <button
1566
- onClick={() => handleStartServices(activeWsId)}
1567
- className="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded border border-emerald-500/20 bg-emerald-500/5 hover:bg-emerald-500/10 text-emerald-400 font-bold transition-all cursor-pointer text-[10px]"
1568
- >
1569
- <Play size={10} /> [START SERVICES]
1570
- </button>
1571
- <button
1572
- onClick={() => handleStopServices(activeWsId)}
1573
- className="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded border border-rose-500/20 bg-rose-500/5 hover:bg-rose-500/10 text-rose-400 font-bold transition-all cursor-pointer text-[10px]"
1574
- >
1575
- <X size={10} /> [STOP SERVICES]
1576
- </button>
1577
- </div>
1578
-
1579
- {/* Service List */}
1580
- <div className="mb-3">
1581
- <div className="text-[10px] text-gray-400 uppercase font-bold tracking-wider mb-1 px-1">
1582
- Background Services ({runningServices.filter(rs => rs.pid > 0).length}/{services.length})
1583
- </div>
1584
- {services.length === 0 ? (
1585
- <div className="text-gray-600 italic px-1 text-[10px]">No services detected in workspace.</div>
1586
- ) : (
1587
- <div className="flex flex-col gap-1 max-h-[120px] overflow-y-auto pr-1">
1588
- {services.map((service) => {
1589
- const isRunning = runningServices.some((rs) => rs.name === service.name && rs.pid > 0);
1590
- const isSelected = selectedLogService === service.name;
1591
- return (
1592
- <div
1593
- key={service.name}
1594
- onClick={() => setSelectedLogService(service.name)}
1595
- className={`flex items-center justify-between p-1.5 border rounded cursor-pointer transition-all ${
1596
- isSelected
1597
- ? 'bg-indigo-950/40 border-indigo-500/50 text-white'
1598
- : 'bg-gray-950/20 border-gray-800 hover:border-gray-700 text-gray-300'
1599
- }`}
1600
- >
1601
- <div className="flex items-center gap-2 truncate">
1602
- <span className={`h-1.5 w-1.5 rounded-full ${isRunning ? 'bg-emerald-400 animate-pulse' : 'bg-rose-500'}`}></span>
1603
- <span className="font-bold truncate">{service.name}</span>
1604
- </div>
1605
- <span className="text-[10px] font-mono shrink-0">
1606
- {isRunning ? (
1607
- <span className="text-emerald-400 bg-emerald-500/10 px-1 py-0.2 rounded border border-emerald-500/20 text-[8px] font-bold uppercase">on</span>
1608
- ) : (
1609
- <span className="text-gray-500 bg-gray-900 px-1 py-0.2 rounded border border-gray-800 text-[8px] font-bold uppercase">off</span>
1610
- )}
1611
- </span>
1612
- </div>
1613
- );
1614
- })}
1615
- </div>
1616
- )}
1617
- </div>
1618
-
1619
- {/* MCP Status Check */}
1620
- <div className="mb-3 border-t border-gray-800 pt-2.5">
1621
- <div className="text-[10px] text-gray-400 font-bold uppercase mb-1 px-1">
1622
- Active Local MCP Tools
1623
- </div>
1624
- <div className="flex flex-col gap-1 px-1 text-[10px] text-gray-450 font-mono">
1625
- <div className="flex justify-between">
1626
- <span>• search_workspace</span>
1627
- <span className="text-indigo-400 font-bold">READY</span>
1628
- </div>
1629
- <div className="flex justify-between">
1630
- <span>• get_service_logs</span>
1631
- <span className="text-indigo-400 font-bold">READY</span>
1632
- </div>
1633
- <div className="flex justify-between">
1634
- <span>• delegate_to_local_agent</span>
1635
- <span className={config?.localLlm?.enabled ? "text-indigo-400 font-bold" : "text-gray-600 font-bold"}>
1636
- {config?.localLlm?.enabled ? 'READY' : 'DISABLED'}
1637
- </span>
1638
- </div>
1639
- </div>
1640
- </div>
1641
-
1642
- {/* Interactive CLI Buttons */}
1643
- <div className="border-t border-gray-800 pt-2.5">
1644
- <div className="text-[10px] text-gray-400 font-bold uppercase mb-1.5 px-1">
1645
- Terminal Commands (Click to Run)
1646
- </div>
1647
- <div className="grid grid-cols-2 gap-1.5">
1648
- <button
1649
- onClick={() => executeTerminal('nexusflow sync')}
1650
- className="px-2 py-1.5 text-left border border-gray-850 hover:border-indigo-500 bg-gray-950/40 rounded text-gray-300 hover:text-white hover:bg-indigo-950/20 transition-all font-mono text-[9px] cursor-pointer"
1651
- >
1652
- $ nexusflow sync
1653
- </button>
1654
- <button
1655
- onClick={() => executeTerminal('nexusflow diff')}
1656
- className="px-2 py-1.5 text-left border border-gray-850 hover:border-indigo-500 bg-gray-950/40 rounded text-gray-300 hover:text-white hover:bg-indigo-950/20 transition-all font-mono text-[9px] cursor-pointer"
1657
- >
1658
- $ nexusflow diff
1659
- </button>
1660
- <button
1661
- onClick={() => executeTerminal('nexusflow handoff')}
1662
- className="px-2 py-1.5 text-left border border-gray-850 hover:border-indigo-500 bg-gray-950/40 rounded text-gray-300 hover:text-white hover:bg-indigo-950/20 transition-all font-mono text-[9px] cursor-pointer"
1663
- >
1664
- $ nexusflow handoff
1665
- </button>
1666
- <button
1667
- onClick={() => executeTerminal('nexusflow status')}
1668
- className="px-2 py-1.5 text-left border border-gray-850 hover:border-indigo-500 bg-gray-950/40 rounded text-gray-300 hover:text-white hover:bg-indigo-950/20 transition-all font-mono text-[9px] cursor-pointer"
1669
- >
1670
- $ nexusflow status
1671
- </button>
1672
- </div>
1673
- </div>
1674
- </div>
1675
-
1676
- {/* Bottom Log Pane */}
1677
- <div className="flex-1 flex flex-col min-h-0 bg-[#04060d]">
1678
- <div className="flex items-center justify-between px-3 py-1.5 bg-[#090d1a] border-b border-gray-800/80 shrink-0 text-[9px] text-gray-400 uppercase tracking-wider font-bold">
1679
- <div className="flex items-center gap-1.5">
1680
- <span className="h-1.5 w-1.5 rounded-full bg-indigo-500 animate-pulse"></span>
1681
- <span>log_stream: {selectedLogService || 'none'}</span>
1682
- </div>
1683
- <button
1684
- onClick={() => {
1685
- if (activeWsId && selectedLogService) {
1686
- fetchLogs(activeWsId, selectedLogService);
1687
- }
1688
- }}
1689
- className="text-gray-500 hover:text-white font-mono hover:underline cursor-pointer bg-transparent border-none p-0 outline-none"
1690
- >
1691
- [refresh]
1692
- </button>
1693
- </div>
1694
- <div className="flex-1 p-3 overflow-y-auto font-mono text-[10px] leading-relaxed whitespace-pre-wrap select-text selection:bg-indigo-500/30 text-gray-300">
1695
- {serviceLogs.trim() ? (
1696
- serviceLogs
1697
- ) : (
1698
- <span className="text-gray-600 italic font-mono">(no logs recorded yet)</span>
1699
- )}
1700
- <div ref={logsEndRef} />
1701
- </div>
1702
- </div>
1703
- </div>
1704
- )}
1705
- </div>
1706
- );
1707
- }
1708
-
1709
- return (
1710
- <div className="flex min-h-screen bg-[#05070c] text-slate-300 font-mono crt-screen">
1711
- {/* Sidebar Navigation */}
1712
- <aside className="w-64 bg-[#05070c] border-r border-slate-900 flex flex-col p-6 shrink-0 relative z-10 neon-border">
1713
- <div className="flex items-center gap-3 mb-10 px-2">
1714
- <div className="w-9 h-9 bg-gradient-to-tr from-cyan-500 to-violet-600 rounded-lg flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-500/10 text-lg select-none">
1715
- NF
1716
- </div>
1717
- <div>
1718
- <span className="text-sm font-bold tracking-wider text-white">
1719
- NexusFlow
1720
- </span>
1721
- <span className="block text-[8px] text-cyan-400 font-semibold tracking-widest uppercase mt-0.5">COMMAND_CENTER</span>
1722
- </div>
1723
- </div>
1724
- <nav className="flex-1">
1725
- <ul className="flex flex-col gap-2">
1726
- <li>
1727
- <button
1728
- className={`w-full flex items-center gap-3 p-3 rounded-lg text-xs font-bold transition-all cursor-pointer border relative overflow-hidden group ${
1729
- view === 'guide'
1730
- ? 'text-cyan-400 bg-cyan-950/30 border-cyan-900/60 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
1731
- : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-900/40'
1732
- }`}
1733
- onClick={() => {
1734
- setView('guide');
1735
- }}
1736
- >
1737
- <Sparkles size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
1738
- Getting Started
1739
- </button>
1740
- </li>
1741
- <li>
1742
- <button
1743
- className={`w-full flex items-center gap-3 p-3 rounded-lg text-xs font-bold transition-all cursor-pointer border relative overflow-hidden group ${
1744
- view === 'create'
1745
- ? 'text-cyan-400 bg-cyan-950/30 border-cyan-900/60 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
1746
- : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-900/40'
1747
- }`}
1748
- onClick={() => {
1749
- setView('create');
1750
- setActiveStep(0);
1751
- setCreatedWorkspace(null);
1752
- setBranchName('');
1753
- setDescription('');
1754
- setSelectedRepos([]);
1755
- setLocalLlmEnabled(config?.localLlm?.enabled || false);
1756
- }}
1757
- >
1758
- <PlusCircle size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
1759
- New Workspace
1760
- </button>
1761
- </li>
1762
- <li>
1763
- <button
1764
- className={`w-full flex items-center gap-3 p-3 rounded-lg text-xs font-bold transition-all cursor-pointer border relative overflow-hidden group ${
1765
- view === 'workspaces'
1766
- ? 'text-cyan-400 bg-cyan-950/30 border-cyan-900/60 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
1767
- : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-900/40'
1768
- }`}
1769
- onClick={() => {
1770
- setView('workspaces');
1771
- fetchWorkspaces();
1772
- }}
1773
- >
1774
- <FolderGit2 size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
1775
- Active Workspaces
1776
- </button>
1777
- </li>
1778
- <li>
1779
- <button
1780
- className={`w-full flex items-center gap-3 p-3 rounded-lg text-xs font-bold transition-all cursor-pointer border relative overflow-hidden group ${
1781
- view === 'workflows'
1782
- ? 'text-cyan-400 bg-cyan-950/30 border-cyan-900/60 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
1783
- : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-900/40'
1784
- }`}
1785
- onClick={() => {
1786
- setView('workflows');
1787
- fetchWorkflowTemplates();
1788
- if (workflowTemplates.length > 0) {
1789
- const first = workflowTemplates[0];
1790
- setSelectedMgtTemplateId(first.id);
1791
- setMgtTemplateName(first.name);
1792
- setMgtTemplateContent(first.content);
1793
- }
1794
- setAnalysisResult(null);
1795
- }}
1796
- >
1797
- <Workflow size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
1798
- Team Strategies
1799
- </button>
1800
- </li>
1801
- <li>
1802
- <button
1803
- className={`w-full flex items-center gap-3 p-3 rounded-lg text-xs font-bold transition-all cursor-pointer border relative overflow-hidden group ${
1804
- view === 'settings'
1805
- ? 'text-cyan-400 bg-cyan-950/30 border-cyan-900/60 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
1806
- : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-900/40'
1807
- }`}
1808
- onClick={() => {
1809
- setView('settings');
1810
- fetchConfig();
1811
- }}
1812
- >
1813
- <SettingsIcon size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
1814
- Settings
1815
- </button>
1816
- </li>
1817
- </ul>
1818
- </nav>
1819
- <div className="pt-6 border-t border-slate-900 text-[10px] text-slate-500 text-center tracking-wider font-semibold uppercase">
1820
- SYSTEM_UI: ONLINE v{appVersion}
1821
- </div>
1822
- </aside>
1823
-
1824
- {/* Main Content Area */}
1825
- <main className="flex-1 p-8 overflow-y-auto max-w-7xl w-full mx-auto">
1826
- {configLoading ? (
1827
- <div className="flex flex-col items-center justify-center py-40 gap-4 text-gray-400">
1828
- <RefreshCw className="animate-spin text-indigo-400" size={32} />
1829
- <span className="text-sm font-medium">Loading config settings...</span>
1830
- </div>
1831
- ) : (
1832
- <>
1833
- {/* Update Notification Banner */}
1834
- {updateStatus && updateStatus.updateAvailable && (
1835
- <div className="mb-6 p-4 bg-gradient-to-r from-amber-500/10 to-orange-600/10 border border-amber-500/30 rounded-xl shadow-lg flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 backdrop-blur-sm">
1836
- <div className="flex items-center gap-3">
1837
- <div className="w-10 h-10 rounded-lg bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400 shrink-0">
1838
- {updatingApp ? (
1839
- <RefreshCw size={20} className="animate-spin text-amber-400" />
1840
- ) : (
1841
- <Sparkles size={20} />
1842
- )}
1843
- </div>
1844
- <div>
1845
- <h4 className="text-sm font-bold text-amber-300">
1846
- {updatingApp ? (
1847
- updateStep === 'downloading' ? 'Downloading Update...' : 'Applying Update...'
1848
- ) : (
1849
- 'A new version of NexusFlow is available!'
1850
- )}
1851
- </h4>
1852
- <p className="text-xs text-gray-400 mt-0.5">
1853
- {updatingApp ? (
1854
- updateStep === 'downloading' ? 'Fetching installer from GitHub Releases...' : 'Closing app and launching silent installer setup...'
1855
- ) : (
1856
- `Upgrade from v${updateStatus.currentVersion} to v${updateStatus.latestVersion} to get the latest features and bug fixes.`
1857
- )}
1858
- </p>
1859
- </div>
1860
- </div>
1861
- <div className="flex items-center gap-3 shrink-0">
1862
- {typeof window !== 'undefined' && (window as any).Neutralino && updateStatus.downloadUrl && (
1863
- <button
1864
- onClick={handleAutoUpdate}
1865
- disabled={updatingApp}
1866
- className="px-4 py-2 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold text-xs rounded-lg transition-all shadow-md disabled:opacity-50 cursor-pointer flex items-center gap-1.5"
1867
- >
1868
- {updatingApp ? 'Installing...' : 'Install Automatically'}
1869
- </button>
1870
- )}
1871
- <button
1872
- onClick={() => {
1873
- navigator.clipboard.writeText('npm install -g @mrpatronz/nexusflow');
1874
- showToast('Update command copied to clipboard!', 'success');
1875
- }}
1876
- disabled={updatingApp}
1877
- className="px-4 py-2 bg-amber-500 hover:bg-amber-600 text-[#060813] font-bold text-xs rounded-lg transition-all shadow-md shadow-amber-500/10 disabled:opacity-50 cursor-pointer flex items-center gap-1.5"
1878
- >
1879
- Copy Update Command
1880
- </button>
1881
- </div>
1882
- </div>
1883
- )}
1884
-
1885
- {/* View 0: Getting Started Guide */}
1886
- {view === 'guide' && config && (
1887
- <div className="max-w-4xl mx-auto">
1888
- <header className="mb-10">
1889
- <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 text-xs font-semibold uppercase tracking-wider mb-4">
1890
- <Sparkles size={12} className="text-cyan-400" /> Getting Started Guide
1891
- </div>
1892
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2 bg-gradient-to-r from-white via-gray-200 to-indigo-300 bg-clip-text text-transparent">
1893
- Welcome to NexusFlow
1894
- </h1>
1895
- <p className="text-sm text-gray-400">
1896
- NexusFlow orchestrates multi-repository developer environments. It combines isolated Git worktrees, automatic code analyzer sweeps, and background process running into a single dashboard.
1897
- </p>
1898
- </header>
1899
-
1900
- <div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
1901
- {/* Left: Interactive Stepper */}
1902
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-6 shadow-xl backdrop-blur-sm space-y-6">
1903
- <h2 className="text-lg font-bold text-white mb-4">NexusFlow Workflows</h2>
1904
-
1905
- <div className="flex gap-4">
1906
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0">
1907
- 1
1908
- </div>
1909
- <div>
1910
- <h3 className="text-sm font-semibold text-white">Configure Folders</h3>
1911
- <p className="text-xs text-gray-400 mt-1">
1912
- Specify your Development folder (where repositories live) and Workspaces folder. These are currently set to:
1913
- <code className="block mt-1 text-[10px] text-indigo-300 break-all">{config.devDir || 'Not Configured'}</code>
1914
- </p>
1915
- </div>
1916
- </div>
1917
-
1918
- <div className="flex gap-4">
1919
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0">
1920
- 2
1921
- </div>
1922
- <div>
1923
- <h3 className="text-sm font-semibold text-white">Build Workspaces</h3>
1924
- <p className="text-xs text-gray-400 mt-1">
1925
- Choose repositories and feature branch. NexusFlow checks out dependencies under a unified workspace directory using Git Worktrees, keeping original projects clean.
1926
- </p>
1927
- </div>
1928
- </div>
1929
-
1930
- <div className="flex gap-4">
1931
- <div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-sm shrink-0">
1932
- 3
1933
- </div>
1934
- <div>
1935
- <h3 className="text-sm font-semibold text-white">Align AI Context</h3>
1936
- <p className="text-xs text-gray-400 mt-1">
1937
- NexusFlow writes files like <code>CLAUDE.md</code>, <code>.cursorrules</code>, <code>AGENTS.md</code> prompting the LLM to inspect project relations and list key assumptions and questions before coding.
1938
- </p>
1939
- </div>
1940
- </div>
1941
-
1942
-
1943
- </div>
1944
-
1945
- {/* Right: Quick actions and Telemetry info */}
1946
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-6 shadow-xl backdrop-blur-sm flex flex-col justify-between">
1947
- <div>
1948
- <h2 className="text-lg font-bold text-white mb-4">Current Configuration</h2>
1949
- <div className="space-y-3 text-xs">
1950
- <div className="flex justify-between py-1.5 border-b border-gray-800/60">
1951
- <span className="text-gray-500">Dev Folder:</span>
1952
- <span className="text-gray-300 font-mono text-[10px] truncate max-w-[200px]">{config.devDir}</span>
1953
- </div>
1954
- <div className="flex justify-between py-1.5 border-b border-gray-800/60">
1955
- <span className="text-gray-500">Workspaces Folder:</span>
1956
- <span className="text-gray-300 font-mono text-[10px] truncate max-w-[200px]">{config.workspacesDir}</span>
1957
- </div>
1958
- <div className="flex justify-between py-1.5 border-b border-gray-800/60">
1959
- <span className="text-gray-500">Preferred AI:</span>
1960
- <span className="text-indigo-400 font-semibold uppercase">{config.defaultAssistant || 'None'}</span>
1961
- </div>
1962
- <div className="flex justify-between py-1.5">
1963
- <span className="text-gray-500">Scan Depth:</span>
1964
- <span className="text-gray-300 font-semibold">{config.scanDepth} levels</span>
1965
- </div>
1966
- </div>
1967
- </div>
1968
-
1969
- <div className="pt-6 border-t border-gray-800/60 flex flex-col gap-2">
1970
- <button
1971
- className="w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-xs font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-md shadow-indigo-500/10 hover:-translate-y-0.5 active:translate-y-0 transition-all cursor-pointer"
1972
- onClick={() => setView('create')}
1973
- >
1974
- <PlusCircle size={14} /> Create a Workspace
1975
- </button>
1976
- <button
1977
- className="w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-xs font-semibold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
1978
- onClick={() => setView('settings')}
1979
- >
1980
- <SettingsIcon size={14} className="text-gray-500" /> Modify Settings
1981
- </button>
1982
- </div>
1983
- </div>
1984
- </div>
1985
-
1986
- {/* Compare dashboard at the bottom */}
1987
- <div className="bg-[#111827]/20 border border-gray-800/80 rounded-xl p-6 shadow-md">
1988
- <h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-3">Industry Comparison</h4>
1989
- <div className="overflow-x-auto">
1990
- <table className="w-full text-[10px] text-gray-400 text-left border-collapse">
1991
- <thead>
1992
- <tr className="border-b border-gray-800 text-gray-500">
1993
- <th className="py-2 pr-4 font-semibold">Orchestrator</th>
1994
- <th className="py-2 px-4 font-semibold">Multi-Repo</th>
1995
- <th className="py-2 px-4 font-semibold">AI Rules Integration</th>
1996
- <th className="py-2 pl-4 font-semibold">Weight</th>
1997
- </tr>
1998
- </thead>
1999
- <tbody>
2000
- <tr className="border-b border-gray-800/40">
2001
- <td className="py-2 pr-4 font-semibold text-white">Docker Compose</td>
2002
- <td className="py-2 px-4">Yes (Containers only)</td>
2003
- <td className="py-2 px-4">No</td>
2004
- <td className="py-2 pl-4">Medium (VM overhead)</td>
2005
- </tr>
2006
- <tr className="border-b border-gray-800/40">
2007
- <td className="py-2 pr-4 font-semibold text-white">Lerna / Turborepo</td>
2008
- <td className="py-2 px-4">Monorepo only</td>
2009
- <td className="py-2 px-4">No</td>
2010
- <td className="py-2 pl-4">Light</td>
2011
- </tr>
2012
- <tr className="border-b border-gray-800/40">
2013
- <td className="py-2 pr-4 font-semibold text-white">DevPod / Gitpod</td>
2014
- <td className="py-2 px-4">Yes (Complex config)</td>
2015
- <td className="py-2 px-4">No</td>
2016
- <td className="py-2 pl-4">Heavy (Full virtual VM)</td>
2017
- </tr>
2018
- <tr>
2019
- <td className="py-2 pr-4 font-semibold text-indigo-400">NexusFlow</td>
2020
- <td className="py-2 px-4 text-indigo-300">Yes (Native Worktrees)</td>
2021
- <td className="py-2 px-4 text-indigo-300">Yes (CLAUDE.md/MDC rules)</td>
2022
- <td className="py-2 pl-4 text-indigo-300">Extremely Light (Native processes)</td>
2023
- </tr>
2024
- </tbody>
2025
- </table>
2026
- </div>
2027
- </div>
2028
- </div>
2029
- )}
2030
-
2031
- {/* View 1: Wizard Workspace Builder */}
2032
- {view === 'create' && (
2033
- <div className="max-w-4xl mx-auto">
2034
- <header className="mb-10">
2035
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">New Feature Workspace</h1>
2036
- <p className="text-sm text-gray-400">
2037
- Set up unified git worktrees, scan tech stacks, and write contextual configurations for your AI assistant.
2038
- </p>
2039
- </header>
2040
-
2041
- {/* Progress Circle bar */}
2042
- <div className="flex justify-between items-center max-w-2xl mx-auto mb-14 relative px-4">
2043
- <div className="absolute top-4 left-6 right-6 h-[2px] bg-gray-800 -z-10"></div>
2044
- <div
2045
- className="absolute top-4 left-6 h-[2px] bg-gradient-to-r from-cyan-400 to-indigo-500 -z-10 transition-all duration-300"
2046
- style={{ width: `${(activeStep / 4) * 95}%` }}
2047
- ></div>
2048
- <div className="flex flex-col items-center gap-2">
2049
- <div className={`w-8 h-8 rounded-full border-2 flex items-center justify-center font-bold text-xs transition-all ${
2050
- activeStep > 0 ? 'bg-emerald-500 border-emerald-500 text-white' : activeStep === 0 ? 'border-indigo-500 bg-[#0b0f19] text-white shadow-lg shadow-indigo-500/20' : 'border-gray-800 bg-gray-900 text-gray-500'
2051
- }`}>
2052
- {activeStep > 0 ? <Check size={14} /> : '1'}
2053
- </div>
2054
- <span className={`text-[11px] font-semibold tracking-wide uppercase ${activeStep === 0 ? 'text-white' : 'text-gray-500'}`}>Details</span>
2055
- </div>
2056
- <div className="flex flex-col items-center gap-2">
2057
- <div className={`w-8 h-8 rounded-full border-2 flex items-center justify-center font-bold text-xs transition-all ${
2058
- activeStep > 1 ? 'bg-emerald-500 border-emerald-500 text-white' : activeStep === 1 ? 'border-indigo-500 bg-[#0b0f19] text-white shadow-lg shadow-indigo-500/20' : 'border-gray-800 bg-gray-900 text-gray-500'
2059
- }`}>
2060
- {activeStep > 1 ? <Check size={14} /> : '2'}
2061
- </div>
2062
- <span className={`text-[11px] font-semibold tracking-wide uppercase ${activeStep === 1 ? 'text-white' : 'text-gray-500'}`}>Repos</span>
2063
- </div>
2064
- <div className="flex flex-col items-center gap-2">
2065
- <div className={`w-8 h-8 rounded-full border-2 flex items-center justify-center font-bold text-xs transition-all ${
2066
- activeStep > 2 ? 'bg-emerald-500 border-emerald-500 text-white' : activeStep === 2 ? 'border-indigo-500 bg-[#0b0f19] text-white shadow-lg shadow-indigo-500/20' : 'border-gray-800 bg-gray-900 text-gray-500'
2067
- }`}>
2068
- {activeStep > 2 ? <Check size={14} /> : '3'}
2069
- </div>
2070
- <span className={`text-[11px] font-semibold tracking-wide uppercase ${activeStep === 2 ? 'text-white' : 'text-gray-500'}`}>Assistant</span>
2071
- </div>
2072
- <div className="flex flex-col items-center gap-2">
2073
- <div className={`w-8 h-8 rounded-full border-2 flex items-center justify-center font-bold text-xs transition-all ${
2074
- activeStep > 3 ? 'bg-emerald-500 border-emerald-500 text-white' : activeStep === 3 ? 'border-indigo-500 bg-[#0b0f19] text-white shadow-lg shadow-indigo-500/20' : 'border-gray-800 bg-gray-900 text-gray-500'
2075
- }`}>
2076
- {activeStep > 3 ? <Check size={14} /> : '4'}
2077
- </div>
2078
- <span className={`text-[11px] font-semibold tracking-wide uppercase ${activeStep === 3 ? 'text-white' : 'text-gray-500'}`}>Strategy</span>
2079
- </div>
2080
- <div className="flex flex-col items-center gap-2">
2081
- <div className={`w-8 h-8 rounded-full border-2 flex items-center justify-center font-bold text-xs transition-all ${
2082
- activeStep === 4 ? 'border-emerald-500 bg-[#0b0f19] text-emerald-400 shadow-lg shadow-emerald-500/20' : 'border-gray-800 bg-gray-900 text-gray-500'
2083
- }`}>
2084
- 5
2085
- </div>
2086
- <span className={`text-[11px] font-semibold tracking-wide uppercase ${activeStep === 4 ? 'text-emerald-400' : 'text-gray-500'}`}>Complete</span>
2087
- </div>
2088
- </div>
2089
-
2090
- {/* Step 0: Name & Description */}
2091
- {activeStep === 0 && (
2092
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm">
2093
- <div className="mb-6">
2094
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Feature Branch Name</label>
2095
- <input
2096
- type="text"
2097
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner"
2098
- placeholder="e.g., feature/oauth-authentication-flow"
2099
- value={branchName}
2100
- onChange={(e) => setBranchName(e.target.value)}
2101
- />
2102
- </div>
2103
- <div className="mb-8">
2104
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Feature Purpose & Context</label>
2105
- <textarea
2106
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm min-h-[140px] resize-y shadow-inner"
2107
- placeholder="Describe what you want to build. This helps AI assistants analyze context and produce matching plans."
2108
- value={description}
2109
- onChange={(e) => setDescription(e.target.value)}
2110
- />
2111
- </div>
2112
- <div className="flex justify-end">
2113
- <button
2114
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-md shadow-indigo-500/10 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed hover:-translate-y-0.5 active:translate-y-0"
2115
- disabled={!branchName || !description}
2116
- onClick={() => setActiveStep(1)}
2117
- >
2118
- Next Step <ArrowRight size={16} />
2119
- </button>
2120
- </div>
2121
- </div>
2122
- )}
2123
-
2124
- {/* Step 1: Repo Selector */}
2125
- {activeStep === 1 && (
2126
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm">
2127
- <div className="flex justify-between items-center mb-6">
2128
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">Select Repositories</label>
2129
- <div className="relative w-64">
2130
- <Search size={14} className="absolute left-3 top-3 text-gray-500" />
2131
- <input
2132
- type="text"
2133
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg pl-9 pr-4 py-2 text-white placeholder-gray-600 transition-all outline-none text-xs"
2134
- placeholder="Search repo name..."
2135
- value={repoSearch}
2136
- onChange={(e) => setRepoSearch(e.target.value)}
2137
- />
2138
- </div>
2139
- </div>
2140
-
2141
- {reposLoading ? (
2142
- <div className="flex flex-col items-center py-20 gap-3 text-gray-500">
2143
- <RefreshCw className="animate-spin text-indigo-400" size={24} />
2144
- <span className="text-xs">Scanning local projects directory...</span>
2145
- </div>
2146
- ) : (
2147
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-[380px] overflow-y-auto p-2 border border-gray-800/60 rounded-lg bg-gray-950/20 mb-8">
2148
- {filteredRepos.map((repo) => {
2149
- const isSelected = selectedRepos.some((r) => r.path === repo.path);
2150
- return (
2151
- <div
2152
- key={repo.path}
2153
- className={`bg-[#111827]/60 border rounded-xl p-4 flex items-start gap-3 cursor-pointer hover:bg-gray-800/20 hover:border-gray-700 transition-all ${
2154
- isSelected ? 'border-indigo-500 bg-indigo-500/5' : 'border-gray-800/80'
2155
- }`}
2156
- onClick={() => handleToggleRepo(repo)}
2157
- >
2158
- <input
2159
- type="checkbox"
2160
- className="mt-1 h-4 w-4 accent-indigo-500 rounded cursor-pointer"
2161
- checked={isSelected}
2162
- readOnly
2163
- />
2164
- <div className="flex-1 min-w-0">
2165
- <div className="text-sm font-semibold text-white truncate">{repo.name}</div>
2166
- <div className="text-[10px] text-gray-500 truncate mt-1">{repo.path}</div>
2167
- </div>
2168
- <span className="text-[9px] px-2 py-0.5 rounded bg-gray-800 text-gray-400 font-semibold uppercase">{repo.defaultBranch}</span>
2169
- </div>
2170
- );
2171
- })}
2172
- </div>
2173
- )}
2174
-
2175
- <div className="flex justify-between">
2176
- <button
2177
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2178
- onClick={() => setActiveStep(0)}
2179
- >
2180
- <ArrowLeft size={16} /> Back
2181
- </button>
2182
- <button
2183
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-md shadow-indigo-500/10 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed hover:-translate-y-0.5 active:translate-y-0"
2184
- disabled={selectedRepos.length === 0}
2185
- onClick={() => setActiveStep(2)}
2186
- >
2187
- Next Step <ArrowRight size={16} />
2188
- </button>
2189
- </div>
2190
- </div>
2191
- )}
2192
-
2193
- {/* Step 2: AI & Editor Settings */}
2194
- {activeStep === 2 && (
2195
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm">
2196
- {creating ? (
2197
- <div className="flex flex-col items-center py-6">
2198
- <h3 className="text-lg font-bold text-white mb-2 flex items-center gap-2.5">
2199
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2200
- Building Workspace...
2201
- </h3>
2202
- <p className="text-xs text-gray-400 mb-8">
2203
- Setting up your multi-repo workspace. This will take a moment.
2204
- </p>
2205
-
2206
- <div className="w-full max-w-md space-y-4">
2207
- {creationSteps.map((step) => {
2208
- const isPending = step.status === 'pending';
2209
- const isRunning = step.status === 'running';
2210
- const isCompleted = step.status === 'completed';
2211
- const isFailed = step.status === 'failed';
2212
-
2213
- return (
2214
- <div
2215
- key={step.id}
2216
- className={`flex items-start gap-4 p-4 rounded-xl border transition-all duration-300 ${
2217
- isRunning
2218
- ? 'bg-indigo-500/10 border-indigo-500/50 shadow-md shadow-indigo-500/5'
2219
- : isCompleted
2220
- ? 'bg-emerald-500/5 border-emerald-500/20 opacity-80'
2221
- : isFailed
2222
- ? 'bg-rose-500/5 border-rose-500/30'
2223
- : 'bg-gray-900/20 border-gray-800/40 opacity-40'
2224
- }`}
2225
- >
2226
- <div className="mt-0.5">
2227
- {isRunning && (
2228
- <div className="relative flex h-5 w-5 items-center justify-center">
2229
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
2230
- <RefreshCw className="animate-spin text-indigo-400 relative" size={16} />
2231
- </div>
2232
- )}
2233
- {isCompleted && (
2234
- <div className="h-5 w-5 rounded-full bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center text-emerald-400">
2235
- <Check size={12} />
2236
- </div>
2237
- )}
2238
- {isFailed && (
2239
- <div className="h-5 w-5 rounded-full bg-rose-500/20 border border-rose-500/40 flex items-center justify-center text-rose-400">
2240
- <AlertTriangle size={12} />
2241
- </div>
2242
- )}
2243
- {isPending && (
2244
- <div className="h-5 w-5 rounded-full border-2 border-gray-800 flex items-center justify-center text-gray-600">
2245
- <div className="w-1.5 h-1.5 rounded-full bg-gray-800"></div>
2246
- </div>
2247
- )}
2248
- </div>
2249
- <div className="flex-1 min-w-0">
2250
- <h4 className={`text-sm font-bold truncate ${
2251
- isRunning ? 'text-indigo-400' : isCompleted ? 'text-emerald-400' : isFailed ? 'text-rose-400' : 'text-gray-500'
2252
- }`}>
2253
- {step.name}
2254
- </h4>
2255
- <p className="text-xs text-gray-400 mt-1 font-mono break-words leading-relaxed">
2256
- {step.message}
2257
- </p>
2258
- </div>
2259
- </div>
2260
- );
2261
- })}
2262
- </div>
2263
-
2264
- {creationError && (
2265
- <div className="mt-6 p-4 bg-rose-500/10 border border-rose-500/20 text-rose-450 rounded-xl text-xs w-full max-w-md flex flex-col gap-3">
2266
- <span className="font-bold flex items-center gap-1.5">
2267
- <AlertCircle size={14} className="text-rose-450" /> Build Failed
2268
- </span>
2269
- <span className="font-mono">{creationError}</span>
2270
- <button
2271
- className="w-full mt-2 py-2 px-4 rounded-lg bg-rose-600 hover:bg-rose-700 text-white font-bold text-[11px] transition-colors cursor-pointer"
2272
- onClick={() => {
2273
- setCreating(false);
2274
- setCreationError(null);
2275
- }}
2276
- >
2277
- Dismiss
2278
- </button>
2279
- </div>
2280
- )}
2281
- </div>
2282
- ) : (
2283
- <>
2284
- <div className="mb-8">
2285
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Target AI Assistant(s)</label>
2286
- <p className="text-xs text-gray-500 mb-4">
2287
- We generate context configurations matching the specifications of each checked assistant.
2288
- </p>
2289
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
2290
- {aiAssistants.map((ai) => {
2291
- const isSelected = selectedAI.includes(ai.name);
2292
- return (
2293
- <div
2294
- key={ai.name}
2295
- className={`bg-[#111827]/60 border rounded-xl p-4 flex flex-col gap-3 cursor-pointer hover:border-gray-700 transition-all ${
2296
- isSelected ? 'border-indigo-500 bg-indigo-500/5' : 'border-gray-800/80'
2297
- }`}
2298
- onClick={() => handleToggleAI(ai.name)}
2299
- >
2300
- <div className="flex justify-between items-center">
2301
- <span className="text-sm font-bold text-white">{ai.displayName}</span>
2302
- <span className={`text-[9px] px-2 py-0.5 rounded font-semibold uppercase ${
2303
- ai.detected ? 'bg-emerald-500/10 text-emerald-400' : 'bg-gray-800 text-gray-500'
2304
- }`}>
2305
- {ai.detected ? 'Installed' : 'Missing'}
2306
- </span>
2307
- </div>
2308
- <p className="text-[11px] text-gray-500">
2309
- {ai.name === 'claude' && 'CLAUDE.md guidelines'}
2310
- {ai.name === 'antigravity' && 'CLAUDE.md guidelines (harness)'}
2311
- {ai.name === 'codex' && 'AGENTS.md config structure'}
2312
- {ai.name === 'copilot' && 'GitHub Copilot Workspace configs'}
2313
- {ai.name === 'cursor' && 'Cursor MDC rules and instructions'}
2314
- </p>
2315
- </div>
2316
- );
2317
- })}
2318
- </div>
2319
- </div>
2320
-
2321
- <div className="mb-8">
2322
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Open Workspace In</label>
2323
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
2324
- {editors.map((ed) => {
2325
- const isSelected = selectedEditor?.command === ed.command;
2326
- return (
2327
- <div
2328
- key={ed.command}
2329
- className={`bg-[#111827]/60 border rounded-xl p-4 flex flex-col cursor-pointer hover:border-gray-700 transition-all ${
2330
- isSelected ? 'border-indigo-500 bg-indigo-500/5' : 'border-gray-800/80'
2331
- }`}
2332
- onClick={() => {
2333
- setSelectedEditor(ed);
2334
- if (config) {
2335
- const updatedConfig = { ...config, defaultEditor: ed.command };
2336
- setConfig(updatedConfig);
2337
- saveAppConfig(updatedConfig);
2338
- }
2339
- }}
2340
- >
2341
- <span className="text-sm font-bold text-white">{ed.name}</span>
2342
- <span className={`text-[9px] mt-2 w-max px-2 py-0.5 rounded font-semibold uppercase ${
2343
- ed.detected ? 'bg-emerald-500/10 text-emerald-400' : 'bg-gray-800 text-gray-500'
2344
- }`}>
2345
- {ed.detected ? 'Detected' : 'Missing'}
2346
- </span>
2347
- </div>
2348
- );
2349
- })}
2350
- <div
2351
- className={`bg-[#111827]/60 border rounded-xl p-4 flex flex-col cursor-pointer hover:border-gray-700 transition-all ${
2352
- selectedEditor === null ? 'border-indigo-500 bg-indigo-500/5' : 'border-gray-800/80'
2353
- }`}
2354
- onClick={() => {
2355
- setSelectedEditor(null);
2356
- if (config) {
2357
- const updatedConfig = { ...config, defaultEditor: 'none' };
2358
- setConfig(updatedConfig);
2359
- saveAppConfig(updatedConfig);
2360
- }
2361
- }}
2362
- >
2363
- <span className="text-sm font-bold text-white">None</span>
2364
- <span className="text-[9px] mt-2 w-max px-2 py-0.5 rounded font-semibold uppercase bg-gray-800 text-gray-500">
2365
- Skip opening
2366
- </span>
2367
- </div>
2368
- </div>
2369
- </div>
2370
-
2371
- <div className={`mb-8 border rounded-xl p-5 ${config?.localLlm?.enabled ? 'border-gray-800/80 bg-gray-950/20' : 'border-gray-800/40 bg-gray-950/10 opacity-60'}`}>
2372
- <h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4 flex items-center gap-2">
2373
- <Cpu size={14} className="text-indigo-400" /> Local AI Co-processor
2374
- </h4>
2375
- <p className="text-[11px] text-gray-500 mb-4">
2376
- Enabling the Local AI Co-processor inserts guidelines in the workspace context files, instructing remote agents to delegate heavy tasks (such as searching, log analysis, and boilerplate generation) to your local Ollama/LM Studio model.
2377
- </p>
2378
- {!config?.localLlm?.enabled && (
2379
- <p className="text-[11px] text-amber-400/80 mb-3 flex items-center gap-1.5">
2380
- <AlertTriangle size={12} /> Enable a local LLM provider in Settings first.
2381
- </p>
2382
- )}
2383
- <div className="flex items-center gap-3">
2384
- <input
2385
- type="checkbox"
2386
- id="wizardLocalLlmEnabled"
2387
- className="w-4 h-4 rounded border-gray-800 text-indigo-600 bg-gray-950/20 focus:ring-indigo-500 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
2388
- checked={localLlmEnabled}
2389
- onChange={(e) => setLocalLlmEnabled(e.target.checked)}
2390
- disabled={!config?.localLlm?.enabled}
2391
- />
2392
- <label htmlFor="wizardLocalLlmEnabled" className={`text-xs font-semibold cursor-pointer select-none ${config?.localLlm?.enabled ? 'text-white' : 'text-gray-500 cursor-not-allowed'}`}>
2393
- Include Local AI Co-processor in this workspace context
2394
- </label>
2395
- </div>
2396
- </div>
2397
-
2398
- <div className="mb-8 border border-gray-800/80 rounded-xl p-5 bg-gray-950/20">
2399
- <h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4 flex items-center gap-2">
2400
- <Terminal size={14} className="text-indigo-400" /> Advanced Session Resumption Settings
2401
- </h4>
2402
- <p className="text-[11px] text-gray-500 mb-4">
2403
- Configure setup and verification test commands. AI assistants and the dashboard use these to resume your sessions green.
2404
- </p>
2405
- <div className="space-y-4">
2406
- <div>
2407
- <label className="block text-[11px] font-semibold text-gray-400 mb-1.5">Verification / Test Command</label>
2408
- <input
2409
- type="text"
2410
- className="w-full bg-[#030408] border border-gray-800 focus:border-indigo-500 rounded-lg px-3 py-2 text-xs font-mono text-white outline-none transition-all"
2411
- placeholder="e.g., npm run test or vitest run"
2412
- value={testCommand}
2413
- onChange={(e) => setTestCommand(e.target.value)}
2414
- />
2415
- </div>
2416
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
2417
- <div>
2418
- <label className="block text-[11px] font-semibold text-gray-400 mb-1.5">Mock / Database Command (Optional)</label>
2419
- <input
2420
- type="text"
2421
- className="w-full bg-[#030408] border border-gray-800 focus:border-indigo-500 rounded-lg px-3 py-2 text-xs font-mono text-white outline-none transition-all"
2422
- placeholder="e.g., docker compose up -d redis"
2423
- value={mockCommand}
2424
- onChange={(e) => setMockCommand(e.target.value)}
2425
- />
2426
- </div>
2427
- <div>
2428
- <label className="block text-[11px] font-semibold text-gray-400 mb-1.5">Start / Run Command (Optional)</label>
2429
- <input
2430
- type="text"
2431
- className="w-full bg-[#030408] border border-gray-800 focus:border-indigo-500 rounded-lg px-3 py-2 text-xs font-mono text-white outline-none transition-all"
2432
- placeholder="e.g., npm run start:dev"
2433
- value={startCommand}
2434
- onChange={(e) => setStartCommand(e.target.value)}
2435
- />
2436
- </div>
2437
- </div>
2438
- </div>
2439
- </div>
2440
-
2441
- <div className="flex justify-between">
2442
- <button
2443
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2444
- onClick={() => setActiveStep(1)}
2445
- >
2446
- <ArrowLeft size={16} /> Back
2447
- </button>
2448
- <button
2449
- className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-lg shadow-indigo-500/20 transition-all cursor-pointer hover:-translate-y-0.5 active:translate-y-0"
2450
- onClick={() => setActiveStep(3)}
2451
- >
2452
- Next Step <ArrowRight size={16} />
2453
- </button>
2454
- </div>
2455
- </>
2456
- )}
2457
- </div>
2458
- )}
2459
-
2460
- {/* Step 3: Team Strategy */}
2461
- {activeStep === 3 && (
2462
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm">
2463
- {creating ? (
2464
- <div className="flex flex-col items-center py-6">
2465
- <h3 className="text-lg font-bold text-white mb-2 flex items-center gap-2.5">
2466
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2467
- Building Workspace...
2468
- </h3>
2469
- <p className="text-xs text-gray-400 mb-8">
2470
- Setting up your multi-repo workspace. This will take a moment.
2471
- </p>
2472
-
2473
- <div className="w-full max-w-md space-y-4">
2474
- {creationSteps.map((step) => {
2475
- const isPending = step.status === 'pending';
2476
- const isRunning = step.status === 'running';
2477
- const isCompleted = step.status === 'completed';
2478
- const isFailed = step.status === 'failed';
2479
-
2480
- return (
2481
- <div
2482
- key={step.id}
2483
- className={`flex items-start gap-4 p-4 rounded-xl border transition-all duration-300 ${
2484
- isRunning
2485
- ? 'bg-indigo-500/10 border-indigo-500/50 shadow-md shadow-indigo-500/5'
2486
- : isCompleted
2487
- ? 'bg-emerald-500/5 border-emerald-500/20 opacity-80'
2488
- : isFailed
2489
- ? 'bg-rose-500/5 border-rose-500/30'
2490
- : 'bg-gray-900/20 border-gray-800/40 opacity-40'
2491
- }`}
2492
- >
2493
- <div className="mt-0.5">
2494
- {isRunning && (
2495
- <div className="relative flex h-5 w-5 items-center justify-center">
2496
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
2497
- <RefreshCw className="animate-spin text-indigo-400 relative" size={16} />
2498
- </div>
2499
- )}
2500
- {isCompleted && (
2501
- <div className="h-5 w-5 rounded-full bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center text-emerald-400">
2502
- <Check size={12} />
2503
- </div>
2504
- )}
2505
- {isFailed && (
2506
- <div className="h-5 w-5 rounded-full bg-rose-500/20 border border-rose-500/40 flex items-center justify-center text-rose-400">
2507
- <AlertTriangle size={12} />
2508
- </div>
2509
- )}
2510
- {isPending && (
2511
- <div className="h-5 w-5 rounded-full border-2 border-gray-800 flex items-center justify-center text-gray-600">
2512
- <div className="w-1.5 h-1.5 rounded-full bg-gray-800"></div>
2513
- </div>
2514
- )}
2515
- </div>
2516
- <div className="flex-1 min-w-0">
2517
- <h4 className={`text-sm font-bold truncate ${
2518
- isRunning ? 'text-indigo-400' : isCompleted ? 'text-emerald-400' : isFailed ? 'text-rose-400' : 'text-gray-500'
2519
- }`}>
2520
- {step.name}
2521
- </h4>
2522
- <p className="text-xs text-gray-400 mt-1 font-mono break-words leading-relaxed">
2523
- {step.message}
2524
- </p>
2525
- </div>
2526
- </div>
2527
- );
2528
- })}
2529
- </div>
2530
-
2531
- {creationError && (
2532
- <div className="mt-6 p-4 bg-rose-500/10 border border-rose-500/20 text-rose-450 rounded-xl text-xs w-full max-w-md flex flex-col gap-3">
2533
- <span className="font-bold flex items-center gap-1.5">
2534
- <AlertCircle size={14} className="text-rose-450" /> Build Failed
2535
- </span>
2536
- <span className="font-mono">{creationError}</span>
2537
- <button
2538
- className="w-full mt-2 py-2 px-4 rounded-lg bg-rose-600 hover:bg-rose-700 text-white font-bold text-[11px] transition-colors cursor-pointer"
2539
- onClick={() => {
2540
- setCreating(false);
2541
- setCreationError(null);
2542
- }}
2543
- >
2544
- Dismiss
2545
- </button>
2546
- </div>
2547
- )}
2548
- </div>
2549
- ) : (
2550
- <>
2551
- <div className="mb-8">
2552
- <div className="flex justify-between items-center mb-2">
2553
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">Team Collaboration Strategy</label>
2554
- <button
2555
- type="button"
2556
- className="px-3 py-1.5 rounded-lg bg-indigo-650 hover:bg-indigo-700 text-white font-bold text-[11px] transition-all cursor-pointer flex items-center gap-1.5 disabled:opacity-50"
2557
- onClick={handleSuggestWorkflow}
2558
- disabled={suggestingWorkflow}
2559
- >
2560
- <Sparkles size={12} className={suggestingWorkflow ? "animate-spin" : ""} />
2561
- {suggestingWorkflow ? "Analyzing..." : "Auto-Suggest Strategy"}
2562
- </button>
2563
- </div>
2564
- <p className="text-xs text-gray-500 mb-4">
2565
- Select an agent cooperation pattern. This writes instructions to <code>AGENTS.md</code> directing how the team coordinates.
2566
- </p>
2567
-
2568
- {suggestedDifficulty && (
2569
- <div className="mb-6 p-4 bg-indigo-950/20 border border-indigo-500/30 rounded-xl text-xs flex flex-col gap-2 shadow-inner">
2570
- <div className="flex items-center gap-2">
2571
- <span className="font-bold text-gray-300">Suggested Task Difficulty:</span>
2572
- <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide ${
2573
- suggestedDifficulty === 'simple'
2574
- ? 'bg-emerald-500/20 border border-emerald-500/30 text-emerald-400'
2575
- : suggestedDifficulty === 'moderate'
2576
- ? 'bg-amber-500/20 border border-amber-500/30 text-amber-400'
2577
- : 'bg-rose-500/20 border border-rose-500/30 text-rose-450'
2578
- }`}>
2579
- {suggestedDifficulty}
2580
- </span>
2581
- </div>
2582
- <p className="text-gray-400 leading-relaxed font-sans">
2583
- {suggestedRationale}
2584
- </p>
2585
- </div>
2586
- )}
2587
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
2588
- {workflowTemplates.map((template) => {
2589
- const isSelected = selectedWorkflowId === template.id;
2590
- return (
2591
- <div
2592
- key={template.id}
2593
- className={`bg-[#111827]/60 border rounded-xl p-4 flex flex-col gap-2 cursor-pointer hover:border-gray-700 transition-all ${
2594
- isSelected ? 'border-indigo-500 bg-indigo-500/5' : 'border-gray-800/80'
2595
- }`}
2596
- onClick={() => {
2597
- setSelectedWorkflowId(template.id);
2598
- setCustomTeamworkInstructions(template.content);
2599
- }}
2600
- >
2601
- <span className="text-sm font-bold text-white">{template.name}</span>
2602
- <p className="text-[11px] text-gray-500 leading-relaxed font-sans">
2603
- {template.description}
2604
- </p>
2605
- </div>
2606
- );
2607
- })}
2608
- </div>
2609
- </div>
2610
-
2611
- <div className="mb-8">
2612
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Cooperation Instructions (AGENTS.md)</label>
2613
- <p className="text-xs text-gray-500 mb-3 font-sans">
2614
- You can customize these instructions directly. They will be saved in the workspace context.
2615
- </p>
2616
- <textarea
2617
- className="w-full bg-[#111827] border border-gray-850 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-605 transition-all outline-none text-xs font-mono min-h-[220px] resize-y shadow-inner leading-relaxed"
2618
- value={customTeamworkInstructions}
2619
- onChange={(e) => setCustomTeamworkInstructions(e.target.value)}
2620
- placeholder="Enter custom instructions for how the AI agents should coordinate..."
2621
- />
2622
- </div>
2623
-
2624
- <div className="flex justify-between">
2625
- <button
2626
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2627
- onClick={() => setActiveStep(2)}
2628
- >
2629
- <ArrowLeft size={16} /> Back
2630
- </button>
2631
- <button
2632
- className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-lg shadow-indigo-500/20 transition-all cursor-pointer hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-40 disabled:cursor-not-allowed"
2633
- disabled={creating}
2634
- onClick={handleCreateWorkspace}
2635
- >
2636
- {creating ? (
2637
- <>
2638
- <RefreshCw className="animate-spin" size={14} /> Building...
2639
- </>
2640
- ) : (
2641
- <>
2642
- <Sparkles size={14} /> Build Workspace
2643
- </>
2644
- )}
2645
- </button>
2646
- </div>
2647
- </>
2648
- )}
2649
- </div>
2650
- )}
2651
-
2652
- {/* Step 4: Success Screen */}
2653
- {activeStep === 4 && createdWorkspace && (
2654
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-10 text-center shadow-xl backdrop-blur-sm">
2655
- <div className="inline-flex items-center justify-center p-3 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 mb-6">
2656
- <Check size={36} />
2657
- </div>
2658
- <h2 className="text-2xl font-bold text-white mb-2">Workspace Generated!</h2>
2659
- <p className="text-gray-400 text-sm max-w-lg mx-auto mb-8">
2660
- We spun up git worktrees for your branch <code>{branchName}</code>, analyzed the code architectures, and generated your AI assistant instructions.
2661
- </p>
2662
-
2663
- <div className="flex flex-col gap-3 bg-gray-950/40 border border-gray-800/60 rounded-xl p-5 w-fit min-w-[360px] mx-auto text-left text-xs mb-8">
2664
- <div className="flex justify-between items-center">
2665
- <span className="text-gray-500 font-medium">Branch name:</span>
2666
- <code className="text-indigo-400 font-bold">{branchName}</code>
2667
- </div>
2668
- <div className="flex justify-between items-center gap-4">
2669
- <span className="text-gray-500 font-medium">Location:</span>
2670
- <code className="text-gray-400 font-mono text-[10px] break-all">{createdWorkspace.path}</code>
2671
- </div>
2672
- </div>
2673
-
2674
- <div className="flex justify-center gap-4">
2675
- <button
2676
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2677
- onClick={() => {
2678
- setView('workspaces');
2679
- fetchWorkspaces();
2680
- setActiveWsId(branchName);
2681
- }}
2682
- >
2683
- <Terminal size={14} className="text-cyan-400" /> Manage Services
2684
- </button>
2685
- <button
2686
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-md shadow-indigo-500/10 transition-all cursor-pointer hover:-translate-y-0.5 active:translate-y-0"
2687
- onClick={() => handleOpenInEditor(createdWorkspace.path)}
2688
- >
2689
- <FolderOpen size={14} /> Open Editor
2690
- </button>
2691
- </div>
2692
- </div>
2693
- )}
2694
- </div>
2695
- )}
2696
-
2697
- {/* View 2: Active Workspaces Panel */}
2698
- {view === 'workspaces' && (
2699
- <WorkspaceList
2700
- workspacesLoading={workspacesLoading}
2701
- workspaces={workspaces}
2702
- activeWsId={activeWsId}
2703
- setActiveWsId={setActiveWsId}
2704
- resumingWs={resumingWs}
2705
- handleResumeSession={handleResumeSession}
2706
- handleCopyPrompt={handleCopyPrompt}
2707
- handleOpenInEditor={handleOpenInEditor}
2708
- fetchWorkspaces={fetchWorkspaces}
2709
- aiAssistants={aiAssistants}
2710
- repos={repos}
2711
- deleteWsLoading={deleteWsLoading}
2712
- addRepoLoading={addRepoLoading}
2713
- handleDeleteWorkspace={handleDeleteWorkspace}
2714
- handleAddRepo={handleAddRepo}
2715
- services={services}
2716
- runningServices={runningServices}
2717
- selectedLogService={selectedLogService}
2718
- serviceLogs={serviceLogs}
2719
- logsEndRef={logsEndRef}
2720
- setSelectedLogService={setSelectedLogService}
2721
- handleStartServices={handleStartServices}
2722
- handleStopServices={handleStopServices}
2723
- orchTools={orchTools}
2724
- servicesLoading={servicesLoading}
2725
- gitChanges={gitChanges}
2726
- gitChangesLoading={gitChangesLoading}
2727
- syncLoading={syncLoading}
2728
- syncResults={syncResults}
2729
- commitMessage={commitMessage}
2730
- showCommitModal={showCommitModal}
2731
- commitLoading={commitLoading}
2732
- commitResults={commitResults}
2733
- setSyncResults={setSyncResults}
2734
- setCommitResults={setCommitResults}
2735
- setCommitMessage={setCommitMessage}
2736
- setShowCommitModal={setShowCommitModal}
2737
- fetchGitChanges={fetchGitChanges}
2738
- handleSyncAll={handleSyncAll}
2739
- handleCommitAll={handleCommitAll}
2740
- knowledgeContent={knowledgeContent}
2741
- knowledgeLoading={knowledgeLoading}
2742
- isEditingKnowledge={isEditingKnowledge}
2743
- editedKnowledge={editedKnowledge}
2744
- saveKnowledgeLoading={saveKnowledgeLoading}
2745
- setEditedKnowledge={setEditedKnowledge}
2746
- setIsEditingKnowledge={setIsEditingKnowledge}
2747
- handleSaveKnowledge={handleSaveKnowledge}
2748
- planContent={planContent}
2749
- planLoading={planLoading}
2750
- sessions={sessions}
2751
- sessionsLoading={sessionsLoading}
2752
- activeSession={activeSession}
2753
- transcript={transcript}
2754
- transcriptLoading={transcriptLoading}
2755
- setActiveSession={setActiveSession}
2756
- setTranscript={setTranscript}
2757
- fetchSessionTranscript={fetchSessionTranscript}
2758
- subTab={subTab}
2759
- setSubTab={setSubTab}
2760
- />
2761
- )}
2762
-
2763
- {/* View: Workflows View */}
2764
- {view === 'workflows' && (
2765
- <div className="max-w-6xl mx-auto">
2766
- <header className="mb-8">
2767
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">Team Collaboration Strategies</h1>
2768
- <p className="text-sm text-gray-400 font-sans">Predefine and review custom teamwork guidelines injected into workspaces' AGENTS.md files.</p>
2769
- </header>
2770
-
2771
- <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 text-left">
2772
- {/* Left sidebar: Strategy List */}
2773
- <div className="lg:col-span-4 flex flex-col gap-4">
2774
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-5 shadow-md flex flex-col gap-4">
2775
- <div className="flex justify-between items-center">
2776
- <h3 className="text-xs font-semibold uppercase tracking-wider text-gray-500">Strategies</h3>
2777
- <button
2778
- className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold bg-indigo-500 hover:bg-indigo-600 text-white transition-all cursor-pointer shadow-md shadow-indigo-500/10"
2779
- onClick={() => {
2780
- setIsEditingTemplate(true);
2781
- setMgtTemplateName('New Strategy');
2782
- setMgtTemplateContent('# New Strategy\n\nWrite custom teamwork rules here for subagent orchestration...');
2783
- setSelectedMgtTemplateId(null);
2784
- setAnalysisResult(null);
2785
- }}
2786
- >
2787
- <PlusCircle size={12} /> Add New
2788
- </button>
2789
- </div>
2790
-
2791
- <div className="flex flex-col gap-2 max-h-[480px] overflow-y-auto pr-1">
2792
- {workflowTemplates.map((template) => {
2793
- const isSelected = selectedMgtTemplateId === template.id;
2794
- return (
2795
- <div
2796
- key={template.id}
2797
- className={`p-3 rounded-lg border text-left cursor-pointer transition-all flex flex-col gap-1.5 ${
2798
- isSelected
2799
- ? 'border-indigo-500 bg-indigo-500/5'
2800
- : 'border-gray-800/80 bg-gray-900/10 hover:border-gray-700/60 hover:bg-gray-800/10'
2801
- }`}
2802
- onClick={() => {
2803
- setSelectedMgtTemplateId(template.id);
2804
- setIsEditingTemplate(false);
2805
- setMgtTemplateName(template.name);
2806
- setMgtTemplateContent(template.content);
2807
- setAnalysisResult(null);
2808
- }}
2809
- >
2810
- <div className="flex items-center justify-between gap-2">
2811
- <span className={`text-xs font-bold ${isSelected ? 'text-indigo-400' : 'text-white'}`}>
2812
- {template.name}
2813
- </span>
2814
- <span
2815
- className={`text-[9px] px-1.5 py-0.5 rounded font-mono uppercase tracking-wider font-semibold border ${
2816
- template.custom
2817
- ? 'text-cyan-400 border-cyan-500/20 bg-cyan-500/5'
2818
- : 'text-gray-400 border-gray-800 bg-gray-800/20'
2819
- }`}
2820
- >
2821
- {template.custom ? 'Custom' : 'Built-in'}
2822
- </span>
2823
- </div>
2824
- <p className="text-[10px] text-gray-500 line-clamp-2 leading-relaxed font-sans">
2825
- {template.description}
2826
- </p>
2827
- </div>
2828
- );
2829
- })}
2830
- </div>
2831
- </div>
2832
- </div>
2833
-
2834
- {/* Right side: Strategy Detail / Edit Panel */}
2835
- <div className="lg:col-span-8 flex flex-col gap-6">
2836
- {(selectedMgtTemplateId || isEditingTemplate) ? (
2837
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-6 shadow-md flex flex-col gap-5">
2838
- <div className="flex justify-between items-start gap-4">
2839
- <div className="flex-1">
2840
- {isEditingTemplate ? (
2841
- <div className="flex flex-col gap-2">
2842
- <label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Strategy Name</label>
2843
- <input
2844
- type="text"
2845
- className="w-full bg-[#111827] border border-gray-850 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-2 text-white text-sm outline-none transition-all"
2846
- value={mgtTemplateName}
2847
- onChange={(e) => setMgtTemplateName(e.target.value)}
2848
- placeholder="e.g. Test-Driven Development"
2849
- />
2850
- </div>
2851
- ) : (
2852
- <div>
2853
- <h2 className="text-xl font-bold text-white flex items-center gap-2">
2854
- {mgtTemplateName}
2855
- {workflowTemplates.find(t => t.id === selectedMgtTemplateId)?.custom && (
2856
- <span className="text-[10px] px-2 py-0.5 rounded font-mono bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 uppercase">
2857
- Custom Template
2858
- </span>
2859
- )}
2860
- </h2>
2861
- <p className="text-xs text-gray-500 mt-1 font-sans">
2862
- {workflowTemplates.find(t => t.id === selectedMgtTemplateId)?.description || 'Custom strategy template.'}
2863
- </p>
2864
- </div>
2865
- )}
2866
- </div>
2867
-
2868
- <div className="flex gap-2">
2869
- {isEditingTemplate ? (
2870
- <>
2871
- <button
2872
- className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-bold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2873
- onClick={() => {
2874
- if (selectedMgtTemplateId) {
2875
- const original = workflowTemplates.find(t => t.id === selectedMgtTemplateId);
2876
- if (original) {
2877
- setMgtTemplateName(original.name);
2878
- setMgtTemplateContent(original.content);
2879
- }
2880
- setIsEditingTemplate(false);
2881
- } else {
2882
- setIsEditingTemplate(false);
2883
- if (workflowTemplates.length > 0) {
2884
- const first = workflowTemplates[0];
2885
- setSelectedMgtTemplateId(first.id);
2886
- setMgtTemplateName(first.name);
2887
- setMgtTemplateContent(first.content);
2888
- }
2889
- }
2890
- }}
2891
- >
2892
- Cancel
2893
- </button>
2894
- <button
2895
- className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold bg-indigo-500 hover:bg-indigo-600 text-white transition-all cursor-pointer shadow-md shadow-indigo-500/10"
2896
- onClick={handleSaveTemplate}
2897
- disabled={savingTemplate}
2898
- >
2899
- {savingTemplate ? 'Saving...' : 'Save Strategy'}
2900
- </button>
2901
- </>
2902
- ) : (
2903
- <>
2904
- {workflowTemplates.find(t => t.id === selectedMgtTemplateId)?.custom && (
2905
- <>
2906
- <button
2907
- className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-xs font-bold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2908
- onClick={() => setIsEditingTemplate(true)}
2909
- >
2910
- Edit
2911
- </button>
2912
- <button
2913
- className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-xs font-bold bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 text-rose-400 transition-all cursor-pointer"
2914
- onClick={() => handleDeleteTemplate(selectedMgtTemplateId!)}
2915
- disabled={deletingTemplate}
2916
- >
2917
- <Trash2 size={12} /> Delete
2918
- </button>
2919
- </>
2920
- )}
2921
- {!workflowTemplates.find(t => t.id === selectedMgtTemplateId)?.custom && (
2922
- <button
2923
- className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-xs font-bold bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white transition-all cursor-pointer"
2924
- onClick={() => {
2925
- setIsEditingTemplate(true);
2926
- setMgtTemplateName(`${mgtTemplateName} Copy`);
2927
- setMgtTemplateContent(mgtTemplateContent);
2928
- setSelectedMgtTemplateId(null);
2929
- setAnalysisResult(null);
2930
- }}
2931
- >
2932
- Duplicate & Customize
2933
- </button>
2934
- )}
2935
- </>
2936
- )}
2937
- </div>
2938
- </div>
2939
-
2940
- <div className="flex flex-col gap-2">
2941
- <label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Guidelines Markdown</label>
2942
- <textarea
2943
- className="w-full bg-[#111827] border border-gray-850 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-xs font-mono min-h-[250px] resize-y shadow-inner leading-relaxed"
2944
- value={mgtTemplateContent}
2945
- onChange={(e) => setMgtTemplateContent(e.target.value)}
2946
- disabled={!isEditingTemplate}
2947
- placeholder="Write cooperation guidelines in Markdown..."
2948
- />
2949
- </div>
2950
-
2951
- {!isEditingTemplate && selectedMgtTemplateId && (
2952
- <div className="border-t border-gray-800/60 pt-5 flex flex-col gap-4">
2953
- <div className="flex flex-col gap-4">
2954
- <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
2955
- <div className="flex flex-col">
2956
- <span className="text-xs font-bold text-white flex items-center gap-1.5">
2957
- <Sparkles size={14} className="text-indigo-400" /> AI Strategy Analysis
2958
- </span>
2959
- <span className="text-[10px] text-gray-500 mt-0.5 font-sans">Select an AI assistant harness installed on your system to inspect these guidelines.</span>
2960
- </div>
2961
-
2962
- <div className="flex items-center gap-3 w-full sm:w-auto justify-end">
2963
- <div className="flex items-center gap-2">
2964
- <span className="text-[10px] text-gray-400 font-semibold uppercase tracking-wider font-sans">Harness:</span>
2965
- <select
2966
- className="bg-gray-900 border border-gray-800 text-xs text-white rounded-lg px-2.5 py-1.5 focus:border-indigo-500 transition-all outline-none disabled:opacity-40"
2967
- value={selectedInspectAssistant}
2968
- onChange={(e) => setSelectedInspectAssistant(e.target.value)}
2969
- disabled={aiAssistants.filter(ai => ai.detected && ai.command).length === 0}
2970
- >
2971
- {aiAssistants.filter(ai => ai.detected && ai.command).length > 0 ? (
2972
- aiAssistants
2973
- .filter(ai => ai.detected && ai.command)
2974
- .map(ai => (
2975
- <option key={ai.name} value={ai.name}>
2976
- {ai.displayName}
2977
- </option>
2978
- ))
2979
- ) : (
2980
- <option value="">No Harness Found</option>
2981
- )}
2982
- </select>
2983
- </div>
2984
-
2985
- <button
2986
- className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold bg-indigo-500 hover:bg-indigo-600 text-white transition-all cursor-pointer shadow-md shadow-indigo-500/10 disabled:opacity-40"
2987
- onClick={() => handleAnalyzeTemplate(selectedMgtTemplateId, mgtTemplateContent, selectedInspectAssistant)}
2988
- disabled={analyzingTemplate || aiAssistants.filter(ai => ai.detected && ai.command).length === 0}
2989
- >
2990
- {analyzingTemplate ? (
2991
- <>
2992
- <RefreshCw className="animate-spin" size={12} /> Inspecting...
2993
- </>
2994
- ) : (
2995
- <>
2996
- <Cpu size={12} /> Inspect Strategy
2997
- </>
2998
- )}
2999
- </button>
3000
- </div>
3001
- </div>
3002
-
3003
- <div className="flex flex-col gap-2">
3004
- <label className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider text-left">Evaluation Focus / Instructions (Optional)</label>
3005
- <textarea
3006
- className="w-full bg-[#111827]/40 border border-gray-850 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-2 text-white placeholder-gray-600 transition-all outline-none text-xs min-h-[60px] resize-y leading-relaxed font-sans"
3007
- value={mgtAnalysisComment}
3008
- onChange={(e) => setMgtAnalysisComment(e.target.value)}
3009
- placeholder="e.g. Focus on checking if timeouts are handled well, check subagent roles coordination..."
3010
- disabled={analyzingTemplate}
3011
- />
3012
- </div>
3013
- </div>
3014
-
3015
- {analysisResult && (
3016
- <div className="flex flex-col gap-4">
3017
- <div className="bg-[#1e1e38]/20 border border-indigo-500/10 rounded-xl p-5 text-xs text-gray-300 leading-relaxed font-sans max-h-[300px] overflow-y-auto text-left whitespace-pre-wrap select-text">
3018
- {analysisResult}
3019
- </div>
3020
- {suggestedImprovement && (
3021
- <div className="flex justify-end">
3022
- <button
3023
- className="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-lg text-xs font-bold bg-green-600 hover:bg-green-700 text-white transition-all cursor-pointer shadow-md shadow-green-600/10"
3024
- onClick={() => {
3025
- setMgtTemplateContent(suggestedImprovement);
3026
- setIsEditingTemplate(true);
3027
- setSuggestedImprovement(null);
3028
- setAnalysisResult(null);
3029
- showToast('Suggested improvements applied! Click Save to persist changes.', 'success');
3030
- }}
3031
- >
3032
- <CheckCircle size={14} /> Apply Suggested Improvements
3033
- </button>
3034
- </div>
3035
- )}
3036
- </div>
3037
- )}
3038
- </div>
3039
- )}
3040
- </div>
3041
- ) : (
3042
- <div className="h-[400px] bg-[#111827]/10 border border-gray-800/60 border-dashed rounded-xl flex flex-col items-center justify-center text-center p-6">
3043
- <FolderOpen size={36} className="text-gray-600 mb-3" />
3044
- <span className="text-sm font-semibold text-gray-400">No strategy template selected</span>
3045
- <p className="text-xs text-gray-500 mt-1 max-w-sm font-sans">
3046
- Select a template from the list to view, edit, or analyze it, or add a new custom teamwork workflow template.
3047
- </p>
3048
- </div>
3049
- )}
3050
- </div>
3051
- </div>
3052
- </div>
3053
- )}
3054
-
3055
- {/* View 3: Settings View */}
3056
- {view === 'settings' && config && (
3057
- <div className="max-w-4xl mx-auto">
3058
- <header className="mb-8">
3059
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">Global Settings</h1>
3060
- <p className="text-sm text-gray-400">Configure your local directories, search parameters, and editor defaults.</p>
3061
- </header>
3062
-
3063
- {saveStatus === 'success' && (
3064
- <div className="flex items-center gap-3 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 rounded-xl p-4 mb-6 text-sm">
3065
- <Check size={18} /> Settings successfully saved and updated!
3066
- </div>
3067
- )}
3068
- {saveStatus === 'error' && (
3069
- <div className="flex items-center gap-3 bg-rose-500/10 border border-rose-500/20 text-rose-400 rounded-xl p-4 mb-6 text-sm">
3070
- <AlertTriangle size={18} /> Error: Could not save configuration details to disk.
3071
- </div>
3072
- )}
3073
-
3074
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm mb-6">
3075
- <div className="flex flex-col">
3076
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Development Directory</label>
3077
- <input
3078
- type="text"
3079
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner"
3080
- value={config.devDir}
3081
- onChange={(e) => setConfig({ ...config, devDir: e.target.value })}
3082
- />
3083
- <span className="text-[10px] text-gray-500 mt-1">Directory where your git projects are scanned.</span>
3084
- </div>
3085
-
3086
- <div className="flex flex-col">
3087
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Workspaces Directory</label>
3088
- <input
3089
- type="text"
3090
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner"
3091
- value={config.workspacesDir}
3092
- onChange={(e) => setConfig({ ...config, workspacesDir: e.target.value })}
3093
- />
3094
- <span className="text-[10px] text-gray-500 mt-1">Directory where unified worktree environments are spun up.</span>
3095
- </div>
3096
-
3097
- <div className="flex flex-col">
3098
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Repo Search Depth</label>
3099
- <input
3100
- type="number"
3101
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner"
3102
- min={1}
3103
- max={5}
3104
- value={config.scanDepth}
3105
- onChange={(e) => setConfig({ ...config, scanDepth: parseInt(e.target.value, 10) })}
3106
- />
3107
- <span className="text-[10px] text-gray-500 mt-1">Max directory levels deep the system will traverse for git repos.</span>
3108
- </div>
3109
-
3110
- <div className="flex flex-col">
3111
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Default Assistant</label>
3112
- <select
3113
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white transition-all outline-none text-sm shadow-inner cursor-pointer"
3114
- value={config.defaultAssistant || ''}
3115
- onChange={(e) => setConfig({ ...config, defaultAssistant: e.target.value || null })}
3116
- >
3117
- <option value="">None (Prompt me)</option>
3118
- <option value="claude">Claude Code</option>
3119
- <option value="antigravity">Antigravity</option>
3120
- <option value="codex">Codex</option>
3121
- <option value="copilot">GitHub Copilot</option>
3122
- <option value="cursor">Cursor</option>
3123
- </select>
3124
- <span className="text-[10px] text-gray-500 mt-1">Your preferred workspace context manager.</span>
3125
- </div>
3126
-
3127
- <div className="flex flex-col">
3128
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Default Editor</label>
3129
- <select
3130
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white transition-all outline-none text-sm shadow-inner cursor-pointer"
3131
- value={config.defaultEditor || ''}
3132
- onChange={(e) => setConfig({ ...config, defaultEditor: e.target.value || null })}
3133
- >
3134
- <option value="">None (Skip opening)</option>
3135
- {editors.map((ed) => (
3136
- <option key={ed.command} value={ed.command}>
3137
- {ed.name} {ed.detected ? '(Detected)' : '(Not found)'}
3138
- </option>
3139
- ))}
3140
- </select>
3141
- <span className="text-[10px] text-gray-500 mt-1">Your preferred code editor for opening workspaces.</span>
3142
- </div>
3143
-
3144
- <div className="flex flex-col md:col-span-2 border-t border-gray-800/60 pt-6 mt-2">
3145
- <h3 className="text-sm font-bold text-white mb-2">Workspace Storage Settings</h3>
3146
- <div className="flex flex-col gap-1.5">
3147
- <label htmlFor="storageProvider" className="text-xs font-semibold text-white select-none">
3148
- Storage Provider
3149
- </label>
3150
- <select
3151
- id="storageProvider"
3152
- className="w-full max-w-md text-xs bg-[#111827] border border-gray-800 rounded-lg p-2 text-white focus:outline-none focus:border-indigo-500"
3153
- value={config.storageProvider || 'local'}
3154
- onChange={(e) => {
3155
- const val = e.target.value;
3156
- const newConf = { ...config, storageProvider: val };
3157
- const selectedAdapter = adapters.find(a => a.name === val);
3158
- if (selectedAdapter?.configFields?.length) {
3159
- if (!newConf.adapterConfig) newConf.adapterConfig = {};
3160
- if (!newConf.adapterConfig[val]) newConf.adapterConfig[val] = {};
3161
- selectedAdapter.configFields.forEach(f => {
3162
- if (newConf.adapterConfig![val][f.key] === undefined && f.default !== undefined) {
3163
- newConf.adapterConfig![val][f.key] = f.default;
3164
- }
3165
- });
3166
- }
3167
- setConfig(newConf);
3168
- }}
3169
- >
3170
- {adapters.length > 0 ? (
3171
- adapters.map((a) => (
3172
- <option key={a.name} value={a.name}>
3173
- {a.displayName}
3174
- </option>
3175
- ))
3176
- ) : (
3177
- <>
3178
- <option value="local">Local Workspace (Folders)</option>
3179
- <option value="central-vault">Obsidian Central Vault</option>
3180
- </>
3181
- )}
3182
- </select>
3183
- </div>
3184
- <span className="text-[10px] text-gray-500 mt-1">
3185
- {adapters.find(a => a.name === (config.storageProvider || 'local'))?.description ||
3186
- 'Choose where to store maps, plans, and knowledge files. Centralized vault keeps repositories 100% clean and allows Obsidian integration.'}
3187
- </span>
3188
-
3189
- {/* Dynamic config fields */}
3190
- {(() => {
3191
- const activeProv = config.storageProvider || 'local';
3192
- const selectedAdapter = adapters.find((a) => a.name === activeProv);
3193
- if (!selectedAdapter || !selectedAdapter.configFields || selectedAdapter.configFields.length === 0) {
3194
- return null;
3195
- }
3196
- return (
3197
- <div className="mt-4 p-4 rounded-lg bg-gray-950/40 border border-gray-800/80 max-w-md flex flex-col gap-4">
3198
- <h4 className="text-xs font-semibold text-gray-300 uppercase tracking-wider">
3199
- {selectedAdapter.displayName} Settings
3200
- </h4>
3201
- {selectedAdapter.configFields.map((field) => {
3202
- const value = config.adapterConfig?.[activeProv]?.[field.key] ?? field.default ?? '';
3203
- const updateField = (val: any) => {
3204
- const newAdapterConfig = {
3205
- ...(config.adapterConfig || {}),
3206
- [activeProv]: {
3207
- ...(config.adapterConfig?.[activeProv] || {}),
3208
- [field.key]: val,
3209
- },
3210
- };
3211
- setConfig({ ...config, adapterConfig: newAdapterConfig });
3212
- };
3213
-
3214
- if (field.type === 'boolean') {
3215
- return (
3216
- <label key={field.key} className="flex items-start gap-3 cursor-pointer select-none">
3217
- <input
3218
- type="checkbox"
3219
- className="mt-0.5 rounded border-gray-800 bg-gray-950 text-indigo-500 focus:ring-0 focus:ring-offset-0"
3220
- checked={!!value}
3221
- onChange={(e) => updateField(e.target.checked)}
3222
- />
3223
- <div className="flex flex-col">
3224
- <span className="text-xs font-medium text-slate-200">{field.label}</span>
3225
- {field.description && (
3226
- <span className="text-[10px] text-gray-500 leading-normal mt-0.5">{field.description}</span>
3227
- )}
3228
- </div>
3229
- </label>
3230
- );
3231
- }
3232
-
3233
- return (
3234
- <div key={field.key} className="flex flex-col gap-1">
3235
- <label className="text-[11px] font-semibold text-slate-300">{field.label}</label>
3236
- <input
3237
- type={field.type === 'number' ? 'number' : 'text'}
3238
- className="w-full text-xs bg-slate-950 border border-gray-800 rounded p-2 text-white focus:outline-none focus:border-indigo-500"
3239
- value={value}
3240
- onChange={(e) => {
3241
- const val = field.type === 'number' ? Number(e.target.value) : e.target.value;
3242
- updateField(val);
3243
- }}
3244
- />
3245
- {field.description && (
3246
- <span className="text-[10px] text-gray-500 leading-normal mt-0.5">{field.description}</span>
3247
- )}
3248
- </div>
3249
- );
3250
- })}
3251
- </div>
3252
- );
3253
- })()}
3254
- </div>
3255
- </div>
3256
-
3257
- {/* Local AI Co-Processor Settings */}
3258
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm mb-6 mt-6">
3259
- <h3 className="text-lg font-bold text-white mb-2">Local AI Co-Processor Settings</h3>
3260
- <p className="text-xs text-gray-450 mb-6 font-semibold">Enable a local LLM to handle simple tasks like log analysis, git diff summaries, and boilerplate code generation without using remote tokens.</p>
3261
-
3262
- {recommendation && (
3263
- <div className="bg-[#0c1020]/80 border border-indigo-500/20 text-xs text-gray-300 rounded-xl p-4 mb-6 flex flex-col gap-1.5">
3264
- <div className="flex items-center gap-2 text-indigo-400 font-bold">
3265
- <Cpu size={14} /> Local System Scan Results
3266
- </div>
3267
- <div>• Detected Memory: <strong>{recommendation.totalRamGb} GB RAM</strong></div>
3268
- <div>• Detected GPU: <strong>{recommendation.gpuName}</strong></div>
3269
- <div>• Recommended Model: <span className="bg-indigo-500/10 text-indigo-300 px-2 py-0.5 rounded font-mono text-[11px] border border-indigo-500/20">{recommendation.recommendedModel}</span></div>
3270
- </div>
3271
- )}
3272
-
3273
- <div className="flex items-center gap-3 mb-6">
3274
- <input
3275
- type="checkbox"
3276
- id="localLlmEnabled"
3277
- className="w-4 h-4 rounded border-gray-800 bg-[#111827] text-indigo-600 focus:ring-indigo-500 cursor-pointer"
3278
- checked={config.localLlm?.enabled || false}
3279
- onChange={(e) => {
3280
- const defaultLlm = { enabled: e.target.checked, provider: 'ollama' as const, endpoint: 'http://localhost:11434', model: recommendation?.recommendedModel || 'qwen2.5-coder:1.5b' };
3281
- setConfig({
3282
- ...config,
3283
- localLlm: config.localLlm ? { ...config.localLlm, enabled: e.target.checked } : defaultLlm
3284
- });
3285
- }}
3286
- />
3287
- <label htmlFor="localLlmEnabled" className="text-sm font-semibold text-white cursor-pointer select-none">
3288
- Enable Local AI Delegation Tool
3289
- </label>
3290
- </div>
3291
-
3292
- {config.localLlm?.enabled && (
3293
- <div className="space-y-6 border-t border-gray-800/60 pt-6">
3294
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
3295
- <div className="flex flex-col">
3296
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Local Provider</label>
3297
- <select
3298
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white transition-all outline-none text-sm shadow-inner cursor-pointer"
3299
- value={config.localLlm.provider}
3300
- onChange={(e) => setConfig({
3301
- ...config,
3302
- localLlm: { ...config.localLlm!, provider: e.target.value as any }
3303
- })}
3304
- >
3305
- <option value="ollama">Ollama</option>
3306
- <option value="openai-compatible">OpenAI-Compatible (e.g. LM Studio)</option>
3307
- </select>
3308
- <span className="text-[10px] text-gray-500 mt-1">Provider protocol to connect to.</span>
3309
- </div>
3310
-
3311
- <div className="flex flex-col">
3312
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Endpoint URL</label>
3313
- <input
3314
- type="text"
3315
- className={`w-full bg-[#111827] border focus:ring-1 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner ${
3316
- config.localLlm.endpoint && !config.localLlm.endpoint.trim().startsWith('http://') && !config.localLlm.endpoint.trim().startsWith('https://')
3317
- ? 'border-rose-500/60 focus:border-rose-500 focus:ring-rose-500'
3318
- : 'border-gray-800 focus:border-indigo-500 focus:ring-indigo-500'
3319
- }`}
3320
- value={config.localLlm.endpoint}
3321
- onChange={(e) => setConfig({
3322
- ...config,
3323
- localLlm: { ...config.localLlm!, endpoint: e.target.value }
3324
- })}
3325
- />
3326
- {config.localLlm.endpoint && !config.localLlm.endpoint.trim().startsWith('http://') && !config.localLlm.endpoint.trim().startsWith('https://') && (
3327
- <span className="text-[10px] text-rose-400 mt-1">Endpoint must start with http:// or https://</span>
3328
- )}
3329
- <span className="text-[10px] text-gray-500 mt-1">API base address of the local runner.</span>
3330
- </div>
3331
-
3332
- <div className="flex flex-col md:col-span-2">
3333
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Model Name</label>
3334
- <input
3335
- type="text"
3336
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-600 transition-all outline-none text-sm shadow-inner"
3337
- value={config.localLlm.model}
3338
- onChange={(e) => setConfig({
3339
- ...config,
3340
- localLlm: { ...config.localLlm!, model: e.target.value }
3341
- })}
3342
- />
3343
- <span className="text-[10px] text-gray-500 mt-1">Exact name of the model registered on the server (e.g., <code>qwen2.5-coder:1.5b</code>).</span>
3344
- </div>
3345
-
3346
- {config.localLlm.provider === 'openai-compatible' && (
3347
- <div className="flex flex-col md:col-span-2">
3348
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">API Key (Optional)</label>
3349
- <input
3350
- type="password"
3351
- className="w-full bg-[#111827] border border-gray-800 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 rounded-lg px-4 py-3 text-white placeholder-gray-605 transition-all outline-none text-sm shadow-inner"
3352
- value={config.localLlm.apiKey || ''}
3353
- placeholder="sk-..."
3354
- onChange={(e) => setConfig({
3355
- ...config,
3356
- localLlm: { ...config.localLlm!, apiKey: e.target.value }
3357
- })}
3358
- />
3359
- <span className="text-[10px] text-gray-500 mt-1">Bearer token for authenticating with cloud LLM providers (e.g. OpenAI, DeepSeek, OpenRouter).</span>
3360
- </div>
3361
- )}
3362
- </div>
3363
-
3364
- <div className="flex items-center justify-between pt-4 border-t border-gray-800/40">
3365
- <button
3366
- type="button"
3367
- disabled={testingLlm}
3368
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 border border-indigo-500/20 rounded-lg text-xs font-semibold bg-indigo-500/5 hover:bg-indigo-500/10 text-indigo-400 transition-all cursor-pointer disabled:opacity-40"
3369
- onClick={testLlmConnection}
3370
- >
3371
- {testingLlm ? 'Testing...' : 'Test Connection'}
3372
- </button>
3373
-
3374
- {testStatus && (
3375
- <div className={`text-xs px-3 py-1.5 rounded-lg border ${
3376
- testStatus.success
3377
- ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
3378
- : 'bg-rose-500/10 border-rose-500/20 text-rose-400'
3379
- }`}>
3380
- {testStatus.message}
3381
- </div>
3382
- )}
3383
- </div>
3384
- </div>
3385
- )}
3386
- </div>
3387
-
3388
- <div className="flex justify-end">
3389
- <button
3390
- className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white shadow-lg shadow-indigo-500/20 transition-all cursor-pointer hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:translate-y-0"
3391
- disabled={!isSettingsFormValid}
3392
- onClick={() => saveAppConfig(config)}
3393
- >
3394
- Save Configuration
3395
- </button>
3396
- </div>
3397
-
3398
- {/* Toolchain Updates Section */}
3399
- <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm mt-6">
3400
- <div className="flex justify-between items-center mb-6">
3401
- <div>
3402
- <h3 className="text-lg font-bold text-white mb-1">AI Toolchain Updates</h3>
3403
- <p className="text-xs text-gray-555">Monitor and update CLI packages and assistants in your workflow.</p>
3404
- </div>
3405
- <button
3406
- onClick={() => fetchToolsStatus(true)}
3407
- disabled={toolsLoading}
3408
- className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-gray-800 hover:border-gray-700 bg-gray-950/20 text-xs font-semibold text-white transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
3409
- >
3410
- <RefreshCw size={13} className={toolsLoading ? 'animate-spin' : ''} />
3411
- {toolsLoading ? 'Checking...' : 'Check Now'}
3412
- </button>
3413
- </div>
3414
-
3415
- {toolsLoading && toolsStatus.length === 0 ? (
3416
- <div className="flex flex-col items-center justify-center py-8 gap-2 text-gray-400">
3417
- <RefreshCw className="animate-spin text-indigo-400" size={24} />
3418
- <span className="text-xs">Fetching registry version details...</span>
3419
- </div>
3420
- ) : (
3421
- <div className="space-y-4">
3422
- {toolsStatus.map((tool) => (
3423
- <div key={tool.id} className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-4 border border-gray-800/60 rounded-xl bg-gray-950/10">
3424
- <div className="min-w-0">
3425
- <h4 className="text-sm font-bold text-white flex items-center gap-2">
3426
- {tool.name}
3427
- <span className={`text-[9px] px-2 py-0.5 rounded font-semibold uppercase ${
3428
- tool.installed ? 'bg-emerald-500/10 text-emerald-400' : 'bg-gray-800 text-gray-500'
3429
- }`}>
3430
- {tool.installed ? 'Installed' : 'Not Installed'}
3431
- </span>
3432
- </h4>
3433
- <div className="flex items-center gap-4 mt-1.5 text-xs text-gray-500">
3434
- <span>Installed version: <code className="text-gray-300 font-mono text-[10px]">{tool.currentVersion}</code></span>
3435
- {tool.installed && (
3436
- <span>Latest: <code className="text-gray-300 font-mono text-[10px]">{tool.latestVersion}</code></span>
3437
- )}
3438
- </div>
3439
- <p className="text-[10px] text-gray-600 mt-1 font-mono">{tool.updateCmd}</p>
3440
- </div>
3441
-
3442
- <div className="shrink-0 flex items-center gap-3">
3443
- {tool.updateAvailable ? (
3444
- <button
3445
- onClick={() => handleUpdateTool(tool.id)}
3446
- disabled={updatingToolId !== null}
3447
- className="px-3.5 py-2 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-[#060813] font-bold text-xs rounded-lg transition-all shadow-md shadow-amber-500/10 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1.5"
3448
- >
3449
- {updatingToolId === tool.id ? (
3450
- <>
3451
- <RefreshCw className="animate-spin" size={12} /> Updating...
3452
- </>
3453
- ) : (
3454
- <>
3455
- <Sparkles size={12} /> Update Tool
3456
- </>
3457
- )}
3458
- </button>
3459
- ) : tool.installed ? (
3460
- <div className="flex items-center gap-2">
3461
- <span className="text-xs font-semibold text-emerald-400 flex items-center gap-1.5 bg-emerald-500/5 px-2.5 py-1.5 rounded-lg border border-emerald-500/10">
3462
- <Check size={12} /> Up to Date
3463
- </span>
3464
- <button
3465
- onClick={() => handleUpdateTool(tool.id)}
3466
- disabled={updatingToolId !== null}
3467
- className="px-3.5 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-750 text-gray-300 font-bold text-xs rounded-lg transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1.5"
3468
- >
3469
- {updatingToolId === tool.id ? (
3470
- <>
3471
- <RefreshCw className="animate-spin" size={12} /> Reinstalling...
3472
- </>
3473
- ) : (
3474
- 'Reinstall'
3475
- )}
3476
- </button>
3477
- </div>
3478
- ) : (
3479
- <button
3480
- onClick={() => handleUpdateTool(tool.id)}
3481
- disabled={updatingToolId !== null}
3482
- className="px-3.5 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 text-white font-bold text-xs rounded-lg transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
3483
- >
3484
- {updatingToolId === tool.id ? (
3485
- <>
3486
- <RefreshCw className="animate-spin" size={12} /> Installing...
3487
- </>
3488
- ) : (
3489
- 'Install CLI'
3490
- )}
3491
- </button>
3492
- )}
3493
- </div>
3494
- </div>
3495
- ))}
3496
- </div>
3497
- )}
3498
- </div>
3499
- </div>
3500
- )}
3501
- </>
3502
- )}
3503
- </main>
3504
-
3505
- {/* Transcript Modal Overlay */}
3506
- {activeSession && (
3507
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4">
3508
- <div className="bg-[#0b0f19] border border-gray-800 rounded-2xl w-full max-w-4xl h-[80vh] flex flex-col shadow-2xl overflow-hidden animate-fadeIn">
3509
- {/* Modal Header */}
3510
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-800 bg-gray-950/40">
3511
- <div>
3512
- <div className="flex items-center gap-2 mb-1">
3513
- <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
3514
- activeSession.assistant === 'antigravity' ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' :
3515
- activeSession.assistant === 'claude' ? 'bg-amber-500/20 text-amber-300 border border-amber-500/30' :
3516
- activeSession.assistant === 'codex' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' :
3517
- 'bg-sky-500/20 text-sky-300 border border-sky-500/30'
3518
- }`}>
3519
- {activeSession.assistant === 'antigravity' ? 'Antigravity' :
3520
- activeSession.assistant === 'claude' ? 'Claude Code' :
3521
- activeSession.assistant === 'codex' ? 'OpenAI Codex' : 'GitHub Copilot'}
3522
- </span>
3523
- <span className="text-[10px] text-gray-500">Session: {activeSession.id}</span>
3524
- </div>
3525
- <h3 className="text-sm font-bold text-white truncate max-w-xl" title={activeSession.title}>
3526
- {activeSession.title}
3527
- </h3>
3528
- </div>
3529
- <button
3530
- className="text-gray-400 hover:text-white p-2 hover:bg-gray-800/80 rounded-lg transition-colors cursor-pointer"
3531
- onClick={() => setActiveSession(null)}
3532
- >
3533
- <X size={18} />
3534
- </button>
3535
- </div>
3536
-
3537
- {/* Modal Body / Chat Messages */}
3538
- <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-gray-950/10">
3539
- {transcriptLoading ? (
3540
- <div className="flex flex-col items-center justify-center h-full gap-2">
3541
- <RefreshCw className="animate-spin text-indigo-400" size={24} />
3542
- <span className="text-xs text-gray-500 font-medium">Loading conversation history...</span>
3543
- </div>
3544
- ) : transcript.length === 0 ? (
3545
- <div className="flex items-center justify-center h-full text-xs text-gray-500">
3546
- No messages found in this transcript.
3547
- </div>
3548
- ) : (
3549
- transcript.map((msg, idx) => (
3550
- <div key={idx} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
3551
- <div className="text-[10px] text-gray-500 mb-1 px-1">
3552
- {msg.role === 'user' ? 'Developer' : activeSession.assistant === 'antigravity' ? 'Antigravity' : activeSession.assistant === 'claude' ? 'Claude' : activeSession.assistant === 'codex' ? 'Codex' : 'Copilot'}
3553
- {msg.timestamp && ` • ${new Date(msg.timestamp).toLocaleTimeString()}`}
3554
- </div>
3555
- <div className={`max-w-[85%] rounded-2xl px-4 py-3 text-xs leading-relaxed whitespace-pre-wrap ${
3556
- msg.role === 'user'
3557
- ? 'bg-indigo-650 text-white rounded-tr-none shadow-md border border-indigo-500/10'
3558
- : 'bg-gray-900 border border-gray-800 text-gray-200 rounded-tl-none shadow-sm font-sans'
3559
- }`}>
3560
- {msg.content}
3561
- </div>
3562
- </div>
3563
- ))
3564
- )}
3565
- </div>
3566
-
3567
- {/* Modal Footer */}
3568
- <div className="px-6 py-4 border-t border-gray-800 bg-gray-950/40 flex justify-between items-center">
3569
- <span className="text-[11px] text-gray-550">
3570
- Resuming will copy the shell command and open your code editor.
3571
- </span>
3572
- <div className="flex gap-2">
3573
- <button
3574
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 rounded-lg text-xs font-bold text-white transition-all cursor-pointer"
3575
- onClick={() => {
3576
- const cmd = activeSession.assistant === 'antigravity' ? `agy --conversation ${activeSession.id}` :
3577
- activeSession.assistant === 'claude' ? `claude --resume ${activeSession.id}` :
3578
- activeSession.assistant === 'codex' ? `codex resume ${activeSession.id}` :
3579
- `copilot --resume ${activeSession.id}`;
3580
- navigator.clipboard.writeText(cmd);
3581
- showToast(`Copied run command to clipboard:\n\n${cmd}`, 'success');
3582
- }}
3583
- >
3584
- <Copy size={13} /> Copy Resume Command
3585
- </button>
3586
- <button
3587
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-all cursor-pointer"
3588
- onClick={() => {
3589
- const ws = workspaces.find(w => w.branchName === activeSession.workspacePath.split(/[\\/]/).pop());
3590
- handleResumeSession(ws || workspaces[0], activeSession.id, activeSession.assistant);
3591
- setActiveSession(null);
3592
- }}
3593
- >
3594
- <Play size={12} /> Resume Conversation
3595
- </button>
3596
- </div>
3597
- </div>
3598
- </div>
3599
- </div>
3600
- )}
3601
-
3602
- {/* Toast Notifications Container */}
3603
- <div className="fixed bottom-5 right-5 z-[9999] flex flex-col gap-3 pointer-events-none max-w-md w-full">
3604
- {toasts.map((toast) => (
3605
- <div
3606
- key={toast.id}
3607
- className={`pointer-events-auto flex items-start justify-between gap-3 px-4 py-3 rounded-xl border shadow-2xl transition-all duration-300 animate-slide-in ${
3608
- toast.type === 'success'
3609
- ? 'bg-[#062c1b]/95 border-emerald-800/80 text-emerald-100'
3610
- : toast.type === 'error'
3611
- ? 'bg-[#2c0e0e]/95 border-red-900/80 text-red-100'
3612
- : 'bg-[#131926]/95 border-slate-800/80 text-slate-100'
3613
- }`}
3614
- >
3615
- <div className="flex items-start gap-2.5 text-xs font-semibold flex-1">
3616
- {toast.type === 'success' && <Check className="text-emerald-400 shrink-0 mt-0.5" size={16} />}
3617
- {toast.type === 'error' && <AlertCircle className="text-red-400 shrink-0 mt-0.5" size={16} />}
3618
- {toast.type === 'info' && <Sparkles className="text-indigo-400 shrink-0 mt-0.5" size={16} />}
3619
- <span className="whitespace-pre-line text-left leading-relaxed">{toast.message}</span>
3620
- </div>
3621
- <button
3622
- onClick={() => setToasts((prev) => prev.filter((t) => t.id !== toast.id))}
3623
- className="text-slate-400 hover:text-white transition-colors cursor-pointer shrink-0 mt-0.5"
3624
- >
3625
- <X size={14} />
3626
- </button>
3627
- </div>
3628
- ))}
3629
- </div>
3630
- </div>
3631
- );
3632
- }