patch-cc 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- patch_cc/bun/__init__.py +6 -0
- patch_cc/bun/blob.py +256 -0
- patch_cc/bun/container.py +129 -0
- patch_cc/bun/elf.py +281 -0
- patch_cc/bun/errors.py +12 -0
- patch_cc/bun/macho.py +124 -0
- patch_cc/cache.py +86 -0
- patch_cc/cli.py +399 -0
- patch_cc/doctor.py +116 -0
- patch_cc/locate.py +117 -0
- patch_cc/menu.py +1135 -0
- patch_cc/patcher.py +244 -0
- patch_cc/patches/__init__.py +75 -0
- patch_cc/patches/agents.py +289 -0
- patch_cc/patches/base.py +200 -0
- patch_cc/patches/chrome.py +187 -0
- patch_cc/patches/output.py +173 -0
- patch_cc/patches/streaming.py +922 -0
- patch_cc/patches/thinking.py +88 -0
- patch_cc/ui.py +23 -0
- patch_cc-0.1.0.dist-info/METADATA +141 -0
- patch_cc-0.1.0.dist-info/RECORD +26 -0
- patch_cc-0.1.0.dist-info/WHEEL +4 -0
- patch_cc-0.1.0.dist-info/entry_points.txt +2 -0
- patch_cc-0.1.0.dist-info/licenses/LICENSE +21 -0
patch_cc/patches/base.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Patch framework: how a single rewrite of the bundle is described and run.
|
|
2
|
+
|
|
3
|
+
Matcher rules, learned the hard way upstream (see docs/PLAYBOOK.md):
|
|
4
|
+
|
|
5
|
+
* Never anchor on minified locals (``A_``, ``mET``, ``wg6``); they are
|
|
6
|
+
regenerated on every upstream build.
|
|
7
|
+
* Anchor on string literals, ``case`` labels, prop names, or control-flow shape.
|
|
8
|
+
* When upstream ships several shapes, add a second narrow branch rather than
|
|
9
|
+
widening one regex until it over-matches.
|
|
10
|
+
|
|
11
|
+
Porting rules, for anyone translating more of upstream's JS:
|
|
12
|
+
|
|
13
|
+
* JS ``.replace(re, fn)`` without ``/g`` replaces **once** -- that is
|
|
14
|
+
``re.sub(..., count=1)``. Python's default replaces every occurrence.
|
|
15
|
+
* JS ``.replace("a", "b")`` on plain strings also replaces once --
|
|
16
|
+
``str.replace(a, b, 1)``.
|
|
17
|
+
* Compile with :data:`re.ASCII` so ``\\w`` stays ASCII as it is in JS.
|
|
18
|
+
* Always pass a *function* to :func:`re.sub`; a string template would treat
|
|
19
|
+
backslashes in the replacement as escapes.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
#: Prefix for every identifier we inject. These are the only reliable
|
|
29
|
+
#: fingerprints of our work -- value flips like ``verbose:!0`` are
|
|
30
|
+
#: indistinguishable from the 11 that upstream already ships.
|
|
31
|
+
SENTINEL = "__cc_"
|
|
32
|
+
|
|
33
|
+
# Groups, in display order.
|
|
34
|
+
GROUP_OUTPUT = "Output & diffs"
|
|
35
|
+
GROUP_THINKING = "Thinking"
|
|
36
|
+
GROUP_LIVE = "Live thinking (streaming)"
|
|
37
|
+
GROUP_AGENTS = "Subagents"
|
|
38
|
+
GROUP_CHROME = "Chrome & branding"
|
|
39
|
+
|
|
40
|
+
#: Default brand: the name shown unless the user overrides it. One home so the
|
|
41
|
+
#: field default and the "is it rebranded?" test can never disagree.
|
|
42
|
+
DEFAULT_BRAND = "Claude Code"
|
|
43
|
+
|
|
44
|
+
#: Default --version marker text.
|
|
45
|
+
DEFAULT_SUFFIX = "(patched)"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def derived_brand() -> str:
|
|
49
|
+
"""The branding default: the system username, possessive.
|
|
50
|
+
|
|
51
|
+
``anfreire`` becomes ``anfreire's Code``. Falls back to the unbranded
|
|
52
|
+
default when no username can be determined.
|
|
53
|
+
"""
|
|
54
|
+
import getpass
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
user = getpass.getuser().strip()
|
|
58
|
+
except OSError:
|
|
59
|
+
user = ""
|
|
60
|
+
return f"{user}'s Code" if user else DEFAULT_BRAND
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True)
|
|
64
|
+
class Options:
|
|
65
|
+
"""User customisation handed to every patch."""
|
|
66
|
+
|
|
67
|
+
brand: str = DEFAULT_BRAND
|
|
68
|
+
version_suffix: str = DEFAULT_SUFFIX
|
|
69
|
+
subagent_models: dict[str, str] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def rebrands(self) -> bool:
|
|
73
|
+
return self.brand != DEFAULT_BRAND
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True)
|
|
77
|
+
class Outcome:
|
|
78
|
+
"""What a patch found and what it changed.
|
|
79
|
+
|
|
80
|
+
``candidates`` and ``applied`` describe different failures and must not be
|
|
81
|
+
collapsed into one number:
|
|
82
|
+
|
|
83
|
+
* ``candidates == 0`` -- the anchor is gone. A real regression.
|
|
84
|
+
* ``candidates > 0, applied == 0`` -- shape found, rewrite was a no-op.
|
|
85
|
+
Usually means already patched, not broken.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
candidates: int = 0
|
|
89
|
+
applied: int = 0
|
|
90
|
+
notes: list[str] = field(default_factory=list)
|
|
91
|
+
#: Named sub-steps, for patches built from several independent rewrites.
|
|
92
|
+
steps: dict[str, "Outcome"] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def landed(self) -> bool:
|
|
96
|
+
return self.applied > 0
|
|
97
|
+
|
|
98
|
+
def note(self, message: str) -> None:
|
|
99
|
+
self.notes.append(message)
|
|
100
|
+
|
|
101
|
+
def step(self, name: str) -> "Outcome":
|
|
102
|
+
"""Get (or create) a named sub-outcome.
|
|
103
|
+
|
|
104
|
+
A single scalar count cannot distinguish "all twelve rewrites landed"
|
|
105
|
+
from "six landed and six silently drifted" -- which is exactly how
|
|
106
|
+
upstream's live-thinking patch hides its own regressions. Recording each
|
|
107
|
+
rewrite separately turns that into an actionable "reducer.message_stop
|
|
108
|
+
missed".
|
|
109
|
+
"""
|
|
110
|
+
return self.steps.setdefault(name, Outcome())
|
|
111
|
+
|
|
112
|
+
def finalize(self) -> "Outcome":
|
|
113
|
+
"""Roll sub-step totals up into this outcome."""
|
|
114
|
+
if self.steps:
|
|
115
|
+
self.candidates += sum(s.candidates for s in self.steps.values())
|
|
116
|
+
self.applied += sum(s.applied for s in self.steps.values())
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def missed_steps(self) -> list[str]:
|
|
120
|
+
"""Sub-steps whose shape was *found* but which failed to rewrite.
|
|
121
|
+
|
|
122
|
+
A step that matched nothing (``candidates == 0``) is usually a shape
|
|
123
|
+
that simply is not on this build -- most patches carry several
|
|
124
|
+
mutually-exclusive version variants -- so it is reported separately by
|
|
125
|
+
:meth:`absent_steps`, not here. A step that found candidates yet applied
|
|
126
|
+
none is the genuine concern.
|
|
127
|
+
"""
|
|
128
|
+
return [
|
|
129
|
+
name
|
|
130
|
+
for name, sub in self.steps.items()
|
|
131
|
+
if sub.candidates > 0 and not sub.landed
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def absent_steps(self) -> list[str]:
|
|
135
|
+
"""Sub-steps that matched nothing on this build (informational)."""
|
|
136
|
+
return [name for name, sub in self.steps.items() if sub.candidates == 0]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
PatchFn = Callable[[str, Options, Outcome], str]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(slots=True)
|
|
143
|
+
class Patch:
|
|
144
|
+
id: str
|
|
145
|
+
title: str
|
|
146
|
+
summary: str
|
|
147
|
+
group: str
|
|
148
|
+
fn: PatchFn
|
|
149
|
+
default: bool = True
|
|
150
|
+
#: Anchors to report on when this patch stops matching.
|
|
151
|
+
anchors: tuple[str, ...] = ()
|
|
152
|
+
|
|
153
|
+
def run(self, content: str, options: Options) -> tuple[str, Outcome]:
|
|
154
|
+
outcome = Outcome()
|
|
155
|
+
try:
|
|
156
|
+
content = self.fn(content, options, outcome)
|
|
157
|
+
except Exception as exc: # noqa: BLE001 - one bad patch must not abort the run
|
|
158
|
+
outcome.note(f"raised {type(exc).__name__}: {exc}")
|
|
159
|
+
return content, outcome.finalize()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def compile_js(pattern: str, flags: int = 0) -> re.Pattern[str]:
|
|
163
|
+
"""Compile a matcher with JS-compatible ``\\w`` semantics."""
|
|
164
|
+
return re.compile(pattern, flags | re.ASCII)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Matches a minified identifier, the JS `[A-Za-z_$][\w$]*` idiom.
|
|
168
|
+
IDENT = r"[A-Za-z_$][\w$]*"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def switch_case_end(content: str, start: int) -> int:
|
|
172
|
+
"""End offset of a ``switch`` arm beginning at ``start``.
|
|
173
|
+
|
|
174
|
+
Upstream's arms end at the next ``case"`` or ``default:``, whichever comes
|
|
175
|
+
first. Mirrors the scan every case-based patch does.
|
|
176
|
+
"""
|
|
177
|
+
nxt_case = content.find('case"', start)
|
|
178
|
+
nxt_default = content.find("default:", start)
|
|
179
|
+
ends = [i for i in (nxt_case, nxt_default) if i != -1]
|
|
180
|
+
return min(ends) if ends else len(content)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def iter_segments(content: str, needle: str):
|
|
184
|
+
"""Yield ``(start, end)`` for each switch arm introduced by ``needle``.
|
|
185
|
+
|
|
186
|
+
The caller rewrites and the generator is restarted, so this is deliberately
|
|
187
|
+
a simple finder rather than a stateful cursor.
|
|
188
|
+
"""
|
|
189
|
+
index = 0
|
|
190
|
+
while True:
|
|
191
|
+
start = content.find(needle, index)
|
|
192
|
+
if start == -1:
|
|
193
|
+
return
|
|
194
|
+
end = switch_case_end(content, start + len(needle))
|
|
195
|
+
yield start, end
|
|
196
|
+
index = start + len(needle)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def splice(content: str, start: int, end: int, replacement: str) -> str:
|
|
200
|
+
return content[:start] + replacement + content[end:]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""UI chrome: spinner tips, the --version marker, and branding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .base import (
|
|
8
|
+
DEFAULT_SUFFIX,
|
|
9
|
+
GROUP_CHROME,
|
|
10
|
+
IDENT,
|
|
11
|
+
Options,
|
|
12
|
+
Outcome,
|
|
13
|
+
Patch,
|
|
14
|
+
compile_js,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------- spinner tips
|
|
18
|
+
|
|
19
|
+
_SPINNER_GUARD = compile_js(rf"if\({IDENT}\(\)\.spinnerTipsEnabled===!1\)return;")
|
|
20
|
+
_SPINNER_EXPR = compile_js(rf"{IDENT}\.spinnerTipsEnabled!==!1")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _disable_spinner_tips(content: str, _options: Options, outcome: Outcome) -> str:
|
|
24
|
+
"""Force spinner tips off through both code paths that can enable them."""
|
|
25
|
+
guard = outcome.step("guard")
|
|
26
|
+
|
|
27
|
+
def kill_guard(_match: re.Match[str]) -> str:
|
|
28
|
+
guard.candidates += 1
|
|
29
|
+
guard.applied += 1
|
|
30
|
+
return "if(!0)return;"
|
|
31
|
+
|
|
32
|
+
output = _SPINNER_GUARD.sub(kill_guard, content)
|
|
33
|
+
|
|
34
|
+
expr = outcome.step("expr")
|
|
35
|
+
|
|
36
|
+
def kill_expr(_match: re.Match[str]) -> str:
|
|
37
|
+
expr.candidates += 1
|
|
38
|
+
expr.applied += 1
|
|
39
|
+
return "!1"
|
|
40
|
+
|
|
41
|
+
return _SPINNER_EXPR.sub(kill_expr, output)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------- version marker
|
|
45
|
+
|
|
46
|
+
_VERSION_NEEDLE = "}.VERSION} (Claude Code)"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _js_template_escape(text: str) -> str:
|
|
50
|
+
"""Make arbitrary text safe inside a JS template literal."""
|
|
51
|
+
return text.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _version_output(content: str, options: Options, outcome: Outcome) -> str:
|
|
55
|
+
"""Append a marker line to plain ``--version`` output.
|
|
56
|
+
|
|
57
|
+
The marker text is the user's ``--suffix`` (default ``(patched)``). The
|
|
58
|
+
needle sits inside a template literal, so the suffix is escaped for that
|
|
59
|
+
context.
|
|
60
|
+
"""
|
|
61
|
+
marker = "\\n" + _js_template_escape(options.version_suffix or DEFAULT_SUFFIX)
|
|
62
|
+
output = content
|
|
63
|
+
index = output.find(_VERSION_NEEDLE)
|
|
64
|
+
while index != -1:
|
|
65
|
+
outcome.candidates += 1
|
|
66
|
+
marker_at = index + len(_VERSION_NEEDLE)
|
|
67
|
+
if output[marker_at : marker_at + len(marker)] == marker:
|
|
68
|
+
index = output.find(_VERSION_NEEDLE, marker_at + len(marker))
|
|
69
|
+
continue
|
|
70
|
+
output = output[:marker_at] + marker + output[marker_at:]
|
|
71
|
+
outcome.applied += 1
|
|
72
|
+
index = output.find(_VERSION_NEEDLE, marker_at + len(marker))
|
|
73
|
+
return output
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------- branding
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _branding(content: str, options: Options, outcome: Outcome) -> str:
|
|
80
|
+
"""Rename visible ``Claude Code`` branding to the user's chosen name.
|
|
81
|
+
|
|
82
|
+
Only the small set of visible startup/help strings are touched, each as its
|
|
83
|
+
own step so partial upstream drift is visible.
|
|
84
|
+
"""
|
|
85
|
+
if not options.rebrands:
|
|
86
|
+
outcome.note("brand unchanged; nothing to do")
|
|
87
|
+
return content
|
|
88
|
+
|
|
89
|
+
brand = options.brand
|
|
90
|
+
esc = brand.replace("\\", "\\\\").replace('"', '\\"')
|
|
91
|
+
output = content
|
|
92
|
+
|
|
93
|
+
def swap(step_name: str, pattern: re.Pattern[str], replace) -> None:
|
|
94
|
+
nonlocal output
|
|
95
|
+
step = outcome.step(step_name)
|
|
96
|
+
|
|
97
|
+
def rewrite(match: re.Match[str]) -> str:
|
|
98
|
+
step.candidates += 1
|
|
99
|
+
result = replace(match)
|
|
100
|
+
if result != match.group(0):
|
|
101
|
+
step.applied += 1
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
output = pattern.sub(rewrite, output)
|
|
105
|
+
|
|
106
|
+
swap(
|
|
107
|
+
"bold-text",
|
|
108
|
+
compile_js(
|
|
109
|
+
rf'({IDENT})\.createElement\(({IDENT}),\{{bold:!0\}},"Claude Code"\)'
|
|
110
|
+
),
|
|
111
|
+
lambda m: f'{m.group(1)}.createElement({m.group(2)},{{bold:!0}},"{esc}")',
|
|
112
|
+
)
|
|
113
|
+
swap(
|
|
114
|
+
"bold-jsx",
|
|
115
|
+
compile_js(
|
|
116
|
+
rf'({IDENT})\.(jsx|jsxs)\(({IDENT}),\{{bold:!0,children:"Claude Code"\}}\)'
|
|
117
|
+
),
|
|
118
|
+
lambda m: (
|
|
119
|
+
f'{m.group(1)}.{m.group(2)}({m.group(3)},{{bold:!0,children:"{esc}"}})'
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
swap(
|
|
123
|
+
"help-title",
|
|
124
|
+
compile_js(
|
|
125
|
+
r"title:(`Claude Code v\$\{[\s\S]*?\.VERSION\}`),"
|
|
126
|
+
r'color:"professionalBlue",defaultTab:"general"'
|
|
127
|
+
),
|
|
128
|
+
lambda m: (
|
|
129
|
+
f'title:{m.group(1)}.replace("Claude Code","{esc}"),'
|
|
130
|
+
f'color:"professionalBlue",defaultTab:"general"'
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
swap(
|
|
134
|
+
"welcome-for",
|
|
135
|
+
compile_js(r'"Welcome to Claude Code for "'),
|
|
136
|
+
lambda _m: f'"Welcome to {esc} for "',
|
|
137
|
+
)
|
|
138
|
+
swap(
|
|
139
|
+
"welcome",
|
|
140
|
+
compile_js(r'"Welcome to Claude Code"'),
|
|
141
|
+
lambda _m: f'"Welcome to {esc}"',
|
|
142
|
+
)
|
|
143
|
+
swap(
|
|
144
|
+
"children-array",
|
|
145
|
+
compile_js(r'(color:"claude",bold:!0,children:\[)"Claude Code"(," "\])'),
|
|
146
|
+
lambda m: f'{m.group(1)}"{esc}"{m.group(2)}',
|
|
147
|
+
)
|
|
148
|
+
swap(
|
|
149
|
+
"styled-title",
|
|
150
|
+
compile_js(rf'({IDENT})\("claude",({IDENT})\)\("Claude Code"\)'),
|
|
151
|
+
lambda m: f'{m.group(1)}("claude",{m.group(2)})("{esc}")',
|
|
152
|
+
)
|
|
153
|
+
swap(
|
|
154
|
+
"styled-title-padded",
|
|
155
|
+
compile_js(rf'({IDENT})\("claude",({IDENT})\)\(" Claude Code "\)'),
|
|
156
|
+
lambda m: f'{m.group(1)}("claude",{m.group(2)})(" {esc} ")',
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return output
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
PATCHES = [
|
|
163
|
+
Patch(
|
|
164
|
+
id="spinner-tips",
|
|
165
|
+
title="Disable spinner tips",
|
|
166
|
+
summary="Stop the loading spinner from showing rotating tips.",
|
|
167
|
+
group=GROUP_CHROME,
|
|
168
|
+
fn=_disable_spinner_tips,
|
|
169
|
+
anchors=("spinnerTipsEnabled",),
|
|
170
|
+
),
|
|
171
|
+
Patch(
|
|
172
|
+
id="version-marker",
|
|
173
|
+
title="Mark --version as patched",
|
|
174
|
+
summary="Append a marker line to `claude --version` (custom text via --suffix).",
|
|
175
|
+
group=GROUP_CHROME,
|
|
176
|
+
fn=_version_output,
|
|
177
|
+
anchors=("}.VERSION} (Claude Code)",),
|
|
178
|
+
),
|
|
179
|
+
Patch(
|
|
180
|
+
id="branding",
|
|
181
|
+
title="Custom startup name",
|
|
182
|
+
summary="Rename the startup/help branding (default: your username's Code).",
|
|
183
|
+
group=GROUP_CHROME,
|
|
184
|
+
fn=_branding,
|
|
185
|
+
anchors=('"Welcome to Claude Code"', '{bold:!0},"Claude Code"'),
|
|
186
|
+
),
|
|
187
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tool-call and diff rendering patches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .base import (
|
|
8
|
+
GROUP_OUTPUT,
|
|
9
|
+
IDENT,
|
|
10
|
+
Options,
|
|
11
|
+
Outcome,
|
|
12
|
+
Patch,
|
|
13
|
+
compile_js,
|
|
14
|
+
splice,
|
|
15
|
+
switch_case_end,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# --------------------------------------------------------------- tool calls
|
|
19
|
+
|
|
20
|
+
_COLLAPSED_RETURN = compile_js(
|
|
21
|
+
rf'case"collapsed_read_search":return ({IDENT})\.createElement\(({IDENT}),\{{([^}}]*)\}}\)'
|
|
22
|
+
)
|
|
23
|
+
_COLLAPSED_CALL = compile_js(
|
|
24
|
+
r"(?:createElement|jsx|jsxs)\("
|
|
25
|
+
+ IDENT
|
|
26
|
+
+ r",\{message:[^}]*inProgressToolUseIDs:[^}]*"
|
|
27
|
+
r"shouldAnimate:[^}]*verbose:[^,}]+,tools:[^}]*lookups:[^}]*isActiveGroup:[^}]*\}\)"
|
|
28
|
+
)
|
|
29
|
+
_VERBOSE_PROP = compile_js(r"verbose:[^,}]+")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tool_call_verbose(content: str, _options: Options, outcome: Outcome) -> str:
|
|
33
|
+
"""Render collapsed read/search rows as if verbose mode were on."""
|
|
34
|
+
|
|
35
|
+
def rewrite_return(match: re.Match[str]) -> str:
|
|
36
|
+
ns, component, props = match.group(1), match.group(2), match.group(3)
|
|
37
|
+
if "verbose:" not in props:
|
|
38
|
+
return match.group(0)
|
|
39
|
+
outcome.candidates += 1
|
|
40
|
+
next_props = _VERBOSE_PROP.sub("verbose:!0", props, count=1)
|
|
41
|
+
if next_props == props:
|
|
42
|
+
return match.group(0)
|
|
43
|
+
outcome.applied += 1
|
|
44
|
+
return f'case"collapsed_read_search":return {ns}.createElement({component},{{{next_props}}})'
|
|
45
|
+
|
|
46
|
+
output = _COLLAPSED_RETURN.sub(rewrite_return, content)
|
|
47
|
+
|
|
48
|
+
# Newer builds use a block-form arm with a JSX-runtime call.
|
|
49
|
+
needle = 'case"collapsed_read_search":{'
|
|
50
|
+
index = 0
|
|
51
|
+
while True:
|
|
52
|
+
start = output.find(needle, index)
|
|
53
|
+
if start == -1:
|
|
54
|
+
break
|
|
55
|
+
end = switch_case_end(output, start + len(needle))
|
|
56
|
+
segment = output[start:end]
|
|
57
|
+
index = start + len(needle)
|
|
58
|
+
|
|
59
|
+
has_renderer = any(
|
|
60
|
+
tok in segment for tok in ("createElement(", "jsx(", "jsxs(")
|
|
61
|
+
)
|
|
62
|
+
if not has_renderer or "verbose:" not in segment:
|
|
63
|
+
continue
|
|
64
|
+
if not _COLLAPSED_CALL.search(segment):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
outcome.candidates += 1
|
|
68
|
+
next_segment = _VERBOSE_PROP.sub("verbose:!0", segment, count=1)
|
|
69
|
+
if next_segment != segment:
|
|
70
|
+
outcome.applied += 1
|
|
71
|
+
output = splice(output, start, end, next_segment)
|
|
72
|
+
index = start + len(next_segment)
|
|
73
|
+
|
|
74
|
+
return output
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------------------- create diff
|
|
78
|
+
|
|
79
|
+
_CREATE_RETURN = compile_js(
|
|
80
|
+
rf"return ({IDENT})\.(createElement|jsx|jsxs)\(({IDENT}),"
|
|
81
|
+
rf"\{{filePath:({IDENT}),content:({IDENT}),verbose:({IDENT})\}}\)"
|
|
82
|
+
)
|
|
83
|
+
_UPDATE_RENDERER = compile_js(
|
|
84
|
+
rf"(?:createElement|jsx|jsxs)\(({IDENT}),\{{filePath:[^}}]*structuredPatch:[^}}]*"
|
|
85
|
+
rf"style:({IDENT}),verbose:{IDENT}"
|
|
86
|
+
)
|
|
87
|
+
_LINE_COUNTER = compile_js(
|
|
88
|
+
rf"let {IDENT}=({IDENT})\({IDENT}\);return {IDENT}\.(?:createElement|jsxs)"
|
|
89
|
+
rf"\({IDENT},(?:null,|\{{children:\[)\"Wrote \""
|
|
90
|
+
)
|
|
91
|
+
_ALREADY_CREATE_DIFF = "structuredPatch:[{oldStart:1,oldLines:0,newStart:1"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _create_diff_colors(content: str, _options: Options, outcome: Outcome) -> str:
|
|
95
|
+
"""Render newly-created files through the diff component so lines get ``+``."""
|
|
96
|
+
output = content
|
|
97
|
+
index = 0
|
|
98
|
+
create_needle, update_needle = 'case"create":', 'case"update":'
|
|
99
|
+
|
|
100
|
+
while True:
|
|
101
|
+
create_start = output.find(create_needle, index)
|
|
102
|
+
if create_start == -1:
|
|
103
|
+
break
|
|
104
|
+
update_start = output.find(update_needle, create_start + len(create_needle))
|
|
105
|
+
if update_start == -1:
|
|
106
|
+
index = create_start + len(create_needle)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
switch_end = switch_case_end(output, update_start + len(update_needle))
|
|
110
|
+
create_segment = output[create_start:update_start]
|
|
111
|
+
update_segment = output[update_start:switch_end]
|
|
112
|
+
index = update_start + len(update_needle)
|
|
113
|
+
|
|
114
|
+
if _ALREADY_CREATE_DIFF in create_segment:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
create_match = _CREATE_RETURN.search(create_segment)
|
|
118
|
+
if not create_match:
|
|
119
|
+
continue
|
|
120
|
+
update_match = _UPDATE_RENDERER.search(update_segment)
|
|
121
|
+
if not update_match:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
outcome.candidates += 1
|
|
125
|
+
ns, factory = create_match.group(1), create_match.group(2)
|
|
126
|
+
file_var, content_var, verbose_var = create_match.group(4, 5, 6)
|
|
127
|
+
diff_renderer, style_var = update_match.group(1), update_match.group(2)
|
|
128
|
+
|
|
129
|
+
counter = _LINE_COUNTER.search(create_segment)
|
|
130
|
+
line_count = (
|
|
131
|
+
f"{counter.group(1)}({content_var})"
|
|
132
|
+
if counter
|
|
133
|
+
else f'{content_var}===""?0:{content_var}.split(`\\n`).length'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
before = create_match.group(0)
|
|
137
|
+
after = (
|
|
138
|
+
f"return {ns}.{factory}({diff_renderer},{{"
|
|
139
|
+
f"filePath:{file_var},structuredPatch:[{{oldStart:1,oldLines:0,newStart:1,"
|
|
140
|
+
f"newLines:{line_count},"
|
|
141
|
+
f'lines:{content_var}===""?[]:{content_var}.split(`\\n`)'
|
|
142
|
+
f'.map((__cc_line)=>"+"+__cc_line)}}],'
|
|
143
|
+
f"firstLine:{content_var}.split(`\\n`)[0]??null,"
|
|
144
|
+
f'fileContent:"",style:{style_var},verbose:{verbose_var},previewHint:void 0}})'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
next_segment = create_segment.replace(before, after, 1)
|
|
148
|
+
if next_segment != create_segment:
|
|
149
|
+
outcome.applied += 1
|
|
150
|
+
output = splice(output, create_start, update_start, next_segment)
|
|
151
|
+
index = create_start + len(next_segment)
|
|
152
|
+
|
|
153
|
+
return output
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
PATCHES = [
|
|
157
|
+
Patch(
|
|
158
|
+
id="tool-calls",
|
|
159
|
+
title="Detailed tool calls",
|
|
160
|
+
summary="Show full read/search tool calls instead of collapsed one-line summaries.",
|
|
161
|
+
group=GROUP_OUTPUT,
|
|
162
|
+
fn=_tool_call_verbose,
|
|
163
|
+
anchors=('case"collapsed_read_search"',),
|
|
164
|
+
),
|
|
165
|
+
Patch(
|
|
166
|
+
id="create-diff",
|
|
167
|
+
title="Colour new files as diffs",
|
|
168
|
+
summary="Render created files through the diff view so added lines keep + and green.",
|
|
169
|
+
group=GROUP_OUTPUT,
|
|
170
|
+
fn=_create_diff_colors,
|
|
171
|
+
anchors=('case"create":', 'case"update":'),
|
|
172
|
+
),
|
|
173
|
+
]
|