SLOPSHOPPER

task-progress

給 Claude 一個回報工具,把任務清單和進度寫進 cmux 左側欄

newguardprompttoolprocess
v0.1.0no licenseupdated 2026-10-08Toriwuuu/claude-mods/mods/task-progress
A shopper browsing a rack in a slop shop
README

claude-mods

Daniel 的 Claude Code mod 清單,讓每台電腦都裝到同一套 mod。

  • 自己做的 mod:程式碼放在 mods/。
  • 別人的 mod:只在清單裡指定 repo,並鎖定審查過的版本(commit),作者之後的更新不會自動進來。

清單本體是 .claude-plugin/marketplace.json。需要 Claude Code 2.1.287 以上。

收錄的 mod

mod來源做什麼指令
cache-glance自己做的輸入框上方常駐一行快取狀態:還有多久過期、過期後重寫要多少錢無
task-progress自己做的給 Claude 一個回報工具,多步驟工作時把任務清單和進度寫進 cmux 左側欄(只在 cmux 裡啟動;搭配下方的 cmux 側欄)無
auto-panes自己做的每次開 Claude Code 等 1.5 秒自動打開右側的 Diff(/diff)和 Files(/filetree)。Source Control 不自動開:它的 /git 打開時會搶走鍵盤,第一個打的字會進 commit 欄,所以用 ◨ 手動開無
image-viewjarrodwatts/claude-image-view貼上的圖片在輸入框上方顯示縮圖(需要支援圖片的終端機)無
filetreedata-goblin/claude-code-filetree右側欄的檔案樹,標出 Claude 正在讀寫的檔案(需要全螢幕模式)/filetree
cache-taxkaranb192/cache-tax快取保溫;閒置過久後的第一則訊息會擋下一次並顯示重寫價格/keepwarm、/cache-tax
next-stepsanthropics/claude-plugins-community(Thariq Shihipar)每次回覆後在輸入框上方給最多三個下一步建議;對話框是空的時按 1、2、3 填進去,再按 Enter 送出,0 關掉,Tab 採用第一個。不會自己送出無
source-controlmanuel-will/cc-source-controlVS Code 風格的原始碼控制:commit 歷史分支圖、暫存、commit、同步(介面只有英文、德文)/git

別人的 mod 都在 2026-10-08 讀過程式碼:image-view、filetree 不連網;cache-tax 不連網,但開了 /keepwarm 會定時送請求、吃額度;source-control 只執行 git、沒有破壞性操作,背景每 10 分鐘 git fetch 一次(設定 fetchIntervalMinutes 為 0 可關閉),從它的面板 commit 時不會跑專案的 git hooks;next-steps 只讀指令清單、填對話框,不連網、不執行程式,但每輪回覆後會送一次分身請求問建議,對話越長這次請求越貴(讀一次整段快取,跟一次保溫差不多)。

filetree 和 source-control 共用右側面板,用 source-control 那列 Commit 按鈕右邊的 ◨ 切換。

cmux 側欄:task-cards

cmux/task-cards.js 是 cmux 的自訂左側欄(cmux 的 Custom Sidebars beta 功能),不是 Claude Code 的 mod。它照 cmux 內建的 workspaces 範本改,每個工作區一張卡片,把「最後一則訊息」換成 task-progress 寫進來的任務清單:

  • 選取中的工作區:列出每一步(✓ 完成、▸ 進行中、· 還沒做)
  • 其他工作區:進度條+目前做到哪一步
  • 沒有清單時:顯示 Claude 的狀態(工作中、等你回覆、閒置)

task-progress 把清單寫在工作區的「描述」、摘要寫在「進度」。換回 cmux 預設側欄時,這兩項也會顯示在工作區下面。

安裝:在 ~/.config/cmux/sidebars/ 放一個捷徑,再到 cmux 側欄切換鈕上按右鍵,選 task-cards。

mkdir -p ~/.config/cmux/sidebars
# 主力電腦(清單在 ~/claude-mods)
ln -s ~/claude-mods/cmux/task-cards.js ~/.config/cmux/sidebars/
# 其他電腦(清單是從 GitHub 加的,放在 Claude Code 的下載資料夾)
ln -s ~/.claude/plugins/marketplaces/claude-mods/cmux/task-cards.js ~/.config/cmux/sidebars/
cmux sidebar validate task-cards

