| #!/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 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 <<< | |
| # --- 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[@]}"} |
Anthropic Says China Was Never Authorized to Use Claude Code Anyway