SLOPSHOPPER

working-memory

把 context window 畫成一顆腦:工具呼叫點亮對應腦區,用量越滿越擁擠,compact 時睡眠整理

newpaneguardcommandprompttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · working-memory
│ ┃ 工作記憶 · Working Memory ✕ › fix the failing auth test and add an audit log call │ ┃ 工作記憶 ██████████░░░░░░░░░░ 49% 97k/200k │ ┃ 狀態:清醒、專注 ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ █·███ █··███ ⏺ 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 │ ┃ ███·█████ ░░·░····░██████ │ ┃ ███· ·░·░····░░·░░···░·░░·████ › /brain │ ┃ ░█░░··░·░░░░░·░░··░····░░░·█░··░░░ ⎿ working-memory: 工作記憶腦圖已打開。 │ ┃ ·░·░░░·░·▒▒▒░░···░░░░░··· │ ┃ ▒▒▒░░░░░··░····· │ ┃ ▒▒▒▒····░░░░·░· │ ┃ ▒▒▒▒ ·░· │ ┃ │ ┃ ● 額葉·規劃 ● 運動皮質·動手改 ● 頂葉·操作 │ ┃ ● 枕葉·閱讀 ○ 顳葉·聽外界 ○ 小腦·分身協作 │ ┃ │ ┃ 最近的神經活動 │ ┃ 運動皮質 Write audit.ts │ ┃ 運動皮質 Write cache.ts ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 工作記憶 · Working Memory
工作記憶 ██████████░░░░░░░░░░ 49% 97k/200k 狀態:清醒、專注 █·███ █··███ █████████ ████████░████ ██████████████· ███████████████████ ░███░███·██·████·█ ·███████████·████·██ ██████████████████ ██████·███████·█████ ██████░███████░███· ████░████████████·███ █████████████░██·████ ██████░█·██░██░██████ █░█·███·░████·██·████ ██░ ·░░░░██·██████ ███·█████ ░░·░····░██████ ███· ·░·░····░░·░░···░·░░·████ ░█░░··░·░░░░░·░░··░····░░░·█░··░░░ ·░·░░░·░·▒▒▒░░···░░░░░··· ▒▒▒░░░░░··░····· ▒▒▒▒····░░░░·░· ▒▒▒▒ ·░· ● 額葉·規劃 ● 運動皮質·動手改 ● 頂葉·操作環境 ● 枕葉·閱讀 ○ 顳葉·聽外界 ○ 小腦·分身協作 最近的神經活動 運動皮質 Write audit.ts 運動皮質 Write cache.ts 頂葉   Bash bun test 頂葉   Bash git status --porcelain 頂葉   Bash rm -rf build && git push --force or… ▸ 頂葉   Bash cat .env
README

learning-hacker-claude-mod

Learning Hacker 的 Claude Code mods 合集。主題有兩個:把 agent 的運作畫成看得懂的東西,以及讓「同時開好幾個 Claude Code session 分工」這件事變順。

狀態:照現狀提供(as-is),作為示範與實用工具分享,不提供支援。

收錄的 mod

mod做什麼版本
dispatch-board多個 session 分工時的派工看板:讀你寫的 mission 檔,顯示誰完成、誰在跑、誰在等你,狀態轉換時跳 toast0.1.0
dispatch-relaydispatch-board 的搭檔:worker 把回報寫進結果檔後以一行結束,由 mod 在 turn 結束時轉送給指揮站0.1.0
handoff-runnerbg session 在 worktree 做完的分支,由你在面板按一顆按鈕快轉進 main,執行前檢查、執行後驗證0.1.0
working-memory把 context window 畫成一顆腦:工具呼叫點亮對應腦區,用量越滿越擁擠,compact 時睡眠整理0.1.0

每個 mod 都可以單獨安裝,不用全裝。

安裝

需要 Claude Code v2.1.287 以上(claude --version 確認)。

先加入 marketplace(一次就好):

/plugin marketplace add Hangghost/learning-hacker-claude-mod

