@aion0/forge 0.5.29 → 0.5.31

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/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,8 @@
1
- # Forge v0.5.29
1
+ # Forge v0.5.31
2
2
 
3
- Released: 2026-04-09
3
+ Released: 2026-04-10
4
4
 
5
- ## Changes since v0.5.28
5
+ ## Changes since v0.5.30
6
6
 
7
- ### Bug Fixes
8
- - fix: auto-reconnect workspace terminal WebSocket on disconnect
9
7
 
10
-
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.28...v0.5.29
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.30...v0.5.31
@@ -85,6 +85,9 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
85
85
  const [projectSkills, setProjectSkills] = useState<{ name: string; displayName: string; type: string; scope: string; version: string; installedVersion: string; hasUpdate: boolean; source: 'registry' | 'local' }[]>([]);
86
86
  const [showSkillsDetail, setShowSkillsDetail] = useState(false);
87
87
  const [projectTab, setProjectTab] = useState<'workspace' | 'sessions' | 'code' | 'skills' | 'claudemd' | 'pipelines'>('code');
88
+ // Lazy-mount workspace: only mount after first visit, keep mounted to preserve terminal state
89
+ const [wsMounted, setWsMounted] = useState(false);
90
+ useEffect(() => { if (projectTab === 'workspace') setWsMounted(true); }, [projectTab]);
88
91
  const wsViewRef = useRef<import('./WorkspaceView').WorkspaceViewHandle>(null);
89
92
  // Pipeline bindings state
90
93
  const [pipelineBindings, setPipelineBindings] = useState<{ id: number; workflowName: string; enabled: boolean; config: any; lastRunAt: string | null; nextRunAt: string | null }[]>([]);
@@ -566,52 +569,52 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
566
569
  </div>
567
570
  {/* Tab switcher */}
568
571
  <div className="flex items-center gap-2 mt-1.5">
569
- <div className="flex bg-[var(--bg-tertiary)] rounded p-0.5">
572
+ <div className="flex bg-[var(--bg-tertiary)] rounded p-1 gap-0.5">
570
573
  <button
571
574
  onClick={() => setProjectTab('code')}
572
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
573
- projectTab === 'code' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
575
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
576
+ projectTab === 'code' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
574
577
  }`}
575
578
  >Code</button>
576
579
  <button
577
580
  onClick={() => setProjectTab('workspace')}
578
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
579
- projectTab === 'workspace' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
581
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
582
+ projectTab === 'workspace' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
580
583
  }`}
581
584
  >🔨 Workspace</button>
582
585
  <button
583
586
  onClick={() => setProjectTab('sessions')}
584
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
585
- projectTab === 'sessions' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
587
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
588
+ projectTab === 'sessions' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
586
589
  }`}
587
590
  >Sessions</button>
588
591
  <button
589
592
  onClick={() => setProjectTab('skills')}
590
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
591
- projectTab === 'skills' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
593
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
594
+ projectTab === 'skills' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
592
595
  }`}
593
596
  >
594
597
  Skills & Cmds
595
- {projectSkills.length > 0 && <span className="ml-1 text-[8px] text-[var(--text-secondary)]">({projectSkills.length})</span>}
596
- {projectSkills.some(s => s.hasUpdate) && <span className="ml-1 text-[8px] text-[var(--yellow)]">!</span>}
598
+ {projectSkills.length > 0 && <span className="ml-1 text-[9px] opacity-70">({projectSkills.length})</span>}
599
+ {projectSkills.some(s => s.hasUpdate) && <span className="ml-1 text-[9px] text-[var(--yellow)]">!</span>}
597
600
  </button>
598
601
  <button
599
602
  onClick={() => setProjectTab('claudemd')}
