@@ -20,10 +20,10 @@ export async function showProjects() {
2020 loadingBar . start ( ) ;
2121
2222 try {
23- const [ projRes , stateRes ] = await Promise . all ( [
24- fetch ( '/api/projects' ) ,
25- fetch ( ' /api/export/state' ) . catch ( ( ) => null ) ,
26- ] ) ;
23+ // Load projects first. Do not Promise.all with /api/export/state: that endpoint
24+ // takes a file lock; if it blocks (stale lock, slow disk), the UI would hang here
25+ // forever even though /api/projects already succeeded.
26+ const projRes = await fetch ( '/api/projects' ) ;
2727 if ( ! projRes . ok ) {
2828 let msg = `Failed to load projects (${ projRes . status } )` ;
2929 try { const body = await projRes . json ( ) ; if ( body . error ) msg = body . error ; } catch { try { msg = await projRes . text ( ) || msg ; } catch { /* ignore */ } }
@@ -32,9 +32,9 @@ export async function showProjects() {
3232 return ;
3333 }
3434 const projects = await projRes . json ( ) ;
35- loadingBar . done ( ) ;
3635
3736 if ( ! projects . length ) {
37+ loadingBar . done ( ) ;
3838 smoothSet ( content , '<div class="empty-state">No Claude Code projects found.<br>Make sure Claude Code has been used on this machine.</div>' ) ;
3939 return ;
4040 }
@@ -44,15 +44,25 @@ export async function showProjects() {
4444
4545 let lastExportHtml = '' ;
4646 let hasPreviousExport = false ;
47- if ( stateRes ) {
47+ let stateRes = null ;
48+ const exportCtrl = new AbortController ( ) ;
49+ const exportTid = setTimeout ( ( ) => exportCtrl . abort ( ) , 5000 ) ;
50+ try {
51+ stateRes = await fetch ( '/api/export/state' , { signal : exportCtrl . signal } ) ;
52+ } catch {
53+ /* timeout or network — still render project list */
54+ } finally {
55+ clearTimeout ( exportTid ) ;
56+ }
57+ if ( stateRes && stateRes . ok ) {
4858 try {
4959 const exportState = await stateRes . json ( ) ;
5060 if ( exportState . last_export_time ) {
5161 const d = new Date ( exportState . last_export_time ) ;
5262 if ( ! isNaN ( d . getTime ( ) ) ) {
5363 hasPreviousExport = true ;
5464 const sessionCount = Math . max ( 0 , parseInt ( exportState . last_export_session_count ?? exportState . export_count ?? 0 , 10 ) || 0 ) ;
55- lastExportHtml = `<p class="text-muted text-sm" style="margin:0" >Last export: ${ d . toLocaleString ( ) } (${ sessionCount } sessions in last export)</p>` ;
65+ lastExportHtml = `<p class="text-muted text-sm">Last export: ${ d . toLocaleString ( ) } (${ sessionCount } sessions in last export)</p>` ;
5666 }
5767 }
5868 } catch ( e ) { }
@@ -65,20 +75,20 @@ export async function showProjects() {
6575 </button>`
6676 : '' ;
6777
68- let html = `<div class="page-header">
69- <div>
78+ let html = `<div class="page-header page-header--projects ">
79+ <div class="page-header__top" >
7080 <h1>Projects</h1>
71- <p class="text-muted">Browse your Claude Code conversations by project. Click on a project to view its sessions.</p>
72- </div>
73- <div style="display:flex;flex-direction:column;align-items:flex-end;gap:0.4rem">
7481 <div class="btn-group">
7582 ${ sinceBtnHtml }
7683 <button class="btn btn-outline btn-sm" id="btn-export-all" onclick="bulkExport('all')">
7784 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
7885 Export all
7986 </button>
8087 </div>
81- ${ lastExportHtml }
88+ </div>
89+ <div class="page-header__subrow">
90+ <p class="text-muted page-header__intro">Browse your Claude Code conversations by project. Click on a project to view its sessions.</p>
91+ ${ lastExportHtml ? `<div class="page-header__export-meta">${ lastExportHtml } </div>` : '' }
8292 </div>
8393 </div>` ;
8494
@@ -128,6 +138,7 @@ export async function showProjects() {
128138 html += `</tbody></table></div></div>` ;
129139 }
130140
141+ loadingBar . done ( ) ;
131142 smoothSet ( content , html ) ;
132143 } catch ( e ) {
133144 loadingBar . done ( ) ;
0 commit comments