patch-cc 0.1.2__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 (35) hide show
  1. {patch_cc-0.1.2 → patch_cc-0.1.4}/PKG-INFO +10 -6
  2. {patch_cc-0.1.2 → patch_cc-0.1.4}/README.md +9 -5
  3. {patch_cc-0.1.2 → patch_cc-0.1.4}/docs/CONDUCT.md +8 -4
  4. patch_cc-0.1.4/docs/PLAYBOOK.md +367 -0
  5. {patch_cc-0.1.2 → patch_cc-0.1.4}/pyproject.toml +1 -1
  6. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/__init__.py +1 -1
  7. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/cache.py +7 -5
  8. patch_cc-0.1.4/src/patch_cc/cli.py +688 -0
  9. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/doctor.py +12 -17
  10. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/menu.py +31 -21
  11. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patcher.py +45 -16
  12. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/__init__.py +1 -2
  13. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/agents.py +84 -27
  14. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/base.py +88 -6
  15. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/chrome.py +16 -4
  16. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/streaming.py +82 -18
  17. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/patches/thinking.py +72 -5
  18. patch_cc-0.1.4/src/patch_cc/ui.py +69 -0
  19. {patch_cc-0.1.2 → patch_cc-0.1.4}/uv.lock +1 -1
  20. patch_cc-0.1.2/docs/PLAYBOOK.md +0 -208
  21. patch_cc-0.1.2/src/patch_cc/cli.py +0 -399
  22. patch_cc-0.1.2/src/patch_cc/ui.py +0 -23
  23. {patch_cc-0.1.2 → patch_cc-0.1.4}/.github/workflows/ci.yml +0 -0
  24. {patch_cc-0.1.2 → patch_cc-0.1.4}/.github/workflows/release.yml +0 -0
  25. {patch_cc-0.1.2 → patch_cc-0.1.4}/.gitignore +0 -0
  26. {patch_cc-0.1.2 → patch_cc-0.1.4}/LICENSE +0 -0
  27. {patch_cc-0.1.2 → patch_cc-0.1.4}/docs/INTERNALS.md +0 -0
  28. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/__init__.py +0 -0
  29. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/blob.py +0 -0
  30. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/container.py +0 -0
  31. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/elf.py +0 -0
  32. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/errors.py +0 -0
  33. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/bun/macho.py +0 -0
  34. {patch_cc-0.1.2 → patch_cc-0.1.4}/src/patch_cc/locate.py +0 -0
  35. {patch_cc-0.1.2 → 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.2
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,8 +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 | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
80
- | Live thinking | Stream thinking live | See reasoning as it is generated, inline and in order |
79
+ | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
80
+ | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
81
+ | | Stream thinking live | See reasoning as it is generated, inline and in order |
81
82
  | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
82
83
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
83
84
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
@@ -96,9 +97,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
96
97
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
97
98
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
98
99
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
100
+ uvx patch-cc apply --from-cache # replay your last remembered selection
99
101
  uvx patch-cc status # exactly what is applied
100
102
  uvx patch-cc doctor # do all patches match this build?
101
- uvx patch-cc list # patches + your binary's agents/models
103
+ uvx patch-cc doctor path/to/claude # ...or match some other binary
104
+ uvx patch-cc list # every patch, described
102
105
  uvx patch-cc restore # put the original back
103
106
  ```
104
107
 
@@ -108,8 +111,9 @@ against what your installed binary actually ships.
108
111
  ## After a Claude update
109
112
 
110
113
  Claude auto-updates roughly daily and replaces the binary, which reverts the
111
- patch. Re-run `patch-cc` — the menu remembers your last selection — or re-apply
112
- 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:
113
117
 
114
118
  ```bash
115
119
  uvx patch-cc apply --brand --model Explore=haiku
@@ -48,8 +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 | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
52
- | Live thinking | Stream thinking live | See reasoning as it is generated, inline and in order |
51
+ | Thinking | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
52
+ | | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
53
+ | | Stream thinking live | See reasoning as it is generated, inline and in order |
53
54
  | Subagents | Show subagent prompts | Prompt blocks visible during normal use |
54
55
  | | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
55
56
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
@@ -68,9 +69,11 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
68
69
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
69
70
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
70
71
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
72
+ uvx patch-cc apply --from-cache # replay your last remembered selection
71
73
  uvx patch-cc status # exactly what is applied
72
74
  uvx patch-cc doctor # do all patches match this build?
73
- uvx patch-cc list # patches + your binary's agents/models
75
+ uvx patch-cc doctor path/to/claude # ...or match some other binary
76
+ uvx patch-cc list # every patch, described
74
77
  uvx patch-cc restore # put the original back
75
78
  ```
76
79
 
@@ -80,8 +83,9 @@ against what your installed binary actually ships.
80
83
  ## After a Claude update
81
84
 
82
85
  Claude auto-updates roughly daily and replaces the binary, which reverts the
83
- patch. Re-run `patch-cc` — the menu remembers your last selection — or re-apply
84
- 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:
85
89
 
86
90
  ```bash
87
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
@@ -41,7 +43,9 @@ shape that already absorbs every case, found rather than bolted on.
41
43
  - **Report absent apart from broken.** A matcher that finds nothing may be a
42
44
  shape this build simply lacks — most patches carry several — not a regression.
43
45
  Keep "gone", "already applied", and "not on this build" as distinct signals;
44
- never collapse them into one number.
46
+ never collapse them into one number. Which one a sub-step's silence means is
47
+ not guesswork: declare it (`expect=True`, or a variant group) so a green tick
48
+ cannot cover a dead feature. See [PLAYBOOK.md](PLAYBOOK.md).
45
49
 
46
50
  - **Port faithfully.** When you change a patch, verify its output against the
47
51
  upstream reference on a real bundle — byte-identical where behaviour must not
@@ -0,0 +1,367 @@
1
+ # Patch playbook
2
+
3
+ For maintainers. How the patches are built to survive Claude updates, and how to
4
+ repair one when a build breaks it.
5
+
6
+ This is a Python port of
7
+ [a-connoisseur/patch-claude-code](https://github.com/a-connoisseur/patch-claude-code);
8
+ that repo's git history is the best archive of how each matcher has drifted over
9
+ time.
10
+
11
+ ## Matcher rules
12
+
13
+ The minified bundle is regenerated on every Claude build, so identifiers churn
14
+ constantly. These rules are what keep matchers alive:
15
+
16
+ - **Never** anchor on a minified local (`A_`, `mET`, `wg6`). Anchor on string
17
+ literals, `case` labels, prop names, or control-flow shape.
18
+ - Match the *semantic shape* of a function body, not its symbol names.
19
+ - When upstream ships more than one shape for the same feature, add a second
20
+ narrow branch — don't widen one regex until it over-matches.
21
+ - Accept statement variants a minifier flips between. The thinking gate broke
22
+ on 2.1.216 solely because `if(x)return null;` became `if(x){return null}` —
23
+ matchers should allow both from day one.
24
+ - Match an optional brace **pair** conditionally (`(\{)?…(?(1)\})`), never as
25
+ two independent `\{?` / `\}?`. A lone optional closing brace eats the
26
+ *enclosing* block's `}` on the unbraced shape, and a rewrite that emits its
27
+ own leaves the bundle unbalanced — with `candidates`/`applied` unchanged, so
28
+ nothing looks wrong until Claude fails to start.
29
+ - Compile with `re.ASCII` (helper `compile_js`) so `\w` behaves like JS.
30
+ - JS `.replace(re, fn)` without `/g` replaces **once** → `re.sub(..., count=1)`.
31
+ JS `.replace("a","b")` on a string also replaces once → `str.replace(a, b, 1)`.
32
+ Getting this wrong over-rewrites.
33
+ - Always pass a *function* replacement to `re.sub`, never a template string
34
+ (backslashes and `\g<>` would be interpreted).
35
+
36
+ ## Discovery instead of hardcoding
37
+
38
+ Anything the binary can enumerate for us, it does:
39
+
40
+ - **Built-in agents** are found by the definition shape
41
+ `agentType:"<name>",whenToUse:` carrying `source:"built-in"` in the same
42
+ object (`agents.discover_agents`). Definitions whose `whenToUse` begins with
43
+ `"Internal` are plumbing and are not offered.
44
+ - **Model aliases** come from the Task tool's own input schema: the
45
+ `model:<zod>.enum([...])` whose describe-string starts
46
+ `Optional model override` (`agents.discover_models`).
47
+
48
+ A new upstream agent or model appears in `patch-cc list`, the menu, and
49
+ `--model` validation without a code change. If the enum anchor ever vanishes,
50
+ discovery falls back to `haiku/sonnet/opus` — `doctor` prints both lists, so a
51
+ missing agent or alias is visible at a glance.
52
+
53
+ ## The manifest
54
+
55
+ Every patched bundle ends with one comment line:
56
+
57
+ ```
58
+ //patch-cc {"v":1,"tool":"0.1.0","patches":[...],"brand":...,"models":{...}}
59
+ ```
60
+
61
+ A comment cannot collide with code, survives re-extraction, and makes `status`
62
+ a parse instead of a guess — several patches are value flips
63
+ (`verbose:!0`) that leave no other fingerprint. `is_patched` also still
64
+ recognises the legacy fingerprints (`__cc_` identifiers, the old `--version`
65
+ marker) so binaries patched by pre-manifest versions are not mistaken for
66
+ clean.
67
+
68
+ ## How resilience is detected
69
+
70
+ `patch-cc doctor` runs every patch against a **clean** bundle (the pristine
71
+ backup if the installed binary is already patched) and reports, per patch:
72
+
73
+ - `candidates` — how many times the anchor/shape was found
74
+ - `applied` — how many rewrites actually changed something
75
+
76
+ Configurable patches are fed a synthetic configuration built from the bundle's
77
+ own discovered agents and models (every agent assigned a model different from
78
+ its current one, a test brand), so branding and the model overrides are
79
+ exercised for real — nothing is exempt from the dry run.
80
+
81
+ Two failure modes, kept distinct:
82
+
83
+ | symptom | meaning |
84
+ |---|---|
85
+ | `candidates == 0` | the anchor is **gone** — a real regression |
86
+ | `candidates > 0, applied == 0` | shape found, rewrite was a no-op — usually already patched |
87
+
88
+ `doctor` prints the documented anchor counts for any broken patch, so a `0`
89
+ next to an anchor points straight at what moved.
90
+
91
+ ### Expectations — why a green tick means something
92
+
93
+ Counting alone cannot tell "this build lacks that shape" from "the feature is
94
+ dead": a patch whose optional shapes rewrite happily while a load-bearing one
95
+ is gone still changes bytes, and would read as green. So each sub-step declares
96
+ what its absence *means* (`Outcome.step(..., expect=...)`):
97
+
98
+ | mark | meaning | absence is |
99
+ |---|---|---|
100
+ | *(default)* | a shape only some builds carry | informational |
101
+ | `expect=True` | the patch does nothing useful without it | a regression |
102
+ | `expect="<group>"` | alternate shapes; at least one must land | a regression |
103
+
104
+ `Outcome.unmet()` turns a violation into a sentence (`required step
105
+ group-routing found nothing`); `Outcome.failures()` adds any exception the
106
+ patch raised, because a patch that threw and a patch that missed an
107
+ expectation are the same verdict wearing different clothes; `Outcome.health`
108
+ folds them into `ok` / `partial` / `broken`. Every surface (`apply`, `doctor`,
109
+ the menu) reads *those two* and never re-derives either — `doctor` once judged
110
+ on counts alone and printed a red cross and "all patches still match" in the
111
+ same report. Adding a sub-step means deciding which row of the table it is in;
112
+ that decision is the whole safety net.
113
+
114
+ Two rules keep the net from having holes:
115
+
116
+ - **Count what the rewrite *achieved*, not that some rewrite happened.** A step
117
+ built from many literal edits lands as soon as *one* of them applies, so an
118
+ incidental edit can vouch for the essential ones. `live-thinking` learned this
119
+ the hard way: a reducer whose setter threading applied while every event arm
120
+ had drifted reported hits and streamed nothing. The two edits that *are* the
121
+ feature are now checked by the markers their builders emit
122
+ (`streaming._CORE_UPDATES`), which no amount of neighbouring churn can fake.
123
+ - **Declare an expectation before the work, not inside it.** A step created only
124
+ by its own success cannot report its own absence. `_live_thinking` registers
125
+ the core updates up front; `agents.bypassed_agents` resolves the pinned agent
126
+ from the helper's durable head so a drifted body still has a step to fail.
127
+
128
+ `apply` acts on the verdict: a broken patch is re-run out of the final pass, so
129
+ its orphan edits never reach the binary, the manifest never claims it, and the
130
+ command exits non-zero. The healthy patches still apply.
131
+
132
+ That re-run is a **fixpoint**, not one retry. Patches see each other's output,
133
+ so dropping one can change what the next finds; the set is settled only when a
134
+ whole run comes back with nothing broken, and each patch is reported by the last
135
+ run it took part in. Judging the bytes of the final run by the verdicts of the
136
+ first is how a manifest starts lying. The manifest is held to the same rule:
137
+ `brand`, `suffix` and `models` are recorded only when *their* patch landed, so
138
+ `status` can never assert a name the bundle does not contain.
139
+
140
+ ## Sub-steps, and why `live-thinking` has them
141
+
142
+ Most patches are one rewrite. `live-thinking` is ~11 named sub-steps, because
143
+ upstream has reshaped the stream reducer at least three times and a single hit
144
+ count cannot tell "all landed" from "half silently drifted".
145
+
146
+ Each sub-step records its own `candidates`/`applied`. Sub-steps come in two
147
+ kinds:
148
+
149
+ - **Independent fixes** (`memo-cache`, `linger`, `display-mode`, `bottom-row`,
150
+ …) — each may or may not be present on a given build.
151
+ - **Mutually-exclusive reducer variants** — `reducer-destructured` (2.1.138+),
152
+ `reducer-inner` (2.1.183+), `reducer-legacy` (pre-2.1.138). On any one build
153
+ exactly one should land. On 2.1.216 it is `reducer-inner`.
154
+
155
+ An *optional* sub-step that finds nothing is reported as absent, not broken —
156
+ it is just a shape this build doesn't have. A sub-step that finds a shape but
157
+ fails to rewrite it (`candidates > 0, applied == 0`) shows up in
158
+ `missed_steps()`.
159
+
160
+ The rest carry expectations, so their absence is checked rather than noted:
161
+
162
+ - **Required** — `prop-threading`, `display-mode`, `transcript-signature`,
163
+ `inline-extras`, plus `block-start` and `thinking-delta`. Each is a link in
164
+ the chain from stream event to rendered row; without any one of them live
165
+ thinking is dead however many other steps land. The last two are not matchers
166
+ but *proof*: they are credited only when the rewritten reducer body actually
167
+ contains the state updates, which is what recognising a reducer does not by
168
+ itself establish.
169
+ - **The `reducer` group** — `reducer-destructured`, `reducer-inner`,
170
+ `reducer-legacy`. At least one must land; none landing is the signal that
171
+ upstream shipped a fourth reducer shape that needs a new variant. Two landing
172
+ is allowed on purpose — a transitional build carrying two reducers would have
173
+ both correctly patched, and that is no reason to cry wolf.
174
+ - **Optional** — `memo-cache`, `memo-removal`, `linger`, `bottom-row`,
175
+ `final-summary`. The first four match nothing on 2.1.216+ and are kept for
176
+ older builds; `discover` is a notes-only channel that records how far back the
177
+ state back-scan reached, which is the early warning for `_DISCOVER_WINDOW`
178
+ (35,116 of 50,000 on 2.1.217).
179
+
180
+ Notes print on every run, green ones included — an early warning held back
181
+ until something breaks arrives too late to be one. Absences are the noisy half
182
+ (most patches lack several shapes on any build) and wait for a verdict that is
183
+ not `ok`. Both surfaces draw the same list (`ui.findings`): the CLI and the menu
184
+ each worded their own once, and the menu's copy had quietly lost the exception
185
+ that broke a patch along with every note.
186
+
187
+ ## Repairing a broken patch
188
+
189
+ 1. Get a clean bundle from a current binary:
190
+
191
+ ```bash
192
+ patch-cc extract ~/.local/share/claude/versions/<ver> > clean.js
193
+ ```
194
+
195
+ 2. Run `patch-cc doctor`. Note which patch dropped to `candidates == 0`, or —
196
+ for `live-thinking` — which sub-step.
197
+
198
+ 3. Search the clean bundle for the *semantic* anchor, not the old identifier:
199
+
200
+ ```bash
201
+ rg 'case"collapsed_read_search"|case"thinking_delta"|spinnerTipsEnabled|Backgrounded agent' clean.js
202
+ ```
203
+
204
+ 4. If the anchor moved, find the new shape and update the matcher in the
205
+ relevant `src/patch_cc/patches/*.py`. Prefer adding a branch over loosening
206
+ the existing regex.
207
+
208
+ 5. Re-run `doctor` until the patch (and each expected sub-step) is green, then
209
+ apply to a real binary and check the behaviour at runtime.
210
+
211
+ 6. Sweep the fix over the versions you still have. `doctor` takes a path, and
212
+ every binary patch-cc has ever touched left a pristine copy in
213
+ `~/.local/share/patch-cc/backups/`:
214
+
215
+ ```bash
216
+ for b in ~/.local/share/patch-cc/backups/*.orig; do
217
+ echo "== $b"; patch-cc doctor "$b" || true
218
+ done
219
+ ```
220
+
221
+ That is what keeps "add a narrow branch" honest: a widened regex that
222
+ over-matches an older build shows up here instead of in a bug report.
223
+
224
+ ## Patch reference
225
+
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.
229
+
230
+ ### Output & diffs — `output.py`
231
+
232
+ - **`tool-calls`** — force verbose collapsed read/search rows.
233
+ Anchor: `case"collapsed_read_search"`.
234
+ *Value-flip* (`verbose:!0`) — the manifest is its only fingerprint.
235
+ - **`create-diff`** — render created files through the diff renderer with `+`
236
+ lines. Anchors: adjacent `case"create":` / `case"update":`; the update arm
237
+ must expose `structuredPatch`.
238
+
239
+ ### Thinking — `thinking.py`
240
+
241
+ - **`thinking-summaries`** — stop echoing the account's server-side experiment
242
+ bucket, so the API returns thinking blocks with text in them.
243
+ Anchor: the `?.atis` read plus the getter's whole return shape, the read tied
244
+ to both its uses by backreference (header name: `x-cc-atis`). Matching the
245
+ property and then replacing a brace-free body would also hit any *other*
246
+ function reading it, deleting whatever else that one did; one `.atis` per
247
+ bundle is today's happenstance, not an invariant. Candidates are counted off
248
+ the header name, not the matcher, so a reshaped getter reads as
249
+ `candidates > 0, applied == 0` — a matcher to repair — instead of the zero
250
+ that would equally mean upstream retired the mechanism.
251
+ Claude Code caches a GrowthBook assignment (`clientDataCacheSlots[...].atis`
252
+ in `~/.claude.json`, one slot per account × entrypoint × model) and replays it
253
+ to the API on every request so the server applies the same bucket. A slot in a
254
+ bucket that withholds thinking summaries is served thinking blocks carrying a
255
+ signature and an **empty string** — no `display` mode, effort level or
256
+ `thinking.type` changes it, and two requests that differ only in the header
257
+ differ in nothing else. Because the slot is per model too, one model can think
258
+ visibly while another stays blank in the same session. Every other thinking
259
+ patch then
260
+ renders that empty string faithfully, which is why the symptom reads as
261
+ "thinking works on one account and not another, same binary, same config".
262
+ The getter is read in exactly one place, to set that one header, so emptying
263
+ it lets the header's existing `if(value!==void 0)` guard skip it — nothing is
264
+ sent, and no branch was added to stop it. Mind the breadth: this drops the
265
+ bucket for *every* experiment the account is enrolled in, not just the one
266
+ that empties thinking. Local feature values still come from the on-disk cache,
267
+ so only the server's view of the assignment changes.
268
+
269
+ Diagnose from a transcript rather than by eye — `thinking` blocks are
270
+ recorded whether or not they carry text:
271
+
272
+ ```bash
273
+ jq -r 'select(.type=="assistant").message.content[]?
274
+ | select(.type=="thinking") | (.thinking|length)' \
275
+ ~/.claude/projects/<slug>/<session>.jsonl | sort -n | uniq -c
276
+ ```
277
+
278
+ A column of `0`s is this patch missing (or a bucket it does not yet cover);
279
+ a spread of real lengths means the text arrived and the problem is rendering.
280
+
281
+ - **`thinking-inline`** — make historical thinking blocks render inline.
282
+ Anchor: `case"thinking":` containing `isTranscriptMode:`. Two rewrites:
283
+ remove the early null-return (both `if(!a&&!b)return null;` and the 2.1.216
284
+ block form `if(!a&&!b){return null}`), then force `isTranscriptMode:!0`
285
+ (and `hideInTranscript:!1` where present) in the renderer props. The
286
+ component itself has no gate — an empty summary renders nothing, which is
287
+ why trivially short thinks may still show no block.
288
+
289
+ ### Live thinking — `streaming.py`
290
+
291
+ - **`live-thinking`** — the ~11-step patch above. Discovery anchor is
292
+ `onStreamingThinking:` → `useState(null)` (the older `hidePastThinking`
293
+ anchor is gone as of 2.1.216 — the fallback back-scan is load-bearing).
294
+ Reducer anchors: `type==="stream_request_start"`, `case"thinking_delta"`,
295
+ `content_block_start`.
296
+ The `display-mode` sub-step defaults the request's thinking display to
297
+ `"summarized"`; without it the API only streams summary text when the
298
+ `showThinkingSummaries` setting is on. Two shapes: the legacy inline env
299
+ check, and the 2.1.216 form that hoists
300
+ `X=qt(process.env.CLAUDE_CODE_DISABLE_THINKING)` and gates the display
301
+ behind extra feature-helper calls (kept verbatim by the matcher).
302
+
303
+ ### Subagents — `agents.py`
304
+
305
+ - **`subagent-prompt`** — show the Prompt block outside transcript mode.
306
+ Anchor: `"Backgrounded agent"` + `action:"app:toggleTranscript"`.
307
+ - **`subagent-models`** — write the chosen model into each overridden built-in
308
+ definition (discovered as above): rewrite the `model:"..."` literal when the
309
+ definition has one, insert `model:"...",` right after `agentType:"...",`
310
+ when it doesn't. Both splice at offsets from a fresh discovery pass.
311
+ **The bypass:** one helper ignores the definition's model for a single
312
+ pinned agent (Explore today) — shape
313
+ `function f(def,main){if(def.agentType!==X.agentType||def.source!=="built-in")return def.model;…;return g(main)?PIN:"inherit"}`.
314
+ When the pinned agent (resolved by following `X` back to its
315
+ `X={agentType:"..."}` assignment) is among the overrides, the body is
316
+ rewritten to `return def.model`. Without this, Explore ignores every
317
+ override at runtime — the literal is written but dead.
318
+
319
+ The helper is matched in two pieces on purpose. Its **head** (the
320
+ two-condition guard) identifies it and names the pinned agent; its **body**
321
+ is what gets replaced, and upstream keeps growing it — 2.1.217 inserted a
322
+ `CLAUDE_CODE_DISABLE_EXPLORE_INHERIT_CAP` escape hatch between the two,
323
+ which silently cost every Explore override until the body matcher learned to
324
+ skip intervening brace-free statements. Resolving the agent from the head
325
+ alone is what makes a future body reshape *loud*: we still know an override
326
+ is at stake, so `bypass:<agent>` is a required step that fails, instead of the
327
+ agent's identity vanishing with the match and the step never existing.
328
+
329
+ If the **head** goes too there is no step to fail — nothing left names the
330
+ agent — and no way to tell "upstream stopped pinning" from "the guard
331
+ reshaped". That one is a note instead, on a patch that stays green; it is why
332
+ notes print on green runs.
333
+
334
+ `bypassed_agents` returns each helper's **offset**, and the rewrite is
335
+ anchored there (`_MODEL_BYPASS.match(content, at)`). Identifying one helper
336
+ and then rewriting whichever one a fresh search happens to find first is how
337
+ you neutralise an unrelated helper and report success; anchoring also means a
338
+ build that pins two agents gets both handled, rewritten last-first so earlier
339
+ offsets stay valid.
340
+
341
+ ### Chrome & branding — `chrome.py`
342
+
343
+ - **`spinner-tips`** — force spinner tips off. Anchors: `spinnerTipsEnabled===!1`
344
+ guard and `spinnerTipsEnabled!==!1` expression (two paths). *Value-flip.*
345
+ Each path counts candidates off the **setting name**, not its own regex, so a
346
+ reshaped path reads as `candidates > 0, applied == 0` — a miss — instead of
347
+ the zero that would be indistinguishable from a build that lacks it, with the
348
+ other path carrying the patch to green and tips still showing.
349
+ - **`version-marker`** — append `\n<suffix>` after `}.VERSION} (Claude Code)`
350
+ (default `(patched)`, customisable via `--suffix`; escaped for the template
351
+ literal it lands in).
352
+ - **`branding`** — rename visible `Claude Code` startup/help strings to a
353
+ chosen name. Several string shapes, each its own sub-step. On by default,
354
+ deriving `<username>'s Code`; `--brand NAME` names it explicitly, and selects
355
+ the patch when it is not already in the set.
356
+
357
+ ## Removed patches
358
+
359
+ Kept here so nobody reintroduces them without knowing why they left:
360
+
361
+ - **`word-diff-bg`** — as of 2.1.216 the word spans are nested inside a row
362
+ element that already carries the line background; the fallback could never
363
+ change a pixel. Confirmed redundant in live A/B.
364
+ - **`installer-label`** — its target string left the bundle in ~2.1.186.
365
+ - **`redacted-thinking`** — untestable against the real API (no way to elicit
366
+ a `redacted_thinking` block), and the native-only tool keeps its surface to
367
+ what can be verified.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patch-cc"
7
- version = "0.1.2"
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.2"
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