SLOPSHOPPER

ultra-config

Five setting profiles in a side pane (Token saver, Fast flow, Max quality, Guarded, Autopilot): press one to see exactly what it sets, then Activate; every…

panecommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ultra-config
│ ┃ ultra config ✕ › fix the failing auth test and add an audit log call │ ┃  ⚙ ULTRA  CONFIG  unset · default effort · │ ┃ ──────────────────────────────────────────── ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ ▾  ◔  Token saver ■■■■■■■■■ ⏺ Update(src/auth.ts) │ ┃       Sonnet, low effort, short answers, Hai ⎿ Added 2 lines, removed 1 line │ ┃ ╭─────────────────────────────────────────── ⏺ Bash(bun test) │ ┃ │ ⊘ model: not in this Claude Code version ⎿ 3 pass, 1 fail │ ┃ │ ● Effort                  default → low │ ┃ │   less thinking per request; saved for the ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ● Subagent model          unset → haiku │ ┃ │   subagents run on the cheapest model ✻ Worked for 42s · done 4:20 PM │ ┃ │ ⊘ fast: not in this Claude Code version │ ┃ │ ⊘ outputStyle: not in this Claude Code ver › /ultra-config │ ┃ │ ⊘ promptSuggestionEnabled: not in this Cla ⎿ ultra-config: ultra-config opened · press a profile to see what │ ┃ │ ⊘ recap: not in this Claude Code version │ ┃ │ ⊘ autoCompact: not in this Claude Code ver │ ┃ │ ⊘ workflowSizeGuideline: not in this Claud │ ┃ │ ⊘ workflowKeywordTriggerEnabled: not in th │ ┃ │ ● MCP output cap          unset → 10000 │ ┃ │   caps MCP tool results (default 25000) │ ┃ │ │ ┃ │ [ Activate Token saver ] │ ┃ ╰─────────────────────────────────────────── │ ┃ │ ┃ ▸  »  Fast flow ■■■■■■■■■ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · ultra config
 ⚙ ULTRA  CONFIG  unset · default effort · unset mode ─────────────────────────────────────────────────────── ▾  ◔  Token saver ■■■■■■■■■■  0/3            Sonnet, low effort, short answers, Haiku subagen… ╭─────────────────────────────────────────────────────╮ │ ⊘ model: not in this Claude Code version │ │ ● Effort                  default → low │ │   less thinking per request; saved for the model i… │ │ ● Subagent model          unset → haiku │ │   subagents run on the cheapest model │ │ ⊘ fast: not in this Claude Code version │ │ ⊘ outputStyle: not in this Claude Code version │ │ ⊘ promptSuggestionEnabled: not in this Claude Code… │ │ ⊘ recap: not in this Claude Code version │ │ ⊘ autoCompact: not in this Claude Code version │ │ ⊘ workflowSizeGuideline: not in this Claude Code v… │ │ ⊘ workflowKeywordTriggerEnabled: not in this Claud… │ │ ● MCP output cap          unset → 10000 │ │   caps MCP tool results (default 25000) │ │ │ │ [ Activate Token saver ] │ ╰─────────────────────────────────────────────────────╯ ▸  »  Fast flow ■■■■■■■■■■  0/1            Edits auto-accepted, medium effort, fewer interr… ▸  ◆  Max quality ■■■■■■■■■■  1/2            Most capable model, deepest reasoning, big workf… ▸  ▣  Guarded ■■■■■■■■■■  0/2            You approve each change; no reading secrets; no … ▸  ➤  Autopilot ■■■■■■■■■■  0/1            Long unattended runs: auto mode, recap, push ale…
README

ultra-config

Five setting profiles in a side pane. Each one sets Claude Code up for one way of working: press a profile to see exactly what it sets, then press Activate. Every activation can be undone.

⚙ ULTRA  CONFIG          default · default effort · default mode
──────────────────────────────────────────────────────────────
▾  ◔  Token saver                      ■■■□□□□□□□  3/11
      Sonnet, low effort, short answers, Haiku subagents
╭────────────────────────────────────────────────────────────╮
│ ● Model                   Default (recommended) → sonnet   │
│   cheaper per token than Opus                              │
│ ● Effort                  medium → low                     │
│   less thinking per request                                │
│ ✓ Auto-compact            on                               │
│ …                                                          │
│ [ Activate Token saver ]                                   │
╰────────────────────────────────────────────────────────────╯
▸  »  Fast flow                        ■■■□□□□□□□  3/8
      Edits auto-accepted, medium effort, fewer interruptions
▸  ◆  Max quality                      ■■■■■■□□□□  6/9
▸  ▣  Guarded                          ■■■□□□□□□□  2/7
▸  ➤  Autopilot                        ■■■■■□□□□□  4/8

Commands

CommandWhat it does
/ultra-configOpens the pane
/ultra-config <profile>Opens the pane on that profile
/ultra-config apply <profile>Activates a profile and lists what it changed and skipped
/ultra-config undoReverts the last activated profile (newest first)
/ultra-config closeCloses the pane

Profile ids: saver, flow, quality, guarded, autopilot.

In the pane everything is a press: a profile's name opens its detail (one is always open, the first to start), and Activate applies it (a while the pane has focus). There are no tabs or arrow-key menus, so nothing competes with Claude Code's own ← for the agents view.

The profiles

ProfileForSets
◔ Token saverSpending fewer tokensModel sonnet, effort low, subagents on haiku, Concise output, no prompt suggestions or recap, auto-compact on, small workflows, no ultracode keyword, MCP output capped at 10,000 tokens, fast mode off
» Fast flowLess waitingModel sonnet, effort medium, edits auto-accepted, Concise output, prompt suggestions on, turn duration and progress bar on, medium workflows
◆ Max qualityThe best resultModel best, effort xhigh, subagents keep their own model, default output, workflows on with large fan-outs and the ultracode keyword, checkpoints and auto-compact on
▣ GuardedControl and safetyEvery edit and command asks first, plan mode never acts on its own, bypassPermissions disabled, Read deny rules for .env, secrets/, ~/.ssh and ~/.aws (they stop the Read tool, not a Bash cat), checkpoints on, verbose output, no ultracode keyword
➤ AutopilotLong unattended runs (fewer approval prompts)Auto permission mode (also during plan), effort high, workflows on, recap on, push notifications when Claude decides or needs you, auto-compact on

Each profile shows how much of it is already in force (3/11), and ✓ active when all of it is. In the detail, ● is a change (old value struck through, then the new one and why), ✓ is already set and ⊘ is skipped.

Where each change is written

  • /config rows (model, permission mode, output style, auto-compact…) are written with $.config.set, exactly as if you changed them in /config. Claude Code picks the file: most go to ~/.claude/settings.json or ~/.claude.json, and Output style goes to the project's .claude/settings.local.json, the same as the menu.
  • Effort is saved per model, the way /effort saves it: modelSettings["<model id>"].effortLevel in ~/.claude/settings.json, for the model your next message runs on (the mod learns its id from that request and sends the request at the new effort too). Since Claude Code 2.1.251 a top-level effortLevel in user settings is ignored by Opus 5.5 and later models, so the mod never writes it. A model id with a dot (Bedrock, Vertex) can't be saved this way; then the effort applies to that one request only.
  • Keys with no /config row (env.CLAUDE_CODE_SUBAGENT_MODEL, env.MAX_MCP_OUTPUT_TOKENS, permissions.deny, permissions.disableBypassPermissionsMode) are written to ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR/settings.json). Before each write the file is copied to settings.json.ultra-config.bak (only the latest copy is kept; it holds whatever the file held, env values included). A file that is not valid JSON is never overwritten: those steps are skipped and the pane says why.

Every activated profile is kept (the last 20) in the plugin's store, so undo works after a restart and puts back exactly the values it changed. A deny rule you already had stays when you undo. A profile never touches a row your organization locked, a value this Claude Code version does not offer, or a row Claude Code reserves for its own panel (it says so, for example "can only be turned on from the /config panel"); those show as ⊘ in the detail.

The /config rows apply right away and the effort from your next message. The env variables (CLAUDE_CODE_SUBAGENT_MODEL, MAX_MCP_OUTPUT_TOKENS) are read when Claude Code starts, so count on them from the next session.

Where it draws

In the fullscreen layout the pane docks beside the transcript (/tui fullscreen, at least 110 columns). On the classic layout it sits above the prompt. In Claude Desktop it draws in the session's side panel; padded cells use non-breaking spaces so the columns keep their width.

Options

Set them in user or managed settings under pluginConfigs["ultra-config@skills-dir"].options ("ultra-config" when loaded with --plugin-dir):

OptionDefaultWhat it does
columns62Width asked for the docked pane (44–100); a width you drag it to wins

Install

npx claude-code-templates@latest --mod productivity/ultra-config

Needs Claude Code 2.1.287 or later and a trusted project folder (or install it under ~/.claude/skills/).

Notes for maintainers

Claude Code writes /config rows from the settings it holds in memory, and it does not reload a settings.json write that lands right after one of its own. So the mod writes settings.json first, waits until $.settings.read shows the new values (writing the file once more if the first write was not picked up), and only then sets /config rows. If the engine never reloads the file, the row changes are skipped and the pane says to restart the session. Without this ordering, the next /config change would silently put back the old values.

Tests: claude plugin test productivity/ultra-config (from cli-tool/components/mods).

