iT邦幫忙

0

用 Claude Code 的 mod 介面做側邊欄和 statusline:踩坑紀錄

  • 分享至 

  • xImage
  •  

我想在 Claude Code 的對話旁邊看額度、context、花費這些數字,就用 Claude Code 的 mod 介面(還在搶先體驗階段)做了一個側邊欄,它也能當 statusline。這篇先簡單說 mod 長什麼樣子,再記下做的過程中踩到的七個坑(在 Claude Code 2.1.300 上實測)。寫 mod 或 statusline 腳本的人也許用得到。

ccdrawu

先說 mod 長什麼樣子

一個 mod 是一個資料夾,裡面三個檔案:

my-mod/
├── .claude-plugin/plugin.json   # name、version、description
└── hooks/
    ├── hooks.json               # { "modules": ["./register.tsx"] }
    └── register.tsx

register.tsx 匯出一個 register,用 on(事件, hook) 掛在 Claude Code 的事件上。每個 hook 都是 ($, e, next):$ 是 Claude Code 給的介面,e 是事件的輸入,next(e) 交給下一層(其他外掛,最後是 Claude Code 自己)。下面這個 mod 在 session 開始時開一個面板,內容是一行 hello:

import type { Register } from 'claude-code'

export const register: Register = on => {
  on('session.start', async ($, e, next) => {
    void $.ui.open({ id: 'hello', title: 'Hello' })
    return next(e)
  })

  on('ui.render', { component: 'Pane', requestId: 'hello' }, async ($, e) => {
    const { Box, Text } = $.ui.resolve(e)
    return <Box><Text>hello</Text></Box>
  })
}

用 claude --plugin-dir my-mod 啟動就會載入。claude plugin validate my-mod 檢查 manifest 和用到的事件,claude plugin test my-mod 跑資料夾裡的 *.test.ts。

1. 查 API 靠型別檔

寫的時候主要靠型別檔查 API。用 --plugin-dir 載入之後,mod 資料夾裡會多出 .claude-plugin/types/,其中 claude-code/index.d.ts 有每個事件的輸入與結果、$ 上每個方法的說明和範例,一萬六千多行,拿事件名稱去 grep 就能找到:

grep -n "'session.compact'" my-mod/.claude-plugin/types/claude-code/index.d.ts

同一個資料夾還有一份 tsconfig.json:mod 自己的 tsconfig.json 寫 { "extends": "./.claude-plugin/types/tsconfig.json" },就能用 tsc -p . 做型別檢查。這個資料夾每次載入都會重寫,所以加進了 .gitignore。

2. 面板的位置不能自己指定

$.ui.open 只能指定大小(放在對話旁邊時的寬度、放在輸入框上方時的高度),ui.render 拿到的 placement 是唯讀的。實測到的規則:

  • fullscreen(/tui fullscreen)而且終端機夠寬,才放在對話旁邊。型別檔寫的是:自動開的從 144 欄開始,使用者開過的(例如從斜線指令開)從 110 欄開始。
  • 放在旁邊的面板一縮窄,會被移到輸入框上方。
  • 自動開的面板在窄的時候先不畫(isPlaced 是 false),等變寬才出現;它也不會被放到輸入框上方。
  • 輸入框上方的面板什麼都不畫,還是會留一個上下兩列的框。

所以「某個位置不要顯示」只能做成把面板關掉,沒辦法畫空的內容。

3. /reload-plugins 之後,模組裡的變數歸零了

reload 是重新載入模組:register 重跑,自己宣告的變數全部重來,但 $.state 裡的值還在。新版多加欄位的話,舊的 state 裡沒有那一欄,會讀到 undefined、算出 NaN,所以在 session 開始時補上預設值:

// pet 是用 atom() 宣告的 state,PET_START 是它每個欄位的預設值
await update($, pet, old => ({ ...PET_START, ...old }))

另外,/reload-plugins 只會對改過的模組重跑 session.start,沒改過的模組要先改一下才會重跑。

4. compact 之後,context 停在舊的數字

在 classic.PostCompact(以及 session.compact 的 next 之後)讀 $.session.usage(),拿到的還是 compact 前的 context,大約一秒後才清空。之後要等下一次回覆結束才有事件可以再讀,所以畫面一直停在舊的數字。Claude Code 給 statusline 的 stdin,在 compact 當下 context_window.used_percentage 就已經是 null。