600
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
601
- projectTab === 'claudemd' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
603
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
604
+ projectTab === 'claudemd' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
602
605
  }`}
603
606
  >
604
607
  CLAUDE.md
605
- {claudeMdExists && <span className="ml-1 text-[8px] text-[var(--green)]">•</span>}
608
+ {claudeMdExists && <span className="ml-1 text-[9px] text-[var(--green)]">•</span>}
606
609
  </button>
607
610
  <button
608
611
  onClick={() => setProjectTab('pipelines')}
609
- className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
610
- projectTab === 'pipelines' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
612
+ className={`text-[11px] font-medium px-2.5 py-1 rounded transition-all ${
613
+ projectTab === 'pipelines' ? 'bg-[var(--accent)]/20 text-[var(--accent)] shadow-sm ring-1 ring-[var(--accent)]/40' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
611
614
  }`}
612
615
  >
613
616
  Pipelines
614
- {pipelineBindings.length > 0 && <span className="ml-1 text-[8px] text-[var(--text-secondary)]">({pipelineBindings.length})</span>}
617
+ {pipelineBindings.length > 0 && <span className="ml-1 text-[9px] opacity-70">({pipelineBindings.length})</span>}
615
618
  </button>
616
619
  </div>
617
620
  </div>
@@ -642,9 +645,9 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
642
645
  </div>
643
646
  )}
644
647
 
645
- {/* Workspace tab */}
646
- {projectTab === 'workspace' && (
647
- <div className="flex-1 flex min-h-0 overflow-hidden">
648
+ {/* Workspace tab — always mounted to preserve terminal state, hidden via CSS */}
649
+ {wsMounted && (
650
+ <div className={`flex-1 flex min-h-0 overflow-hidden ${projectTab === 'workspace' ? '' : 'hidden'}`}>
648
651
  <Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
649
652
  <WorkspaceViewLazy
650
653
  ref={wsViewRef}
@@ -21,12 +21,22 @@ interface ProjectTab {
21
21
  mountedAt: number; // timestamp for LRU eviction
22
22
  }
23
23
 
24
- const MAX_MOUNTED_TABS = 5;
24
+ const MAX_MOUNTED_TABS = 20;
25
25
  function genTabId(): number { return Date.now() + Math.floor(Math.random() * 10000); }
26
26
 
27
27
  export default function ProjectManager() {
28
28
  const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 240, minWidth: 140, maxWidth: 400 });
29
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
29
+ const [sidebarCollapsed, setSidebarCollapsedState] = useState(false);
30
+ // Load collapsed preference from localStorage on mount
31
+ useEffect(() => {
32
+ try {
33
+ if (localStorage.getItem('forge.pm.sidebarCollapsed') === '1') setSidebarCollapsedState(true);
34
+ } catch {}
35
+ }, []);
36
+ const setSidebarCollapsed = useCallback((v: boolean) => {
37
+ setSidebarCollapsedState(v);
38
+ try { localStorage.setItem('forge.pm.sidebarCollapsed', v ? '1' : '0'); } catch {}
39
+ }, []);
30
40
  const [projects, setProjects] = useState<Project[]>([]);
31
41
  const [showClone, setShowClone] = useState(false);
32
42
  const [cloneUrl, setCloneUrl] = useState('');
@@ -103,40 +113,37 @@ export default function ProjectManager() {
103
113
 
104
114
  // Open a project in a tab
105
115
  const openProjectTab = useCallback((p: Project) => {
106
- setTabs(prev => {
107
- const existing = prev.find(t => t.projectPath === p.path);
108
- if (existing) {
109
- // Activate existing tab
110
- const updated = prev.map(t => t.id === existing.id ? { ...t, mountedAt: Date.now() } : t);
111
- setActiveTabId(existing.id);
112
- persistTabs(updated, existing.id);
113
- return updated;
114
- }
115
- // Create new tab
116
- const newTab: ProjectTab = {
117
- id: genTabId(),
118
- projectPath: p.path,
119
- projectName: p.name,
120
- hasGit: p.hasGit,
121
- mountedAt: Date.now(),
122
- };
123
- const updated = [...prev, newTab];
124
- setActiveTabId(newTab.id);
125
- persistTabs(updated, newTab.id);
126
- return updated;
127
- });
128
- }, [persistTabs]);
116
+ const existing = tabs.find(t => t.projectPath === p.path);
117
+ if (existing) {
118
+ const updated = tabs.map(t => t.id === existing.id ? { ...t, mountedAt: Date.now() } : t);
119
+ setTabs(updated);
120
+ setActiveTabId(existing.id);
121
+ persistTabs(updated, existing.id);
122
+ return;
123
+ }
124
+ const newTab: ProjectTab = {
125
+ id: genTabId(),
126
+ projectPath: p.path,
127
+ projectName: p.name,
128
+ hasGit: p.hasGit,
129
+ mountedAt: Date.now(),
130
+ };
131
+ const updated = [...tabs, newTab];
132
+ setTabs(updated);
133
+ setActiveTabId(newTab.id);
134
+ persistTabs(updated, newTab.id);
135
+ }, [tabs, persistTabs]);
129
136
 
130
137
  const activateTab = useCallback((id: number) => {
138
+ const updated = tabs.map(t => t.id === id ? { ...t, mountedAt: Date.now() } : t);
139
+ setTabs(updated);
131
140
  setActiveTabId(id);
132
- setTabs(prev => {
133
- const updated = prev.map(t => t.id === id ? { ...t, mountedAt: Date.now() } : t);
134
- persistTabs(updated, id);
135
- return updated;
136
- });
137
- }, [persistTabs]);
141
+ persistTabs(updated, id);
142
+ }, [tabs, persistTabs]);
138
143
 
139
144
  const closeTab = useCallback((id: number) => {
145
+ const tab = tabs.find(t => t.id === id);
146
+ if (tab && !window.confirm(`Close "${tab.projectName}"?`)) return;
140
147
  setTabs(prev => {
141
148
  const idx = prev.findIndex(t => t.id === id);
142
149
  const updated = prev.filter(t => t.id !== id);
@@ -154,7 +161,7 @@ export default function ProjectManager() {
154
161
  }
155
162
  return updated;
156
163
  });
157
- }, [activeTabId, persistTabs]);
164
+ }, [activeTabId, persistTabs, tabs]);
158
165
 
159
166
  // Determine which tabs to mount (max 5, LRU eviction)
160
167
  const mountedTabIds = new Set<number>();
@@ -213,7 +220,7 @@ export default function ProjectManager() {
213
220
 
214
221
  return (
215
222
  <div className="flex-1 flex min-h-0">
216
- {/* Collapsed sidebar — narrow strip with project initials */}
223
+ {/* Collapsed sidebar — narrow strip with open tabs on top, all projects below */}
217
224
  {sidebarCollapsed && (
218
225
  <div className="w-10 border-r border-[var(--border)] flex flex-col shrink-0 overflow-hidden">
219
226
  <button onClick={() => setSidebarCollapsed(false)}
@@ -221,17 +228,46 @@ export default function ProjectManager() {
221
228
  title="Expand sidebar">
222
229
 
223
230
  </button>
231
+ {/* Open tabs */}
232
+ {tabs.length > 0 && (
233
+ <>
234
+ <div className="text-[7px] text-[var(--text-secondary)] text-center py-0.5 border-y border-[var(--border)] bg-[var(--bg-tertiary)]">OPEN</div>
235
+ {tabs.map(t => {
236
+ const isActive = t.id === activeTabId;
237
+ const initial = t.projectName.slice(0, 2).toUpperCase();
238
+ return (
239
+ <div key={`tab-${t.id}`} className="group relative">
240
+ <button
241
+ onClick={() => activateTab(t.id)}
242
+ title={t.projectName}
243
+ className={`w-full py-1.5 text-[9px] font-bold text-center border-l-2 ${
244
+ isActive ? 'text-[var(--accent)] bg-[var(--accent)]/10 border-[var(--accent)]' : 'text-[var(--green)] border-transparent hover:bg-[var(--bg-secondary)]'
245
+ }`}
246
+ >
247
+ {initial}
248
+ </button>
249
+ <button
250
+ onClick={(e) => { e.stopPropagation(); closeTab(t.id); }}
251
+ className="absolute top-0 right-0 text-[8px] text-[var(--text-secondary)] hover:text-[var(--red)] opacity-0 group-hover:opacity-100 transition-opacity px-0.5"
252
+ title="Close"
253
+ >×</button>
254
+ </div>
255
+ );
256
+ })}
257
+ <div className="border-b border-[var(--border)] my-1"></div>
258
+ </>
259
+ )}
260
+ {/* All projects */}
224
261
  <div className="flex-1 overflow-y-auto">
225
262
  {[...projects].sort((a, b) => a.name.localeCompare(b.name)).map(p => {
226
- const isActive = activeTab?.projectPath === p.path;
263
+ const openTab = tabs.find(t => t.projectPath === p.path);
264
+ if (openTab) return null; // already in Open section above
227
265
  const initial = p.name.slice(0, 2).toUpperCase();
228
266
  return (
229
267
  <button key={p.path}
230
268
  onClick={() => openProjectTab(p)}
231
269
  title={p.name}
232
- className={`w-full py-1.5 text-[9px] font-bold text-center ${
233
- isActive ? 'text-[var(--accent)] bg-[var(--accent)]/10' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
234
- }`}>
270
+ className="w-full py-1.5 text-[9px] font-bold text-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]">
235
271
  {initial}
236
272
  </button>
237
273
  );
@@ -285,24 +321,40 @@ export default function ProjectManager() {
285
321
  <span className="text-[8px]">{collapsedRoots.has('__favorites__') ? '▸' : '▾'}</span>
286
322
  <span>★</span> Favorites ({favoriteProjects.length})
287
323
  </button>
288
- {!collapsedRoots.has('__favorites__') && favoriteProjects.map(p => (
289
- <button
290
- key={`fav-${p.path}`}
291
- onClick={() => openProjectTab(p)}
292
- className={`w-full text-left px-3 py-1.5 text-xs border-b border-[var(--border)]/30 flex items-center gap-2 ${
293
- tabs.find(t => t.id === activeTabId)?.projectPath === p.path ? 'bg-[var(--accent)]/10 text-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)]'
294
- }`}
295
- >
296
- <span
297
- onClick={(e) => { e.stopPropagation(); toggleFavorite(p.path); }}
298
- className="text-[13px] text-[var(--yellow)] shrink-0 cursor-pointer leading-none"
299
- title="Remove from favorites"
300
- >★</span>
301
- <span className="truncate">{p.name}</span>
302
- {p.language && <span className="text-[8px] text-[var(--text-secondary)] ml-auto shrink-0">{p.language}</span>}
303
- {p.hasGit && <span className="text-[8px] text-[var(--accent)] shrink-0">git</span>}
304
- </button>
305
- ))}
324
+ {!collapsedRoots.has('__favorites__') && favoriteProjects.map(p => {
325
+ const openTab = tabs.find(t => t.projectPath === p.path);
326
+ const isActive = openTab?.id === activeTabId;
327
+ return (
328
+ <div
329
+ key={`fav-${p.path}`}
330
+ className={`group w-full border-b border-[var(--border)]/30 flex items-center gap-2 px-3 py-1.5 text-xs ${
331
+ isActive ? 'bg-[var(--accent)]/10 text-[var(--accent)]' : openTab ? 'bg-[var(--bg-tertiary)]/40 text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)]'
332
+ }`}
333
+ >
334
+ <span
335
+ onClick={(e) => { e.stopPropagation(); toggleFavorite(p.path); }}
336
+ className="text-[13px] text-[var(--yellow)] shrink-0 cursor-pointer leading-none"
337
+ title="Remove from favorites"
338
+ >★</span>
339
+ <button
340
+ onClick={() => openProjectTab(p)}
341
+ className="flex-1 text-left flex items-center gap-2 min-w-0"
342
+ >
343
+ {openTab && <span className="w-1.5 h-1.5 rounded-full bg-[var(--green)] shrink-0" title="Open"></span>}
344
+ <span className="truncate">{p.name}</span>
345
+ {p.language && <span className="text-[8px] text-[var(--text-secondary)] ml-auto shrink-0">{p.language}</span>}
346
+ {p.hasGit && <span className="text-[8px] text-[var(--accent)] shrink-0">git</span>}
347
+ </button>
348
+ {openTab && (
349
+ <button
350
+ onClick={(e) => { e.stopPropagation(); closeTab(openTab.id); }}
351
+ className="text-[var(--text-secondary)] hover:text-[var(--red)] text-[11px] leading-none opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
352
+ title="Close tab"
353
+ >×</button>
354
+ )}
355
+ </div>
356
+ );
357
+ })}
306
358
  </div>
307
359
  )}
308
360
 
@@ -320,24 +372,40 @@ export default function ProjectManager() {
320
372
  <span className="text-[8px]">{isCollapsed ? '▸' : '▾'}</span>
321
373
  {rootName} ({rootProjects.length})
322
374
  </button>
323
- {!isCollapsed && rootProjects.map(p => (
324
- <button
325
- key={p.path}
326
- onClick={() => openProjectTab(p)}
327
- className={`w-full text-left px-3 py-1.5 text-xs border-b border-[var(--border)]/30 flex items-center gap-2 ${
328
- tabs.find(t => t.id === activeTabId)?.projectPath === p.path ? 'bg-[var(--accent)]/10 text-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)]'
329
- }`}
330
- >
331
- <span
332
- onClick={(e) => { e.stopPropagation(); toggleFavorite(p.path); }}
333
- className={`text-[13px] shrink-0 cursor-pointer leading-none ${favorites.includes(p.path) ? 'text-[var(--yellow)]' : 'text-[var(--text-secondary)]/30 hover:text-[var(--yellow)]'}`}
334
- title={favorites.includes(p.path) ? 'Remove from favorites' : 'Add to favorites'}
335
- >{favorites.includes(p.path) ? '' : ''}</span>
336
- <span className="truncate">{p.name}</span>
337
- {p.language && <span className="text-[8px] text-[var(--text-secondary)] ml-auto shrink-0">{p.language}</span>}
338
- {p.hasGit && <span className="text-[8px] text-[var(--accent)] shrink-0">git</span>}
339
- </button>
340
- ))}
375
+ {!isCollapsed && rootProjects.map(p => {
376
+ const openTab = tabs.find(t => t.projectPath === p.path);
377
+ const isActive = openTab?.id === activeTabId;
378
+ return (
379
+ <div
380
+ key={p.path}
381
+ className={`group w-full border-b border-[var(--border)]/30 flex items-center gap-2 px-3 py-1.5 text-xs ${
382
+ isActive ? 'bg-[var(--accent)]/10 text-[var(--accent)]' : openTab ? 'bg-[var(--bg-tertiary)]/40 text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)]'
383
+ }`}
384
+ >
385
+ <span
386
+ onClick={(e) => { e.stopPropagation(); toggleFavorite(p.path); }}
387
+ className={`text-[13px] shrink-0 cursor-pointer leading-none ${favorites.includes(p.path) ? 'text-[var(--yellow)]' : 'text-[var(--text-secondary)]/30 hover:text-[var(--yellow)]'}`}
388
+ title={favorites.includes(p.path) ? 'Remove from favorites' : 'Add to favorites'}
389
+ >{favorites.includes(p.path) ? '★' : '☆'}</span>
390
+ <button
391
+ onClick={() => openProjectTab(p)}
392
+ className="flex-1 text-left flex items-center gap-2 min-w-0"
393
+ >
394
+ {openTab && <span className="w-1.5 h-1.5 rounded-full bg-[var(--green)] shrink-0" title="Open"></span>}
395
+ <span className="truncate">{p.name}</span>
396
+ {p.language && <span className="text-[8px] text-[var(--text-secondary)] ml-auto shrink-0">{p.language}</span>}
397
+ {p.hasGit && <span className="text-[8px] text-[var(--accent)] shrink-0">git</span>}
398
+ </button>
399
+ {openTab && (
400
+ <button
401
+ onClick={(e) => { e.stopPropagation(); closeTab(openTab.id); }}
402
+ className="text-[var(--text-secondary)] hover:text-[var(--red)] text-[11px] leading-none opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
403
+ title="Close tab"
404
+ >×</button>
405
+ )}
406
+ </div>
407
+ );
408
+ })}
341
409
  </div>
342
410
  );
343
411
  })}
@@ -356,15 +424,6 @@ export default function ProjectManager() {
356
424
 
357
425
  {/* Main area */}
358
426
  <div className="flex-1 flex flex-col min-w-0">
359
- {/* Tab bar */}
360
- {tabs.length > 0 && (
361
- <TabBar
362
- tabs={tabs.map(t => ({ id: t.id, label: t.projectName }))}
363
- activeId={activeTabId}
364
- onActivate={activateTab}
365
- onClose={closeTab}
366
- />
367
- )}
368
427
 
369
428
  {/* Tab content */}
370
429
  {tabs.length > 0 ? (