SLOPSHOPPER

link-shelf

Save labeled HTTP and HTTPS reference links.

newpanecommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · link-shelf
│ ┃ Link Shelf ✕ › fix the failing auth test and add an audit log call │ ┃ Link Shelf │ ┃ ⏺ Read(src/auth.ts) │ ┃ Save labeled HTTP and HTTPS reference links. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ Reference links · 0 items ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Label | URL: Type and press Enter ⏎ save ⎿ 3 pass, 1 fail │ ┃ │ ┃ Saved per workspace. Refresh to see changes ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ from other sessions. │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ [ Reload saved items ] │ › /link-shelf │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Link Shelf
Link Shelf Save labeled HTTP and HTTPS reference links. Reference links · 0 items Label | URL: Type and press Enter ⏎ save Saved per workspace. Refresh to see changes from other sessions. [ Reload saved items ]
README

Link Shelf

Save labeled HTTP and HTTPS reference links.

Use it when: Keep docs for the current project visible.

Link Shelf render preview

Screenshot of this mod's render tree in the fixture preview, with sample data. This is not a capture of the Claude Code application. How previews are made.

Install and open

claude plugin marketplace add whyashthakker/awesome-claude-code-mods
claude plugin install link-shelf@awesome-claude-code-mods

Run /reload-plugins in an existing session, then /link-shelf. Use Tab and Enter to operate controls; Esc closes the pane. Terminal and Desktop Code tab supported; pane UI is unavailable in claude -p and the VS Code chat panel.

Try the source without installing:

claude --plugin-dir ./mods/link-shelf

Example: open /link-shelf and enter Mods docs | https://code.claude.com/docs/en/plugins/mods/overview in its input.

Access and behavior

Stores user-entered items in this plugin’s local store, partitioned by workspace path. Each item has a separate key. No file writes or network.

Module variables reset on hot reload. Workspace collections persist in Claude Code's local plugin store and reload when reopened. Deleting an item removes only that item's store key. Collections show the latest 100 items; concurrent changes to the same item use last-write-wins behavior. All display output is bounded; viewers may truncate long content to 9,000 characters. Relative file paths and Git commands use the session working directory.

Verify

Tested with Claude Code 2.1.288. Minimum documented mods version: 2.1.287. The API can change; validate against your installed version.

claude plugin validate mods/link-shelf --strict
claude plugin test mods/link-shelf

Tests cover plugin commands, pane isolation, both UI surfaces, and the behavior described in tests/register.test.ts. Drawing tests validate element trees; they do not verify pixels painted by the native apps. Validation details.

MIT licensed. No runtime packages or build step required.

