From 67324cc3bcfe428b59bc30d4081c3111d6633648 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Fri, 3 Apr 2026 04:33:24 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=2018=20=E2=80=94=20file=20previe?= =?UTF-8?q?w=20auto-close,=20thinking=20display,=20workspace=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - File preview auto-close: clearPreview() extracted as named function and called from loadDir(). Navigating directories (breadcrumbs, up button, folder clicks) now automatically closes the right panel file preview instead of leaving stale content visible. - Thinking/reasoning display: assistant messages with structured content arrays containing type=thinking or type=reasoning blocks now render as collapsible gold-themed cards above the response text. Collapsed by default, click header to expand. Works with Claude extended thinking and o3 reasoning tokens when preserved in the message array. - Workspace tree view (Issue #22): directories expand/collapse in-place with toggle arrows. Single-click toggles, double-click navigates (breadcrumb view). Subdirectory contents fetched lazily and cached. Indentation shows nesting depth. Empty directories show "(empty)". S._expandedDirs tracks open state, S._dirCache caches fetched entries. Tests: 295 passed, 23 pre-existing failures, 0 regressions. Co-Authored-By: Claude Opus 4.6 (1M context) --- static/boot.js | 18 +++++---- static/style.css | 15 ++++++++ static/ui.js | 91 ++++++++++++++++++++++++++++++++++++++++----- static/workspace.js | 1 + 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/static/boot.js b/static/boot.js index cb76044..f0a8450 100644 --- a/static/boot.js +++ b/static/boot.js @@ -43,14 +43,16 @@ $('importFileInput').onchange=async(e)=>{ } }; // btnRefreshFiles is now panel-icon-btn in header (see HTML) -$('btnClearPreview').onclick=()=>{ - $('previewArea').classList.remove('visible'); - $('previewImg').src=''; - $('previewMd').innerHTML=''; - $('previewCode').textContent=''; - $('previewPathText').textContent=''; - $('fileTree').style.display=''; -}; +function clearPreview(){ + const pa=$('previewArea');if(pa)pa.classList.remove('visible'); + const pi=$('previewImg');if(pi)pi.src=''; + const pm=$('previewMd');if(pm)pm.innerHTML=''; + const pc=$('previewCode');if(pc)pc.textContent=''; + const pp=$('previewPathText');if(pp)pp.textContent=''; + const ft=$('fileTree');if(ft)ft.style.display=''; + _previewCurrentPath='';_previewCurrentMode='';_previewDirty=false; +} +$('btnClearPreview').onclick=clearPreview; // workspacePath click handler removed -- use topbar workspace chip dropdown instead $('modelSelect').onchange=async()=>{ if(!S.session)return; diff --git a/static/style.css b/static/style.css index 5c05c70..dd214d1 100644 --- a/static/style.css +++ b/static/style.css @@ -216,6 +216,9 @@ .file-item{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:7px;cursor:pointer;font-size:12px;color:var(--muted);transition:all .12s;min-width:0;} .file-item:hover{background:rgba(255,255,255,.07);color:var(--text);} .file-item.active{background:rgba(124,185,255,.12);color:var(--blue);} + .file-tree-toggle{font-size:10px;color:var(--muted);flex-shrink:0;width:10px;text-align:center;line-height:1;} + .file-item.file-empty{color:var(--muted);opacity:.5;font-style:italic;cursor:default;font-size:11px;} + .file-item.file-empty:hover{background:none;color:var(--muted);} .preview-area{flex:1;overflow:auto;padding:14px;flex-direction:column;gap:8px;display:none;opacity:0;transition:opacity .15s;} .preview-area.visible{display:flex;opacity:1;} .preview-path{font-size:11px;color:var(--muted);padding-bottom:8px;border-bottom:1px solid var(--border);flex-shrink:0;} @@ -572,4 +575,16 @@ body.resizing{user-select:none;cursor:col-resize;} .tool-cards-toggle button{background:none;border:none;color:var(--blue);font-size:10px;cursor:pointer;opacity:.6;padding:0;} .tool-cards-toggle button:hover{opacity:1;text-decoration:underline;} +/* ── Thinking/reasoning card ── */ +.thinking-card{background:rgba(201,168,76,.06);border:1px solid rgba(201,168,76,.2);border-radius:10px;margin:4px 0 2px 40px;overflow:hidden;transition:border-color .15s;} +.thinking-card:hover{border-color:rgba(201,168,76,.35);} +.thinking-card-header{display:flex;align-items:center;gap:6px;padding:6px 12px;cursor:pointer;font-size:12px;color:var(--gold);user-select:none;} +.thinking-card-icon{font-size:14px;} +.thinking-card-label{font-weight:600;letter-spacing:.02em;} +.thinking-card-toggle{margin-left:auto;font-size:10px;transition:transform .15s;} +.thinking-card.open .thinking-card-toggle{transform:rotate(90deg);} +.thinking-card-body{display:none;padding:0 12px 10px;max-height:300px;overflow-y:auto;} +.thinking-card.open .thinking-card-body{display:block;} +.thinking-card-body pre{font-family:'SF Mono',ui-monospace,monospace;font-size:11px;line-height:1.5;color:var(--muted);white-space:pre-wrap;word-break:break-word;margin:0;} + .bg-error-banner{background:rgba(229,62,62,.15);border:1px solid rgba(229,62,62,.3);color:#fca5a5;padding:8px 16px;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:0;} diff --git a/static/ui.js b/static/ui.js index 9bc77f2..e71352f 100644 --- a/static/ui.js +++ b/static/ui.js @@ -365,9 +365,20 @@ function renderMessages(){ for(let vi=0;vip&&p.type==='text').map(p=>p.text||p.content||'').join('\n'); + // Extract thinking/reasoning blocks from structured content (Claude extended thinking, o3) + let thinkingText=''; + if(Array.isArray(content)){ + thinkingText=content.filter(p=>p&&(p.type==='thinking'||p.type==='reasoning')).map(p=>p.thinking||p.reasoning||p.text||'').join('\n'); + content=content.filter(p=>p&&p.type==='text').map(p=>p.text||p.content||'').join('\n'); + } const isUser=m.role==='user'; const isLastAssistant=!isUser&&vi===visWithIdx.length-1; + // Render thinking card before the assistant message (collapsed by default) + if(thinkingText&&!isUser){ + const thinkRow=document.createElement('div');thinkRow.className='msg-row thinking-card-row'; + thinkRow.innerHTML=`
💡Thinking
${esc(thinkingText)}
`; + inner.appendChild(thinkRow); + } const row=document.createElement('div');row.className='msg-row'; row.dataset.msgIdx=rawIdx; let filesHtml=''; @@ -744,27 +755,49 @@ function renderBreadcrumb(){ } } +// Track expanded directories for tree view +if(!S._expandedDirs) S._expandedDirs=new Set(); +// Cache of fetched directory contents: path -> entries[] +if(!S._dirCache) S._dirCache={}; + function renderFileTree(){ const box=$('fileTree');box.innerHTML=''; - for(const item of S.entries){ + // Cache current dir entries + S._dirCache[S.currentDir||'.']=S.entries; + _renderTreeItems(box, S.entries, 0); +} + +function _renderTreeItems(container, entries, depth){ + for(const item of entries){ const el=document.createElement('div');el.className='file-item'; + el.style.paddingLeft=(8+depth*16)+'px'; + + if(item.type==='dir'){ + // Toggle arrow for directories + const arrow=document.createElement('span'); + arrow.className='file-tree-toggle'; + const isExpanded=S._expandedDirs.has(item.path); + arrow.textContent=isExpanded?'\u25BE':'\u25B8'; + el.appendChild(arrow); + } // Icon const iconEl=document.createElement('span'); iconEl.className='file-icon';iconEl.textContent=fileIcon(item.name,item.type); el.appendChild(iconEl); - // Name -- takes all remaining space, truncates with ellipsis + // Name const nameEl=document.createElement('span'); nameEl.className='file-name';nameEl.textContent=item.name;nameEl.title='Double-click to rename'; - // Inline rename on double-click nameEl.ondblclick=(e)=>{ e.stopPropagation(); + // For directories, double-click navigates (breadcrumb view) + if(item.type==='dir'){loadDir(item.path);return;} const inp=document.createElement('input'); inp.className='file-rename-input';inp.value=item.name; inp.onclick=(e2)=>e2.stopPropagation(); const finish=async(save)=>{ - inp.onblur=null; // prevent double-call: Enter triggers blur after replaceWith + inp.onblur=null; if(save){ const newName=inp.value.trim(); if(newName&&newName!==item.name){ @@ -773,6 +806,8 @@ function renderFileTree(){ session_id:S.session.session_id,path:item.path,new_name:newName })}); showToast(`Renamed to ${newName}`); + // Invalidate cache and re-render + delete S._dirCache[S.currentDir]; await loadDir(S.currentDir); }catch(err){showToast('Rename failed: '+err.message);} } @@ -789,7 +824,7 @@ function renderFileTree(){ }; el.appendChild(nameEl); - // Size -- only for files, right-aligned, shrinks but never wraps + // Size -- only for files if(item.type==='file'&&item.size){ const sizeEl=document.createElement('span'); sizeEl.className='file-size'; @@ -797,16 +832,52 @@ function renderFileTree(){ el.appendChild(sizeEl); } - // Delete button -- for files, shown on hover + // Delete button -- for files if(item.type==='file'){ const del=document.createElement('button'); - del.className='file-del-btn';del.title='Delete';del.textContent='×'; + del.className='file-del-btn';del.title='Delete';del.textContent='\u00d7'; del.onclick=async(e)=>{e.stopPropagation();await deleteWorkspaceFile(item.path,item.name);}; el.appendChild(del); } - el.onclick=async()=>item.type==='dir'?loadDir(item.path):openFile(item.path); - box.appendChild(el); + if(item.type==='dir'){ + // Single-click toggles expand/collapse + el.onclick=async(e)=>{ + e.stopPropagation(); + if(S._expandedDirs.has(item.path)){ + S._expandedDirs.delete(item.path); + renderFileTree(); + }else{ + S._expandedDirs.add(item.path); + // Fetch children if not cached + if(!S._dirCache[item.path]){ + try{ + const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(item.path)}`); + S._dirCache[item.path]=data.entries||[]; + }catch(e2){S._dirCache[item.path]=[];} + } + renderFileTree(); + } + }; + }else{ + el.onclick=async()=>openFile(item.path); + } + + container.appendChild(el); + + // Render children if directory is expanded + if(item.type==='dir'&&S._expandedDirs.has(item.path)){ + const children=S._dirCache[item.path]||[]; + if(children.length){ + _renderTreeItems(container, children, depth+1); + }else{ + const empty=document.createElement('div'); + empty.className='file-item file-empty'; + empty.style.paddingLeft=(8+(depth+1)*16)+'px'; + empty.textContent='(empty)'; + container.appendChild(empty); + } + } } } diff --git a/static/workspace.js b/static/workspace.js index b19c9c2..9623a7d 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -12,6 +12,7 @@ async function loadDir(path){ S.currentDir=path||'.'; const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`); S.entries=data.entries||[];renderBreadcrumb();renderFileTree(); + if(typeof clearPreview==='function')clearPreview(); }catch(e){console.warn('loadDir',e);} }