Skip to content

Instantly share code, notes, and snippets.

@phase3dev
Last active July 8, 2026 11:49
Show Gist options
  • Select an option

  • Save phase3dev/1f7ee1cb17151c19d538e053f4f548ca to your computer and use it in GitHub Desktop.

Select an option

Save phase3dev/1f7ee1cb17151c19d538e053f4f548ca to your computer and use it in GitHub Desktop.
Launcher to run GLM 5.2 in Claude Code harness (full launcher with all features: thinking summaries, context icon, markdown copy)
#!/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.
# 2026-07-07: cannot override the server-side experiment that blanks Opus 4.8
# summaries (x-cc-atis header); see the README's 2026-07-07 update. The
# launcher can apply that update's mitigation - opt in with CC_ATIS_OPTOUT=1
# (OFF by default; see the section below and the README for its caveats).
# 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)
# export CC_ATIS_OPTOUT=1 # purge cached x-cc-atis experiment
# # assignment + essential-traffic-only env (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 GLM 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 >>>
# --- Connection ---
export ANTHROPIC_BASE_URL="https://api.z.ai/api/anthropic"
export ANTHROPIC_AUTH_TOKEN="your token"
# --- Model mapping ---
export ANTHROPIC_MODEL="glm-5.2"
export ANTHROPIC_DEFAULT_OPUS_MODEL="glm-5.2"
export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-5.2"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-5.2"
export CLAUDE_CODE_SUBAGENT_MODEL="glm-5.2"
# --- Session isolation (run this alongside the default `claude` at the same time) ---
export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude-glm}"
# <<< ccwa-local-env <<<
# --- x-cc-atis experiment opt-out (OFF by default) ---------------------------
#
# CC_ATIS_OPTOUT=1 applies the verified 2026-07-07 mitigation for the
# server-side experiment that blanks Opus 4.8 thinking summaries (README
# "2026-07-07 update"):
# 1. purge any cached x-cc-atis experiment assignment from ~/.claude.json, and
# 2. launch with CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 so the CLI cannot
# re-enroll this install. (2026-07-08 correction: DISABLE_GROWTHBOOK=1 does
# NOT stop re-enrollment - the assignment arrives via the CLI's startup
# bootstrap fetch, which only the essential-traffic-only switch blocks;
# measured. DISABLE_GROWTHBOOK stays exported as harmless defense in depth.)
# Both halves are required - the env var alone still sends the already-cached
# token, and the purge alone lasts only until the next refresh (both measured).
#
# OFF by default, unlike the other fixes, because it is not free: it edits the
# CLI's own config file, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 restricts
# the CLI to essential traffic ENTIRELY - it also disables claude.ai Projects
# sync, DesignSync, /feedback, --enable-live-preview, and telemetry (local
# sessions, transcripts, and resume are unaffected). Enable it deliberately.
#
# Safety model matches the bundle patches: idempotent (no rewrite when nothing
# is cached), one-time backup (~/.claude.json.bak-cc-workarounds, only if
# absent), atomic temp+mv write (jq exits non-zero on malformed input, so a bad
# parse never replaces the file), best-effort (never blocks the launch). The
# purge half needs jq; without it the launcher warns and still sets the env
# vars (an already-cached assignment is still sent until purged by hand - see
# the README's manual recipe).
CC_ATIS_OPTOUT="${CC_ATIS_OPTOUT:-0}"
_cc_atis_optout() {
[ "$CC_WORKAROUNDS" != "0" ] || return 0
[ "$CC_ATIS_OPTOUT" != "0" ] || return 0
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export DISABLE_GROWTHBOOK=1
local cfg="$HOME/.claude.json" tmp
[ -f "$cfg" ] && [ -r "$cfg" ] || return 0
if ! command -v jq >/dev/null 2>&1; then
echo "claudemax: CC_ATIS_OPTOUT=1 but jq is not installed; cached x-cc-atis assignment (if any) NOT purged" >&2
return 0
fi
# Idempotent: rewrite only when an assignment is actually cached.
jq -e '((.clientDataCacheSlots // {}) | any(.[]?; .data.atis? != null))
or ((.clientDataCache // {}) | has("atis"))' "$cfg" >/dev/null 2>&1 || return 0
if [ ! -e "${cfg}.bak-cc-workarounds" ]; then
cp -p "$cfg" "${cfg}.bak-cc-workarounds" 2>/dev/null || true
fi
tmp="${cfg}.ccatis.$$"
if jq '.clientDataCacheSlots |= (if . then with_entries(select((.value.data.atis? // null) == null)) else . end)
| del(.clientDataCache.atis)' "$cfg" > "$tmp" 2>/dev/null \
&& [ -s "$tmp" ]; then
mv -f "$tmp" "$cfg" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
else
rm -f "$tmp" 2>/dev/null || true
fi
}
_cc_atis_optout || true
# --- 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
_cc_reconcile_index_js "$f"
_cc_reconcile_index_css "${f%/index.js}/index.css"
done
}
# Best-effort: invoking under `|| true` suspends `set -e` for the whole pass, so
# nothing here can block the launch (matches the previous safety model).
_cc_reconcile || true
# The `${args[@]+...}` form guards the empty-array case under `set -u`,
# including older Bash versions such as the default Bash on older macOS systems.
exec "$REAL_CLAUDE" ${args[@]+"${args[@]}"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment