patch-cc 0.2.0__tar.gz → 0.2.1__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.
- {patch_cc-0.2.0 → patch_cc-0.2.1}/PKG-INFO +34 -21
- {patch_cc-0.2.0 → patch_cc-0.2.1}/README.md +33 -20
- {patch_cc-0.2.0 → patch_cc-0.2.1}/docs/INTERNALS.md +1 -1
- {patch_cc-0.2.0 → patch_cc-0.2.1}/docs/PLAYBOOK.md +48 -3
- patch_cc-0.2.1/docs/demo.gif +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/pyproject.toml +1 -1
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/cache.py +3 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/cli.py +27 -2
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/doctor.py +6 -1
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/menu.py +37 -9
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patcher.py +5 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/__init__.py +2 -4
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/agents.py +12 -3
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/base.py +11 -5
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/chrome.py +56 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/codex.py +2 -2
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/streaming.py +2 -2
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/patches/thinking.py +80 -3
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/ui.py +2 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/uv.lock +1 -1
- {patch_cc-0.2.0 → patch_cc-0.2.1}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/.github/workflows/release.yml +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/.gitignore +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/AGENTS.md +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/CLAUDE.md +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/LICENSE +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/docs/CONDUCT.md +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/blob.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/container.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.2.0 → patch_cc-0.2.1}/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.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -34,8 +34,16 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
An interactive patcher for the **Claude Code native binary**. Pick the tweaks
|
|
36
36
|
you want — inline and live thinking, detailed tool calls, subagent model
|
|
37
|
-
overrides, your own startup name
|
|
38
|
-
|
|
37
|
+
overrides, your own startup name, your **ChatGPT/Codex-plan GPT models as
|
|
38
|
+
native models** — and apply them to your installed `claude` in one keystroke.
|
|
39
|
+
Fully reversible: a pristine backup is kept, `patch-cc restore` puts it back.
|
|
40
|
+
Pure Python; no Node, no Bun.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uvx patch-cc # fullscreen menu, no install needed
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+

