SLOPSHOPPER

desktop-review-desk

Switch between Git status, staged diff, and unstaged diff.

newpanecommandtoastprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · desktop-review-desk
│ ┃ desktop-review-desk ✕ › fix the failing auth test and add an audit log call │ ┃ Desktop Review Desk requires the Claude │ ┃ Desktop Code tab. ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /desktop-review-desk │ ⎿ desktop-review-desk: Desktop Review Desk requires the Claude Des │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · desktop-review-desk
Desktop Review Desk requires the Claude Desktop Code tab.
README

Desktop Review Desk

Switch between Git status, staged diff, and unstaged diff.

Use it when: Inspect a change from one desktop review pane.

Desktop Review Desk fixture preview

Desktop-surface native test-kit tree, painted in a browser fixture preview with sample data. This is not a Claude Desktop app capture. Provenance.

Install and open

Requires Claude Code 2.1.287+ in the Claude Desktop Code tab; tested with 2.1.288's native test kit. Chat/Cowork tabs do not host this API. WSL Desktop sessions do not support plugins. No runtime packages or build step.

Install the current checkout immediately from a terminal in this repository:

claude plugin marketplace add .
claude plugin install desktop-review-desk@awesome-claude-code-mods

Or install the published collection:

claude plugin marketplace add whyashthakker/awesome-claude-code-mods
claude plugin install desktop-review-desk@awesome-claude-code-mods

Start a new local Code session in Claude Desktop, or run /reload-plugins in an open Code session, then /desktop-review-desk. Click controls or use Tab and Enter; Esc closes the pane. Non-Desktop commands return a compatibility explanation. Desktop setup and all 20 mods.

Access and behavior

Runs read-only Git argv commands in session cwd only when opened or refreshed. External diff and textconv disabled. Output truncated to 9,000 characters. Clipboard only on press.

Histories begin at load and reset on reload; no earlier activity is reconstructed. Store keys use the workspace directory; saved items are local to this plugin. Files/processes use session cwd. Stored content and clipboard exports can contain private information. No model calls, automatic prompt submissions, or permission approvals.

Verify

claude plugin validate mods/desktop-review-desk --strict
claude plugin test mods/desktop-review-desk

Native tests cover command opening, pane isolation, Desktop controls at two widths, unsupported surfaces, error/empty states, and preview capture. These checks validate element trees and callbacks; native Desktop painting and keyboard acceptance remain a manual check. Validation.

MIT licensed, with source and tests included.

