Skip to content

Commit 354e73b

Browse files
RafaelPogithub-actions[bot]
authored andcommitted
fix: widget UX polish — remove Settings, fix download, improve popover (#5072)
## Summary Widget UX polish — multiple fixes from live testing in Claude.ai. ### UX Changes - **Remove Settings button** — format dropdown (CSV/TSV/JSON) was unnecessary complexity. Copy always uses CSV. - **Popover not cut off** — wider (560px, 90vw cap), scrollable content - **"... more" visually clickable** — pill-style background highlight - **Download CSV** — uses `app.openLink()` (ext-apps SDK) with copy-modal fallback for iframe sandbox restrictions - **Bump ext-apps SDK 1.0.1 → 1.3.1** — adds `sendMessage`, `openLink`, `updateModelContext` ### Completion Notification - On task completion, widget calls `app.sendMessage()` to prefill the composer with "The task is now done. Get the results." - User hits Enter → Claude calls `futuresearch_results` → presents results + download link - `futuresearch_status` prompts updated: "do not proactively call futuresearch_results" → "call if user asks to see results" ### What didn't work (Claude.ai limitations) - `updateModelContext` — not surfaced to the model by Claude.ai (removed) - `window.open` — blocked by iframe sandbox `allow-popups` - `navigator.clipboard` — blocked by permissions policy - `sendMessage` — prefills composer only, doesn't auto-send (Claude design choice) ## Test plan - [x] Tested in Claude.ai via CF tunnel - [x] Task completion → composer prefilled → user sends → Claude calls futuresearch_results - [x] Download CSV → openLink or copy modal fallback - [x] Popover scrollable, not cut off - [x] "... more" pill style visible - [x] No Settings button - [x] 356 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Sourced from commit c327fa84ccd3c5637208a8a84cea784c498119fd
1 parent 647d965 commit 354e73b

File tree

2 files changed

+33
-33
lines changed

2 files changed

+33
-33
lines changed

futuresearch-mcp/src/futuresearch_mcp/templates.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@
107107
.hdr-row .row-num{z-index:4;font-weight:600;color:var(--text-sec);cursor:default;background:var(--bg-toolbar)}
108108
.flt-row .row-num{z-index:4;cursor:default;background:var(--bg-toolbar)}
109109
tr.selected .row-num{background:var(--bg-selected)!important}
110-
.popover{position:fixed;background:var(--pop-bg);border:1px solid var(--border);border-radius:4px;box-shadow:var(--pop-shadow);max-width:420px;min-width:200px;z-index:100;overflow:hidden;opacity:0;transform:translateY(-4px);transition:opacity .15s,transform .15s;pointer-events:none}
110+
.popover{position:fixed;background:var(--pop-bg);border:1px solid var(--border);border-radius:4px;box-shadow:var(--pop-shadow);max-width:min(720px,90vw);min-width:280px;max-height:min(500px,70vh);z-index:100;overflow:hidden;opacity:0;transform:translateY(-4px);transition:opacity .15s,transform .15s;pointer-events:none;display:flex;flex-direction:column}
111111
.popover.visible{opacity:1;transform:translateY(0);pointer-events:auto}
112112
.pop-hdr{padding:8px 12px;font-size:10px;font-weight:600;color:var(--text-sec);border-bottom:1px solid var(--border-light);background:var(--bg-alt);text-transform:uppercase;letter-spacing:0.03em}
113-
.pop-body{padding:10px 12px;font-size:11px;line-height:1.5;white-space:pre-wrap;max-height:300px;overflow-y:auto;color:var(--text)}
113+
.pop-body{padding:10px 12px;font-size:11px;line-height:1.5;white-space:pre-wrap;overflow-y:auto;color:var(--text);flex:1}
114114
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(60px);background:var(--toast-bg);color:var(--toast-text);padding:6px 16px;border-radius:4px;font-size:11px;font-weight:500;opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:200}
115115
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
116116
.resize-handle{height:4px;background:var(--border-light);cursor:ns-resize;border-radius:0 0 4px 4px;transition:background .15s;margin-top:-1px;border:1px solid var(--border);border-top:none}
@@ -132,26 +132,17 @@
132132
body.col-resizing,body.col-resizing *{cursor:col-resize!important;user-select:none!important}
133133
body.row-resizing,body.row-resizing *{cursor:row-resize!important;user-select:none!important}
134134
.cell-text{display:inline}
135-
.cell-more,.cell-less{cursor:pointer;color:var(--accent);font-size:10px;margin-left:2px;white-space:nowrap;font-weight:500}
136-
.cell-more:hover,.cell-less:hover{text-decoration:underline;text-underline-offset:2px}
135+
.cell-more,.cell-less{cursor:pointer;color:var(--accent);font-size:10px;margin-left:4px;white-space:nowrap;font-weight:500;padding:1px 4px;border-radius:3px;background:rgba(77,79,189,0.08)}
136+
.cell-more:hover,.cell-less:hover{text-decoration:underline;text-underline-offset:2px;background:rgba(77,79,189,0.15)}
137137
.export-btns{display:inline-flex;gap:2px}
138-
.export-btns button{padding:3px 8px;font-size:10px}
138+
.export-btns a{font-family:inherit}
139139
#globalSearch{padding:4px 8px;border:1px solid var(--input-border);border-radius:4px;font-size:11px;background:var(--input-bg);color:var(--text);outline:none;width:160px;transition:border-color .15s ease,width .2s ease;font-family:inherit}
140140
#globalSearch:focus{border-color:var(--input-focus);width:220px}
141141
#globalSearch::placeholder{color:var(--text-dim)}
142142
.col-ghost{position:fixed;background:var(--bg-toolbar);border:1px solid var(--accent);border-radius:4px;padding:4px 8px;font-size:11px;font-weight:600;opacity:.85;pointer-events:none;z-index:200;white-space:nowrap}
143143
body.col-dragging,body.col-dragging *{cursor:grabbing!important;user-select:none!important}
144144
.hdr-row th.drag-over-left{box-shadow:inset 3px 0 0 var(--accent)}
145145
.hdr-row th.drag-over-right{box-shadow:inset -3px 0 0 var(--accent)}
146-
.settings-wrap{position:relative;display:inline-block}
147-
#settingsBtn{font-size:14px;padding:5px 8px}
148-
.settings-drop{position:absolute;top:100%;right:0;margin-top:4px;background:var(--pop-bg);border:1px solid var(--border);border-radius:4px;box-shadow:var(--pop-shadow);padding:8px 0;z-index:100;min-width:130px;display:none}
149-
.settings-drop.show{display:block}
150-
.settings-drop .drop-hdr{padding:2px 12px;font-size:10px;font-weight:600;color:var(--text-sec);text-transform:uppercase;letter-spacing:0.03em}
151-
.settings-drop label{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:11px;cursor:pointer;white-space:nowrap}
152-
.settings-drop label:hover{background:var(--bg-hover)}
153-
.settings-drop input[type="radio"]{margin:0}
154-
.settings-drop .drop-sep{border-top:1px solid var(--border-light);margin:4px 0}
155146
/* ── Widget frame ── */
156147
.widget-frame{border:1px solid var(--border);border-radius:4px;margin:4px;overflow:hidden}
157148
</style></head><body>
@@ -182,8 +173,7 @@
182173
<span id="sum">Loading...</span>
183174
<button id="selAllBtn">Select all</button>
184175
<button id="copyBtn" disabled>Copy CSV (0)</button>
185-
<span class="export-btns"><button id="exportLink" title="Copy CSV download link to clipboard">Copy link</button></span>
186-
<span class="settings-wrap"><button id="settingsBtn" title="Settings">Settings</button><div id="settingsDrop" class="settings-drop"><div class="drop-hdr">Copy format</div><label><input type="radio" name="cfmt" value="csv" checked> CSV</label><label><input type="radio" name="cfmt" value="tsv"> TSV (tabs)</label><label><input type="radio" name="cfmt" value="json"> JSON</label></div></span>
176+
<span class="export-btns"><button id="exportLink" title="Copy CSV download link to clipboard">Download CSV</button></span>
187177
</div>
188178
<div class="wrap" id="wrap" style="max-height:520px"><table id="tbl"></table></div>
189179
<div class="resize-handle" id="resizeHandle"></div>
@@ -226,15 +216,14 @@
226216
let sessionUrl="",csvUrl="",pollToken="",downloadUrl="";
227217
const TRUNC=200;
228218
let didDrag=false;
229-
let copyFmt="csv";
219+
const copyFmt="csv";
230220
let widgetActive=false;
231-
const settingsBtn=document.getElementById("settingsBtn");
232-
const settingsDrop=document.getElementById("settingsDrop");
233221
const S={rows:[],allCols:[],filteredIdx:[],sortCol:null,sortDir:0,filters:{},globalQuery:"",selected:new Set(),lastClick:null,isFullscreen:false,focusedCell:null};
234222
235223
/* ── progress state ── */
236224
let pollUrl=null,pollTimer=null,wasDone=false,pollCursor=null;
237-
let progressMode=false,resultsFetched=false;
225+
let progressMode=false,resultsFetched=false,notifiedClaude=false;
226+
let currentTaskId=null;
238227
const aggHistory=[]; /* [{aggregate,micros:[{text,row_index}],ts}] */
239228
let activeTab="activity";
240229
@@ -382,11 +371,22 @@
382371
progressSection.classList.add("flash");
383372
/* auto-fetch results on completion */
384373
if(!resultsFetched)autoFetchResults();
374+
/* notify Claude so it can present results in the conversation */
375+
notifyClaude(d);
385376
}
386377
if(done&&pollTimer){clearInterval(pollTimer);pollTimer=null;}
387378
}
388379
389380
381+
/* ── notify Claude on completion so it can present results ── */
382+
async function notifyClaude(d){
383+
if(notifiedClaude||!currentTaskId)return;
384+
notifiedClaude=true;
385+
try{
386+
await app.sendMessage({role:"user",content:[{type:"text",text:"The task is now done. Get the results."}]});
387+
}catch(e){console.error("[notify] sendMessage failed:",e);}
388+
}
389+
390390
/* ── auto-fetch results on completion ── */
391391
async function autoFetchResults(){
392392
if(resultsFetched)return;
@@ -430,6 +430,9 @@
430430
activityTab.style.display="block";
431431
resultsTab.style.display="none";
432432
sessionUrl=d.session_url||sessionUrl;
433+
if(d.task_id)currentTaskId=d.task_id;
434+
/* Extract task_id from progress_url as fallback */
435+
if(!currentTaskId&&d.progress_url){const m=d.progress_url.match(/progress\\/([0-9a-f-]+)/);if(m)currentTaskId=m[1];}
433436
if(d.poll_token)pollToken=d.poll_token;
434437
if(d.download_url)downloadUrl=d.download_url;
435438
renderProgress(d);
@@ -707,21 +710,16 @@
707710
if(!S.selected.size)return;
708711
const text=buildCopyText();
709712
const msg="Copied "+S.selected.size+" row"+(S.selected.size>1?"s":"")+" as "+copyFmt.toUpperCase();
713+
/* Clipboard API often fails in sandboxed iframes — try it first,
714+
fall back to execCommand, then show modal for manual copy. */
710715
try{await navigator.clipboard.writeText(text);showToast(msg);return;}catch{}
711-
if(execCopy(text)){showToast(msg);return;}
716+
try{if(execCopy(text)){showToast(msg);return;}}catch{}
712717
showCopyModal(text);
713718
});
714719
closeCopyModal.addEventListener("click",()=>copyModal.classList.remove("show"));
715720
copyModal.addEventListener("click",e=>{if(e.target===copyModal)copyModal.classList.remove("show");});
716721
function showToast(msg){toast.textContent=msg;toast.classList.add("show");setTimeout(()=>toast.classList.remove("show"),2000);}
717722
718-
/* --- settings dropdown --- */
719-
settingsBtn.addEventListener("click",e=>{e.stopPropagation();settingsDrop.classList.toggle("show");});
720-
document.addEventListener("click",()=>settingsDrop.classList.remove("show"));
721-
settingsDrop.addEventListener("click",e=>e.stopPropagation());
722-
settingsDrop.querySelectorAll('input[name="cfmt"]').forEach(r=>{
723-
r.addEventListener("change",()=>{copyFmt=r.value;updateCopyBtn();});
724-
});
725723
726724
/* --- popover --- */
727725
let popTimer=null,popTarget=null,popVisible=false;
@@ -938,7 +936,7 @@
938936
document.getElementById("exportLink")?.addEventListener("click",()=>{
939937
const url=getDownloadUrl();
940938
if(!url){showToast("No download link yet");return;}
941-
copyToClipboard(url).then(ok=>{if(ok)showToast("Link copied");});
939+
app.openLink({url}).catch(()=>showCopyModal(url));
942940
});
943941
944942
/* --- row resize (drag bottom border) --- */

