From 27986a4694b1e61b2fa1e3ccfa7384e61a3c0264 Mon Sep 17 00:00:00 2001 From: ManushPatel08 Date: Thu, 22 Oct 2026 09:50:40 -0400 Subject: [PATCH 1/2] mission-control: a code map on Windows The Code view ran Chrome from its macOS path through sh, so on Windows it only said it needed Chrome. And no Windows terminal shows images, so even a found Chrome would have drawn the alt text alone. On Windows the Code view now lists the map as lines (state, imports, the why), and b opens the drawn map in the default browser as a page that reloads itself, redrawn on each change once it is open. No Chrome is needed: the browser draws the SVG. Backslash paths are turned to slashes, so names and imports resolve. The test mocks now read paths the way the engine hands them on Windows (C:\tmp\...), which two tests tripped on. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- README.md | 2 +- docs/mods.md | 4 +- .../.claude-plugin/plugin.json | 2 +- mods/mission-control/hooks/map.ts | 30 +++++++++- mods/mission-control/hooks/register.tsx | 60 ++++++++++++++++--- .../tests/mission-control.test.tsx | 39 ++++++++++-- 7 files changed, 120 insertions(+), 19 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1593a31..b5269b5 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.3", + "version": "0.1.4", "source": "./mods/mission-control", "category": "See what's happening", "tags": [ diff --git a/README.md b/README.md index 3732f47..f447736 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,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 also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere. +> On macOS the Code view also needs Google Chrome 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 38e84c5..1c9ed7a 100644 --- a/docs/mods.md +++ b/docs/mods.md @@ -34,8 +34,8 @@ Shows every Claude Code subagent, tool call and touched file in a live pane. - Press `w` for Who, `c` for Code and `q` to close - `/mission code` opens straight to the code map, `/mission who` to the agents -- Headless Chrome (`/Applications/Google Chrome.app`) draws the code map as an image, in a throwaway profile -- The Code view is macOS only and needs a terminal that shows images (Ghostty, kitty, iTerm2) +- On macOS, headless Chrome (`/Applications/Google Chrome.app`) draws the code map as an image, in a throwaway profile, for 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 cfa1114..8ef90aa 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.3", + "version": "0.1.4", "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 389724c..59a78ff 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 } 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' @@ -30,8 +30,10 @@ 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" +// 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[]) { @@ -55,6 +57,7 @@ let bandOrder: Promise | undefined // read once per session: restart t 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(() => {}) $.clock.every(1000, () => { void (async () => { @@ -101,7 +104,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 @@ -158,6 +162,7 @@ export const register: Register = on => {