SLOPSHOPPER

og-focus-mode

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

newspinnerrowscommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · og-focus-mode
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines · Edit src/auth.ts +0 −0 ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. › /og-focus ⎿ og-focus-mode: Focus mode on. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Tool row
· Edit src/auth.ts +0 −0
README

og-focus-mode

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 callsOne 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
ResultsHidden, unless the call fails
Errors and interruptionsClaude Code's full row, as usual
Baked for 3s, notices under the logoHidden
? for shortcutsHidden 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.

Source 3 files
hooks/register.tsx 92 lines
1import { 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}
92
hooks/compact.ts 90 lines
1// 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}
90
types/index.d.ts 11 lines
1declare 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