SLOPSHOPPER

handoff

Builds a detailed handoff prompt at 250k tokens, on cold cache, or on /handoff, with a one-click fresh chat.

newpanecommandtoastpromptmodel
v0.1.0no licenseupdated 2026-10-25ikentrock/starwars-claude-mods/handoff
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · handoff
│ ┃ Handoff ✕ › fix the failing auth test and add an audit log call │ ┃ You asked for it. │ ┃ [ Start fresh chat ][ Copy ] ⏺ Read(src/auth.ts) │ ┃ Handoff ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ OK ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Files read ⎿ 3 pass, 1 fail │ ┃ • none │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Files created or edited │ ┃ • none ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ Skills used › /handoff │ ┃ • none ⎿ handoff: Handoff ready in the Handoff pane. │ ┃ │ ┃ Tools used │ ┃ • undefined │ ┃ │ ┃ Links │ ┃ • none │ ┃ │ ┃ Continue from "Next steps". Re-read the │ ┃ files above as needed before acting. │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Handoff
You asked for it. [ Start fresh chat ][ Copy ] Handoff OK Files read • none Files created or edited • none Skills used • none Tools used • undefined Links • none Continue from "Next steps". Re-read the files above as needed before acting.
README

starwars-claude-mods

Nine local Claude Code mods (UI and hook plugins) with a light Star Wars flavor.

ModWhat it does
brain-dumpIntercepts long, unstructured prompts, asks clarifying questions, rewrites them into a structured prompt. !raw prefix skips it.
helper-deskProgress bar from the to-do list, plus a side panel for sub-agents (/helper-desk).
cache-clockPrompt-cache countdown, Keep Warm button, /keep-warm.
handoffGenerates a handoff prompt at 250k tokens, on cold cache, or on /handoff.
reflectOffers to save rules/preferences to memory, CLAUDE.md, or a second brain.
chat-inventoryZero-token "In This Chat" panel (/inventory).
switchboard/mods panel to toggle mods and hide panels.
power-cellsGauges above the prompt for context, the 5-hour limit, and a weekly pacing plan: a daily ration, work-day weighting, over/under-plan warnings. /power plan shows the week.
droid-consoleRestyles the whole UI as a droid console: personality palettes, transmission headers, droid spinner words, status band. /droid switches personality.

Status: experimental. They pass claude plugin validate and tsc, but have not been tested at runtime. Expect rough edges.

Install

Add the marketplace once, then install the mods you want (user scope makes them load in every session):

claude plugin marketplace add ikentrock/starwars-claude-mods
claude plugin install brain-dump@starwars-claude-mods --scope user
claude plugin install cache-clock@starwars-claude-mods --scope user
claude plugin install chat-inventory@starwars-claude-mods --scope user
claude plugin install droid-console@starwars-claude-mods --scope user
claude plugin install handoff@starwars-claude-mods --scope user
claude plugin install helper-desk@starwars-claude-mods --scope user
claude plugin install power-cells@starwars-claude-mods --scope user
claude plugin install reflect@starwars-claude-mods --scope user
claude plugin install switchboard@starwars-claude-mods --scope user

Or one at a time from a session prompt:

/plugin install <mod> --marketplace ikentrock/starwars-claude-mods

Update later with claude plugin marketplace update starwars-claude-mods.