Source 3 files
hooks/register.tsx 560 lines
1/**
2 * ultra-config — Claude Mod
3 *
4 * A side pane with five profiles (Token saver, Fast flow, Max quality,
5 * Guarded, Autopilot) that set Claude Code up for one way of working in one
6 * press. Each profile opens to show exactly what it sets, with one button to
7 * activate it.
8 *
9 *   `/ultra-config` opens it · `/ultra-config <profile>` opens that profile ·
10 *   `/ultra-config apply <profile>` activates it · `/ultra-config undo`
11 *   reverts the last one · `/ultra-config close` closes the pane
12 *
13 * `/config` rows are written with `$.config.set`, the way the menu writes
14 * them; effort, the subagent model, env caps and deny rules (keys with no
15 * `/config` row) are written to ~/.claude/settings.json, after a copy of the
16 * file is saved beside it as settings.json.ultra-config.bak. Each activated
17 * profile is kept in the plugin's store so `undo` puts back exactly what it
18 * changed.
19 *
20 * Needs Claude Code >= 2.1.287 (mods on by default); every UI prop it uses
21 * is in that version's API, so the Desktop app's bundled engine draws it too.
22 *
23 * Options (pluginConfigs["ultra-config@skills-dir"].options):
24 *   columns: number   width asked for the docked pane (default 62)
25 */
26import { atom, read, update } from 'claude-code'
27import type { ConfigRow, EngineInterface, Register } from 'claude-code'
28
29import type {
30  UltraConfigBackup as Backup,
31  UltraConfigEntry,
32  UltraConfigLive,
33  UltraConfigPending,
34  UltraConfigSnapshot as Snapshot,
35  UltraConfigUi,
36} from '../types'
37import {
38  PROFILES,
39  applySettings,
40  currentEffort,
41  effortPath,
42  inSync,
43  matchOf,
44  nb,
45  pad,
46  paneColumns,
47  planFor,
48  profileById,
49  restorable,
50  revertSettings,
51  show,
52  userSettingsPath,
53} from './profiles.ts'
54import type { Effort, Json, ListStep, Live, PlanItem, Profile, SettingsStep } from './profiles.ts'
55
56const PANE = 'ultra-config'
57const COMMAND = 'ultra-config'
58const STORE_HISTORY = 'history'
59const KEEP = 20
60/** How long a write waits for the engine to reload it, twice at most: 20 × 150 ms. */
61const RELOAD_TRIES = 20
62const RELOAD_STEP_MS = 150
63const INK = '#141414'
64const ACCENT = '#d97757'
65
66type Host = EngineInterface
67
68const ui = atom({ plugin: 'ultra-config', key: 'ui' } as const, {
69  selected: null,
70  notice: null,
71  isBusy: false,
72} satisfies UltraConfigUi)
73const snapshot = atom({ plugin: 'ultra-config', key: 'snapshot' } as const, {
74  rows: [],
75  user: {},
76  path: null,
77  error: null,
78} satisfies Snapshot)
79const history = atom({ plugin: 'ultra-config', key: 'history' } as const, [] as Backup[])
80/** The main loop's model id and effort, as the last request carried them. */
81const live = atom({ plugin: 'ultra-config', key: 'live' } as const, {} as UltraConfigLive)
82/** An activated effort waiting for the next request to learn the model id it is saved under. */
83const pending = atom({ plugin: 'ultra-config', key: 'pending' } as const, null as UltraConfigPending | null)
84const STORE_PENDING = 'pendingEffort'
85
86const message = (err: unknown): string => (err instanceof Error ? err.message : String(err))
87
88// This module holds every call on `$`; profiles.ts is pure.
89
90/** Reads the settings file, or says why it can't be trusted for a write. */
91async function readUser($: Host, path: string | null): Promise<{ user: Json; text: string | null; error: string | null }> {
92  if (path === null) return { user: {}, text: null, error: 'no home directory to find settings.json in' }
93  let text: string
94  try {
95    text = await $.fs.read(path)
96  } catch {
97    // no file yet is fine; a file the engine reads but we can't is not
98    const loaded = await $.settings.read({ source: 'user' }).catch(() => ({}))
99    return Object.keys(loaded).length === 0
100      ? { user: {}, text: null, error: null }
101      : { user: { ...loaded }, text: null, error: `cannot read ${path}` }
102  }
103  if (text.trim() === '') return { user: {}, text, error: null }
104  try {
105    const parsed: unknown = JSON.parse(text)
106    if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
107      return { user: {}, text, error: 'settings.json is not a JSON object' }
108    return { user: parsed as Json, text, error: null }
109  } catch (err) {
110    return { user: {}, text, error: `settings.json is not valid JSON (${message(err)})` }
111  }
112}
113
114async function settingsPath($: Host): Promise<string | null> {
115  const dir = await $.env.get('CLAUDE_CONFIG_DIR')
116  const home = (await $.env.get('HOME')) ?? (await $.env.get('USERPROFILE'))
117  return userSettingsPath(dir, home) ?? null
118}
119
120async function refresh($: Host): Promise<Snapshot> {
121  const rows = await $.config.list().catch(() => [] as ConfigRow[])
122  const path = await settingsPath($)
123  const { user, error } = await readUser($, path)
124  const next: Snapshot = { rows: [...rows], user, path, error }
125  await update($, snapshot, () => next)
126  return next
127}
128
129async function notify($: Host, text: string, tone: 'success' | 'warning' | 'error'): Promise<void> {
130  await update($, ui, s => ({ ...s, notice: { text, tone } }))
131}
132
133async function saveHistory($: Host, list: Backup[]): Promise<void> {
134  await update($, history, () => list)
135  await $.store.set(STORE_HISTORY, list).catch(err => $.ui.log(`ultra-config: history not saved: ${message(err)}`))
136}
137
138/**
139 * Writes the settings object, keeping a copy of the file as it was, and
140 * resolves once the engine has loaded it (false if it never did).
141 *
142 * The engine writes /config rows from the settings it holds in memory, so a
143 * `$.config.set` before it reloads would put back what this write changed.
144 * And it does not reload a write that lands just after one of its own: then
145 * the file is written again, once, which it does pick up.
146 */
147async function writeUser($: Host, path: string, before: string | null, next: Json, paths: string[]): Promise<boolean> {
148  if (before !== null) await $.fs.write(`${path}.ultra-config.bak`, before)
149  const text = `${JSON.stringify(next, null, 2)}\n`
150  for (const body of [text, text.trimEnd()]) {
151    await $.fs.write(path, body)
152    for (let i = 0; i < RELOAD_TRIES; i++) {
153      const loaded = await $.settings.read({ source: 'user' }).catch(() => undefined)
154      if (loaded && inSync(loaded, next, paths)) return true
155      await $.clock.sleep(RELOAD_STEP_MS)
156    }
157  }
158  $.ui.log(`ultra-config: Claude Code did not reload ${path}`)
159  return false
160}
161
162const NOT_RELOADED = 'Claude Code did not reload settings.json; restart the session, then apply again'
163
164/** The model and effort in use, with an effort waiting for the next request. */
165async function liveNow($: Host): Promise<Live> {
166  const want = await read($, pending)
167  return { ...(await read($, live)), ...(want ? { pending: want.value } : {}) }
168}
169
170async function setPending($: Host, value: UltraConfigPending | null): Promise<void> {
171  await update($, pending, () => value)
172  await $.store.set(STORE_PENDING, value).catch(err => $.ui.log(`ultra-config: pending effort not saved: ${message(err)}`))
173}
174
175/**
176 * Saves an activated effort under the model id this request names, and adds
177 * that write to the activation's history entry so undo puts it back. Returns
178 * false when it could not be written (the request then still gets the effort).
179 */
180async function saveEffort($: Host, model: string, want: UltraConfigPending): Promise<boolean> {
181  const path = await settingsPath($)
182  const at = effortPath(model)
183  if (path === null || at === undefined) {
184    $.ui.log(`ultra-config: effort not saved for ${model} (no settings.json, or a model id with a dot)`)
185    return false
186  }
187  const { user, text, error } = await readUser($, path)
188  if (error !== null) {
189    $.ui.log(`ultra-config: effort not saved: ${error}`)
190    return false
191  }
192  const { next, entries } = applySettings(user, [{ layer: 'settings', path: at, label: 'Effort', value: want.value, why: '' }])
193  if (entries.length === 0) return true
194  const list = await read($, history)
195  await saveHistory($, list.map(b => (b.id === want.backupId ? { ...b, entries: [...b.entries, ...entries] } : b)))
196  return writeUser($, path, text, next, [at])
197}
198
199async function applyProfile($: Host, profile: Profile): Promise<Backup> {
200  const snap = await refresh($)
201  const plan = planFor(profile, snap.rows, snap.user, await liveNow($))
202  const entries: UltraConfigEntry[] = []
203  const refused: { label: string; reason: string }[] = []
204  let isSynced = true
205
206  // settings.json first: the /config writes below then start from it
207  const fileSteps = profile.steps.filter((s): s is SettingsStep | ListStep => s.layer === 'settings' || s.layer === 'list')
208  if (fileSteps.length > 0) {
209    const { user, text, error } = await readUser($, snap.path)
210    if (error !== null || snap.path === null) {
211      for (const s of fileSteps) refused.push({ label: s.label, reason: error ?? 'settings.json not found' })
212    } else {
213      const { next, entries: written } = applySettings(user, fileSteps)
214      if (written.length > 0) {
215        // recorded before the write: a write that fails halfway can still be undone
216        entries.push(...written)
217        try {
218          isSynced = await writeUser($, snap.path, text, next, written.flatMap(w => (w.layer === 'config' ? [] : [w.path])))
219        } catch (err) {
220          isSynced = false
221          for (const w of written) refused.push({ label: w.label, reason: `may not be written: ${message(err)}` })
222        }
223      }
224    }
225  }
226
227  for (const step of profile.steps) {
228    if (step.layer !== 'config') continue
229    const item = plan.find(p => p.id === `config:${step.key}`)!
230    if (item.status === 'same') continue
231    if (item.status !== 'change') {
232      refused.push({ label: item.label, reason: SKIP[item.status] })
233      continue
234    }
235    if (!isSynced) {
236      refused.push({ label: item.label, reason: NOT_RELOADED })
237      continue
238    }
239    const row = snap.rows.find(r => r.key === step.key)!
240    const r = await $.config.set({ key: step.key, value: step.value }).catch(err => ({ deny: message(err) }))
241    if (r.deny !== undefined) refused.push({ label: item.label, reason: r.deny })
242    else entries.push({ layer: 'config', key: step.key, label: row.label, previous: restorable(row) })
243  }
244
245  // effort is saved per model, under the id the next request names
246  const effortStep = profile.steps.find(s => s.layer === 'effort')
247  const effortItem = plan.find(p => p.id === 'effort')
248  const effort: Effort | undefined = effortStep && effortItem?.status === 'change' ? effortStep.value : undefined
249
250  const backup: Backup = {
251    id: `${Date.now()}`,
252    profile: profile.id,
253    name: profile.name,
254    at: new Date().toISOString(),
255    entries,
256    refused,
257    ...(effort ? { effort } : {}),
258  }
259  if (entries.length > 0 || effort) await saveHistory($, [backup, ...(await read($, history))].slice(0, KEEP))
260  if (effort) await setPending($, { value: effort, backupId: backup.id })
261  await refresh($)
262  return backup
263}
264
265async function undoLast($: Host): Promise<{ text: string; tone: 'success' | 'warning' | 'error' }> {
266  const [last, ...rest] = await read($, history)
267  if (!last) return { text: 'Nothing to undo', tone: 'warning' }
268  // an effort not yet saved for any model is simply dropped
269  if ((await read($, pending))?.backupId === last.id) await setPending($, null)
270  const failed: string[] = []
271  let isSynced = true
272  // settings.json first, as when applying
273  const fileEntries = last.entries.filter(e => e.layer !== 'config')
274  if (fileEntries.length > 0) {
275    const path = await settingsPath($)
276    const { user, text, error } = await readUser($, path)
277    if (error !== null || path === null) failed.push(`settings.json (${error ?? 'not found'})`)
278    else
279      isSynced = await writeUser($, path, text, revertSettings(user, fileEntries), fileEntries.map(f => f.path)).catch(err => {
280        failed.push(`settings.json (${message(err)})`)
281        return true
282      })
283  }
284  for (const entry of [...last.entries].reverse()) {
285    if (entry.layer !== 'config') continue
286    if (!isSynced) {
287      failed.push(`${entry.label} (${NOT_RELOADED})`)
288      continue
289    }
290    const r = await $.config.set({ key: entry.key, value: entry.previous }).catch(err => ({ deny: message(err) }))
291    if (r.deny !== undefined) failed.push(`${entry.label} (${r.deny})`)
292  }
293  // a settings.json that could not be restored keeps the entry, so undo can be tried again
294  const fileFailed = failed.some(f => f.startsWith('settings.json'))
295  if (!fileFailed) await saveHistory($, rest)
296  await refresh($)
297  if (fileFailed) return { text: `Not undone: ${failed.join(', ')}. Fix it and run /ultra-config undo again`, tone: 'error' }
298  return failed.length === 0
299    ? { text: `Undid ${last.name}: ${last.entries.length} setting${last.entries.length === 1 ? '' : 's'} restored`, tone: 'success' }
300    : { text: `Undid ${last.name}, except ${failed.join(', ')}`, tone: 'warning' }
301}
302
303const SKIP: Record<'locked' | 'missing' | 'invalid', string> = {
304  locked: 'locked by your organization',
305  missing: 'not in this Claude Code version',
306  invalid: 'value not offered here',
307}
308
309const summary = (b: Backup): string => {
310  const n = b.entries.length + (b.effort ? 1 : 0)
311  const head = n === 0 ? `${b.name}: already in place` : `${b.name} activated: ${n} setting${n === 1 ? '' : 's'} changed`
312  const tail = b.effort ? ` (effort ${b.effort} from your next message)` : ''
313  return b.refused.length === 0 ? `${head}${tail}` : `${head}${tail}, ${b.refused.length} skipped`
314}
315
316function planText(profile: Profile, plan: PlanItem[]): string {
317  const lines = [`${profile.icon} ${profile.name}: ${profile.tagline}`]
318  for (const p of plan) {
319    const mark = p.status === 'change' ? '●' : p.status === 'same' ? '✓' : '⊘'
320    const tail = p.status === 'change' ? `${p.from} → ${p.to}  (${p.why})` : p.status === 'same' ? p.to : SKIP[p.status]
321    lines.push(`  ${mark} ${p.label}: ${tail}`)
322  }
323  lines.push(`/ultra-config apply ${profile.id} to apply`)
324  return lines.join('\n')
325}
326
327export const register: Register = (on, options) => {
328  const columns = paneColumns(options.columns)
329
330  on('session.start', async ($, e, next) => {
331    const r = await next(e)
332    await $.command
333      .register({
334        name: COMMAND,
335        description: 'Setting profiles: token saver, fast flow, max quality, guarded, autopilot',
336        argumentHint: '[profile | apply <profile> | undo | close]',
337      })
338      .catch(err => $.ui.log(`ultra-config: /${COMMAND} not registered: ${message(err)}`))
339    const stored = await $.store.get(STORE_HISTORY).catch(() => undefined)
340    if (Array.isArray(stored)) await update($, history, () => stored as Backup[])
341    const want = await $.store.get(STORE_PENDING).catch(() => undefined)
342    if (want && typeof want === 'object') await update($, pending, () => want as UltraConfigPending)
343    return r
344  })
345
346  // The main loop's requests: learn the model id and effort in use, and save
347  // an activated effort under that id (and send it with this request).
348  on('turn.step', async function* ($, e, next) {
349    if (e.agentId) return yield* next(e)
350    const want = await read($, pending)
351    if (!want) {
352      await update($, live, () => ({ model: e.model, effort: e.effort }))
353      return yield* next(e)
354    }
355    await setPending($, null)
356    await saveEffort($, e.model, want).catch(err => {
357      $.ui.log(`ultra-config: effort not saved: ${message(err)}`)
358      return false
359    })
360    await update($, live, () => ({ model: e.model, effort: want.value }))
361    await refresh($).catch(() => undefined)
362    return yield* next({ ...e, effort: want.value })
363  })
364
365  // keep the cards in step with changes made in /config itself
366  on('config.set', async ($, e, next) => {
367    const r = await next(e)
368    if (r.deny === undefined) await refresh($).catch(() => undefined)
369    return r
370  }).catch(($, e, next) => next(e))
371
372  on('command.run', { command: COMMAND }, async ($, e) => {
373    const [verb = '', arg = ''] = e.args.trim().split(/\s+/)
374    const word = verb.toLowerCase()
375    if (word === 'close' || word === 'stop') {
376      await $.ui.close({ id: PANE }).catch(() => undefined)
377      return { text: 'ultra-config closed' }
378    }
379    if (word === 'undo') {
380      const { text, tone } = await undoLast($)
381      await notify($, text, tone)
382      return { text }
383    }
384    if (word === 'apply') {
385      const profile = profileById(arg)
386      if (!profile) return { text: `Unknown profile "${arg}". Profiles: ${PROFILES.map(p => p.id).join(', ')}` }
387      const b = await applyProfile($, profile)
388      const text = summary(b)
389      await notify($, text, b.refused.length === 0 ? 'success' : 'warning')
390      const skipped = b.refused.map(x => `\n  ⊘ ${x.label}: ${x.reason}`).join('')
391      return { text: `${text}${skipped}\n/ultra-config undo reverts it` }
392    }
393    const profile = word === '' ? undefined : profileById(word)
394    if (word !== '' && !profile)
395      return { text: `Unknown argument "${word}". Try a profile (${PROFILES.map(p => p.id).join(', ')}), apply, undo or close.` }
396    const snap = await refresh($)
397    await update($, ui, s => ({
398      ...s,
399      selected: profile ? profile.id : s.selected,
400      notice: null,
401    }))
402    let placed = true
403    try {
404      placed = (await $.ui.open({ id: PANE, title: 'ultra config', focus: true, columns })).isPlaced
405    } catch (err) {
406      placed = false
407      $.ui.log(`ultra-config: pane not opened: ${message(err)}`)
408    }
409    if (profile) {
410      const plan = planFor(profile, snap.rows, snap.user, await liveNow($))
411      if (!placed) return { text: planText(profile, plan) }
412      const n = plan.filter(p => p.status === 'change').length
413      return { text: `${profile.icon} ${profile.name}: ${n} change${n === 1 ? '' : 's'}, shown in the pane · /ultra-config apply ${profile.id}` }
414    }
415    if (!placed) return { text: PROFILES.map(p => `${p.icon} ${p.id}: ${p.tagline}`).join('\n') }
416    // a Desktop session reports isFullscreen false too, so the reply names no layout
417    return { text: 'ultra-config opened · press a profile to see what it sets · /ultra-config close' }
418  })
419
420  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
421    const { Box, Text, Button } = $.ui.resolve(e)
422    const W = Math.max(40, (e.props.bodyColumns ?? columns) - 1)
423    const state = await read($, ui)
424    const snap = await read($, snapshot)
425    const now = await liveNow($)
426    const set = (patch: Partial<UltraConfigUi>) => update($, ui, s => ({ ...s, ...patch }))
427
428    const effort = currentEffort(snap.user, now)
429    const model = snap.rows.find(r => r.key === 'model')
430    const mode = snap.rows.find(r => r.key === 'permissionMode')
431    const facts = [show(model?.value).replace(/ \(.*\)$/, '').toLowerCase(), `${effort === undefined ? 'default' : show(effort)} effort`, `${show(mode?.value)} mode`]
432
433    // one profile is always open; the first until another is pressed
434    const open = (state.selected ? profileById(state.selected) : undefined) ?? PROFILES[0]!
435    const labelW = Math.min(27, Math.max(14, Math.floor(W * 0.44)))
436
437    const activate = async (p: (typeof PROFILES)[number]) => {
438      if ((await read($, ui)).isBusy) return
439      await set({ isBusy: true })
440      try {
441        const b = await applyProfile($, p)
442        await set({ notice: { text: `${summary(b)} · /ultra-config undo reverts it`, tone: b.refused.length === 0 ? 'success' : 'warning' } })
443        $.ui.toast(`${p.icon} ${summary(b)}`)
444      } catch (err) {
445        await set({ notice: { text: `Not activated: ${message(err)}`, tone: 'error' } })
446      } finally {
447        await set({ isBusy: false })
448      }
449    }
450
451    const detail = (p: (typeof PROFILES)[number]) => {
452      const plan = planFor(p, snap.rows, snap.user, now)
453      const changes = plan.filter(x => x.status === 'change').length
454      // every step, so Activate never applies one that was not shown; the pane scrolls
455      const lines = []
456      for (const x of plan) {
457        if (x.status === 'change')
458          lines.push(
459            <Box key={`chg:${x.id}`} flexDirection="column">
460              <Text wrap="truncate-end">
461                <Text color={p.color}>● </Text>
462                <Text>{pad(x.label, labelW)}</Text>
463                {/* drop the menu hint, e.g. "Opus (default)" -> "Opus", so the old -> new row fits a narrow pane */}
464                <Text dimColor strikethrough>{x.from.replace(/ \(.*\)$/, '')}</Text>
465                <Text dimColor>{nb(' → ')}</Text>
466                <Text color={p.color} bold>{x.to}</Text>
467              </Text>
468              <Text color="#8a8a8a" wrap="truncate-end">{nb(`  ${x.why}`)}</Text>
469            </Box>,
470          )
471        else if (x.status === 'same')
472          lines.push(
473            <Text key={`same:${x.id}`} wrap="truncate-end">
474              <Text color="success">✓ </Text>
475              <Text dimColor>{pad(x.label, labelW)}{x.to}</Text>
476            </Text>,
477          )
478        else
479          lines.push(
480            <Text key={`skip:${x.id}`} color="warning" wrap="truncate-end">
481              ⊘ <Text dimColor>{`${x.label}: ${SKIP[x.status]}`}</Text>
482            </Text>,
483          )
484      }
485      return (
486        <Box key={`detail:${p.id}`} flexDirection="column" borderStyle="round" borderColor={p.color} paddingX={1} width={W}>
487          {lines}
488          <Box flexDirection="row" marginTop={1}>
489            {changes > 0 ? (
490              <Button
491                key={`activate:${p.id}`}
492                variant="primary"
493                hotkey="a"
494                autoFocus
495                label={state.isBusy ? 'Activating…' : `Activate ${p.name}`}
496                onPress={() => activate(p)}
497              />
498            ) : (
499              <Text color="success" bold>✓ Active: nothing to change</Text>
500            )}
501          </Box>
502        </Box>
503      )
504    }
505
506    const cards = PROFILES.map(p => {
507      const plan = planFor(p, snap.rows, snap.user, now)
508      const { same, of } = matchOf(plan)
509      const isActive = of > 0 && same === of
510      const isOpen = p.id === open.id
511      const meter = 10
512      const lit = of === 0 ? 0 : Math.round((same / of) * meter)
513      return (
514        <Box key={`card:${p.id}`} flexDirection="column" width={W}>
515          <Box flexDirection="row" justifyContent="space-between" width={W}>
516            <Box flexDirection="row">
517              <Text>
518                <Text color={isOpen ? p.color : '#5a5a5a'}>{isOpen ? '▾ ' : '▸ '}</Text>
519                <Text backgroundColor={p.color} color={INK} bold>{nb(` ${p.icon} `)}</Text>
520                <Text>{'\u00a0'}</Text>
521              </Text>
522              {/* a label string, not rich children: Claude Code 2.1.287 (and the Desktop build) skips the whole render on those */}
523              <Button key={`pick:${p.id}`} plain dimColor={!isOpen} label={p.name} onPress={() => set({ selected: p.id, notice: null })} />
524            </Box>
525            <Text>
526              <Text color={p.color}>{'■'.repeat(lit)}</Text>
527              <Text color="#3a3a3a">{'■'.repeat(meter - lit)}</Text>
528              <Text color={isActive ? 'success' : undefined} dimColor={!isActive}>
529                {nb(isActive ? '  ✓ active' : ` ${String(same).padStart(2)}/${of}`.padEnd(10))}
530              </Text>
531            </Text>
532          </Box>
533          <Text dimColor wrap="truncate-end">{nb(`      ${p.tagline}`)}</Text>
534          {isOpen && detail(p)}
535        </Box>
536      )
537    })
538
539    return (
540      <Box flexDirection="column" width={W} gap={1}>
541        <Box key="head" flexDirection="column" width={W}>
542          <Box flexDirection="row" justifyContent="space-between" width={W}>
543            <Text>
544              <Text backgroundColor={ACCENT} color={INK} bold>{nb(' ⚙ ULTRA ')}</Text>
545              <Text backgroundColor="#3a2a24" color="#f4ece6" bold>{nb(' CONFIG ')}</Text>
546            </Text>
547            <Text dimColor>{nb(facts.join(' · '))}</Text>
548          </Box>
549          <Text color="#3a3a3a">{'─'.repeat(W)}</Text>
550          {state.notice && (
551            <Text color={state.notice.tone}>{state.notice.tone === 'success' ? '✓ ' : '! '}{state.notice.text}</Text>
552          )}
553          {snap.error && <Text color="warning">! {snap.error}: effort, env and deny rules are left alone</Text>}
554        </Box>
555        {cards}
556      </Box>
557    )
558  })
559}
560
hooks/profiles.ts 372 lines
1/**
2 * ultra-config: the profiles and the pure logic that turns a profile into a
3 * plan, applies it to a settings object and undoes it.
4 * Nothing here calls `$`, so every rule is unit-tested in tests/.
5 *
6 * A profile changes two layers:
7 *   - `config`: a row of the `/config` menu, written through `$.config.set`
8 *     exactly as if the person changed it there (the engine picks the file).
9 *   - `settings` / `list`: keys of ~/.claude/settings.json that have no
10 *     `/config` row (effortLevel, env, permissions), read-modified-written.
11 */
12import type { ConfigRow, ConfigValue } from 'claude-code'
13
14/** The part of a `/config` row the plan reads. */
15export type Row = Pick<ConfigRow, 'key' | 'label' | 'kind' | 'value' | 'options' | 'isLocked'>
16
17export type ConfigStep = { layer: 'config'; key: string; value: ConfigValue; why: string }
18/** `value: null` removes the key. `path` is dotted: `env.MAX_MCP_OUTPUT_TOKENS`. */
19export type SettingsStep = { layer: 'settings'; path: string; label: string; value: string | null; why: string }
20/** Adds entries to a list (`permissions.deny`), leaving what is there. */
21export type ListStep = { layer: 'list'; path: string; label: string; add: string[]; why: string }
22export type Effort = 'low' | 'medium' | 'high' | 'xhigh'
23/**
24 * The effort for the model in use. Since Claude Code 2.1.251 effort is saved
25 * per model (`modelSettings[<model id>].effortLevel`): a top-level
26 * `effortLevel` in user settings is ignored by Opus 5.5 and later models. The
27 * model id is only known at the next request, so this step is written then.
28 */
29export type EffortStep = { layer: 'effort'; value: Effort; why: string }
30export type Step = ConfigStep | SettingsStep | ListStep | EffortStep
31
32export type ProfileId = 'saver' | 'flow' | 'quality' | 'guarded' | 'autopilot'
33
34export type Profile = {
35  id: ProfileId
36  name: string
37  icon: string
38  color: string
39  tagline: string
40  steps: Step[]
41}
42
43const cfg = (key: string, value: ConfigValue, why: string): ConfigStep => ({ layer: 'config', key, value, why })
44const set = (path: string, label: string, value: string | null, why: string): SettingsStep => ({
45  layer: 'settings',
46  path,
47  label,
48  value,
49  why,
50})
51
52const effort = (value: Effort, why: string): EffortStep => ({ layer: 'effort', value, why })
53export const SUBAGENT = { path: 'env.CLAUDE_CODE_SUBAGENT_MODEL', label: 'Subagent model' } as const
54
55export const PROFILES: Profile[] = [
56  {
57    id: 'saver',
58    name: 'Token saver',
59    icon: '◔',
60    color: '#7fd18b',
61    tagline: 'Sonnet, low effort, short answers, Haiku subagents',
62    steps: [
63      cfg('model', 'sonnet', 'cheaper per token than Opus'),
64      effort('low', 'less thinking per request'),
65      set(SUBAGENT.path, SUBAGENT.label, 'haiku', 'subagents run on the cheapest model'),
66      cfg('fast', false, 'fast mode bills at a higher rate'),
67      cfg('outputStyle', 'Concise', 'shorter answers, fewer output tokens'),
68      cfg('promptSuggestionEnabled', false, 'each suggestion is an extra model call'),
69      cfg('recap', false, 'the recap is an extra model call'),
70      cfg('autoCompact', true, 'keeps the context from growing unbounded'),
71      cfg('workflowSizeGuideline', 'small', 'smaller multi-agent fan-outs'),
72      cfg('workflowKeywordTriggerEnabled', false, 'no accidental ultracode fan-outs'),
73      set('env.MAX_MCP_OUTPUT_TOKENS', 'MCP output cap', '10000', 'caps MCP tool results (default 25000)'),
74    ],
75  },
76  {
77    id: 'flow',
78    name: 'Fast flow',
79    icon: '»',
80    color: '#f2b84b',
81    tagline: 'Edits auto-accepted, medium effort, fewer interruptions',
82    steps: [
83      cfg('model', 'sonnet', 'faster answers than Opus'),
84      effort('medium', 'balanced reasoning time'),
85      cfg('permissionMode', 'acceptEdits', 'file edits go through without a prompt'),
86      cfg('outputStyle', 'Concise', 'less to read between steps'),
87      cfg('promptSuggestionEnabled', true, 'Tab accepts the next likely prompt'),
88      cfg('turnDuration', true, 'see how long each turn took'),
89      cfg('progressBar', true, 'progress shows in the terminal tab'),
90      cfg('workflowSizeGuideline', 'medium', 'parallel agents when they help'),
91    ],
92  },
93  {
94    id: 'quality',
95    name: 'Max quality',
96    icon: '◆',
97    color: '#b48cff',
98    tagline: 'Most capable model, deepest reasoning, big workflows',
99    steps: [
100      cfg('model', 'best', 'the most capable model available to you'),
101      effort('xhigh', 'the deepest reasoning per request'),
102      set(SUBAGENT.path, SUBAGENT.label, null, 'subagents keep the model they ask for'),
103      cfg('outputStyle', 'default', 'full explanations, nothing trimmed'),
104      cfg('workflows', true, 'multi-agent workflows available'),
105      cfg('workflowKeywordTriggerEnabled', true, '"ultracode" starts a workflow'),
106      cfg('workflowSizeGuideline', 'large', 'room for large fan-outs'),
107      cfg('checkpoints', true, 'rewind any change'),
108      cfg('autoCompact', true, 'long sessions keep going'),
109    ],
110  },
111  {
112    id: 'guarded',
113    name: 'Guarded',
114    icon: '▣',
115    color: '#6cb6ff',
116    tagline: 'You approve each change; no reading secrets; no bypass',
117    steps: [
118      cfg('permissionMode', 'default', 'every edit and command asks first'),
119      cfg('useAutoModeDuringPlan', false, 'plan mode never acts on its own'),
120      set('permissions.disableBypassPermissionsMode', 'Bypass mode', 'disable', 'bypassPermissions cannot be turned on'),
121      {
122        layer: 'list',
123        path: 'permissions.deny',
124        label: 'Deny rules',
125        add: ['Read(./.env)', 'Read(./.env.*)', 'Read(./secrets/**)', 'Read(~/.ssh/**)', 'Read(~/.aws/**)'],
126        why: 'the Read tool is blocked on .env, secrets/, ~/.ssh, ~/.aws',
127      },
128      cfg('checkpoints', true, 'every change can be rewound'),
129      cfg('verbose', true, 'see each tool call in full'),
130      cfg('workflowKeywordTriggerEnabled', false, 'no fan-out from a keyword'),
131    ],
132  },
133  {
134    id: 'autopilot',
135    name: 'Autopilot',
136    icon: '➤',
137    color: '#ff8f6b',
138    tagline: 'Long unattended runs: auto mode, recap, push alerts',
139    steps: [
140      cfg('permissionMode', 'auto', 'the classifier approves safe actions'),
141      cfg('useAutoModeDuringPlan', true, 'plans run without stopping'),
142      effort('high', 'fewer wrong turns on long runs'),
143      cfg('workflows', true, 'parallel agents for big tasks'),
144      cfg('recap', true, 'a summary waits for you when you return'),
145      cfg('agentPushNotifEnabled', true, 'a push when Claude decides something'),
146      cfg('inputNeededNotifEnabled', true, 'a push when it needs you'),
147      cfg('autoCompact', true, 'the run never stops on a full context'),
148    ],
149  },
150]
151
152export const profileById = (id: string): Profile | undefined =>
153  PROFILES.find(p => p.id === id.trim().toLowerCase())
154
155// ---------------------------------------------------------------- planning
156
157export type Status = 'change' | 'same' | 'locked' | 'missing' | 'invalid'
158
159export type PlanItem = {
160  id: string
161  layer: Step['layer']
162  label: string
163  from: string
164  to: string
165  why: string
166  status: Status
167}
168
169export type Json = Record<string, unknown>
170
171/** A row's value as the menu shows it equals the value a profile sets. */
172export function sameValue(current: ConfigValue | undefined, wanted: ConfigValue): boolean {
173  if (current === wanted) return true
174  if (typeof current !== 'string' || typeof wanted !== 'string') return false
175  const c = current.toLowerCase()
176  const w = wanted.toLowerCase()
177  // the menu shows `Default (recommended)` for the value `default`
178  return c === w || c.startsWith(`${w} (`)
179}
180
181export const show = (v: unknown): string => {
182  if (v === undefined || v === null || v === '') return 'unset'
183  if (v === true) return 'on'
184  if (v === false) return 'off'
185  if (Array.isArray(v)) return v.length === 0 ? 'none' : `${v.length} rules`
186  return String(v)
187}
188
189export function getPath(obj: Json, path: string): unknown {
190  let cur: unknown = obj
191  for (const part of path.split('.')) {
192    if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) return undefined
193    cur = (cur as Json)[part]
194  }
195  return cur
196}
197
198/** A copy of `obj` with `path` set, or removed when `value` is undefined; empty parents are dropped. */
199export function setPath(obj: Json, path: string, value: unknown): Json {
200  const [head, ...rest] = path.split('.')
201  const out: Json = { ...obj }
202  if (rest.length === 0) {
203    if (value === undefined) delete out[head!]
204    else out[head!] = value
205    return out
206  }
207  const child = out[head!]
208  const base = child !== null && typeof child === 'object' && !Array.isArray(child) ? (child as Json) : {}
209  const next = setPath(base, rest.join('.'), value)
210  if (Object.keys(next).length === 0) delete out[head!]
211  else out[head!] = next
212  return out
213}
214
215const listAt = (obj: Json, path: string): string[] => {
216  const v = getPath(obj, path)
217  return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
218}
219
220/**
221 * What the engine last sent (the main loop's model id and effort, from
222 * `turn.step`), and an activated effort still waiting for the next request.
223 */
224export type Live = { model?: string; effort?: string | number; pending?: string }
225
226/**
227 * Where a model's saved effort lives in settings.json. Undefined for an id with
228 * a dot (a Bedrock or Vertex id), which a dotted path cannot name.
229 */
230export const effortPath = (model: string): string | undefined =>
231  model.includes('.') || model.length === 0 ? undefined : `modelSettings.${model}.effortLevel`
232
233/** The effort the model in use runs at: its saved level, else what the last request carried. */
234export function currentEffort(user: Json, live: Live): string | undefined {
235  if (live.pending !== undefined) return live.pending
236  const path = live.model ? effortPath(live.model) : undefined
237  const saved = path ? getPath(user, path) : undefined
238  if (typeof saved === 'string') return saved
239  return live.effort === undefined ? undefined : String(live.effort)
240}
241
242export function planFor(profile: Profile, rows: readonly Row[], user: Json, live: Live = {}): PlanItem[] {
243  const modelStep = profile.steps.find((s): s is ConfigStep => s.layer === 'config' && s.key === 'model')
244  const modelRow = rows.find(r => r.key === 'model')
245  const switchesModel = modelStep !== undefined && modelRow !== undefined && !sameValue(modelRow.value, modelStep.value)
246  return profile.steps.map(step => {
247    if (step.layer === 'effort') {
248      const current = live.pending ?? (switchesModel ? undefined : currentEffort(user, live))
249      return {
250        id: 'effort',
251        layer: step.layer,
252        label: 'Effort',
253        from: current ?? 'default',
254        to: step.value,
255        why: `${step.why}; saved for the model in use at your next message`,
256        status: current === step.value ? ('same' as const) : ('change' as const),
257      }
258    }
259    if (step.layer === 'config') {
260      const row = rows.find(r => r.key === step.key)
261      const base = { id: `config:${step.key}`, layer: step.layer, why: step.why, to: show(step.value) }
262      if (!row) return { ...base, label: step.key, from: '—', status: 'missing' as const }
263      const item = { ...base, label: row.label, from: show(row.value) }
264      if (row.isLocked) return { ...item, status: 'locked' as const }
265      if (row.kind === 'choice' && typeof step.value === 'string' && row.options && !row.options.includes(step.value))
266        return { ...item, status: 'invalid' as const }
267      return { ...item, status: sameValue(row.value, step.value) ? ('same' as const) : ('change' as const) }
268    }
269    if (step.layer === 'settings') {
270      const current = getPath(user, step.path)
271      const same = (current ?? null) === step.value
272      return {
273        id: `settings:${step.path}`,
274        layer: step.layer,
275        label: step.label,
276        from: show(current),
277        to: show(step.value),
278        why: step.why,
279        status: same ? ('same' as const) : ('change' as const),
280      }
281    }
282    const have = listAt(user, step.path)
283    const missing = step.add.filter(x => !have.includes(x))
284    return {
285      id: `list:${step.path}`,
286      layer: step.layer,
287      label: step.label,
288      from: show(have),
289      to: missing.length === 0 ? show(have) : `+${missing.length} rules`,
290      why: step.why,
291      status: missing.length === 0 ? ('same' as const) : ('change' as const),
292    }
293  })
294}
295
296/** How much of a profile is already in force: applicable steps that match, of those that apply. */
297export function matchOf(plan: PlanItem[]): { same: number; of: number } {
298  const applicable = plan.filter(p => p.status === 'same' || p.status === 'change')
299  return { same: applicable.filter(p => p.status === 'same').length, of: applicable.length }
300}
301
302/** A row's value as `$.config.set` takes it back: a choice's option, not the menu's label for it. */
303export function restorable(row: Row): ConfigValue {
304  if (row.kind === 'choice' && row.options) return row.options.find(o => sameValue(row.value, o)) ?? row.value
305  return row.value
306}
307
308// ---------------------------------------------------------------- applying and undoing
309
310export type BackupEntry =
311  | { layer: 'config'; key: string; label: string; previous: ConfigValue }
312  | { layer: 'settings'; path: string; label: string; previous: unknown; had: boolean }
313  | { layer: 'list'; path: string; label: string; added: string[] }
314
315/** Applies the profile's settings and list steps to the settings object; returns the new object and what to undo. */
316export function applySettings(user: Json, steps: readonly (SettingsStep | ListStep)[]): { next: Json; entries: BackupEntry[] } {
317  let next = user
318  const entries: BackupEntry[] = []
319  for (const step of steps) {
320    if (step.layer === 'settings') {
321      const had = getPath(next, step.path) !== undefined
322      const previous = getPath(next, step.path)
323      if ((previous ?? null) === step.value) continue
324      next = setPath(next, step.path, step.value ?? undefined)
325      entries.push({ layer: 'settings', path: step.path, label: step.label, previous: had ? previous : null, had })
326    } else if (step.layer === 'list') {
327      const have = listAt(next, step.path)
328      const added = step.add.filter(x => !have.includes(x))
329      if (added.length === 0) continue
330      next = setPath(next, step.path, [...have, ...added])
331      entries.push({ layer: 'list', path: step.path, label: step.label, added })
332    }
333  }
334  return { next, entries }
335}
336
337/** Puts back what `applySettings` changed; a key the person changed since is restored too. */
338export function revertSettings(user: Json, entries: BackupEntry[]): Json {
339  let next = user
340  for (const entry of [...entries].reverse()) {
341    if (entry.layer === 'settings') next = setPath(next, entry.path, entry.had ? entry.previous : undefined)
342    else if (entry.layer === 'list') {
343      const kept = listAt(next, entry.path).filter(x => !entry.added.includes(x))
344      next = setPath(next, entry.path, kept.length === 0 ? undefined : kept)
345    }
346  }
347  return next
348}
349
350/** Whether the settings the engine has loaded hold `next`'s value at every path. */
351export function inSync(loaded: Json, next: Json, paths: string[]): boolean {
352  return paths.every(path => JSON.stringify(getPath(loaded, path) ?? null) === JSON.stringify(getPath(next, path) ?? null))
353}
354
355/** Where the user's settings file lives: `CLAUDE_CONFIG_DIR`, else `~/.claude`. */
356export function userSettingsPath(configDir: string | undefined, home: string | undefined): string | undefined {
357  const dir = configDir && configDir.length > 0 ? configDir : home ? `${home.replace(/[\\/]+$/, '')}/.claude` : undefined
358  return dir === undefined ? undefined : `${dir.replace(/[\\/]+$/, '')}/settings.json`
359}
360
361/** Pads with non-breaking spaces so Desktop keeps the width (plain spaces collapse there). */
362export const pad = (s: string, n: number): string => {
363  const cut = [...s]
364  return cut.length >= n ? `${cut.slice(0, Math.max(0, n - 2)).join('')}…\u00a0` : s + '\u00a0'.repeat(n - cut.length)
365}
366export const nb = (s: string): string => s.replace(/ /g, '\u00a0')
367
368export const paneColumns = (v: unknown): number => {
369  const n = typeof v === 'number' && Number.isFinite(v) ? Math.round(v) : 62
370  return Math.min(100, Math.max(44, n))
371}
372
types/index.d.ts 67 lines
1// The contract of the ultra-config plugin: the state its pane draws from.
2
3/** One `/config` row as `$.config.list()` returns it (ConfigRow in the API). */
4export type UltraConfigRow = {
5  key: string
6  label: string
7  description?: string
8  kind: 'boolean' | 'choice' | 'text' | 'number'
9  value: boolean | string | number | readonly string[]
10  options?: readonly string[]
11  isLocked: boolean
12}
13
14/** What the pane knows about the person's configuration, read on open and after every change. */
15export type UltraConfigSnapshot = {
16  rows: UltraConfigRow[]
17  /** ~/.claude/settings.json as parsed; `{}` when there is none. */
18  user: Record<string, unknown>
19  /** Where that file is; null when neither CLAUDE_CONFIG_DIR nor a home directory is known. */
20  path: string | null
21  /** Why the file could not be read (bad JSON): settings-layer steps are then skipped. */
22  error: string | null
23}
24
25export type UltraConfigEntry =
26  | { layer: 'config'; key: string; label: string; previous: string | number | boolean | readonly string[] }
27  | { layer: 'settings'; path: string; label: string; previous: unknown; had: boolean }
28  | { layer: 'list'; path: string; label: string; added: string[] }
29
30/** One applied profile, kept so it can be undone. */
31export type UltraConfigBackup = {
32  id: string
33  profile: 'saver' | 'flow' | 'quality' | 'guarded' | 'autopilot'
34  name: string
35  at: string
36  entries: UltraConfigEntry[]
37  refused: { label: string; reason: string }[]
38  /** The effort the profile set, saved per model at the next request. */
39  effort?: 'low' | 'medium' | 'high' | 'xhigh'
40}
41
42/** The main loop's model id and effort, as the last request carried them. */
43export type UltraConfigLive = { model?: string; effort?: string | number }
44
45/** An effort waiting for the next request to name the model it is saved under. */
46export type UltraConfigPending = { value: 'low' | 'medium' | 'high' | 'xhigh'; backupId: string }
47
48export type UltraConfigUi = {
49  /** The profile whose detail is open; null means the first. */
50  selected: string | null
51  /** A one-line result under the header (applied, undone, refused). */
52  notice: { text: string; tone: 'success' | 'warning' | 'error' } | null
53  isBusy: boolean
54}
55
56declare module 'claude-code' {
57  interface PluginState {
58    'ultra-config': {
59      ui: UltraConfigUi
60      snapshot: UltraConfigSnapshot
61      history: UltraConfigBackup[]
62      live: UltraConfigLive
63      pending: UltraConfigPending | null
64    }
65  }
66}
67