@mrpatronz/nexusflow 0.2.6 → 0.2.8

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 (103) hide show
  1. package/.github/workflows/release.yml +7 -0
  2. package/dist/commands/create.d.ts.map +1 -1
  3. package/dist/commands/create.js +6 -1
  4. package/dist/commands/create.js.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +58 -0
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +73 -1
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/core/config.d.ts.map +1 -1
  12. package/dist/core/config.js +11 -1
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/config.test.d.ts +2 -0
  15. package/dist/core/config.test.d.ts.map +1 -0
  16. package/dist/core/config.test.js +75 -0
  17. package/dist/core/config.test.js.map +1 -0
  18. package/dist/core/scanner.test.d.ts +2 -0
  19. package/dist/core/scanner.test.d.ts.map +1 -0
  20. package/dist/core/scanner.test.js +53 -0
  21. package/dist/core/scanner.test.js.map +1 -0
  22. package/dist/core/worktree.d.ts.map +1 -1
  23. package/dist/core/worktree.js +19 -11
  24. package/dist/core/worktree.js.map +1 -1
  25. package/dist/generators/base.d.ts.map +1 -1
  26. package/dist/generators/base.js +8 -2
  27. package/dist/generators/base.js.map +1 -1
  28. package/dist/generators/skills-generator.js +1 -1
  29. package/dist/generators/skills-generator.js.map +1 -1
  30. package/dist/gui/assets/index-Ca2VRMrQ.js +22 -0
  31. package/dist/gui/assets/index-fj0RJiOb.css +2 -0
  32. package/dist/gui/index.html +2 -2
  33. package/dist/mcp/server.d.ts.map +1 -1
  34. package/dist/mcp/server.js +132 -93
  35. package/dist/mcp/server.js.map +1 -1
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +132 -11
  38. package/dist/server.js.map +1 -1
  39. package/dist/server.test.d.ts +2 -0
  40. package/dist/server.test.d.ts.map +1 -0
  41. package/dist/server.test.js +318 -0
  42. package/dist/server.test.js.map +1 -0
  43. package/dist/types.d.ts +12 -0
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/utils/detect-ai.test.d.ts +2 -0
  46. package/dist/utils/detect-ai.test.d.ts.map +1 -0
  47. package/dist/utils/detect-ai.test.js +37 -0
  48. package/dist/utils/detect-ai.test.js.map +1 -0
  49. package/dist/utils/detect-editors.test.d.ts +2 -0
  50. package/dist/utils/detect-editors.test.d.ts.map +1 -0
  51. package/dist/utils/detect-editors.test.js +39 -0
  52. package/dist/utils/detect-editors.test.js.map +1 -0
  53. package/dist/utils/git.test.d.ts +2 -0
  54. package/dist/utils/git.test.d.ts.map +1 -0
  55. package/dist/utils/git.test.js +55 -0
  56. package/dist/utils/git.test.js.map +1 -0
  57. package/dist/utils/local-ai.d.ts +27 -0
  58. package/dist/utils/local-ai.d.ts.map +1 -0
  59. package/dist/utils/local-ai.js +96 -0
  60. package/dist/utils/local-ai.js.map +1 -0
  61. package/dist/utils/local-ai.test.d.ts +2 -0
  62. package/dist/utils/local-ai.test.d.ts.map +1 -0
  63. package/dist/utils/local-ai.test.js +108 -0
  64. package/dist/utils/local-ai.test.js.map +1 -0
  65. package/dist/utils/system-scanner.d.ts +16 -0
  66. package/dist/utils/system-scanner.d.ts.map +1 -0
  67. package/dist/utils/system-scanner.js +85 -0
  68. package/dist/utils/system-scanner.js.map +1 -0
  69. package/dist/utils/system-scanner.test.d.ts +2 -0
  70. package/dist/utils/system-scanner.test.d.ts.map +1 -0
  71. package/dist/utils/system-scanner.test.js +73 -0
  72. package/dist/utils/system-scanner.test.js.map +1 -0
  73. package/gui/e2e/wizard.spec.ts +327 -0
  74. package/gui/package-lock.json +64 -0
  75. package/gui/package.json +4 -1
  76. package/gui/playwright.config.ts +41 -0
  77. package/gui/src/App.tsx +376 -786
  78. package/gui/src/index.css +16 -0
  79. package/package.json +3 -1
  80. package/scripts/simulate-workspaces.ts +55 -0
  81. package/src/commands/create.ts +7 -1
  82. package/src/commands/doctor.ts +56 -0
  83. package/src/commands/init.ts +77 -1
  84. package/src/core/config.test.ts +96 -0
  85. package/src/core/config.ts +11 -1
  86. package/src/core/scanner.test.ts +61 -0
  87. package/src/core/worktree.ts +19 -10
  88. package/src/generators/base.ts +13 -2
  89. package/src/generators/skills-generator.ts +1 -1
  90. package/src/mcp/server.ts +144 -95
  91. package/src/server.test.ts +367 -0
  92. package/src/server.ts +139 -10
  93. package/src/types.ts +16 -0
  94. package/src/utils/detect-ai.test.ts +44 -0
  95. package/src/utils/detect-editors.test.ts +47 -0
  96. package/src/utils/git.test.ts +74 -0
  97. package/src/utils/local-ai.test.ts +130 -0
  98. package/src/utils/local-ai.ts +111 -0
  99. package/src/utils/system-scanner.test.ts +89 -0
  100. package/src/utils/system-scanner.ts +96 -0
  101. package/vitest.config.ts +2 -1
  102. package/dist/gui/assets/index-BgEo5w2Q.js +0 -22
  103. package/dist/gui/assets/index-D-VigurY.css +0 -2
