SLOPSHOPPER

prompt-jump

/prompts lists the messages you sent in this session; press one to scroll the transcript to it

panerowscommandtoastprompt
v0.1.0no licenseupdated 2026-10-17JunWang666/cc-cockpit/mods/prompt-jump
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-jump
│ ┃ 我的消息 ✕ › fix the failing auth test and add an audit lo╭───────────────────╮ │ ┃ 1 条消息,最新的在上 · 按编号跳转,0 │ prompt-jump │ │ ┃ 回到底部,Esc 关闭 ⏺ Read(src/auth.ts) │ 还不知道底部在哪:发一条消息后再试 │ │ ┃ 0: ↓ 回到底部 ⎿ Read 6 lines ╰───────────────────╯ │ ┃ 1: p1 ⏺ 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 │ │ › /prompts │ ⎿ prompt-jump: 已打开消息列表。 │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 我的消息
1 条消息,最新的在上 · 按编号跳转,0 回到底部,Esc 关闭 0: ↓ 回到底部 1: p1
README

cc-cockpit

Claude Code mods for a terminal cockpit: a stats band above the prompt and quick navigation through your own messages. Needs Claude Code 2.1.287 or newer.

ModWhat it does
quota-bandA divider between the transcript and the prompt, and one row under it: turn number, output speed (tok/s), turn and session cost, prompt-cache countdown, 5-hour and weekly quota with reset times, and context usage.
welcomeWhen a new session starts, a card above the prompt shows the project's branch, uncommitted changes, ahead/behind, the last three commits, a one-line gist of README.md / AGENTS.md / CLAUDE.md (press one to read its opening lines), what the last session did (its recap and the files it edited) and the project's five most recent sessions; press a digit in the empty prompt to resume one. It hides on your first message; /welcome brings it back.
desktop-cockpitThe three mods above, drawn for Claude Code Desktop. Above the prompt: the stats row (turn, tok/s, cost, cache minutes left, quota and context as bars) and, in a new session, the welcome card. `/cockpit [usage\project\prompts] [keyword]` (or the 驾驶舱 › button) opens a pane with three tabs: usage with reset times, the project's git state, docs and last session, and your messages, each one click to scroll to. On desktop, quota-band and welcome leave the band to it; in a terminal it draws nothing above the prompt. Needs no Python.
auto-continue`/auto-continue on [1d\12h\90m] turns on auto-continue for the session (off turns it off, no argument shows its state). When a turn stops because the weekly (or 5-hour) quota is used up, it waits for that window to reset and then sends a prompt to carry the work on — but only if the reset comes within the wait you allowed, counted from when the quota ran out (24 hours by default). While waiting, the status line shows when it will continue and the prompt footer shows auto-continue. It also continues at a set time: /auto-continue at 14:30 [message] (today, or tomorrow once passed), at 10/22 09:00 [message] or in 2h [message] sends the message (continue by default) then; cancel drops it. Options: on by default, the longest wait, the prompts it sends, and whether to wait for the weekly quota only. Works in the terminal and the desktop app. With quota-band or desktop-cockpit installed, its state also shows in their stats row beside the quota (⏳ 10:01 自动续, ⏰ 14:30 定时续, ⟳ 自动续), and desktop-cockpit's usage tab has a picker for the longest wait (or off), a field for the message to send, a picker of set times (30 minutes to 5 hours, the next 9:00), a field for any time (14:30, 10/22 09:00, 2h`) and a button to cancel it.
prompt-jump/prompts lists the messages you sent in this session; press a number to scroll the transcript to one. 0 or /bottom jumps back to the end. Scrolling needs the fullscreen layout (/tui fullscreen).

Install

/plugin install quota-band --marketplace JunWang666/cc-cockpit
/plugin install prompt-jump --marketplace JunWang666/cc-cockpit
/plugin install welcome --marketplace JunWang666/cc-cockpit
/plugin install desktop-cockpit --marketplace JunWang666/cc-cockpit
/plugin install auto-continue --marketplace JunWang666/cc-cockpit

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with JunWang666/cc-cockpit, then Add plugin.

Develop

Each mod is in mods/<name>/. Check one with claude plugin validate mods/<name> and claude plugin test mods/<name>, then /reload-plugins in a session.

