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.
@@ -0,0 +1,922 @@
1
+ """Live (streaming) thinking.
2
+
3
+ This is the single most fragile patch in the set: upstream has reshaped the
4
+ stream reducer at least three times, and most of their commit traffic lands
5
+ here. Two structural choices follow from that:
6
+
7
+ 1. It is built from ~11 **named steps**, each recording its own outcome. A
8
+ scalar hit count cannot tell "everything landed" from "half of it silently
9
+ drifted", which is precisely how this patch hides its own regressions.
10
+ 2. Steps share discovered identifiers through :class:`Discovery` rather than
11
+ re-deriving them, and every step tolerates its own failure so one drifted
12
+ shape does not take the rest down with it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass
19
+
20
+ from .base import GROUP_LIVE, IDENT, Options, Outcome, Patch, compile_js, splice
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class Discovery:
25
+ """Identifiers found in one step and consumed by later ones."""
26
+
27
+ streaming_var: str | None = None
28
+ create_message_helper: str | None = None
29
+ transcript_var: str | None = None
30
+
31
+
32
+ # --------------------------------------------------------------- JS builders
33
+
34
+
35
+ def _reset(setter: str, ended_at: str) -> str:
36
+ """The `mark the live block finished` state update, used by many cases."""
37
+ return (
38
+ f"{setter}?.((__cc_prevStreamingThinking)=>__cc_prevStreamingThinking?"
39
+ f"{{...__cc_prevStreamingThinking,isStreaming:!1,streamingEndedAt:{ended_at},"
40
+ f"currentIndex:null,currentMessage:null}}:__cc_prevStreamingThinking)"
41
+ )
42
+
43
+
44
+ def _block_start(event: str, setter: str, helper: str) -> str:
45
+ """Create a virtual message when a thinking content block starts.
46
+
47
+ Keyed by content-block index, so a block-start handled twice replaces its
48
+ entry rather than appending a duplicate live block.
49
+ """
50
+ return (
51
+ f"{setter}?.((__cc_prevStreamingThinking)=>{{"
52
+ f"let __cc_streamingThinkingMessage={helper}({{content:["
53
+ f'{event}.event.content_block.type==="redacted_thinking"'
54
+ f'?{{type:"redacted_thinking",data:{event}.event.content_block.data??""}}'
55
+ f':{{type:"thinking",thinking:""}}],isVirtual:!0}}),'
56
+ f"__cc_nextStreamingThinkingMessages=[...(__cc_prevStreamingThinking?.messages??[])"
57
+ f".filter((__cc_entry)=>__cc_entry.index!=={event}.event.index),"
58
+ f"{{index:{event}.event.index,message:__cc_streamingThinkingMessage}}];"
59
+ f'return{{thinking:{event}.event.content_block.type==="redacted_thinking"'
60
+ f'?{event}.event.content_block.data??"":"",isStreaming:!0,streamingEndedAt:void 0,'
61
+ f"currentIndex:{event}.event.index,currentMessage:__cc_streamingThinkingMessage,"
62
+ f"messages:__cc_nextStreamingThinkingMessages}}}})"
63
+ )
64
+
65
+
66
+ def _delta(event: str, setter: str, helper: str) -> str:
67
+ """Append a thinking delta to the live block."""
68
+ return (
69
+ f"{setter}?.((__cc_prevStreamingThinking)=>{{"
70
+ f"let __cc_nextStreamingThinkingDelta=typeof {event}.event.delta.thinking==="
71
+ f'"string"?{event}.event.delta.thinking:"",'
72
+ f'__cc_nextStreamingThinkingText=(__cc_prevStreamingThinking?.thinking??"")'
73
+ f"+__cc_nextStreamingThinkingDelta,"
74
+ f"__cc_nextStreamingThinkingIndex=__cc_prevStreamingThinking?.currentIndex"
75
+ f"??{event}.event.index,"
76
+ f"__cc_nextStreamingThinkingMessage={helper}({{content:["
77
+ f'{{type:"thinking",thinking:__cc_nextStreamingThinkingText}}],isVirtual:!0}}),'
78
+ f"__cc_nextStreamingThinkingMessages=[...(__cc_prevStreamingThinking?.messages??[])"
79
+ f".filter((__cc_entry)=>__cc_entry.index!==__cc_nextStreamingThinkingIndex),"
80
+ f"{{index:__cc_nextStreamingThinkingIndex,message:__cc_nextStreamingThinkingMessage}}];"
81
+ f"return __cc_prevStreamingThinking?{{...__cc_prevStreamingThinking,"
82
+ f"thinking:__cc_nextStreamingThinkingText,isStreaming:!0,streamingEndedAt:void 0,"
83
+ f"currentIndex:__cc_nextStreamingThinkingIndex,"
84
+ f"currentMessage:__cc_nextStreamingThinkingMessage,"
85
+ f"messages:__cc_nextStreamingThinkingMessages}}"
86
+ f":{{thinking:__cc_nextStreamingThinkingText,isStreaming:!0,streamingEndedAt:void 0,"
87
+ f"currentIndex:{event}.event.index,"
88
+ f"currentMessage:__cc_nextStreamingThinkingMessage,"
89
+ f"messages:[{{index:{event}.event.index,"
90
+ f"message:__cc_nextStreamingThinkingMessage}}]}}}})"
91
+ )
92
+
93
+
94
+ # ------------------------------------------------------------------- step 1
95
+
96
+ _MEMO_CACHE = compile_js(
97
+ rf"if\(({IDENT})\[(\d+)\]!==({IDENT})\|\|\1\[(\d+)\]!==({IDENT})\|\|\1\[(\d+)\]!==({IDENT})\)"
98
+ rf"([\s\S]{{0,700}}?thinking:\5\.thinking[\s\S]{{0,700}}?)"
99
+ rf"\1\[\2\]=\3,\1\[\4\]=\5,\1\[\6\]=\7,(\1\[\d+\]={IDENT};)"
100
+ )
101
+
102
+
103
+ def _step_memo_cache(content: str, outcome: Outcome) -> str:
104
+ """Key the memo cache on `thinking?.thinking`, not the wrapper object.
105
+
106
+ Without this the comparator sees the same object identity across deltas and
107
+ never re-renders while text is still streaming in.
108
+ """
109
+ step = outcome.step("memo-cache")
110
+
111
+ def rewrite(match: re.Match[str]) -> str:
112
+ cache, i1, v1, i2, v2, i3, v3, middle, tail = match.groups()
113
+ step.candidates += 1
114
+ if f"{v2}?.thinking" in match.group(0):
115
+ return match.group(0)
116
+ step.applied += 1
117
+ return (
118
+ f"if({cache}[{i1}]!=={v1}||{cache}[{i2}]!=={v2}?.thinking||{cache}[{i3}]!=={v3})"
119
+ f"{middle}{cache}[{i1}]={v1},{cache}[{i2}]={v2}?.thinking,{cache}[{i3}]={v3},{tail}"
120
+ )
121
+
122
+ return _MEMO_CACHE.sub(rewrite, content)
123
+
124
+
125
+ # ------------------------------------------------------------------- step 2
126
+
127
+ _HIDE_PAST = compile_js(rf"hidePastThinking:!0,streamingThinking:({IDENT})")
128
+ _ON_STREAMING = compile_js(rf"onStreamingThinking:({IDENT})")
129
+ _CREATE_ELEMENT_CALL = compile_js(rf"createElement\(({IDENT}),\{{([^{{}}]*?)\}}\)")
130
+ _PROMPT_RENDERER = compile_js(
131
+ rf"createElement\(({IDENT}),\{{([\s\S]{{0,2000}}?placeholderElement:[\s\S]{{0,2000}}?"
132
+ rf"agentDefinitions:[^}}]*?onOpenRateLimitOptions:[^}}]*?isLoading:)([^,}}]+)"
133
+ rf"(,streamingText:[^}}]*?(?:showThinkingHint:[^}}]*?)?isBriefOnly:[^}}]*?)\}}\)"
134
+ )
135
+ _JSX_MAIN_PROPS = compile_js(
136
+ r"(screen:[^,}]+,streamingToolUses:[^,}]+,)"
137
+ r"(showAllInTranscript:[^,}]+,agentDefinitions:[^,}]+,onOpenRateLimitOptions:[^,}]+,isLoading:[^,}]+)"
138
+ )
139
+ _JSX_TRANSCRIPT_PROPS = compile_js(
140
+ r"(screen:[^,}]+,agentDefinitions:[^,}]+,streamingToolUses:[^,}]+,)"
141
+ r"(showAllInTranscript:[^,}]+,onOpenRateLimitOptions:[^,}]+,isLoading:[^,}]+)"
142
+ )
143
+
144
+
145
+ def _discover_streaming_var(content: str, found: Discovery, outcome: Outcome) -> None:
146
+ """Find the state variable holding live thinking.
147
+
148
+ 2.1.216 no longer ships `hidePastThinking`, so the primary anchor is already
149
+ dead and we rely on the `onStreamingThinking` -> `useState(null)` back-scan.
150
+ Losing that fallback too would leave this patch with nothing.
151
+ """
152
+ primary = _HIDE_PAST.search(content)
153
+ if primary:
154
+ found.streaming_var = primary.group(1)
155
+ return
156
+
157
+ outcome.step("discover").note(
158
+ "hidePastThinking anchor gone; using useState back-scan"
159
+ )
160
+ for match in _ON_STREAMING.finditer(content):
161
+ setter = match.group(1)
162
+ start = max(0, match.start() - 50_000)
163
+ window = content[start : match.start()]
164
+ state = compile_js(
165
+ rf"\[({IDENT}),{re.escape(setter)}\]={IDENT}\.useState\(null\)"
166
+ )
167
+ candidates = list(state.finditer(window))
168
+ if candidates:
169
+ found.streaming_var = candidates[-1].group(1)
170
+ return
171
+
172
+
173
+ def _step_prop_threading(content: str, found: Discovery, outcome: Outcome) -> str:
174
+ """Pass the live-thinking state into the renderers that need it."""
175
+ step = outcome.step("prop-threading")
176
+ if found.streaming_var is None:
177
+ step.note("no streaming state variable found; skipped")
178
+ return content
179
+ var = found.streaming_var
180
+
181
+ def rewrite_create_element(match: re.Match[str]) -> str:
182
+ component, props = match.group(1), match.group(2)
183
+ required = (
184
+ "streamingToolUses:",
185
+ "toolJSX:",
186
+ "agentDefinitions:",
187
+ "onOpenRateLimitOptions:",
188
+ "conversationId:",
189
+ "isLoading:",
190
+ )
191
+ forbidden = ("streamingThinking:", "hidePastThinking:")
192
+ if any(tok not in props for tok in required) or any(
193
+ t in props for t in forbidden
194
+ ):
195
+ return match.group(0)
196
+ step.candidates += 1
197
+ step.applied += 1
198
+ return f"createElement({component},{{{props},streamingThinking:{var}}})"
199
+
200
+ output = _CREATE_ELEMENT_CALL.sub(rewrite_create_element, content)
201
+
202
+ def rewrite_prompt(match: re.Match[str]) -> str:
203
+ if "streamingThinking:" in match.group(0):
204
+ return match.group(0)
205
+ component, before, is_loading, after = match.groups()
206
+ step.candidates += 1
207
+ step.applied += 1
208
+ return (
209
+ f"createElement({component},{{{before}{is_loading},"
210
+ f"streamingThinking:{var}{after}}})"
211
+ )
212
+
213
+ output = _PROMPT_RENDERER.sub(rewrite_prompt, output)
214
+
215
+ def inject(match: re.Match[str]) -> str:
216
+ if "streamingThinking:" in match.group(0):
217
+ return match.group(0)
218
+ step.candidates += 1
219
+ step.applied += 1
220
+ return f"{match.group(1)}streamingThinking:{var},{match.group(2)}"
221
+
222
+ output = _JSX_MAIN_PROPS.sub(inject, output)
223
+ return _JSX_TRANSCRIPT_PROPS.sub(inject, output)
224
+
225
+
226
+ # ------------------------------------------------------------------- step 3
227
+
228
+ _THINKING_DISPLAY = compile_js(
229
+ rf"({IDENT})=({IDENT})\.type!==\"disabled\"&&!({IDENT})"
230
+ rf"\(process\.env\.CLAUDE_CODE_DISABLE_THINKING\),({IDENT})=\1"
231
+ rf"(?:&&{IDENT}\(\)&&{IDENT}\({IDENT}\))?\?\2\.display(?:\?\?void 0)?:void 0,({IDENT})=void 0;"
232
+ )
233
+
234
+ # 2.1.216 hoists the env check into its own variable and gates the display
235
+ # value behind extra feature/model helpers. The helper chain is kept verbatim;
236
+ # only the display expression gains the `??"summarized"` default.
237
+ _THINKING_DISPLAY_2 = compile_js(
238
+ rf"({IDENT})=({IDENT})\(process\.env\.CLAUDE_CODE_DISABLE_THINKING\),"
239
+ rf"({IDENT})=({IDENT})\.type!==\"disabled\"&&!\1,"
240
+ rf"({IDENT})=\3((?:&&{IDENT}\((?:{IDENT})?\))*)\?\4\.display:void 0,"
241
+ )
242
+
243
+
244
+ def _step_display_mode(content: str, outcome: Outcome) -> str:
245
+ """Default the thinking request to `summarized`.
246
+
247
+ Without a display mode in the request the API streams signature-only (or
248
+ late) thinking, so the live row starves -- worst on short thinks. Upstream
249
+ only requests summaries when the `showThinkingSummaries` setting is on;
250
+ default it on instead.
251
+ """
252
+ step = outcome.step("display-mode")
253
+
254
+ def rewrite(match: re.Match[str]) -> str:
255
+ enabled, config, env_helper, display, request = match.groups()
256
+ step.candidates += 1
257
+ if 'display??"summarized"' in match.group(0):
258
+ return match.group(0)
259
+ step.applied += 1
260
+ return (
261
+ f'{enabled}={config}.type!=="disabled"&&!{env_helper}'
262
+ f"(process.env.CLAUDE_CODE_DISABLE_THINKING),"
263
+ f'{display}={enabled}?{config}.display??"summarized":void 0,{request}=void 0;'
264
+ )
265
+
266
+ output = _THINKING_DISPLAY.sub(rewrite, content)
267
+
268
+ def rewrite_hoisted(match: re.Match[str]) -> str:
269
+ env_var, env_helper, enabled, config, display, guards = match.groups()
270
+ step.candidates += 1
271
+ step.applied += 1
272
+ return (
273
+ f"{env_var}={env_helper}(process.env.CLAUDE_CODE_DISABLE_THINKING),"
274
+ f'{enabled}={config}.type!=="disabled"&&!{env_var},'
275
+ f'{display}={enabled}{guards}?{config}.display??"summarized":void 0,'
276
+ )
277
+
278
+ return _THINKING_DISPLAY_2.sub(rewrite_hoisted, output, count=1)
279
+
280
+
281
+ # ------------------------------------------------------------------- step 4
282
+
283
+ _ASSISTANT_THINKING = compile_js(
284
+ rf"let ({IDENT})=({IDENT})\.message\.content\.find\(\(({IDENT})\)=>"
285
+ rf'\3\.type==="thinking"\);if\(\1&&\1\.type==="thinking"\)({IDENT})'
286
+ rf"\?\.\(\(\)=>\(\{{thinking:\1\.thinking,isStreaming:!1,"
287
+ rf"streamingEndedAt:Date\.now\(\)\}}\)\)"
288
+ )
289
+
290
+
291
+ def _step_final_summary(content: str, outcome: Outcome) -> str:
292
+ """Include redacted thinking in the final assistant-message summary."""
293
+ step = outcome.step("final-summary")
294
+
295
+ def rewrite(match: re.Match[str]) -> str:
296
+ block, message, item, setter = match.groups()
297
+ step.candidates += 1
298
+ step.applied += 1
299
+ return (
300
+ f"let {block}={message}.message.content.find(({item})=>"
301
+ f'{item}.type==="thinking"||{item}.type==="redacted_thinking");'
302
+ f'if({block}&&({block}.type==="thinking"||{block}.type==="redacted_thinking"))'
303
+ f'{setter}?.(()=>({{thinking:{block}.type==="thinking"'
304
+ f'?{block}.thinking:{block}.data??"",isStreaming:!1,'
305
+ f"streamingEndedAt:Date.now()}}))"
306
+ )
307
+
308
+ return _ASSISTANT_THINKING.sub(rewrite, content)
309
+
310
+
311
+ # ------------------------------------------------------------------- step 5
312
+
313
+ _MEMO_ASSIGN = compile_js(rf"({IDENT})=({IDENT})\.memo\(({IDENT}),({IDENT})\)")
314
+
315
+
316
+ def _step_memo_removal(content: str, outcome: Outcome) -> str:
317
+ """Unwrap the message-row memo whose comparator suppresses live updates."""
318
+ step = outcome.step("memo-removal")
319
+ output = content
320
+ pos = 0
321
+ while True:
322
+ match = _MEMO_ASSIGN.search(output, pos)
323
+ if not match:
324
+ break
325
+ lhs, _ns, render_fn, comparator = match.groups()
326
+ pos = match.end()
327
+
328
+ start = output.find(f"function {comparator}(")
329
+ if start == -1:
330
+ continue
331
+ body = output[start : start + 2200]
332
+ if not all(
333
+ tok in body
334
+ for tok in (
335
+ ".screen!==",
336
+ ".columns!==",
337
+ ".lastThinkingBlockId",
338
+ ".streamingToolUseIDs",
339
+ )
340
+ ):
341
+ continue
342
+
343
+ step.candidates += 1
344
+ replacement = f"{lhs}={render_fn}"
345
+ if replacement != match.group(0):
346
+ output = splice(output, match.start(), match.end(), replacement)
347
+ step.applied += 1
348
+ pos = match.start() + len(replacement)
349
+ return output
350
+
351
+
352
+ # ------------------------------------------------------------------- step 6
353
+
354
+ _LINGER_LABEL = compile_js(
355
+ rf"({IDENT}):\{{if\(!({IDENT})\)\{{({IDENT})=!1;break \1\}}"
356
+ rf"if\(\2\.isStreaming\)\{{\3=!0;break \1\}}"
357
+ rf"if\(\2\.streamingEndedAt\)\{{\3=Date\.now\(\)-\2\.streamingEndedAt<30000;break \1\}}"
358
+ rf"\3=!1\}}let ({IDENT})=\3"
359
+ )
360
+ _LINGER_MEMO = compile_js(
361
+ rf"({IDENT})=({IDENT})\.useMemo\(\(\)=>\{{if\(!({IDENT})\)return!1;"
362
+ rf"if\(\3\.isStreaming\)return!0;"
363
+ rf"if\(\3\.streamingEndedAt\)return Date\.now\(\)-\3\.streamingEndedAt<30000;"
364
+ rf"return!1\}},\[\3\]\)"
365
+ )
366
+
367
+
368
+ def _step_linger(content: str, outcome: Outcome) -> str:
369
+ """Drop the 30-second post-stream linger; show only while streaming."""
370
+ step = outcome.step("linger")
371
+
372
+ def rewrite_label(match: re.Match[str]) -> str:
373
+ step.candidates += 1
374
+ step.applied += 1
375
+ return (
376
+ f"let {match.group(4)}=!!({match.group(2)}&&{match.group(2)}.isStreaming)"
377
+ )
378
+
379
+ def rewrite_memo(match: re.Match[str]) -> str:
380
+ visible, ns, stream = match.groups()
381
+ step.candidates += 1
382
+ step.applied += 1
383
+ return (
384
+ f"{visible}={ns}.useMemo(()=>!!({stream}&&{stream}.isStreaming),[{stream}])"
385
+ )
386
+
387
+ output = _LINGER_LABEL.sub(rewrite_label, content)
388
+ return _LINGER_MEMO.sub(rewrite_memo, output)
389
+
390
+
391
+ # ------------------------------------------------------------------- step 7
392
+
393
+ _TOOLUSE_HELPERS = compile_js(
394
+ rf"let {IDENT}=({IDENT})\(\{{content:\[{IDENT}\.contentBlock\]\}}\);"
395
+ rf"return {IDENT}\.uuid=({IDENT})\({IDENT}\.contentBlock\.id,0\),({IDENT})\(\[{IDENT}\]\)"
396
+ )
397
+ _RENDERER_HAS_VAR = compile_js(
398
+ rf"\(\{{messages:[^}}]*?streamingToolUses:{IDENT},streamingThinking:({IDENT}),showAllInTranscript:"
399
+ )
400
+ _RENDERER_SIGNATURE = compile_js(
401
+ rf"(\(\{{messages:[^}}]*?streamingToolUses:{IDENT},)(showAllInTranscript:)"
402
+ )
403
+ _TRANSCRIPT_VAR = compile_js(
404
+ rf"streamingToolUses:{IDENT},[^}}]*streamingThinking:({IDENT}),streamingText:"
405
+ )
406
+
407
+
408
+ def _step_transcript_signature(content: str, found: Discovery, outcome: Outcome) -> str:
409
+ """Make sure the transcript renderer actually receives the live state."""
410
+ step = outcome.step("transcript-signature")
411
+
412
+ helpers = _TOOLUSE_HELPERS.search(content)
413
+ if helpers:
414
+ found.create_message_helper = helpers.group(1)
415
+
416
+ existing = _RENDERER_HAS_VAR.search(content)
417
+ if existing:
418
+ found.transcript_var = existing.group(1)
419
+ step.candidates += 1
420
+ step.applied += 1 # nothing to do; upstream already threads it
421
+ return content
422
+
423
+ output = content
424
+ if found.streaming_var is not None:
425
+
426
+ def inject(match: re.Match[str]) -> str:
427
+ if "streamingThinking:" in match.group(0):
428
+ return match.group(0)
429
+ step.candidates += 1
430
+ step.applied += 1
431
+ found.transcript_var = "__cc_streamingThinking"
432
+ return f"{match.group(1)}streamingThinking:__cc_streamingThinking,{match.group(2)}"
433
+
434
+ output = _RENDERER_SIGNATURE.sub(inject, output, count=1)
435
+
436
+ if found.transcript_var is None:
437
+ fallback = _TRANSCRIPT_VAR.search(output)
438
+ if fallback:
439
+ found.transcript_var = fallback.group(1)
440
+ return output
441
+
442
+
443
+ # ------------------------------------------------------------------- step 8
444
+
445
+ _INLINE_EXTRAS = compile_js(
446
+ rf"({IDENT})=({IDENT})\.useMemo\(\(\)=>({IDENT})\.flatMap\(\(({IDENT})\)=>\{{"
447
+ rf"let ({IDENT})=({IDENT})\(\{{content:\[\4\.contentBlock\]\}}\);"
448
+ rf"return \5\.uuid=({IDENT})\(\4\.contentBlock\.id,0\),({IDENT})\(\[\5\]\)\}}\),\[\3\]\)"
449
+ )
450
+
451
+
452
+ def _step_inline_extras(content: str, found: Discovery, outcome: Outcome) -> str:
453
+ """Render live thinking inline, ordered with streaming tool-use blocks."""
454
+ step = outcome.step("inline-extras")
455
+ if not found.transcript_var:
456
+ step.note("no transcript streaming variable; skipped")
457
+ return content
458
+ var = found.transcript_var
459
+
460
+ def rewrite(match: re.Match[str]) -> str:
461
+ extras, ns, tool_uses, entry, message, helper, uuid_helper, normalize = (
462
+ match.groups()
463
+ )
464
+ step.candidates += 1
465
+ step.applied += 1
466
+ found.create_message_helper = helper
467
+ return (
468
+ f"{extras}={ns}.useMemo(()=>{{"
469
+ f"let __cc_streamingToolUseExtras={tool_uses}.map(({entry})=>{{"
470
+ f"let {message}={helper}({{content:[{entry}.contentBlock]}});"
471
+ f"return {message}.uuid={uuid_helper}({entry}.contentBlock.id,0),"
472
+ f"{{index:{entry}.index??9007199254740991,"
473
+ f"messages:{normalize}([{message}])}}}}),"
474
+ f"__cc_streamingThinkingExtras=({var}?.messages??[])"
475
+ f".map((__cc_entry,__cc_index)=>({{"
476
+ f"index:__cc_entry.index??9007199254740991+__cc_index,"
477
+ f"messages:{normalize}([__cc_entry.message??__cc_entry])}}));"
478
+ f"return[...__cc_streamingToolUseExtras,...__cc_streamingThinkingExtras]"
479
+ f".sort((__cc_a,__cc_b)=>__cc_a.index===__cc_b.index?0:__cc_a.index-__cc_b.index)"
480
+ f".flatMap((__cc_entry)=>__cc_entry.messages)}},[{tool_uses},{var}])"
481
+ )
482
+
483
+ return _INLINE_EXTRAS.sub(rewrite, content)
484
+
485
+
486
+ # ------------------------------------------------------------------- step 9
487
+
488
+ _LIVE_ROW = compile_js(
489
+ rf"({IDENT})&{{2}}({IDENT})&{{2}}!({IDENT})&{{2}}({IDENT})\.createElement\(({IDENT}),"
490
+ rf"\{{marginTop:1\}},\4\.createElement\(({IDENT}),\{{param:\{{type:\"thinking\","
491
+ rf"thinking:\2\.thinking\}},addMargin:!1,isTranscriptMode:!0,verbose:({IDENT}),"
492
+ rf"hideInTranscript:!1\}}\)\)"
493
+ )
494
+
495
+
496
+ def _step_bottom_row(content: str, outcome: Outcome) -> str:
497
+ """Remove the separate bottom-pinned live row now that it renders inline."""
498
+ step = outcome.step("bottom-row")
499
+
500
+ def rewrite(_match: re.Match[str]) -> str:
501
+ step.candidates += 1
502
+ step.applied += 1
503
+ return "null"
504
+
505
+ return _LIVE_ROW.sub(rewrite, content)
506
+
507
+
508
+ # ------------------------------------------------------ steps 10-11: reducer
509
+
510
+ _PROGRESS_ONLY = (
511
+ r'case"thinking_delta":\{{let\{{delta:({ident})\}}={event}\.event;'
512
+ r'if\("estimated_tokens"in \1&&typeof \1\.estimated_tokens==="number"\)'
513
+ r'({ident})\?\.\(\{{type:"thinking_progress",'
514
+ r"estimatedTokensDelta:\1\.estimated_tokens\}}\);return\}}"
515
+ )
516
+ _PROGRESS_WITH_TEXT = (
517
+ r'case"thinking_delta":\{{let\{{delta:({ident})\}}={event}\.event;'
518
+ r'if\("estimated_tokens"in \1&&typeof \1\.estimated_tokens==="number"\)'
519
+ r'({ident})\?\.\(\{{type:"thinking_progress",'
520
+ r"estimatedTokensDelta:\1\.estimated_tokens\}}\);"
521
+ r'else if\("thinking"in \1&&typeof \1\.thinking==="string"&&\1\.thinking\.length>0\)'
522
+ r'\2\?\.\(\{{type:"thinking_progress",'
523
+ r"estimatedTokensDelta:({ident})\(\1\.thinking\)\}}\);return\}}"
524
+ )
525
+
526
+
527
+ def _apply_pairs(segment: str, pairs: list[tuple[str, str]], step: Outcome) -> str:
528
+ """Apply literal before/after rewrites, counting each independently."""
529
+ for before, after in pairs:
530
+ if before and before in segment:
531
+ step.candidates += 1
532
+ segment = segment.replace(before, after, 1)
533
+ if after in segment:
534
+ step.applied += 1
535
+ return segment
536
+
537
+
538
+ def _apply_progress_variants(
539
+ segment: str, event: str, delta_body: str, step: Outcome
540
+ ) -> str:
541
+ """Keep upstream's thinking-progress metrics while adding our state update."""
542
+ for template in (_PROGRESS_ONLY, _PROGRESS_WITH_TEXT):
543
+ pattern = compile_js(template.format(ident=IDENT, event=re.escape(event)))
544
+
545
+ def rewrite(match: re.Match[str]) -> str:
546
+ groups = match.groups()
547
+ delta_var, metrics = groups[0], groups[1]
548
+ tail = (
549
+ f"let{{delta:{delta_var}}}={event}.event;"
550
+ f'if("estimated_tokens"in {delta_var}&&'
551
+ f'typeof {delta_var}.estimated_tokens==="number")'
552
+ f'{metrics}?.({{type:"thinking_progress",'
553
+ f"estimatedTokensDelta:{delta_var}.estimated_tokens}});"
554
+ )
555
+ if len(groups) > 2:
556
+ estimator = groups[2]
557
+ tail += (
558
+ f'else if("thinking"in {delta_var}&&'
559
+ f'typeof {delta_var}.thinking==="string"&&'
560
+ f"{delta_var}.thinking.length>0)"
561
+ f'{metrics}?.({{type:"thinking_progress",'
562
+ f"estimatedTokensDelta:{estimator}({delta_var}.thinking)}});"
563
+ )
564
+ return f'case"thinking_delta":{{{delta_body}{tail}return}}'
565
+
566
+ updated = pattern.sub(rewrite, segment, count=1)
567
+ if updated != segment:
568
+ step.candidates += 1
569
+ step.applied += 1
570
+ segment = updated
571
+ return segment
572
+
573
+
574
+ def _reducer_pairs(
575
+ event: str,
576
+ setter: str,
577
+ mode: str,
578
+ tools: str,
579
+ helper: str,
580
+ *,
581
+ optional: bool,
582
+ options_param: str | None = None,
583
+ display_transform: str | None = None,
584
+ ) -> list[tuple[str, str]]:
585
+ """Before/after rewrites for one stream-reducer shape.
586
+
587
+ ``optional`` selects whether upstream calls the setters directly or through
588
+ ``?.`` -- both spellings exist in the wild.
589
+ """
590
+ call = "?." if optional else ""
591
+ ended = _reset(setter, "Date.now()")
592
+ cleared = _reset(setter, "void 0")
593
+ pairs: list[tuple[str, str]] = [
594
+ (
595
+ f'if({event}.type==="stream_request_start"){{{mode}("requesting");return}}',
596
+ f'if({event}.type==="stream_request_start"){{{setter}?.(null),{mode}{call}("requesting");return}}',
597
+ ),
598
+ (
599
+ f'if({event}.type==="stream_request_start"){{{mode}?.("requesting");return}}',
600
+ f'if({event}.type==="stream_request_start"){{{setter}?.(null),{mode}?.("requesting");return}}',
601
+ ),
602
+ ]
603
+
604
+ if options_param:
605
+ pairs.append(
606
+ (
607
+ f'if({event}.event.type==="message_stop"){{{options_param}.displayTransform?.finalize(),'
608
+ f'{mode}("tool-use"),{tools}(()=>[]);return}}',
609
+ f'if({event}.event.type==="message_stop"){{{options_param}.displayTransform?.finalize(),'
610
+ f'{ended},{mode}("tool-use"),{tools}(()=>[]);return}}',
611
+ )
612
+ )
613
+ if display_transform:
614
+ for prefix in (
615
+ f"{display_transform}.finalize()",
616
+ f"{display_transform}?.finalize()",
617
+ ):
618
+ for mo, to in ((mode, tools), (f"{mode}?.", f"{tools}?.")):
619
+ pairs.append(
620
+ (
621
+ f'if({event}.event.type==="message_stop"){{{prefix},'
622
+ f'{mo}("tool-use"),{to}(()=>[]);return}}',
623
+ f'if({event}.event.type==="message_stop"){{{display_transform}?.finalize(),'
624
+ f'{ended},{mode}?.("tool-use"),{tools}?.(()=>[]);return}}',
625
+ )
626
+ )
627
+
628
+ pairs += [
629
+ (
630
+ f'if({event}.event.type==="message_stop"){{{mode}("tool-use"),{tools}(()=>[]);return}}',
631
+ f'if({event}.event.type==="message_stop"){{{ended},{mode}{call}("tool-use"),'
632
+ f"{tools}{call}(()=>[]);return}}",
633
+ ),
634
+ (
635
+ f'if({event}.event.type==="message_stop"){{{mode}?.("tool-use"),{tools}?.(()=>[]);return}}',
636
+ f'if({event}.event.type==="message_stop"){{{ended},{mode}?.("tool-use"),'
637
+ f"{tools}?.(()=>[]);return}}",
638
+ ),
639
+ (
640
+ f'case"thinking":case"redacted_thinking":{mode}("thinking");return;',
641
+ f'case"thinking":case"redacted_thinking":{_block_start(event, setter, helper)},'
642
+ f'{mode}{call}("thinking");return;',
643
+ ),
644
+ (
645
+ f'case"thinking":case"redacted_thinking":{mode}?.("thinking");return;',
646
+ f'case"thinking":case"redacted_thinking":{_block_start(event, setter, helper)},'
647
+ f'{mode}?.("thinking");return;',
648
+ ),
649
+ (
650
+ f'case"text":{mode}("responding");return;',
651
+ f'case"text":{cleared},{mode}{call}("responding");return;',
652
+ ),
653
+ (
654
+ f'case"text":{mode}?.("responding");return;',
655
+ f'case"text":{cleared},{mode}?.("responding");return;',
656
+ ),
657
+ (
658
+ f'case"message_delta":if({mode}("responding"),{event}.event.usage.output_tokens!=null)',
659
+ f'case"message_delta":if({cleared},{mode}("responding"),'
660
+ f"{event}.event.usage.output_tokens!=null)",
661
+ ),
662
+ (
663
+ f'case"message_delta":{mode}("responding");return;',
664
+ f'case"message_delta":{cleared},{mode}{call}("responding");return;',
665
+ ),
666
+ (
667
+ f'case"message_delta":{mode}?.("responding");return;',
668
+ f'case"message_delta":{cleared},{mode}?.("responding");return;',
669
+ ),
670
+ (
671
+ f'case"message_delta":{{{mode}("responding");',
672
+ f'case"message_delta":{{{cleared},{mode}{call}("responding");',
673
+ ),
674
+ (
675
+ f'case"message_delta":{{{mode}?.("responding");',
676
+ f'case"message_delta":{{{cleared},{mode}?.("responding");',
677
+ ),
678
+ ]
679
+ return pairs
680
+
681
+
682
+ _DESTRUCTURED_HANDLER = compile_js(
683
+ rf"function {IDENT}\(({IDENT}),({IDENT})\)\{{let\{{([^}}]*onStreamingThinking:{IDENT}[^}}]*)\}}=\2;"
684
+ )
685
+ _MISSING_HANDLER = compile_js(
686
+ rf"function {IDENT}\(({IDENT}),({IDENT})(?:,{IDENT})?\)\{{let\{{([^}}]*)\}}=\2;"
687
+ )
688
+ _LEGACY_ANCHOR = 'type!=="stream_event"&&'
689
+ _FN_SIG = compile_js(rf"^function {IDENT}\(([^)]*)\)\{{")
690
+
691
+
692
+ def _prop_var(props: str, name: str, *, shorthand: bool = False) -> str | None:
693
+ alias = compile_js(rf"{re.escape(name)}:({IDENT})").search(props)
694
+ if alias:
695
+ return alias.group(1)
696
+ if shorthand and compile_js(rf"(?:^|,){re.escape(name)}(?:,|$)").search(props):
697
+ return name
698
+ return None
699
+
700
+
701
+ def _handler_is_stream_reducer(segment: str) -> bool:
702
+ return all(
703
+ tok in segment
704
+ for tok in (
705
+ 'type==="stream_request_start"',
706
+ 'case"thinking_delta"',
707
+ "content_block_start",
708
+ )
709
+ )
710
+
711
+
712
+ def _step_reducer_destructured(content: str, found: Discovery, outcome: Outcome) -> str:
713
+ """2.1.138+ shape: options bag that already destructures onStreamingThinking."""
714
+ step = outcome.step("reducer-destructured")
715
+ helper = found.create_message_helper
716
+ if helper is None:
717
+ step.note("no virtual-message helper discovered; skipped")
718
+ return content
719
+
720
+ output, pos = content, 0
721
+ while True:
722
+ match = _DESTRUCTURED_HANDLER.search(output, pos)
723
+ if not match:
724
+ break
725
+ event, options_param, props = match.groups()
726
+ pos = match.end()
727
+
728
+ mode = _prop_var(props, "onSetStreamMode")
729
+ tools = _prop_var(props, "onStreamingToolUses")
730
+ setter = _prop_var(props, "onStreamingThinking")
731
+ if not (mode and tools and setter):
732
+ continue
733
+
734
+ end = output.find("function ", match.end())
735
+ if end == -1:
736
+ continue
737
+ segment = output[match.start() : end]
738
+ if not _handler_is_stream_reducer(segment):
739
+ continue
740
+
741
+ delta_body = _delta(event, setter, helper) + ";"
742
+ pairs = _reducer_pairs(
743
+ event,
744
+ setter,
745
+ mode,
746
+ tools,
747
+ helper,
748
+ optional=False,
749
+ options_param=options_param,
750
+ )
751
+ pairs.append(
752
+ (
753
+ 'case"thinking_delta":return;',
754
+ f'case"thinking_delta":{{{delta_body}return;}}',
755
+ )
756
+ )
757
+
758
+ updated = _apply_pairs(segment, pairs, step)
759
+ updated = _apply_progress_variants(updated, event, delta_body, step)
760
+ if updated != segment:
761
+ output = splice(output, match.start(), end, updated)
762
+ pos = match.start() + len(updated)
763
+ return output
764
+
765
+
766
+ def _step_reducer_inner(content: str, found: Discovery, outcome: Outcome) -> str:
767
+ """2.1.183+ shape: inner handler that dropped onStreamingThinking."""
768
+ step = outcome.step("reducer-inner")
769
+ helper = found.create_message_helper
770
+ if helper is None:
771
+ step.note("no virtual-message helper discovered; skipped")
772
+ return content
773
+
774
+ setter = "__cc_onStreamingThinking"
775
+ output, pos = content, 0
776
+ while True:
777
+ match = _MISSING_HANDLER.search(output, pos)
778
+ if not match:
779
+ break
780
+ event, options_param, props = match.groups()
781
+ pos = match.end()
782
+ if "onStreamingThinking:" in props:
783
+ continue
784
+
785
+ mode = _prop_var(props, "onSetStreamMode", shorthand=True)
786
+ tools = _prop_var(props, "onStreamingToolUses", shorthand=True)
787
+ display = _prop_var(props, "displayTransform", shorthand=True)
788
+ if not (mode and tools):
789
+ continue
790
+
791
+ end = output.find("function ", match.end())
792
+ if end == -1:
793
+ continue
794
+ segment = output[match.start() : end]
795
+ if not _handler_is_stream_reducer(segment):
796
+ continue
797
+
798
+ delta_body = _delta(event, setter, helper) + ";"
799
+ pairs = [
800
+ (
801
+ f"let{{{props}}}={options_param};",
802
+ f"let{{{props},onStreamingThinking:{setter}}}={options_param};",
803
+ )
804
+ ]
805
+ pairs += _reducer_pairs(
806
+ event, setter, mode, tools, helper, optional=True, display_transform=display
807
+ )
808
+ pairs.append(
809
+ (
810
+ 'case"thinking_delta":return;',
811
+ f'case"thinking_delta":{{{delta_body}return;}}',
812
+ )
813
+ )
814
+
815
+ updated = _apply_pairs(segment, pairs, step)
816
+ updated = _apply_progress_variants(updated, event, delta_body, step)
817
+ if updated != segment:
818
+ output = splice(output, match.start(), end, updated)
819
+ pos = match.start() + len(updated)
820
+ return output
821
+
822
+
823
+ def _step_reducer_legacy(content: str, found: Discovery, outcome: Outcome) -> str:
824
+ """Pre-2.1.138 shape: positional parameters, no options bag."""
825
+ step = outcome.step("reducer-legacy")
826
+ anchor = content.find(_LEGACY_ANCHOR)
827
+ if anchor == -1:
828
+ return content
829
+ if content.find('type==="stream_request_start"', anchor) == -1:
830
+ return content
831
+ if content.find('case"thinking_delta"', anchor) == -1:
832
+ return content
833
+
834
+ start = content.rfind("function ", 0, anchor)
835
+ end = content.find("function ", anchor + len(_LEGACY_ANCHOR))
836
+ if start == -1 or end == -1:
837
+ return content
838
+
839
+ segment = content[start:end]
840
+ signature = _FN_SIG.search(segment)
841
+ if not signature:
842
+ return content
843
+ params = [p.strip() for p in signature.group(1).split(",")]
844
+ if len(params) < 7:
845
+ return content
846
+
847
+ event, append_output, mode, tools, setter = (
848
+ params[0],
849
+ params[2],
850
+ params[3],
851
+ params[4],
852
+ params[6],
853
+ )
854
+ helper = found.create_message_helper
855
+
856
+ pairs = _reducer_pairs(event, setter, mode, tools, helper or "", optional=False)
857
+ if helper is None:
858
+ # Without the helper we cannot synthesise virtual messages; keep only
859
+ # the rewrites that do not need it.
860
+ pairs = [p for p in pairs if "__cc_streamingThinkingMessage" not in p[1]]
861
+ else:
862
+ delta_body = _delta(event, setter, helper) + ";"
863
+ pairs += [
864
+ (
865
+ f'case"thinking_delta":{append_output}({event}.event.delta.thinking);return;',
866
+ f'case"thinking_delta":{{{append_output}({event}.event.delta.thinking);'
867
+ f"{delta_body}return;}}",
868
+ ),
869
+ (
870
+ 'case"thinking_delta":return;',
871
+ f'case"thinking_delta":{{{delta_body}return;}}',
872
+ ),
873
+ ]
874
+
875
+ updated = _apply_pairs(segment, pairs, step)
876
+ if helper is not None:
877
+ updated = _apply_progress_variants(
878
+ updated, event, _delta(event, setter, helper) + ";", step
879
+ )
880
+ return splice(content, start, end, updated) if updated != segment else content
881
+
882
+
883
+ # ------------------------------------------------------------------ assembly
884
+
885
+
886
+ def _live_thinking(content: str, _options: Options, outcome: Outcome) -> str:
887
+ found = Discovery()
888
+ output = _step_memo_cache(content, outcome)
889
+ _discover_streaming_var(output, found, outcome)
890
+ output = _step_prop_threading(output, found, outcome)
891
+ output = _step_display_mode(output, outcome)
892
+ output = _step_final_summary(output, outcome)
893
+ output = _step_memo_removal(output, outcome)
894
+ output = _step_linger(output, outcome)
895
+ output = _step_transcript_signature(output, found, outcome)
896
+ output = _step_inline_extras(output, found, outcome)
897
+ output = _step_bottom_row(output, outcome)
898
+ output = _step_reducer_destructured(output, found, outcome)
899
+ output = _step_reducer_inner(output, found, outcome)
900
+ output = _step_reducer_legacy(output, found, outcome)
901
+
902
+ if found.streaming_var is None:
903
+ outcome.note("live-thinking state variable was never found")
904
+ return output
905
+
906
+
907
+ PATCHES = [
908
+ Patch(
909
+ id="live-thinking",
910
+ title="Stream thinking live",
911
+ summary="Show thinking as it is generated, inline and in order, instead of "
912
+ "only after the turn finishes.",
913
+ group=GROUP_LIVE,
914
+ fn=_live_thinking,
915
+ anchors=(
916
+ "onStreamingThinking:",
917
+ 'case"thinking_delta"',
918
+ 'type==="stream_request_start"',
919
+ "content_block_start",
920
+ ),
921
+ ),
922
+ ]