Focus mode: a cleaner terminal interface, toggled with /og-focus

A cleaner terminal interface, toggled with /og-focus. The choice is remembered across sessions, and the footer shows og-focus while the mode is on.
| With the mode on | |
|---|---|
| Tool calls | One dim line: · Bash git status, · Read src/org.ts, · jira · jira_get_issue (◦ while running) |
| Edits (Edit/Write) | One line with counts: · Edit src/org.ts +12 −3 |
| Results | Hidden, unless the call fails |
| Errors and interruptions | Claude Code's full row, as usual |
Baked for 3s, notices under the logo | Hidden |
? for shortcuts | Hidden while idle (esc to interrupt stays during a turn) |
Out of a mod's reach: hook output lines (SessionStart:startup hook success) and system blocks. The full transcript (ctrl+o) is compact too while the mode is on. To see everything, turn it off with /og-focus.
hooks/register.tsx 92 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { compactLine } from './compact'
5
6const COMMAND = 'og-focus'
7const STORE_KEY = 'isOn'
8
9const isOn = atom({ plugin: 'og-focus-mode', key: 'isOn' } as const, false)
10const cwd = atom({ plugin: 'og-focus-mode', key: 'cwd' } as const, null)
11
12async function toggle($: EngineInterface) {
13 const next = !(await read($, isOn))
14 if (next && (await read($, cwd)) === null) {
15 const dir = await $.session.cwd()
16 await update($, cwd, () => dir)
17 }
18 await update($, isOn, () => next)
19 await $.store.set(STORE_KEY, next)
20
21 return next
22}
23
24export const register: Register = on => {
25 on('session.start', async ($, e, next) => {
26 const started = await next(e)
27 await $.command.register({ name: COMMAND, description: 'Turns focus mode on or off: a cleaner interface' })
28 const [stored, dir] = await Promise.all([$.store.get(STORE_KEY), $.session.cwd()])
29 await update($, isOn, () => stored === true)
30 await update($, cwd, () => dir)
31
32 return started
33 })
34
35 on('command.run', { command: COMMAND }, async $ => {
36 const isNowOn = await toggle($)
37
38 return { text: isNowOn ? 'Focus mode on.' : 'Focus mode off.' }
39 }).catch(() => ({ text: 'og-focus-mode: could not toggle the mode.' }))
40
41 // A tool call: one dim line; an error or an interruption keeps the full row.
42 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
43 const { isErrored, isInterrupted, isRunning, tool, input, output } = e.props
44 if (!(await read($, isOn)) || isErrored || isInterrupted) return next(e)
45 const { Box, Text } = $.ui.resolve(e)
46 const line = compactLine(tool, input, output, (await read($, cwd)) ?? undefined)
47
48 return (
49 <Box>
50 <Text dimColor wrap="truncate-end">
51 {isRunning ? '◦' : '·'} {line.label}
52 {line.subject !== '' && ` ${line.subject}`}
53 {line.counts !== undefined && <Text color="diffAdded">{` ${line.counts}`}</Text>}
54 </Text>
55 </Box>
56 )
57 })
58
59 // The result under a call: drawn only when it failed.
60 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
61 if (!(await read($, isOn)) || e.props.isErrored) return next(e)
62 const { Box } = $.ui.resolve(e)
63
64 return <Box display="none" />
65 })
66
67 // `Baked for 3s` and the notices under the logo.
68 on('ui.render', { component: ['TurnDuration', 'InfoNotice'] }, async ($, e, next) => {
69 if (!(await read($, isOn))) return next(e)
70 const { Box } = $.ui.resolve(e)
71
72 return <Box display="none" />
73 })
74
75 // `? for shortcuts` while idle; `esc to interrupt` stays while a turn runs.
76 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
77 if (!(await read($, isOn)) || e.props.isWorking) return next(e)
78
79 // What other mods added at the end (`tail`) stays, as the line itself; a
80 // rewritten hint would otherwise drop it.
81 const tail = (e.props.tail ?? '').replace(/^\s*·\s*/, '')
82 return next({ ...e, props: { ...e.props, hint: tail, tail: undefined } })
83 })
84
85 // The footer says when focus mode is on.
86 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
87 if (!(await read($, isOn))) return next(e)
88
89 return next({ ...e, props: { ...e.props, modes: [...e.props.modes, 'og-focus'] } })
90 })
91}
92hooks/compact.ts 90 lines1// Pure logic: the one line a tool call is drawn as in focus mode.
2
3type Input = Record<string, unknown>
4type Patch = { lines?: unknown }[]
5
6const text = (value: unknown) => (typeof value === 'string' ? value : undefined)
7
8const firstLine = (value: string) => value.trim().split('\n')[0] ?? ''
9
10/** A path relative to the session's directory when it lies under it. */
11export const shortPath = (path: string, cwd: string | undefined) =>
12 cwd !== undefined && path.startsWith(`${cwd}/`) ? path.slice(cwd.length + 1) : path
13
14const host = (url: string) => {
15 try {
16 return new URL(url).host
17 } catch {
18 return url
19 }
20}
21
22const lineCount = (value: string | undefined) => (value === undefined || value === '' ? 0 : value.split('\n').length)
23
24/** Added and removed lines: from the result's patch once done, else from the input. */
25export const changeCounts = (tool: string, input: Input, output: unknown) => {
26 const patch = (output as { structuredPatch?: Patch } | undefined)?.structuredPatch
27 if (Array.isArray(patch)) {
28 let added = 0
29 let removed = 0
30 for (const hunk of patch) {
31 for (const line of Array.isArray(hunk.lines) ? hunk.lines : []) {
32 if (typeof line !== 'string') continue
33 if (line.startsWith('+')) added += 1
34 else if (line.startsWith('-')) removed += 1
35 }
36 }
37 return { added, removed }
38 }
39 if (tool === 'Write') return { added: lineCount(text(input.content)), removed: 0 }
40 if (tool === 'Edit') return { added: lineCount(text(input.new_string)), removed: lineCount(text(input.old_string)) }
41 return undefined
42}
43
44/** What a call is about, in a few words: its command, file, pattern or query. */
45export const subject = (tool: string, input: Input, cwd?: string): string => {
46 const path = text(input.file_path) ?? text(input.notebook_path) ?? text(input.path)
47 switch (tool) {
48 case 'Bash':
49 return firstLine(text(input.command) ?? '')
50 case 'Read':
51 case 'Edit':
52 case 'Write':
53 case 'NotebookEdit':
54 return path === undefined ? '' : shortPath(path, cwd)
55 case 'Grep':
56 case 'Glob':
57 return [text(input.pattern), path && `em ${shortPath(path, cwd)}`].filter(Boolean).join(' ')
58 case 'WebFetch':
59 return host(text(input.url) ?? '')
60 case 'WebSearch':
61 return text(input.query) ?? ''
62 case 'Agent':
63 case 'Task':
64 return text(input.description) ?? ''
65 case 'Skill':
66 return text(input.skill) ?? ''
67 default:
68 return text(input.description) ?? ''
69 }
70}
71
72/** `mcp__jira__jira_get_issue` → `jira · jira_get_issue`. */
73export const toolLabel = (tool: string) => {
74 const mcp = /^mcp__(.+?)__(.+)$/.exec(tool)
75 return mcp ? `${mcp[1]} · ${mcp[2]}` : tool
76}
77
78export type CompactLine = { label: string; subject: string; counts?: string }
79
80export const compactLine = (tool: string, input: unknown, output: unknown, cwd?: string): CompactLine => {
81 const fields = input !== null && typeof input === 'object' ? (input as Input) : {}
82 const counts = tool === 'Edit' || tool === 'Write' ? changeCounts(tool, fields, output) : undefined
83
84 return {
85 label: toolLabel(tool),
86 subject: subject(tool, fields, cwd),
87 counts: counts && `+${counts.added} −${counts.removed}`,
88 }
89}
90types/index.d.ts 11 lines1declare module 'claude-code' {
2 interface PluginState {
3 'og-focus-mode': {
4 /** Whether the cleaner interface is on; kept in the store across sessions. */
5 isOn: boolean
6 /** The session's directory, so file paths draw relative to it. */
7 cwd: string | null
8 }
9 }
10}
11