再挑想要的裝:

/plugin install dispatch-board@learning-hacker-claude-mod
/plugin install dispatch-relay@learning-hacker-claude-mod
/plugin install handoff-runner@learning-hacker-claude-mod
/plugin install working-memory@learning-hacker-claude-mod

dispatch-relay 需要 v2.1.289 以上。

在 shell 裡用 claude plugin marketplace add …、claude plugin install … 也可以。已開著的 session 執行 /reload-plugins 載入。

更新:第三方 marketplace 的自動更新預設關閉。可在 /plugin 的 Marketplaces 分頁對本 marketplace 選 Enable auto-update,或手動執行 /plugin marketplace update learning-hacker-claude-mod。

dispatch-board:多 session 派工看板

同時開幾個 session 各做一段工作時,你很難一眼看出誰做完了、誰卡住在等你。dispatch-board 讀你寫的 mission 檔,在提示框上方放一行摘要,/dispatch 打開完整面板。

  • mission 檔是一個 JSON:列出節點、負責的 session 名稱、完成條件(一條 shell 指令,結束碼 0 代表完成)
  • 面板分成「需要你/跑著/做完待收斂」,完成條件跑不起來時顯示「無法判定」,不會被當成還在跑
  • session 狀態取自 claude agents --json
  • /dispatch demo 用假資料試看畫面

安全邊界:完成條件是 shell 指令,mod 會執行它,所以 mission 檔只從使用者層級的目錄讀(預設 ~/.claude/missions/),不讀任何專案目錄或 repo 內的檔案,也不接受專案設定改掉這個目錄。clone 一個陌生 repo 不會讓 mod 執行對方寫的指令。

mission 檔格式與範例見 plugins/dispatch-board/README.md。

dispatch-relay:worker 的回報自動送到指揮站

分工時 worker 做完要自己 SendMessage 把回報全文送給指揮站,同一份回報等於生成兩次,送完還要多跑一次收尾。dispatch-relay 裝在 worker 那一側:worker 把回報寫進結果檔、以一行結束 turn,mod 在 turn 結束時把新增的內容和最終回覆原文送給指揮站。

  • 在 dispatch-board 的 mission 檔加上 station(指揮站的 session 名稱)才啟用;沒寫的 mission 不受影響
  • 身份判定:本 session 的 id → claude agents --json 查出名稱 → mission 檔裡恰好一個節點指派給這個名稱才啟用
  • 只送有變更的內容(追加時只送追加段);送不到時請 worker 自己 SendMessage,同一份內容只請一次

前提:指揮站與 worker 都要用 --name 開,名稱與 claude agents 一致;新目錄第一次 claude --bg 前先在該目錄跑一次 claude 接受信任(否則回 Workspace not trusted…);用 --worktree <名> 開的 worker,result_file 相對的是 <repo>/.claude/worktrees/<名>。自訂 mission 目錄只要在 dispatch-board 設一次,relay 會沿用。

安全邊界:mod 會把檔案內容送到另一個 session。它只送對到的節點的結果檔(必須在 worker 的專案根目錄底下,解析符號連結後也一樣)與最終回覆,只送給名冊上恰好一筆對到 station 的 session;mission 檔的讀取規則與 dispatch-board 相同。tool.check 只放行它自己發出的送件。

細節見 plugins/dispatch-relay/README.md。

handoff-runner:一鍵把 worktree 分支落地

背景 session 在自己的 worktree 做完一條分支後,要併進 main,但 main 正被你的主 checkout 持有,背景 session 動不了它。handoff-runner 讓 session 簽發一張「交接單」,你在 /handoff 面板看過步驟後按「執行」,由 mod 完成 fast-forward。

  • Claude 可以呼叫工具 handoff_issue 簽發交接單,也可以用 /handoff issue <branch> [target] 手動簽發
  • 交接單放在 <git common dir>/handoffs/,在 .git 裡面,不會被 commit
  • 執行前檢查:分支沒有前進、目標可以 fast-forward、持有目標分支的 worktree 在落地路徑上沒有未提交的修改
  • 執行後驗證:目標分支確實前進到該 commit
  • 預設不 push,可在設定開啟