在另一台電腦安裝(例如公司 Mac mini)

repo 是私有的,第一次要先讓那台電腦能讀 GitHub:

gh auth login
gh auth setup-git

加入清單,再裝想要的 mod:

claude plugin marketplace add Toriwuuu/claude-mods
claude plugin install cache-glance@claude-mods
claude plugin install image-view@claude-mods
claude plugin install filetree@claude-mods
claude plugin install cache-tax@claude-mods
claude plugin install source-control@claude-mods
claude plugin install task-progress@claude-mods
claude plugin install auto-panes@claude-mods
claude plugin install next-steps@claude-mods

裝好後重開 Claude Code。要用 task-cards 側欄的話,照上面「cmux 側欄」那段放捷徑。

更新

在主力電腦改完並 push 之後,另一台執行:

claude plugin marketplace update claude-mods
claude plugin update cache-glance@claude-mods

然後重開 Claude Code。

注意:Claude Code 是看 plugin.json 裡的 version 判斷有沒有新版。改了自己的 mod,要把版本號往上加(例如 0.1.0 → 0.1.1),另一台才會更新。

開發(主力電腦)

主力電腦的清單直接指向本機資料夾:

claude plugin marketplace add ~/claude-mods

這樣改了 mods/ 裡的檔案,在 Claude Code 裡執行 /reload-plugins 就會生效,不用先 push。確認沒問題再 commit、push。

改完可以先跑檢查:

claude plugin validate mods/cache-glance
claude plugin test mods/cache-glance

升級別人的 mod

不要直接換 sha。先讀過新版本的程式碼,確認沒問題再把 marketplace.json 裡的 sha 換成新的 commit。

