| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- <!--
- summon in-chat CARD picker — TEMPLATE (default presentation for picking sessions)
- How Claude uses this:
- 1. run `summon pick --json --rich --days 30` (schema claude-mods.summon.pick/v2)
- 2. drop the envelope's `data` array into the `S` array below (shape documented
- inline) — one object per session, keys verbatim from pick/v2
- 3. render via the show_widget tool (Tabler `ti` icons + host CSS vars are both
- available in that context; light/dark safe)
- Self-contained: host CSS variables only, no external assets beyond the Tabler
- webfont the widget host already loads. sendPrompt(text) is a host global that
- submits a chat message as if the user typed it.
- DATA SHAPE per session (pick/v2 object — extra keys are ignored):
- { sessionId, title, projectRoot, cwd, worktree, isArchived, isRunning,
- lastActivityAt (ISO), turns, model, effort, events, toolCalls,
- densityBuckets:[24], durationMin, sizeKB, ctxTokens, ctxPeak, ctxWindow,
- ctxPct, ctxPeakPct, firstAsk }
- `firstAsk` is the mechanical opening ask. Claude MAY overwrite it with a
- distilled one-line summary before injecting; the widget just renders `summary`
- if present, else `firstAsk`.
- Inject a GENEROUS window (`--days 30`): the widget has its own client-side age
- filter (24h/3d/7d/30d/all) so the user narrows without a re-run. Archived
- sessions are hidden by default (toggle to show). Copies of one session across
- accounts are deduped by sessionId, newest kept.
- CLICK ACTIONS (all route through sendPrompt → one chat line Claude turns into
- action; per-card recover/summon should be spawned as background chips):
- ↗ summon · copy this session into the other account
- ⟳ recover · distil a handover brief + start a fresh seeded session
- ◉ peek · show the session's last messages
- ⧉ copy · copy the sessionId to the clipboard
- -->
- <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>
- <style>
- .ctl{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 14px}
- .ctl input,.ctl select{font-size:13px}
- .ctl .flt{flex:1;min-width:150px}
- .seg{display:inline-flex;border:.5px solid var(--border-strong);border-radius:var(--radius);overflow:hidden}
- .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}
- .seg button.on{background:var(--surface-1);color:var(--text-primary)}
- .cbx{font-size:12px;color:var(--text-secondary);display:inline-flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap}
- .deck.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
- .deck.list{display:flex;flex-direction:column;gap:8px}
- .card{background:var(--surface-2);border:.5px solid var(--border);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:9px}
- .card.sel{border-color:var(--border-accent);box-shadow:0 0 0 1px var(--border-accent)}
- .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}
- .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}
- .deck.list .sum{-webkit-line-clamp:2}
- /* Header overflow discipline (a half-width grid card is ~250-300px, so space
- is tight): .proj is the shrinker (min-width:0 + ellipsis); the action icons
- are grouped in .acts with margin-left:auto so they hug the right edge; and
- .hd may WRAP (row-gap) as a hard backstop — icons drop to a second line
- rather than ever spilling past the border. Context lives as a chip in the
- stats row (not the header) to keep the header light. */
- .hd{display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap;row-gap:6px}
- .proj{font-size:12px;color:var(--text-secondary);font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
- .tag{font-size:10px;border-radius:5px;padding:0 5px;border:.5px solid var(--border);color:var(--text-muted);flex:none}
- .tag.run{background:var(--bg-success);color:var(--text-success);border:0}
- .acts{display:inline-flex;align-items:center;gap:2px;margin-left:auto;flex:none}
- .ib{border:0;background:transparent;color:var(--text-muted);cursor:pointer;padding:2px;display:inline-flex;font-size:13px;border-radius:5px;flex:none}
- .ib:hover{color:var(--text-accent);background:var(--surface-1)}
- .cchip i{font-size:13px}
- .ttl{font-size:15px;font-weight:500;color:var(--text-primary);line-height:1.35}
- .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}
- .dens{display:flex;align-items:flex-end;gap:2px;height:30px}
- .dens i{flex:1;border-radius:1px;min-height:1px;display:block}
- .stats{display:flex;flex-wrap:wrap;gap:5px}
- .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}
- .chip i{font-size:13px;color:var(--text-muted)}
- .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}
- .foot{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:14px 0 0}
- .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}
- .foot button:disabled{opacity:.45;cursor:default}
- </style>
- <div class="ctl">
- <input class="flt" id="flt" type="text" placeholder="filter title, project, or path…">
- <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>
- <select id="sort" style="width:auto">
- <option value="recent">Most recent</option>
- <option value="ctx">Most context used</option>
- <option value="size">Largest size</option>
- <option value="turns">Most turns</option>
- <option value="msgs">Most messages</option>
- <option value="project">Project</option>
- </select>
- <label class="cbx"><input type="checkbox" id="arch"> show archived</label>
- <div class="seg">
- <button id="bGrid" class="on" aria-label="Grid view"><i class="ti ti-layout-grid" aria-hidden="true"></i>Grid</button>
- <button id="bList" aria-label="List view"><i class="ti ti-list" aria-hidden="true"></i>List</button>
- </div>
- </div>
- <div id="deck" class="deck grid"></div>
- <div class="foot">
- <span id="cnt" style="font-size:13px;color:var(--text-secondary)">0 selected</span>
- <span style="flex:1"></span>
- <button id="bRec" disabled><i class="ti ti-refresh" aria-hidden="true"></i>Recover selected</button>
- <button id="bSum" disabled><i class="ti ti-arrow-up-right" aria-hidden="true"></i>Summon selected</button>
- <button id="bClr" disabled><i class="ti ti-x" aria-hidden="true"></i>Clear</button>
- </div>
- <script id="D" type="application/json">
- [{"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"}]
- </script>
- <script>
- (function(){
- var RAW=JSON.parse(document.getElementById('D').textContent);
- var seen={},DATA=[];
- RAW.forEach(function(s){if(!seen[s.sessionId]){seen[s.sessionId]=1;DATA.push(s);}});
- var PAL=['#7F77DD','#1D9E75','#D85A30','#378ADD','#BA7517','#D4537E','#534AB7','#0F6E56','#993C1D'];
- function projName(p){return (p||'').replace(/\//g,'\\').replace(/\\$/,'').split('\\').pop()||p;}
- 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];}
- var NOW=Date.now();
- 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';}
- function nf(n){return (n||0).toLocaleString('en-US');}
- function fSize(kb){kb=kb||0;return kb>=1024?(kb/1024).toFixed(1)+' MB':kb+' KB';}
- function fDur(m){m=m||0;return m<60?m+'m':m<1440?Math.round(m/60)+'h':Math.round(m/1440)+'d';}
- function fTok(n){n=n||0;return n>=1000?Math.round(n/1000)+'k':''+n;}
- function esc(s){return (s||'').replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
- 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;}
- function ctxColor(p){return p<50?'#1D9E75':p<75?'#BA7517':'#E24B4A';}
- 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('');}
- 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>';}
- var picked={};
- function card(s){
- var col=projColor(s.projectRoot),body=s.summary||s.firstAsk||'',pk=picked[s.sessionId];
- var ctxTip=fTok(s.ctxTokens)+' of '+fTok(s.ctxWindow)+' context used'+(s.ctxPeakPct>s.ctxPct?' · peak '+fTok(s.ctxPeak)+' ('+s.ctxPeakPct+'%)':'');
- var wt=s.worktree?'<span class="tag" title="worktree '+esc(s.worktree)+'">worktree</span>':'';
- var run=s.isRunning?'<span class="tag run">running</span>':'';
- var arch=s.isArchived?'<span class="tag">archived</span>':'';
- var ttl=(!s.title||s.title==='(untitled)')?'<span style="color:var(--text-muted);font-style:italic">untitled session</span>':esc(s.title);
- return '<div class="card'+(pk?' sel':'')+'" data-id="'+esc(s.sessionId)+'">'
- +'<div class="hd">'
- +'<input type="checkbox" class="pk" '+(pk?'checked':'')+' aria-label="Select session" style="margin:0 2px 0 0">'
- +'<span style="width:8px;height:8px;border-radius:50%;background:'+col+';flex:none"></span>'
- +'<span class="proj">'+esc(projName(s.projectRoot))+'</span>'+wt+run+arch
- +'<span class="acts">'
- +'<button class="ib act-peek" aria-label="Peek last messages" title="Peek last messages"><i class="ti ti-eye" aria-hidden="true"></i></button>'
- +'<button class="ib act-copy" aria-label="Copy session id" title="Copy session id"><i class="ti ti-copy" aria-hidden="true"></i></button>'
- +'<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>'
- +'<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>'
- +'</span>'
- +'</div>'
- +'<div class="ttl">'+ttl+'</div>'
- +(body?'<div class="sum">'+esc(body)+'</div>':'')
- +'<div class="dens" title="activity density across session lifetime">'+bars(s.densityBuckets,col)+'</div>'
- +'<div class="stats">'
- +'<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>'
- +chip('cpu',shortModel(s.model),s.effort||'')
- +chip('clock',age(s.lastActivityAt),'')
- +chip('messages',nf(s.turns),'turns')
- +chip('message-2',nf(s.events),'msgs')
- +chip('tool',nf(s.toolCalls),'tools')
- +chip('database',fSize(s.sizeKB),'')
- +chip('hourglass',fDur(s.durationMin),'')
- +'</div>'
- +'<div class="dir" title="'+esc(s.cwd)+'"><i class="ti ti-folder" aria-hidden="true"></i>'+esc(s.cwd)+'</div>'
- +'</div>';
- }
- var deck=document.getElementById('deck'),flt=document.getElementById('flt'),win=document.getElementById('win'),
- sortEl=document.getElementById('sort'),archEl=document.getElementById('arch'),cnt=document.getElementById('cnt');
- function visible(){
- var q=flt.value.toLowerCase(),maxH=win.value?+win.value:Infinity,showArch=archEl.checked;
- return DATA.filter(function(s){
- if(s.isArchived&&!showArch)return false;
- if((NOW-Date.parse(s.lastActivityAt))/36e5>maxH)return false;
- if(q){var hay=((s.title||'')+' '+(s.projectRoot||'')+' '+(s.cwd||'')+' '+(s.worktree||'')).toLowerCase();if(hay.indexOf(q)<0)return false;}
- return true;
- });
- }
- function order(a){var m=sortEl.value,x=a.slice();
- if(m==='ctx')x.sort(function(p,q){return (q.ctxTokens||0)-(p.ctxTokens||0);});
- else if(m==='size')x.sort(function(p,q){return (q.sizeKB||0)-(p.sizeKB||0);});
- else if(m==='turns')x.sort(function(p,q){return (q.turns||0)-(p.turns||0);});
- else if(m==='msgs')x.sort(function(p,q){return (q.events||0)-(p.events||0);});
- else if(m==='project')x.sort(function(p,q){return projName(p.projectRoot).localeCompare(projName(q.projectRoot))||(q.ctxTokens||0)-(p.ctxTokens||0);});
- else x.sort(function(p,q){return Date.parse(q.lastActivityAt)-Date.parse(p.lastActivityAt);});
- return x;
- }
- function fmtList(list){return list.map(function(s){return '- "'+s.title+'" ('+s.sessionId+', '+s.cwd+')';}).join('\n');}
- function refreshFoot(){
- var ids=Object.keys(picked).filter(function(k){return picked[k];}),n=ids.length;
- cnt.textContent=n+' selected';
- ['bRec','bSum','bClr'].forEach(function(id){document.getElementById(id).disabled=!n;});
- }
- function render(){
- var arr=order(visible());
- 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>';
- deck.querySelectorAll('.card').forEach(function(el){
- var s=DATA.filter(function(d){return d.sessionId===el.dataset.id;})[0];if(!s)return;
- el.querySelector('.pk').addEventListener('change',function(){picked[s.sessionId]=this.checked;el.classList.toggle('sel',this.checked);refreshFoot();});
- 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.');});
- 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);});
- 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.');});
- 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).');});
- });
- refreshFoot();
- }
- [flt,win,sortEl,archEl].forEach(function(e){e.addEventListener(e===flt?'input':'change',render);});
- document.getElementById('bGrid').addEventListener('click',function(){deck.className='deck grid';this.classList.add('on');document.getElementById('bList').classList.remove('on');});
- document.getElementById('bList').addEventListener('click',function(){deck.className='deck list';this.classList.add('on');document.getElementById('bGrid').classList.remove('on');});
- document.getElementById('bClr').addEventListener('click',function(){picked={};render();});
- 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));});
- 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));});
- render();
- })();
- </script>
|