Source 2 files
hooks/register.js 64 lines
1import { clean, text, code, row, frame, bar, pretty, bounded, entries, safePath } from './ui.js'
2const ID = "link-shelf"
3
4let prefix=''
5let items=[]
6let message=''
7async function load($) {
8 const keys=(await $.store.keys()).filter(k=>k.startsWith(prefix)).sort().slice(-100)
9 items=[]
10 for(const key of keys) {
11  const item=await $.store.get(key)
12  if(item && typeof item.text==='string')items.push({...item,key})
13 }
14 $.ui.invalidate('ui.render')
15}
16async function save($, value) {
17 try {
18  const content=bounded(value.trim(),4000)
19  if(!content)return
20  const parts=content.split('|');const u=new URL(parts.slice(1).join('|').trim());if(!['http:','https:'].includes(u.protocol)||u.username||u.password)throw new Error('Use Label | https://example.com without credentials.')
21  const date=new Date(await $.clock.now()).toISOString()
22  const key=prefix+date+'-'+crypto.randomUUID()
23  await $.store.set(key,{text:content,done:false,date})
24  message='Saved locally.'
25  await load($)
26 } catch(error){message=error.message;$.ui.invalidate('ui.render')}
27}
28
29export function register(on) {
30  on('session.start', async ($, e, next) => {
31    prefix='workspace:'+encodeURIComponent(e.cwd)+':item:'
32await load($)
33    await $.command.register({name:ID, description:"Save labeled HTTP and HTTPS reference links.", immediate:true, argumentHint:"[entry]"})
34    return next(e)
35  })
36  on('command.run', {command:"link-shelf"}, async ($, e) => {
37    await load($)
38if(e.args.trim()) await save($,e.args)
39    await $.ui.open({id:ID,title:"Link Shelf",focus:true,closeOnEscape:true})
40    return {}
41  })
42
43  on('ui.render', {component:'Pane'}, async ($, e, next) => {
44    if(e.requestId !== ID) return next(e)
45    const E = $.ui.resolve(e)
46    try {
47      const body = await draw($, E)
48      return frame(E,"Link Shelf","Save labeled HTTP and HTTPS reference links.",body)
49    } catch(error) {
50      return frame(E,"Link Shelf",'Unable to inspect this data.',[text(E,error.message,{color:'yellow'}),E.Button({key:'refresh',label:'Refresh',onPress:()=>$.ui.invalidate('ui.render')})])
51    }
52  })
53}
54async function draw($, E) {
55  const refresh = E.Button({key:'refresh',label:'Refresh',hotkey:'r',plain:true,onPress:()=>$.ui.invalidate('ui.render')})
56
57return [text(E,"Reference links"+' · '+items.length+' items',{bold:true}),
58 E.Input({key:'entry',label:"Label | URL",value:'',placeholder:'Type and press Enter',submitLabel:'save',onSubmit:(v)=>save($,v)}),
59 ...items.map((item,i)=>E.Box({flexDirection:'column',children:[E.Link({label:clean(item.text.split('|')[0].trim(),200),href:item.text.split('|').slice(1).join('|').trim()}),E.Button({key:'delete-'+i,label:'Delete',plain:true,dimColor:true,onPress:async()=>{await $.store.delete(item.key);await load($)}})]})),
60  text(E,message || 'Saved per workspace. Refresh to see changes from other sessions.',{dimColor:true}),
61 E.Button({key:'refresh',label:'Reload saved items',onPress:()=>load($)})]
62
63}
64
hooks/ui.js 36 lines
1// Pure UI helpers. No engine access; each plugin contains its own copy.
2export function clean(value, limit = 9000) {
3  return String(value ?? '').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '').slice(0, limit)
4}
5export function text(E, value, props = {}) {
6  return E.Text({ ...props, children: [clean(value)] })
7}
8export function code(E, value, language = 'text') {
9  return E.Code({ source: clean(value), language })
10}
11export function row(E, children) {
12  return E.Box({ flexDirection: 'row', columnGap: 2, flexWrap: 'wrap', children })
13}
14export function frame(E, title, subtitle, children) {
15  return E.Box({ flexDirection: 'column', gap: 1, children: [
16    text(E, title, { bold: true, color: 'cyan' }),
17    text(E, subtitle, { dimColor: true }), ...children,
18  ] })
19}
20export function bar(percent, width = 24) {
21  const p = Math.max(0, Math.min(100, Number(percent) || 0))
22  const n = Math.round(p / 100 * width)
23  return '[' + '#'.repeat(n) + '-'.repeat(width - n) + '] ' + p.toFixed(1) + '%'
24}
25export function pretty(value) { return JSON.stringify(value, null, 2) }
26export function bounded(value, max = 9000) {
27  if (value.length > max) throw new Error('Input exceeds ' + max + ' characters. Use a smaller selection.')
28  return value
29}
30export function entries(value) { return value && typeof value === 'object' ? Object.entries(value) : [] }
31export function safePath(value) {
32  const path = value.trim()
33  if (!path || path.startsWith('-') || /[\x00-\x1f]/.test(path)) throw new Error('Enter a path without control characters or leading flags.')
34  return path
35}
36