patch-cc 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {patch_cc-0.1.3 → patch_cc-0.1.4}/PKG-INFO +8 -6
  2. {patch_cc-0.1.3 → patch_cc-0.1.4}/README.md +7 -5
  3. {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/CONDUCT.md +5 -3
  4. {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/PLAYBOOK.md +3 -2
  5. {patch_cc-0.1.3 → patch_cc-0.1.4}/pyproject.toml +1 -1
  6. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/__init__.py +1 -1
  7. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/cache.py +7 -5
  8. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/cli.py +306 -34
  9. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/menu.py +10 -1
  10. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/__init__.py +1 -2
  11. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/agents.py +1 -0
  12. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/base.py +5 -1
  13. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/chrome.py +2 -0
  14. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/streaming.py +2 -2
  15. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/thinking.py +2 -2
  16. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/ui.py +17 -1
  17. {patch_cc-0.1.3 → patch_cc-0.1.4}/uv.lock +1 -1
  18. {patch_cc-0.1.3 → patch_cc-0.1.4}/.github/workflows/ci.yml +0 -0
  19. {patch_cc-0.1.3 → patch_cc-0.1.4}/.github/workflows/release.yml +0 -0
  20. {patch_cc-0.1.3 → patch_cc-0.1.4}/.gitignore +0 -0
  21. {patch_cc-0.1.3 → patch_cc-0.1.4}/LICENSE +0 -0
  22. {patch_cc-0.1.3 → patch_cc-0.1.4}/docs/INTERNALS.md +0 -0
  23. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/__init__.py +0 -0
  24. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/blob.py +0 -0
  25. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/container.py +0 -0
  26. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/elf.py +0 -0
  27. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/errors.py +0 -0
  28. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/bun/macho.py +0 -0
  29. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/doctor.py +0 -0
  30. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/locate.py +0 -0
  31. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patcher.py +0 -0
  32. {patch_cc-0.1.3 → patch_cc-0.1.4}/src/patch_cc/patches/output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patch-cc
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Interactive patcher for the Claude Code native binary
5
5
  Project-URL: Homepage, https://github.com/anfreire/patch-cc
6
6
  Project-URL: Repository, https://github.com/anfreire/patch-cc
@@ -76,9 +76,9 @@ patch-cc # then just run it
76
76
  |---|---|---|
77
77
  | Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
78
78
  | | Colour new files as diffs | Created files render with `+` lines and green |
79
- | Thinking | Opt out of server-side experiments | Some accounts land in one that empties every thinking block |
79
+ | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
80
80
  | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
81
- | Live thinking | Stream thinking live | See reasoning as it is generated, inline and in order |
81
+ | | Stream thinking live | See reasoning as it is generated, inline and in order |
82
82
  | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
83
83
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
84
84
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
@@ -97,10 +97,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
97
97
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
98
98
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
99
99
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
100
+ uvx patch-cc apply --from-cache # replay your last remembered selection
100
101
  uvx patch-cc status # exactly what is applied
101
102
  uvx patch-cc doctor # do all patches match this build?
102
103
  uvx patch-cc doctor path/to/claude # ...or match some other binary
103
- uvx patch-cc list # patches + your binary's agents/models
104
+ uvx patch-cc list # every patch, described
104
105
  uvx patch-cc restore # put the original back
105
106
  ```
106
107
 
@@ -110,8 +111,9 @@ against what your installed binary actually ships.
110
111
  ## After a Claude update
111
112
 
112
113
  Claude auto-updates roughly daily and replaces the binary, which reverts the
113
- patch. Re-run `patch-cc` — the menu remembers your last selection — or re-apply
114
- your set explicitly:
114
+ patch. Re-run `patch-cc` — the menu remembers your last selection — replay it
115
+ without the menu via `patch-cc apply --from-cache`, or re-apply your set
116
+ explicitly:
115
117
 
116
118
  ```bash
117
119
  uvx patch-cc apply --brand --model Explore=haiku
@@ -48,9 +48,9 @@ patch-cc # then just run it
48
48
  |---|---|---|
49
49
  | Output & diffs | Detailed tool calls | Show full read/search calls, not collapsed summaries |
50
50
  | | Colour new files as diffs | Created files render with `+` lines and green |
51
- | Thinking | Opt out of server-side experiments | Some accounts land in one that empties every thinking block |
51
+ | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
52
52
  | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
53
- | Live thinking | Stream thinking live | See reasoning as it is generated, inline and in order |
53
+ | | Stream thinking live | See reasoning as it is generated, inline and in order |
54
54
  | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
55
55
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
56
56
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
@@ -69,10 +69,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
69
69
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
70
70
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
71
71
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
72
+ uvx patch-cc apply --from-cache # replay your last remembered selection
72
73
  uvx patch-cc status # exactly what is applied
73
74
  uvx patch-cc doctor # do all patches match this build?
74
75
  uvx patch-cc doctor path/to/claude # ...or match some other binary
75
- uvx patch-cc list # patches + your binary's agents/models
76
+ uvx patch-cc list # every patch, described
76
77
  uvx patch-cc restore # put the original back
77
78
  ```
78
79
 
@@ -82,8 +83,9 @@ against what your installed binary actually ships.
82
83
  ## After a Claude update
83
84
 
84
85
  Claude auto-updates roughly daily and replaces the binary, which reverts the
85
- patch. Re-run `patch-cc` — the menu remembers your last selection — or re-apply
86
- your set explicitly:
86
+ patch. Re-run `patch-cc` — the menu remembers your last selection — replay it
87
+ without the menu via `patch-cc apply --from-cache`, or re-apply your set
88
+ explicitly:
87
89
 
88
90
  ```bash
89
91
  uvx patch-cc apply --brand --model Explore=haiku
@@ -29,9 +29,11 @@ shape that already absorbs every case, found rather than bolted on.
29
29
  brick.
30
30
 
31
31
  - **Explicit invocations are hermetic.** A non-interactive command's arguments
32
- are its whole input; no saved state may silently change what it does, so the
33
- same command always yields the same result. Persisted choices belong to the
34
- interactive UI alone they pre-fill a prompt, never trigger an action.
32
+ are its whole input; no saved state may *silently* change what it does.
33
+ Persisted choices pre-fill the interactive UI and never act on their own — the
34
+ lone exception is `apply --from-cache`, which takes the cache as its *named*
35
+ input, so the state it reads is declared in the arguments, not hidden behind a
36
+ default. Absent that flag, the same command still always yields the same result.
35
37
 
36
38
  - **Anchor matchers on meaning.** String literals, `case` labels, prop names,
37
39
  control-flow shape — never a minified local that changes every build. A new
@@ -223,8 +223,9 @@ that broke a patch along with every note.
223
223
 
224
224
  ## Patch reference
225
225
 
226
- Grouped as in `patch-cc list`. Each entry: what it changes, the stable anchor,
227
- and where it lives.
226
+ Grouped by source module the UI folds `live-thinking` into the Thinking group,
227
+ but it still lives in `streaming.py`. Each entry: what it changes, the stable
228
+ anchor, and where it lives.
228
229
 
229
230
  ### Output & diffs — `output.py`
230
231
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patch-cc"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Interactive patcher for the Claude Code native binary"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.4"
@@ -1,10 +1,12 @@
1
1
  """Remembered interactive selection.
2
2
 
3
- The menu's only memory: the patches and customisations picked last time, so the
4
- next interactive run comes up pre-filled even after a Claude auto-update wiped
5
- the patched binary (and its manifest) away. It is never read by the ``apply``
6
- args path and never applies anything on its own -- deleting the file simply
7
- resets the menu to defaults.
3
+ The last selection made, so the next interactive run comes up pre-filled even
4
+ after a Claude auto-update wiped the patched binary (and its manifest) away.
5
+ Written by the interactive menu and by any ``apply`` given an explicit selection
6
+ -- a bare ``apply`` (the default set) leaves it untouched, so it never clobbers a
7
+ remembered custom pick. ``apply --from-cache`` is the non-interactive reader,
8
+ replaying that selection when explicitly asked. Deleting the file resets the
9
+ menu to defaults and leaves ``--from-cache`` with nothing to replay.
8
10
  """
9
11
 
10
12
  from __future__ import annotations
@@ -25,16 +25,64 @@ from pathlib import Path
25
25
 
26
26
  from rich.markup import escape
27
27
 
28
- from . import locate, patcher
28
+ from . import cache, locate, patcher
29
29
  from .bun import BunError, Bundle
30
- from .patches import Options, Outcome, by_group, default_ids, derived_brand, ids
30
+ from .patches import (
31
+ DEFAULT_SUFFIX,
32
+ GROUP_ORDER,
33
+ Options,
34
+ Outcome,
35
+ Patch,
36
+ by_group,
37
+ default_ids,
38
+ derived_brand,
39
+ ids,
40
+ )
31
41
  from .patches.agents import INHERIT, discover_agents, discover_models
32
- from .ui import MARKS, console, err, findings, heading, ok, warn
42
+ from .ui import MARKS, applied_value, console, err, findings, heading, ok, warn
33
43
 
34
44
  #: ``--brand`` with no value: derive the name from the system username.
35
45
  _DERIVE = ""
36
46
 
37
47
 
48
+ def _enable_hint(patch: Patch) -> str:
49
+ """How a patch is turned on: blank for the default set, else how to opt in.
50
+
51
+ Reads the patch's own ``option``, so a non-default patch says exactly how it
52
+ is enabled -- ``subagent-models`` needs ``--model``, ``spinner-tips`` is just
53
+ named -- instead of the old blanket ``(via --model)`` that mislabelled every
54
+ off-by-default patch.
55
+ """
56
+ if patch.default:
57
+ return ""
58
+ if patch.option:
59
+ return f"(off by default; enable with {patch.option})"
60
+ return "(off by default; name it to apply)"
61
+
62
+
63
+ def _list_hint(patch: Patch, cached: cache.Selection) -> str:
64
+ """Simple, binary-free hints for `list`: how to enable a patch, its default
65
+ configurable value, and the value your last interactive run cached where
66
+ that is meaningful. The dynamic agent/model catalog lives in `apply --help`.
67
+ """
68
+ parts: list[str] = []
69
+ if enable := _enable_hint(patch):
70
+ parts.append(enable.strip("()"))
71
+ opts = cached.options
72
+ if patch.id == "branding":
73
+ parts.append(f"default {derived_brand()!r}")
74
+ if opts.rebrands and "branding" in cached.patches:
75
+ parts.append(f"cached {opts.brand!r}")
76
+ elif patch.id == "version-marker":
77
+ parts.append(f"default {DEFAULT_SUFFIX!r}")
78
+ if opts.version_suffix != DEFAULT_SUFFIX:
79
+ parts.append(f"cached {opts.version_suffix!r}")
80
+ elif patch.id == "subagent-models" and opts.subagent_models:
81
+ picks = ", ".join(f"{a}={m}" for a, m in opts.subagent_models.items())
82
+ parts.append(f"cached {picks}")
83
+ return " · ".join(parts)
84
+
85
+
38
86
  def _parse_models(specs: list[str], source: str) -> dict[str, str]:
39
87
  """Validate ``AGENT=MODEL`` pairs against what this bundle offers."""
40
88
  agents = {a.name: a for a in discover_agents(source)}
@@ -96,17 +144,86 @@ def _requested(args, source: str) -> tuple[list[str], Options]:
96
144
  return selected, options
97
145
 
98
146
 
147
+ def _has_selection_args(args) -> bool:
148
+ """Did the user pass an explicit selection (vs. bare `apply` = the defaults)?
149
+
150
+ Only an explicit pick is worth remembering: caching the default set would
151
+ clobber a previously remembered custom selection with nothing meaningful.
152
+ """
153
+ return bool(args.patches or args.brand is not None or args.model or args.suffix)
154
+
155
+
156
+ def _valid_models(
157
+ models: dict[str, str], source: str
158
+ ) -> tuple[dict[str, str], list[str]]:
159
+ """Split cached overrides into those this binary still accepts and the rest.
160
+
161
+ A build can drop an agent or retire a model between the interactive apply
162
+ that cached the choice and a later ``--from-cache`` replay; those are skipped
163
+ with a warning rather than written blind, mirroring the menu's own check.
164
+ """
165
+ known_agents = {a.name for a in discover_agents(source)}
166
+ known_models = {INHERIT, *discover_models(source)}
167
+ valid: dict[str, str] = {}
168
+ dropped: list[str] = []
169
+ for agent, model in models.items():
170
+ if agent in known_agents and model in known_models:
171
+ valid[agent] = model
172
+ else:
173
+ dropped.append(f"{agent}={model}")
174
+ return valid, dropped
175
+
176
+
177
+ def _from_cache(args, source: str) -> tuple[list[str], Options]:
178
+ """Rebuild the last interactive selection for a non-interactive apply.
179
+
180
+ The single place a persisted choice drives an action rather than pre-filling
181
+ the menu -- entered explicitly via ``--from-cache``, so the cache is a named
182
+ argument, not hidden state (see docs/CONDUCT.md). Model overrides are
183
+ re-validated against the binary in hand.
184
+ """
185
+ if args.patches or args.brand is not None or args.model or args.suffix:
186
+ err(
187
+ "--from-cache replays your last interactive selection; do not combine "
188
+ "it with patch ids or --brand / --model / --suffix."
189
+ )
190
+ raise SystemExit(2)
191
+ if not cache.cache_path().exists():
192
+ err(
193
+ "No cached selection yet. Apply once from the interactive menu "
194
+ "(run `patch-cc`), then `--from-cache` replays it."
195
+ )
196
+ raise SystemExit(2)
197
+
198
+ selection = cache.load()
199
+ options = selection.options
200
+ selected = list(selection.patches)
201
+ if options.subagent_models:
202
+ valid, dropped = _valid_models(options.subagent_models, source)
203
+ options.subagent_models = valid
204
+ if dropped:
205
+ warn(
206
+ "cached model override(s) not valid for this build, skipped: "
207
+ + ", ".join(dropped)
208
+ )
209
+ if not valid and "subagent-models" in selected:
210
+ selected.remove("subagent-models")
211
+ return selected, options
212
+
213
+
99
214
  def _print_findings(outcome: Outcome) -> None:
100
215
  """The detail under a patch line -- worded in :func:`ui.findings`."""
101
216
  for style, text in findings(outcome):
102
217
  console.print(f" [{style}]· {text}[/{style}]")
103
218
 
104
219
 
105
- def _print_report(report: patcher.PatchReport) -> None:
220
+ def _print_report(report: patcher.PatchReport, options: Options) -> None:
106
221
  heading("Patch results")
107
222
  for patch, outcome in report.results:
108
223
  mark, colour = MARKS[outcome.health]
109
224
  detail = f" applied {outcome.applied}" if outcome.applied else ""
225
+ if outcome.applied and (value := applied_value(patch, options)):
226
+ detail += f" [dim]→ {escape(value)}[/dim]"
110
227
  console.print(f" [{colour}]{mark}[/{colour}] {patch.title:28s}{detail}")
111
228
  _print_findings(outcome)
112
229
 
@@ -139,9 +256,15 @@ def _print_report(report: patcher.PatchReport) -> None:
139
256
  def cmd_apply(args) -> int:
140
257
  install = locate.find_or_raise()
141
258
  bundle = patcher.read_pristine(install)
142
- selected, options = _requested(args, bundle.source)
143
-
144
- heading(f"Patching Claude {install.version or '?'} ({install.binary.name})")
259
+ if args.from_cache:
260
+ selected, options = _from_cache(args, bundle.source)
261
+ else:
262
+ selected, options = _requested(args, bundle.source)
263
+
264
+ version = install.version or "?"
265
+ name = install.binary.name
266
+ where = version if name == version else f"{version} ({name})"
267
+ heading(f"Patching Claude {where}")
145
268
  try:
146
269
  report = patcher.patch_installation(install, selected, options, bundle=bundle)
147
270
  except patcher.AlreadyPatchedError as exc:
@@ -151,8 +274,14 @@ def cmd_apply(args) -> int:
151
274
  err(str(exc))
152
275
  return 1
153
276
 
154
- _print_report(report)
277
+ _print_report(report, options)
155
278
  if report.output is not None:
279
+ # Remember an explicit pick so `apply --from-cache` and the menu can
280
+ # replay it -- but never a bare `apply`, which would overwrite a
281
+ # remembered selection with the defaults. `--from-cache` never reaches
282
+ # here with selection args, so a replay does not re-cache itself.
283
+ if _has_selection_args(args):
284
+ cache.save(cache.Selection(patches=selected, options=options))
156
285
  console.print("\n[dim]Restart Claude Code for changes to take effect.[/dim]")
157
286
  return 0 if report.ok else 1
158
287
 
@@ -279,32 +408,25 @@ def cmd_doctor(args) -> int:
279
408
 
280
409
 
281
410
  def cmd_list(args) -> int:
411
+ """The quick catalog: id, description, simple hints. Registry + cache only --
412
+
413
+ no binary read, so it is fast and works anywhere. The dynamic agent/model
414
+ catalog and full usage live in `apply --help`.
415
+ """
282
416
  heading("Available patches")
417
+ console.print(
418
+ " [dim]apply with `patch-cc apply <id>`; "
419
+ "see `apply --help` for flags, examples & your binary's agents/models[/dim]"
420
+ )
421
+ cached = cache.load()
283
422
  for group, patches in by_group().items():
284
423
  if not patches:
285
424
  continue
286
425
  console.print(f"\n[bold]{group}[/bold]")
287
426
  for patch in patches:
288
- tag = "[dim](via --model)[/dim]" if not patch.default else ""
289
- console.print(f" [cyan]{patch.id:18s}[/cyan] {patch.summary} {tag}")
290
-
291
- install = locate.find()
292
- if install is None:
293
- return 0
294
- try:
295
- source = patcher.read_pristine(install).source
296
- except (BunError, OSError):
297
- return 0
298
- agents = discover_agents(source)
299
- if agents:
300
- console.print(f"\n[bold]Subagents in Claude {install.version or '?'}[/bold]")
301
- for agent in agents:
302
- console.print(
303
- f" [cyan]{agent.name:18s}[/cyan] default model: {agent.effective_model}"
304
- )
305
- console.print(
306
- f" [dim]models: {', '.join([INHERIT, *discover_models(source)])}[/dim]"
307
- )
427
+ console.print(f" [cyan]{patch.id:18s}[/cyan] {patch.summary}")
428
+ if hint := _list_hint(patch, cached):
429
+ console.print(f" [dim]{'':18s} {hint}[/dim]")
308
430
  return 0
309
431
 
310
432
 
@@ -335,36 +457,182 @@ def cmd_menu(args) -> int:
335
457
  return run_menu()
336
458
 
337
459
 
460
+ _MAIN_EPILOG = """\
461
+ Run with no arguments to open the interactive menu.
462
+
463
+ common tasks:
464
+ patch-cc apply apply the default patch set to the installed binary
465
+ patch-cc apply --help every patch id, the flags, and worked examples
466
+ patch-cc status show exactly what is applied right now
467
+ patch-cc doctor check every patch still matches this build
468
+ patch-cc list describe every patch: ids, descriptions, hints
469
+ patch-cc restore put the original binary back from backup
470
+ """
471
+
472
+
473
+ def _discover_binary() -> tuple[str | None, str | None]:
474
+ """The installed binary's JS source and version, or ``(None, version?)``.
475
+
476
+ Best-effort: ``apply --help`` reads the real binary so its agent/model list
477
+ matches what ``--model`` accepts, but a missing or unreadable install just
478
+ drops the dynamic block instead of failing the help.
479
+ """
480
+ install = locate.find()
481
+ if install is None:
482
+ return None, None
483
+ try:
484
+ return patcher.read_pristine(install).source, install.version
485
+ except (BunError, OSError):
486
+ return None, install.version
487
+
488
+
489
+ def _example_model(models: list[str], offset: int = 0) -> str:
490
+ """A concrete (non-inherit) model alias for a ``--model`` example."""
491
+ concrete = [m for m in models if m != INHERIT]
492
+ return concrete[offset % len(concrete)] if concrete else INHERIT
493
+
494
+
495
+ def _apply_epilog() -> str:
496
+ """Build ``apply --help`` from the registry *and* the installed binary.
497
+
498
+ Nothing is hardcoded: the patch list and default markers come from the
499
+ registry, the subagent agents and models from the real binary. The binary
500
+ read is deferred to the help action, so only ``apply --help`` pays for it and
501
+ a missing install degrades to the static parts.
502
+ """
503
+ source, version = _discover_binary()
504
+ agents = discover_agents(source) if source else []
505
+ models = [INHERIT, *discover_models(source)] if source else []
506
+ grouped = by_group()
507
+
508
+ lines = ["patches (name them to apply exactly those; * = the default set):", ""]
509
+ for group in GROUP_ORDER:
510
+ group_patches = grouped.get(group, [])
511
+ if not group_patches:
512
+ continue
513
+ lines.append(f" {group}")
514
+ for patch in group_patches:
515
+ mark = "*" if patch.default else " "
516
+ hint = _enable_hint(patch)
517
+ row = f" {mark} {patch.id:20s}{patch.title}"
518
+ lines.append(f"{row} {hint}" if hint else row)
519
+ lines.append("")
520
+
521
+ lines += [
522
+ "naming ids replaces the default set with exactly what you name; the",
523
+ "flags below always add their own patch on top of whatever you named.",
524
+ "",
525
+ "configuring patches:",
526
+ f" --brand [NAME] selects branding · no value -> {derived_brand()!r}",
527
+ f" --suffix TEXT selects version-marker · default {DEFAULT_SUFFIX!r}",
528
+ " --model AGENT=MODEL selects subagent-models · repeatable, one per agent",
529
+ ]
530
+ if agents:
531
+ lines += [
532
+ f" agents (Claude {version or '?'}): "
533
+ + " ".join(a.name for a in agents),
534
+ " models: " + " ".join(models),
535
+ ]
536
+ elif source is None:
537
+ lines.append(
538
+ " (the agents/models list shows when a Claude install is present)"
539
+ )
540
+
541
+ ex_one = f"{agents[0].name}={_example_model(models)}" if agents else "Explore=haiku"
542
+ ex_two = f"{agents[1].name}={_example_model(models, 1)}" if len(agents) > 1 else ""
543
+
544
+ lines += [
545
+ "",
546
+ "examples:",
547
+ " patch-cc apply",
548
+ " the default set (the * patches above)",
549
+ " patch-cc apply tool-calls live-thinking",
550
+ " only these two; the default set is replaced",
551
+ " patch-cc apply --brand",
552
+ f" the default set, branded {derived_brand()!r}",
553
+ ' patch-cc apply --brand "Ada\'s Code" --suffix "(ada)"',
554
+ " default set with an explicit startup name and version marker",
555
+ f" patch-cc apply --model {ex_one}" + (f" --model {ex_two}" if ex_two else ""),
556
+ " default set plus subagent model override" + ("s" if ex_two else ""),
557
+ " patch-cc apply --from-cache",
558
+ " re-apply your last interactive menu selection (saved by `patch-cc`)",
559
+ ]
560
+ return "\n".join(lines)
561
+
562
+
563
+ class _ApplyHelpAction(argparse.Action):
564
+ """``apply -h/--help``: assemble the dynamic epilog, then print help.
565
+
566
+ Deferred here rather than at parser-build time so only ``apply --help`` reads
567
+ the binary -- every other ``patch-cc`` invocation stays fast and needs no
568
+ install.
569
+ """
570
+
571
+ def __init__(self, option_strings, dest, **kwargs):
572
+ super().__init__(
573
+ option_strings, dest, nargs=0, default=argparse.SUPPRESS, **kwargs
574
+ )
575
+
576
+ def __call__(self, parser, namespace, values, option_string=None):
577
+ parser.epilog = _apply_epilog()
578
+ parser.print_help()
579
+ parser.exit()
580
+
581
+
338
582
  def build_parser() -> argparse.ArgumentParser:
339
583
  parser = argparse.ArgumentParser(
340
584
  prog="patch-cc",
341
585
  description="Interactive patcher for the Claude Code native binary.",
586
+ epilog=_MAIN_EPILOG,
587
+ formatter_class=argparse.RawDescriptionHelpFormatter,
342
588
  )
343
589
  parser.set_defaults(func=cmd_menu)
344
590
  sub = parser.add_subparsers(dest="command")
345
591
 
346
- p_apply = sub.add_parser("apply", help="apply patches to the installed binary")
592
+ p_apply = sub.add_parser(
593
+ "apply",
594
+ help="apply patches to the installed binary",
595
+ description="Apply patches to the installed Claude binary. Always starts "
596
+ "from a pristine copy, so re-applying replaces the previous set rather "
597
+ "than stacking on it.",
598
+ add_help=False,
599
+ formatter_class=argparse.RawDescriptionHelpFormatter,
600
+ )
601
+ p_apply.add_argument(
602
+ "-h",
603
+ "--help",
604
+ action=_ApplyHelpAction,
605
+ help="show this help (reads the installed binary for its agents/models)",
606
+ )
347
607
  p_apply.add_argument(
348
608
  "patches",
349
609
  nargs="*",
350
610
  metavar="PATCH",
351
- help="patch ids to apply (default: the default set)",
611
+ help="patch ids to apply (default: the default set; all ids listed below)",
612
+ )
613
+ p_apply.add_argument(
614
+ "--from-cache",
615
+ action="store_true",
616
+ help="re-apply your last interactive menu selection (ignores other args)",
352
617
  )
353
618
  p_apply.add_argument(
354
619
  "--brand",
355
620
  nargs="?",
356
621
  const=_DERIVE,
357
622
  metavar="NAME",
358
- help="custom startup name (no value: <username>'s Code)",
623
+ help="startup name; selects `branding` (no value: <username>'s Code)",
359
624
  )
360
625
  p_apply.add_argument(
361
626
  "--model",
362
627
  action="append",
363
628
  metavar="AGENT=MODEL",
364
- help="override a subagent's default model (repeatable)",
629
+ help="set a subagent's model; selects `subagent-models` (repeatable)",
365
630
  )
366
631
  p_apply.add_argument(
367
- "--suffix", metavar="TEXT", help="--version marker text (default: (patched))"
632
+ "--suffix",
633
+ metavar="TEXT",
634
+ help="`claude --version` marker text; selects `version-marker` "
635
+ "(default: (patched))",
368
636
  )
369
637
  p_apply.set_defaults(func=cmd_apply)
370
638
 
@@ -381,7 +649,11 @@ def build_parser() -> argparse.ArgumentParser:
381
649
  )
382
650
  p_doctor.set_defaults(func=cmd_doctor)
383
651
  sub.add_parser(
384
- "list", help="list patches, and the agents/models in your binary"
652
+ "list",
653
+ help="describe every patch + the subagent models your binary offers",
654
+ description="Describe every patch (what it does, whether it is on by "
655
+ "default, and how to enable it) plus the subagent models discovered in "
656
+ "your installed binary. For apply syntax and examples, see `apply --help`.",
385
657
  ).set_defaults(func=cmd_list)
386
658
  sub.add_parser(
387
659
  "restore", help="restore the original binary from backup"
@@ -49,7 +49,7 @@ from .patches import (
49
49
  derived_brand,
50
50
  )
51
51
  from .patches.agents import INHERIT, BuiltinAgent, discover_agents, discover_models
52
- from .ui import MARKS, console, err, findings
52
+ from .ui import MARKS, applied_value, console, err, findings
53
53
 
54
54
  if TYPE_CHECKING:
55
55
  from .doctor import DryRun, Status
@@ -1028,12 +1028,15 @@ class MenuApp:
1028
1028
  report = self.report
1029
1029
  if report is None:
1030
1030
  return lines, None
1031
+ options = self.model.selection().options
1031
1032
  for patch, outcome in report.results:
1032
1033
  mark, style = MARKS[outcome.health]
1033
1034
  line = Text()
1034
1035
  line.append(f" {mark} ", style=style)
1035
1036
  line.append(f"{patch.title:<32}")
1036
1037
  line.append(f"{outcome.applied or '':>3}", style="dim")
1038
+ if outcome.applied and (value := applied_value(patch, options)):
1039
+ line.append(f" → {value}", style="dim")
1037
1040
  lines.append(line)
1038
1041
  lines += _findings(outcome)
1039
1042
 
@@ -1063,6 +1066,12 @@ class MenuApp:
1063
1066
  )
1064
1067
  lines.append(line)
1065
1068
  lines.append(Text(" Restart Claude Code to see it.", style="dim"))
1069
+ lines.append(
1070
+ Text(
1071
+ " Reapply this selection anytime: patch-cc apply --from-cache",
1072
+ style="dim",
1073
+ )
1074
+ )
1066
1075
  return lines, None
1067
1076
 
1068
1077
  def _body_doctor(self, panel_width: int) -> tuple[list[Text], int | None]:
@@ -13,7 +13,6 @@ from .base import (
13
13
  DEFAULT_SUFFIX,
14
14
  GROUP_AGENTS,
15
15
  GROUP_CHROME,
16
- GROUP_LIVE,
17
16
  GROUP_OUTPUT,
18
17
  GROUP_THINKING,
19
18
  SENTINEL,
@@ -31,7 +30,7 @@ ALL_PATCHES: list[Patch] = [
31
30
  *chrome.PATCHES,
32
31
  ]
33
32
 
34
- GROUP_ORDER = [GROUP_OUTPUT, GROUP_THINKING, GROUP_LIVE, GROUP_AGENTS, GROUP_CHROME]
33
+ GROUP_ORDER = [GROUP_OUTPUT, GROUP_THINKING, GROUP_AGENTS, GROUP_CHROME]
35
34
 
36
35
  _BY_ID = {patch.id: patch for patch in ALL_PATCHES}
37
36
 
@@ -341,6 +341,7 @@ PATCHES = [
341
341
  group=GROUP_AGENTS,
342
342
  fn=_subagent_models,
343
343
  default=False,
344
+ option="--model",
344
345
  anchors=('agentType:"', "Optional model override"),
345
346
  ),
346
347
  ]
@@ -33,7 +33,6 @@ SENTINEL = "__cc_"
33
33
  # Groups, in display order.
34
34
  GROUP_OUTPUT = "Output & diffs"
35
35
  GROUP_THINKING = "Thinking"
36
- GROUP_LIVE = "Live thinking (streaming)"
37
36
  GROUP_AGENTS = "Subagents"
38
37
  GROUP_CHROME = "Chrome & branding"
39
38
 
@@ -218,6 +217,11 @@ class Patch:
218
217
  group: str
219
218
  fn: PatchFn
220
219
  default: bool = True
220
+ #: The CLI flag that configures and auto-selects this patch (``--brand``,
221
+ #: ``--model``, ``--suffix``); ``None`` for patches selected only by id. The
222
+ #: single home for the patch<->flag coupling, so ``list`` and ``apply
223
+ #: --help`` can say how an opt-in patch is turned on rather than inferring it.
224
+ option: str | None = None
221
225
  #: Anchors to report on when this patch stops matching.
222
226
  anchors: tuple[str, ...] = ()
223
227
 
@@ -184,6 +184,7 @@ PATCHES = [
184
184
  summary="Append a marker line to `claude --version` (custom text via --suffix).",
185
185
  group=GROUP_CHROME,
186
186
  fn=_version_output,
187
+ option="--suffix",
187
188
  anchors=("}.VERSION} (Claude Code)",),
188
189
  ),
189
190
  Patch(
@@ -192,6 +193,7 @@ PATCHES = [
192
193
  summary="Rename the startup/help branding (default: your username's Code).",
193
194
  group=GROUP_CHROME,
194
195
  fn=_branding,
196
+ option="--brand",
195
197
  anchors=('"Welcome to Claude Code"', '{bold:!0},"Claude Code"'),
196
198
  ),
197
199
  ]
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
  import re
24
24
  from dataclasses import dataclass
25
25
 
26
- from .base import GROUP_LIVE, IDENT, Options, Outcome, Patch, compile_js, splice
26
+ from .base import GROUP_THINKING, IDENT, Options, Outcome, Patch, compile_js, splice
27
27
 
28
28
  #: The mutually-exclusive reducer variants. On any one build one of them lands;
29
29
  #: none landing means upstream shipped a fourth shape and live thinking is dead.
@@ -974,7 +974,7 @@ PATCHES = [
974
974
  title="Stream thinking live",
975
975
  summary="Show thinking as it is generated, inline and in order, instead of "
976
976
  "only after the turn finishes.",
977
- group=GROUP_LIVE,
977
+ group=GROUP_THINKING,
978
978
  fn=_live_thinking,
979
979
  anchors=(
980
980
  "onStreamingThinking:",
@@ -192,8 +192,8 @@ def _thinking_summaries(content: str, _options: Options, outcome: Outcome) -> st
192
192
  PATCHES = [
193
193
  Patch(
194
194
  id="thinking-summaries",
195
- title="Opt out of server-side experiments",
196
- summary="Stop sending the account's experiment-bucket header, which can empty every thinking block.",
195
+ title="Fix blank thinking blocks",
196
+ summary="Opt out of the server-side experiment bucket that can return empty thinking blocks on some accounts. Drops all experiment enrollment, not just this one.",
197
197
  group=GROUP_THINKING,
198
198
  fn=_thinking_summaries,
199
199
  anchors=(_BUCKET_HEADER, "?.atis"),
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from rich.console import Console
6
6
 
7
- from .patches.base import Outcome
7
+ from .patches.base import Options, Outcome, Patch
8
8
 
9
9
  console = Console()
10
10
 
@@ -37,6 +37,22 @@ def findings(outcome: Outcome) -> list[tuple[str, str]]:
37
37
  return lines + [("dim", note) for note in notes]
38
38
 
39
39
 
40
+ def applied_value(patch: Patch, options: Options) -> str | None:
41
+ """The value a configurable patch actually wrote, for the report line.
42
+
43
+ Branding, the version marker, and model overrides each carry a chosen value;
44
+ a plain toggle patch carries none. One source so the CLI and the menu report
45
+ the same thing after an apply.
46
+ """
47
+ if patch.id == "branding":
48
+ return options.brand
49
+ if patch.id == "version-marker":
50
+ return options.version_suffix
51
+ if patch.id == "subagent-models" and options.subagent_models:
52
+ return ", ".join(f"{a}={m}" for a, m in options.subagent_models.items())
53
+ return None
54
+
55
+
40
56
  def heading(text: str) -> None:
41
57
  console.print(f"\n[bold]{text}[/bold]")
42
58
 
@@ -256,7 +256,7 @@ wheels = [
256
256
 
257
257
  [[package]]
258
258
  name = "patch-cc"
259
- version = "0.1.3"
259
+ version = "0.1.4"
260
260
  source = { editable = "." }
261
261
  dependencies = [
262
262
  { name = "blessed" },
File without changes
File without changes
File without changes