SLOPSHOPPER

agents-view

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.

newpanecommandtimer
★ 12v0.1.0no licenseupdated 2026-10-07ushironoko/dotfiles/claude/.claude/skills/agents-view
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agents-view
│ ┃ Agents ✕ › fix the failing auth test and add an audit log call │ ┃ Agents 0 running / 0 total │ ┃ No subagents in this session yet. ⏺ Read(src/auth.ts) │ ┃ ─────────────────────────────────────────── ⎿ Read 6 lines │ ┃ ──────────── ⏺ Update(src/auth.ts) │ ┃ Select an agent above. ⎿ 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 │ │ › /subagents │ ⎿ agents-view: Agents pane shown. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agents
Agents 0 running / 0 total No subagents in this session yet. ─────────────────────────────────────────────────────── Select an agent above.
README

Dotfiles

ushironoko's dotfiles management system.

Setup

# 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

Commands

dotfiles install

Creates symbolic links from repository to system locations.

  • -d, --dryRun: Preview changes
  • -f, --force: Force overwrite
  • -s, --select: Interactive selection
  • -v, --verbose: Detailed output

dotfiles list

Shows all managed dotfiles and their status.

  • -v, --verbose: Detailed information

dotfiles restore

Restores files from backup.

  • -b, --backup <timestamp>: Specific backup
  • -l, --list: List available backups
  • -v, --verbose: Detailed output

dotfiles doctor

Diagnoses environment issues.

  • -c, --check <categories>: Check specific areas (environment,conflicts,ghq,config,mcp)
  • -v, --verbose: Detailed diagnostics

Troubleshooting

Quick Diagnostics

If you're having issues with the initial setup, run the diagnostic tool:

./init.sh --check

This will show:

  • Binary locations (mise, bun, node)
  • Command availability in PATH
  • mise installation status
  • Installed tools
  • Recommendations for fixing issues

Common Issues

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

Configuration

Edit dotfiles.config.ts to manage your files:

  • file: Single file symlink
  • directory: Entire directory symlink
  • selective: Specific files with permissions

Codex harness

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.

Live config (~/.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.

Source 1 files
hooks/register.tsx 461 lines
1import 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