安全邊界:交接單只帶參數(分支、目標、釘住的 commit),不帶任何指令。要執行的 git 步驟由 mod 依參數產生,執行前比對「畫面上顯示的步驟」與「實際要跑的步驟」完全一致,並只允許 fast-forward、帶預期舊值的 ref 更新、非強制的 push。偽造的交接單最多只能讓自己從面板消失,不能讓 mod 執行別的指令。

細節見 plugins/handoff-runner/README.md。

working-memory:把 context 畫成一顆腦

/brain 在側邊開一個 pane,畫出一顆側面的腦(左邊是前額)。

腦區比喻什麼時候亮
額葉規劃送出 prompt、TaskCreate/TodoWrite、Skill
運動皮質動手改Edit、Write
頂葉操作環境Bash 與其他工具
枕葉閱讀Read、Grep、Glob
顳葉聽外界WebFetch、WebSearch、MCP 工具
小腦分身協作Agent、SendMessage
  • context 用量越高,腦裡被 ░ 填滿的格子越多;超過 85% 會紅色閃爍
  • compact 時,一道 ✦ 光波從後腦掃到前額,代表「睡眠整理記憶」

腦區對應是比喻,不是神經科學主張。

安裝前看看它會做什麼

mod 不在沙盒裡執行,會以你的權限跑在 Claude Code 內。建議安裝前先讀原始碼,或 clone 後執行 claude plugin validate ./plugins/<名稱>,看它處理哪些事件、呼叫哪些能力。以下是 v0.1.0 的結果:

dispatch-board

hooks: session.start, command.run{command=dispatch}, ui.close, ui.render{component=AbovePrompt},
       ui.render{component=Pane, requestId=dispatch-board}
calls: $.clock.every, $.clock.now, $.command.register, $.env.get, $.fs.exists, $.fs.list, $.fs.read,
       $.fs.stat, $.process.run, $.session.cwd, $.session.root, $.settings.read, $.state.get,
       $.state.set, $.ui.close, $.ui.copy, $.ui.open, $.ui.resolve, $.ui.toast

$.process.run 只用來跑你 mission 檔裡的完成條件(/bin/sh -c,每條 5 秒逾時)和 claude agents --json;檔案只讀不寫;不連網、不呼叫模型、不送 prompt。

dispatch-relay

hooks: session.start, prompt.submit, prompt.compose, turn.start, tool.call{tool=SendMessage},
       session.send, tool.check{tool=SendMessage}, turn.complete
calls: $.env.get, $.fs.exists, $.fs.list, $.fs.read, $.fs.stat, $.process.run, $.prompt.submit,
       $.session.cwd, $.session.id, $.session.root, $.session.send, $.settings.read,
       $.store.delete, $.store.get, $.store.set, $.ui.log

$.process.run 只跑 claude agents --json;$.session.send 只送給 mission 檔 station 對到的那一個 session;$.prompt.submit 只在送不到時請 worker 自己送;檔案只讀不寫;不連網、不呼叫模型。

handoff-runner

hooks: session.start, tool.call{tool=mcp__handoff-runner__handoff_issue}, command.run{command=handoff},
       ui.close, ui.render{component=AbovePrompt}, ui.render{component=Pane, requestId=handoff-runner}
calls: $.clock.every, $.command.register, $.fs.list, $.fs.read, $.fs.write, $.process.run,
       $.session.cwd, $.state.get, $.state.set, $.tool.register, $.ui.close, $.ui.open,
       $.ui.resolve, $.ui.toast

檔案只讀寫 <git common dir>/handoffs/ 底下;$.process.run 只跑唯讀的 git 檢查、交接單的鎖,以及你按下「執行」後的落地步驟;只有開啟 push 時才連網;不呼叫模型、不送 prompt。

working-memory

