From 12a8c051fb501beb1d1a63aaae43899c88f49f60 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 24 Apr 2026 23:09:44 -0700 Subject: [PATCH] fix: inject full workspace path into agent context for uploaded files (#997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: inject full workspace path into agent context for uploaded files (#997) Uploaded files (drag-and-drop or paperclip) were saved correctly to the workspace but the agent message only contained the bare filename — `photo.jpg` instead of the full path. The agent couldn't call `read_file` or `vision_analyze` without a full path. `uploadPendingFiles()` now returns `{name, path}` objects from `/api/upload` (`data.path` was always returned, just never threaded through). The agent message gets the full absolute path; all display surfaces (badges, session history, INFLIGHT state, POST body) continue showing only the bare filename. Three fixes absorbed during review: - Second `saveInflightState()` call was passing raw `{name,path}` objects instead of the `uploadedNames` string array (INFLIGHT localStorage corruption on page reload) - `attachLiveStream()` was being called with the raw object array; changed to pass `uploadedNames` so the `done` handler receives strings, not objects - `attachLiveStream` `done` handler referenced `uploadedNames` which is out of scope there (ReferenceError on every upload success); fixed to use the `uploaded` param Co-authored-by: nesquena-hermes Closes #996 --- CHANGELOG.md | 5 +++++ static/messages.js | 20 +++++++++++--------- static/ui.js | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd5e06e..686cccdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Fixed - **Reasoning chip now appears after the model chip** in the composer toolbar — model is a more fundamental choice and should be stable in position regardless of whether reasoning is active. Order: Profile → Workspace → Model → Reasoning. (`static/index.html`) +## v0.50.206 — 2026-04-25 + +### Fixed +- **Uploaded files now resolve to their full workspace path in agent context** — drag-and-drop and paperclip file uploads were correctly saved to the workspace, but the agent received only the bare filename (e.g. `photo.jpg`) in the message context rather than an absolute path. The agent could not call `read_file` or `vision_analyze` without a full path. `uploadPendingFiles()` now returns `{name, path}` objects from the `/api/upload` response (`data.path` was always returned but never threaded through). The agent message uses the full path; all display surfaces (badges, session history, INFLIGHT state, POST body) continue showing only the bare filename. (`static/ui.js`, `static/messages.js`) Closes #996. [#997] + ## v0.50.205 — 2026-04-24 ### Fixed diff --git a/static/messages.js b/static/messages.js index 8b81e5f1..1a2927e8 100644 --- a/static/messages.js +++ b/static/messages.js @@ -60,20 +60,22 @@ async function send(){ try{uploaded=await uploadPendingFiles();} catch(e){if(!text){setComposerStatus(`Upload error: ${e.message}`);return;}} + const uploadedNames=uploaded.map(u=>u.name||u); + const uploadedPaths=uploaded.map(u=>u.path||u.name||u); let msgText=text; - if(uploaded.length&&!msgText)msgText=`I've uploaded ${uploaded.length} file(s): ${uploaded.join(', ')}`; - else if(uploaded.length)msgText=`${text}\n\n[Attached files: ${uploaded.join(', ')}]`; + if(uploaded.length&&!msgText)msgText=`I've uploaded ${uploaded.length} file(s): ${uploadedPaths.join(', ')}`; + else if(uploaded.length)msgText=`${text}\n\n[Attached files: ${uploadedPaths.join(', ')}]`; if(!msgText){setComposerStatus('Nothing to send');return;} $('msg').value='';autoResize(); - const displayText=text||(uploaded.length?`Uploaded: ${uploaded.join(', ')}`:'(file upload)'); - const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploaded:undefined,_ts:Date.now()/1000}; + const displayText=text||(uploaded.length?`Uploaded: ${uploadedNames.join(', ')}`:'(file upload)'); + const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploadedNames:undefined,_ts:Date.now()/1000}; S.toolCalls=[]; // clear tool calls from previous turn clearLiveToolCards(); // clear any leftover live cards from last turn S.messages.push(userMsg);renderMessages();appendThinking();setBusy(true); - INFLIGHT[activeSid]={messages:[...S.messages],uploaded,toolCalls:[]}; + INFLIGHT[activeSid]={messages:[...S.messages],uploaded:uploadedNames,toolCalls:[]}; if(typeof saveInflightState==='function'){ - saveInflightState(activeSid,{streamId:null,messages:INFLIGHT[activeSid].messages,uploaded,toolCalls:[]}); + saveInflightState(activeSid,{streamId:null,messages:INFLIGHT[activeSid].messages,uploaded:uploadedNames,toolCalls:[]}); } startApprovalPolling(activeSid); startClarifyPolling(activeSid); @@ -100,7 +102,7 @@ async function send(){ const startData=await api('/api/chat/start',{method:'POST',body:JSON.stringify({ session_id:activeSid,message:msgText, model:S.session.model||$('modelSelect').value,workspace:S.session.workspace, - attachments:uploaded.length?uploaded:undefined + attachments:uploaded.length?uploadedNames:undefined })}); if(startData.effective_model && S.session){ S.session.model=startData.effective_model; @@ -115,7 +117,7 @@ async function send(){ } markInflight(activeSid, streamId); if(typeof saveInflightState==='function'){ - saveInflightState(activeSid,{streamId,messages:INFLIGHT[activeSid].messages,uploaded,toolCalls:INFLIGHT[activeSid].toolCalls||[]}); + saveInflightState(activeSid,{streamId,messages:INFLIGHT[activeSid].messages,uploaded:uploadedNames,toolCalls:INFLIGHT[activeSid].toolCalls||[]}); } // Refresh session list so background streaming indicators appear immediately for the // session that was just started and any others that may already be running. @@ -158,7 +160,7 @@ async function send(){ } // Open SSE stream and render tokens live - attachLiveStream(activeSid, streamId, uploaded); + attachLiveStream(activeSid, streamId, uploadedNames); } diff --git a/static/ui.js b/static/ui.js index 00cf1261..f0778f06 100644 --- a/static/ui.js +++ b/static/ui.js @@ -2790,7 +2790,7 @@ async function uploadPendingFiles(){ if(!res.ok){const err=await res.text();throw new Error(err);} const data=await res.json(); if(data.error)throw new Error(data.error); - names.push(data.filename); + names.push({name: data.filename, path: data.path}); }catch(e){failures++;setStatus(`\u274c ${t('upload_failed')}${f.name} \u2014 ${e.message}`);} bar.style.width=`${Math.round((i+1)/total*100)}%`; }