My Dotfiles!

sh -c "$(curl -fsLS get.chezmoi.io)" -- -b $HOME/.local/bin init --source=~/ghq/github.com/0ta2/dots git@github.com:0ta2/dots.git
~/ghq/github.com/0ta2/dots/bootstrap.sh
Homebrew・mise を導入した後 mise run install (ドットファイル反映・不足ツール・skill・plugin の 導入・Codex 設定同期) を実行する。
mise run sync # ドットファイル反映 + Codex設定マージ (副作用なし、何度実行しても収束する)
mise run update # brew/mise/skill/uv tool の更新、最後に sync
直接 chezmoi コマンドを実行する場合は下記の通り:
~/.local/bin/chezmoi --source=~/ghq/github.com/0ta2/dots apply
コンピュータ名 を変更パスワードやパスキーを自動入力 をOFFドラッグにトラックパッドを使用 をONDock のサイズ小さく好みのサイズへ変更Dock の拡大を好みの大きさへ変更Dock を自動的に表示/非表示をOFF外観モードをダークに変更Spotlight検索を表示 のショートカットのチェックを外すCmd + スペース に変更Cmd + shift + スペース に変更CapsLock を Ctrl に変更するiwano.nvim / amanoukihashi.nvim をローカルのリポジトリから読み込むため、下記のシンボリックリンクを作成する:
mkdir -p ~/.local/share/nvim/site/pack/mine/opt
ln -s ~/ghq/github.com/0ta2/iwano.nvim ~/.local/share/nvim/site/pack/mine/opt/iwano.nvim-local
ln -s ~/ghq/github.com/0ta2/amanoukihashi.nvim ~/.local/share/nvim/site/pack/mine/opt/amanoukihashi.nvim-localhooks/register.tsx 215 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { agentStateOf, cards, type Board, type Main, type Status } from './board'
5
6const PANE = 'overview'
7const EMPTY: Board = { reply: [], working: [], review: [], done: [] }
8const board = atom({ plugin: 'overview', key: 'board' } as const, EMPTY)
9const isOpen = atom({ plugin: 'overview', key: 'isOpen' } as const, false)
10const POLL_MS = 30_000
11const FRESH_MS = 24 * 60 * 60 * 1000
12
13type Json = Record<string, unknown>
14type Pane = { pane: string; workspace: string; tabId: string }
15type Agent = { pane: string; tabId: string; name: string; status: string }
16
17const string = (value: unknown) => (typeof value === 'string' && value ? value : undefined)
18
19const result = (text: string): Json | undefined => {
20 try {
21 const value = JSON.parse(text) as { result?: unknown }
22 return value.result && typeof value.result === 'object' ? value.result as Json : undefined
23 } catch {
24 return undefined
25 }
26}
27
28const records = (text: string, key: string): Json[] => {
29 const value = result(text)?.[key]
30 return Array.isArray(value) ? value.filter((item): item is Json => !!item && typeof item === 'object') : []
31}
32
33const panesOf = (text: string): Pane[] =>
34 records(text, 'panes').flatMap(item => {
35 const pane = string(item.pane_id)
36 const workspace = string(item.workspace_id)
37 const tabId = string(item.tab_id)
38 return pane && workspace && tabId ? [{ pane, workspace, tabId }] : []
39 })
40
41const tabsOf = (text: string): { tabId: string; label: string }[] =>
42 records(text, 'tabs').flatMap(item => {
43 const tabId = string(item.tab_id)
44 return tabId ? [{ tabId, label: string(item.label) ?? string(item.name) ?? '' }] : []
45 })
46
47const agentsOf = (text: string): Agent[] =>
48 records(text, 'agents').flatMap(item => {
49 const pane = string(item.pane_id)
50 const tabId = string(item.tab_id)
51 return pane && tabId ? [{ pane, tabId, name: string(item.agent) ?? '', status: agentStateOf(string(item.agent_status) ?? string(item.status) ?? string(item.state)) }] : []
52 })
53
54const stateOf = (text: string): (Status & { isLead: boolean; updatedAt: number }) | undefined => {
55 try {
56 const value = JSON.parse(text) as Json
57 return value.isLead === true && typeof value.updatedAt === 'number' ? value as Status & { isLead: boolean; updatedAt: number } : undefined
58 } catch {
59 return undefined
60 }
61}
62
63const taskOf = (text: string): string | undefined => {
64 try {
65 const task = (JSON.parse(text) as Json).task
66 return typeof task === 'string' && task.trim() ? task.trim() : undefined
67 } catch {
68 return undefined
69 }
70}
71
72const herdr = async ($: EngineInterface, args: string[]) => {
73 const response = await $.process.run(['herdr', ...args], { timeoutMs: 10_000 }).catch(() => undefined)
74 return response?.exitCode === 0 && !response.isStdoutTruncated ? response.stdout : undefined
75}
76
77const key = (workspace: string, tabId: string) => `${workspace}\u0000${tabId}`
78
79async function mainsOf($: EngineInterface, home: string, panes: Pane[], labels: Map<string, string>, now: number): Promise<Main[]> {
80 const live = new Map(panes.map(pane => [pane.pane, pane]))
81 const root = `${home}/.local/state/imadoko`
82 const workspaces = (await $.fs.list(root).catch(() => [])).filter(entry => entry.kind === 'dir')
83 const candidates = await Promise.all(
84 workspaces.map(async workspace =>
85 Promise.all(
86 (await $.fs.list(`${root}/${workspace.name}`).catch(() => []))
87 .filter(entry => entry.kind === 'file' && entry.name.endsWith('.json'))
88 .map(async entry => ({ pane: entry.name.slice(0, -'.json'.length), state: stateOf(await $.fs.read(`${root}/${workspace.name}/${entry.name}`).catch(() => '')) })),
89 ),
90 ),
91 )
92 const newest = new Map<string, Status & { isLead: boolean; updatedAt: number }>()
93 for (const candidate of candidates.flat()) {
94 const previous = newest.get(candidate.pane)
95 if (candidate.state && live.has(candidate.pane) && now - candidate.state.updatedAt >= 0 && now - candidate.state.updatedAt <= FRESH_MS && (previous === undefined || previous.updatedAt < candidate.state.updatedAt)) {
96 newest.set(candidate.pane, candidate.state)
97 }
98 }
99 return [...newest].flatMap(([paneId, status]) => {
100 const pane = live.get(paneId)!
101 return [{ workspace: pane.workspace, pane: pane.pane, tabId: pane.tabId, mark: labels.get(key(pane.workspace, pane.tabId)) ?? '', purpose: typeof status.purpose === 'string' ? status.purpose : '', status }]
102 })
103}
104
105async function refresh($: EngineInterface) {
106 const now = await $.clock.now()
107 const [home, paneText, agentText] = await Promise.all([$.env.get('HOME'), herdr($, ['pane', 'list']), herdr($, ['agent', 'list'])])
108 if (!home || !paneText) return
109 const panes = panesOf(paneText)
110 const workspaces = [...new Set(panes.map(pane => pane.workspace))]
111 const tabTexts = await Promise.all(workspaces.map(workspace => herdr($, ['tab', 'list', '--workspace', workspace])))
112 const labels = new Map(tabTexts.flatMap((text, index) => text === undefined ? [] : tabsOf(text).map(tab => [key(workspaces[index]!, tab.tabId), tab.label] as const)))
113 const blocked = await Promise.all(
114 agentsOf(agentText ?? '')
115 .filter(agent => agent.status === 'blocked')
116 .flatMap(agent => {
117 const pane = panes.find(item => item.pane === agent.pane && item.tabId === agent.tabId)
118 return pane ? [{ agent, pane }] : []
119 })
120 .map(async ({ agent, pane }) => ({
121 workspace: pane.workspace,
122 tabId: agent.tabId,
123 name: agent.name,
124 label: labels.get(key(pane.workspace, agent.tabId)) ?? '',
125 task: taskOf(await $.fs.read(`${home}/.local/state/herdr-team/${pane.workspace}/${agent.tabId}.json`).catch(() => '')),
126 })),
127 )
128 const mains = await mainsOf($, home, panes, labels, now)
129 await update($, board, () => cards(mains, blocked, now))
130}
131
132let poll: { cancel: () => void } | undefined
133
134const startPolling = ($: EngineInterface) => {
135 poll?.cancel()
136 let busy = false
137 poll = $.clock.every(POLL_MS, () => {
138 if (busy) return
139 busy = true
140 void refresh($).finally(() => {
141 busy = false
142 })
143 })
144}
145
146const hostOf = (url: string | undefined) => url?.match(/^https?:\/\/([^/?#]+)/)?.[1]
147
148const elapsed = (ms: number | undefined) => {
149 if (ms === undefined) return '—'
150 if (ms < 60_000) return `${Math.floor(ms / 1000)}秒`
151 if (ms < 60 * 60_000) return `${Math.floor(ms / 60_000)}分`
152 return `${Math.floor(ms / (60 * 60_000))}時間`
153}
154
155export const register: Register = on => {
156 on('session.start', async ($, event, next) => {
157 await $.command.register({ name: 'overview', description: 'herdr の全 main をカンバンで表示する' })
158 if ((await $.env.get('HERDR_ENV')) === '1' && await read($, isOpen)) {
159 void refresh($).catch(() => undefined)
160 startPolling($)
161 }
162 return next(event)
163 })
164
165 on('command.run', { command: 'overview' }, async $ => {
166 if ((await $.env.get('HERDR_ENV')) === '1') {
167 await refresh($)
168 await update($, isOpen, () => true)
169 startPolling($)
170 }
171 await $.ui.open({ id: PANE, title: 'overview', columns: 200, closeOnEscape: true })
172 return { text: 'Overview pane opened.' }
173 })
174
175 on('ui.close', { id: PANE }, async ($, event, next) => {
176 await update($, isOpen, () => false)
177 poll?.cancel()
178 poll = undefined
179 return next(event)
180 })
181
182 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, event) => {
183 const { Box, Button, Text } = $.ui.resolve(event)
184 if ((await $.env.get('HERDR_ENV')) !== '1') return <Text>herdr の中で開いてください</Text>
185 const current = await read($, board)
186 const columns = [
187 ['reply', '要返信', 'warning', '#ffc8d8'],
188 ['working', '作業中', undefined, '#fff3a0'],
189 ['review', 'レビュー待ち', undefined, '#c8e4ff'],
190 ['done', '完了', undefined, '#cdeccd'],
191 ] as const
192 return (
193 <Box flexDirection="row" gap={2}>
194 {columns.map(([column, title, color, paper]) => (
195 <Box key={column} flexDirection="column" flexGrow={1} gap={1}>
196 <Text bold color={color}>{`${title} (${current[column].length})`}</Text>
197 {current[column].map((card, index) => (
198 <Box key={`box:${column}:${index}`} flexDirection="column" backgroundColor={paper} paddingX={1} paddingY={1}>
199 <Box flexDirection="row" justifyContent="space-between">
200 <Text bold color="black">{card.title}</Text>
201 <Box flexDirection="row" gap={1}>
202 {card.url ? <Button key={`url:${column}:${index}`} plain label="🔗" onPress={() => $.process.run(['open', card.url!], { timeoutMs: 10_000 }).catch(() => undefined)} /> : null}
203 <Button key={`card:${column}:${index}`} plain label="↗" onPress={() => herdr($, ['tab', 'focus', card.tabId])} />
204 </Box>
205 </Box>
206 <Text color="#555555">{[card.workspace, card.mark, elapsed(card.elapsedMs), hostOf(card.url)].filter(Boolean).join(' · ')}</Text>
207 </Box>
208 ))}
209 </Box>
210 ))}
211 </Box>
212 )
213 })
214}
215hooks/board.ts 78 lines1export type Task = { title?: unknown; state?: unknown; waitsFor?: unknown; url?: unknown }
2
3export type Status = {
4 purpose?: unknown
5 tasks?: Task[]
6 pending?: unknown[]
7 isWorking?: unknown
8 idleSince?: unknown
9 updatedAt?: unknown
10}
11
12export type Main = {
13 workspace: string
14 pane: string
15 tabId?: string
16 mark?: string
17 purpose?: string
18 status: Status
19}
20
21export type Blocked = { workspace: string; tabId: string; name: string; label: string; task?: string }
22
23export type Column = 'reply' | 'working' | 'review' | 'done'
24
25export type Card = { column: Column; title: string; workspace: string; mark: string; tabId: string; elapsedMs?: number; url?: string }
26
27export type Board = Record<Column, Card[]>
28
29const card = (column: Column, main: Main, title: string): Card | undefined =>
30 main.tabId === undefined ? undefined : { column, title, workspace: main.workspace, mark: main.mark ?? '', tabId: main.tabId }
31
32export const cards = (mains: Main[], blocked: Blocked[], now: number): Board => {
33 const reply = blocked.map(agent => ({ column: 'reply' as const, title: agent.task ?? agent.label, workspace: agent.workspace, mark: agent.label, tabId: agent.tabId }))
34 const pending: Card[] = []
35 const working: Card[] = []
36 const review: Card[] = []
37 const done: Card[] = []
38
39 for (const main of mains) {
40 const { status } = main
41 if (status.isWorking === false && Array.isArray(status.pending)) {
42 const elapsedMs = typeof status.idleSince === 'number' ? Math.max(0, now - status.idleSince) : undefined
43 for (const title of status.pending) {
44 if (typeof title !== 'string') continue
45 const next = card('reply', main, title)
46 if (next !== undefined) pending.push({ ...next, ...(elapsedMs !== undefined && { elapsedMs }) })
47 }
48 }
49
50 const tasks = status.tasks ?? []
51 for (const task of tasks) {
52 if (typeof task.title !== 'string') continue
53 if (task.state === 'doing') {
54 const next = card('working', main, task.title)
55 if (next !== undefined) working.push(next)
56 }
57 if (task.state === 'waiting' && task.waitsFor === 'others') {
58 const next = card('review', main, task.title)
59 if (next !== undefined) review.push(typeof task.url === 'string' && /^https?:\/\/\S+$/.test(task.url) ? { ...next, url: task.url } : next)
60 }
61 }
62 if (status.isWorking !== true && tasks.length > 0 && tasks.every(task => task.state === 'done') && Array.isArray(status.pending) && status.pending.length === 0) {
63 const next = card('done', main, main.purpose?.trim() || (main.mark ?? ''))
64 if (next !== undefined) done.push(next)
65 }
66 }
67
68 pending.sort((a, b) => (b.elapsedMs ?? -1) - (a.elapsedMs ?? -1))
69 return { reply: [...reply, ...pending], working, review, done }
70}
71
72const ALIASES: Readonly<Record<string, string>> = { busy: 'working', running: 'working', waiting: 'blocked', finished: 'done' }
73
74export const agentStateOf = (raw: string | undefined): string => {
75 const state = (raw ?? '').toLowerCase()
76 return Object.hasOwn(ALIASES, state) ? ALIASES[state]! : state
77}
78types/index.d.ts 10 lines1export type OverviewColumn = 'reply' | 'working' | 'review' | 'done'
2export type OverviewCard = { column: OverviewColumn; title: string; workspace: string; mark: string; tabId: string; elapsedMs?: number; url?: string }
3export type OverviewBoard = Record<OverviewColumn, OverviewCard[]>
4
5declare module 'claude-code' {
6 interface PluginState {
7 'overview': { board: OverviewBoard; isOpen: boolean }
8 }
9}
10