Source 2 files
hooks/register.tsx 155 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const PANE = 'handoff'
5const TOKEN_LIMIT = 250_000
6const COLD_MS = 5 * 60 * 1000
7const MODEL = 'sonnet'
8const DIGEST_CHARS = 90_000
9
10const text = atom({ plugin: 'handoff', key: 'text' } as const, '', { shape: 'text-1' })
11const why = atom({ plugin: 'handoff', key: 'why' } as const, '', { shape: 'why-1' })
12const isOffered = atom({ plugin: 'handoff', key: 'isOffered' } as const, false, {
13  shape: 'offered-1',
14})
15
16const unique = (items: string[]) => [...new Set(items)]
17const list = (items: string[]) => (items.length ? items.map(i => `- ${i}`).join('\n') : '- none')
18
19async function build($: EngineInterface, reason: string) {
20  const messages = await $.session.messages()
21  const read_: string[] = []
22  const made: string[] = []
23  const skills: string[] = []
24  const tools: string[] = []
25  const links: string[] = []
26  const lines: string[] = []
27
28  for (const m of messages) {
29    lines.push(`${m.role.toUpperCase()}: ${m.text}`)
30    links.push(...(m.text.match(/https?:\/\/[^\s)>\]"']+/g) ?? []))
31    for (const use of m.toolUses) {
32      tools.push(use.tool)
33      const path = typeof use.input.file_path === 'string' ? use.input.file_path : undefined
34      if (use.tool === 'Read' && path) read_.push(path)
35      if ((use.tool === 'Write' || use.tool === 'Edit') && path) made.push(`${use.tool}: ${path}`)
36      if (use.tool === 'Skill') skills.push(String(use.input.skill ?? 'unknown'))
37      for (const value of Object.values(use.input)) {
38        if (typeof value === 'string') links.push(...(value.match(/https?:\/\/[^\s)>\]"']+/g) ?? []))
39      }
40    }
41  }
42
43  const whole = lines.join('\n\n')
44  const digest =
45    whole.length > DIGEST_CHARS
46      ? `${whole.slice(0, DIGEST_CHARS / 3)}\n\n[… middle omitted …]\n\n${whole.slice(-DIGEST_CHARS * 2 / 3)}`
47      : whole
48
49  const written = await $.model.complete({
50    model: MODEL,
51    maxTokens: 4000,
52    prompt: `Below is a coding-assistant conversation. Write a HANDOFF PROMPT that a fresh session can start from with no other context. Use these sections: Goal; Current state (what is done, what is half-done); Key decisions and why; Constraints and user preferences; Open problems; Next steps (numbered, concrete). Write it as instructions to the next assistant. Do not list files, tools, skills or links: they are appended separately.\n\n<conversation>\n${digest}\n</conversation>`,
53  })
54  const body = written.isAnswered ? written.text.trim() : '(The summary model failed; see the inventory below.)'
55
56  const handoff = `# Handoff\n\n${body}\n\n## Files read\n${list(unique(read_))}\n\n## Files created or edited\n${list(unique(made))}\n\n## Skills used\n${list(unique(skills))}\n\n## Tools used\n${list(unique(tools))}\n\n## Links\n${list(unique(links).slice(0, 60))}\n\nContinue from "Next steps". Re-read the files above as needed before acting.`
57
58  await update($, text, () => handoff)
59  await update($, why, () => reason)
60  await $.ui.open({ id: PANE, title: 'Handoff', focus: true })
61}
62
63export const register: Register = on => {
64  let coldTimer: { cancel: () => void } | undefined
65
66  on('session.start', async ($, e, next) => {
67    await $.command.register({
68      name: 'handoff',
69      description: 'Write a handoff prompt for a fresh chat',
70    })
71
72    return next(e)
73  })
74
75  on('command.run', { command: 'handoff' }, async $ => {
76    await build($, 'You asked for it.')
77
78    return { text: 'Handoff ready in the Handoff pane.' }
79  })
80
81  on('session.end', async ($, e, next) => {
82    if (e.reason === 'clear') {
83      await update($, isOffered, () => false)
84    }
85
86    return next(e)
87  })
88
89  on('prompt.submit', ($, e, next) => {
90    if (!e.isMeta) {
91      coldTimer?.cancel()
92      coldTimer = undefined
93    }
94
95    return next(e)
96  })
97
98  on('turn.complete', async ($, e, next) => {
99    if (e.agentId !== undefined || !e.usage) return next(e)
100
101    const u = e.usage
102    const context = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
103    if (context >= TOKEN_LIMIT && !(await read($, isOffered))) {
104      await update($, isOffered, () => true)
105      $.ui.toast('Handoff: 250,000 tokens, the context has grown. Preparing a handoff.')
106      void build($, `The conversation reached ${context.toLocaleString()} tokens.`)
107    }
108
109    coldTimer?.cancel()
110    coldTimer = $.clock.after(COLD_MS, async () => {
111      if (await read($, isOffered)) return
112      await update($, isOffered, () => true)
113      void build($, 'The prompt cache went cold.')
114    })
115
116    return next(e)
117  })
118
119  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
120    const { Box, Button, Markdown, Text } = $.ui.resolve(e)
121    const handoff = await read($, text)
122    const reason = await read($, why)
123
124    if (handoff === '') return <Text dimColor>Nothing yet. Type /handoff.</Text>
125
126    const room = Math.max(8, (e.viewport?.rows ?? 30) - 8)
127    const shown = handoff.split('\n').slice(0, room * 2).join('\n')
128
129    return (
130      <Box flexDirection="column">
131        <Text dimColor>{reason}</Text>
132        <Box>
133          <Button
134            key="fresh"
135            variant="primary"
136            label="Start fresh chat"
137            onPress={async () => {
138              await $.command.run({ command: 'clear' })
139              await $.prompt.fill({ text: handoff, mode: 'replace' })
140              await update($, isOffered, () => false)
141              $.ui.toast('Handoff placed in your prompt box. Press Enter to begin.')
142            }}
143          />
144          <Button
145            key="copy"
146            label="Copy"
147            onPress={() => $.ui.copy({ text: handoff, surface: e.surface })}
148          />
149        </Box>
150        <Markdown key="preview" text={shown} />
151      </Box>
152    )
153  })
154}
155
types/index.d.ts 8 lines
1export type HandoffText = string
2
3declare module 'claude-code' {
4  interface PluginState {
5    handoff: { text: Shaped<string>; why: Shaped<string>; isOffered: Shaped<boolean> }
6  }
7}
8