zugzbot 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.utils/zugzweb/client/dist/assets/index-CVh0OG8j.js +291 -0
- package/.utils/zugzweb/client/dist/assets/index-GxkFc3Mh.css +1 -0
- package/.utils/zugzweb/client/dist/index.html +2 -2
- package/.utils/zugzweb/client/src/App.tsx +646 -58
- package/.utils/zugzweb/daemon.js +57 -3
- package/.utils/zugzweb/daemon.log +5 -0
- package/bin/web.js +20 -0
- package/package.json +48 -3
- package/.utils/zugzweb/client/dist/assets/index-CcbbJtSz.css +0 -1
- package/.utils/zugzweb/client/dist/assets/index-DXxAlmSX.js +0 -266
|
@@ -23,7 +23,12 @@ import {
|
|
|
23
23
|
Code,
|
|
24
24
|
Sparkles,
|
|
25
25
|
Clock,
|
|
26
|
-
AlertCircle
|
|
26
|
+
AlertCircle,
|
|
27
|
+
Folder,
|
|
28
|
+
FolderOpen,
|
|
29
|
+
ArrowUp,
|
|
30
|
+
Home,
|
|
31
|
+
Compass
|
|
27
32
|
} from 'lucide-react'
|
|
28
33
|
|
|
29
34
|
// Interfaces basadas en la API de Opencode
|
|
@@ -112,6 +117,11 @@ const AVAILABLE_AGENTS = [
|
|
|
112
117
|
export default function App() {
|
|
113
118
|
const [sessions, setSessions] = useState<Session[]>([])
|
|
114
119
|
const [currentSessionId, setCurrentSessionId] = useState<string>('')
|
|
120
|
+
const currentSessionIdRef = useRef<string>('')
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
currentSessionIdRef.current = currentSessionId
|
|
124
|
+
}, [currentSessionId])
|
|
115
125
|
const [currentSession, setCurrentSession] = useState<Session | null>(null)
|
|
116
126
|
const [messages, setMessages] = useState<Message[]>([])
|
|
117
127
|
const [inputValue, setInputValue] = useState('')
|
|
@@ -122,6 +132,7 @@ export default function App() {
|
|
|
122
132
|
const [tunnelUrl, setTunnelUrl] = useState<string>('')
|
|
123
133
|
const [expandedParts, setExpandedParts] = useState<Record<string, boolean>>({})
|
|
124
134
|
const [errorMsg, setErrorMsg] = useState<string>('')
|
|
135
|
+
const [systemCwd, setSystemCwd] = useState<string>('')
|
|
125
136
|
|
|
126
137
|
// Soporte para multi-instancia / selección de proyectos
|
|
127
138
|
const [instances, setInstances] = useState<OpencodeInstance[]>([])
|
|
@@ -137,13 +148,7 @@ export default function App() {
|
|
|
137
148
|
// ignore
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
|
-
return [
|
|
141
|
-
{
|
|
142
|
-
id: 'workspace_raiz',
|
|
143
|
-
name: 'Workspace Raíz',
|
|
144
|
-
path: '/Users/wavesbyte/Documents/Repositorio Zugzbot'
|
|
145
|
-
}
|
|
146
|
-
]
|
|
151
|
+
return []
|
|
147
152
|
})
|
|
148
153
|
|
|
149
154
|
const [sessionMappings, setSessionMappings] = useState<Record<string, string>>(() => {
|
|
@@ -188,6 +193,22 @@ export default function App() {
|
|
|
188
193
|
localStorage.setItem('zugzweb_session_mappings', JSON.stringify(sessionMappings))
|
|
189
194
|
}, [sessionMappings])
|
|
190
195
|
|
|
196
|
+
const [customSessionTitles, setCustomSessionTitles] = useState<Record<string, string>>(() => {
|
|
197
|
+
const saved = localStorage.getItem('zugzweb_custom_session_titles')
|
|
198
|
+
if (saved) {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(saved)
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
localStorage.setItem('zugzweb_custom_session_titles', JSON.stringify(customSessionTitles))
|
|
210
|
+
}, [customSessionTitles])
|
|
211
|
+
|
|
191
212
|
useEffect(() => {
|
|
192
213
|
localStorage.setItem('zugzweb_active_project_id', activeProjectId)
|
|
193
214
|
}, [activeProjectId])
|
|
@@ -224,7 +245,191 @@ export default function App() {
|
|
|
224
245
|
const [newSessionFirstPrompt, setNewSessionFirstPrompt] = useState<string>('')
|
|
225
246
|
const [selectedAgent, setSelectedAgent] = useState<string>('sdd-orchestrator')
|
|
226
247
|
|
|
248
|
+
// Estado de Onboarding Setup
|
|
249
|
+
const [setupCompleted, setSetupCompleted] = useState<boolean>(() => {
|
|
250
|
+
return localStorage.getItem('zugzweb_setup_completed') === 'true'
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Estados para el explorador de archivos
|
|
254
|
+
const [showExplorerModal, setShowExplorerModal] = useState(false)
|
|
255
|
+
const [explorerCurrentPath, setExplorerCurrentPath] = useState('')
|
|
256
|
+
const [explorerParentPath, setExplorerParentPath] = useState<string | null>(null)
|
|
257
|
+
const [explorerDirectories, setExplorerDirectories] = useState<string[]>([])
|
|
258
|
+
const [explorerHomePath, setExplorerHomePath] = useState('')
|
|
259
|
+
const [explorerCwdPath, setExplorerCwdPath] = useState('')
|
|
260
|
+
const [explorerError, setExplorerError] = useState('')
|
|
261
|
+
const [explorerLoading, setExplorerLoading] = useState(false)
|
|
262
|
+
const [explorerCallback, setExplorerCallback] = useState<((path: string) => void) | null>(null)
|
|
263
|
+
const [explorerTitle, setExplorerTitle] = useState('Seleccionar Carpeta')
|
|
264
|
+
|
|
265
|
+
// Función para cargar un directorio en el explorador de archivos
|
|
266
|
+
const loadDirectory = async (pathStr?: string) => {
|
|
267
|
+
setExplorerLoading(true)
|
|
268
|
+
setExplorerError('')
|
|
269
|
+
try {
|
|
270
|
+
const queryParam = pathStr ? `?path=${encodeURIComponent(pathStr)}` : ''
|
|
271
|
+
const res = await fetch(`/api-custom/list-dir${queryParam}`)
|
|
272
|
+
if (res.ok) {
|
|
273
|
+
const data = await res.json()
|
|
274
|
+
setExplorerCurrentPath(data.currentPath)
|
|
275
|
+
setExplorerParentPath(data.parentPath)
|
|
276
|
+
setExplorerDirectories(data.directories || [])
|
|
277
|
+
setExplorerHomePath(data.home || '')
|
|
278
|
+
setExplorerCwdPath(data.cwd || '')
|
|
279
|
+
setExplorerError('')
|
|
280
|
+
} else {
|
|
281
|
+
const data = await res.json()
|
|
282
|
+
setExplorerError(data.error || 'No se pudo leer el directorio')
|
|
283
|
+
}
|
|
284
|
+
} catch (err: any) {
|
|
285
|
+
setExplorerError('Error de red al conectar con el servidor: ' + err.message)
|
|
286
|
+
} finally {
|
|
287
|
+
setExplorerLoading(false)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Abre el explorador de archivos con una configuración específica
|
|
292
|
+
const openExplorer = (title: string, initialPath: string, callback: (selectedPath: string) => void) => {
|
|
293
|
+
setExplorerTitle(title)
|
|
294
|
+
setExplorerCallback(() => callback)
|
|
295
|
+
setShowExplorerModal(true)
|
|
296
|
+
loadDirectory(initialPath || systemCwd)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Carga inicial del explorador de archivos en base al CWD del sistema al arrancar
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (systemCwd && !explorerCurrentPath) {
|
|
302
|
+
setExplorerCurrentPath(systemCwd)
|
|
303
|
+
loadDirectory(systemCwd)
|
|
304
|
+
}
|
|
305
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
306
|
+
}, [systemCwd])
|
|
307
|
+
|
|
308
|
+
const renderExplorerContent = (onConfirm: (path: string) => void) => {
|
|
309
|
+
return (
|
|
310
|
+
<div className="space-y-4">
|
|
311
|
+
{/* Caja de Texto de la Ruta y Controles Rápidos */}
|
|
312
|
+
<div className="space-y-2">
|
|
313
|
+
<div className="flex gap-2">
|
|
314
|
+
<input
|
|
315
|
+
type="text"
|
|
316
|
+
value={explorerCurrentPath}
|
|
317
|
+
onChange={(e) => setExplorerCurrentPath(e.target.value)}
|
|
318
|
+
onKeyDown={(e) => {
|
|
319
|
+
if (e.key === 'Enter') {
|
|
320
|
+
loadDirectory(explorerCurrentPath)
|
|
321
|
+
}
|
|
322
|
+
}}
|
|
323
|
+
className="flex-1 bg-[#121215] border border-[#27272a] focus:border-[#3f3f46] rounded-xl px-3.5 py-2 text-xs font-mono text-white outline-none transition"
|
|
324
|
+
placeholder="Ruta absoluta..."
|
|
325
|
+
/>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => loadDirectory(explorerCurrentPath)}
|
|
329
|
+
className="px-3 py-2 bg-white text-[#09090b] text-xs font-bold rounded-xl hover:bg-[#e4e4e7] transition cursor-pointer"
|
|
330
|
+
>
|
|
331
|
+
Ir
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div className="flex gap-2 items-center flex-wrap">
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
disabled={!explorerParentPath}
|
|
339
|
+
onClick={() => explorerParentPath && loadDirectory(explorerParentPath)}
|
|
340
|
+
className="flex items-center gap-1.5 px-3 py-1.5 bg-[#1c1c1f] hover:bg-[#27272a] disabled:opacity-40 disabled:hover:bg-[#1c1c1f] border border-[#2e2e33] rounded-lg text-xs text-white transition cursor-pointer font-medium"
|
|
341
|
+
title="Subir un nivel"
|
|
342
|
+
>
|
|
343
|
+
<ArrowUp size={12} />
|
|
344
|
+
<span>Subir Nivel</span>
|
|
345
|
+
</button>
|
|
346
|
+
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
onClick={() => loadDirectory(explorerHomePath)}
|
|
350
|
+
className="flex items-center gap-1.5 px-3 py-1.5 bg-[#1c1c1f] hover:bg-[#27272a] border border-[#2e2e33] rounded-lg text-xs text-white transition cursor-pointer font-medium"
|
|
351
|
+
title="Carpeta Home"
|
|
352
|
+
>
|
|
353
|
+
<Home size={12} />
|
|
354
|
+
<span>Home</span>
|
|
355
|
+
</button>
|
|
356
|
+
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
onClick={() => loadDirectory(explorerCwdPath)}
|
|
360
|
+
className="flex items-center gap-1.5 px-3 py-1.5 bg-[#1c1c1f] hover:bg-[#27272a] border border-[#2e2e33] rounded-lg text-xs text-white transition cursor-pointer font-medium"
|
|
361
|
+
title="Directorio Raíz del Servidor"
|
|
362
|
+
>
|
|
363
|
+
<Compass size={12} />
|
|
364
|
+
<span>CWD Servidor</span>
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{/* Mensaje de Error */}
|
|
370
|
+
{explorerError && (
|
|
371
|
+
<div className="p-3 bg-red-950/40 border border-red-900/50 rounded-xl flex items-center gap-2 text-red-400 text-xs animate-pulse">
|
|
372
|
+
<AlertCircle size={14} className="shrink-0" />
|
|
373
|
+
<span>{explorerError}</span>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{/* Lista de Carpetas */}
|
|
378
|
+
<div className="border border-[#1f1f23] rounded-xl bg-[#070709] overflow-hidden">
|
|
379
|
+
<div className="p-2 border-b border-[#1f1f23] bg-[#0c0c0e]/60 text-[10px] text-[#71717a] font-bold uppercase tracking-wider select-none">
|
|
380
|
+
Subcarpetas en este directorio:
|
|
381
|
+
</div>
|
|
382
|
+
<div className="max-h-64 overflow-y-auto p-2 space-y-1 scrollbar-thin">
|
|
383
|
+
{explorerLoading ? (
|
|
384
|
+
<div className="py-8 flex flex-col items-center justify-center text-[#71717a] space-y-2">
|
|
385
|
+
<RefreshCw size={18} className="animate-spin text-white/60" />
|
|
386
|
+
<span className="text-xs">Cargando carpetas...</span>
|
|
387
|
+
</div>
|
|
388
|
+
) : explorerDirectories.length === 0 ? (
|
|
389
|
+
<div className="py-8 text-center text-xs text-[#52525b] italic">
|
|
390
|
+
No se encontraron subcarpetas visibles en este directorio.
|
|
391
|
+
</div>
|
|
392
|
+
) : (
|
|
393
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
|
|
394
|
+
{explorerDirectories.map((dir, dIdx) => (
|
|
395
|
+
<button
|
|
396
|
+
key={dIdx}
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => {
|
|
399
|
+
const separator = explorerCurrentPath.includes('\\') ? '\\' : '/'
|
|
400
|
+
const newPath = explorerCurrentPath.endsWith(separator)
|
|
401
|
+
? explorerCurrentPath + dir
|
|
402
|
+
: explorerCurrentPath + separator + dir
|
|
403
|
+
loadDirectory(newPath)
|
|
404
|
+
}}
|
|
405
|
+
className="flex items-center gap-2 p-2 rounded-lg bg-[#0c0c0e]/40 border border-[#1c1c1f] hover:border-[#27272a] hover:bg-[#121215] text-left text-xs text-white transition cursor-pointer outline-none truncate"
|
|
406
|
+
>
|
|
407
|
+
<Folder size={14} className="text-amber-500 shrink-0" />
|
|
408
|
+
<span className="truncate font-medium">{dir}</span>
|
|
409
|
+
</button>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* Botón de Confirmación */}
|
|
417
|
+
<div className="flex justify-end pt-2">
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
onClick={() => onConfirm(explorerCurrentPath)}
|
|
421
|
+
className="flex items-center gap-1.5 px-4 py-2.5 bg-emerald-500 hover:bg-emerald-400 text-[#09090b] text-xs font-bold rounded-xl transition cursor-pointer shadow-md"
|
|
422
|
+
>
|
|
423
|
+
<Check size={14} strokeWidth={2.5} />
|
|
424
|
+
<span>Confirmar Selección</span>
|
|
425
|
+
</button>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
227
431
|
const chatContainerRef = useRef<HTMLDivElement>(null)
|
|
432
|
+
const isAbortingRef = useRef<boolean>(false)
|
|
228
433
|
const [userHasScrolledUp, setUserHasScrolledUp] = useState(false)
|
|
229
434
|
|
|
230
435
|
const navigateTo = (path: string) => {
|
|
@@ -242,6 +447,7 @@ export default function App() {
|
|
|
242
447
|
// 1. Cargar estado inicial y levantar listeners
|
|
243
448
|
useEffect(() => {
|
|
244
449
|
fetchTunnelStatus()
|
|
450
|
+
fetchSystemCwd()
|
|
245
451
|
fetchInstances()
|
|
246
452
|
fetchSessions()
|
|
247
453
|
|
|
@@ -270,7 +476,7 @@ export default function App() {
|
|
|
270
476
|
if (currentSessionId) {
|
|
271
477
|
fetchMessages(currentSessionId)
|
|
272
478
|
fetchSessionStatus()
|
|
273
|
-
fetchSessionDetails(currentSessionId)
|
|
479
|
+
fetchSessionDetails(currentSessionId, true)
|
|
274
480
|
}
|
|
275
481
|
}
|
|
276
482
|
|
|
@@ -281,34 +487,94 @@ export default function App() {
|
|
|
281
487
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
282
488
|
}, [currentSessionId, currentPort])
|
|
283
489
|
|
|
284
|
-
//
|
|
490
|
+
// Referencias para el throttle del refresco de mensajes en tiempo real
|
|
491
|
+
const lastFetchTimeRef = useRef<number>(0)
|
|
492
|
+
const pendingFetchRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
493
|
+
|
|
494
|
+
const fetchMessagesThrottled = (id: string) => {
|
|
495
|
+
if (!id) return
|
|
496
|
+
const now = Date.now()
|
|
497
|
+
const limit = 200 // refresco a lo sumo cada 200ms para un streaming ultrasuave
|
|
498
|
+
|
|
499
|
+
if (pendingFetchRef.current) {
|
|
500
|
+
return // Ya hay una petición en cola que capturará el último token
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const remaining = limit - (now - lastFetchTimeRef.current)
|
|
504
|
+
if (remaining <= 0) {
|
|
505
|
+
fetchMessages(id)
|
|
506
|
+
lastFetchTimeRef.current = now
|
|
507
|
+
} else {
|
|
508
|
+
pendingFetchRef.current = setTimeout(() => {
|
|
509
|
+
fetchMessages(id)
|
|
510
|
+
lastFetchTimeRef.current = Date.now()
|
|
511
|
+
pendingFetchRef.current = null
|
|
512
|
+
}, remaining)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 3. Sistema SSE en tiempo real con fallback de Heartbeat lento
|
|
285
517
|
useEffect(() => {
|
|
286
518
|
if (!currentSessionId) return
|
|
287
519
|
|
|
288
|
-
|
|
520
|
+
// Configurar EventSource para recibir eventos de Opencode en caliente
|
|
521
|
+
const es = new EventSource('/api/event')
|
|
522
|
+
|
|
523
|
+
const handleEvent = (event: MessageEvent) => {
|
|
524
|
+
try {
|
|
525
|
+
const data = JSON.parse(event.data)
|
|
526
|
+
const eventType = data.type || event.type
|
|
527
|
+
const sessionID = data.properties?.sessionID || data.properties?.sessionId || data.sessionId || data.session_id
|
|
528
|
+
|
|
529
|
+
if (eventType.startsWith('message.') && (!sessionID || sessionID === currentSessionId)) {
|
|
530
|
+
fetchMessagesThrottled(currentSessionId)
|
|
531
|
+
} else if (eventType.startsWith('session.') && (!sessionID || sessionID === currentSessionId)) {
|
|
532
|
+
fetchSessionStatus()
|
|
533
|
+
fetchSessionDetails(currentSessionId)
|
|
534
|
+
fetchMessagesThrottled(currentSessionId)
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
// En caso de que no sea JSON, refrescar de todos modos si es relevante
|
|
538
|
+
fetchMessagesThrottled(currentSessionId)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
es.addEventListener('message', handleEvent)
|
|
289
543
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
544
|
+
const sseEventTypes = [
|
|
545
|
+
'message.part.updated',
|
|
546
|
+
'message.part.removed',
|
|
547
|
+
'message.updated',
|
|
548
|
+
'session.status',
|
|
549
|
+
'session.idle',
|
|
550
|
+
'session.updated',
|
|
551
|
+
'session.created'
|
|
552
|
+
]
|
|
553
|
+
sseEventTypes.forEach(type => {
|
|
554
|
+
es.addEventListener(type, handleEvent)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
es.onerror = () => {
|
|
558
|
+
// El navegador reconecta automáticamente, pero podemos forzar un refresh por si acaso
|
|
297
559
|
}
|
|
298
560
|
|
|
299
|
-
// Heartbeat lento constante (cada
|
|
561
|
+
// Heartbeat lento constante (cada 6s) como red de seguridad/fallback
|
|
300
562
|
const heartbeatIntervalId = setInterval(() => {
|
|
301
563
|
fetchSessionStatus()
|
|
302
|
-
|
|
564
|
+
fetchMessagesThrottled(currentSessionId)
|
|
303
565
|
fetchSessionDetails(currentSessionId)
|
|
304
|
-
},
|
|
566
|
+
}, 6000)
|
|
305
567
|
|
|
306
568
|
return () => {
|
|
307
|
-
|
|
569
|
+
es.close()
|
|
570
|
+
if (pendingFetchRef.current) {
|
|
571
|
+
clearTimeout(pendingFetchRef.current)
|
|
572
|
+
pendingFetchRef.current = null
|
|
573
|
+
}
|
|
308
574
|
clearInterval(heartbeatIntervalId)
|
|
309
575
|
}
|
|
310
576
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
311
|
-
}, [currentSessionId, currentPort
|
|
577
|
+
}, [currentSessionId, currentPort])
|
|
312
578
|
|
|
313
579
|
// 3. Scroll automático inteligente al recibir nuevos mensajes
|
|
314
580
|
useEffect(() => {
|
|
@@ -383,6 +649,52 @@ export default function App() {
|
|
|
383
649
|
}
|
|
384
650
|
}
|
|
385
651
|
|
|
652
|
+
async function fetchSystemCwd() {
|
|
653
|
+
try {
|
|
654
|
+
const res = await fetch('/api-custom/cwd')
|
|
655
|
+
if (res.ok) {
|
|
656
|
+
const data = await res.json()
|
|
657
|
+
if (data.cwd) {
|
|
658
|
+
setSystemCwd(data.cwd)
|
|
659
|
+
|
|
660
|
+
setProjects(prev => {
|
|
661
|
+
const hasSaved = localStorage.getItem('zugzweb_projects')
|
|
662
|
+
if (!hasSaved || prev.length === 0) {
|
|
663
|
+
return [
|
|
664
|
+
{
|
|
665
|
+
id: 'workspace_raiz',
|
|
666
|
+
name: 'Carpeta Base Zugz',
|
|
667
|
+
path: data.cwd
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Si tiene proyectos guardados, nos aseguramos de que "workspace_raiz" apunte dinámicamente al cwd de esta ejecución
|
|
673
|
+
const baseProjExists = prev.some(p => p.id === 'workspace_raiz')
|
|
674
|
+
if (!baseProjExists) {
|
|
675
|
+
return [
|
|
676
|
+
{
|
|
677
|
+
id: 'workspace_raiz',
|
|
678
|
+
name: 'Carpeta Base Zugz',
|
|
679
|
+
path: data.cwd
|
|
680
|
+
},
|
|
681
|
+
...prev
|
|
682
|
+
]
|
|
683
|
+
}
|
|
684
|
+
return prev.map(p => {
|
|
685
|
+
if (p.id === 'workspace_raiz') {
|
|
686
|
+
return { ...p, name: 'Carpeta Base Zugz', path: data.cwd }
|
|
687
|
+
}
|
|
688
|
+
return p
|
|
689
|
+
})
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('Error al obtener cwd:', error)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
386
698
|
async function fetchInstances() {
|
|
387
699
|
try {
|
|
388
700
|
const res = await fetch('/api-custom/instances')
|
|
@@ -403,14 +715,33 @@ export default function App() {
|
|
|
403
715
|
const data = await res.json()
|
|
404
716
|
const sessionList = Array.isArray(data) ? data : (data.sessions || [])
|
|
405
717
|
setSessions(sessionList)
|
|
718
|
+
|
|
719
|
+
// Registrar de forma permanente en localStorage el mapeo de las sesiones descubiertas al proyecto activo
|
|
720
|
+
setSessionMappings(prev => {
|
|
721
|
+
let updated = false
|
|
722
|
+
const nextMappings = { ...prev }
|
|
723
|
+
for (const s of sessionList) {
|
|
724
|
+
if (!nextMappings[s.id]) {
|
|
725
|
+
nextMappings[s.id] = activeProjectId
|
|
726
|
+
updated = true
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return updated ? nextMappings : prev
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
// Limpiar errores inmediatamente al lograr conectar exitosamente con la API de Opencode
|
|
733
|
+
setErrorMsg('')
|
|
734
|
+
|
|
406
735
|
if (sessionList.length > 0) {
|
|
407
|
-
|
|
736
|
+
// Validar que la sesión actual exista de verdad en el servidor activo
|
|
737
|
+
const exists = sessionList.some((s: Session) => s.id === currentSessionIdRef.current)
|
|
738
|
+
if (!currentSessionIdRef.current || !exists) {
|
|
739
|
+
// Si no hay sesión seleccionada o es un ID huérfano viejo, auto-seleccionar la primera disponible
|
|
408
740
|
setCurrentSessionId(sessionList[0].id)
|
|
409
741
|
}
|
|
410
742
|
} else {
|
|
411
743
|
navigateTo('/new')
|
|
412
744
|
}
|
|
413
|
-
setErrorMsg('')
|
|
414
745
|
} else {
|
|
415
746
|
const errData = await res.json().catch(() => ({}))
|
|
416
747
|
setErrorMsg(errData.message || 'No se pudo conectar con Opencode. ¿Has iniciado "opencode serve"?')
|
|
@@ -420,19 +751,20 @@ export default function App() {
|
|
|
420
751
|
}
|
|
421
752
|
}
|
|
422
753
|
|
|
423
|
-
async function fetchSessionDetails(id: string) {
|
|
754
|
+
async function fetchSessionDetails(id: string, isInitialLoad = false) {
|
|
424
755
|
try {
|
|
425
756
|
const res = await fetch(`/api/session/${id}`)
|
|
426
757
|
if (res.ok) {
|
|
427
758
|
const data = await res.json()
|
|
428
759
|
setCurrentSession(data)
|
|
429
|
-
if (data.agent) {
|
|
760
|
+
if (data.agent && isInitialLoad) {
|
|
430
761
|
setSelectedAgent(data.agent)
|
|
431
762
|
}
|
|
432
763
|
setErrorMsg('')
|
|
433
764
|
} else {
|
|
434
|
-
|
|
435
|
-
|
|
765
|
+
// ID de sesión huérfano. Limpiamos para que fetchSessions auto-asigne uno nuevo válido en el próximo ciclo
|
|
766
|
+
setCurrentSessionId('')
|
|
767
|
+
setCurrentSession(null)
|
|
436
768
|
}
|
|
437
769
|
} catch (error) {
|
|
438
770
|
console.error(error)
|
|
@@ -468,12 +800,14 @@ export default function App() {
|
|
|
468
800
|
|
|
469
801
|
if (isBusy) {
|
|
470
802
|
setIsProcessing(prev => {
|
|
803
|
+
if (isAbortingRef.current) return false
|
|
471
804
|
if (!prev) {
|
|
472
805
|
return true
|
|
473
806
|
}
|
|
474
807
|
return prev
|
|
475
808
|
})
|
|
476
809
|
} else {
|
|
810
|
+
isAbortingRef.current = false
|
|
477
811
|
setIsProcessing(prev => {
|
|
478
812
|
if (prev) {
|
|
479
813
|
showNotification('Opencode: ¡Ejecución Completada! 🎉', 'El agente ha finalizado el procesamiento de tu solicitud con éxito.')
|
|
@@ -500,11 +834,12 @@ export default function App() {
|
|
|
500
834
|
e.preventDefault()
|
|
501
835
|
if (!newProjectName.trim() || !newProjectPath.trim()) return
|
|
502
836
|
|
|
503
|
-
// Asegurarse de que la ruta sea absoluta. Si es una subcarpeta relativa, concatenar a la
|
|
837
|
+
// Asegurarse de que la ruta sea absoluta. Si es una subcarpeta relativa, concatenar a la Carpeta Base Zugz
|
|
504
838
|
let resolvedPath = newProjectPath.trim()
|
|
505
839
|
if (!resolvedPath.startsWith('/')) {
|
|
506
|
-
const
|
|
507
|
-
|
|
840
|
+
const baseProject = projects.find(p => p.id === 'workspace_raiz')
|
|
841
|
+
const basePath = baseProject ? baseProject.path : systemCwd
|
|
842
|
+
resolvedPath = `${basePath}/${resolvedPath}`
|
|
508
843
|
}
|
|
509
844
|
|
|
510
845
|
const newId = `project_${Date.now()}`
|
|
@@ -567,6 +902,53 @@ export default function App() {
|
|
|
567
902
|
setExpandedProjects(prev => ({ ...prev, [projectId]: !prev[projectId] }))
|
|
568
903
|
}
|
|
569
904
|
|
|
905
|
+
// Cambia de sesión y conmuta de forma automática el proyecto activo si es necesario
|
|
906
|
+
const handleSelectSession = async (sessionId: string, projectId: string) => {
|
|
907
|
+
const project = projects.find(p => p.id === projectId)
|
|
908
|
+
if (!project) return
|
|
909
|
+
|
|
910
|
+
if (activeProjectId !== projectId) {
|
|
911
|
+
setIsProcessing(true)
|
|
912
|
+
setErrorMsg(`Activando entorno de "${project.name}"...`)
|
|
913
|
+
try {
|
|
914
|
+
const res = await fetch('/api-custom/activate-project', {
|
|
915
|
+
method: 'POST',
|
|
916
|
+
headers: { 'Content-Type': 'application/json' },
|
|
917
|
+
body: JSON.stringify({ path: project.path })
|
|
918
|
+
})
|
|
919
|
+
if (res.ok) {
|
|
920
|
+
setActiveProjectId(projectId)
|
|
921
|
+
|
|
922
|
+
// Limpiar de forma temporal para evitar ruidos de la sesión anterior
|
|
923
|
+
setSessions([])
|
|
924
|
+
setCurrentSessionId('')
|
|
925
|
+
setCurrentSession(null)
|
|
926
|
+
setMessages([])
|
|
927
|
+
|
|
928
|
+
// Forzar la recarga de sesiones de este proyecto
|
|
929
|
+
await fetchSessions()
|
|
930
|
+
|
|
931
|
+
// Establecer la sesión seleccionada
|
|
932
|
+
setCurrentSessionId(sessionId)
|
|
933
|
+
setErrorMsg('')
|
|
934
|
+
navigateTo('/')
|
|
935
|
+
} else {
|
|
936
|
+
const errData = await res.json().catch(() => ({}))
|
|
937
|
+
setErrorMsg(errData.error || 'Error al activar el proyecto para esta sesión')
|
|
938
|
+
}
|
|
939
|
+
} catch {
|
|
940
|
+
setErrorMsg('Error de conexión al activar el proyecto de la sesión')
|
|
941
|
+
} finally {
|
|
942
|
+
setIsProcessing(false)
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
// Si ya está en el proyecto activo, simplemente la seleccionamos
|
|
946
|
+
setCurrentSessionId(sessionId)
|
|
947
|
+
setErrorMsg('')
|
|
948
|
+
navigateTo('/')
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
570
952
|
// Activa un proyecto levantando opencode serve en su directorio en segundo plano
|
|
571
953
|
const handleActivateProject = async (projectId: string) => {
|
|
572
954
|
const project = projects.find(p => p.id === projectId)
|
|
@@ -655,6 +1037,12 @@ export default function App() {
|
|
|
655
1037
|
setCurrentSessionId(newSess.id)
|
|
656
1038
|
setCurrentSession(newSess)
|
|
657
1039
|
setMessages([])
|
|
1040
|
+
|
|
1041
|
+
// Registrar el título personalizado en el diccionario local
|
|
1042
|
+
setCustomSessionTitles(prev => ({
|
|
1043
|
+
...prev,
|
|
1044
|
+
[newSess.id]: title.trim()
|
|
1045
|
+
}))
|
|
658
1046
|
|
|
659
1047
|
// Guardar mapeo sesión -> proyecto
|
|
660
1048
|
setSessionMappings(prev => ({
|
|
@@ -753,6 +1141,12 @@ export default function App() {
|
|
|
753
1141
|
setCurrentSessionId(newSess.id)
|
|
754
1142
|
setCurrentSession(newSess)
|
|
755
1143
|
setMessages([])
|
|
1144
|
+
|
|
1145
|
+
// Registrar el título personalizado en el diccionario local
|
|
1146
|
+
setCustomSessionTitles(prev => ({
|
|
1147
|
+
...prev,
|
|
1148
|
+
[newSess.id]: newSessionTitle.trim()
|
|
1149
|
+
}))
|
|
756
1150
|
|
|
757
1151
|
// Guardar el mapeo de la sesión al proyecto activo
|
|
758
1152
|
setSessionMappings(prev => ({
|
|
@@ -923,17 +1317,23 @@ export default function App() {
|
|
|
923
1317
|
|
|
924
1318
|
const handleAbort = async () => {
|
|
925
1319
|
if (!currentSessionId) return
|
|
1320
|
+
isAbortingRef.current = true
|
|
1321
|
+
setIsProcessing(false) // Optimista
|
|
926
1322
|
try {
|
|
927
1323
|
const res = await fetch(`/api/session/${currentSessionId}/abort`, {
|
|
928
1324
|
method: 'POST'
|
|
929
1325
|
})
|
|
930
1326
|
if (res.ok) {
|
|
931
|
-
setIsProcessing(false)
|
|
932
1327
|
showNotification('Opencode: Abortado', 'La sesión en ejecución ha sido cancelada forzadamente.')
|
|
933
1328
|
triggerImmediateUpdate()
|
|
934
1329
|
}
|
|
935
1330
|
} catch (e) {
|
|
936
1331
|
console.error(e)
|
|
1332
|
+
} finally {
|
|
1333
|
+
// Dejar que transcurran 2.5 segundos para que el servidor complete el aborto y se asiente
|
|
1334
|
+
setTimeout(() => {
|
|
1335
|
+
isAbortingRef.current = false
|
|
1336
|
+
}, 2500)
|
|
937
1337
|
}
|
|
938
1338
|
}
|
|
939
1339
|
|
|
@@ -982,7 +1382,7 @@ export default function App() {
|
|
|
982
1382
|
return sessions.filter(s => s.parent_id === parentId || s.parentID === parentId)
|
|
983
1383
|
}
|
|
984
1384
|
|
|
985
|
-
// Filtrar rootSessions por proyecto
|
|
1385
|
+
// Filtrar rootSessions por proyecto, asociando dinámicamente las sesiones sin mapear al proyecto activo actual
|
|
986
1386
|
const getSessionsForProject = (projectId: string) => {
|
|
987
1387
|
return rootSessions.filter(s => {
|
|
988
1388
|
const mappedId = sessionMappings[s.id]
|
|
@@ -991,7 +1391,10 @@ export default function App() {
|
|
|
991
1391
|
return mappedId === projectId
|
|
992
1392
|
}
|
|
993
1393
|
}
|
|
994
|
-
|
|
1394
|
+
|
|
1395
|
+
// Si la sesión no tiene un mapeo de proyecto en localStorage, asumimos
|
|
1396
|
+
// que pertenece al entorno del proyecto que está actualmente activo y conectado
|
|
1397
|
+
return activeProjectId === projectId
|
|
995
1398
|
})
|
|
996
1399
|
}
|
|
997
1400
|
|
|
@@ -1001,6 +1404,85 @@ export default function App() {
|
|
|
1001
1404
|
// Obtener la instancia activa del Daemon
|
|
1002
1405
|
const activeInstance = instances.find(inst => inst.port === currentPort)
|
|
1003
1406
|
|
|
1407
|
+
if (!setupCompleted) {
|
|
1408
|
+
return (
|
|
1409
|
+
<div className="flex h-screen bg-[#09090b] text-[#f4f4f5] items-center justify-center p-4 overflow-hidden select-none">
|
|
1410
|
+
<div className="w-full max-w-2xl bg-[#0c0c0e] border border-[#1f1f23] rounded-2xl shadow-2xl p-6 md:p-8 space-y-6 flex flex-col max-h-[90vh]">
|
|
1411
|
+
{/* Cabecera */}
|
|
1412
|
+
<div className="text-center space-y-2 shrink-0">
|
|
1413
|
+
<div className="inline-flex p-3 bg-white text-[#09090b] rounded-2xl shadow-md mb-2">
|
|
1414
|
+
<Terminal size={28} className="font-bold animate-pulse" />
|
|
1415
|
+
</div>
|
|
1416
|
+
<h1 className="text-2xl font-black tracking-tight text-white flex items-center justify-center gap-2">
|
|
1417
|
+
<span>Bienvenido a ZugzWeb</span>
|
|
1418
|
+
<span className="text-[10px] bg-emerald-950 text-emerald-400 border border-emerald-900/50 px-1.5 py-0.5 rounded font-bold uppercase tracking-wider">Harness v1.2</span>
|
|
1419
|
+
</h1>
|
|
1420
|
+
<p className="text-xs text-[#a1a1aa] max-w-md mx-auto leading-relaxed">
|
|
1421
|
+
Para comenzar a programar de forma autónoma con tu arnés SDD, necesitamos que selecciones la carpeta de tu computadora que servirá como tu espacio de trabajo inicial.
|
|
1422
|
+
</p>
|
|
1423
|
+
</div>
|
|
1424
|
+
|
|
1425
|
+
{/* Cuerpo del Explorador */}
|
|
1426
|
+
<div className="flex-1 overflow-y-auto py-1 scrollbar-none">
|
|
1427
|
+
{renderExplorerContent(async (selectedPath) => {
|
|
1428
|
+
// 1. Guardar el Workspace Raíz inicial
|
|
1429
|
+
const initialProjects = [
|
|
1430
|
+
{
|
|
1431
|
+
id: 'workspace_raiz',
|
|
1432
|
+
name: 'Carpeta Base Zugz',
|
|
1433
|
+
path: selectedPath
|
|
1434
|
+
}
|
|
1435
|
+
]
|
|
1436
|
+
setProjects(initialProjects)
|
|
1437
|
+
localStorage.setItem('zugzweb_projects', JSON.stringify(initialProjects))
|
|
1438
|
+
|
|
1439
|
+
setActiveProjectId('workspace_raiz')
|
|
1440
|
+
localStorage.setItem('zugzweb_active_project_id', 'workspace_raiz')
|
|
1441
|
+
|
|
1442
|
+
// 2. Activar el proyecto llamando al Daemon en segundo plano
|
|
1443
|
+
setIsProcessing(true)
|
|
1444
|
+
setErrorMsg('Activando entorno de desarrollo inicial...')
|
|
1445
|
+
try {
|
|
1446
|
+
// Asegurarse de que exista físicamente llamando a /api-custom/create-folder
|
|
1447
|
+
await fetch('/api-custom/create-folder', {
|
|
1448
|
+
method: 'POST',
|
|
1449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1450
|
+
body: JSON.stringify({ path: selectedPath })
|
|
1451
|
+
})
|
|
1452
|
+
|
|
1453
|
+
await fetch('/api-custom/activate-project', {
|
|
1454
|
+
method: 'POST',
|
|
1455
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1456
|
+
body: JSON.stringify({ path: selectedPath })
|
|
1457
|
+
})
|
|
1458
|
+
|
|
1459
|
+
// 3. Completar Setup
|
|
1460
|
+
localStorage.setItem('zugzweb_setup_completed', 'true')
|
|
1461
|
+
setSetupCompleted(true)
|
|
1462
|
+
|
|
1463
|
+
// Forzar carga de sesiones de este nuevo directorio
|
|
1464
|
+
setTimeout(() => {
|
|
1465
|
+
fetchSessions()
|
|
1466
|
+
fetchInstances()
|
|
1467
|
+
}, 1000)
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
console.error(err)
|
|
1470
|
+
} finally {
|
|
1471
|
+
setIsProcessing(false)
|
|
1472
|
+
setErrorMsg('')
|
|
1473
|
+
}
|
|
1474
|
+
})}
|
|
1475
|
+
</div>
|
|
1476
|
+
|
|
1477
|
+
{/* Footer Informativo */}
|
|
1478
|
+
<div className="text-center text-[10px] text-[#52525b] border-t border-[#1f1f23]/60 pt-4 shrink-0">
|
|
1479
|
+
Podrás añadir, explorar o alternar más carpetas de proyectos en cualquier momento desde la barra lateral.
|
|
1480
|
+
</div>
|
|
1481
|
+
</div>
|
|
1482
|
+
</div>
|
|
1483
|
+
)
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1004
1486
|
return (
|
|
1005
1487
|
<div className="flex h-screen bg-[#09090b] text-[#f4f4f5] overflow-hidden antialiased font-sans select-none">
|
|
1006
1488
|
|
|
@@ -1059,14 +1541,31 @@ export default function App() {
|
|
|
1059
1541
|
</div>
|
|
1060
1542
|
<div className="space-y-1">
|
|
1061
1543
|
<label className="text-[9px] font-bold text-[#71717a] uppercase">Subcarpeta o Ruta Absoluta</label>
|
|
1062
|
-
<
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1544
|
+
<div className="flex gap-2">
|
|
1545
|
+
<input
|
|
1546
|
+
type="text"
|
|
1547
|
+
required
|
|
1548
|
+
value={newProjectPath}
|
|
1549
|
+
onChange={e => setNewProjectPath(e.target.value)}
|
|
1550
|
+
placeholder="Ej. mi-proyecto o /Users/nombre/mi-proyecto"
|
|
1551
|
+
className="flex-1 bg-[#0c0c0e] border border-[#27272a] rounded-lg px-2.5 py-1.5 text-xs text-white outline-none focus:border-[#3f3f46] font-mono"
|
|
1552
|
+
/>
|
|
1553
|
+
<button
|
|
1554
|
+
type="button"
|
|
1555
|
+
onClick={() => openExplorer('Buscar Carpeta del Proyecto', newProjectPath || systemCwd, (selected) => setNewProjectPath(selected))}
|
|
1556
|
+
className="px-2.5 py-1 bg-[#1c1c1f] hover:bg-[#27272a] border border-[#2e2e33] text-[10px] rounded-lg text-white font-semibold transition cursor-pointer shrink-0"
|
|
1557
|
+
>
|
|
1558
|
+
Buscar...
|
|
1559
|
+
</button>
|
|
1560
|
+
</div>
|
|
1561
|
+
<div className="text-[9px] text-[#71717a] bg-[#09090b]/40 p-2 rounded border border-[#1f1f23] font-mono break-all leading-normal mt-1">
|
|
1562
|
+
<span className="text-[#a1a1aa] font-semibold">📁 Ruta Destino: </span>
|
|
1563
|
+
<span className="text-emerald-400">
|
|
1564
|
+
{newProjectPath.trim().startsWith('/')
|
|
1565
|
+
? newProjectPath.trim()
|
|
1566
|
+
: `${systemCwd}/${newProjectPath.trim() || 'mi-proyecto'}`}
|
|
1567
|
+
</span>
|
|
1568
|
+
</div>
|
|
1070
1569
|
</div>
|
|
1071
1570
|
<div className="flex gap-2 justify-end pt-1">
|
|
1072
1571
|
<button
|
|
@@ -1121,14 +1620,44 @@ export default function App() {
|
|
|
1121
1620
|
</button>
|
|
1122
1621
|
|
|
1123
1622
|
<div className="flex items-center gap-1 shrink-0 ml-1">
|
|
1623
|
+
{project.id !== 'workspace_raiz' && (
|
|
1624
|
+
<button
|
|
1625
|
+
onClick={() => handleCreateSessionInProject(project.id)}
|
|
1626
|
+
className="p-1 hover:bg-[#27272a] text-emerald-500 hover:text-emerald-400 rounded transition cursor-pointer"
|
|
1627
|
+
title="Crear Sesión en este proyecto"
|
|
1628
|
+
>
|
|
1629
|
+
<Plus size={10} strokeWidth={3} />
|
|
1630
|
+
</button>
|
|
1631
|
+
)}
|
|
1124
1632
|
<button
|
|
1125
|
-
onClick={() =>
|
|
1126
|
-
|
|
1127
|
-
|
|
1633
|
+
onClick={() => {
|
|
1634
|
+
openExplorer(`Carpeta para: ${project.name}`, project.path, async (selected) => {
|
|
1635
|
+
setProjects(prev => prev.map(p => p.id === project.id ? { ...p, path: selected } : p))
|
|
1636
|
+
if (project.id === activeProjectId) {
|
|
1637
|
+
setIsProcessing(true)
|
|
1638
|
+
setErrorMsg('Re-activando proyecto en nueva ruta...')
|
|
1639
|
+
try {
|
|
1640
|
+
await fetch('/api-custom/activate-project', {
|
|
1641
|
+
method: 'POST',
|
|
1642
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1643
|
+
body: JSON.stringify({ path: selected })
|
|
1644
|
+
})
|
|
1645
|
+
fetchSessions()
|
|
1646
|
+
} catch (e) {
|
|
1647
|
+
console.error(e)
|
|
1648
|
+
} finally {
|
|
1649
|
+
setIsProcessing(false)
|
|
1650
|
+
setErrorMsg('')
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
})
|
|
1654
|
+
}}
|
|
1655
|
+
className="p-1 hover:bg-[#27272a] text-[#71717a] hover:text-white rounded transition cursor-pointer"
|
|
1656
|
+
title="Explorar/Cambiar Carpeta del Proyecto"
|
|
1128
1657
|
>
|
|
1129
|
-
<
|
|
1658
|
+
<FolderOpen size={10} />
|
|
1130
1659
|
</button>
|
|
1131
|
-
{!isProjectActive && (
|
|
1660
|
+
{!isProjectActive && project.id !== 'workspace_raiz' && (
|
|
1132
1661
|
<button
|
|
1133
1662
|
onClick={() => handleActivateProject(project.id)}
|
|
1134
1663
|
className="p-1 hover:bg-[#27272a] text-[#71717a] hover:text-white rounded transition cursor-pointer"
|
|
@@ -1152,7 +1681,14 @@ export default function App() {
|
|
|
1152
1681
|
{/* Lista de sesiones anidadas del proyecto */}
|
|
1153
1682
|
{isExpanded && (
|
|
1154
1683
|
<div className="ml-3 pl-2.5 border-l border-[#1f1f23] space-y-1 pt-1.5 pb-0.5">
|
|
1155
|
-
{
|
|
1684
|
+
{project.id === 'workspace_raiz' ? (
|
|
1685
|
+
<div className="text-[10px] text-[#71717a] py-1 pl-1 space-y-1 pr-2 leading-relaxed">
|
|
1686
|
+
<span className="block font-bold text-amber-500/90">📁 Carpeta Base Central</span>
|
|
1687
|
+
<p className="text-[#8e8e93]">
|
|
1688
|
+
Para iniciar chats de desarrollo, crea primero un subproyecto con el botón <strong className="text-white">"+ Proyecto"</strong> de arriba.
|
|
1689
|
+
</p>
|
|
1690
|
+
</div>
|
|
1691
|
+
) : projectSessions.length === 0 ? (
|
|
1156
1692
|
<div className="text-[10px] text-[#52525b] italic py-1 pl-1">
|
|
1157
1693
|
No hay sesiones en este proyecto. Haz clic en el botón + de arriba para iniciar una.
|
|
1158
1694
|
</div>
|
|
@@ -1172,14 +1708,12 @@ export default function App() {
|
|
|
1172
1708
|
}`}>
|
|
1173
1709
|
<button
|
|
1174
1710
|
onClick={() => {
|
|
1175
|
-
|
|
1176
|
-
setErrorMsg('')
|
|
1177
|
-
navigateTo('/')
|
|
1711
|
+
handleSelectSession(root.id, project.id)
|
|
1178
1712
|
}}
|
|
1179
1713
|
className="flex-1 text-left cursor-pointer outline-none min-w-0"
|
|
1180
1714
|
>
|
|
1181
1715
|
<span className="font-semibold truncate text-[11px] block">
|
|
1182
|
-
💬 {root.title || `Sesión: ${root.id.substring(0, 8)}`}
|
|
1716
|
+
💬 {customSessionTitles[root.id] || root.title || `Sesión: ${root.id.substring(0, 8)}`}
|
|
1183
1717
|
</span>
|
|
1184
1718
|
<div className="flex items-center justify-between text-[9px] text-[#52525b] mt-0.5">
|
|
1185
1719
|
<span>{root.agent || 'sdd-orchestrator'}</span>
|
|
@@ -1206,9 +1740,7 @@ export default function App() {
|
|
|
1206
1740
|
<button
|
|
1207
1741
|
key={sub.id}
|
|
1208
1742
|
onClick={() => {
|
|
1209
|
-
|
|
1210
|
-
setErrorMsg('')
|
|
1211
|
-
navigateTo('/')
|
|
1743
|
+
handleSelectSession(sub.id, project.id)
|
|
1212
1744
|
}}
|
|
1213
1745
|
className={`w-full text-left p-1.5 rounded-md flex flex-col gap-0.5 transition duration-150 cursor-pointer text-[10px] ${
|
|
1214
1746
|
isSubActive
|
|
@@ -1269,7 +1801,7 @@ export default function App() {
|
|
|
1269
1801
|
}`}
|
|
1270
1802
|
>
|
|
1271
1803
|
<span className="font-semibold truncate text-[11px]">
|
|
1272
|
-
💬 {root.title || `Sesión: ${root.id.substring(0, 8)}`}
|
|
1804
|
+
💬 {customSessionTitles[root.id] || root.title || `Sesión: ${root.id.substring(0, 8)}`}
|
|
1273
1805
|
</span>
|
|
1274
1806
|
<div className="flex items-center justify-between text-[9px] text-[#52525b]">
|
|
1275
1807
|
<span>{root.agent || 'sdd-orchestrator'}</span>
|
|
@@ -1519,7 +2051,7 @@ export default function App() {
|
|
|
1519
2051
|
<div className="h-14 border-b border-[#1f1f23] flex items-center justify-between px-6 bg-[#09090b]/80 backdrop-blur-md z-10 shrink-0">
|
|
1520
2052
|
<div className="flex flex-col">
|
|
1521
2053
|
<h1 className="text-sm font-bold m-0 p-0 text-[#fafafa] flex items-center gap-2">
|
|
1522
|
-
{currentSession?.title || 'Cargando sesión...'}
|
|
2054
|
+
{(currentSession && customSessionTitles[currentSession.id]) || currentSession?.title || 'Cargando sesión...'}
|
|
1523
2055
|
{isProcessing && (
|
|
1524
2056
|
<span className="flex h-2 w-2 relative">
|
|
1525
2057
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#22c55e] opacity-75"></span>
|
|
@@ -1832,6 +2364,30 @@ export default function App() {
|
|
|
1832
2364
|
)
|
|
1833
2365
|
})
|
|
1834
2366
|
)}
|
|
2367
|
+
|
|
2368
|
+
{/* INDICADOR DE CARGA EN TIEMPO REAL DEL AGENTE */}
|
|
2369
|
+
{isProcessing && messages.length > 0 && messages[messages.length - 1].info.role === 'user' && (
|
|
2370
|
+
<div className="flex gap-4 max-w-4xl mx-auto justify-start">
|
|
2371
|
+
<div className="h-8 w-8 rounded-full border border-[#2e2e33] bg-[#18181b] flex items-center justify-center text-[#fafafa] shrink-0">
|
|
2372
|
+
<Bot size={16} className="animate-spin text-emerald-400" style={{ animationDuration: '4s' }} />
|
|
2373
|
+
</div>
|
|
2374
|
+
<div className="rounded-xl px-4 py-3 bg-[#0c0c0e] text-[#f4f4f5] border border-[#1f1f23] max-w-[85%] shadow-sm space-y-1">
|
|
2375
|
+
<div className="flex justify-between items-start border-b border-[#1f1f23]/40 pb-1 text-[11px] text-[#71717a] font-medium gap-2">
|
|
2376
|
+
<div className="flex items-center gap-2">
|
|
2377
|
+
<span>AGENTE</span>
|
|
2378
|
+
<span>•</span>
|
|
2379
|
+
<span className="text-[10px] text-emerald-500 font-bold animate-pulse">PENSANDO...</span>
|
|
2380
|
+
</div>
|
|
2381
|
+
</div>
|
|
2382
|
+
<div className="flex items-center gap-1.5 text-sm text-[#a1a1aa] py-1.5">
|
|
2383
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-bounce" style={{ animationDelay: '0ms' }}></span>
|
|
2384
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-bounce" style={{ animationDelay: '150ms' }}></span>
|
|
2385
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-bounce" style={{ animationDelay: '300ms' }}></span>
|
|
2386
|
+
<span className="ml-1 text-xs text-[#a1a1aa] font-medium">Conectando con el modelo de IA...</span>
|
|
2387
|
+
</div>
|
|
2388
|
+
</div>
|
|
2389
|
+
</div>
|
|
2390
|
+
)}
|
|
1835
2391
|
</div>
|
|
1836
2392
|
|
|
1837
2393
|
{/* CONSOLA DE ENTRADA, SELECCIÓN DE MODELO Y ENVÍO */}
|
|
@@ -1948,6 +2504,38 @@ export default function App() {
|
|
|
1948
2504
|
)}
|
|
1949
2505
|
</div>
|
|
1950
2506
|
|
|
2507
|
+
{/* MODAL DEL EXPLORADOR DE ARCHIVOS GENERAL */}
|
|
2508
|
+
{showExplorerModal && (
|
|
2509
|
+
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 z-[9999] animate-fadeIn">
|
|
2510
|
+
<div className="w-full max-w-xl bg-[#0c0c0e] border border-[#1f1f23] rounded-2xl shadow-2xl p-6 space-y-4 flex flex-col max-h-[85vh]">
|
|
2511
|
+
{/* Header del Modal */}
|
|
2512
|
+
<div className="flex items-center justify-between shrink-0 border-b border-[#1f1f23] pb-3">
|
|
2513
|
+
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
|
2514
|
+
<FolderOpen size={16} className="text-[#3b82f6]" />
|
|
2515
|
+
<span>{explorerTitle}</span>
|
|
2516
|
+
</h3>
|
|
2517
|
+
<button
|
|
2518
|
+
type="button"
|
|
2519
|
+
onClick={() => setShowExplorerModal(false)}
|
|
2520
|
+
className="text-xs text-[#71717a] hover:text-white border border-[#1f1f23] hover:border-[#27272a] px-2.5 py-1 rounded-md transition cursor-pointer"
|
|
2521
|
+
>
|
|
2522
|
+
Cerrar
|
|
2523
|
+
</button>
|
|
2524
|
+
</div>
|
|
2525
|
+
|
|
2526
|
+
{/* Contenido del Explorador */}
|
|
2527
|
+
<div className="flex-1 overflow-y-auto scrollbar-none py-1">
|
|
2528
|
+
{renderExplorerContent((selectedPath) => {
|
|
2529
|
+
if (explorerCallback) {
|
|
2530
|
+
explorerCallback(selectedPath)
|
|
2531
|
+
}
|
|
2532
|
+
setShowExplorerModal(false)
|
|
2533
|
+
})}
|
|
2534
|
+
</div>
|
|
2535
|
+
</div>
|
|
2536
|
+
</div>
|
|
2537
|
+
)}
|
|
2538
|
+
|
|
1951
2539
|
</div>
|
|
1952
2540
|
)
|
|
1953
2541
|
}
|