package/gui/src/App.tsx CHANGED
@@ -5,8 +5,6 @@ import {
5
5
  Settings as SettingsIcon,
6
6
  Terminal,
7
7
  Play,
8
- Square,
9
- ExternalLink,
10
8
  Check,
11
9
  AlertTriangle,
12
10
  AlertCircle,
@@ -18,19 +16,20 @@ import {
18
16
  Sparkles,
19
17
  Copy,
20
18
  X,
21
- MessageSquare,
22
- MessageSquareCode,
23
- BookOpen,
24
- ListOrdered,
25
- Save,
26
- Edit,
27
- Download,
28
- Trash2,
19
+ Cpu,
29
20
  } from 'lucide-react';
30
21
  import './App.css';
22
+ import { WorkspaceList } from './features/workspace/WorkspaceList.js';
31
23
 
32
24
 
33
25
  // Types matched with src/types.ts
26
+ interface LocalLlmConfig {
27
+ enabled: boolean;
28
+ provider: 'ollama' | 'openai-compatible';
29
+ endpoint: string;
30
+ model: string;
31
+ }
32
+
34
33
  interface NexusFlowConfig {
35
34
  version: string;
36
35
  devDir: string;
@@ -38,6 +37,8 @@ interface NexusFlowConfig {
38
37
  defaultAssistant: string | null;
39
38
  defaultEditor?: string | null;
40
39
  scanDepth: number;
40
+ localLlm?: LocalLlmConfig;
41
+ packContextXml?: boolean;
41
42
  }
42
43
 
43
44
  interface DetectedAI {
@@ -95,90 +96,24 @@ interface RunningService {
95
96
  const API_BASE = import.meta.env.DEV ? 'http://localhost:3000' : '';
96
97
  const isVsCode = new URLSearchParams(window.location.search).get('env') === 'vscode';
97
98
 
98
- const parseInlineStyles = (text: string): React.ReactNode => {
99
- const tokenRegex = /(\*\*.*?\*\*|`.*?`|\*.*?\*)/g;
100
- const parts = text.split(tokenRegex);
101
-
102
- return parts.map((part, index) => {
103
- if (part.startsWith('**') && part.endsWith('**')) {
104
- return <strong key={index} className="font-bold text-gray-200">{part.slice(2, -2)}</strong>;
105
- }
106
- if (part.startsWith('`') && part.endsWith('`')) {
107
- return <code key={index} className="px-1.5 py-0.5 bg-gray-900 border border-gray-800 text-indigo-300 rounded font-mono text-[10px]">{part.slice(1, -1)}</code>;
108
- }
109
- if (part.startsWith('*') && part.endsWith('*')) {
110
- return <em key={index} className="italic text-gray-300">{part.slice(1, -1)}</em>;
111
- }
112
- return part;
113
- });
114
- };
115
-
116
- const renderFormattedDescription = (text: string) => {
117
- if (!text) return null;
118
-
119
- const lines = text.split('\n');
120
- const elements: React.ReactNode[] = [];
121
- let currentList: { type: 'ul' | 'ol'; items: string[] } | null = null;
122
-
123
- const flushList = (key: number) => {
124
- if (currentList) {
125
- const ListTag = currentList.type;
126
- elements.push(
127
- <ListTag key={`list-${key}`} className={currentList.type === 'ul' ? 'list-disc pl-5 my-1.5 space-y-1' : 'list-decimal pl-5 my-1.5 space-y-1'}>
128
- {currentList.items.map((item, idx) => (
129
- <li key={`item-${idx}`} className="leading-relaxed">{parseInlineStyles(item)}</li>
130
- ))}
131
- </ListTag>
132
- );
133
- currentList = null;
134
- }
135
- };
136
-
137
- lines.forEach((line, idx) => {
138
- const trimmed = line.trim();
139
-
140
- // Check for bullet list item
141
- const bulletMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
142
- if (bulletMatch) {
143
- if (!currentList || currentList.type !== 'ul') {
144
- flushList(idx);
145
- currentList = { type: 'ul', items: [] };
146
- }
147
- currentList.items.push(bulletMatch[2]);
148
- return;
149
- }
150
-
151
- // Check for numbered list item
152
- const numberMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
153
- if (numberMatch) {
154
- if (!currentList || currentList.type !== 'ol') {
155
- flushList(idx);
156
- currentList = { type: 'ol', items: [] };
157
- }
158
- currentList.items.push(numberMatch[2]);
159
- return;
160
- }
161
-
162
- // If not a list item, flush any active list first
163
- flushList(idx);
164
-
165
- if (trimmed === '') {
166
- elements.push(<div key={`empty-${idx}`} className="h-2" />);
167
- } else {
168
- elements.push(
169
- <div key={`line-${idx}`} className="leading-relaxed">
170
- {parseInlineStyles(line)}
171
- </div>
172
- );
173
- }
174
- });
175
-
176
- flushList(lines.length);
177
- return <div className="space-y-1">{elements}</div>;
178
- };
179
-
180
99
  export default function App() {
181
100
  const [view, setView] = useState<'guide' | 'create' | 'workspaces' | 'settings'>('guide');
101
+
102
+ // Toast State
103
+ interface Toast {
104
+ id: string;
105
+ message: string;
106
+ type: 'success' | 'error' | 'info';
107
+ duration?: number;
108
+ }
109
+ const [toasts, setToasts] = useState<Toast[]>([]);
110
+ const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info', duration = 5000) => {
111
+ const id = Math.random().toString(36).substring(2, 9);
112
+ setToasts((prev) => [...prev, { id, message, type, duration }]);
113
+ setTimeout(() => {
114
+ setToasts((prev) => prev.filter((t) => t.id !== id));
115
+ }, duration);
116
+ };
182
117
 
183
118
  // App Config
184
119
  const [config, setConfig] = useState<NexusFlowConfig | null>(null);
@@ -209,6 +144,7 @@ export default function App() {
209
144
  const [selectedRepos, setSelectedRepos] = useState<RepoInfo[]>([]);
210
145
  const [selectedAI, setSelectedAI] = useState<string[]>([]);
211
146
  const [selectedEditor, setSelectedEditor] = useState<DetectedEditor | null>(null);
147
+ const [localLlmEnabled, setLocalLlmEnabled] = useState(false);
212
148
  const [creating, setCreating] = useState(false);
213
149
  const [createdWorkspace, setCreatedWorkspace] = useState<{ path: string } | null>(null);
214
150
  const [creationError, setCreationError] = useState<string | null>(null);
@@ -284,6 +220,23 @@ export default function App() {
284
220
  const [serviceLogs, setServiceLogs] = useState<string>('');
285
221
  const logsEndRef = useRef<HTMLDivElement | null>(null);
286
222
 
223
+ // Local LLM states
224
+ const [recommendation, setRecommendation] = useState<{ totalRamGb: number; gpuName: string; recommendedModel: string } | null>(null);
225
+ const [testStatus, setTestStatus] = useState<{ success: boolean; message: string } | null>(null);
226
+ const [testingLlm, setTestingLlm] = useState(false);
227
+
228
+ const isSettingsFormValid = (() => {
229
+ if (!config) return false;
230
+ if (!config.devDir || config.devDir.trim() === '' || !config.workspacesDir || config.workspacesDir.trim() === '') return false;
231
+ if (config.localLlm?.enabled) {
232
+ const endpoint = config.localLlm.endpoint?.trim() || '';
233
+ if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) {
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ })();
239
+
287
240
  // Initial loads
288
241
  useEffect(() => {
289
242
  fetchConfig();
@@ -292,10 +245,11 @@ export default function App() {
292
245
  fetchUpdateStatus();
293
246
  }, []);
294
247
 
295
- // Load tool updates status when settings view is open
248
+ // Load tool updates status and LLM recommendations when settings view is open
296
249
  useEffect(() => {
297
250
  if (view === 'settings') {
298
251
  fetchToolsStatus();
252
+ fetchLlmRecommendation();
299
253
  }
300
254
  }, [view]);
301
255
 
@@ -406,26 +360,62 @@ export default function App() {
406
360
  });
407
361
  const data = await res.json();
408
362
  if (res.ok && data.success) {
409
- alert(`${toolId} updated successfully!\n\nOutput:\n${data.output}`);
363
+ showToast(`${toolId} updated successfully!\n\nOutput:\n${data.output}`, 'success');
410
364
  fetchToolsStatus(true);
411
365
  fetchUpdateStatus();
412
366
  } else {
413
- alert(`Error: ${data.error || 'Failed to update tool'}`);
367
+ showToast(`Error: ${data.error || 'Failed to update tool'}`, 'error');
414
368
  }
415
369
  } catch (e) {
416
370
  console.error(e);
417
- alert('Network error when updating tool.');
371
+ showToast('Network error when updating tool.', 'error');
418
372
  } finally {
419
373
  setUpdatingToolId(null);
420
374
  }
421
375
  };
422
376
 
377
+ const fetchLlmRecommendation = async () => {
378
+ try {
379
+ const res = await fetch(`${API_BASE}/api/local-llm/recommend`);
380
+ if (res.ok) {
381
+ const data = await res.json();
382
+ setRecommendation(data);
383
+ }
384
+ } catch (e) {
385
+ console.error('Error fetching LLM recommendation:', e);
386
+ }
387
+ };
388
+
389
+ const testLlmConnection = async () => {
390
+ if (!config?.localLlm) return;
391
+ setTestingLlm(true);
392
+ setTestStatus(null);
393
+ try {
394
+ const res = await fetch(`${API_BASE}/api/local-llm/test`, {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/json' },
397
+ body: JSON.stringify(config.localLlm),
398
+ });
399
+ const data = await res.json();
400
+ if (data.success) {
401
+ setTestStatus({ success: true, message: data.message });
402
+ } else {
403
+ setTestStatus({ success: false, message: data.error || 'Connection failed.' });
404
+ }
405
+ } catch (e: any) {
406
+ setTestStatus({ success: false, message: e.message || 'Network error.' });
407
+ } finally {
408
+ setTestingLlm(false);
409
+ }
410
+ };
411
+
423
412
  const fetchConfig = async () => {
424
413
  try {
425
414
  const res = await fetch(`${API_BASE}/api/config`);
426
415
  const data = await res.json();
427
416
  setConfig(data.config);
428
417
  setConfigExists(data.exists);
418
+ setLocalLlmEnabled(data.config?.localLlm?.enabled || false);
429
419
 
430
420
  fetchEditors(data.config?.defaultEditor);
431
421
 
@@ -462,6 +452,7 @@ export default function App() {
462
452
  setSaveStatus('success');
463
453
  setConfig(newConfig);
464
454
  setConfigExists(true);
455
+ setLocalLlmEnabled(newConfig.localLlm?.enabled || false);
465
456
  fetchRepos();
466
457
  fetchWorkspaces();
467
458
  } else {
@@ -732,6 +723,7 @@ export default function App() {
732
723
  description,
733
724
  repos: selectedRepos,
734
725
  assistants: selectedAI,
726
+ localLlmEnabled,
735
727
  resumption: {
736
728
  testCommand,
737
729
  mockCommand: mockCommand || undefined,
@@ -852,15 +844,15 @@ export default function App() {
852
844
  setActiveWsId(ws.branchName);
853
845
  } else {
854
846
  navigator.clipboard.writeText(data.resumeCommand);
855
- alert(`Session Resumed!\n\n1. Editor launched.\n2. Command "${data.resumeCommand}" copied to clipboard! Paste it into your terminal inside the workspace to continue.`);
847
+ 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');
856
848
  setActiveWsId(ws.branchName);
857
849
  }
858
850
  } else {
859
- alert(`Error: ${data.error || 'Failed to resume session'}`);
851
+ showToast(`Error: ${data.error || 'Failed to resume session'}`, 'error');
860
852
  }
861
853
  } catch (e) {
862
854
  console.error(e);
863
- alert('Network error when resuming session.');
855
+ showToast('Network error when resuming session.', 'error');
864
856
  } finally {
865
857
  setResumingWs(null);
866
858
  }
@@ -872,7 +864,7 @@ export default function App() {
872
864
  return;
873
865
  }
874
866
  if (config?.defaultEditor === 'none') {
875
- alert('Your preferred editor is set to "None" (skip opening). You can change this in Settings.');
867
+ showToast('Your preferred editor is set to "None" (skip opening). You can change this in Settings.', 'info');
876
868
  return;
877
869
  }
878
870
  let editor = null;
@@ -883,7 +875,7 @@ export default function App() {
883
875
  editor = editors.find((e) => e.detected) || editors[0];
884
876
  }
885
877
  if (!editor) {
886
- alert('No detected editors available.');
878
+ showToast('No detected editors available.', 'error');
887
879
  return;
888
880
  }
889
881
  try {
@@ -900,18 +892,7 @@ export default function App() {
900
892
  }
901
893
  };
902
894
 
903
- const handleExportContext = async (wsBranchName: string) => {
904
- const encodedId = encodeURIComponent(wsBranchName);
905
- const downloadUrl = `${API_BASE}/api/workspace/${encodedId}/pack`;
906
-
907
- // Create temporary link and click it
908
- const link = document.createElement('a');
909
- link.href = downloadUrl;
910
- link.setAttribute('download', '');
911
- document.body.appendChild(link);
912
- link.click();
913
- document.body.removeChild(link);
914
- };
895
+
915
896
 
916
897
  const handleCopyPrompt = (ws: Feature) => {
917
898
  const repoNames = ws.repos.map((r) => r.split(/[\\/]/).pop()).join(', ');
@@ -929,7 +910,7 @@ Core Instructions:
929
910
  3. Follow all project-specific rules in "CLAUDE.md", ".cursorrules", or "AGENTS.md" in sub-repositories.
930
911
  `;
931
912
  navigator.clipboard.writeText(prompt);
932
- alert('Universal AI briefing prompt copied to clipboard!');
913
+ showToast('Universal AI briefing prompt copied to clipboard!', 'success');
933
914
  };
934
915
 
935
916
  const handleDeleteWorkspace = async (wsName: string) => {
@@ -948,13 +929,13 @@ Core Instructions:
948
929
  setActiveWsId(null);
949
930
  }
950
931
  await fetchWorkspaces();
951
- alert(`Workspace ${wsName} successfully deleted.`);
932
+ showToast(`Workspace ${wsName} successfully deleted.`, 'success');
952
933
  } else {
953
- alert(`Failed to delete workspace: ${data.error || 'Unknown error'}`);
934
+ showToast(`Failed to delete workspace: ${data.error || 'Unknown error'}`, 'error');
954
935
  }
955
936
  } catch (e) {
956
937
  console.error(e);
957
- alert('Network error while deleting workspace.');
938
+ showToast('Network error while deleting workspace.', 'error');
958
939
  } finally {
959
940
  setDeleteWsLoading(null);
960
941
  }
@@ -977,13 +958,13 @@ Core Instructions:
977
958
  fetchGitChanges(wsName);
978
959
  fetchWorkspaceSessions(wsName);
979
960
  }
980
- alert('Repository successfully added. Configurations and Repomix packing updated.');
961
+ showToast('Repository successfully added. Configurations and Repomix packing updated.', 'success');
981
962
  } else {
982
- alert(`Failed to add repository: ${data.error || 'Unknown error'}`);
963
+ showToast(`Failed to add repository: ${data.error || 'Unknown error'}`, 'error');
983
964
  }
984
965
  } catch (e) {
985
966
  console.error(e);
986
- alert('Network error while adding repository.');
967
+ showToast('Network error while adding repository.', 'error');
987
968
  } finally {
988
969
  setAddRepoLoading(false);
989
970
  }
@@ -1154,6 +1135,24 @@ Core Instructions:
1154
1135
  )}
1155
1136
  </div>
1156
1137
 
1138
+ <div className="pt-2">
1139
+ <div className="flex items-center gap-3">
1140
+ <input
1141
+ type="checkbox"
1142
+ id="onboardingPackContextXml"
1143
+ className="w-4 h-4 rounded border-gray-800 bg-[#111827] text-indigo-600 focus:ring-indigo-500 cursor-pointer"
1144
+ checked={config.packContextXml || false}
1145
+ onChange={(e) => setConfig({ ...config, packContextXml: e.target.checked })}
1146
+ />
1147
+ <label htmlFor="onboardingPackContextXml" className="text-xs font-semibold text-white cursor-pointer select-none">
1148
+ Pack Codebase Context with Repomix (XML)
1149
+ </label>
1150
+ </div>
1151
+ <span className="text-[10px] text-gray-500 mt-1 block">
1152
+ Aggregates all repository files into a single token-efficient XML file (<code>nexusflow-context.xml</code>) at the workspace root, giving AI assistants immediate access to the full codebase state.
1153
+ </span>
1154
+ </div>
1155
+
1157
1156
  {/* Form Buttons */}
1158
1157
  <div className="flex gap-2 pt-2">
1159
1158
  <button
@@ -1248,6 +1247,7 @@ Core Instructions:
1248
1247
  setBranchName('');
1249
1248
  setDescription('');
1250
1249
  setSelectedRepos([]);
1250
+ setLocalLlmEnabled(config?.localLlm?.enabled || false);
1251
1251
  }}
1252
1252
  >
1253
1253
  <PlusCircle size={18} className="text-indigo-400" />
@@ -1322,7 +1322,7 @@ Core Instructions:
1322
1322
  <button
1323
1323
  onClick={() => {
1324
1324
  navigator.clipboard.writeText('npm install -g @mrpatronz/nexusflow');
1325
- alert('Update command copied to clipboard!');
1325
+ showToast('Update command copied to clipboard!', 'success');
1326
1326
  }}
1327
1327
  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 cursor-pointer flex items-center gap-1.5"
1328
1328
  >
@@ -1394,9 +1394,9 @@ Core Instructions:
1394
1394
  4
1395
1395
  </div>
1396
1396
  <div>
1397
- <h3 className="text-sm font-semibold text-white">Repomix Packing</h3>
1397
+ <h3 className="text-sm font-semibold text-white">Repomix Packing (Optional)</h3>
1398
1398
  <p className="text-xs text-gray-400 mt-1">
1399
- NexusFlow aggregates the entire multi-repo codebase using <code>Repomix</code> into a token-efficient XML file <code>nexusflow-context.xml</code>, giving AI immediate access to the full codebase state.
1399
+ If enabled, NexusFlow aggregates the entire multi-repo codebase using <code>Repomix</code> into a token-efficient XML file <code>nexusflow-context.xml</code>, giving AI immediate access to the full codebase state. You can toggle this setting in Settings.
1400
1400
  </p>
1401
1401
  </div>
1402
1402
  </div>
@@ -1820,6 +1820,33 @@ Core Instructions:
1820
1820
  </div>
1821
1821
  </div>
1822
1822
 
1823
+ <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'}`}>
1824
+ <h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4 flex items-center gap-2">
1825
+ <Cpu size={14} className="text-indigo-400" /> Local AI Co-processor
1826
+ </h4>
1827
+ <p className="text-[11px] text-gray-500 mb-4">
1828
+ 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.
1829
+ </p>
1830
+ {!config?.localLlm?.enabled && (
1831
+ <p className="text-[11px] text-amber-400/80 mb-3 flex items-center gap-1.5">
1832
+ <AlertTriangle size={12} /> Enable a local LLM provider in Settings first.
1833
+ </p>
1834
+ )}
1835
+ <div className="flex items-center gap-3">
1836
+ <input
1837
+ type="checkbox"
1838
+ id="wizardLocalLlmEnabled"
1839
+ 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"
1840
+ checked={localLlmEnabled}
1841
+ onChange={(e) => setLocalLlmEnabled(e.target.checked)}
1842
+ disabled={!config?.localLlm?.enabled}
1843
+ />
1844
+ <label htmlFor="wizardLocalLlmEnabled" className={`text-xs font-semibold cursor-pointer select-none ${config?.localLlm?.enabled ? 'text-white' : 'text-gray-500 cursor-not-allowed'}`}>
1845
+ Include Local AI Co-processor in this workspace context
1846
+ </label>
1847
+ </div>
1848
+ </div>
1849
+
1823
1850
  <div className="mb-8 border border-gray-800/80 rounded-xl p-5 bg-gray-950/20">
1824
1851
  <h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4 flex items-center gap-2">
1825
1852
  <Terminal size={14} className="text-indigo-400" /> Advanced Session Resumption Settings
@@ -1938,667 +1965,67 @@ Core Instructions:
1938
1965
 
1939
1966
  {/* View 2: Active Workspaces Panel */}
1940
1967
  {view === 'workspaces' && (
1941
- <div className="max-w-5xl mx-auto">
1942
- <header className="flex justify-between items-center mb-8">
1943
- <div>
1944
- <h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">Active Workspaces</h1>
1945
- <p className="text-sm text-gray-400">Monitor running servers, libraries, and processes on your development branches.</p>
1946
- </div>
1947
- <button
1948
- className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
1949
- onClick={fetchWorkspaces}
1950
- disabled={workspacesLoading}
1951
- >
1952
- <RefreshCw size={14} className={workspacesLoading ? 'animate-spin text-indigo-400' : ''} /> Refresh
1953
- </button>
1954
- </header>
1955
-
1956
- {workspacesLoading ? (
1957
- <div className="flex flex-col items-center py-40 gap-4 text-gray-400">
1958
- <RefreshCw className="animate-spin text-indigo-400" size={32} />
1959
- <span className="text-sm">Fetching workspace details...</span>
1960
- </div>
1961
- ) : workspaces.length === 0 ? (
1962
- <div className="flex flex-col items-center justify-center py-24 bg-[#111827]/20 border border-gray-800/80 rounded-xl text-center">
1963
- <FolderGit2 size={44} className="text-gray-600 mb-4" />
1964
- <h3 className="text-lg font-bold text-white">No Active Workspaces</h3>
1965
- <p className="text-xs text-gray-500 max-w-sm mt-1">
1966
- No feature workspaces were detected in your development folder. Create a workspace to start.
1967
- </p>
1968
- </div>
1969
- ) : (
1970
- <div className="flex flex-col gap-6">
1971
- {workspaces.map((ws) => {
1972
- const isExpanded = activeWsId === ws.branchName;
1973
- return (
1974
- <div key={ws.id} className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-6 shadow-md">
1975
- <div className="flex justify-between items-start gap-4 mb-4">
1976
- <div>
1977
- <h3 className="text-lg font-bold text-white hover:text-indigo-400 transition-colors">{ws.branchName}</h3>
1978
- <div className="flex items-center gap-3 text-xs text-gray-500 mt-1">
1979
- <span>Created: {new Date(ws.createdAt).toLocaleDateString()}</span>
1980
- <span className="h-1 w-1 rounded-full bg-gray-700"></span>
1981
- <span>{ws.repos.length} repos</span>
1982
- </div>
1983
- </div>
1984
- <div className="flex gap-2">
1985
- <button
1986
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-semibold transition-all cursor-pointer shadow-md shadow-emerald-600/10 disabled:opacity-40 disabled:cursor-not-allowed"
1987
- onClick={() => handleResumeSession(ws)}
1988
- disabled={resumingWs === ws.branchName}
1989
- >
1990
- <Play size={12} className={resumingWs === ws.branchName ? 'animate-spin' : ''} />
1991
- {resumingWs === ws.branchName ? 'Resuming...' : 'Resume Session'}
1992
- </button>
1993
- <button
1994
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
1995
- onClick={() => handleCopyPrompt(ws)}
1996
- >
1997
- <Sparkles size={12} className="text-cyan-400" /> Copy Prompt
1998
- </button>
1999
- <button
2000
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
2001
- onClick={() => handleExportContext(ws.branchName)}
2002
- >
2003
- <Download size={12} className="text-emerald-400" /> Export Context
2004
- </button>
2005
- <button
2006
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-xs font-semibold transition-all cursor-pointer"
2007
- onClick={() => handleOpenInEditor(ws.workspacePath)}
2008
- >
2009
- <ExternalLink size={12} /> Open
2010
- </button>
2011
- <button
2012
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg text-xs font-semibold transition-all cursor-pointer shadow-md shadow-indigo-500/10"
2013
- onClick={() => {
2014
- if (isExpanded) {
2015
- setActiveWsId(null);
2016
- } else {
2017
- setActiveWsId(ws.branchName);
2018
- }
2019
- }}
2020
- >
2021
- {isExpanded ? 'Hide Runner' : 'Orchestrate'}
2022
- </button>
2023
- <button
2024
- className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 bg-red-950/40 border border-red-900/60 hover:bg-red-900/60 hover:border-red-800 rounded-lg text-xs font-semibold text-red-200 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed animate-fade-in"
2025
- onClick={() => handleDeleteWorkspace(ws.branchName)}
2026
- disabled={deleteWsLoading === ws.branchName}
2027
- >
2028
- <Trash2 size={12} className={deleteWsLoading === ws.branchName ? 'animate-spin' : ''} />
2029
- {deleteWsLoading === ws.branchName ? 'Deleting...' : 'Delete'}
2030
- </button>
2031
- </div>
2032
- </div>
2033
-
2034
- <div className="text-xs text-gray-400 bg-gray-950/40 border-l-2 border-indigo-500 rounded-r-lg px-4 py-3 mb-4 font-medium italic">
2035
- {renderFormattedDescription(ws.description)}
2036
- </div>
2037
-
2038
- <div className="flex items-center gap-3 mb-4">
2039
- <div className="flex flex-wrap gap-2">
2040
- {ws.repos.map((repoPath) => (
2041
- <span key={repoPath} className="text-[10px] px-2.5 py-1 bg-gray-900 border border-gray-800 text-gray-300 rounded-md font-semibold">
2042
- {repoPath.split(/[\\/]/).pop()}
2043
- </span>
2044
- ))}
2045
- </div>
2046
-
2047
- {repos.filter((r) => !ws.repos.includes(r.path)).length > 0 && (
2048
- <div className="flex items-center gap-1.5 ml-auto">
2049
- <select
2050
- id={`add-repo-select-${ws.branchName}`}
2051
- className="bg-gray-900 border border-gray-800 text-gray-300 rounded-md text-[10px] px-2 py-1 font-semibold outline-none focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
2052
- defaultValue=""
2053
- onChange={async (e) => {
2054
- const val = e.target.value;
2055
- if (val) {
2056
- if (window.confirm(`Are you sure you want to add repository "${val.split(/[\\/]/).pop()}" to this workspace?\nThis will create a new git worktree and re-run analysis.`)) {
2057
- await handleAddRepo(ws.branchName, val);
2058
- }
2059
- e.target.value = "";
2060
- }
2061
- }}
2062
- disabled={addRepoLoading}
2063
- >
2064
- <option value="" disabled>+ Add Repository</option>
2065
- {repos
2066
- .filter((r) => !ws.repos.includes(r.path))
2067
- .map((r) => (
2068
- <option key={r.path} value={r.path}>{r.name}</option>
2069
- ))
2070
- }
2071
- </select>
2072
- </div>
2073
- )}
2074
- </div>
2075
-
2076
- {/* Expansion: Process management console */}
2077
- {isExpanded && (
2078
- <div className="mt-6 pt-6 border-t border-gray-800/80">
2079
-
2080
- {/* Telemetry Dashboard Grid */}
2081
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
2082
- {/* Telemetry 1: CPU */}
2083
- <div className="bg-gray-950/40 border border-gray-800/60 rounded-xl p-4 flex flex-col justify-between">
2084
- <div className="flex justify-between items-center text-xs font-semibold text-gray-400 mb-2">
2085
- <span>Simulated CPU Load</span>
2086
- <span className={runningServices.length > 0 ? "text-emerald-400" : "text-gray-500"}>{runningServices.length > 0 ? '4.8%' : '0%'}</span>
2087
- </div>
2088
- <div className="w-full bg-gray-800 rounded-full h-1.5 mb-2 overflow-hidden">
2089
- <div
2090
- className={`h-1.5 rounded-full transition-all duration-500 ${runningServices.length > 0 ? "bg-gradient-to-r from-emerald-500 to-teal-400 animate-pulse" : "bg-gray-700"}`}
2091
- style={{ width: runningServices.length > 0 ? '48%' : '0%' }}
2092
- ></div>
2093
- </div>
2094
- <span className="text-[10px] text-gray-500">Fluctuating active worker processes</span>
2095
- </div>
2096
-
2097
- {/* Telemetry 2: Memory */}
2098
- <div className="bg-gray-950/40 border border-gray-800/60 rounded-xl p-4 flex flex-col justify-between">
2099
- <div className="flex justify-between items-center text-xs font-semibold text-gray-400 mb-2">
2100
- <span>Simulated Memory Allocation</span>
2101
- <span className={runningServices.length > 0 ? "text-cyan-400" : "text-gray-500"}>{runningServices.length > 0 ? '192 MB' : '0 MB'}</span>
2102
- </div>
2103
- <div className="w-full bg-gray-800 rounded-full h-1.5 mb-2 overflow-hidden">
2104
- <div
2105
- className={`h-1.5 rounded-full transition-all duration-500 ${runningServices.length > 0 ? "bg-gradient-to-r from-cyan-500 to-indigo-400" : "bg-gray-700"}`}
2106
- style={{ width: runningServices.length > 0 ? '65%' : '0%' }}
2107
- ></div>
2108
- </div>
2109
- <span className="text-[10px] text-gray-500">Working set size for spawned servers</span>
2110
- </div>
2111
-
2112
- {/* Telemetry 3: Environment Health */}
2113
- <div className="bg-gray-950/40 border border-gray-800/60 rounded-xl p-4 flex flex-col justify-between">
2114
- <div className="flex justify-between items-center text-xs font-semibold text-gray-400 mb-2">
2115
- <span>Active Workspace Status</span>
2116
- <span className={`inline-flex items-center gap-1 text-[10px] uppercase font-bold tracking-wider ${runningServices.length > 0 ? "text-emerald-400" : "text-gray-500"}`}>
2117
- <span className={`w-1.5 h-1.5 rounded-full ${runningServices.length > 0 ? "bg-emerald-500 animate-ping" : "bg-gray-600"}`}></span>
2118
- {runningServices.length > 0 ? "Online" : "Idle"}
2119
- </span>
2120
- </div>
2121
- <div className="text-xs text-white font-medium truncate">
2122
- {runningServices.length > 0 ? `${runningServices.length} processes active` : "All processes offline"}
2123
- </div>
2124
- <span className="text-[10px] text-gray-500">Service state mapping</span>
2125
- </div>
2126
- </div>
2127
-
2128
- {/* Sub-tab Navigation */}
2129
- <div className="flex border-b border-gray-800/80 mb-6 gap-2">
2130
- <button
2131
- className={`px-4 py-2 border-b-2 text-xs font-bold transition-all cursor-pointer ${
2132
- subTab === 'sessions'
2133
- ? 'border-indigo-500 text-white'
2134
- : 'border-transparent text-gray-500 hover:text-gray-300'
2135
- }`}
2136
- onClick={() => setSubTab('sessions')}
2137
- >
2138
- AI Session History
2139
- </button>
2140
- <button
2141
- className={`px-4 py-2 border-b-2 text-xs font-bold transition-all cursor-pointer ${
2142
- subTab === 'services'
2143
- ? 'border-indigo-500 text-white'
2144
- : 'border-transparent text-gray-500 hover:text-gray-300'
2145
- }`}
2146
- onClick={() => setSubTab('services')}
2147
- >
2148
- Orchestrated Services
2149
- </button>
2150
- <button
2151
- className={`px-4 py-2 border-b-2 text-xs font-bold transition-all cursor-pointer ${
2152
- subTab === 'changes'
2153
- ? 'border-indigo-500 text-white'
2154
- : 'border-transparent text-gray-500 hover:text-gray-300'
2155
- }`}
2156
- onClick={() => setSubTab('changes')}
2157
- >
2158
- Active Changes (AI Diffs)
2159
- </button>
2160
- <button
2161
- className={`px-4 py-2 border-b-2 text-xs font-bold transition-all cursor-pointer ${
2162
- subTab === 'knowledge'
2163
- ? 'border-indigo-500 text-white'
2164
- : 'border-transparent text-gray-500 hover:text-gray-300'
2165
- }`}
2166
- onClick={() => setSubTab('knowledge')}
2167
- >
2168
- Knowledge Base
2169
- </button>
2170
- <button
2171
- className={`px-4 py-2 border-b-2 text-xs font-bold transition-all cursor-pointer ${
2172
- subTab === 'plan'
2173
- ? 'border-indigo-500 text-white'
2174
- : 'border-transparent text-gray-500 hover:text-gray-300'
2175
- }`}
2176
- onClick={() => setSubTab('plan')}
2177
- >
2178
- Implementation Plan
2179
- </button>
2180
- </div>
2181
-
2182
- {subTab === 'sessions' && (
2183
- sessionsLoading ? (
2184
- <div className="flex justify-center py-10">
2185
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2186
- </div>
2187
- ) : sessions.length === 0 ? (
2188
- <div className="text-center py-10 bg-gray-950/20 border border-gray-800/60 rounded-xl p-6">
2189
- <MessageSquareCode size={36} className="text-gray-650 mx-auto mb-3" />
2190
- <h4 className="text-sm font-bold text-gray-400 mb-1">No Past AI Sessions Found</h4>
2191
- <p className="text-xs text-gray-505 max-w-md mx-auto leading-relaxed">
2192
- Start a conversation with your AI assistant (e.g. running <code>claude</code>, <code>agy</code>, <code>codex</code>, or <code>copilot</code> inside this directory) to track session history here.
2193
- </p>
2194
- </div>
2195
- ) : (
2196
- <div className="space-y-3 mb-6">
2197
- {sessions.map((sess) => (
2198
- <div key={sess.id} className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gray-950/20 border border-gray-800/60 rounded-xl p-4 hover:border-gray-700/80 transition-all">
2199
- <div className="flex-1 min-w-0">
2200
- <div className="flex items-center gap-2.5 mb-1.5">
2201
- <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
2202
- sess.assistant === 'antigravity' ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' :
2203
- sess.assistant === 'claude' ? 'bg-amber-500/20 text-amber-300 border border-amber-500/30' :
2204
- sess.assistant === 'codex' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' :
2205
- 'bg-sky-500/20 text-sky-300 border border-sky-500/30'
2206
- }`}>
2207
- {sess.assistant === 'antigravity' ? 'Antigravity' :
2208
- sess.assistant === 'claude' ? 'Claude Code' :
2209
- sess.assistant === 'codex' ? 'OpenAI Codex' : 'GitHub Copilot'}
2210
- </span>
2211
- <span className="text-[10px] text-gray-500">
2212
- Updated: {new Date(sess.updatedAt).toLocaleString()}
2213
- </span>
2214
- <span className="text-[10px] text-gray-500">•</span>
2215
- <span className="text-[10px] text-gray-550 font-medium">
2216
- {sess.messageCount} messages
2217
- </span>
2218
- </div>
2219
- <h4 className="text-xs font-semibold text-white truncate pr-4" title={sess.title}>
2220
- {sess.title}
2221
- </h4>
2222
- </div>
2223
- <div className="flex items-center gap-2 shrink-0">
2224
- <button
2225
- className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 bg-indigo-500/10 border border-indigo-500/20 hover:bg-indigo-500/20 text-indigo-300 rounded-lg text-xs font-bold transition-all cursor-pointer"
2226
- onClick={() => {
2227
- setActiveSession(sess);
2228
- setTranscript([]);
2229
- fetchSessionTranscript(sess.assistant, sess.id);
2230
- }}
2231
- >
2232
- <MessageSquare size={13} /> View Chat Log
2233
- </button>
2234
- <button
2235
- className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-all cursor-pointer"
2236
- onClick={() => handleResumeSession(ws, sess.id, sess.assistant)}
2237
- >
2238
- <Play size={11} /> Resume
2239
- </button>
2240
- </div>
2241
- </div>
2242
- ))}
2243
- </div>
2244
- )
2245
- )}
2246
-
2247
- {subTab === 'services' && (
2248
- servicesLoading ? (
2249
- <div className="flex justify-center py-10">
2250
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2251
- </div>
2252
- ) : (
2253
- <div>
2254
- {orchTools.length > 0 && (
2255
- <div className="flex items-center gap-3 bg-indigo-500/5 border border-indigo-500/20 rounded-xl p-4 mb-4 text-xs text-indigo-300">
2256
- <AlertTriangle size={16} className="text-indigo-400 shrink-0" />
2257
- <div>
2258
- <strong>Orchestration manifest detected:</strong> {orchTools.map((t) => t.tool).join(', ')}. You can start them inside sub-repos.
2259
- </div>
2260
- </div>
2261
- )}
2262
-
2263
- <div className="flex flex-col gap-2 mb-4">
2264
- {services.map((svc) => {
2265
- const isRunning = runningServices.some((rs) => rs.name === svc.name);
2266
- return (
2267
- <div key={svc.name} className="flex justify-between items-center bg-gray-950/20 border border-gray-800/60 rounded-xl px-4 py-3 text-xs">
2268
- <div className="flex items-center gap-3">
2269
- <span className={`w-2.5 h-2.5 rounded-full ${isRunning ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-gray-700'}`}></span>
2270
- <div>
2271
- <span className="font-bold text-white">{svc.name}</span>
2272
- <span className="text-[10px] text-gray-500 ml-3">
2273
- {svc.command} {svc.args.join(' ')} {svc.port ? `• port ${svc.port}` : ''}
2274
- </span>
2275
- </div>
2276
- </div>
2277
- <span className="text-[9px] px-2 py-0.5 rounded bg-gray-800 text-gray-500 font-bold uppercase">{svc.source}</span>
2278
- </div>
2279
- );
2280
- })}
2281
- </div>
2282
-
2283
- <div className="flex gap-2 mb-6">
2284
- <button
2285
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg text-xs font-semibold transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed shadow-md shadow-emerald-500/10 hover:-translate-y-0.5 active:translate-y-0"
2286
- onClick={() => handleStartServices(ws.branchName)}
2287
- disabled={runningServices.length > 0}
2288
- >
2289
- <Play size={14} /> Start All Services
2290
- </button>
2291
- <button
2292
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2.5 bg-rose-600 hover:bg-rose-700 text-white rounded-lg text-xs font-semibold transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed shadow-md shadow-rose-600/10 hover:-translate-y-0.5 active:translate-y-0"
2293
- onClick={() => handleStopServices(ws.branchName)}
2294
- disabled={runningServices.length === 0}
2295
- >
2296
- <Square size={14} /> Stop All
2297
- </button>
2298
- </div>
2299
-
2300
- {/* Interactive Console Screen */}
2301
- {services.length > 0 && (
2302
- <div className="border border-gray-800/80 rounded-xl overflow-hidden shadow-2xl bg-gray-950/60">
2303
- {/* Terminal Window Header */}
2304
- <div className="flex items-center justify-between bg-gray-900/80 px-4 py-3 border-b border-gray-800/60">
2305
- <div className="flex items-center gap-2">
2306
- <div className="flex gap-1.5">
2307
- <span className="w-3 h-3 rounded-full bg-rose-500/20 border border-rose-500/30"></span>
2308
- <span className="w-3 h-3 rounded-full bg-amber-500/20 border border-amber-500/30"></span>
2309
- <span className="w-3 h-3 rounded-full bg-emerald-500/20 border border-emerald-500/30"></span>
2310
- </div>
2311
- <span className="text-[11px] font-mono text-gray-400 font-semibold ml-2">nexusflow-terminal: {selectedLogService}</span>
2312
- </div>
2313
- <div className="flex gap-1 bg-gray-950/40 p-1 rounded-lg border border-gray-800/60">
2314
- {services.map((svc) => (
2315
- <button
2316
- key={svc.name}
2317
- className={`px-3 py-1 rounded-md text-[10px] font-mono font-bold transition-all cursor-pointer ${
2318
- selectedLogService === svc.name
2319
- ? 'bg-slate-800 text-white shadow-sm'
2320
- : 'text-gray-500 hover:text-gray-300'
2321
- }`}
2322
- onClick={() => setSelectedLogService(svc.name)}
2323
- >
2324
- {svc.name}
2325
- </button>
2326
- ))}
2327
- </div>
2328
- </div>
2329
-
2330
- {/* Terminal Screen Body */}
2331
- <div className="bg-[#030408] p-5 font-mono text-[11px] text-cyan-400/90 h-80 overflow-y-auto whitespace-pre-wrap shadow-inner relative scrollbar-thin scrollbar-thumb-gray-800">
2332
- <div className="absolute top-3 right-3 text-[9px] bg-slate-900/60 border border-gray-800 text-cyan-500/60 px-2 py-0.5 rounded font-mono select-none uppercase tracking-wider">
2333
- LIVE LOGS
2334
- </div>
2335
- {serviceLogs}
2336
- <div ref={logsEndRef}></div>
2337
- </div>
2338
- </div>
2339
- )}
2340
- </div>
2341
- )
2342
- )}
2343
-
2344
- {/* Active Changes View */}
2345
- {subTab === 'changes' && (
2346
- <div>
2347
- <header className="flex justify-between items-center mb-4 flex-wrap gap-2">
2348
- <h4 className="text-sm font-bold text-white flex items-center gap-2">
2349
- <FolderGit2 size={16} className="text-cyan-400" /> Active Workspace Git Diffs
2350
- </h4>
2351
- <div className="flex gap-2">
2352
- <button
2353
- className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-[10px] font-semibold transition-all cursor-pointer text-gray-300"
2354
- onClick={() => fetchGitChanges(ws.branchName)}
2355
- disabled={gitChangesLoading}
2356
- >
2357
- <RefreshCw size={11} className={gitChangesLoading ? 'animate-spin text-indigo-400' : ''} /> Refresh Changes
2358
- </button>
2359
- <button
2360
- className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[10px] font-semibold transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
2361
- onClick={() => handleSyncAll(ws.branchName)}
2362
- disabled={syncLoading}
2363
- >
2364
- {syncLoading ? <RefreshCw size={11} className="animate-spin text-indigo-300" /> : <RefreshCw size={11} />} Sync All
2365
- </button>
2366
- <button
2367
- className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-[10px] font-semibold transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
2368
- onClick={() => setShowCommitModal(true)}
2369
- disabled={gitChanges.every(repo => repo.files.length === 0) || commitLoading}
2370
- >
2371
- Commit & Push All
2372
- </button>
2373
- </div>
2374
- </header>
2375
-
2376
- {syncResults && (
2377
- <div className="bg-[#090d1a]/60 border border-gray-800 rounded-xl p-4 mb-4 text-xs">
2378
- <div className="flex justify-between items-center mb-2 border-b border-gray-800/80 pb-2">
2379
- <h5 className="font-bold text-white font-mono text-[11px]">Rebase/Sync Results</h5>
2380
- <button className="text-gray-500 hover:text-gray-300 text-[10px] cursor-pointer" onClick={() => setSyncResults(null)}>Dismiss</button>
2381
- </div>
2382
- <div className="space-y-1.5">
2383
- {syncResults.map((r: any) => (
2384
- <div key={r.repoName} className="flex justify-between items-center font-mono text-[10px]">
2385
- <span className="text-gray-300">{r.repoName}</span>
2386
- <span className={r.success ? "text-emerald-400" : "text-rose-400"}>
2387
- {r.success ? `✅ Synced (${r.message})` : `⚠️ Conflict: ${r.message}`}
2388
- </span>
2389
- </div>
2390
- ))}
2391
- </div>
2392
- </div>
2393
- )}
2394
-
2395
- {commitResults && (
2396
- <div className="bg-[#090d1a]/60 border border-gray-800 rounded-xl p-4 mb-4 text-xs">
2397
- <div className="flex justify-between items-center mb-2 border-b border-gray-800/80 pb-2">
2398
- <h5 className="font-bold text-white font-mono text-[11px]">Commit/Push Results</h5>
2399
- <button className="text-gray-500 hover:text-gray-300 text-[10px] cursor-pointer" onClick={() => setCommitResults(null)}>Dismiss</button>
2400
- </div>
2401
- <div className="space-y-1.5">
2402
- {commitResults.map((r: any) => (
2403
- <div key={r.repoName} className="flex justify-between items-center font-mono text-[10px]">
2404
- <span className="text-gray-300">{r.repoName}</span>
2405
- <span className={r.success ? "text-emerald-400" : "text-rose-400"}>
2406
- {r.success ? `✅ Committed ${r.filesChanged} file(s) (${r.commitHash || 'no hash'})` : `⚠️ Error: ${r.message}`}
2407
- </span>
2408
- </div>
2409
- ))}
2410
- </div>
2411
- </div>
2412
- )}
2413
-
2414
- {showCommitModal && (
2415
- <div className="bg-[#0b0f19] border border-gray-800 rounded-xl p-4 mb-4">
2416
- <h5 className="text-xs font-bold text-white mb-2">Enter Commit Message</h5>
2417
- <input
2418
- type="text"
2419
- className="w-full bg-gray-950 border border-gray-800 rounded-lg px-3 py-2 text-xs font-mono text-gray-350 focus:outline-none focus:border-indigo-500 mb-3"
2420
- placeholder="feat: implement logic..."
2421
- value={commitMessage}
2422
- onChange={(e) => setCommitMessage(e.target.value)}
2423
- onKeyDown={(e) => {
2424
- if (e.key === 'Enter') handleCommitAll(ws.branchName);
2425
- }}
2426
- />
2427
- <div className="flex justify-end gap-2">
2428
- <button
2429
- className="px-3 py-1.5 bg-gray-900 border border-gray-800 hover:bg-gray-850 rounded-lg text-[10px] font-semibold transition-all cursor-pointer text-gray-400"
2430
- onClick={() => {
2431
- setShowCommitModal(false);
2432
- setCommitMessage('');
2433
- }}
2434
- disabled={commitLoading}
2435
- >
2436
- Cancel
2437
- </button>
2438
- <button
2439
- className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 rounded-lg text-[10px] font-semibold text-white transition-all cursor-pointer"
2440
- onClick={() => handleCommitAll(ws.branchName)}
2441
- disabled={commitLoading || !commitMessage.trim()}
2442
- >
2443
- {commitLoading ? 'Committing...' : 'Commit & Push'}
2444
- </button>
2445
- </div>
2446
- </div>
2447
- )}
2448
-
2449
- {gitChangesLoading ? (
2450
- <div className="flex justify-center py-10">
2451
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2452
- </div>
2453
- ) : (
2454
- <div className="space-y-4">
2455
- {gitChanges.every(repo => repo.files.length === 0) ? (
2456
- <div className="flex flex-col items-center justify-center py-12 bg-gray-950/20 border border-gray-800/40 rounded-xl text-center">
2457
- <Check size={28} className="text-emerald-500 mb-2" />
2458
- <h5 className="text-xs font-bold text-white">No Uncommitted Changes</h5>
2459
- <p className="text-[10px] text-gray-500 mt-0.5">Workspace is completely synced with Git feature branches.</p>
2460
- </div>
2461
- ) : (
2462
- gitChanges.map((repo) => {
2463
- if (repo.files.length === 0) return null;
2464
- const totalFilesChanged = repo.files.length;
2465
- const repoAdditions = repo.files.reduce((acc: number, f: any) => acc + (f.additions || 0), 0);
2466
- const repoDeletions = repo.files.reduce((acc: number, f: any) => acc + (f.deletions || 0), 0);
2467
-
2468
- return (
2469
- <div key={repo.repoName} className="bg-gray-950/20 border border-gray-800/60 rounded-xl p-4">
2470
- <div className="flex justify-between items-center mb-3">
2471
- <div className="flex items-center gap-2">
2472
- <h5 className="text-xs font-bold text-white font-mono">{repo.repoName}</h5>
2473
- <span className="text-[9px] px-1.5 py-0.5 rounded bg-gray-900 border border-gray-800 text-gray-400 font-mono">
2474
- {totalFilesChanged} file{totalFilesChanged === 1 ? '' : 's'} changed
2475
- </span>
2476
- {(repoAdditions > 0 || repoDeletions > 0) && (
2477
- <span className="text-[9px] font-mono">
2478
- <span className="text-emerald-400 font-bold">+{repoAdditions}</span>{' '}
2479
- <span className="text-rose-400 font-bold">-{repoDeletions}</span>
2480
- </span>
2481
- )}
2482
- </div>
2483
- <span className="text-[9px] text-gray-500 font-mono truncate max-w-[280px]">{repo.repoPath}</span>
2484
- </div>
2485
- <div className="space-y-1.5">
2486
- {repo.files.map((fileInfo: any) => (
2487
- <div key={fileInfo.file} className="flex justify-between items-center bg-[#090d1a]/40 px-3 py-2 rounded-lg border border-gray-800/30 text-xs">
2488
- <div className="flex flex-col min-w-0">
2489
- <span className="font-mono text-gray-300 text-[11px] truncate max-w-[320px]">{fileInfo.file}</span>
2490
- {(fileInfo.additions > 0 || fileInfo.deletions > 0) && (
2491
- <span className="text-[9px] font-mono mt-0.5">
2492
- <span className="text-emerald-500">+{fileInfo.additions}</span>{' '}
2493
- <span className="text-rose-500">-{fileInfo.deletions}</span>
2494
- </span>
2495
- )}
2496
- </div>
2497
- <span className={`text-[9px] px-2 py-0.5 rounded font-bold uppercase ${
2498
- fileInfo.type === 'added' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' :
2499
- fileInfo.type === 'deleted' ? 'bg-rose-500/10 text-rose-400 border border-rose-500/20' :
2500
- 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
2501
- }`}>
2502
- {fileInfo.type}
2503
- </span>
2504
- </div>
2505
- ))}
2506
- </div>
2507
- </div>
2508
- );
2509
- })
2510
- )}
2511
- </div>
2512
- )}
2513
- </div>
2514
- )}
2515
-
2516
- {/* Knowledge Base View */}
2517
- {subTab === 'knowledge' && (
2518
- <div className="bg-[#090d1a]/20 border border-gray-800/60 rounded-xl p-4">
2519
- <header className="flex justify-between items-center mb-4">
2520
- <h4 className="text-sm font-bold text-white flex items-center gap-2">
2521
- <BookOpen size={16} className="text-cyan-400" /> Persistent Knowledge Memory (nexusflow-knowledge.md)
2522
- </h4>
2523
- <div className="flex gap-2">
2524
- {isEditingKnowledge ? (
2525
- <>
2526
- <button
2527
- className="px-2.5 py-1.5 bg-gray-900 border border-gray-800 hover:bg-gray-850 rounded-lg text-[10px] font-semibold transition-all cursor-pointer text-gray-400"
2528
- onClick={() => {
2529
- setEditedKnowledge(knowledgeContent);
2530
- setIsEditingKnowledge(false);
2531
- }}
2532
- disabled={saveKnowledgeLoading}
2533
- >
2534
- Cancel
2535
- </button>
2536
- <button
2537
- className="inline-flex items-center gap-1.5 px-2.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-[10px] font-semibold transition-all cursor-pointer"
2538
- onClick={() => handleSaveKnowledge(ws.branchName)}
2539
- disabled={saveKnowledgeLoading}
2540
- >
2541
- {saveKnowledgeLoading ? <RefreshCw className="animate-spin" size={10} /> : <Save size={10} />} Save
2542
- </button>
2543
- </>
2544
- ) : (
2545
- <button
2546
- className="inline-flex items-center gap-1.5 px-2.5 py-1.5 bg-gray-900 border border-gray-800 hover:bg-gray-800 hover:border-gray-700 rounded-lg text-[10px] font-semibold transition-all cursor-pointer text-gray-350"
2547
- onClick={() => setIsEditingKnowledge(true)}
2548
- disabled={knowledgeLoading}
2549
- >
2550
- <Edit size={10} /> Edit Knowledge
2551
- </button>
2552
- )}
2553
- </div>
2554
- </header>
2555
-
2556
- {knowledgeLoading ? (
2557
- <div className="flex justify-center py-10">
2558
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2559
- </div>
2560
- ) : isEditingKnowledge ? (
2561
- <textarea
2562
- className="w-full h-96 bg-gray-950/60 border border-gray-800/80 rounded-xl p-4 text-xs font-mono text-gray-305 focus:outline-none focus:border-indigo-500/80 resize-y"
2563
- value={editedKnowledge}
2564
- onChange={(e) => setEditedKnowledge(e.target.value)}
2565
- />
2566
- ) : (
2567
- <div className="bg-gray-950/40 border border-gray-800/30 rounded-xl p-4 text-gray-350 text-xs leading-relaxed overflow-auto font-mono whitespace-pre-wrap max-h-96">
2568
- {knowledgeContent || "No knowledge file generated yet."}
2569
- </div>
2570
- )}
2571
- </div>
2572
- )}
2573
-
2574
- {/* Implementation Plan View */}
2575
- {subTab === 'plan' && (
2576
- <div className="bg-[#090d1a]/20 border border-gray-800/60 rounded-xl p-4">
2577
- <header className="flex justify-between items-center mb-4">
2578
- <h4 className="text-sm font-bold text-white flex items-center gap-2">
2579
- <ListOrdered size={16} className="text-cyan-400" /> Inter-Repo Implementation Plan (nexusflow-plan.md)
2580
- </h4>
2581
- </header>
2582
-
2583
- {planLoading ? (
2584
- <div className="flex justify-center py-10">
2585
- <RefreshCw className="animate-spin text-indigo-400" size={20} />
2586
- </div>
2587
- ) : (
2588
- <div className="bg-gray-950/40 border border-gray-800/30 rounded-xl p-4 text-gray-350 text-xs leading-relaxed overflow-auto font-mono whitespace-pre-wrap max-h-96">
2589
- {planContent || "No implementation plan generated yet."}
2590
- </div>
2591
- )}
2592
- </div>
2593
- )}
2594
- </div>
2595
- )}
2596
- </div>
2597
- );
2598
- })}
2599
- </div>
2600
- )}
2601
- </div>
1968
+ <WorkspaceList
1969
+ workspacesLoading={workspacesLoading}
1970
+ workspaces={workspaces}
1971
+ activeWsId={activeWsId}
1972
+ setActiveWsId={setActiveWsId}
1973
+ resumingWs={resumingWs}
1974
+ handleResumeSession={handleResumeSession}
1975
+ handleCopyPrompt={handleCopyPrompt}
1976
+ handleOpenInEditor={handleOpenInEditor}
1977
+ fetchWorkspaces={fetchWorkspaces}
1978
+ repos={repos}
1979
+ deleteWsLoading={deleteWsLoading}
1980
+ addRepoLoading={addRepoLoading}
1981
+ handleDeleteWorkspace={handleDeleteWorkspace}
1982
+ handleAddRepo={handleAddRepo}
1983
+ services={services}
1984
+ runningServices={runningServices}
1985
+ selectedLogService={selectedLogService}
1986
+ serviceLogs={serviceLogs}
1987
+ logsEndRef={logsEndRef}
1988
+ setSelectedLogService={setSelectedLogService}
1989
+ handleStartServices={handleStartServices}
1990
+ handleStopServices={handleStopServices}
1991
+ orchTools={orchTools}
1992
+ servicesLoading={servicesLoading}
1993
+ gitChanges={gitChanges}
1994
+ gitChangesLoading={gitChangesLoading}
1995
+ syncLoading={syncLoading}
1996
+ syncResults={syncResults}
1997
+ commitMessage={commitMessage}
1998
+ showCommitModal={showCommitModal}
1999
+ commitLoading={commitLoading}
2000
+ commitResults={commitResults}
2001
+ setSyncResults={setSyncResults}
2002
+ setCommitResults={setCommitResults}
2003
+ setCommitMessage={setCommitMessage}
2004
+ setShowCommitModal={setShowCommitModal}
2005
+ fetchGitChanges={fetchGitChanges}
2006
+ handleSyncAll={handleSyncAll}
2007
+ handleCommitAll={handleCommitAll}
2008
+ knowledgeContent={knowledgeContent}
2009
+ knowledgeLoading={knowledgeLoading}
2010
+ isEditingKnowledge={isEditingKnowledge}
2011
+ editedKnowledge={editedKnowledge}
2012
+ saveKnowledgeLoading={saveKnowledgeLoading}
2013
+ setEditedKnowledge={setEditedKnowledge}
2014
+ setIsEditingKnowledge={setIsEditingKnowledge}
2015
+ handleSaveKnowledge={handleSaveKnowledge}
2016
+ planContent={planContent}
2017
+ planLoading={planLoading}
2018
+ sessions={sessions}
2019
+ sessionsLoading={sessionsLoading}
2020
+ activeSession={activeSession}
2021
+ transcript={transcript}
2022
+ transcriptLoading={transcriptLoading}
2023
+ setActiveSession={setActiveSession}
2024
+ setTranscript={setTranscript}
2025
+ fetchSessionTranscript={fetchSessionTranscript}
2026
+ subTab={subTab}
2027
+ setSubTab={setSubTab}
2028
+ />
2602
2029
  )}
2603
2030
 
2604
2031
  {/* View 3: Settings View */}
@@ -2689,11 +2116,145 @@ Core Instructions:
2689
2116
  </select>
2690
2117
  <span className="text-[10px] text-gray-500 mt-1">Your preferred code editor for opening workspaces.</span>
2691
2118
  </div>
2119
+
2120
+ <div className="flex flex-col md:col-span-2 border-t border-gray-800/60 pt-6 mt-2">
2121
+ <h3 className="text-sm font-bold text-white mb-2">Codebase Context Settings</h3>
2122
+ <div className="flex items-center gap-3">
2123
+ <input
2124
+ type="checkbox"
2125
+ id="packContextXml"
2126
+ className="w-4 h-4 rounded border-gray-800 bg-[#111827] text-indigo-600 focus:ring-indigo-500 cursor-pointer"
2127
+ checked={config.packContextXml || false}
2128
+ onChange={(e) => setConfig({ ...config, packContextXml: e.target.checked })}
2129
+ />
2130
+ <label htmlFor="packContextXml" className="text-xs font-semibold text-white cursor-pointer select-none">
2131
+ Pack Codebase Context with Repomix (XML)
2132
+ </label>
2133
+ </div>
2134
+ <span className="text-[10px] text-gray-500 mt-1">
2135
+ Aggregates files of all repositories in a workspace into a single token-efficient XML file (<code>nexusflow-context.xml</code>) at the workspace root, giving AI assistants immediate access to the full codebase state.
2136
+ </span>
2137
+ </div>
2138
+ </div>
2139
+
2140
+ {/* Local AI Co-Processor Settings */}
2141
+ <div className="bg-[#111827]/40 border border-gray-800/80 rounded-xl p-8 shadow-xl backdrop-blur-sm mb-6 mt-6">
2142
+ <h3 className="text-lg font-bold text-white mb-2">Local AI Co-Processor Settings</h3>
2143
+ <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>
2144
+
2145
+ {recommendation && (
2146
+ <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">
2147
+ <div className="flex items-center gap-2 text-indigo-400 font-bold">
2148
+ <Cpu size={14} /> Local System Scan Results
2149
+ </div>
2150
+ <div>• Detected Memory: <strong>{recommendation.totalRamGb} GB RAM</strong></div>
2151
+ <div>• Detected GPU: <strong>{recommendation.gpuName}</strong></div>
2152
+ <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>
2153
+ </div>
2154
+ )}
2155
+
2156
+ <div className="flex items-center gap-3 mb-6">
2157
+ <input
2158
+ type="checkbox"
2159
+ id="localLlmEnabled"
2160
+ className="w-4 h-4 rounded border-gray-800 bg-[#111827] text-indigo-600 focus:ring-indigo-500 cursor-pointer"
2161
+ checked={config.localLlm?.enabled || false}
2162
+ onChange={(e) => {
2163
+ const defaultLlm = { enabled: e.target.checked, provider: 'ollama' as const, endpoint: 'http://localhost:11434', model: recommendation?.recommendedModel || 'qwen2.5-coder:1.5b' };
2164
+ setConfig({
2165
+ ...config,
2166
+ localLlm: config.localLlm ? { ...config.localLlm, enabled: e.target.checked } : defaultLlm
2167
+ });
2168
+ }}
2169
+ />
2170
+ <label htmlFor="localLlmEnabled" className="text-sm font-semibold text-white cursor-pointer select-none">
2171
+ Enable Local AI Delegation Tool
2172
+ </label>
2173
+ </div>
2174
+
2175
+ {config.localLlm?.enabled && (
2176
+ <div className="space-y-6 border-t border-gray-800/60 pt-6">
2177
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
2178
+ <div className="flex flex-col">
2179
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Local Provider</label>
2180
+ <select
2181
+ 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"
2182
+ value={config.localLlm.provider}
2183
+ onChange={(e) => setConfig({
2184
+ ...config,
2185
+ localLlm: { ...config.localLlm!, provider: e.target.value as any }
2186
+ })}
2187
+ >
2188
+ <option value="ollama">Ollama</option>
2189
+ <option value="openai-compatible">OpenAI-Compatible (e.g. LM Studio)</option>
2190
+ </select>
2191
+ <span className="text-[10px] text-gray-500 mt-1">Provider protocol to connect to.</span>
2192
+ </div>
2193
+
2194
+ <div className="flex flex-col">
2195
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Endpoint URL</label>
2196
+ <input
2197
+ type="text"
2198
+ 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 ${
2199
+ config.localLlm.endpoint && !config.localLlm.endpoint.trim().startsWith('http://') && !config.localLlm.endpoint.trim().startsWith('https://')
2200
+ ? 'border-rose-500/60 focus:border-rose-500 focus:ring-rose-500'
2201
+ : 'border-gray-800 focus:border-indigo-500 focus:ring-indigo-500'
2202
+ }`}
2203
+ value={config.localLlm.endpoint}
2204
+ onChange={(e) => setConfig({
2205
+ ...config,
2206
+ localLlm: { ...config.localLlm!, endpoint: e.target.value }
2207
+ })}
2208
+ />
2209
+ {config.localLlm.endpoint && !config.localLlm.endpoint.trim().startsWith('http://') && !config.localLlm.endpoint.trim().startsWith('https://') && (
2210
+ <span className="text-[10px] text-rose-400 mt-1">Endpoint must start with http:// or https://</span>
2211
+ )}
2212
+ <span className="text-[10px] text-gray-500 mt-1">API base address of the local runner.</span>
2213
+ </div>
2214
+
2215
+ <div className="flex flex-col md:col-span-2">
2216
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Model Name</label>
2217
+ <input
2218
+ type="text"
2219
+ 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"
2220
+ value={config.localLlm.model}
2221
+ onChange={(e) => setConfig({
2222
+ ...config,
2223
+ localLlm: { ...config.localLlm!, model: e.target.value }
2224
+ })}
2225
+ />
2226
+ <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>
2227
+ </div>
2228
+ </div>
2229
+
2230
+ <div className="flex items-center justify-between pt-4 border-t border-gray-800/40">
2231
+ <button
2232
+ type="button"
2233
+ disabled={testingLlm}
2234
+ 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"
2235
+ onClick={testLlmConnection}
2236
+ >
2237
+ {testingLlm ? 'Testing...' : 'Test Connection'}
2238
+ </button>
2239
+
2240
+ {testStatus && (
2241
+ <div className={`text-xs px-3 py-1.5 rounded-lg border ${
2242
+ testStatus.success
2243
+ ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
2244
+ : 'bg-rose-500/10 border-rose-500/20 text-rose-400'
2245
+ }`}>
2246
+ {testStatus.message}
2247
+ </div>
2248
+ )}
2249
+ </div>
2250
+ </div>
2251
+ )}
2692
2252
  </div>
2693
2253
 
2694
2254
  <div className="flex justify-end">
2695
2255
  <button
2696
- 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"
2256
+ 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"
2257
+ disabled={!isSettingsFormValid}
2697
2258
  onClick={() => saveAppConfig(config)}
2698
2259
  >
2699
2260
  Save Configuration
@@ -2883,7 +2444,7 @@ Core Instructions:
2883
2444
  activeSession.assistant === 'codex' ? `codex resume ${activeSession.id}` :
2884
2445
  `copilot --resume ${activeSession.id}`;
2885
2446
  navigator.clipboard.writeText(cmd);
2886
- alert(`Copied run command to clipboard:\n\n${cmd}`);
2447
+ showToast(`Copied run command to clipboard:\n\n${cmd}`, 'success');
2887
2448
  }}
2888
2449
  >
2889
2450
  <Copy size={13} /> Copy Resume Command
@@ -2903,6 +2464,35 @@ Core Instructions:
2903
2464
  </div>
2904
2465
  </div>
2905
2466
  )}
2467
+
2468
+ {/* Toast Notifications Container */}
2469
+ <div className="fixed bottom-5 right-5 z-[9999] flex flex-col gap-3 pointer-events-none max-w-md w-full">
2470
+ {toasts.map((toast) => (
2471
+ <div
2472
+ key={toast.id}
2473
+ 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 ${
2474
+ toast.type === 'success'
2475
+ ? 'bg-[#062c1b]/95 border-emerald-800/80 text-emerald-100'
2476
+ : toast.type === 'error'
2477
+ ? 'bg-[#2c0e0e]/95 border-red-900/80 text-red-100'
2478
+ : 'bg-[#131926]/95 border-slate-800/80 text-slate-100'
2479
+ }`}
2480
+ >
2481
+ <div className="flex items-start gap-2.5 text-xs font-semibold flex-1">
2482
+ {toast.type === 'success' && <Check className="text-emerald-400 shrink-0 mt-0.5" size={16} />}
2483
+ {toast.type === 'error' && <AlertCircle className="text-red-400 shrink-0 mt-0.5" size={16} />}
2484
+ {toast.type === 'info' && <Sparkles className="text-indigo-400 shrink-0 mt-0.5" size={16} />}
2485
+ <span className="whitespace-pre-line text-left leading-relaxed">{toast.message}</span>
2486
+ </div>
2487
+ <button
2488
+ onClick={() => setToasts((prev) => prev.filter((t) => t.id !== toast.id))}
2489
+ className="text-slate-400 hover:text-white transition-colors cursor-pointer shrink-0 mt-0.5"
2490
+ >
2491
+ <X size={14} />
2492
+ </button>
2493
+ </div>
2494
+ ))}
2495
+ </div>
2906
2496
  </div>
2907
2497
  );
2908
2498
  }