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

Switch between Git status, staged diff, and unstaged diff.
Use it when: Inspect a change from one desktop review pane.

Desktop-surface native test-kit tree, painted in a browser fixture preview with sample data. This is not a Claude Desktop app capture. Provenance.
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.
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.
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.
hooks/register.js 39 lines1import {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}
39hooks/ui.js 33 lines1// 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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