hooks: session.start, command.run{command=brain}, prompt.submit, tool.call,
       session.measure, session.compact, ui.render{component=Pane, requestId=working-memory}
calls: $.clock.every, $.command.register, $.session.usage, $.state.get, $.state.set,
       $.ui.open, $.ui.resolve

只讀工具呼叫的名稱與參數摘要來畫圖,不讀寫檔案、不啟動程式、不連網、不呼叫模型。

開發

claude --plugin-dir ./plugins/<名稱>   # 單次載入
claude plugin test ./plugins/<名稱>     # 跑測試

handoff-runner 另有兩支需要 Node 23.6 以上的測試:node plugins/handoff-runner/tests/real-git.mjs(在暫存 repo 上用真的 git 落地)與 node plugins/handoff-runner/tests/boundary.mjs(檢查執行入口只有一個)。

各 plugin 的 tsconfig.json 依賴 .claude-plugin/types/,那是 Claude Code 產生的型別檔,不放進 repo。

English

A collection of Claude Code mods by Learning Hacker. Shared as-is, without support. Each mod installs on its own.

/plugin marketplace add Hangghost/learning-hacker-claude-mod
/plugin install dispatch-board@learning-hacker-claude-mod
/plugin install dispatch-relay@learning-hacker-claude-mod
/plugin install handoff-runner@learning-hacker-claude-mod
/plugin install working-memory@learning-hacker-claude-mod

Requires Claude Code v2.1.287+ (dispatch-relay: v2.1.289+).

  • dispatch-board: a board for running several Claude Code sessions in parallel. Reads mission files you write (nodes, the session responsible, a shell command that exits 0 when done) and shows who is done, who is running and who is waiting on you. Mission files are read only from a user-level directory (default ~/.claude/missions/), never from a project, so cloning a repo cannot make it run someone else's commands.
  • dispatch-relay: the worker-side companion to dispatch-board. A worker writes its report to a result file and ends the turn with one line; the mod forwards what changed, plus the final reply, to the commanding session named by the mission's station field (opt-in per mission). It sends only the matched node's result file, kept inside the worker's project root, and only to the one roster session matching station.
  • handoff-runner: lets a background session that finished a branch in its own worktree issue a handoff ticket, which you land into main with one button. Tickets carry only parameters (branch, target, pinned commit); the git steps are derived by the mod, checked before running and verified after. Push is off by default.
  • working-memory: draws your context window as a brain. Tool calls light up regions, the brain fills up as context grows, and compaction plays a "sleep" sweep.

License

MIT

