Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
{
"name": "heatmap",
"description": "A GitHub-style grid of your Claude Code time in a /heatmap pane, with streaks, peaks and top projects.",
"version": "0.1.2",
"version": "0.1.3",
"source": "./mods/heatmap",
"category": "See what's happening",
"tags": [
Expand Down Expand Up @@ -118,7 +118,7 @@
{
"name": "mission-control",
"description": "/mission opens a live map of the main agent, its subagents and every tool call, plus a code map of the files they touch.",
"version": "0.1.4",
"version": "0.1.5",
"source": "./mods/mission-control",
"category": "See what's happening",
"tags": [
Expand Down Expand Up @@ -276,7 +276,7 @@
{
"name": "where-am-i",
"description": "A live recap above the prompt: goal, doing now, waiting on you, next. /where for a longer one.",
"version": "0.1.6",
"version": "0.1.7",
"source": "./mods/where-am-i",
"category": "See what's happening",
"tags": [
Expand Down
2 changes: 1 addition & 1 deletion mods/heatmap/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "heatmap",
"version": "0.1.2",
"version": "0.1.3",
"description": "A GitHub-style grid of your Claude Code time in a /heatmap pane, with streaks, peaks and top projects.",
"author": {
"name": "Hamza Zafar",
Expand Down
2 changes: 1 addition & 1 deletion mods/heatmap/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const register: Register = on => {
on('command.run', { command: 'heatmap' }, async $ => {
await update($, isLoading, () => rows === null)
await $.ui.open({ id: PANE, title: 'Heatmap', focus: true })
void refresh($).then(() => update($, isLoading, () => false)).catch(() => {}) // today stays current mid-session
void refresh($).finally(() => update($, isLoading, () => false)).catch(() => {}) // today stays current mid-session; never stuck on loading
return { text: 'Opened the heatmap' }
})

Expand Down
2 changes: 1 addition & 1 deletion mods/mission-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mission-control",
"version": "0.1.4",
"version": "0.1.5",
"description": "/mission opens a live map of the main agent, its subagents and every tool call, plus a code map of the files they touch.",
"author": {
"name": "Hamza Zafar",
Expand Down
19 changes: 12 additions & 7 deletions mods/mission-control/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// them, a glow on what it touches right now, and one line on each change
// A band line sums it up while Claude works.
import { atom, read, update } from 'claude-code'
import type { EngineInterface, Register } from 'claude-code'
import type { EngineInterface, Register, Timer } from 'claude-code'

import type { MapFile, MissionNode } from '../types'
import { importsOf, svg } from './map'
Expand Down Expand Up @@ -39,6 +39,7 @@ const now = atom({ plugin: 'mission-control', key: 'now' } as const, 0)
// Drawing bookkeeping; a reload starts it over.
const draw = { size: { columns: 100, rows: 30 }, pending: false, dirty: false, n: 0, error: '' }
const changes = new Map<string, string[]>() // path → this turn's edits, for the one-line "why"
const MAX_CHANGES = 5 // the first edit and the latest few per file are enough for the "why"

// Under the cap, the oldest tool calls go first; main and the agents always stay.
export function cap(list: MissionNode[]) {
Expand All @@ -60,10 +61,13 @@ function words(text: string, max: number) {
let bandOrder: Promise<string[]> | undefined // read once per session: restart to apply

export const register: Register = on => {
let timer: Timer | undefined // one clock, even if session.start fires again

on('session.start', async ($, e, next) => {
const r = await next(e)
await $.command.register({ name: 'mission', description: 'Open the live map of agents and tool calls; /mission code opens the code map', argumentHint: '[who|code]' }).catch(() => {})
$.clock.every(1000, () => {
timer?.cancel()
timer = $.clock.every(1000, () => {
void (async () => {
const busy = (await read($, nodes)).some(n => n.status === 'running')
if (busy) await update($, now, () => Date.now())
Expand Down Expand Up @@ -121,7 +125,8 @@ export const register: Register = on => {
}
if (path && !failed && (e.tool === 'Edit' || e.tool === 'Write')) {
const what = e.tool === 'Edit' ? `- ${String(args.old_string ?? '').slice(0, 300)}\n+ ${String(args.new_string ?? '').slice(0, 300)}` : `wrote ${String(args.content ?? '').slice(0, 400)}`
changes.set(path, [...(changes.get(path) ?? []), what])
const list = [...(changes.get(path) ?? []), what]
changes.set(path, list.length > MAX_CHANGES ? [list[0], ...list.slice(1 - MAX_CHANGES)] : list) // the first edit and the latest few
await touch($, path, e.tool === 'Edit' ? 'edit' : 'write', true)
}
return r
Expand Down Expand Up @@ -150,7 +155,7 @@ export const register: Register = on => {
const arg = e.args.trim().toLowerCase()
if (arg === 'code' || arg === 'who') await update($, view, () => arg)
await $.ui.open({ id: PANE, title: 'Mission Control', focus: true })
void renderSoon($)
void renderSoon($).catch(() => {})
const s = summary(await read($, nodes))
return { text: `Mission Control: ${s.agents} agents · ${s.tools} tool calls · ${(await read($, files)).length} files. w: who · c: code · q: close` }
})
Expand All @@ -164,7 +169,7 @@ export const register: Register = on => {
const head = (
<Box flexDirection="row" gap={1}>
<Button key="who" label="Who" hotkey="w" variant={v === 'who' ? 'primary' : undefined} onPress={() => update($, view, () => 'who')} />
<Button key="code" label="Code" hotkey="c" variant={v === 'code' ? 'primary' : undefined} onPress={() => void setCode($)} />
<Button key="code" label="Code" hotkey="c" variant={v === 'code' ? 'primary' : undefined} onPress={() => void setCode($).catch(() => {})} />
<Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
</Box>
)
Expand All @@ -184,7 +189,7 @@ export const register: Register = on => {
const imageRows = Math.min(rows, Math.round(columns * 0.3))
if (draw.size.columns !== columns || draw.size.rows !== imageRows) {
draw.size = { columns, rows: imageRows }
void renderSoon($)
void renderSoon($).catch(() => {})
}
const f = await read($, frame)
return (
Expand Down Expand Up @@ -335,7 +340,7 @@ async function renderMap($: EngineInterface) {
return
}
draw.error = ''
const tmp = ((await $.env.get('TMPDIR')) ?? '/tmp').replace(/\/$/, '')
const tmp = ((await $.env.get('TMPDIR').catch(() => undefined)) ?? '/tmp').replace(/\/$/, '')
const dir = `${tmp}/mission-control`
const n = ++draw.n
const width = Math.round(draw.size.columns * 9)
Expand Down
13 changes: 13 additions & 0 deletions mods/mission-control/tests/mission-control.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ describe('mission-control', () => {
expect(summary([]).tools).toBe(0)
})

test('a second session.start keeps one clock, not two', async ($, on) => {
const { clock } = engine(on)
let ticks = 0 // each tick with work running writes now once
on('state.set', (_$: any, e: any, next: any) => (e.key === 'now' && ticks++, next(e)))
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/repo' } as any)
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/repo' } as any)
await $.turn.start({ text: 'fix the login bug', turnId: 't1' } as any) // main is running
ticks = 0
await clock.advance(1000)
await new Promise(done => (globalThis as any).setTimeout(done, 10))
expect(ticks).toBe(1)
})

test('follows the band order file', async ($, on) => {
// another band mod drew its column beneath
const beneath = {
Expand Down
2 changes: 1 addition & 1 deletion mods/where-am-i/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "where-am-i",
"version": "0.1.6",
"version": "0.1.7",
"description": "A live recap above the prompt: goal, doing now, waiting on you, next. /where for a longer one.",
"author": {
"name": "Hamza Zafar",
Expand Down
17 changes: 16 additions & 1 deletion mods/where-am-i/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function resetBandOrder() {
export const register: Register = on => {
let prompt = ''
let log: string[] = []
let slash = false // the last message was a slash command, which may or may not start a turn

on('session.start', async ($, e, next) => {
const r = await next(e)
Expand All @@ -40,16 +41,30 @@ export const register: Register = on => {
})

on('prompt.submit', async ($, e, next) => {
// A slash command like /where answers nothing the last turn waited on, and may start no turn:
// leave the band as it is. One that does start a turn is picked up by turn.start.
if (e.text.startsWith('/')) {
slash = true
return next(e)
}
slash = false
prompt = e.text.slice(0, 1500)
log = []
await update($, live, () => 'reading your message')
if (!e.text.startsWith('/')) await clearWaiting($) // a slash command like /where answers nothing the last turn waited on
await clearWaiting($)
return next(e)
})

// A new turn of Claude's own: a summary of an older one is stale, and the person has answered.
on('turn.start', async ($, e, next) => {
if (!e.agentId) {
// A turn a slash command started: its text is the prompt. Other turns keep the person's
// own message, including ones Claude Code starts itself, like a background agent finishing.
if (slash && e.text) {
prompt = e.text.slice(0, 1500)
log = []
}
slash = false
await update($, latest, () => e.turnId ?? '')
await clearWaiting($)
}
Expand Down
30 changes: 28 additions & 2 deletions mods/where-am-i/tests/where-am-i.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const wait = () => new Promise(done => (globalThis as any).setTimeout(done, 10))

// Stands for the engine beneath the mod.
// `reply` is Haiku's text, or a handler that answers `model.complete` itself.
function engine(on: any, reply: string | (() => unknown), beneath?: unknown) {
function engine(on: any, reply: string | ((...args: any[]) => unknown), beneath?: unknown) {
on('session.start', (_$: any, e: any) => ({ sessionId: 's', cwd: e.cwd }))
on('command.register', () => ({ value: undefined }))
on('prompt.submit', () => ({ text: '' }))
Expand Down Expand Up @@ -91,10 +91,36 @@ describe('where-am-i', () => {
haiku.answer(OLD)
await wait()
const ui = await $.ui.mount({ plugin: 'where-am-i', surface: 'terminal', ...BAND } as any)
expect(await ui.find({ type: 'Text', text: /waiting on you: you to test it/ })).toBeDefined() // now shows the live line meanwhile
expect(await ui.find({ type: 'Text', text: /waiting on you: you to test it/ })).toBeDefined()
await ui.unmount()
})

test('a slash command that starts no turn leaves now as it was', async ($, on) => {
engine(on, '{"goal":"Ship 3 mods","now":"built Where Am I","waiting":"","next":""}')
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' } as any)
await $.turn.complete({ turnId: 't1', reason: 'answer', answer: 'done', durationMs: 1 } as any)
await wait()
await $.prompt.submit({ text: '/where' } as any)
const ui = await $.ui.mount({ plugin: 'where-am-i', surface: 'terminal', ...BAND } as any)
expect(await ui.find({ type: 'Text', text: /^built Where Am I$/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /reading your message/ })).toBeUndefined()
await ui.unmount()
})

test('a turn Claude Code starts itself keeps the person\'s message as the prompt', async ($, on) => {
const asked: string[] = []
engine(on, (_$: any, e: any) => (asked.push(e.prompt), { value: { isAnswered: true, text: '{"goal":"g","now":"n","waiting":"","next":""}', usage: { input_tokens: 1, output_tokens: 1 } } }))
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' } as any)
await $.prompt.submit({ text: 'ship all 3' } as any)
await $.turn.start({ turnId: 't1', text: 'ship all 3' } as any)
await $.turn.complete({ turnId: 't1', reason: 'answer', answer: 'started an agent', durationMs: 1 } as any)
await $.turn.start({ turnId: 't2', text: '<task-notification>agent done</task-notification>' } as any) // a background agent finished
await $.turn.complete({ turnId: 't2', reason: 'answer', answer: 'the agent is done', durationMs: 1 } as any)
await wait()
expect(asked[1]).toContain("The person's latest message: ship all 3")
expect(asked[1]).not.toContain('task-notification')
})

test('leaves out next while the next-steps mod shows it', async ($, on) => {
engine(on, '{"goal":"Ship 3 mods","now":"built Where Am I","waiting":"","next":"Rulebook Guard"}')
let nextStepsOn = false // stands for the value next-steps sets when it starts
Expand Down
4 changes: 2 additions & 2 deletions site/mods/heatmap/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400;600&family=Atkinson+Hyperlegible+Next:wght@400;600;700&display=swap">
<link rel="stylesheet" href="../../style.css">
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "SoftwareSourceCode", "name": "heatmap", "description": "A GitHub-style grid of your Claude Code time in a pane: one square per day of active minutes, with your streak, peak hours and top projects. Run /heatmap.", "url": "https://hamzafer.github.io/claude-code-mods/mods/heatmap/", "codeRepository": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/heatmap", "programmingLanguage": "TypeScript", "runtimePlatform": "Claude Code", "version": "0.1.2", "license": "https://opensource.org/licenses/MIT", "author": {"@type": "Person", "name": "hamzafer", "url": "https://github.com/hamzafer"}, "image": "https://hamzafer.github.io/claude-code-mods/images/gallery/heatmap.png", "isPartOf": {"@type": "CollectionPage", "name": "Claude Code mods", "url": "https://hamzafer.github.io/claude-code-mods/"}}</script>
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "SoftwareSourceCode", "name": "heatmap", "description": "A GitHub-style grid of your Claude Code time in a pane: one square per day of active minutes, with your streak, peak hours and top projects. Run /heatmap.", "url": "https://hamzafer.github.io/claude-code-mods/mods/heatmap/", "codeRepository": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/heatmap", "programmingLanguage": "TypeScript", "runtimePlatform": "Claude Code", "version": "0.1.3", "license": "https://opensource.org/licenses/MIT", "author": {"@type": "Person", "name": "hamzafer", "url": "https://github.com/hamzafer"}, "image": "https://hamzafer.github.io/claude-code-mods/images/gallery/heatmap.png", "isPartOf": {"@type": "CollectionPage", "name": "Claude Code mods", "url": "https://hamzafer.github.io/claude-code-mods/"}}</script>
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "Claude Code mods", "item": "https://hamzafer.github.io/claude-code-mods/"}, {"@type": "ListItem", "position": 2, "name": "heatmap", "item": "https://hamzafer.github.io/claude-code-mods/mods/heatmap/"}]}</script>
</head>
<body class="mod c-see">
Expand All @@ -43,7 +43,7 @@
<header class="mod-head">
<h1>heatmap</h1>
<p class="lede">A GitHub-style grid of your Claude Code time, one square per day.</p>
<p class="meta"><span class="cat-chip"><span class="sq" aria-hidden="true">■</span> See what&#x27;s happening</span> <span>Version 0.1.2</span></p>
<p class="meta"><span class="cat-chip"><span class="sq" aria-hidden="true">■</span> See what&#x27;s happening</span> <span>Version 0.1.3</span></p>
</header>
<figure class="shot"><div class="shot-frame"><img style="--w:836px" src="../../images/gallery/heatmap.png" width="836" height="412" alt="heatmap: a year of Claude Code activity as orange squares, with a 25-day streak, peak hours and top projects" decoding="async" fetchpriority="high"></div><figcaption>A year of sample activity, with the streak and top projects.</figcaption></figure>
<div class="mod-body">
Expand Down
4 changes: 2 additions & 2 deletions site/mods/mission-control/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400;600&family=Atkinson+Hyperlegible+Next:wght@400;600;700&display=swap">
<link rel="stylesheet" href="../../style.css">
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "SoftwareSourceCode", "name": "mission-control", "description": "See every Claude Code subagent, tool call and file they touch, live, in a pane next to the chat. Type /mission to open the map.", "url": "https://hamzafer.github.io/claude-code-mods/mods/mission-control/", "codeRepository": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/mission-control", "programmingLanguage": "TypeScript", "runtimePlatform": "Claude Code", "version": "0.1.4", "license": "https://opensource.org/licenses/MIT", "author": {"@type": "Person", "name": "hamzafer", "url": "https://github.com/hamzafer"}, "image": "https://hamzafer.github.io/claude-code-mods/images/og.png", "isPartOf": {"@type": "CollectionPage", "name": "Claude Code mods", "url": "https://hamzafer.github.io/claude-code-mods/"}}</script>
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "SoftwareSourceCode", "name": "mission-control", "description": "See every Claude Code subagent, tool call and file they touch, live, in a pane next to the chat. Type /mission to open the map.", "url": "https://hamzafer.github.io/claude-code-mods/mods/mission-control/", "codeRepository": "https://github.com/hamzafer/claude-code-mods/tree/main/mods/mission-control", "programmingLanguage": "TypeScript", "runtimePlatform": "Claude Code", "version": "0.1.5", "license": "https://opensource.org/licenses/MIT", "author": {"@type": "Person", "name": "hamzafer", "url": "https://github.com/hamzafer"}, "image": "https://hamzafer.github.io/claude-code-mods/images/og.png", "isPartOf": {"@type": "CollectionPage", "name": "Claude Code mods", "url": "https://hamzafer.github.io/claude-code-mods/"}}</script>
<script type="application/ld+json">{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "Claude Code mods", "item": "https://hamzafer.github.io/claude-code-mods/"}, {"@type": "ListItem", "position": 2, "name": "mission-control", "item": "https://hamzafer.github.io/claude-code-mods/mods/mission-control/"}]}</script>
</head>
<body class="mod c-see">
Expand All @@ -43,7 +43,7 @@
<header class="mod-head">
<h1>mission-control</h1>
<p class="lede">Every subagent, every tool call and every file they touch, in a pane next to the chat.</p>
<p class="meta"><span class="cat-chip"><span class="sq" aria-hidden="true">■</span> See what&#x27;s happening</span> <span>Version 0.1.4</span></p>
<p class="meta"><span class="cat-chip"><span class="sq" aria-hidden="true">■</span> See what&#x27;s happening</span> <span>Version 0.1.5</span></p>
</header>
<figure class="shot"><div class="shot-frame"><video style="--w:1100px" width="1100" height="618" autoplay muted loop playsinline preload="metadata" poster="../../images/mission-control-poster.jpg" aria-label="mission-control at 4x speed: two subagents building a logout feature across four files, with the code map on the right"><source src="../../images/mission-control.mp4" type="video/mp4"></video></div><figcaption>Shown at 4x: two subagents building a logout feature across four files.</figcaption></figure>
<div class="mod-body">
Expand Down
Loading
Loading