最後的做法是在 PostCompact 直接把 context 清掉,不重讀:

// usage 是存用量的 state;agent_id 有值時是 subagent 的 compact
on('classic.PostCompact', async ($, e, next) => {
  const result = await next(e)
  if (e.agent_id === undefined) {
    await update($, usage, u => (u === null ? u : { ...u, contextPercent: undefined, contextTokens: undefined }))
  }
  return result
})

5. $.ui.status 不適合當 statusline

mod 有 $.ui.status(text),看起來就是 statusline,實測卻不合用:它被畫成亮黃色、前面加上「⚠ 外掛名稱:」,跳脫字元變成 �,而且只有一行。

後來改用 Claude Code 原生的 statusLine,讓 mod 跟一支指令分工。~/.claude/settings.json 設成:

"statusLine": { "type": "command", "command": "ccdrawu statusline", "refreshInterval": 10 }

Claude Code 在對話有進展時、以及沒有動靜時每 refreshInterval 秒跑一次這個指令,stdin 給一份 JSON(session_id、model、context_window、cost…),stdout 的第一行就是 statusline。分工是這樣:

  • mod 每秒把算好的值(含顏色)寫進 ~/.ccdrawu/sessions/<session id>.json,內容沒變就不寫。
  • 指令只從 stdin 拿 session_id 找到檔案,負責排版。

過程中碰到的:

  • /clear 之後 session id 會換,所以 mod 每次寫檔前都重新問一次 session id。
  • 太長的話,Claude Code 會從中間截斷。它左右各留兩格,所以用 COLUMNS 減 4,先自己截掉結尾。
  • mod 的 $.fs 沒有刪檔的方法,舊的 session 檔由指令這邊清。

6. 想把 statusline 整列藏起來,卻留下一列空白

設了 statusLine 之後,指令印空字串、只印換行或空白,或 exit 1,都會留下一列空白。試到最後,只印一個 SGR reset(有沒有換行都一樣),那一列才會收起來:

printf '\033[0m'

這是實測的結果,Claude Code 改版後可能會變回空白的一列。

7. 從 GitHub 裝的 plugin 沒有自己更新

從 GitHub 或 git 加進來的 marketplace 沒有自己更新:~/.claude/plugins/known_marketplaces.json 的 lastUpdated,官方的是當天,第三方的停在幾週到幾個月前。更新用的是這兩步:

claude plugin marketplace update <marketplace>
claude plugin update <plugin>@<marketplace>

marketplace 的來源寫法試過三種:

  • https://github.com/<owner>/<repo>.git#v1.2.3:走 HTTPS,可以固定在某個 tag,外掛跟發布的版本一致。最後用這個。
  • <owner>/<repo>:裝到的是預設分支當下的內容,push 上去的改動使用者馬上就拿到,跟執行檔的版本對不上。
  • <owner>/<repo>#<ref>:會走 SSH clone,沒有 GitHub SSH key 的人裝不起來。

做出來的東西:ccdrawu

ccdrawu 的側邊欄能顯示 31 項:5 小時與每週額度的用量和重置倒數、context、模型、effort、權限模式、花費、prompt cache 命中率、git、正在跑的 subagent 和背景工作等等。要顯示哪些、怎麼排,在 ccdrawu 指令的 TUI 裡挑;它也可以當 statusline,或設成終端機寬的時候看側邊欄、窄的時候換成 statusline。最上面有一隻像素寵物,會跟著 Claude 的工作狀態走動、坐下、睡覺。不呼叫模型,不花 token。

ccdrawu settings

它分成兩部分:側邊欄是 TypeScript 寫的 mod;設定介面和 ccdrawu statusline 是同一個 Go 執行檔,TUI 用 Bubble Tea 和自己寫的 terminu-go。設定存成一個 JSON 檔,mod 每秒讀一次,開著的 session 一秒內就會跟上。

brew install vulcanshen/tap/ccdrawu
ccdrawu install claude

需要 Claude Code 2.1.287 以上和 Nerd Font;側邊欄要先執行過一次 /tui fullscreen,終端機也要夠寬。macOS、Linux、Windows 都有版本,Windows 在 Windows Terminal 上試過。

GitHub(GPL-3.0):https://github.com/vulcanshen/ccdrawu


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言