feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting

Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory

Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally

Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json

5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-02 17:31:31 -07:00
parent 856f5c21e1
commit 0f2bd537f1
13 changed files with 478 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
const S={session:null,messages:[],entries:[],busy:false,pendingFiles:[],toolCalls:[],activeStreamId:null};
const S={session:null,messages:[],entries:[],busy:false,pendingFiles:[],toolCalls:[],activeStreamId:null,currentDir:'.'};
const INFLIGHT={}; // keyed by session_id while request in-flight
const MSG_QUEUE=[]; // messages queued while a request is in-flight
const $=id=>document.getElementById(id);
@@ -705,6 +705,45 @@ function fileIcon(name, type){
return '📄';
}
function renderBreadcrumb(){
const bar=$('breadcrumbBar');
const upBtn=$('btnUpDir');
if(!bar)return;
if(S.currentDir==='.'){
bar.style.display='none';
if(upBtn)upBtn.style.display='none';
return;
}
bar.style.display='flex';
if(upBtn)upBtn.style.display='';
bar.innerHTML='';
// Root segment
const root=document.createElement('span');
root.className='breadcrumb-seg breadcrumb-link';
root.textContent='~';
root.onclick=()=>loadDir('.');
bar.appendChild(root);
// Path segments
const parts=S.currentDir.split('/');
let accumulated='';
for(let i=0;i<parts.length;i++){
const sep=document.createElement('span');
sep.className='breadcrumb-sep';sep.textContent='/';
bar.appendChild(sep);
accumulated+=(accumulated?'/':'')+parts[i];
const seg=document.createElement('span');
seg.textContent=parts[i];
if(i<parts.length-1){
seg.className='breadcrumb-seg breadcrumb-link';
const target=accumulated;
seg.onclick=()=>loadDir(target);
} else {
seg.className='breadcrumb-seg breadcrumb-current';
}
bar.appendChild(seg);
}
}
function renderFileTree(){
const box=$('fileTree');box.innerHTML='';
for(const item of S.entries){
@@ -734,7 +773,7 @@ function renderFileTree(){
session_id:S.session.session_id,path:item.path,new_name:newName
})});
showToast(`Renamed to ${newName}`);
await loadDir('.');
await loadDir(S.currentDir);
}catch(err){showToast('Rename failed: '+err.message);}
}
}
@@ -779,7 +818,7 @@ async function deleteWorkspaceFile(relPath, name){
showToast(`Deleted ${name}`);
// Close preview if we just deleted the viewed file
if($('previewPathText').textContent===relPath)$('btnClearPreview').onclick();
await loadDir('.');
await loadDir(S.currentDir);
}catch(e){setStatus('Delete failed: '+e.message);}
}
@@ -787,12 +826,12 @@ async function promptNewFile(){
if(!S.session)return;
const name=prompt('New file name (e.g. notes.md):','');
if(!name||!name.trim())return;
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
try{
await api('/api/file/create',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:name.trim(),content:''})});
await api('/api/file/create',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath,content:''})});
showToast(`Created ${name.trim()}`);
await loadDir('.');
// Open the new file immediately
openFile(name.trim());
await loadDir(S.currentDir);
openFile(relPath);
}catch(e){setStatus('Create failed: '+e.message);}
}
@@ -800,10 +839,11 @@ async function promptNewFolder(){
if(!S.session)return;
const name=prompt('New folder name:','');
if(!name||!name.trim())return;
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
try{
await api('/api/file/create-dir',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:name.trim()})});
await api('/api/file/create-dir',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath})});
showToast(`Created folder ${name.trim()}`);
await loadDir('.');
await loadDir(S.currentDir);
}catch(e){setStatus('Create folder failed: '+e.message);}
}