-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathclaudemax
More file actions
executable file
·1012 lines (946 loc) · 45.7 KB
/
Copy pathclaudemax
File metadata and controls
executable file
·1012 lines (946 loc) · 45.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# claudemax - Claude Code launcher that combines three unofficial fixes:
#
# 1. Restores extended-thinking summaries on Opus 4.7 / 4.8, where the
# "Thinking" section otherwise renders empty in the VS Code extension and
# headless -p/SDK. Done by injecting `--thinking-display summarized` into the
# launch args - the one lever that is NOT interactivity-gated. Edits nothing.
# 2. Restores the always-visible context-usage icon in the VS Code chat input.
# Recent extension builds (2.1.165+) hide that icon until you have used
# >50% of the context window; with the 1M window that is ~500k tokens, so it
# is effectively never shown. There is no env/CLI lever for this, so (unlike
# fix #1) this wrapper idempotently patches the extension's webview bundle on
# each launch, flipping the threshold so the icon shows at any usage level.
# Because it re-applies every launch, it survives extension updates.
# 3. Adds a single-click "Copy as Markdown" icon to every message (and a floating
# "copy conversation" icon) in the VS Code chat; the icon flips to a checkmark
# only when the copy truly lands. Like fix #2 there is no env/CLI lever, so this
# wrapper idempotently appends a self-contained block to the webview bundle
# (index.js + index.css) each launch; it fails safe (the controls simply do
# not appear if the markup moves) and survives extension updates.
#
# This single launcher carries every fix, each independently switchable by an
# environment variable (all on by default): CC_THINKING_DISPLAY=omitted (fix 1),
# CC_PATCH_CONTEXT_ICON=0 (fix 2), CC_PATCH_MD_COPY=0 (fix 3). E.g. for thinking
# summaries only, set CC_PATCH_CONTEXT_ICON=0 AND CC_PATCH_MD_COPY=0.
#
# NOTE: unlike fix #1, fixes #2 and #3 DO edit the extension's bundled webview
# files (#2 patches index.js in place; #3 appends a block to index.js + index.css).
# Those edits are idempotent and ownership-marked, snapshotted once to
# index.js.bak-cc-workarounds (emergency restore only), written atomically (a
# failed write leaves the original untouched), best-effort (it never blocks the
# launch), reconciled per file every launch, and toggle-able with
# CC_PATCH_CONTEXT_ICON=0 / CC_PATCH_MD_COPY=0 (or CC_WORKAROUNDS=0 / CC_RECONCILE=0).
#
# Use it:
# - VS Code (official "Claude Code" extension): set "claudeCode.claudeProcessWrapper"
# to the FULL path of this file, then reload the window. In a multi-root
# .code-workspace this setting is window-scoped, so put it in the workspace
# file's "settings" block (or User settings), not a folder .vscode/settings.json.
# - VS Code (third-party "Claude Code Chat"): set "claudeCodeChat.executable.path".
# - Terminal: run `claudemax` in place of `claude`.
#
# Toggle off (defaults in parentheses):
# export CC_THINKING_DISPLAY=omitted # hide thinking summaries (summarized)
# export CC_PATCH_CONTEXT_ICON=0 # leave the context-usage icon as-is (1)
# export CC_PATCH_MD_COPY=0 # no copy controls / webview append (1)
# export CC_WORKAROUNDS=0 # master: disable every fix (1)
# export CC_RECONCILE=0 # do not touch the webview bundle (1)
# export CC_SCRUB_ROUTING=1 # force the default Anthropic account (0)
#
# The real `claude` must be installed. This wrapper finds it automatically; if it
# cannot, set CLAUDE_REAL_BIN to the full path of your real claude binary.
set -euo pipefail
# --- Locate the real claude binary -----------------------------------------
self="$(readlink -f "$0" 2>/dev/null || echo "$0")"
# Process-wrapper convention: the official VS Code extension invokes the wrapper
# as <wrapper> <REAL_CLAUDE...> <args...>, passing the real CLI ahead of the
# args. <REAL_CLAUDE...> is either a single native-binary path (".../claude") or
# a node interpreter followed by the bundled cli.js (".../node .../cli.js").
# Peel that off so it is not forwarded as a stray positional argument, and
# prefer it as the real claude. (Plain "claudemax <args>" use is unaffected:
# <args> never begins with an existing claude/node binary path.)
wrapper_bin=""
if [ "$#" -gt 0 ] \
&& printf '%s' "$1" | grep -Eqi '/claude(\.exe|\.cmd|\.bat)?$' \
&& [ -e "$1" ]; then
wrapper_bin="$1"
shift
elif [ "$#" -ge 2 ] \
&& printf '%s' "$1" | grep -Eqi '/node(\.exe)?$' && [ -e "$1" ] \
&& printf '%s' "$2" | grep -Eqi '\.(c?js|mjs)$' && [ -e "$2" ]; then
# node + cli.js: exec node directly and keep cli.js as the first forwarded arg.
wrapper_bin="$1"
shift
fi
REAL_CLAUDE="${CLAUDE_REAL_BIN:-}"
if [ -z "$REAL_CLAUDE" ] && [ -n "$wrapper_bin" ]; then
REAL_CLAUDE="$wrapper_bin"
fi
if [ -z "$REAL_CLAUDE" ]; then
for c in \
"$HOME/.local/bin/claude" \
/usr/local/bin/claude \
/usr/bin/claude \
/opt/homebrew/bin/claude \
"$(command -v claude 2>/dev/null || true)"; do
[ -n "$c" ] && [ -x "$c" ] || continue
[ "$(readlink -f "$c" 2>/dev/null || echo "$c")" = "$self" ] && continue
REAL_CLAUDE="$c"
break
done
fi
[ -n "$REAL_CLAUDE" ] || {
echo "claudemax: could not find the real 'claude' binary; set CLAUDE_REAL_BIN" >&2
exit 1
}
# --- Behavior ---------------------------------------------------------------
# Set CC_THINKING_DISPLAY=omitted to hide thinking; default shows summaries.
DISPLAY_VALUE="${CC_THINKING_DISPLAY:-summarized}"
case "$DISPLAY_VALUE" in
summarized|omitted) ;;
*)
echo "claudemax: invalid CC_THINKING_DISPLAY=$DISPLAY_VALUE; using summarized" >&2
DISPLAY_VALUE="summarized"
;;
esac
# ===== FEATURE DEFAULTS (edit to taste; environment variables override) =====
# Master switch: 0 disables every workaround (argument injection AND bundle
# patches) and reconcile reverts the webview to clean on this launch. When 1,
# the per-feature toggles below govern.
CC_WORKAROUNDS="${CC_WORKAROUNDS:-1}"
# Emergency bundle bypass: 0 means do NOT read or write the webview bundle at all
# this launch (argument injection is unaffected). Leaves any existing patches in
# place without uninstalling.
CC_RECONCILE="${CC_RECONCILE:-1}"
# context-icon bundle patch: 0 leaves the webview's context-usage icon unpatched.
CC_PATCH_CONTEXT_ICON="${CC_PATCH_CONTEXT_ICON:-1}"
# (CC_THINKING_DISPLAY is handled above as DISPLAY_VALUE: summarized | omitted.)
# markdown copy/export bundle patch: 0 leaves the webview without the copy controls.
CC_PATCH_MD_COPY="${CC_PATCH_MD_COPY:-1}"
# ============================================================================
# --- Optional customizations ------------------------------------------------
#
# Raise reasoning effort - longer, more detailed summaries. Uses more tokens:
# export CLAUDE_CODE_EFFORT_LEVEL="${CLAUDE_CODE_EFFORT_LEVEL:-xhigh}"
#
# Auto mode - let a classifier pick the effort level per task. This is an
# ALTERNATIVE to a fixed effort level above (when auto mode is on, a fixed
# CLAUDE_CODE_EFFORT_LEVEL may be ignored). Another frequently-requested feature:
# export CLAUDE_CODE_ENABLE_AUTO_MODE="${CLAUDE_CODE_ENABLE_AUTO_MODE:-1}"
#
# Longer network timeout for large requests:
# export API_TIMEOUT_MS="${API_TIMEOUT_MS:-600000}"
# --- Routing scrub + local environment --------------------------------------
#
# CC_SCRUB_ROUTING=1 clears third-party model-routing variables before launch so
# Claude Code always uses the default Anthropic account. Useful when you also run
# wrappers (e.g. a DeepSeek launcher) that export ANTHROPIC_BASE_URL /
# ANTHROPIC_AUTH_TOKEN / *_MODEL to point Claude Code at a non-Anthropic model.
# Default 0: leave the environment as-is.
CC_SCRUB_ROUTING="${CC_SCRUB_ROUTING:-0}"
if [ "$CC_SCRUB_ROUTING" != "0" ]; then
unset CLAUDE_CONFIG_DIR \
ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_MODEL \
ANTHROPIC_DEFAULT_OPUS_MODEL ANTHROPIC_DEFAULT_SONNET_MODEL \
ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION \
ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES \
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES \
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES 2>/dev/null || true
fi
# Personal/local exports go between the markers below. They are a stable splice
# point: the Linux deploy step and the Windows build.ps1 inject a private env
# file here, so a personal build never hand-merges into the launcher body.
# Anything set here (effort level, API timeout, even routing) applies this launch
# and, coming after the scrub above, wins over it.
# >>> ccwa-local-env >>>
# <<< ccwa-local-env <<<
# --- Inject the thinking-display fix into the launch args -------------------
#
# Fire on a real agent invocation. Surfaces signal a real run differently:
# - the VS Code extension passes "--max-thinking-tokens N" (N > 0) plus the
# stream-json I/O flags, and does NOT pass "--thinking adaptive" or "-p";
# - the SDK / older extensions pass "--thinking adaptive" (or "enabled");
# - headless passes "-p" / "--print".
#
# Skip injection when:
# - thinking is explicitly disabled
# - --thinking-display is already present (no double-inject vs a patched extension)
# - CC_THINKING_DISPLAY=omitted
# - the command is a subcommand/probe such as mcp, config, or --version,
# which carries none of these markers
args=("$@")
have_display=false
thinking_adaptive=false
thinking_disabled=false
print_mode=false
max_thinking_on=false
prev=""
for a in "$@"; do
case "$a" in
--thinking-display|--thinking-display=*)
have_display=true
;;
--thinking=adaptive|--thinking=enabled)
thinking_adaptive=true
;;
--thinking=disabled)
thinking_disabled=true
;;
--max-thinking-tokens=*)
v="${a#*=}"
if [ -n "$v" ] && [ "$v" != "0" ]; then
max_thinking_on=true
fi
;;
-p|--print)
print_mode=true
;;
esac
if [ "$prev" = "--thinking" ]; then
case "$a" in
adaptive|enabled)
thinking_adaptive=true
;;
disabled)
thinking_disabled=true
;;
esac
fi
if [ "$prev" = "--max-thinking-tokens" ] && [ "$a" != "0" ]; then
max_thinking_on=true
fi
prev="$a"
done
if [ "$CC_WORKAROUNDS" != "0" ] \
&& [ "$have_display" = false ] \
&& [ "$thinking_disabled" = false ] \
&& [ "$DISPLAY_VALUE" != "omitted" ] \
&& { [ "$thinking_adaptive" = true ] || [ "$print_mode" = true ] || [ "$max_thinking_on" = true ]; }; then
args+=(--thinking-display "$DISPLAY_VALUE")
fi
# --- Reconcile the webview bundle: apply enabled bundle-patch features, undo
# disabled ones, PER FILE, every launch ---------------------------------
#
# Generic engine (replaces the single hard-coded context-icon sed). Each
# bundle-patch feature registers, per target file, an idempotent + reversible
# (apply, undo) pair. Every applied edit carries an ownership MARKER, and undo
# keys off our own fingerprints (the MARKER, plus any legacy unmarked form an
# older version of this tool wrote), so the launcher reverses ONLY its own edits
# and never touches upstream code that merely resembles a patched value.
#
# Per-file reconcile (see TECHNICAL.md "patch composition"):
# C = current bytes with every KNOWN feature's undo applied in REVERSE order
# (the pristine bundle, regardless of which of our patches were present)
# D = C with every ENABLED feature's apply applied in FORWARD order
# write D only when it differs from the current bytes (idempotent)
#
# Best-effort: every step is guarded and the whole pass runs under `|| true`, so
# it can never block the launch. Writes go through a metadata-preserving temp
# (`cp -p`, portable - the GNU-only `--reference` is avoided so this also works
# on macOS/BSD) and an atomic `mv -f`; a failed step leaves the original
# untouched.
#
# context-icon feature - component `FJe` in webview/index.js:
# if(t===0)return null;if(c>=50)return null}
# -> if(c>=101)return null}/*ccwa-context-icon:t:c*/
# `c` is "% of context remaining" (maxes at 100), so >=101 never fires. Removing
# the t===0 guard keeps the icon visible across a reload gap; it may briefly show
# 0% until the webview receives fresh usage data. The trailing
# /*ccwa-context-icon:<first-var>:<remaining-var>*/ is our ownership marker.
# Maintenance: this keys off the minified guard pair shape above, not the
# component name or exact minified variable names; if a future build changes that
# shape, apply no-ops loudly (a one-line warning) until the anchor here is updated.
# A bundle feature is enabled when the master switch is on AND its own toggle is
# on. CC_WORKAROUNDS=0 forces every feature off, so reconcile reverts to clean.
_cc_feature_enabled() {
[ "$CC_WORKAROUNDS" != "0" ] || return 1
case "$1" in
context-icon) [ "$CC_PATCH_CONTEXT_ICON" != "0" ] ;;
md-copy) [ "$CC_PATCH_MD_COPY" != "0" ] ;;
*) return 1 ;;
esac
}
# apply/undo operate on a path in place. Each is a no-op when its target state is
# already present/absent, so chaining them is safe and idempotent.
_cc_apply_context_icon() {
local f="$1" tmp count
if grep -q '/\*ccwa-context-icon' "$f" 2>/dev/null; then return 0; fi # already marked
count="$( (grep -E -o 'if\([A-Za-z_$][A-Za-z0-9_$]*===0\)return null;if\([A-Za-z_$][A-Za-z0-9_$]*>=50\)return null\}' "$f" 2>/dev/null || true) | wc -l | tr -d ' ')"
if [ "$count" = "0" ]; then
echo "claudemax: context-icon anchor not found in $f (extension changed?); skipping" >&2
return 0
fi
if [ "$count" != "1" ]; then return 0; fi # ambiguous (version changed) - skip
tmp="${f}.ccapply.$$"
if sed 's#if(\([A-Za-z_$][A-Za-z0-9_$]*\)===0)return null;if(\([A-Za-z_$][A-Za-z0-9_$]*\)>=50)return null}#if(\2>=101)return null}/*ccwa-context-icon:\1:\2*/#' "$f" > "$tmp" 2>/dev/null \
&& [ -s "$tmp" ] && grep -q '/\*ccwa-context-icon' "$tmp" 2>/dev/null; then
cat "$tmp" > "$f" 2>/dev/null || true
fi
rm -f "$tmp" 2>/dev/null || true
}
_cc_undo_context_icon() {
# Revert our edit to the pristine upstream form. Recognized fingerprints are:
# the current metadata-marked form; the legacy bare (metadata-less) marker on
# arbitrary guard names (older var-agnostic write); and legacy bare/unmarked
# forms that older t/c-only versions wrote. Marked substitutions run first
# because bare strings are prefixes of marked strings; a final pass strips any
# leftover bare marker so apply (which exits early on ANY marker) is never
# wedged by an unrecognized form. We deliberately do NOT do a generic
# >=101->=50 rewrite: a bare >=101 guard with no marker is not necessarily ours,
# and rewriting it would corrupt upstream code that merely resembles a patched
# value (the ownership invariant above). Every form we actually write is covered
# by the scoped substitutions below.
local f="$1" tmp
# Nothing of ours: no >=101 guard AND no leftover marker. The marker check is
# load-bearing - a file an older buggy undo left wedged (gate already reverted to
# >=50 but the bare marker still appended) has no >=101, yet the orphan strip
# below must still run or apply stays wedged on the surviving marker.
grep -qF '>=101)return null}' "$f" 2>/dev/null \
|| grep -qF '/*ccwa-context-icon' "$f" 2>/dev/null \
|| return 0
tmp="${f}.ccundo.$$"
if sed -e 's#if(\([A-Za-z_$][A-Za-z0-9_$]*\)>=101)return null}/\*ccwa-context-icon:\([A-Za-z_$][A-Za-z0-9_$]*\):\1\*/#if(\2===0)return null;if(\1>=50)return null}#g' \
-e 's#if(\([A-Za-z_$][A-Za-z0-9_$]*\)===0)return null;if(\([A-Za-z_$][A-Za-z0-9_$]*\)>=101)return null}/\*ccwa-context-icon\*/#if(\1===0)return null;if(\2>=50)return null}#g' \
-e 's#if(c>=101)return null}/\*ccwa-context-icon\*/#if(t===0)return null;if(c>=50)return null}#g' \
-e 's#if(t===0)return null;if(c>=101)return null}#if(t===0)return null;if(c>=50)return null}#g' \
-e 's#if(c>=101)return null}#if(t===0)return null;if(c>=50)return null}#g' \
-e 's#)return null}/\*ccwa-context-icon\*/#)return null}#g' "$f" > "$tmp" 2>/dev/null \
&& [ -s "$tmp" ]; then
cat "$tmp" > "$f" 2>/dev/null || true
fi
rm -f "$tmp" 2>/dev/null || true
}
# md-copy feature - a large IIFE appended to webview/index.js plus matching CSS
# appended to webview/index.css, each bracketed by the sentinel
# /* cc-md-copy v1 */ ... /* /cc-md-copy v1 */ (its ownership marker). apply
# appends the block at END-OF-FILE; undo removes exactly that OPEN..CLOSE block
# (marker-scoped, keeps any bytes after CLOSE), so it composes with context-icon
# (an in-place swap elsewhere in index.js) regardless of ordering. The payload below is GENERATED from
# fixes/markdown-copy-export/webview-inject.{js,css} by tools/gen-embeds; do not
# edit it by hand (CI drift check: tools/gen-embeds --check).
# >>>CCWA-MD-COPY-EMBED>>> (generated by tools/gen-embeds; do not edit)
_cc_md_copy_js() { cat <<'CCMDCOPYJS'
/* cc-md-copy: per-message and whole-conversation copy (Markdown) for the
* Claude Code VS Code webview. Self-contained IIFE appended to webview/index.js.
* Each control is a single clipboard icon that flips to a checkmark for ~2s when a
* copy actually succeeds (no text label, no menu). Additive and read-only w.r.t.
* app state; keyed on stable CSS-module class prefixes, so it fails safe (controls
* simply do not appear) if a prefix moves.
* Exposes its pure functions for node unit tests; boot()s only in a real webview. */
/* Leading ';' so that, appended after the bundle, this IIFE can never be parsed as
* a call on the bundle's final expression if it lacks a trailing semicolon (ASI
* safety across extension builds). */
;(function () {
"use strict";
var CONTROL_PREFIX = "cc-md-copy"; // every injected node's class starts with this
var USER_BUBBLE = '[class*="userMessageContainer_"]';
// Assistant message wrapper. Verified on 2.1.170: the render emits exactly one
// `data-testid="assistant-message"` div per assistant turn, with the rating
// widget and content blocks as its children. (The earlier `[data-message-rating]`
// was WRONG: that attribute sits on the nested rating control, which is also only
// rendered behind an experiment+analytics gate.) Re-pinned in Task 6.
var ASSISTANT_BUBBLE = '[data-testid="assistant-message"]';
var MESSAGES_CONTAINER = '[class*="messagesContainer_"]'; // e.g. '[class*="timeline_"]'; "" -> observe document.body
// Optional narrowing only. MUST be a single wrapper around ALL content blocks,
// not a per-block class (a turn has multiple blocks). "" -> use the bubble itself
// (already aggregates all blocks; sanitizeClone is the correctness gate).
var ASSISTANT_CONTENT = "";
var FEEDBACK_MS = 2000; // how long the checkmark shows after a successful copy
// ---- HTML -> Markdown (DOM walk) -------------------------------------------
// Uses only: nodeType, tagName, childNodes, textContent, getAttribute, className.
function htmlToMarkdown(root) {
// Longest run of consecutive backticks in s, so a code delimiter/fence can be
// chosen longer than anything inside it (else ``` in the content closes early).
function backtickRun(s) {
var max = 0, cur = 0;
for (var i = 0; i < s.length; i++) {
if (s.charAt(i) === "`") { cur++; if (cur > max) max = cur; } else cur = 0;
}
return max;
}
function fence(s, min) { var n = backtickRun(s) + 1; if (n < min) n = min; return new Array(n + 1).join("`"); }
function inline(node) {
var out = "";
var kids = node.childNodes || [];
for (var i = 0; i < kids.length; i++) {
var c = kids[i];
if (c.nodeType === 3) { out += c.textContent || ""; continue; }
if (c.nodeType !== 1) continue;
var tag = (c.tagName || "").toUpperCase();
if (tag === "BR") out += "\n";
else if (tag === "STRONG" || tag === "B") out += "**" + inline(c) + "**";
else if (tag === "EM" || tag === "I") out += "*" + inline(c) + "*";
else if (tag === "DEL" || tag === "S") out += "~~" + inline(c) + "~~";
else if (tag === "CODE") {
var ct = c.textContent || "";
var d = fence(ct, 1);
// CommonMark strips one leading+trailing space, so pad when an edge is a
// backtick to keep it from merging with the delimiter.
var p = (ct.charAt(0) === "`" || ct.charAt(ct.length - 1) === "`") ? " " : "";
out += d + p + ct + p + d;
}
else if (tag === "A") {
var href = c.getAttribute ? c.getAttribute("href") : null;
var t = inline(c);
out += href ? "[" + t + "](" + href + ")" : t;
} else out += inline(c); // unknown inline wrapper: keep text, drop tag
}
return out;
}
function langOf(codeEl) {
var cls = "";
if (codeEl) cls = (codeEl.getAttribute && codeEl.getAttribute("class")) || codeEl.className || "";
var m = /language-([A-Za-z0-9+#.\-]+)/.exec(cls || "");
return m ? m[1] : "";
}
function findChildTag(node, tag) {
var kids = node.childNodes || [];
for (var i = 0; i < kids.length; i++) {
if (kids[i].nodeType === 1 && (kids[i].tagName || "").toUpperCase() === tag) return kids[i];
}
return null;
}
function list(node, ordered, depth) {
var out = "", n = 1;
var kids = node.childNodes || [];
for (var i = 0; i < kids.length; i++) {
var li = kids[i];
if (li.nodeType !== 1 || (li.tagName || "").toUpperCase() !== "LI") continue;
var marker = ordered ? n++ + ". " : "- ";
var indent = new Array(depth + 1).join(" ");
var lead = "", nested = "";
var lk = li.childNodes || [];
for (var j = 0; j < lk.length; j++) {
var ch = lk[j];
var ct = ch.nodeType === 1 ? (ch.tagName || "").toUpperCase() : "";
if (ct === "UL") nested += list(ch, false, depth + 1);
else if (ct === "OL") nested += list(ch, true, depth + 1);
else if (ch.nodeType === 3) lead += ch.textContent || "";
else lead += inline(ch);
}
out += indent + marker + lead.trim() + "\n" + nested;
}
return out;
}
function table(node) {
var rows = [];
(function collect(container) {
var kids = container.childNodes || [];
for (var i = 0; i < kids.length; i++) {
var c = kids[i];
if (c.nodeType !== 1) continue;
var t = (c.tagName || "").toUpperCase();
if (t === "THEAD" || t === "TBODY" || t === "TFOOT") collect(c);
else if (t === "TR") {
var cells = [], cc = c.childNodes || [];
for (var j = 0; j < cc.length; j++) {
var d = cc[j];
if (d.nodeType !== 1) continue;
var dt = (d.tagName || "").toUpperCase();
if (dt === "TH" || dt === "TD") cells.push(inline(d).trim());
}
rows.push(cells);
}
}
})(node);
if (!rows.length) return "";
var head = rows[0], body = rows.slice(1);
var sep = head.map(function () { return "---"; });
var out = "| " + head.join(" | ") + " |\n| " + sep.join(" | ") + " |\n";
for (var k = 0; k < body.length; k++) out += "| " + body[k].join(" | ") + " |\n";
return out;
}
function block(node) {
var out = "";
var kids = node.childNodes || [];
for (var i = 0; i < kids.length; i++) {
var c = kids[i];
if (c.nodeType === 3) { if ((c.textContent || "").trim()) out += c.textContent; continue; }
if (c.nodeType !== 1) continue;
var tag = (c.tagName || "").toUpperCase();
if (/^H[1-6]$/.test(tag)) out += new Array(+tag[1] + 1).join("#") + " " + inline(c).trim() + "\n\n";
else if (tag === "P") out += inline(c).trim() + "\n\n";
else if (tag === "UL") out += list(c, false, 0) + "\n";
else if (tag === "OL") out += list(c, true, 0) + "\n";
else if (tag === "PRE") {
var code = findChildTag(c, "CODE");
var lang = langOf(code || c);
var body = (code || c).textContent || "";
var f = fence(body, 3);
out += f + lang + "\n" + body.replace(/\n$/, "") + "\n" + f + "\n\n";
} else if (tag === "BLOCKQUOTE") {
var inner = block(c).trim().split("\n").map(function (l) { return "> " + l; }).join("\n");
out += inner + "\n\n";
} else if (tag === "DETAILS") out += block(c).trim() + "\n\n";
else if (tag === "SUMMARY") out += inline(c).trim() + "\n\n";
else if (tag === "HR") out += "---\n\n";
else if (tag === "TABLE") out += table(c) + "\n";
else if (tag === "BR") out += "\n";
else if (tag === "STRONG" || tag === "B" || tag === "EM" || tag === "I" ||
tag === "A" || tag === "CODE" || tag === "DEL" || tag === "S")
out += inline(c) + "\n\n";
else out += block(c); // unknown wrapper: recurse (drop tag, keep content)
}
return out;
}
// block() dispatches on each CHILD's tag, treating the passed node as a plain
// container. Wrap root in a one-off container so root's OWN tag is dispatched
// too: callers pass either the bubble container (its block children render) or
// a single block element like <pre>/<ul>/<table> (now handled, not flattened).
return block({ childNodes: [root] }).replace(/\n{3,}/g, "\n\n").trim();
}
// ---- pure helpers ----------------------------------------------------------
function hasPrefix(node, prefix) {
if (node.nodeType !== 1 || typeof node.className !== "string") return false;
var parts = node.className.split(/\s+/);
for (var i = 0; i < parts.length; i++) if (parts[i].indexOf(prefix) === 0) return true;
return false;
}
// Class-prefix hooks for non-content chrome that renders *inside* an assistant
// bubble (verified on 2.1.170; Task 6 re-pins these). Tool blocks are excluded
// from message copy; thinking summaries are visible content and must remain
// copyable. unknownContent_ is the renderer's fallback for unrecognized block
// types, so stripping it makes a *future* block type fail safe to excluded rather
// than leaking "Unsupported content" into the copy. Re-pin if a prefix moves.
var CHROME_PREFIXES = ["toolUse_", "toolResult_", "toolReference_", "unknownContent_"];
// True for any node that must never appear in copied output: our own controls,
// the rating widget (`data-message-rating` + its "Thanks for your feedback"
// text), any button (copy-code chrome), and the excluded content blocks above.
function isChrome(node) {
if (node.nodeType !== 1) return false;
if ((node.tagName || "").toUpperCase() === "BUTTON") return true;
if (node.getAttribute && node.getAttribute("data-message-rating") !== null) return true;
if (hasPrefix(node, CONTROL_PREFIX)) return true;
for (var i = 0; i < CHROME_PREFIXES.length; i++) if (hasPrefix(node, CHROME_PREFIXES[i])) return true;
return false;
}
// Deep-clone `contentNode`, then strip every chrome node so copied output is the
// message's text content only. This is a CORRECTNESS GATE, not cosmetic: the
// default content node is the whole bubble (all content-block siblings, so multi-
// block assistant turns are captured), and this strip-list is the only thing
// keeping the rating widget and excluded tool/fallback blocks out of the copy.
function sanitizeClone(contentNode) {
var clone = contentNode.cloneNode(true);
(function strip(node) {
var kids = Array.prototype.slice.call(node.childNodes || []);
for (var i = 0; i < kids.length; i++) {
var c = kids[i];
if (c.nodeType === 1 && isChrome(c)) { node.removeChild(c); continue; }
if (c.nodeType === 1) strip(c);
}
})(clone);
return clone;
}
function hasCopyableContent(contentNode, role) {
function walk(node) {
if (!node) return false;
if (node.nodeType === 3) return !!(node.textContent || "").trim();
if (node.nodeType !== 1) return false;
if (isChrome(node)) return false;
var kids = node.childNodes || [];
for (var i = 0; i < kids.length; i++) if (walk(kids[i])) return true;
return false;
}
return walk(contentNode);
}
function classifyBubble(node) {
if (node.nodeType !== 1) return null;
if (hasPrefix(node, "userMessageContainer_")) return "user";
if (node.getAttribute && node.getAttribute("data-testid") === "assistant-message") return "assistant";
return null;
}
// Build the whole-conversation markdown from an ordered list of bubbles.
// `contentOf(bubble)` resolves the content node (default: the bubble itself, so
// every content block is included; sanitizeClone drops chrome); a default is
// provided for tests.
function conversationToMarkdown(bubbles, contentOf) {
contentOf = contentOf || function (b) { return b; };
var parts = [];
for (var i = 0; i < bubbles.length; i++) {
var role = classifyBubble(bubbles[i]);
if (!role) continue;
var clean = sanitizeClone(contentOf(bubbles[i]));
var body = role === "assistant" ? htmlToMarkdown(clean) : (clean.textContent || "").trim();
if (!body) continue;
parts.push((role === "user" ? "## User" : "## Assistant") + "\n\n" + body);
}
return parts.join("\n\n") + (parts.length ? "\n" : "");
}
// ---- exports (node tests) / boot (real webview) ----------------------------
if (typeof document !== "undefined") {
boot();
} else if (typeof module !== "undefined" && module.exports) {
module.exports = { htmlToMarkdown: htmlToMarkdown, sanitizeClone: sanitizeClone,
classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown,
hasCopyableContent: hasCopyableContent, copyText: copyText };
}
// ---- live-webview wiring (runs only when a document exists) ----------------
function qs(node, sel) { try { return sel && node.querySelector ? node.querySelector(sel) : null; } catch (_) { return null; } }
function qsa(sel) { try { return Array.prototype.slice.call(document.querySelectorAll(sel)); } catch (_) { return []; } }
// The content node to convert/copy: the optional ASSISTANT_CONTENT wrapper if
// pinned and present, else the bubble itself. The bubble already contains every
// content-block sibling of a multi-block turn, and sanitizeClone strips the
// chrome (rating widget, tool/unknown blocks, buttons, our controls)
// either way -- so this is a narrowing, never the thing that guarantees
// correctness.
function contentNodeOf(bubble, role) {
if (role === "assistant" && ASSISTANT_CONTENT) {
var n = qs(bubble, ASSISTANT_CONTENT);
if (n) return n;
}
return bubble;
}
// Copy `s` via a synchronous execCommand("copy") on an off-screen textarea, and
// report whether it actually happened. Done first (and synchronously) because it
// runs inside the click gesture and works whether or not the page is a secure
// context -- so it covers remote / code-server, where the async Clipboard API is
// simply absent. Restores the prior selection/focus so it is invisible.
function execCopy(s) {
try {
if (typeof document === "undefined" || !document.createElement) return false;
var prev = document.activeElement || null;
var sel = document.getSelection ? document.getSelection() : null;
var saved = (sel && sel.rangeCount) ? sel.getRangeAt(0) : null;
var ta = document.createElement("textarea");
ta.value = s;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.top = "-1000px";
ta.style.left = "0";
ta.style.opacity = "0";
(document.body || document.documentElement).appendChild(ta);
ta.focus();
ta.select();
var ok = false;
try { ok = document.execCommand("copy"); } catch (_) { ok = false; }
if (ta.parentNode) ta.parentNode.removeChild(ta);
if (saved && sel) { try { sel.removeAllRanges(); sel.addRange(saved); } catch (_) {} }
if (prev && prev.focus) { try { prev.focus(); } catch (_) {} }
return !!ok;
} catch (_) { return false; }
}
// Copy `text` and resolve to whether the copy ACTUALLY happened, so callers only
// show success on a real copy -- never a false "copied" (the original bug:
// navigator.clipboard was undefined in the webview, the code fell through to
// Promise.resolve(), and the UI claimed success while nothing was written). Empty
// text is a non-copy -> false. execCommand first (gesture-safe, secure-context-
// independent); the async Clipboard API is the fallback. Never throws.
function copyText(text) {
var s = (text == null) ? "" : String(text);
if (!s) return Promise.resolve(false);
if (execCopy(s)) return Promise.resolve(true);
try {
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(s).then(
function () { return true; },
function () { return false; }
);
}
} catch (_) {}
return Promise.resolve(false);
}
function bubbleMarkdown(bubble, role) {
var clean = sanitizeClone(contentNodeOf(bubble, role));
return role === "assistant" ? htmlToMarkdown(clean) : (clean.textContent || "").trim();
}
// Inline SVG icons (currentColor, ~14px). Set via innerHTML on our own buttons
// only; the markup never reaches copied content (sanitizeClone drops our nodes).
var ICON_COPY = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
var ICON_CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>';
// Flip the button to a checkmark for FEEDBACK_MS, then restore. Idempotent across
// rapid clicks (any pending restore is cleared first).
function showCopied(btn) {
try {
if (btn.__ccTimer) clearTimeout(btn.__ccTimer);
btn.classList.add(CONTROL_PREFIX + "-ok");
btn.innerHTML = ICON_CHECK;
btn.__ccTimer = setTimeout(function () {
try { btn.classList.remove(CONTROL_PREFIX + "-ok"); btn.innerHTML = ICON_COPY; } catch (_) {}
btn.__ccTimer = null;
}, FEEDBACK_MS);
} catch (_) {}
}
// Build a single control: one clipboard-icon button. `onCopy()` is invoked
// synchronously on click (so the copy stays inside the user gesture) and must
// return a Promise<boolean>; the checkmark shows only when it resolves true. All
// nodes carry the CONTROL_PREFIX class so sanitizeClone strips them from copies.
function buildControl(onCopy, title) {
var wrap = document.createElement("span");
wrap.className = CONTROL_PREFIX;
var btn = document.createElement("button");
btn.type = "button";
btn.className = CONTROL_PREFIX + "-btn";
btn.title = title || "Copy as Markdown";
btn.setAttribute("aria-label", btn.title);
btn.innerHTML = ICON_COPY;
var busy = false;
btn.addEventListener("click", function (e) {
e.stopPropagation();
if (busy) return;
busy = true;
var p;
try { p = onCopy(); } catch (_) { p = false; }
Promise.resolve(p).then(
function (ok) { busy = false; if (ok) showCopied(btn); },
function () { busy = false; }
);
});
wrap.appendChild(btn);
return wrap;
}
function decorate(bubble) {
try {
var role = classifyBubble(bubble);
if (!role) return;
// Idempotent: keep exactly one control. A React re-render of the bubble can
// leave a stale control behind or transiently defeat an "already decorated"
// guard, which is what produced duplicate rows of buttons; prune any extras
// every sweep and only add one when none remain.
var existing = bubble.querySelectorAll ? bubble.querySelectorAll("." + CONTROL_PREFIX) : null;
if (!hasCopyableContent(contentNodeOf(bubble, role), role)) {
if (existing && existing.length) {
for (var j = existing.length - 1; j >= 0; j--) {
if (existing[j] && existing[j].parentNode) existing[j].parentNode.removeChild(existing[j]);
}
}
return;
}
if (existing && existing.length) {
for (var i = existing.length - 1; i >= 1; i--) {
if (existing[i] && existing[i].parentNode) existing[i].parentNode.removeChild(existing[i]);
}
return;
}
var control = buildControl(function () {
return copyText(bubbleMarkdown(bubble, role));
}, "Copy as Markdown");
bubble.appendChild(control);
} catch (_) {}
}
function copyConversation() {
var bubbles = qsa(USER_BUBBLE + "," + ASSISTANT_BUBBLE);
return copyText(conversationToMarkdown(bubbles, function (b) {
return contentNodeOf(b, classifyBubble(b));
}));
}
// A single floating "Copy conversation" icon, present only while a conversation
// is open (so it never clutters the history-list view). Pinned top-right by CSS,
// clear of the chat input at the bottom; the most-recent-prompt sticky header
// sits to its left.
function installConversationControl() {
try {
var existing = qs(document, "." + CONTROL_PREFIX + "-conversation");
var hasMessages = qsa(USER_BUBBLE + "," + ASSISTANT_BUBBLE).length > 0;
if (!hasMessages) {
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
return;
}
if (existing) return;
var bar = document.createElement("div");
bar.className = CONTROL_PREFIX + "-conversation";
bar.appendChild(buildControl(copyConversation, "Copy conversation"));
document.body.appendChild(bar);
} catch (_) {}
}
function sweep() {
var b = qsa(USER_BUBBLE + "," + ASSISTANT_BUBBLE);
for (var i = 0; i < b.length; i++) decorate(b[i]);
installConversationControl();
}
function boot() {
try {
var target = (MESSAGES_CONTAINER && qs(document, MESSAGES_CONTAINER)) || document.body;
sweep();
if (typeof MutationObserver === "undefined") return;
var obs = new MutationObserver(function () { sweep(); });
obs.observe(target, { childList: true, subtree: true });
} catch (_) {}
}
})();
CCMDCOPYJS
}
_cc_md_copy_css() { cat <<'CCMDCOPYCSS'
.cc-md-copy {
display: inline-flex;
align-items: center;
vertical-align: middle;
margin-left: 6px;
}
.cc-md-copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
color: var(--vscode-foreground);
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
opacity: 0.6;
}
.cc-md-copy-btn svg {
display: block;
width: 14px;
height: 14px;
}
.cc-md-copy-btn:hover {
opacity: 1;
background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15));
}
/* Success state: the icon is a green checkmark for a moment after a real copy. */
.cc-md-copy-btn.cc-md-copy-ok,
.cc-md-copy-btn.cc-md-copy-ok:hover {
opacity: 1;
color: var(--vscode-charts-green, var(--vscode-testing-iconPassed, #89d185));
background: transparent;
}
/* Whole-conversation copy: a single floating icon pinned to the top-right corner,
clear of the chat input at the bottom. Shown only while a conversation is open
(the IIFE adds/removes it). Nudge top/right here if it crowds the sticky header. */
.cc-md-copy-conversation {
position: fixed;
top: 26px;
right: 4px;
z-index: 30;
display: inline-flex;
padding: 2px;
background: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-widget-border, transparent);
border-radius: 6px;
opacity: 0.85;
}
.cc-md-copy-conversation .cc-md-copy {
margin-left: 0;
}
.cc-md-copy-conversation:hover {
opacity: 1;
}
CCMDCOPYCSS
}
# <<<CCWA-MD-COPY-EMBED<<<
_cc_md_copy_has() { grep -qF '/* cc-md-copy v1 */' "$1" 2>/dev/null; }
# Append our sentinel block (byte-identical to the node/python deliveries):
# "\n" + OPEN + "\n" + PAYLOAD + "\n" + CLOSE + "\n"
_cc_apply_md_copy() { # $1=file $2=js|css
local f="$1" kind="$2" tmp
_cc_md_copy_has "$f" && return 0 # already applied
tmp="${f}.ccmdapply.$$"
if { cat "$f" \
&& printf '\n/* cc-md-copy v1 */\n' \
&& { if [ "$kind" = css ]; then _cc_md_copy_css; else _cc_md_copy_js; fi; } \
&& printf '/* /cc-md-copy v1 */\n'; } > "$tmp" 2>/dev/null \
&& [ -s "$tmp" ] && _cc_md_copy_has "$tmp"; then
cat "$tmp" > "$f" 2>/dev/null || true
fi
rm -f "$tmp" 2>/dev/null || true
}
# Reverse transform: marker-scoped block removal (same algorithm as the node/python
# deliveries). Removes exactly our OPEN..CLOSE block plus the separator newline
# apply added, and KEEPS any bytes after CLOSE (prefix + suffix splice, not a
# truncate-to-EOF) - so undo is independent of file ordering and composes with a
# future end-of-file append feature.
_cc_undo_md_copy() {
local f="$1" ooff coff cend size tmp
_cc_md_copy_has "$f" || return 0 # nothing of ours
ooff="$(grep -boF '/* cc-md-copy v1 */' "$f" 2>/dev/null | head -1 | cut -d: -f1)"
coff="$(grep -boF '/* /cc-md-copy v1 */' "$f" 2>/dev/null | head -1 | cut -d: -f1)"
[ -n "$ooff" ] && [ -n "$coff" ] && [ "$coff" -ge "$ooff" ] || return 0 # malformed -> leave intact
[ "$ooff" -gt 0 ] && ooff=$((ooff - 1)) # also remove the separator newline before OPEN
cend=$((coff + 20)) # 20 = byte length of CLOSE marker '/* /cc-md-copy v1 */'
size="$(wc -c < "$f" 2>/dev/null | tr -d ' ')"
# drop the one trailing newline apply added, iff the byte after CLOSE is "\n"
if [ -n "$size" ] && [ "$cend" -lt "$size" ] \
&& [ "$(tail -c "+$((cend + 1))" "$f" 2>/dev/null | head -c 1 | od -An -tu1 | tr -d ' ')" = "10" ]; then
cend=$((cend + 1))
fi
tmp="${f}.ccmdundo.$$"
if { head -c "$ooff" "$f" 2>/dev/null
[ -n "$size" ] && [ "$cend" -lt "$size" ] && tail -c "+$((cend + 1))" "$f" 2>/dev/null
true; } > "$tmp" 2>/dev/null; then
cat "$tmp" > "$f" 2>/dev/null || true
fi
rm -f "$tmp" 2>/dev/null || true
}
# Shared tail for both file reconcilers: write `patched` if it differs from the
# live file, taking the one-time pristine snapshot (= `base`) on first change.
_cc_commit_reconciled() { # $1=f $2=base $3=patched
local f="$1" base="$2" patched="$3" tmpmeta
if cmp -s "$patched" "$f"; then rm -f "$base" "$patched" 2>/dev/null || true; return 0; fi
if [ ! -e "${f}.bak-cc-workarounds" ]; then
if cp -p "$f" "${f}.bak-cc-workarounds" 2>/dev/null; then
cat "$base" > "${f}.bak-cc-workarounds" 2>/dev/null || true
fi
fi
tmpmeta="${f}.ccwrite.$$"
if cp -p "$f" "$tmpmeta" 2>/dev/null && cat "$patched" > "$tmpmeta" 2>/dev/null; then
mv -f "$tmpmeta" "$f" 2>/dev/null || rm -f "$tmpmeta" 2>/dev/null || true
else
rm -f "$tmpmeta" 2>/dev/null || true
fi
rm -f "$base" "$patched" 2>/dev/null || true
}
# Reconcile webview/index.js. Registry (forward apply order): context-icon
# (in-place), then md-copy (append, registered LAST). Undo runs in REVERSE.
_cc_reconcile_index_js() {
local f="$1" base patched
[ -f "$f" ] && [ -r "$f" ] || return 0
base="${f}.ccbase.$$"
patched="${f}.ccnew.$$"
cp "$f" "$base" 2>/dev/null || { rm -f "$base" 2>/dev/null || true; return 0; }
# Clean base C = current with every KNOWN feature undone, REVERSE order.
_cc_undo_md_copy "$base"
_cc_undo_context_icon "$base"
# Desired D = C with every ENABLED feature applied, FORWARD order.
cp "$base" "$patched" 2>/dev/null || { rm -f "$base" "$patched" 2>/dev/null || true; return 0; }
if _cc_feature_enabled context-icon; then _cc_apply_context_icon "$patched"; fi
if _cc_feature_enabled md-copy; then _cc_apply_md_copy "$patched" js; fi
_cc_commit_reconciled "$f" "$base" "$patched"
}
# Reconcile webview/index.css. Registry: md-copy (append) only.
_cc_reconcile_index_css() {
local f="$1" base patched
[ -f "$f" ] && [ -r "$f" ] || return 0
base="${f}.ccbase.$$"
patched="${f}.ccnew.$$"
cp "$f" "$base" 2>/dev/null || { rm -f "$base" 2>/dev/null || true; return 0; }
_cc_undo_md_copy "$base"
cp "$base" "$patched" 2>/dev/null || { rm -f "$base" "$patched" 2>/dev/null || true; return 0; }
if _cc_feature_enabled md-copy; then _cc_apply_md_copy "$patched" css; fi
_cc_commit_reconciled "$f" "$base" "$patched"
}
_cc_reconcile() {
[ "$CC_RECONCILE" != "0" ] || return 0 # emergency bypass: touch nothing
local d extdir f
# Most precise target: walk up from REAL_CLAUDE to the extension root.
d="$(dirname "$REAL_CLAUDE" 2>/dev/null || echo "")"
extdir=""
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "." ]; do
case "${d##*/}" in anthropic.claude-code-*) extdir="$d"; break ;; esac
d="$(dirname "$d" 2>/dev/null || echo "")"
done
if [ -n "$extdir" ]; then
_cc_reconcile_index_js "$extdir/webview/index.js"
_cc_reconcile_index_css "$extdir/webview/index.css"
fi
# Also cover any installed extension under this user's VS Code dirs (terminal
# launches, or when the real binary is the standalone CLI). Unmatched globs
# fall through harmlessly - _cc_reconcile_index_js skips non-files.
for f in \
"$HOME"/.vscode/extensions/anthropic.claude-code-*/webview/index.js \
"$HOME"/.vscode-insiders/extensions/anthropic.claude-code-*/webview/index.js \
"$HOME"/.vscode-server/extensions/anthropic.claude-code-*/webview/index.js \
"$HOME"/.vscode-server-insiders/extensions/anthropic.claude-code-*/webview/index.js; do