picker-widget.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <!--
  2. summon in-chat CARD picker — TEMPLATE (default presentation for picking sessions)
  3. How Claude uses this:
  4. 1. run `summon pick --json --rich --days 30` (schema claude-mods.summon.pick/v2)
  5. 2. drop the envelope's `data` array into the `S` array below (shape documented
  6. inline) — one object per session, keys verbatim from pick/v2
  7. 3. render via the show_widget tool (Tabler `ti` icons + host CSS vars are both
  8. available in that context; light/dark safe)
  9. Self-contained: host CSS variables only, no external assets beyond the Tabler
  10. webfont the widget host already loads. sendPrompt(text) is a host global that
  11. submits a chat message as if the user typed it.
  12. DATA SHAPE per session (pick/v2 object — extra keys are ignored):
  13. { sessionId, title, projectRoot, cwd, worktree, isArchived, isRunning,
  14. lastActivityAt (ISO), turns, model, effort, events, toolCalls,
  15. densityBuckets:[24], durationMin, sizeKB, ctxTokens, ctxPeak, ctxWindow,
  16. ctxPct, ctxPeakPct, firstAsk }
  17. `firstAsk` is the mechanical opening ask. Claude MAY overwrite it with a
  18. distilled one-line summary before injecting; the widget just renders `summary`
  19. if present, else `firstAsk`.
  20. Inject a GENEROUS window (`--days 30`): the widget has its own client-side age
  21. filter (24h/3d/7d/30d/all) so the user narrows without a re-run. Archived
  22. sessions are hidden by default (toggle to show). Copies of one session across
  23. accounts are deduped by sessionId, newest kept.
  24. CLICK ACTIONS (all route through sendPrompt → one chat line Claude turns into
  25. action; per-card recover/summon should be spawned as background chips):
  26. ↗ summon · copy this session into the other account
  27. ⟳ recover · distil a handover brief + start a fresh seeded session
  28. ◉ peek · show the session's last messages
  29. ⧉ copy · copy the sessionId to the clipboard
  30. -->
  31. <h2 class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Interactive card picker of Claude Desktop sessions with per-session context-usage gauge, activity-density strip, turns, size, and summon or recover actions.</h2>
  32. <style>
  33. .ctl{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 14px}
  34. .ctl input,.ctl select{font-size:13px}
  35. .ctl .flt{flex:1;min-width:150px}
  36. .seg{display:inline-flex;border:.5px solid var(--border-strong);border-radius:var(--radius);overflow:hidden}
  37. .seg button{border:0;background:transparent;padding:6px 11px;font-size:13px;color:var(--text-secondary);display:inline-flex;align-items:center;gap:5px;cursor:pointer}
  38. .seg button.on{background:var(--surface-1);color:var(--text-primary)}
  39. .cbx{font-size:12px;color:var(--text-secondary);display:inline-flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap}
  40. .deck.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
  41. .deck.list{display:flex;flex-direction:column;gap:8px}
  42. .card{background:var(--surface-2);border:.5px solid var(--border);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:9px}
  43. .card.sel{border-color:var(--border-accent);box-shadow:0 0 0 1px var(--border-accent)}
  44. .deck.list .card{display:grid;grid-template-columns:1fr 250px;grid-template-areas:"hd dens" "ttl stats" "sum stats" "dir dir";gap:6px 18px;align-items:center;padding:12px 16px}
  45. .deck.list .hd{grid-area:hd}.deck.list .ttl{grid-area:ttl}.deck.list .sum{grid-area:sum}.deck.list .dens{grid-area:dens}.deck.list .stats{grid-area:stats}.deck.list .dir{grid-area:dir}
  46. .deck.list .sum{-webkit-line-clamp:2}
  47. /* Header overflow discipline (a half-width grid card is ~250-300px, so space
  48. is tight): .proj is the shrinker (min-width:0 + ellipsis); the action icons
  49. are grouped in .acts with margin-left:auto so they hug the right edge; and
  50. .hd may WRAP (row-gap) as a hard backstop — icons drop to a second line
  51. rather than ever spilling past the border. Context lives as a chip in the
  52. stats row (not the header) to keep the header light. */
  53. .hd{display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap;row-gap:6px}
  54. .proj{font-size:12px;color:var(--text-secondary);font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  55. .tag{font-size:10px;border-radius:5px;padding:0 5px;border:.5px solid var(--border);color:var(--text-muted);flex:none}
  56. .tag.run{background:var(--bg-success);color:var(--text-success);border:0}
  57. .acts{display:inline-flex;align-items:center;gap:2px;margin-left:auto;flex:none}
  58. .ib{border:0;background:transparent;color:var(--text-muted);cursor:pointer;padding:2px;display:inline-flex;font-size:13px;border-radius:5px;flex:none}
  59. .ib:hover{color:var(--text-accent);background:var(--surface-1)}
  60. .cchip i{font-size:13px}
  61. .ttl{font-size:15px;font-weight:500;color:var(--text-primary);line-height:1.35}
  62. .sum{font-size:13px;color:var(--text-secondary);line-height:1.55;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
  63. .dens{display:flex;align-items:flex-end;gap:2px;height:30px}
  64. .dens i{flex:1;border-radius:1px;min-height:1px;display:block}
  65. .stats{display:flex;flex-wrap:wrap;gap:5px}
  66. .chip{font-size:11px;color:var(--text-secondary);background:var(--surface-1);border-radius:6px;padding:2px 7px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
  67. .chip i{font-size:13px;color:var(--text-muted)}
  68. .dir{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:5px}
  69. .foot{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:14px 0 0}
  70. .foot button{font-size:13px;padding:7px 13px;border-radius:var(--radius);border:.5px solid var(--border-strong);background:transparent;color:var(--text-primary);cursor:pointer;display:inline-flex;align-items:center;gap:6px}
  71. .foot button:disabled{opacity:.45;cursor:default}
  72. </style>
  73. <div class="ctl">
  74. <input class="flt" id="flt" type="text" placeholder="filter title, project, or path…">
  75. <select id="win" title="active in the last…" style="width:auto"><option value="">all</option><option value="24">24h</option><option value="72">3d</option><option value="168" selected>7d</option><option value="720">30d</option></select>
  76. <select id="sort" style="width:auto">
  77. <option value="recent">Most recent</option>
  78. <option value="ctx">Most context used</option>
  79. <option value="size">Largest size</option>
  80. <option value="turns">Most turns</option>
  81. <option value="msgs">Most messages</option>
  82. <option value="project">Project</option>
  83. </select>
  84. <label class="cbx"><input type="checkbox" id="arch"> show archived</label>
  85. <div class="seg">
  86. <button id="bGrid" class="on" aria-label="Grid view"><i class="ti ti-layout-grid" aria-hidden="true"></i>Grid</button>
  87. <button id="bList" aria-label="List view"><i class="ti ti-list" aria-hidden="true"></i>List</button>
  88. </div>
  89. </div>
  90. <div id="deck" class="deck grid"></div>
  91. <div class="foot">
  92. <span id="cnt" style="font-size:13px;color:var(--text-secondary)">0 selected</span>
  93. <span style="flex:1"></span>
  94. <button id="bRec" disabled><i class="ti ti-refresh" aria-hidden="true"></i>Recover selected</button>
  95. <button id="bSum" disabled><i class="ti ti-arrow-up-right" aria-hidden="true"></i>Summon selected</button>
  96. <button id="bClr" disabled><i class="ti ti-x" aria-hidden="true"></i>Clear</button>
  97. </div>
  98. <script id="D" type="application/json">
  99. [{"sessionId":"local_0000","title":"Example session — replace via summon pick --json --rich","projectRoot":"X:\\Example\\Project","cwd":"X:\\Example\\Project","worktree":"","isArchived":false,"isRunning":false,"lastActivityAt":"2026-01-01T00:00:00Z","turns":3,"model":"claude-opus-4-8","effort":"high","events":120,"toolCalls":22,"densityBuckets":[9,7,5,3,0,0,0,0,2,4,6,3,0,0,0,0,1,2,3,0,0,0,4,8],"durationMin":41,"sizeKB":512,"ctxTokens":90000,"ctxPeak":90000,"ctxWindow":200000,"ctxPct":45,"ctxPeakPct":45,"firstAsk":"example opening ask captured from the transcript"}]
  100. </script>
  101. <script>
  102. (function(){
  103. var RAW=JSON.parse(document.getElementById('D').textContent);
  104. var seen={},DATA=[];
  105. RAW.forEach(function(s){if(!seen[s.sessionId]){seen[s.sessionId]=1;DATA.push(s);}});
  106. var PAL=['#7F77DD','#1D9E75','#D85A30','#378ADD','#BA7517','#D4537E','#534AB7','#0F6E56','#993C1D'];
  107. function projName(p){return (p||'').replace(/\//g,'\\').replace(/\\$/,'').split('\\').pop()||p;}
  108. function projColor(p){var h=0,n=projName(p);for(var i=0;i<n.length;i++)h=(h*31+n.charCodeAt(i))>>>0;return PAL[h%PAL.length];}
  109. var NOW=Date.now();
  110. function age(iso){var h=(NOW-Date.parse(iso))/36e5;return h<1?Math.round(h*60)+'m':h<48?Math.round(h)+'h':Math.round(h/24)+'d';}
  111. function nf(n){return (n||0).toLocaleString('en-US');}
  112. function fSize(kb){kb=kb||0;return kb>=1024?(kb/1024).toFixed(1)+' MB':kb+' KB';}
  113. function fDur(m){m=m||0;return m<60?m+'m':m<1440?Math.round(m/60)+'h':Math.round(m/1440)+'d';}
  114. function fTok(n){n=n||0;return n>=1000?Math.round(n/1000)+'k':''+n;}
  115. function esc(s){return (s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
  116. function shortModel(m){m=m||'';if(m.indexOf('opus')>=0)return 'Opus';if(m.indexOf('fable')>=0)return 'Fable 5';if(m.indexOf('sonnet')>=0)return 'Sonnet';if(m.indexOf('haiku')>=0)return 'Haiku';return m;}
  117. function ctxColor(p){return p<50?'#1D9E75':p<75?'#BA7517':'#E24B4A';}
  118. function bars(b,col){b=b||[];var mx=Math.max.apply(null,b)||1;return b.map(function(v){var h=v>0?Math.max(2,Math.round(v/mx*30)):1;return '<i style="height:'+h+'px;background:'+col+';opacity:'+(v>0?1:0.18)+'"></i>';}).join('');}
  119. function chip(ic,val,lbl){return '<span class="chip"><i class="ti ti-'+ic+'" aria-hidden="true"></i>'+val+(lbl?'<span style="color:var(--text-muted)">'+lbl+'</span>':'')+'</span>';}
  120. var picked={};
  121. function card(s){
  122. var col=projColor(s.projectRoot),body=s.summary||s.firstAsk||'',pk=picked[s.sessionId];
  123. var ctxTip=fTok(s.ctxTokens)+' of '+fTok(s.ctxWindow)+' context used'+(s.ctxPeakPct>s.ctxPct?' · peak '+fTok(s.ctxPeak)+' ('+s.ctxPeakPct+'%)':'');
  124. var wt=s.worktree?'<span class="tag" title="worktree '+esc(s.worktree)+'">worktree</span>':'';
  125. var run=s.isRunning?'<span class="tag run">running</span>':'';
  126. var arch=s.isArchived?'<span class="tag">archived</span>':'';
  127. var ttl=(!s.title||s.title==='(untitled)')?'<span style="color:var(--text-muted);font-style:italic">untitled session</span>':esc(s.title);
  128. return '<div class="card'+(pk?' sel':'')+'" data-id="'+esc(s.sessionId)+'">'
  129. +'<div class="hd">'
  130. +'<input type="checkbox" class="pk" '+(pk?'checked':'')+' aria-label="Select session" style="margin:0 2px 0 0">'
  131. +'<span style="width:8px;height:8px;border-radius:50%;background:'+col+';flex:none"></span>'
  132. +'<span class="proj">'+esc(projName(s.projectRoot))+'</span>'+wt+run+arch
  133. +'<span class="acts">'
  134. +'<button class="ib act-peek" aria-label="Peek last messages" title="Peek last messages"><i class="ti ti-eye" aria-hidden="true"></i></button>'
  135. +'<button class="ib act-copy" aria-label="Copy session id" title="Copy session id"><i class="ti ti-copy" aria-hidden="true"></i></button>'
  136. +'<button class="ib act-summon" aria-label="Summon to other account" title="Summon → other account (copy)"><i class="ti ti-arrow-up-right" aria-hidden="true"></i></button>'
  137. +'<button class="ib act-recover" aria-label="Recover: summarise and start fresh" title="Recover — summarise + fresh session"><i class="ti ti-refresh" aria-hidden="true"></i></button>'
  138. +'</span>'
  139. +'</div>'
  140. +'<div class="ttl">'+ttl+'</div>'
  141. +(body?'<div class="sum">'+esc(body)+'</div>':'')
  142. +'<div class="dens" title="activity density across session lifetime">'+bars(s.densityBuckets,col)+'</div>'
  143. +'<div class="stats">'
  144. +'<span class="chip cchip" title="'+ctxTip+'"><i class="ti ti-gauge" aria-hidden="true" style="color:'+ctxColor(s.ctxPct)+'"></i>'+fTok(s.ctxTokens)+'<span style="color:var(--text-muted)">'+s.ctxPct+'%</span></span>'
  145. +chip('cpu',shortModel(s.model),s.effort||'')
  146. +chip('clock',age(s.lastActivityAt),'')
  147. +chip('messages',nf(s.turns),'turns')
  148. +chip('message-2',nf(s.events),'msgs')
  149. +chip('tool',nf(s.toolCalls),'tools')
  150. +chip('database',fSize(s.sizeKB),'')
  151. +chip('hourglass',fDur(s.durationMin),'')
  152. +'</div>'
  153. +'<div class="dir" title="'+esc(s.cwd)+'"><i class="ti ti-folder" aria-hidden="true"></i>'+esc(s.cwd)+'</div>'
  154. +'</div>';
  155. }
  156. var deck=document.getElementById('deck'),flt=document.getElementById('flt'),win=document.getElementById('win'),
  157. sortEl=document.getElementById('sort'),archEl=document.getElementById('arch'),cnt=document.getElementById('cnt');
  158. function visible(){
  159. var q=flt.value.toLowerCase(),maxH=win.value?+win.value:Infinity,showArch=archEl.checked;
  160. return DATA.filter(function(s){
  161. if(s.isArchived&&!showArch)return false;
  162. if((NOW-Date.parse(s.lastActivityAt))/36e5>maxH)return false;
  163. if(q){var hay=((s.title||'')+' '+(s.projectRoot||'')+' '+(s.cwd||'')+' '+(s.worktree||'')).toLowerCase();if(hay.indexOf(q)<0)return false;}
  164. return true;
  165. });
  166. }
  167. function order(a){var m=sortEl.value,x=a.slice();
  168. if(m==='ctx')x.sort(function(p,q){return (q.ctxTokens||0)-(p.ctxTokens||0);});
  169. else if(m==='size')x.sort(function(p,q){return (q.sizeKB||0)-(p.sizeKB||0);});
  170. else if(m==='turns')x.sort(function(p,q){return (q.turns||0)-(p.turns||0);});
  171. else if(m==='msgs')x.sort(function(p,q){return (q.events||0)-(p.events||0);});
  172. else if(m==='project')x.sort(function(p,q){return projName(p.projectRoot).localeCompare(projName(q.projectRoot))||(q.ctxTokens||0)-(p.ctxTokens||0);});
  173. else x.sort(function(p,q){return Date.parse(q.lastActivityAt)-Date.parse(p.lastActivityAt);});
  174. return x;
  175. }
  176. function fmtList(list){return list.map(function(s){return '- "'+s.title+'" ('+s.sessionId+', '+s.cwd+')';}).join('\n');}
  177. function refreshFoot(){
  178. var ids=Object.keys(picked).filter(function(k){return picked[k];}),n=ids.length;
  179. cnt.textContent=n+' selected';
  180. ['bRec','bSum','bClr'].forEach(function(id){document.getElementById(id).disabled=!n;});
  181. }
  182. function render(){
  183. var arr=order(visible());
  184. deck.innerHTML=arr.length?arr.map(card).join(''):'<div style="color:var(--text-muted);font-size:13px;padding:1rem 0">No sessions match the current filter.</div>';
  185. deck.querySelectorAll('.card').forEach(function(el){
  186. var s=DATA.filter(function(d){return d.sessionId===el.dataset.id;})[0];if(!s)return;
  187. el.querySelector('.pk').addEventListener('change',function(){picked[s.sessionId]=this.checked;el.classList.toggle('sel',this.checked);refreshFoot();});
  188. el.querySelector('.act-peek').addEventListener('click',function(){sendPrompt('Peek session '+s.sessionId+' ("'+s.title+'") — run summon --peek '+s.sessionId+' and show me its last few messages.');});
  189. el.querySelector('.act-copy').addEventListener('click',function(){var b=this;navigator.clipboard&&navigator.clipboard.writeText(s.sessionId);var i=b.querySelector('i');i.className='ti ti-check';setTimeout(function(){i.className='ti ti-copy';},900);});
  190. el.querySelector('.act-summon').addEventListener('click',function(){sendPrompt('Summon (copy) the "'+s.title+'" session ('+s.sessionId+') into my other account — show the plan with summon --dry-run first.');});
  191. el.querySelector('.act-recover').addEventListener('click',function(){sendPrompt('Recover the "'+s.title+'" session ('+s.sessionId+', '+s.cwd+'): spawn ONE background chip TITLED with the original session name verbatim — "'+s.title+'" — NOT a "Recover ..." label. Set its cwd to the PROJECT ROOT of that path (drop any \\.claude\\worktrees\\... suffix). That chip IS the recovered session — it reads the original transcript, writes a concise hand-off summary, and resumes the work in place in the project folder. It must NOT spawn any further chip and must NOT open the worktree folder as a separate session (the worktree path is reference-only, for locating the branch and in-progress changes).');});
  192. });
  193. refreshFoot();
  194. }
  195. [flt,win,sortEl,archEl].forEach(function(e){e.addEventListener(e===flt?'input':'change',render);});
  196. document.getElementById('bGrid').addEventListener('click',function(){deck.className='deck grid';this.classList.add('on');document.getElementById('bList').classList.remove('on');});
  197. document.getElementById('bList').addEventListener('click',function(){deck.className='deck list';this.classList.add('on');document.getElementById('bGrid').classList.remove('on');});
  198. document.getElementById('bClr').addEventListener('click',function(){picked={};render();});
  199. document.getElementById('bRec').addEventListener('click',function(){var l=DATA.filter(function(s){return picked[s.sessionId];});sendPrompt('Recover these '+l.length+' sessions — from this single prompt, spawn ONE background chip per session. Title each chip with that session'+String.fromCharCode(39)+'s original name verbatim (the quoted name below) — NOT a "Recover ..." label — and set its cwd to the PROJECT ROOT (drop any \\.claude\\worktrees\\... suffix). Each chip IS its recovered session: read the original transcript, distil a hand-off brief, and resume the work in place in the project folder. No chip may spawn a further chip or open a worktree folder as a separate session:\n'+fmtList(l));});
  200. document.getElementById('bSum').addEventListener('click',function(){var l=DATA.filter(function(s){return picked[s.sessionId];});sendPrompt('Summon (copy) these '+l.length+' sessions into my other account — run summon --select for exactly these, show the plan with --dry-run first:\n'+fmtList(l));});
  201. render();
  202. })();
  203. </script>