Source 3 files
hooks/register.ts 110 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Step } from '../types'
5import { describe, normalize, summarize } from './format'
6
7const TOOL = 'mcp__task-progress__progress'
8
9// Kept in $.state so a reload of the mod can put the sidebar back as it was.
10const steps = atom({ plugin: 'task-progress', key: 'steps' } as const, [] as Step[])
11
12const RULES = `# Task progress
13The person follows your work in their cmux sidebar. For work of 3 or more steps, call ${TOOL} with the whole step list: once before you start, then again each time a step finishes or the plan changes. Use 2 to 8 steps, each title at most 6 words in the person's language, exactly one step "active" while you work, and every step "done" when you finish. Skip it for quick answers and questions.`
14
15// Set at session.start when Claude Code runs inside a cmux terminal; outside cmux
16// the mod registers nothing and adds nothing to the prompt.
17let cmux: { bin: string; workspace: string } | undefined
18
19/** Writes the list into the cmux workspace: the full list as its description, the summary as its progress. */
20async function push($: EngineInterface, list: readonly Step[]) {
21  if (!cmux) return
22  const { bin, workspace } = cmux
23  const run = (args: string[]) =>
24    $.process.run([bin, ...args, '--workspace', workspace], { timeoutMs: 5_000 }).catch(() => undefined)
25
26  if (list.length === 0) {
27    await Promise.all([run(['clear-progress']), run(['workspace-action', '--action', 'clear-description'])])
28    return
29  }
30  const { value, label } = summarize(list)
31  await Promise.all([
32    run(['set-progress', value.toFixed(3), '--label', label]),
33    run(['workspace-action', '--action', 'set-description', '--description', describe(list)]),
34  ])
35}
36
37async function setSteps($: EngineInterface, list: Step[]) {
38  await update($, steps, () => list)
39  await push($, list)
40}
41
42export const register: Register = on => {
43  on('session.start', async ($, e, next) => {
44    const result = await next(e)
45    const workspace = await $.env.get('CMUX_WORKSPACE_ID')
46    if (!workspace) return result
47    cmux = { bin: (await $.env.get('CMUX_BUNDLED_CLI_PATH')) ?? 'cmux', workspace }
48    await $.tool.register({
49      name: 'progress',
50      description:
51        'Shows your step list for the current task in the person\'s cmux sidebar. Send the whole list every time; an empty list clears it.',
52      inputSchema: {
53        type: 'object',
54        required: ['steps'],
55        properties: {
56          steps: {
57            type: 'array',
58            items: {
59              type: 'object',
60              required: ['title', 'status'],
61              properties: {
62                title: { type: 'string', description: 'At most 6 words, in the person\'s language' },
63                status: { enum: ['done', 'active', 'todo'] },
64              },
65            },
66          },
67        },
68      },
69    })
70    // After a reload, put back what the sidebar showed.
71    const current = await read($, steps)
72    if (current.length > 0) await push($, current)
73    return result
74  })
75
76  on('prompt.compose', async ($, e, next) => {
77    const result = await next(e)
78    if (!cmux) return result
79    return { sections: [...result.sections, { id: 'task-progress:rules', text: RULES, scope: 'session' as const }] }
80  })
81
82  on('tool.call', { tool: TOOL }, async ($, e) => {
83    // A subagent's own plan would overwrite the main conversation's.
84    if (e.agentId) return { result: '只有主對話的進度會顯示在側欄,這次沒有更新。' }
85    const list = normalize((e as unknown as { steps?: unknown }).steps)
86    if (typeof list === 'string') return { result: `沒有更新:${list}` }
87    await setSteps($, list)
88    return { result: list.length === 0 ? '側欄的任務清單已清除。' : `側欄已更新:${summarize(list).label}` }
89  })
90
91  // A finished list stays up until the person starts the next request.
92  on('prompt.submit', async ($, e, next) => {
93    const current = await read($, steps)
94    if (current.length > 0 && summarize(current).isFinished) await setSteps($, [])
95    return next(e)
96  })
97
98  on('classic.SessionStart', async ($, e, next) => {
99    const result = await next(e)
100    if (e.source === 'clear') await setSteps($, [])
101    return result
102  })
103
104  // Leave nothing behind in the sidebar once the session is gone.
105  on('session.end', async ($, e, next) => {
106    await push($, [])
107    return next(e)
108  })
109}
110
hooks/format.ts 42 lines
1import type { Step } from '../types'
2
3const MAX_STEPS = 12
4const MAX_TITLE = 40
5
6/** The glyph each status starts its line with; the cmux sidebar reads them back. */
7export const GLYPH: Record<Step['status'], string> = { done: '✓', active: '▸', todo: '·' }
8
9const STATUSES = new Set<string>(['done', 'active', 'todo'])
10
11/** The model's input as a clean list, or why it can't be used. An empty list clears. */
12export function normalize(raw: unknown): Step[] | string {
13  if (!Array.isArray(raw)) return 'steps 必須是陣列'
14  const steps: Step[] = []
15  for (const item of raw.slice(0, MAX_STEPS)) {
16    const title = typeof item?.title === 'string' ? item.title.replace(/\s+/g, ' ').trim().slice(0, MAX_TITLE) : ''
17    const status = typeof item?.status === 'string' && STATUSES.has(item.status) ? (item.status as Step['status']) : null
18    if (!title || !status) return '每一步都要有 title 和 status(done、active 或 todo)'
19    steps.push({ title, status })
20  }
21  return steps
22}
23
24/** The list as the sidebar shows it: one line per step, its glyph first. */
25export const describe = (steps: readonly Step[]): string =>
26  steps.map(step => `${GLYPH[step.status]} ${step.title}`).join('\n')
27
28export type Summary = { value: number; label: string; isFinished: boolean }
29
30/** 2/5 · 寫測試 — what is done, and the step under way (or the next one). */
31export function summarize(steps: readonly Step[]): Summary {
32  const total = steps.length
33  const done = steps.filter(step => step.status === 'done').length
34  const current = steps.find(step => step.status === 'active') ?? steps.find(step => step.status === 'todo')
35  const isFinished = total > 0 && done === total
36  return {
37    value: total === 0 ? 0 : done / total,
38    label: `${done}/${total} · ${isFinished ? '完成' : current?.title ?? ''}`,
39    isFinished,
40  }
41}
42
types/index.d.ts 12 lines
1/** One step of the task list Claude reports. */
2export type Step = { title: string; status: 'done' | 'active' | 'todo' }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'task-progress': {
7      /** The main conversation's current list; empty when there is none. */
8      steps: Step[]
9    }
10  }
11}
12