diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index da4918f..6b38134 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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.5", + "version": "0.1.6", "source": "./mods/mission-control", "category": "See what's happening", "tags": [ diff --git a/README.md b/README.md index 14be484..7fc661d 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Install it like any mod, restart, and type `/mission` (or `/mission code` to ope - ๐ŸŸ  **Orange** while it writes - ๐ŸŸข **Green** when done, with one line on what changed -> The Code view runs on macOS and Linux and needs Google Chrome or Chromium and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere. +> The Code view runs on macOS and Linux and needs Google Chrome or Chromium and a terminal that shows images (Ghostty, kitty, iTerm2). On Windows it lists the map as lines, and `b` opens the drawn map in your browser. The Who view works everywhere. ## ๐Ÿ“š More diff --git a/docs/mods.md b/docs/mods.md index cacd9de..3232cb0 100644 --- a/docs/mods.md +++ b/docs/mods.md @@ -36,6 +36,7 @@ Shows every Claude Code subagent, tool call and touched file in a live pane. - `/mission code` opens straight to the code map, `/mission who` to the agents - Headless Chrome draws the code map as an image, in a throwaway profile. It uses the first of `/Applications/Google Chrome.app` (macOS), `google-chrome-stable`, `google-chrome`, `chromium-browser` or `chromium` in `/usr/bin` (Linux) - The Code view runs on macOS and Linux and needs a terminal that shows images (Ghostty, kitty, iTerm2) +- On Windows, whose terminals show no images, the Code view lists the map as lines, and `b` opens the drawn map in your browser, which follows each change - A file glows blue while Claude reads it, orange while it edits it, and turns green with a check once changed - One Haiku call after each turn writes the line under each changed file diff --git a/mods/mission-control/.claude-plugin/plugin.json b/mods/mission-control/.claude-plugin/plugin.json index 915dd60..58451c0 100644 --- a/mods/mission-control/.claude-plugin/plugin.json +++ b/mods/mission-control/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mission-control", - "version": "0.1.5", + "version": "0.1.6", "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", diff --git a/mods/mission-control/hooks/map.ts b/mods/mission-control/hooks/map.ts index f84e1c4..ec74b01 100644 --- a/mods/mission-control/hooks/map.ts +++ b/mods/mission-control/hooks/map.ts @@ -85,8 +85,8 @@ const esc = (s: string) => export type Look = { width: number; height: number; at: number; turn: number } // The whole picture: boxes per file, arrows per import, a glow on what is being touched now. -const SANS = `-apple-system, 'SF Pro Text', 'Helvetica Neue', sans-serif` -const MONO = `'SF Mono', Menlo, monospace` +const SANS = `-apple-system, 'SF Pro Text', 'Helvetica Neue', 'Segoe UI', sans-serif` +const MONO = `'SF Mono', Menlo, Consolas, monospace` const C = { bg: '#0d1015', grid: '#1a1f28', card: '#151922', cardLive: '#1b2130', line: '#2a3140', text: '#e8ebf2', dim: '#7d8698', faint: '#4b5263', edge: '#3a4252', edgeHot: '#6b7a96', @@ -246,6 +246,32 @@ export function svg(files: MapFile[], look: Look): string { return o.join('\n') } +export type MapLine = { mark: string; tone: 'read' | 'edit' | 'changed' | 'dim'; title: string; imports: string[]; why?: string } + +// The map as lines, for a terminal that draws no pictures: newest first, each file's +// state, the files it imports and the one-line why. +export function textMap(files: MapFile[], look: { at: number; turn: number }): MapLine[] { + const all = files.map(f => f.path) + const es = edges(files) + return files.map(f => { + const isLive = look.at - f.at < 5000 + const isChanged = f.changedTurn === look.turn && look.turn > 0 + const isReading = f.act === 'read' || f.act === 'search' + return { + mark: isLive ? (isReading ? 'โ—' : 'โœŽ') : isChanged ? 'โœ“' : 'ยท', + tone: isLive ? (isReading ? 'read' : 'edit') : isChanged ? 'changed' : 'dim', + title: titleOf(f.path, all), + imports: es.filter(e => e.from === f.path).map(e => titleOf(e.to, all)), + why: f.why, + } + }) +} + +// The picture as a page that reloads itself, so a browser keeps showing the latest. +export function page(picture: string) { + return `Code map\n${picture}\n` +} + // A file's name, or with several of that name the nearest folder that tells them apart: // three `hooks/register.tsx` read as `merge-gate โ€บ register.tsx`, `snake โ€บ register.tsx`. export function titleOf(path: string, all: string[]) { diff --git a/mods/mission-control/hooks/register.tsx b/mods/mission-control/hooks/register.tsx index 03e733d..24cd9d9 100644 --- a/mods/mission-control/hooks/register.tsx +++ b/mods/mission-control/hooks/register.tsx @@ -7,7 +7,7 @@ import { atom, read, update } from 'claude-code' import type { EngineInterface, Register, Timer } from 'claude-code' import type { MapFile, MissionNode } from '../types' -import { importsOf, svg } from './map' +import { importsOf, page, svg, textMap } from './map' import { label, lines, summary } from './tree' import { bandsFile, parseOrder, stackBands } from './bands' @@ -37,9 +37,11 @@ const turn = atom({ plugin: 'mission-control', key: 'turn' } as const, 0) 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 draw = { size: { columns: 100, rows: 30 }, pending: false, dirty: false, n: 0, error: '', isPageOpen: false } const changes = new Map() // 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" +// No Windows terminal draws pictures: there the map is lines in the pane and a page in the browser. +let isWindows = false // Under the cap, the oldest tool calls go first; main and the agents always stay. export function cap(list: MissionNode[]) { @@ -65,6 +67,7 @@ export const register: Register = on => { on('session.start', async ($, e, next) => { const r = await next(e) + isWindows = (await $.env.get('OS')) === 'Windows_NT' 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(() => {}) timer?.cancel() timer = $.clock.every(1000, () => { @@ -112,7 +115,8 @@ export const register: Register = on => { const parent = e.agentId && list.some(n => n.id === e.agentId) ? e.agentId : 'main' return cap([...list, { id, parent, kind: 'tool', label: text, family, status: 'running', start: Date.now() }]) }) - const path = typeof args.file_path === 'string' ? args.file_path : undefined + const raw = typeof args.file_path === 'string' ? args.file_path : undefined + const path = raw && isWindows ? raw.replace(/\\/g, '/') : raw // one separator, so names and imports resolve if (path) await touch($, path, e.tool === 'Read' ? 'read' : e.tool === 'Write' ? 'write' : e.tool === 'Edit' ? 'edit' : 'search', false) let failed = true @@ -170,6 +174,7 @@ export const register: Register = on => {