|
|
39
47
|
|
|
40
48
|
## Requirements
|
|
41
49
|
|
|
@@ -48,17 +56,14 @@ in one keystroke. Pure Python; no Node, no Bun.
|
|
|
48
56
|
- **macOS only:** the Xcode command line tools, for `codesign` — a patched
|
|
49
57
|
binary has to be re-signed or macOS refuses to run it.
|
|
50
58
|
|
|
51
|
-
```bash
|
|
52
|
-
uvx patch-cc # fullscreen menu, no install needed
|
|
53
|
-
```
|
|
54
|
-
|
|
55
59
|
The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
|
|
56
60
|
press `s` to save. Patches that carry a setting — subagent models, Codex models,
|
|
57
|
-
the startup name, the `--version` marker — open a centered
|
|
58
|
-
the row then shows what you chose. Everything choosable
|
|
59
|
-
names and model aliases are **discovered from your
|
|
60
|
-
Codex ones from your plan), so the menu can never
|
|
61
|
-
would reject. Typing exists only for the genuinely
|
|
61
|
+
the startup name, the `--version` marker, the org/email label — open a centered
|
|
62
|
+
modal on `enter`, and the row then shows what you chose. Everything choosable
|
|
63
|
+
is a picker: the agent names and model aliases are **discovered from your
|
|
64
|
+
binary itself** (and the Codex ones from your plan), so the menu can never
|
|
65
|
+
offer something your build would reject. Typing exists only for the genuinely
|
|
66
|
+
free-text values.
|
|
62
67
|
|
|
63
68
|
A patched binary records what was applied inside itself, so the menu always
|
|
64
69
|
comes up showing the real current state, and `patch-cc status` answers
|
|
@@ -75,17 +80,19 @@ patch-cc # then just run it
|
|
|
75
80
|
|
|
76
81
|
| Group | Patch | |
|
|
77
82
|
|---|---|---|
|
|
78
|
-
| Output &
|
|
83
|
+
| Output & display | Detailed tool calls | Show full read/search calls, not collapsed summaries |
|
|
79
84
|
| | Colour new files as diffs | Created files render with `+` lines and green |
|
|
80
|
-
|
|
|
85
|
+
| | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
|
|
81
86
|
| | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
|
|
82
87
|
| | Stream thinking live | See reasoning as it is generated, inline and in order |
|
|
83
|
-
|
|
|
88
|
+
| | Show subagent prompts | Prompt blocks visible during normal use |
|
|
89
|
+
| Models & effort | Persist max effort | `/effort max` saves as your default for new sessions, like the other levels |
|
|
90
|
+
| | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
|
|
84
91
|
| | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
|
|
85
|
-
| Chrome | Disable spinner tips | No rotating tips on the spinner |
|
|
86
|
-
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
92
|
+
| Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
|
|
87
93
|
| | Mark `--version` | Appends `(patched)` — or any marker you choose |
|
|
88
|
-
|
|
|
94
|
+
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
95
|
+
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it |
|
|
89
96
|
|
|
90
97
|
## Usage
|
|
91
98
|
|
|
@@ -99,6 +106,8 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
|
|
|
99
106
|
uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
|
|
100
107
|
uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
|
|
101
108
|
uvx patch-cc apply --suffix "(mine)" # custom --version marker
|
|
109
|
+
uvx patch-cc apply --org-label # hide the welcome screen's org/email
|
|
110
|
+
uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
|
|
102
111
|
uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
|
|
103
112
|
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
104
113
|
uvx patch-cc status # exactly what is applied
|
|
@@ -108,9 +117,9 @@ uvx patch-cc list # every patch, described
|
|
|
108
117
|
uvx patch-cc restore # put the original back
|
|
109
118
|
```
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
validated against what your installed binary
|
|
113
|
-
against what your plan offers.
|
|
120
|
+
A flag that configures a patch also selects it — `apply --help` lists them
|
|
121
|
+
all. Agents and models are validated against what your installed binary
|
|
122
|
+
actually ships, and Codex model ids against what your plan offers.
|
|
114
123
|
|
|
115
124
|
## Codex models
|
|
116
125
|
|
|
@@ -210,3 +219,7 @@ bundle rather than with clodex's TLS interception.
|
|
|
210
219
|
## License
|
|
211
220
|
|
|
212
221
|
MIT
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
**More agent tooling** — [summon-cc](https://github.com/anfreire/summon-cc): give your agent a crew of Claude Code workers · [cc-oc](https://github.com/anfreire/cc-oc): drive opencode from inside Claude Code · [omoctl](https://github.com/anfreire/omoctl): manage oh-my-openagent profiles · [wiki-spaces](https://github.com/anfreire/wiki-spaces): a wiki your AI agent keeps
|
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
An interactive patcher for the **Claude Code native binary**. Pick the tweaks
|
|
7
7
|
you want — inline and live thinking, detailed tool calls, subagent model
|
|
8
|
-
overrides, your own startup name
|
|
9
|
-
|
|
8
|
+
overrides, your own startup name, your **ChatGPT/Codex-plan GPT models as
|
|
9
|
+
native models** — and apply them to your installed `claude` in one keystroke.
|
|
10
|
+
Fully reversible: a pristine backup is kept, `patch-cc restore` puts it back.
|
|
11
|
+
Pure Python; no Node, no Bun.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uvx patch-cc # fullscreen menu, no install needed
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+

|
|
10
18
|
|
|
11
19
|
## Requirements
|
|
12
20
|
|
|
@@ -19,17 +27,14 @@ in one keystroke. Pure Python; no Node, no Bun.
|
|
|
19
27
|
- **macOS only:** the Xcode command line tools, for `codesign` — a patched
|
|
20
28
|
binary has to be re-signed or macOS refuses to run it.
|
|
21
29
|
|
|
22
|
-
```bash
|
|
23
|
-
uvx patch-cc # fullscreen menu, no install needed
|
|
24
|
-
```
|
|
25
|
-
|
|
26
30
|
The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
|
|
27
31
|
press `s` to save. Patches that carry a setting — subagent models, Codex models,
|
|
28
|
-
the startup name, the `--version` marker — open a centered
|
|
29
|
-
the row then shows what you chose. Everything choosable
|
|
30
|
-
names and model aliases are **discovered from your
|
|
31
|
-
Codex ones from your plan), so the menu can never
|
|
32
|
-
would reject. Typing exists only for the genuinely
|
|
32
|
+
the startup name, the `--version` marker, the org/email label — open a centered
|
|
33
|
+
modal on `enter`, and the row then shows what you chose. Everything choosable
|
|
34
|
+
is a picker: the agent names and model aliases are **discovered from your
|
|
35
|
+
binary itself** (and the Codex ones from your plan), so the menu can never
|
|
36
|
+
offer something your build would reject. Typing exists only for the genuinely
|
|
37
|
+
free-text values.
|
|
33
38
|
|
|
34
39
|
A patched binary records what was applied inside itself, so the menu always
|
|
35
40
|
comes up showing the real current state, and `patch-cc status` answers
|
|
@@ -46,17 +51,19 @@ patch-cc # then just run it
|
|
|
46
51
|
|
|
47
52
|
| Group | Patch | |
|
|
48
53
|
|---|---|---|
|
|
49
|
-
| Output &
|
|
54
|
+
| Output & display | Detailed tool calls | Show full read/search calls, not collapsed summaries |
|
|
50
55
|
| | Colour new files as diffs | Created files render with `+` lines and green |
|
|
51
|
-
|
|
|
56
|
+
| | Fix blank thinking blocks | Opt out of the server-side experiment that can empty every thinking block |
|
|
52
57
|
| | Always show thinking | Thinking blocks stay inline — no `ctrl+o` |
|
|
53
58
|
| | Stream thinking live | See reasoning as it is generated, inline and in order |
|
|
54
|
-
|
|
|
59
|
+
| | Show subagent prompts | Prompt blocks visible during normal use |
|
|
60
|
+
| Models & effort | Persist max effort | `/effort max` saves as your default for new sessions, like the other levels |
|
|
61
|
+
| | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
|
|
55
62
|
| | Override subagent models | Pick the model per built-in agent (discovered from your binary) |
|
|
56
|
-
| Chrome | Disable spinner tips | No rotating tips on the spinner |
|
|
57
|
-
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
63
|
+
| Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
|
|
58
64
|
| | Mark `--version` | Appends `(patched)` — or any marker you choose |
|
|
59
|
-
|
|
|
65
|
+
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
66
|
+
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it |
|
|
60
67
|
|
|
61
68
|
## Usage
|
|
62
69
|
|
|
@@ -70,6 +77,8 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
|
|
|
70
77
|
uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
|
|
71
78
|
uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
|
|
72
79
|
uvx patch-cc apply --suffix "(mine)" # custom --version marker
|
|
80
|
+
uvx patch-cc apply --org-label # hide the welcome screen's org/email
|
|
81
|
+
uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
|
|
73
82
|
uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
|
|
74
83
|
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
75
84
|
uvx patch-cc status # exactly what is applied
|
|
@@ -79,9 +88,9 @@ uvx patch-cc list # every patch, described
|
|
|
79
88
|
uvx patch-cc restore # put the original back
|
|
80
89
|
```
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
validated against what your installed binary
|
|
84
|
-
against what your plan offers.
|
|
91
|
+
A flag that configures a patch also selects it — `apply --help` lists them
|
|
92
|
+
all. Agents and models are validated against what your installed binary
|
|
93
|
+
actually ships, and Codex model ids against what your plan offers.
|
|
85
94
|
|
|
86
95
|
## Codex models
|
|
87
96
|
|
|
@@ -181,3 +190,7 @@ bundle rather than with clodex's TLS interception.
|
|
|
181
190
|
## License
|
|
182
191
|
|
|
183
192
|
MIT
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
**More agent tooling** — [summon-cc](https://github.com/anfreire/summon-cc): give your agent a crew of Claude Code workers · [cc-oc](https://github.com/anfreire/cc-oc): drive opencode from inside Claude Code · [omoctl](https://github.com/anfreire/omoctl): manage oh-my-openagent profiles · [wiki-spaces](https://github.com/anfreire/wiki-spaces): a wiki your AI agent keeps
|
|
@@ -93,7 +93,7 @@ its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it means for matcher health:
|
|
|
93
93
|
|
|
94
94
|
```
|
|
95
95
|
//patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"models":{...},
|
|
96
|
-
"codex":{"port":8817,"models":["gpt-5.6-sol", ...]}}
|
|
96
|
+
"org":...,"codex":{"port":8817,"models":["gpt-5.6-sol", ...]}}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
That line is why `patch-cc status` can name exactly what is applied: several
|
|
@@ -233,9 +233,10 @@ that broke a patch along with every note.
|
|
|
233
233
|
|
|
234
234
|
## Patch reference
|
|
235
235
|
|
|
236
|
-
Grouped by source module
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
Grouped by source module. The menu's three groups (Output & display, Models &
|
|
237
|
+
effort, Chrome & branding) are display categories that each draw from several
|
|
238
|
+
modules — module says where a matcher lives, group says what the patch changes
|
|
239
|
+
for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
239
240
|
|
|
240
241
|
### Output & diffs — `output.py`
|
|
241
242
|
|
|
@@ -288,6 +289,36 @@ anchor, and where it lives.
|
|
|
288
289
|
A column of `0`s is this patch missing (or a bucket it does not yet cover);
|
|
289
290
|
a spread of real lengths means the text arrived and the problem is rendering.
|
|
290
291
|
|
|
292
|
+
- **`max-effort`** — let `/effort max` save as the session default, as
|
|
293
|
+
low–xhigh already do. `max` is first-class in session (the binary's level
|
|
294
|
+
list is `["low","medium","high","xhigh","max"]`; the CLI `--effort` and
|
|
295
|
+
`CLAUDE_CODE_EFFORT_LEVEL` both accept it) — only *persistence* runs through
|
|
296
|
+
two whitelists that stop at xhigh, and both are required rewrites because
|
|
297
|
+
either alone changes nothing:
|
|
298
|
+
|
|
299
|
+
- **`gate`** — one tiny whitelist function
|
|
300
|
+
(`if(e==="low"||…||e==="xhigh")return e;return`), and the whole choke
|
|
301
|
+
point: the `/effort` save path writes `effortLevel` to userSettings only
|
|
302
|
+
when it returns a value, the startup resolver reads the setting back
|
|
303
|
+
through it, and the model-picker flow persists through it too — so write
|
|
304
|
+
and read widen together in one rewrite. The matcher accepts both
|
|
305
|
+
minifier statement forms and the whole-body match keeps it off any other
|
|
306
|
+
function that merely compares levels.
|
|
307
|
+
- **`schema`** — the settings validator's
|
|
308
|
+
`effortLevel:*.enum(["low","medium","high","xhigh"])`, anchored on the
|
|
309
|
+
prop name (the describe-string `Persisted effort level` is the listed
|
|
310
|
+
anchor). Its `.catch(void 0)` is the safety property: a **clean** binary
|
|
311
|
+
reading a settings file that still says `"max"` (baked, then reverted by
|
|
312
|
+
a Claude update) treats it as unset — the default effort, never a broken
|
|
313
|
+
settings parse. Losing the patch costs the preference, nothing else.
|
|
314
|
+
|
|
315
|
+
Both matchers tolerate the level already being present and count it as
|
|
316
|
+
landed — an upstream that adopts `max` persistence itself is the goal
|
|
317
|
+
achieved, not a miss (the same judged-on-achievement rule
|
|
318
|
+
`subagent-models` follows). Effort semantics downstream are untouched: the
|
|
319
|
+
org-limit clamp runs before the save, and a model that cannot run `max`
|
|
320
|
+
clamps at request time exactly as an interactive `/effort max` does today.
|
|
321
|
+
|
|
291
322
|
- **`thinking-inline`** — make historical thinking blocks render inline.
|
|
292
323
|
Anchor: `case"thinking":` containing `isTranscriptMode:`. Two rewrites:
|
|
293
324
|
remove the early null-return (both `if(!a&&!b)return null;` and the 2.1.216
|
|
@@ -516,6 +547,20 @@ anchor, and where it lives.
|
|
|
516
547
|
chosen name. Several string shapes, each its own sub-step. On by default,
|
|
517
548
|
deriving `<username>'s Code`; `--brand NAME` names it explicitly, and selects
|
|
518
549
|
the patch when it is not already in the set.
|
|
550
|
+
- **`org-label`** — replace or hide the welcome screen's third segment
|
|
551
|
+
(`model · plan · <organizationName>`; for a personal claude.ai account the
|
|
552
|
+
org name *is* the account email). Anchor: the one conditional template
|
|
553
|
+
joining `.organizationName` behind `!process.env.IS_DEMO`, whose false
|
|
554
|
+
branch is upstream's own no-org shape `` `${model} \xB7 ${plan}` ``. An
|
|
555
|
+
empty label (`--org-label` with no value — empty is the *value* "hide", not
|
|
556
|
+
"unset") emits that false branch verbatim, so the separator leaves with the
|
|
557
|
+
segment; a label takes the org text's place, escaped for the template
|
|
558
|
+
literal it lands in. Candidates count off the `\xB7 ${*.organizationName}`
|
|
559
|
+
composition, so a reshaped ternary reads as `candidates > 0, applied == 0`
|
|
560
|
+
— a matcher to repair — rather than the zero that would equally mean
|
|
561
|
+
upstream retired the segment. Only this line is touched: the `/status`
|
|
562
|
+
Organization/Email rows, the login screen, and the org's startup message
|
|
563
|
+
(`"Message from <org>:"`) still show the real account.
|
|
519
564
|
|
|
520
565
|
## Removed patches
|
|
521
566
|
|
|
Binary file
|
|
@@ -59,6 +59,7 @@ def load_strict() -> Selection | None:
|
|
|
59
59
|
known = set(ids())
|
|
60
60
|
brand = data.get("brand", DEFAULT_BRAND)
|
|
61
61
|
suffix = data.get("suffix", DEFAULT_SUFFIX)
|
|
62
|
+
org = data.get("org_label", "")
|
|
62
63
|
models = data.get("subagent_models", {})
|
|
63
64
|
# Codex ids only, the same as the manifest keeps: a name and a context
|
|
64
65
|
# window are the plan's to report, and remembering either would let a
|
|
@@ -72,6 +73,7 @@ def load_strict() -> Selection | None:
|
|
|
72
73
|
version_suffix=suffix
|
|
73
74
|
if isinstance(suffix, str) and suffix
|
|
74
75
|
else DEFAULT_SUFFIX,
|
|
76
|
+
org_label=org if isinstance(org, str) else "",
|
|
75
77
|
subagent_models={
|
|
76
78
|
a: m
|
|
77
79
|
for a, m in models.items()
|
|
@@ -98,6 +100,7 @@ def save(selection: Selection) -> None:
|
|
|
98
100
|
"patches": selection.patches,
|
|
99
101
|
"brand": selection.options.brand,
|
|
100
102
|
"suffix": selection.options.version_suffix,
|
|
103
|
+
"org_label": selection.options.org_label,
|
|
101
104
|
"subagent_models": selection.options.subagent_models,
|
|
102
105
|
"codex_models": [m.id for m in selection.options.codex_models],
|
|
103
106
|
"codex_port": selection.options.codex_port,
|
|
@@ -4,7 +4,8 @@ Bare ``patch-cc`` opens the interactive menu. Every action is also a
|
|
|
4
4
|
subcommand so nothing needs the TUI:
|
|
5
5
|
|
|
6
6
|
patch-cc apply [PATCH ...] [--brand [NAME]] [--model AGENT=MODEL]
|
|
7
|
-
[--suffix TEXT] [--
|
|
7
|
+
[--suffix TEXT] [--org-label [TEXT]]
|
|
8
|
+
[--codex MODEL] [--codex-port N]
|
|
8
9
|
patch-cc status
|
|
9
10
|
patch-cc doctor [PATH] # PATH: check any binary, e.g. an old backup
|
|
10
11
|
patch-cc list
|
|
@@ -102,6 +103,12 @@ def _list_hint(patch: Patch, cached: cache.Selection) -> str:
|
|
|
102
103
|
parts.append(f"default {DEFAULT_SUFFIX!r}")
|
|
103
104
|
if opts.version_suffix != DEFAULT_SUFFIX:
|
|
104
105
|
parts.append(f"cached {opts.version_suffix!r}")
|
|
106
|
+
elif patch.id == "org-label":
|
|
107
|
+
parts.append("default hides it")
|
|
108
|
+
if "org-label" in cached.patches:
|
|
109
|
+
parts.append(
|
|
110
|
+
f"cached {opts.org_label!r}" if opts.org_label else "cached hidden"
|
|
111
|
+
)
|
|
105
112
|
elif patch.id == "subagent-models" and opts.subagent_models:
|
|
106
113
|
picks = ", ".join(f"{a}={m}" for a, m in opts.subagent_models.items())
|
|
107
114
|
parts.append(f"cached {picks}")
|
|
@@ -218,6 +225,11 @@ def _requested(args, source: str) -> tuple[list[str], Options]:
|
|
|
218
225
|
if "version-marker" not in selected:
|
|
219
226
|
selected.append("version-marker")
|
|
220
227
|
|
|
228
|
+
if args.org_label is not None:
|
|
229
|
+
options.org_label = args.org_label.strip()
|
|
230
|
+
if "org-label" not in selected:
|
|
231
|
+
selected.append("org-label")
|
|
232
|
+
|
|
221
233
|
# Order-preserving, and deduplicated: a repeated `--codex` is one model asked
|
|
222
234
|
# for twice, and every array it reaches -- the enum, the routing list, the
|
|
223
235
|
# remembered selection, the manifest `status` reads back -- would otherwise
|
|
@@ -272,6 +284,7 @@ def _has_selection_args(args) -> bool:
|
|
|
272
284
|
or args.brand is not None
|
|
273
285
|
or args.model
|
|
274
286
|
or args.suffix
|
|
287
|
+
or args.org_label is not None
|
|
275
288
|
or args.codex
|
|
276
289
|
or args.codex_port is not None
|
|
277
290
|
)
|
|
@@ -332,7 +345,8 @@ def _from_cache(args, source: str) -> tuple[list[str], Options]:
|
|
|
332
345
|
if _has_selection_args(args):
|
|
333
346
|
err(
|
|
334
347
|
"--from-cache replays your last interactive selection; do not combine "
|
|
335
|
-
"it with patch ids or --brand / --model / --suffix / --
|
|
348
|
+
"it with patch ids or --brand / --model / --suffix / --org-label / "
|
|
349
|
+
"--codex."
|
|
336
350
|
)
|
|
337
351
|
raise SystemExit(2)
|
|
338
352
|
if not cache.cache_path().exists():
|
|
@@ -494,6 +508,8 @@ def cmd_status(args) -> int:
|
|
|
494
508
|
console.print(f" brand: {st.manifest['brand']}")
|
|
495
509
|
if "suffix" in st.manifest:
|
|
496
510
|
console.print(f" suffix: {st.manifest['suffix']}")
|
|
511
|
+
if "org" in st.manifest:
|
|
512
|
+
console.print(f" org: {st.manifest['org'] or '(hidden)'}")
|
|
497
513
|
for agent, model in (st.manifest.get("models") or {}).items():
|
|
498
514
|
console.print(f" model: {agent} = {model}")
|
|
499
515
|
codex = st.manifest.get("codex")
|
|
@@ -863,6 +879,7 @@ def _apply_epilog() -> str:
|
|
|
863
879
|
"configuring patches:",
|
|
864
880
|
f" --brand [NAME] selects branding · no value -> {derived_brand()!r}",
|
|
865
881
|
f" --suffix TEXT selects version-marker · default {DEFAULT_SUFFIX!r}",
|
|
882
|
+
" --org-label [TEXT] selects org-label · no value -> hide the segment",
|
|
866
883
|
" --model AGENT=MODEL selects subagent-models · repeatable, one per agent",
|
|
867
884
|
]
|
|
868
885
|
if agents:
|
|
@@ -1004,6 +1021,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1004
1021
|
help="`claude --version` marker text; selects `version-marker` "
|
|
1005
1022
|
"(default: (patched))",
|
|
1006
1023
|
)
|
|
1024
|
+
p_apply.add_argument(
|
|
1025
|
+
"--org-label",
|
|
1026
|
+
nargs="?",
|
|
1027
|
+
const="",
|
|
1028
|
+
metavar="TEXT",
|
|
1029
|
+
help="welcome-screen org/email text; selects `org-label` "
|
|
1030
|
+
"(no value: hide the segment)",
|
|
1031
|
+
)
|
|
1007
1032
|
p_apply.add_argument(
|
|
1008
1033
|
"--codex",
|
|
1009
1034
|
action="append",
|
|
@@ -110,7 +110,12 @@ def _synthetic_options(agents: list[BuiltinAgent], models: list[str]) -> Options
|
|
|
110
110
|
if (target := next((m for m in models if m != agent.effective_model), None))
|
|
111
111
|
}
|
|
112
112
|
return Options(
|
|
113
|
-
brand="patch-cc doctor",
|
|
113
|
+
brand="patch-cc doctor",
|
|
114
|
+
subagent_models=overrides,
|
|
115
|
+
codex_models=codex,
|
|
116
|
+
# The replace branch; the hidden form differs only in the emitted tail,
|
|
117
|
+
# so one configuration holds the whole matcher in the net.
|
|
118
|
+
org_label="patch-cc doctor",
|
|
114
119
|
)
|
|
115
120
|
|
|
116
121
|
|
|
@@ -4,9 +4,10 @@ A fullscreen frame: fixed title and status on top, fixed key hints at the
|
|
|
4
4
|
bottom, and only the patch list scrolling in between. Every choice is a picker
|
|
5
5
|
driven by what the binary itself offers -- agents and models are discovered,
|
|
6
6
|
never typed -- and typing exists only where a value is genuinely free text
|
|
7
|
-
(the startup name, the --version marker). All
|
|
8
|
-
centered modals floating over the dimmed list; the
|
|
9
|
-
sub-rows. ``s`` saves, and the same frame then shows
|
|
7
|
+
(the startup name, the --version marker, the org/email label). All
|
|
8
|
+
configuration happens in centered modals floating over the dimmed list; the
|
|
9
|
+
list itself never grows sub-rows. ``s`` saves, and the same frame then shows
|
|
10
|
+
the per-patch results.
|
|
10
11
|
|
|
11
12
|
The engine is deliberately small: ``blessed`` owns the terminal (fullscreen,
|
|
12
13
|
cbreak, parsed keystrokes, live size) and Rich owns every pixel drawn. A frame
|
|
@@ -43,6 +44,7 @@ from .bun import Bundle, BunError, container
|
|
|
43
44
|
from .codex import DEFAULT_PORT, is_valid_port
|
|
44
45
|
from .codex.models import CodexModel, discover, reconcile
|
|
45
46
|
from .patches import (
|
|
47
|
+
ALL_PATCHES,
|
|
46
48
|
DEFAULT_BRAND,
|
|
47
49
|
DEFAULT_SUFFIX,
|
|
48
50
|
GROUP_ORDER,
|
|
@@ -83,8 +85,11 @@ def _hints(*pairs: tuple[str, str]) -> Text:
|
|
|
83
85
|
return text
|
|
84
86
|
|
|
85
87
|
|
|
86
|
-
#: Patches whose row opens a modal on enter instead of plain toggling
|
|
87
|
-
|
|
88
|
+
#: Patches whose row opens a modal on enter instead of plain toggling --
|
|
89
|
+
#: exactly the ones that carry a configurable value, which is what
|
|
90
|
+
#: :attr:`Patch.option` declares. Derived, so a new option-carrying patch
|
|
91
|
+
#: cannot be left off this list with its hint reading "enter toggle".
|
|
92
|
+
_CONFIGURABLE = {patch.id for patch in ALL_PATCHES if patch.option}
|
|
88
93
|
|
|
89
94
|
|
|
90
95
|
# ----------------------------------------------------------------- rows
|
|
@@ -182,6 +187,7 @@ class MenuModel:
|
|
|
182
187
|
model.text_rows["suffix"] = TextRow(
|
|
183
188
|
"suffix", "marker", seed.options.version_suffix
|
|
184
189
|
)
|
|
190
|
+
model.text_rows["org"] = TextRow("org", "label", seed.options.org_label)
|
|
185
191
|
return model
|
|
186
192
|
|
|
187
193
|
def _seed(self) -> cache.Selection:
|
|
@@ -191,6 +197,7 @@ class MenuModel:
|
|
|
191
197
|
codex = manifest.get("codex")
|
|
192
198
|
codex = codex if isinstance(codex, dict) else {}
|
|
193
199
|
port = codex.get("port")
|
|
200
|
+
org = manifest.get("org")
|
|
194
201
|
seed = cache.Selection()
|
|
195
202
|
seed.patches = [
|
|
196
203
|
p for p in manifest.get("patches", []) if isinstance(p, str)
|
|
@@ -198,6 +205,9 @@ class MenuModel:
|
|
|
198
205
|
seed.options = Options(
|
|
199
206
|
brand=manifest.get("brand") or DEFAULT_BRAND,
|
|
200
207
|
version_suffix=manifest.get("suffix") or DEFAULT_SUFFIX,
|
|
208
|
+
# `or ""` would also be right, but only by luck: "" (hidden) is
|
|
209
|
+
# a meaningful manifest value, unlike an absent brand/suffix.
|
|
210
|
+
org_label=org if isinstance(org, str) else "",
|
|
201
211
|
subagent_models={
|
|
202
212
|
a: m
|
|
203
213
|
for a, m in (manifest.get("models") or {}).items()
|
|
@@ -269,6 +279,10 @@ class MenuModel:
|
|
|
269
279
|
options.version_suffix = (
|
|
270
280
|
self.text_rows["suffix"].value.strip() or DEFAULT_SUFFIX
|
|
271
281
|
)
|
|
282
|
+
if "org-label" in selected:
|
|
283
|
+
# Empty stays empty: it is the "hide the segment" value, so unlike
|
|
284
|
+
# brand and suffix there is no default to fall back to.
|
|
285
|
+
options.org_label = self.text_rows["org"].value.strip()
|
|
272
286
|
# A subagent pinned to a Codex model needs codex-models applied too -- it
|
|
273
287
|
# registers the id and installs the redirect, without which the pin
|
|
274
288
|
# resolves to nothing. That coupling is enforced where the user can see
|
|
@@ -709,15 +723,20 @@ class MenuApp:
|
|
|
709
723
|
def _activate(self, row: PatchRow) -> None:
|
|
710
724
|
"""Enter on a patch: toggle plain ones, configure configurable ones."""
|
|
711
725
|
patch_id = row.patch.id
|
|
726
|
+
text_keys = {
|
|
727
|
+
"branding": "brand",
|
|
728
|
+
"version-marker": "suffix",
|
|
729
|
+
"org-label": "org",
|
|
730
|
+
}
|
|
712
731
|
if patch_id == "subagent-models":
|
|
713
732
|
if not self.model.agent_rows:
|
|
714
733
|
self.flash = "no agents discovered in this bundle"
|
|
715
734
|
return
|
|
716
735
|
row.on = True
|
|
717
736
|
self._open_agents_modal()
|
|
718
|
-
elif patch_id in
|
|
737
|
+
elif patch_id in text_keys:
|
|
719
738
|
row.on = True
|
|
720
|
-
self._open_text_modal(
|
|
739
|
+
self._open_text_modal(text_keys[patch_id])
|
|
721
740
|
elif patch_id == "codex-models":
|
|
722
741
|
self._open_codex_menu(row) # submenu: pick models / set gateway port
|
|
723
742
|
else:
|
|
@@ -813,10 +832,16 @@ class MenuApp:
|
|
|
813
832
|
|
|
814
833
|
def _open_text_modal(self, key: str) -> None:
|
|
815
834
|
row = self.model.text_rows[key]
|
|
816
|
-
titles = {
|
|
835
|
+
titles = {
|
|
836
|
+
"brand": "Startup name",
|
|
837
|
+
"suffix": "--version marker",
|
|
838
|
+
"org": "Org/email label (empty hides it)",
|
|
839
|
+
}
|
|
817
840
|
|
|
818
841
|
def entered(value: object) -> None:
|
|
819
|
-
|
|
842
|
+
# For the org label an emptied field is the point -- it means hide
|
|
843
|
+
# the segment -- where an empty brand or marker means "never mind".
|
|
844
|
+
if isinstance(value, str) and (value.strip() or key == "org"):
|
|
820
845
|
row.value = value.strip()
|
|
821
846
|
|
|
822
847
|
self._push(InputModal(titles[key], row.value), entered)
|
|
@@ -1412,6 +1437,9 @@ class MenuApp:
|
|
|
1412
1437
|
return Text(model.text_rows["brand"].value, style=_VALUE)
|
|
1413
1438
|
if row.patch.id == "version-marker":
|
|
1414
1439
|
return Text(model.text_rows["suffix"].value, style=_VALUE)
|
|
1440
|
+
if row.patch.id == "org-label":
|
|
1441
|
+
value = model.text_rows["org"].value
|
|
1442
|
+
return Text(value, style=_VALUE) if value else Text("hidden", style="dim")
|
|
1415
1443
|
if row.patch.id == "codex-models":
|
|
1416
1444
|
count = len(model.codex_models)
|
|
1417
1445
|
if not count:
|
|
@@ -92,6 +92,11 @@ def manifest_payload(landed: list[str], options: Options) -> dict:
|
|
|
92
92
|
payload["suffix"] = options.version_suffix
|
|
93
93
|
if options.subagent_models and "subagent-models" in applied:
|
|
94
94
|
payload["models"] = options.subagent_models
|
|
95
|
+
if "org-label" in applied:
|
|
96
|
+
# Recorded even when empty: "" is the asked-for value (hide the
|
|
97
|
+
# segment), and omitting it would make hidden indistinguishable from
|
|
98
|
+
# never-configured when the menu seeds from this manifest.
|
|
99
|
+
payload["org"] = options.org_label
|
|
95
100
|
if options.codex_models and "codex-models" in applied:
|
|
96
101
|
# Ids and a port -- the whole of what was asked for. A model's name and
|
|
97
102
|
# context window are baked into the bundle itself (the picker rows, the
|
|
@@ -19,11 +19,9 @@ from . import agents, chrome, codex, output, streaming, thinking
|
|
|
19
19
|
from .base import (
|
|
20
20
|
DEFAULT_BRAND,
|
|
21
21
|
DEFAULT_SUFFIX,
|
|
22
|
-
GROUP_AGENTS,
|
|
23
22
|
GROUP_CHROME,
|
|
24
|
-
|
|
23
|
+
GROUP_MODELS,
|
|
25
24
|
GROUP_OUTPUT,
|
|
26
|
-
GROUP_THINKING,
|
|
27
25
|
SENTINEL,
|
|
28
26
|
Options,
|
|
29
27
|
Outcome,
|
|
@@ -40,7 +38,7 @@ ALL_PATCHES: list[Patch] = [
|
|
|
40
38
|
*chrome.PATCHES,
|
|
41
39
|
]
|
|
42
40
|
|
|
43
|
-
GROUP_ORDER = [GROUP_OUTPUT,
|
|
41
|
+
GROUP_ORDER = [GROUP_OUTPUT, GROUP_MODELS, GROUP_CHROME]
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
def ids() -> list[str]:
|
|
@@ -16,7 +16,16 @@ from __future__ import annotations
|
|
|
16
16
|
import re
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
|
|
19
|
-
from .base import
|
|
19
|
+
from .base import (
|
|
20
|
+
GROUP_MODELS,
|
|
21
|
+
GROUP_OUTPUT,
|
|
22
|
+
IDENT,
|
|
23
|
+
Options,
|
|
24
|
+
Outcome,
|
|
25
|
+
Patch,
|
|
26
|
+
compile_js,
|
|
27
|
+
splice,
|
|
28
|
+
)
|
|
20
29
|
|
|
21
30
|
# ------------------------------------------------------- prompt visibility
|
|
22
31
|
|
|
@@ -366,7 +375,7 @@ PATCHES = [
|
|
|
366
375
|
id="subagent-prompt",
|
|
367
376
|
title="Show subagent prompts",
|
|
368
377
|
summary="Show a subagent's Prompt block during normal use, not only in transcript mode.",
|
|
369
|
-
group=
|
|
378
|
+
group=GROUP_OUTPUT,
|
|
370
379
|
fn=_subagent_prompt,
|
|
371
380
|
anchors=('"Backgrounded agent"', 'action:"app:toggleTranscript"'),
|
|
372
381
|
),
|
|
@@ -374,7 +383,7 @@ PATCHES = [
|
|
|
374
383
|
id="subagent-models",
|
|
375
384
|
title="Override subagent models",
|
|
376
385
|
summary="Choose the default model for the built-in agents found in your binary.",
|
|
377
|
-
group=
|
|
386
|
+
group=GROUP_MODELS,
|
|
378
387
|
fn=_subagent_models,
|
|
379
388
|
default=False,
|
|
380
389
|
option="--model",
|
|
@@ -37,11 +37,12 @@ if TYPE_CHECKING:
|
|
|
37
37
|
#: indistinguishable from the 11 that upstream already ships.
|
|
38
38
|
SENTINEL = "__cc_"
|
|
39
39
|
|
|
40
|
-
# Groups, in display order
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
# Groups, in display order: what renders, what runs (and how hard), how it is
|
|
41
|
+
# dressed. Generic on purpose -- a group named after a feature (Thinking,
|
|
42
|
+
# Subagents, Codex) fits exactly that feature, and the next patch that is
|
|
43
|
+
# about behaviour rather than a surface has no home.
|
|
44
|
+
GROUP_OUTPUT = "Output & display"
|
|
45
|
+
GROUP_MODELS = "Models & effort"
|
|
45
46
|
GROUP_CHROME = "Chrome & branding"
|
|
46
47
|
|
|
47
48
|
#: Default brand: the name shown unless the user overrides it. One home so the
|
|
@@ -88,6 +89,11 @@ class Options:
|
|
|
88
89
|
#: from the one home (:data:`patch_cc.codex.DEFAULT_PORT`) so the number
|
|
89
90
|
#: never lives in two places.
|
|
90
91
|
codex_port: int = DEFAULT_PORT
|
|
92
|
+
#: The welcome screen's org segment (for personal claude.ai accounts,
|
|
93
|
+
#: upstream shows the account email there). Empty is a *value* -- hide the
|
|
94
|
+
#: segment -- not "unset": whether the patch acts at all is the selection's
|
|
95
|
+
#: question, never this field's.
|
|
96
|
+
org_label: str = ""
|
|
91
97
|
|
|
92
98
|
@property
|
|
93
99
|
def rebrands(self) -> bool:
|
|
@@ -175,6 +175,52 @@ def _branding(content: str, options: Options, outcome: Outcome) -> str:
|
|
|
175
175
|
return output
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
# ---------------------------------------------------------- startup org label
|
|
179
|
+
|
|
180
|
+
# The separator as the bundle may spell it: today's bundler writes the `·` as
|
|
181
|
+
# the escape text `\xB7`, and a charset flip would emit the raw character. Both
|
|
182
|
+
# spell the same string, so both are accepted -- a representation variant like
|
|
183
|
+
# the minifier's brace flips, not a second shape.
|
|
184
|
+
_SEP = r"(?:\\xB7|\xB7)"
|
|
185
|
+
|
|
186
|
+
# The welcome screen's info line: `model · plan` -- and, when the account has an
|
|
187
|
+
# organizationName (for personal claude.ai accounts, that is the account email),
|
|
188
|
+
# ` · <organizationName>` after it. One conditional template, and upstream ships
|
|
189
|
+
# the no-org variant as its own false branch, so hiding the segment is selecting
|
|
190
|
+
# a shape the bundle already carries -- separator and all -- not inventing one.
|
|
191
|
+
_ORG_LINE = compile_js(
|
|
192
|
+
rf"!process\.env\.IS_DEMO&&({IDENT})\?\.organizationName\?"
|
|
193
|
+
rf"`\$\{{({IDENT})\}} {_SEP} \$\{{({IDENT})\}} {_SEP} \$\{{\1\.organizationName\}}`"
|
|
194
|
+
rf":`\$\{{\2\}} {_SEP} \$\{{\3\}}`"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
#: The composition itself -- a separator joining the account's organizationName
|
|
198
|
+
#: into a template -- is the durable fingerprint; candidates count off it so a
|
|
199
|
+
#: reshaped ternary reads as "found it, rewrote nothing" (a matcher to repair)
|
|
200
|
+
#: instead of the zero that would equally mean upstream retired the segment.
|
|
201
|
+
_ORG_NEEDLE = compile_js(rf"{_SEP} \$\{{{IDENT}\.organizationName\}}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _org_label(content: str, options: Options, outcome: Outcome) -> str:
|
|
205
|
+
"""Replace -- or, with an empty label, drop -- the welcome line's org segment.
|
|
206
|
+
|
|
207
|
+
One rule builds the replacement either way: the line is `model · plan`,
|
|
208
|
+
plus ` · <label>` exactly when there is a label. Empty therefore removes
|
|
209
|
+
the separator with the segment, and a custom label takes the org text's
|
|
210
|
+
place. The `IS_DEMO`/has-org gate goes with the ternary: a baked label is
|
|
211
|
+
unconditional by construction, and the hidden form needs no gate at all.
|
|
212
|
+
"""
|
|
213
|
+
outcome.candidates += len(_ORG_NEEDLE.findall(content))
|
|
214
|
+
label = options.org_label
|
|
215
|
+
tail = f" \\xB7 {_js_template_escape(label)}" if label else ""
|
|
216
|
+
|
|
217
|
+
def rewrite(match: re.Match[str]) -> str:
|
|
218
|
+
outcome.applied += 1
|
|
219
|
+
return f"`${{{match.group(2)}}} \\xB7 ${{{match.group(3)}}}{tail}`"
|
|
220
|
+
|
|
221
|
+
return _ORG_LINE.sub(rewrite, content)
|
|
222
|
+
|
|
223
|
+
|
|
178
224
|
PATCHES = [
|
|
179
225
|
Patch(
|
|
180
226
|
id="spinner-tips",
|
|
@@ -203,4 +249,14 @@ PATCHES = [
|
|
|
203
249
|
option="--brand",
|
|
204
250
|
anchors=('"Welcome to Claude Code"', '{bold:!0},"Claude Code"'),
|
|
205
251
|
),
|
|
252
|
+
Patch(
|
|
253
|
+
id="org-label",
|
|
254
|
+
title="Startup org/email label",
|
|
255
|
+
summary="Replace or hide the org/email shown after the plan name on the welcome screen.",
|
|
256
|
+
group=GROUP_CHROME,
|
|
257
|
+
fn=_org_label,
|
|
258
|
+
default=False,
|
|
259
|
+
option="--org-label",
|
|
260
|
+
anchors=("organizationName", "IS_DEMO"),
|
|
261
|
+
),
|
|
206
262
|
]
|
|
@@ -50,7 +50,7 @@ import re
|
|
|
50
50
|
from ..codex.models import family_aliases, family_of
|
|
51
51
|
from .agents import MODEL_ENUM
|
|
52
52
|
from .base import (
|
|
53
|
-
|
|
53
|
+
GROUP_MODELS,
|
|
54
54
|
IDENT,
|
|
55
55
|
Options,
|
|
56
56
|
Outcome,
|
|
@@ -507,7 +507,7 @@ PATCHES = [
|
|
|
507
507
|
id="codex-models",
|
|
508
508
|
title="Codex models",
|
|
509
509
|
summary="Register your OpenAI/Codex models and route them to the local gateway.",
|
|
510
|
-
group=
|
|
510
|
+
group=GROUP_MODELS,
|
|
511
511
|
fn=_codex_models,
|
|
512
512
|
default=False,
|
|
513
513
|
option="--codex",
|
|
@@ -23,7 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
import re
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
|
|
26
|
-
from .base import
|
|
26
|
+
from .base import GROUP_OUTPUT, 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=
|
|
977
|
+
group=GROUP_OUTPUT,
|
|
978
978
|
fn=_live_thinking,
|
|
979
979
|
anchors=(
|
|
980
980
|
"onStreamingThinking:",
|
|
@@ -22,6 +22,9 @@ Rendering is only half of it. A fourth gate is not in the UI at all: the
|
|
|
22
22
|
account's server-side experiment bucket, which decides whether the API puts any
|
|
23
23
|
text in the thinking blocks it returns. `thinking-summaries` is that half, and
|
|
24
24
|
without it the other three render an empty string perfectly.
|
|
25
|
+
|
|
26
|
+
`max-effort` lives here too: effort is thinking depth, so the module that owns
|
|
27
|
+
how thinking is shown also owns how hard it is asked for.
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
from __future__ import annotations
|
|
@@ -29,7 +32,8 @@ from __future__ import annotations
|
|
|
29
32
|
import re
|
|
30
33
|
|
|
31
34
|
from .base import (
|
|
32
|
-
|
|
35
|
+
GROUP_MODELS,
|
|
36
|
+
GROUP_OUTPUT,
|
|
33
37
|
IDENT,
|
|
34
38
|
Options,
|
|
35
39
|
Outcome,
|
|
@@ -189,12 +193,73 @@ def _thinking_summaries(content: str, _options: Options, outcome: Outcome) -> st
|
|
|
189
193
|
return _CLIENT_BUCKET.sub(rewrite, content)
|
|
190
194
|
|
|
191
195
|
|
|
196
|
+
# ------------------------------------------------------------- max effort
|
|
197
|
+
#
|
|
198
|
+
# `max` is a first-class level in session -- the level list the binary ships is
|
|
199
|
+
# ["low","medium","high","xhigh","max"], and the CLI and env var both accept it
|
|
200
|
+
# -- but persistence runs through two whitelists that stop at xhigh. Both are
|
|
201
|
+
# widened; either alone changes nothing (docs/PLAYBOOK.md), which is also why
|
|
202
|
+
# both steps are required.
|
|
203
|
+
#
|
|
204
|
+
# The gate is one tiny function, and it is the whole choke point: the /effort
|
|
205
|
+
# save path writes settings only when it returns a value, the startup resolver
|
|
206
|
+
# reads settings back through it, and the model-picker flow persists through it
|
|
207
|
+
# too. The statement variants and the optional `||x==="max"` follow the matcher
|
|
208
|
+
# rules: minifiers flip `return e;` and `{return e}`, and a build where upstream
|
|
209
|
+
# has adopted the level itself is the goal already achieved, not a miss.
|
|
210
|
+
_EFFORT_GATE = compile_js(
|
|
211
|
+
rf"function ({IDENT})\(({IDENT})\)\{{"
|
|
212
|
+
rf'if\(\2==="low"\|\|\2==="medium"\|\|\2==="high"\|\|\2==="xhigh"'
|
|
213
|
+
rf'(\|\|\2==="max")?\)'
|
|
214
|
+
rf"(?:\{{return \2\}}|return \2;?)return;?\}}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# The persisted-settings schema entry. Its zod chain ends in `.catch(void 0)`,
|
|
218
|
+
# so a clean binary reading a settings file that carries "max" (baked by us,
|
|
219
|
+
# then reverted by a Claude update) treats it as unset rather than failing the
|
|
220
|
+
# whole settings parse -- the property that makes this patch safe to lose.
|
|
221
|
+
_EFFORT_SCHEMA = compile_js(
|
|
222
|
+
rf'effortLevel:({IDENT})\.enum\(\["low","medium","high","xhigh"(,"max")?\]\)'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _max_effort(content: str, _options: Options, outcome: Outcome) -> str:
|
|
227
|
+
"""Let ``/effort max`` save as the default for new sessions."""
|
|
228
|
+
gate = outcome.step("gate", expect=True)
|
|
229
|
+
schema = outcome.step("schema", expect=True)
|
|
230
|
+
|
|
231
|
+
def widen_gate(match: re.Match[str]) -> str:
|
|
232
|
+
gate.candidates += 1
|
|
233
|
+
gate.applied += 1
|
|
234
|
+
if match.group(3):
|
|
235
|
+
return match.group(0)
|
|
236
|
+
name, value = match.group(1), match.group(2)
|
|
237
|
+
return (
|
|
238
|
+
f"function {name}({value}){{"
|
|
239
|
+
f'if({value}==="low"||{value}==="medium"||{value}==="high"'
|
|
240
|
+
f'||{value}==="xhigh"||{value}==="max")return {value};return}}'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
output = _EFFORT_GATE.sub(widen_gate, content)
|
|
244
|
+
|
|
245
|
+
def widen_schema(match: re.Match[str]) -> str:
|
|
246
|
+
schema.candidates += 1
|
|
247
|
+
schema.applied += 1
|
|
248
|
+
if match.group(2):
|
|
249
|
+
return match.group(0)
|
|
250
|
+
return (
|
|
251
|
+
f'effortLevel:{match.group(1)}.enum(["low","medium","high","xhigh","max"])'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return _EFFORT_SCHEMA.sub(widen_schema, output)
|
|
255
|
+
|
|
256
|
+
|
|
192
257
|
PATCHES = [
|
|
193
258
|
Patch(
|
|
194
259
|
id="thinking-summaries",
|
|
195
260
|
title="Fix blank thinking blocks",
|
|
196
261
|
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
|
-
group=
|
|
262
|
+
group=GROUP_OUTPUT,
|
|
198
263
|
fn=_thinking_summaries,
|
|
199
264
|
anchors=(_BUCKET_HEADER, "?.atis"),
|
|
200
265
|
),
|
|
@@ -202,8 +267,20 @@ PATCHES = [
|
|
|
202
267
|
id="thinking-inline",
|
|
203
268
|
title="Always show thinking",
|
|
204
269
|
summary="Render thinking blocks inline instead of hiding them behind ctrl+o.",
|
|
205
|
-
group=
|
|
270
|
+
group=GROUP_OUTPUT,
|
|
206
271
|
fn=_thinking_inline,
|
|
207
272
|
anchors=('case"thinking":', "isTranscriptMode:", "latestThinkingSummary"),
|
|
208
273
|
),
|
|
274
|
+
Patch(
|
|
275
|
+
id="max-effort",
|
|
276
|
+
title="Persist max effort",
|
|
277
|
+
summary="Let /effort max save as your default for new sessions, like the other levels.",
|
|
278
|
+
group=GROUP_MODELS,
|
|
279
|
+
fn=_max_effort,
|
|
280
|
+
anchors=(
|
|
281
|
+
'==="low"||',
|
|
282
|
+
'"low","medium","high","xhigh"]',
|
|
283
|
+
"Persisted effort level",
|
|
284
|
+
),
|
|
285
|
+
),
|
|
209
286
|
]
|
|
@@ -57,6 +57,8 @@ def applied_value(patch: Patch, outcome: Outcome, options: Options) -> str | Non
|
|
|
57
57
|
return options.brand
|
|
58
58
|
if patch.id == "version-marker":
|
|
59
59
|
return options.version_suffix
|
|
60
|
+
if patch.id == "org-label":
|
|
61
|
+
return options.org_label or "hidden"
|
|
60
62
|
if patch.id == "subagent-models" and options.subagent_models:
|
|
61
63
|
return ", ".join(f"{a}={m}" for a, m in options.subagent_models.items())
|
|
62
64
|
if patch.id == "codex-models" and options.codex_models:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|