A pane listing this session's subagents (Agent tool and workflow agents) with their status; select one to see its prompt, text, tool calls and results.

ushironoko's dotfiles management system.
# Initial setup
git clone https://github.com/ushironoko/dotfiles.git
cd dotfiles
./init.sh
# After setup, the dotfiles command is available:
dotfiles install # Install symlinks
dotfiles list # List managed files
dotfiles restore # Restore from backup
dotfiles doctor # Check environment
# Future updates with ghq (after initial setup):
ghq get ushironoko/dotfiles
cd $(ghq root)/github.com/ushironoko/dotfiles
dotfiles installCreates symbolic links from repository to system locations.
-d, --dryRun: Preview changes-f, --force: Force overwrite-s, --select: Interactive selection-v, --verbose: Detailed outputdotfiles listShows all managed dotfiles and their status.
-v, --verbose: Detailed informationdotfiles restoreRestores files from backup.
-b, --backup <timestamp>: Specific backup-l, --list: List available backups-v, --verbose: Detailed outputdotfiles doctorDiagnoses environment issues.
-c, --check <categories>: Check specific areas (environment,conflicts,ghq,config,mcp)-v, --verbose: Detailed diagnosticsIf you're having issues with the initial setup, run the diagnostic tool:
./init.sh --check
This will show:
Run dotfiles doctor to diagnose issues. Common fixes:
# If mise commands are not found after installation
# Make sure ~/.local/bin is in your PATH and restart shell
export PATH="$HOME/.local/bin:$PATH"
exec $SHELL
# Remove conflicting Bun installation
rm -rf ~/.bun
# Fix PATH permanently
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
exec $SHELL
# Migrate old repos to ghq
./scripts/migrate-to-ghq.sh --dry-run
./scripts/migrate-to-ghq.sh --symlink
Edit dotfiles.config.ts to manage your files:
The versioned Codex harness installs global instructions, native custom agents, lifecycle hooks, command restrictions, and the shared Claude skills alongside ~/.codex/config.toml. Each component is linked selectively so Codex runtime state and bundled skills remain intact. After hook changes, review and trust them with /hooks in Codex.
~/.codex/config.toml)~/.codex/config.toml is symlinked to codex/config.toml, but codex rewrites that file at runtime, filling it with machine-local state — [projects."<path>"] trust levels (absolute paths, including private/client repo names), [mcp_servers.*] tables wired to Codex.app paths, [marketplaces.*] sources, the notify helper path, per-repo [desktop...perPath] prefs, and [hooks.state] approval hashes. A git clean filter (codex-scrub, codex/scrub-config.awk) drops anything carrying a quoted absolute path, every [projects.*]/[mcp_servers.*] table, and the complete [hooks.state] tree at git add time. The path rule remains content-based so unknown machine state is scrubbed by default; URLs and relative paths are kept. The working tree keeps the live file untouched, so codex keeps functioning.
The filter driver lives in .git/config (never committed), so it must be registered once per clone:
bun run setup:git-filters # also run automatically by init.sh and run-all
init.sh runs it on initial setup and bun run run-all runs it before every pre-commit check, so under normal use no manual step is needed. If you commit codex/config.toml on a fresh clone without it, trust state would leak — the filter is set to required = true to fail loudly if the scrubber ever errors.
hooks/register.tsx 461 lines1import type { ElementTable, On } from "claude-code";
2
3const PANE_ID = "agents-view";
4const PANE_TITLE = "Agents";
5const COMMAND = "subagents";
6const TICK_MS = 500;
7const MAX_AGENTS = 100;
8const MAX_ENTRIES = 300;
9const MAX_TEXT_CHARS = 2000;
10const MAX_LINE_CHARS = 100;
11const SUMMARY_FIELDS = [
12 "file_path",
13 "notebook_path",
14 "command",
15 "pattern",
16 "url",
17 "query",
18 "description",
19 "prompt",
20] as const;
21
22type Entry =
23 | { kind: "prompt"; text: string }
24 | { kind: "text"; text: string }
25 | { kind: "tool"; name: string; summary: string }
26 | { kind: "result"; text: string; isError: boolean };
27
28interface Tracked {
29 id: string;
30 label: string;
31 type: string;
32 status: string;
33 isListed: boolean;
34 seq: number;
35 entries: readonly Entry[];
36}
37
38interface Listed {
39 id: string;
40 description: string;
41 type: string;
42 status: string;
43}
44
45interface Host {
46 list: () => Promise<readonly Listed[]>;
47 open: () => Promise<unknown>;
48 redraw: () => void;
49}
50
51interface Row {
52 type: string;
53 role?: string;
54 isMeta?: true;
55 content: readonly { type: string; [field: string]: unknown }[];
56}
57
58type Kit = Pick<ElementTable<"terminal">, "Box" | "Text" | "Button">;
59
60const isDrawable = (char: string): boolean => {
61 const code = char.codePointAt(0) ?? 0;
62
63 return code === 9 || code === 10 || (code >= 32 && code !== 127);
64};
65
66const drawableOf = (text: string, limit: number): string => {
67 const cleaned = [...text.slice(0, limit * 2).replaceAll("\r\n", "\n")]
68 .filter(isDrawable)
69 .join("");
70
71 return cleaned.length > limit ? `${cleaned.slice(0, limit - 1)}…` : cleaned;
72};
73
74const lineOf = (text: string): string =>
75 drawableOf(text.replaceAll(/\s+/g, " ").trim(), MAX_LINE_CHARS);
76
77const summaryOf = (input: unknown): string => {
78 if (typeof input !== "object" || input === null) {
79 return "";
80 }
81
82 const record = input as Record<string, unknown>;
83 const found = SUMMARY_FIELDS.map((field) => record[field]).find(
84 (value): value is string => typeof value === "string" && value !== "",
85 );
86
87 return found === undefined ? "" : lineOf(found);
88};
89
90const resultTextOf = (content: unknown): string => {
91 if (typeof content === "string") {
92 return content;
93 }
94
95 if (!Array.isArray(content)) {
96 return "";
97 }
98
99 return content
100 .map((part: unknown) =>
101 typeof part === "object" &&
102 part !== null &&
103 (part as Record<string, unknown>).type === "text"
104 ? String((part as Record<string, unknown>).text ?? "")
105 : "",
106 )
107 .join("\n");
108};
109
110const resultEntryOf = (content: unknown, isError: boolean): Entry => {
111 const text = resultTextOf(content);
112 const lines = text === "" ? 0 : text.split("\n").length;
113 const head = lineOf(
114 text.split("\n").find((line) => line.trim() !== "") ?? "",
115 );
116
117 return {
118 kind: "result",
119 isError,
120 text: lines > 1 ? `${head} (+${lines - 1} lines)` : head,
121 };
122};
123
124const entriesOf = (row: Row, isOpening: boolean): Entry[] =>
125 row.content.flatMap((block): Entry[] => {
126 if (block.type === "text" && typeof block.text === "string") {
127 const text = drawableOf(block.text, MAX_TEXT_CHARS);
128
129 if (text.trim() === "") {
130 return [];
131 }
132
133 return [
134 row.role === "user" && isOpening
135 ? { kind: "prompt", text }
136 : { kind: "text", text },
137 ];
138 }
139
140 if (block.type === "tool_use") {
141 return [
142 {
143 kind: "tool",
144 name: typeof block.name === "string" ? block.name : "tool",
145 summary: summaryOf(block.input),
146 },
147 ];
148 }
149
150 if (block.type === "tool_result") {
151 return [resultEntryOf(block.content, block.is_error === true)];
152 }
153
154 return [];
155 });
156
157const statusOfReason = (reason: string): string => {
158 switch (reason) {
159 case "answer": {
160 return "completed";
161 }
162 case "aborted": {
163 return "killed";
164 }
165 default: {
166 return "failed";
167 }
168 }
169};
170
171const glyphOf = (status: string): string => {
172 switch (status) {
173 case "running": {
174 return "●";
175 }
176 case "completed": {
177 return "✓";
178 }
179 case "failed": {
180 return "✗";
181 }
182 case "killed": {
183 return "■";
184 }
185 default: {
186 return "○";
187 }
188 }
189};
190
191const orderOf = (agents: ReadonlyMap<string, Tracked>): Tracked[] =>
192 [...agents.values()].toSorted((a, b) => {
193 const running =
194 Number(b.status === "running") - Number(a.status === "running");
195
196 return running === 0 ? b.seq - a.seq : running;
197 });
198
199const prunedOf = (
200 agents: ReadonlyMap<string, Tracked>,
201): ReadonlyMap<string, Tracked> =>
202 agents.size <= MAX_AGENTS
203 ? agents
204 : new Map(
205 orderOf(agents)
206 .slice(0, MAX_AGENTS)
207 .map((one) => [one.id, one]),
208 );
209
210const signatureOf = (agents: ReadonlyMap<string, Tracked>): string =>
211 [...agents.values()]
212 .map((one) => `${one.id}:${one.status}:${one.entries.length}:${one.label}`)
213 .join("|");
214
215const entryView = ({ Text }: Kit, entry: Entry) => {
216 switch (entry.kind) {
217 case "prompt": {
218 return <Text color="cyan">▶ {entry.text}</Text>;
219 }
220 case "text": {
221 return <Text>{entry.text}</Text>;
222 }
223 case "tool": {
224 return (
225 <Text dimColor wrap="truncate-end">
226 → {entry.name} {entry.summary}
227 </Text>
228 );
229 }
230 case "result": {
231 return entry.isError ? (
232 <Text color="red" wrap="truncate-end">
233 {" "}← {entry.text}
234 </Text>
235 ) : (
236 <Text dimColor wrap="truncate-end">
237 {" "}← {entry.text}
238 </Text>
239 );
240 }
241 }
242};
243
244const detailView = (kit: Kit, agent: Tracked | undefined) => {
245 const { Box, Text } = kit;
246
247 if (agent === undefined) {
248 return <Text dimColor>Select an agent above.</Text>;
249 }
250
251 return (
252 <Box flexDirection="column">
253 <Text bold wrap="truncate-end">
254 {agent.label}
255 </Text>
256 <Text dimColor>
257 {agent.type} · {agent.status} · {agent.id}
258 </Text>
259 {agent.entries.length === 0 ? (
260 <Text dimColor>No output recorded yet.</Text>
261 ) : (
262 <Box flexDirection="column">
263 {agent.entries.map((entry) => entryView(kit, entry))}
264 </Box>
265 )}
266 </Box>
267 );
268};
269
270export const register = (on: On) => {
271 let agents: ReadonlyMap<string, Tracked> = new Map();
272 let selectedId: string | null = null;
273 let host: Host | null = null;
274 let isOpen = false;
275 let hasAutoOpened = false;
276 let isDirty = false;
277 let drawnSignature = "";
278 let seq = 0;
279
280 const upsert = (id: string, change: (current: Tracked) => Tracked) => {
281 const current = agents.get(id) ?? {
282 id,
283 label: id,
284 type: "workflow",
285 status: "running",
286 isListed: false,
287 seq: (seq += 1),
288 entries: [],
289 };
290 agents = prunedOf(new Map([...agents, [id, change(current)]]));
291 isDirty = true;
292 };
293
294 const openPane = async (current: Host) => {
295 isOpen = true;
296 await current.open();
297 current.redraw();
298 };
299
300 const tick = async (current: Host) => {
301 const listed = await current.list().catch(() => []);
302
303 listed.forEach((info) =>
304 upsert(info.id, (one) => ({
305 ...one,
306 label: lineOf(info.description) || one.label,
307 type: info.type,
308 status: info.status,
309 isListed: true,
310 })),
311 );
312
313 if (!hasAutoOpened && agents.size > 0) {
314 hasAutoOpened = true;
315
316 if (!isOpen) {
317 await openPane(current);
318 }
319 }
320
321 const signature = signatureOf(agents);
322
323 if (isDirty && signature !== drawnSignature) {
324 drawnSignature = signature;
325 current.redraw();
326 }
327
328 isDirty = false;
329 };
330
331 on("session.start", async ($, e, next) => {
332 const current: Host = {
333 list: () => $.agent.list(),
334 open: () => $.ui.open({ id: PANE_ID, title: PANE_TITLE }),
335 redraw: () => $.ui.invalidate("ui.render"),
336 };
337 host = current;
338
339 await $.command.register({
340 name: COMMAND,
341 description: "Toggle the pane listing this session's subagents",
342 });
343 const loop = () =>
344 $.clock.after(
345 TICK_MS,
346 () =>
347 void tick(current)
348 .catch(() => undefined)
349 .finally(loop),
350 );
351 loop();
352
353 return next(e);
354 });
355
356 on("command.run", { command: COMMAND }, async ($) => {
357 if (host === null) {
358 return { text: "agents-view is not ready yet." };
359 }
360
361 if (isOpen) {
362 isOpen = false;
363 await $.ui.close({ id: PANE_ID });
364
365 return { text: "Agents pane hidden." };
366 }
367
368 hasAutoOpened = true;
369 await openPane(host);
370
371 return { text: "Agents pane shown." };
372 });
373
374 on("session.append", (_, e, next) => {
375 const { agentId, message } = e;
376
377 if (
378 agentId !== undefined &&
379 (message.type === "user" || message.type === "assistant") &&
380 message.isMeta !== true
381 ) {
382 upsert(agentId, (one) => {
383 const added = entriesOf(message, one.entries.length === 0);
384 const opening = added.find((entry) => entry.kind === "prompt");
385
386 return {
387 ...one,
388 label:
389 !one.isListed && one.label === one.id && opening !== undefined
390 ? lineOf(opening.text)
391 : one.label,
392 entries: [...one.entries, ...added].slice(-MAX_ENTRIES),
393 };
394 });
395 }
396
397 return next(e);
398 });
399
400 on("turn.complete", (_, e, next) => {
401 const { agentId } = e;
402
403 if (agentId !== undefined && agents.has(agentId)) {
404 upsert(agentId, (one) =>
405 one.isListed ? one : { ...one, status: statusOfReason(e.reason) },
406 );
407 }
408
409 return next(e);
410 });
411
412 on("ui.render", { component: "Pane" }, async ($, e, next) => {
413 if (e.requestId !== PANE_ID || e.surface !== "terminal") {
414 return next(e);
415 }
416
417 const { Box, Text, Button } = $.ui.resolve(e);
418 const kit: Kit = { Box, Text, Button };
419 const ordered = orderOf(agents);
420 const selected =
421 agents.get(selectedId ?? "") ??
422 ordered.find((one) => one.status === "running") ??
423 ordered[0];
424 const running = ordered.filter((one) => one.status === "running").length;
425
426 return (
427 <Box flexDirection="column" paddingRight={1}>
428 <Text bold>
429 Agents {running} running / {ordered.length} total
430 </Text>
431 {ordered.length === 0 ? (
432 <Text dimColor>No subagents in this session yet.</Text>
433 ) : (
434 <Box flexDirection="column">
435 {ordered.map((one) => (
436 <Button
437 key={`agent:${one.id}`}
438 plain
439 dimColor={one.id !== selected?.id}
440 label={`${one.id === selected?.id ? "❯" : " "} ${glyphOf(one.status)} [${one.type}] ${one.label}`}
441 onPress={() => {
442 selectedId = one.id;
443 host?.redraw();
444 }}
445 />
446 ))}
447 </Box>
448 )}
449 <Text dimColor>{"─".repeat(Math.max(1, e.props.bodyColumns - 1))}</Text>
450 {detailView(kit, selected)}
451 </Box>
452 );
453 });
454
455 on("ui.close", { id: PANE_ID }, (_, e, next) => {
456 isOpen = false;
457
458 return next(e);
459 });
460};
461