futuresearch-mcp/src/futuresearch_mcp/tools.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,8 +1005,9 @@ async def futuresearch_status(
10051005
10061006
Returns a progress widget that auto-updates via REST polling.
10071007
The widget handles both progress tracking and result display.
1008-
After calling this once, do NOT call futuresearch_progress or
1009-
futuresearch_results — the widget handles everything automatically.
1008+
After calling this once, do NOT call futuresearch_progress — the
1009+
widget polls automatically. Only call futuresearch_results if the
1010+
user explicitly asks to see or discuss the results in the chat.
10101011
"""
10111012
logger.debug("futuresearch_status: task_id=%s", params.task_id)
10121013
task_id = params.task_id
@@ -1067,7 +1068,8 @@ async def futuresearch_status(
10671068
text = dedent(f"""\
10681069
Completed: {ts.completed}/{ts.total} ({ts.failed} failed) in {ts.elapsed_s}s.
10691070
Results are loading in the widget above.
1070-
Do NOT call futuresearch_results — the widget displays all results.
1071+
Do NOT proactively call futuresearch_results — the widget displays results automatically.
1072+
If the user asks to see, discuss, or analyze the results, call futuresearch_results(task_id='{task_id}') to load them into the conversation.
10711073
Wait for the user to tell you what to do next.""")
10721074
else:
10731075
fail_part = f", {ts.failed} failed" if ts.failed else ""
@@ -1076,7 +1078,7 @@ async def futuresearch_status(
10761078
Progress and results are handled by the widget above.
10771079
10781080
Important:
1079-
- Do NOT call futuresearch_results — the widget loads results automatically when the task completes.
1081+
- Do NOT proactively call futuresearch_results — the widget loads results automatically when the task completes. Only call it if the user asks to see or discuss the results.
10801082
- Do NOT call futuresearch_progress — the widget polls automatically.
10811083
- Do NOT call futuresearch_status again.
10821084
- Wait for the user to tell you what to do next.

0 commit comments

Comments
 (0)