cd /news/developer-tools/launcher-to-run-glm-5-2-in-claude-co… · home topics developer-tools article
[ARTICLE · art-40330] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Launcher to run GLM 5.2 in Claude Code harness (full launcher with all features: thinking summaries, context icon, markdown copy)

A developer released 'claudemax', a launcher for the Claude Code harness that restores extended-thinking summaries on Opus 4.7/4.8, re-enables the context-usage icon in VS Code, and adds a 'Copy as Markdown' icon to messages. The launcher applies three independent fixes via environment variables, patching the extension's webview bundle idempotently on each launch.

read46 min views18 publishedJun 24, 2026

| #!/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[@]}"} |

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/launcher-to-run-glm-…] indexed:0 read:46min 2026-06-24 ·