Source 2 files
hooks/register.tsx 279 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextUsage } from 'claude-code'
3
4import type { Brain, LogEntry, Region } from '../types'
5
6// ── 腦區與工具的對應(比喻,不是神經科學主張)────────────────────────
7const REGIONS: Region[] = ['frontal', 'motor', 'parietal', 'occipital', 'temporal', 'cerebellum']
8
9const INFO: Record<Region, { name: string; role: string; color: string }> = {
10  frontal: { name: '額葉', role: '規劃', color: 'magenta' },
11  motor: { name: '運動皮質', role: '動手改', color: 'yellow' },
12  parietal: { name: '頂葉', role: '操作環境', color: 'green' },
13  occipital: { name: '枕葉', role: '閱讀', color: 'cyan' },
14  temporal: { name: '顳葉', role: '聽外界', color: 'blue' },
15  cerebellum: { name: '小腦', role: '分身協作', color: 'white' },
16}
17
18const regionOfTool = (tool: string): Region => {
19  if (/^(Read|Grep|Glob|NotebookRead|LS)$/.test(tool)) return 'occipital'
20  if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(tool)) return 'motor'
21  if (/^(WebFetch|WebSearch)$/.test(tool) || tool.startsWith('mcp__')) return 'temporal'
22  if (/^(Agent|Task|SendMessage)$/.test(tool)) return 'cerebellum'
23  if (/^(TaskCreate|TaskUpdate|TodoWrite|Skill|EnterPlanMode|ExitPlanMode)$/.test(tool)) return 'frontal'
24  return 'parietal'
25}
26
27// ── 腦的形狀:側面圖,左邊是前額 ─────────────────────────────────────
28const W = 46
29const H = 15
30const TICK_MS = 100
31const DECAY = 0.86
32const SLEEP_TICKS = 28
33
34type Cell = { region: Region | 'stem'; u: number; seed: number }
35
36const hash = (n: number) => {
37  let x = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b)
38  x ^= x >>> 13
39  x = Math.imul(x, 0xc2b2ae35)
40  x ^= x >>> 16
41  return (x >>> 0) / 4294967296
42}
43
44const MAP: (Cell | null)[][] = Array.from({ length: H }, (_, y) =>
45  Array.from({ length: W }, (_, x) => {
46    const u = (x + 0.5) / W
47    const v = (y + 0.5) / H
48    const seed = y * W + x
49    // 大腦:外緣加一點起伏,像腦回
50    const dx = (u - 0.47) / 0.46
51    const dy = (v - 0.4) / 0.38
52    const wobble = 1 + 0.05 * Math.sin(Math.atan2(dy, dx) * 9)
53    const inCerebrum = dx * dx + dy * dy <= wobble * wobble
54    // 小腦與腦幹
55    const cx = (u - 0.77) / 0.15
56    const cy = (v - 0.8) / 0.17
57    const inCerebellum = cx * cx + cy * cy <= 1
58    const inStem = u > 0.57 && u < 0.65 && v > 0.7
59    if (inCerebrum) {
60      // 外側溝與中央溝留白,讓腦區分得出來
61      const sylvian = Math.abs(v - (0.62 - (u - 0.2) * 0.25)) < 0.035 && u > 0.2 && u < 0.62
62      const central = Math.abs(u - (0.42 + (v - 0.1) * 0.12)) < 0.012 && v < 0.55
63      if (sylvian || central) return null
64      let region: Region
65      if (v > 0.6 - (u - 0.2) * 0.25 && u > 0.2 && u < 0.74) region = 'temporal'
66      else if (u < 0.42 + (v - 0.1) * 0.12 - 0.09) region = 'frontal'
67      else if (u < 0.42 + (v - 0.1) * 0.12) region = 'motor'
68      else if (u > 0.76) region = 'occipital'
69      else region = 'parietal'
70      return { region, u, seed }
71    }
72    if (inCerebellum) return { region: 'cerebellum', u, seed }
73    if (inStem) return { region: 'stem', u, seed }
74    return null
75  }),
76)
77
78// ── 狀態 ─────────────────────────────────────────────────────────────
79const EMPTY: Brain = { tick: 0, act: REGIONS.map(() => 0), fill: 0, tokens: 0, window: 0, sleepAt: null, log: [] }
80const brain = atom({ plugin: 'working-memory', key: 'brain' } as const, EMPTY)
81
82const PANE = 'working-memory'
83
84async function fire($: EngineInterface, region: Region, entry: LogEntry | null) {
85  await update($, brain, b => ({
86    ...b,
87    act: b.act.map((a, i) => (REGIONS[i] === region ? 1 : a)),
88    log: entry ? [...b.log, entry].slice(-6) : b.log,
89  }))
90}
91
92async function measure($: EngineInterface, c: SessionContextUsage) {
93  if (c.percent === undefined || c.tokens === undefined) return
94  const { percent, tokens, window } = c
95  await update($, brain, b => ({ ...b, fill: percent / 100, tokens, window }))
96}
97
98let ticking = false
99
100// 每一格:活化衰減、推進睡眠動畫;沒有東西在動就不重畫
101async function step($: EngineInterface) {
102  if (ticking) return
103  ticking = true
104  try {
105    const b = await read($, brain)
106    const isMoving = b.act.some(a => a > 0.02) || b.sleepAt !== null || b.fill >= 0.85
107    if (!isMoving) return
108    await update($, brain, cur => {
109      const tick = cur.tick + 1
110      const isAwake = cur.sleepAt !== null && tick - cur.sleepAt > SLEEP_TICKS
111      return {
112        ...cur,
113        tick,
114        act: cur.act.map(a => (a * DECAY < 0.02 ? 0 : a * DECAY)),
115        sleepAt: isAwake ? null : cur.sleepAt,
116      }
117    })
118  } finally {
119    ticking = false
120  }
121}
122
123// ── 畫面 ─────────────────────────────────────────────────────────────
124type Run = { text: string; color?: string; bold?: boolean; dim?: boolean }
125
126const paintCell = (cell: Cell | null, b: Brain): Run => {
127  if (cell === null) return { text: ' ' }
128  if (cell.region === 'stem') return { text: '▒', dim: true }
129
130  // 睡眠:一道波從後腦掃到前額,掃過的地方清空
131  if (b.sleepAt !== null) {
132    const wave = 1 - (b.tick - b.sleepAt) / (SLEEP_TICKS * 0.8)
133    const d = cell.u - wave
134    if (Math.abs(d) < 0.04) return { text: '✦', color: 'white', bold: true }
135    if (d > 0) return { text: '·', color: 'blue', dim: true }
136  }
137
138  const color = INFO[cell.region].color
139  const act = b.act[REGIONS.indexOf(cell.region)] ?? 0
140  // 神經元放電:活化越強,同時閃的格子越多,每一格隨影格隨機
141  if (act > 0.02 && hash(cell.seed * 31 + b.tick) < act * 0.85) {
142    return { text: act > 0.55 ? '█' : '▓', color, bold: true }
143  }
144  // 記憶佔用:用量多少,就有多少比例的格子被填滿
145  if (hash(cell.seed) < b.fill) {
146    const isOverload = b.fill >= 0.85 && b.tick % 8 < 4
147    return { text: '░', color: isOverload ? 'red' : color }
148  }
149  return { text: '·', dim: true }
150}
151
152const runsOf = (cells: Run[]): Run[] =>
153  cells.reduce<Run[]>((acc, c) => {
154    const last = acc[acc.length - 1]
155    if (last && last.color === c.color && last.bold === c.bold && last.dim === c.dim) {
156      last.text += c.text
157    } else acc.push({ ...c })
158    return acc
159  }, [])
160
161const fmt = (n: number) =>
162  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
163
164const mood = (b: Brain) =>
165  b.sleepAt !== null
166    ? '睡眠中:整理記憶…'
167    : b.fill >= 0.85
168      ? '過載!快要需要睡一覺(compact)'
169      : b.fill >= 0.6
170        ? '有點擁擠'
171        : '清醒、專注'
172
173const detailOf = (e: Record<string, unknown>): string => {
174  const raw = e.file_path ?? e.command ?? e.pattern ?? e.url ?? e.query ?? e.description ?? e.skill ?? ''
175  const s = String(raw).replace(/\s+/g, ' ')
176  const short = s.includes('/') && !s.includes(' ') ? (s.split('/').pop() ?? s) : s
177  return short.length > 36 ? short.slice(0, 35) + '…' : short
178}
179
180export const register: Register = on => {
181  on('session.start', async ($, e, next) => {
182    await $.command.register({ name: 'brain', description: '打開工作記憶腦圖' })
183    const { context } = await $.session.usage()
184    await measure($, context)
185    $.clock.every(TICK_MS, () => void step($))
186    return next(e)
187  })
188
189  on('command.run', { command: 'brain' }, async $ => {
190    await $.ui.open({ id: PANE, title: '工作記憶 · Working Memory' })
191    return { text: '工作記憶腦圖已打開。' }
192  })
193
194  on('prompt.submit', async ($, e, next) => {
195    await fire($, 'frontal', { region: 'frontal', tool: '思考', detail: '收到新任務' })
196    return next(e)
197  })
198
199  on('tool.call', async ($, e, next) => {
200    const region = regionOfTool(e.tool)
201    await fire($, region, { region, tool: e.tool.replace(/^mcp__[^_]+__/, ''), detail: detailOf(e as Record<string, unknown>) })
202    return next(e)
203  })
204
205  on('session.measure', async ($, e, next) => {
206    if (e.changed.includes('context')) await measure($, e.context)
207    return next(e)
208  })
209
210  on('session.compact', async ($, e, next) => {
211    if (e.trigger !== 'precompute') {
212      await update($, brain, b => ({ ...b, sleepAt: b.tick }))
213    }
214    return next(e)
215  })
216
217  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
218    const { Box, Text } = $.ui.resolve(e)
219    const b = await read($, brain)
220    const pct = Math.round(b.fill * 100)
221    const barCells = 20
222    const filled = Math.round(b.fill * barCells)
223    const barColor = b.fill >= 0.85 ? 'red' : b.fill >= 0.6 ? 'yellow' : 'green'
224
225    return (
226      <Box flexDirection="column">
227        <Box>
228          <Text bold>工作記憶 </Text>
229          <Text color={barColor}>{'█'.repeat(filled)}</Text>
230          <Text dimColor>{'░'.repeat(barCells - filled)}</Text>
231          <Text bold color={barColor}> {pct}%</Text>
232          <Text dimColor> {b.window > 0 ? `${fmt(b.tokens)}/${fmt(b.window)}` : ''}</Text>
233        </Box>
234        <Text dimColor>狀態:{mood(b)}</Text>
235        <Text> </Text>
236        {MAP.map((row, y) => (
237          <Box key={`row${y}`}>
238            {runsOf(row.map(cell => paintCell(cell, b))).map((r, i) => (
239              <Text key={`r${y}-${i}`} color={r.color} bold={r.bold} dimColor={r.dim}>
240                {r.text}
241              </Text>
242            ))}
243          </Box>
244        ))}
245        <Text> </Text>
246        <Box flexDirection="column">
247          {[REGIONS.slice(0, 3), REGIONS.slice(3)].map((group, gi) => (
248            <Box key={`legend${gi}`}>
249              {group.map(r => {
250                const isLit = (b.act[REGIONS.indexOf(r)] ?? 0) > 0.3
251                return (
252                  <Text key={`lg-${r}`} color={INFO[r].color} bold={isLit} dimColor={!isLit}>
253                    {isLit ? '● ' : '○ '}
254                    {INFO[r].name}·{INFO[r].role}
255                    {'   '}
256                  </Text>
257                )
258              })}
259            </Box>
260          ))}
261        </Box>
262        <Text> </Text>
263        <Text dimColor>最近的神經活動</Text>
264        {b.log.length === 0 && <Text dimColor>(還沒有活動)</Text>}
265        {b.log.map((l, i) => (
266          <Box key={`log${i}`}>
267            <Text color={INFO[l.region].color} bold={i === b.log.length - 1}>
268              {i === b.log.length - 1 ? '▸ ' : '  '}
269              {INFO[l.region].name.padEnd(4, ' ')}
270            </Text>
271            <Text> {l.tool}</Text>
272            <Text dimColor> {l.detail}</Text>
273          </Box>
274        ))}
275      </Box>
276    )
277  })
278}
279
types/index.d.ts 24 lines
1export type Region = 'frontal' | 'motor' | 'parietal' | 'occipital' | 'temporal' | 'cerebellum'
2
3export type LogEntry = { region: Region; tool: string; detail: string }
4
5export type Brain = {
6  /** 動畫影格計數 */
7  tick: number
8  /** 各腦區活化程度 0–1,順序同 REGIONS */
9  act: number[]
10  /** context 用量 0–1 */
11  fill: number
12  tokens: number
13  window: number
14  /** compact 動畫開始的影格;null 表示沒在睡 */
15  sleepAt: number | null
16  log: LogEntry[]
17}
18
19declare module 'claude-code' {
20  interface PluginState {
21    'working-memory': { brain: Brain }
22  }
23}
24