Source 2 files
hooks/register.js 39 lines
1import {clean,text,code,row,frame,bounded,path,xml,num,pct,count,waitUntil,chart,gauge,columns,usageChips,strip} from './ui.js'
2const ID="desktop-review-desk", TITLE="Desktop Review Desk"
3let surface=null,prefix=''
4let tab='status',report='Open to inspect Git.'
5
6async function load($){try{const args=tab==='status'?['status','--short','--branch']:tab==='staged'?['diff','--cached','--no-ext-diff','--no-textconv','--no-color','--']:['diff','--no-ext-diff','--no-textconv','--no-color','--']
7 const r=await $.process.run(['git','--no-pager',...args],{cwd:await $.session.cwd(),timeoutMs:10000});report=clean(r.exitCode===0?r.stdout||'No changes.':'Git inspection failed: '+r.stderr)}catch(error){report='Git inspection failed: '+error.message}$.ui.invalidate('ui.render')}
8
9export function register(on){
10 on('session.start',async($,e,next)=>{
11  surface=e.surface;prefix='workspace:'+encodeURIComponent(e.cwd)+':'
12  await $.command.register({name:ID,description:"Switch between Git status, staged diff, and unstaged diff.",immediate:true})
13  if(surface==='desktop'){}
14  return next(e)
15 })
16 on('command.run',{command:"desktop-review-desk"},async($,e)=>{
17  if(surface!=='desktop')return {text:TITLE+' requires the Claude Desktop Code tab.'}
18  await load($)
19  await $.ui.open({id:ID,title:TITLE,focus:true,closeOnEscape:true})
20  return {}
21 })
22
23
24 on('ui.render',{component:'Pane'},async($,e,next)=>{
25  if(e.requestId!==ID)return next(e)
26  const E=$.ui.resolve(e)
27  if(e.surface!=='desktop')return text(E,TITLE+' requires the Claude Desktop Code tab.')
28  try{return frame(E,TITLE,[...await draw($,E,e),E.Button({key:'refresh',label:'Refresh',onPress:()=>$.ui.invalidate('ui.render')})])}
29  catch(error){return frame(E,TITLE,[text(E,'Unable to display: '+clean(error.message,500),{color:'yellow'}),E.Button({key:'refresh',label:'Retry',onPress:()=>$.ui.invalidate('ui.render')})])}
30 })
31}
32async function draw($,E,e){
33 const redraw=()=>$.ui.invalidate('ui.render')
34 const button=(key,label,onPress)=>E.Button({key,label,onPress})
35 const input=(key,label,value,onSubmit)=>E.Input({key,label,value,submitLabel:'apply',onSubmit})
36 const copy=async(value)=>{const r=await $.ui.copy({text:clean(value,30000)});if(!r.isCopied)$.ui.toast('Clipboard unavailable.')}
37 return [row(E,['status','staged','unstaged'].map(t=>button('tab-'+t,t+(tab===t?' ✓':''),async()=>{tab=t;await load($)}))),code(E,report),button('reload','Reload Git',()=>load($)),button('copy','Copy visible report',()=>copy(report)),text(E,'Read-only Git inspection. No staging, commits, or command execution from the diff.') ]
38}
39
hooks/ui.js 33 lines
1// Pure presentation helpers, copied into each standalone plugin.
2export function clean(v, n=9000) { return String(v ?? '').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g,'').slice(0,n) }
3export function text(E,v,props={}) { return E.Text({...props,children:[clean(v)]}) }
4export function code(E,v) { return E.Code({source:clean(v),language:'text'}) }
5export function row(E,children) { return E.Box({flexDirection:'row',columnGap:2,flexWrap:'wrap',children}) }
6export function frame(E,title,children) { return E.Box({flexDirection:'column',gap:1,children:[text(E,title,{bold:true,color:'cyan'}),...children]}) }
7export function bounded(v,n=9000) { if(v.length>n)throw Error('Input exceeds '+n+' characters.');return v }
8export function path(v) { v=v.trim();if(!v||v.startsWith('-')||/[\x00-\x1f]/.test(v))throw Error('Enter a path without control characters or leading flags.');return v }
9export function xml(v) { return clean(v,200).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'}[c])) }
10export function num(v) { return Number.isFinite(v)?v:0 }
11export function pct(v) { return Math.max(0,Math.min(100,num(v))) }
12export function count(v) { return Number.isFinite(v)?v.toLocaleString('en-US'):'not reported' }
13export function waitUntil(v,now) { const n=Date.parse(v);if(!Number.isFinite(n))return 'reset not reported';const m=Math.max(0,Math.ceil((n-now)/60000));return m?Math.floor(m/60)+'h '+m%60+'m':'reset due; awaiting report' }
14export function chart(E,values,labels,alt,scale) {
15 const data=values.slice(-12), names=labels.slice(-12),max=scale||Math.max(1,...data.map(num)),h=40+data.length*30
16 const rows=data.map((v,i)=>'<text x="12" y="'+(28+i*30)+'" fill="#d8dfeb" font-size="12">'+xml(names[i])+'</text><rect x="180" y="'+(12+i*30)+'" width="'+(Math.max(0,num(v))/max*310)+'" height="20" rx="5" fill="#8b9cf7"/><text x="505" y="'+(28+i*30)+'" fill="#d8dfeb" font-size="12">'+xml(Math.round(num(v)*10)/10)+'</text>').join('')
17 return E.Svg({source:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 '+h+'" width="580" height="'+h+'"><rect width="580" height="'+h+'" rx="12" fill="#182234"/>'+rows+'</svg>',alt:clean(alt),height:h})
18}
19export function gauge(E,value,label) { return chart(E,[pct(value)],[label],label+': '+count(value)+'%',100) }
20export function usageChips(u,records,now) {
21 const totals=records.reduce((a,r)=>({input:a.input+num(r.input_tokens),output:a.output+num(r.output_tokens),cache:a.cache+num(r.cache_read_input_tokens)}),{input:0,output:0,cache:0})
22 return [...u.rateLimits.slice(0,4).map(r=>r.kind+' '+count(r.percentUsed)+'% · '+waitUntil(r.resetsAt,now)),
23  'Context '+count(u.context?.percent)+'%', 'Input '+count(totals.input),'Output '+count(totals.output),'Cache read '+count(totals.cache),'USD '+(Number.isFinite(u.cost?.usd)?u.cost.usd.toFixed(2):'not reported')]
24}
25export function strip(E,chips,columns) {
26 const w=Math.min(1000,Math.max(240,(columns||80)*8)),colors=['#bbdbc6','#d4c8ed','#cbd5ed','#edcfbd','#bddec9','#ccd6f1','#e7d6a7']
27 let x=4,y=4
28 const pills=chips.map((s,i)=>{const cw=Math.min(w-8,s.length*6.5+24);if(x+cw>w){x=4;y+=40}const p='<rect x="'+x+'" y="'+y+'" width="'+cw+'" height="32" rx="16" fill="'+colors[i%colors.length]+'"/><text x="'+(x+12)+'" y="'+(y+21)+'" font-family="system-ui" font-size="12" fill="#202523">'+xml(s)+'</text>';x+=cw+8;return p}).join('')
29 const h=y+36
30 return E.Svg({source:'<svg xmlns="http://www.w3.org/2000/svg" width="'+w+'" height="'+h+'" viewBox="0 0 '+w+' '+h+'">'+pills+'</svg>',alt:chips.join('; '),height:h})
31}
32export function columns(E,e,children) { return E.Box({flexDirection:e.props.bodyColumns>=70?'row':'column',gap:2,children:children.map(c=>E.Box({flexDirection:'column',flexGrow:1,children:c}))}) }
33