Source 2 files
hooks/register.tsx 133 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const PANE = 'prompt-jump'
5const HOTKEYS = '123456789abcdefghijklmnopqrstuvwxyz'
6const filter = atom({ plugin: 'prompt-jump', key: 'filter' } as const, '')
7
8// Transcript rows of the person's prompts, in the order the transcript drew them.
9// A render hook may not write $.state, so this lives in the module.
10const drawn: { requestId: string; text: string }[] = []
11// Every transcript row drawn so far, and the newest one first drawn since the last prompt: the bottom.
12const seenRows = new Set<string>()
13let isLive = false
14let bottomRow: string | undefined
15
16const isOwnPrompt = (text: string) => text.trim() !== '' && !text.trimStart().startsWith('<')
17
18function noteRow(requestId: string) {
19  if (seenRows.has(requestId)) return
20  seenRows.add(requestId)
21  if (isLive) bottomRow = requestId
22}
23
24async function scrollTo($: EngineInterface, requestId: string, block: 'start' | 'end') {
25  const moved = await $.ui
26    .scroll({ to: { requestId }, block })
27    .catch((err: unknown) => ({ deny: String(err) }))
28  if (moved.deny) $.ui.toast(`没法滚动过去:${moved.deny}(对话区需在全屏模式 /tui fullscreen)`)
29}
30
31async function scrollToBottom($: EngineInterface) {
32  if (!bottomRow) {
33    $.ui.toast('还不知道底部在哪:发一条消息后再试')
34    return
35  }
36  await scrollTo($, bottomRow, 'end')
37}
38
39const oneLine = (text: string, width: number) => {
40  const flat = text.replace(/\s+/g, ' ').trim()
41  return flat.length > width ? flat.slice(0, width - 1) + '…' : flat
42}
43
44export const register: Register = on => {
45  on('session.start', async ($, e, next) => {
46    await $.command.register({
47      name: 'prompts',
48      description: '列出本会话里你发过的消息,按编号跳到那条(可加关键词过滤)',
49      argumentHint: '[关键词]',
50    })
51    await $.command.register({ name: 'bottom', description: '对话区跳回最底部' })
52    return next(e)
53  })
54
55  on('command.run', { command: 'bottom' }, async $ => {
56    await scrollToBottom($)
57    return { text: '' }
58  })
59
60  on('prompt.submit', ($, e, next) => {
61    isLive = true
62    return next(e)
63  })
64
65  on('ui.render', { component: 'UserMessage' }, ($, e, next) => (noteRow(e.requestId), next(e)))
66  on('ui.render', { component: 'AssistantMessage' }, ($, e, next) => (noteRow(e.requestId), next(e)))
67  on('ui.render', { component: 'ToolUse' }, ($, e, next) => (noteRow(e.requestId), next(e)))
68  on('ui.render', { component: 'ToolResult' }, ($, e, next) => (noteRow(e.requestId), next(e)))
69
70  on('command.run', { command: 'prompts' }, async ($, e) => {
71    await update($, filter, () => (e.args ?? '').trim())
72    const opened = await $.ui.open({ id: PANE, title: '我的消息', focus: true, closeOnEscape: true })
73    return { text: opened.isPlaced ? '已打开消息列表。' : `消息列表暂时无法显示:${opened.reason}` }
74  })
75
76  on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
77    const kind = e.props.origin.kind
78    const isPerson = kind !== 'task-notification' && kind !== 'plugin' && !e.props.from
79    if (isPerson && isOwnPrompt(e.props.text) && !drawn.some(d => d.requestId === e.requestId)) {
80      drawn.push({ requestId: e.requestId, text: e.props.text })
81    }
82    return next(e)
83  })
84
85  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
86    const { Box, Text, Button } = $.ui.resolve(e)
87    const query = (await read($, filter)).toLowerCase()
88    const messages = await $.session.messages()
89
90    // The session's own prompts, matched in order to the transcript rows drawn so far.
91    const pool = [...drawn]
92    const prompts = messages
93      .filter(m => m.role === 'user' && isOwnPrompt(m.text))
94      .map((m, i) => {
95        const at = pool.findIndex(d => d.text.trim() === m.text.trim())
96        const requestId = at >= 0 ? pool.splice(at, 1)[0]?.requestId : undefined
97        return { n: i + 1, text: m.text, requestId }
98      })
99      .filter(p => !query || p.text.toLowerCase().includes(query))
100      .reverse()
101
102    const width = Math.max(20, (e.props.bodyColumns || e.viewport?.columns || 80) - 12)
103
104    const jump = async (requestId: string | undefined) => {
105      if (!requestId) {
106        $.ui.toast('这条还没在对话区加载过:往上滚动经过它一次后就能跳了')
107        return
108      }
109      await scrollTo($, requestId, 'start')
110    }
111
112    return (
113      <Box flexDirection="column">
114        <Text dimColor>
115          {query ? `过滤「${query}」· ` : ''}
116          {prompts.length} 条消息,最新的在上 · 按编号跳转,0 回到底部,Esc 关闭
117        </Text>
118        <Button key="bottom" plain hotkey="0" onPress={() => scrollToBottom($)}>
119          ↓ 回到底部
120        </Button>
121        {prompts.length === 0 && <Text dimColor>还没有消息。</Text>}
122        {prompts.map((p, i) => (
123          <Button key={`p${p.n}`} plain hotkey={HOTKEYS[i]} onPress={() => jump(p.requestId)}>
124            <Text dimColor>#{p.n} </Text>
125            {oneLine(p.text, width)}
126            {p.requestId ? '' : <Text dimColor> ·未加载</Text>}
127          </Button>
128        ))}
129      </Box>
130    )
131  })
132}
133
types/index.d.ts 8 lines
1export type PromptJumpState = Record<string, never>
2
3declare module 'claude-code' {
4  interface PluginState {
5    'prompt-jump': { filter: string }
6  }
7}
8