{"slug": "launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features", "title": "Launcher to run GLM 5.2 in Claude Code harness (full launcher with all features: thinking summaries, context icon, markdown copy)", "summary": "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.", "body_md": "| #!/usr/bin/env bash | |\n| # claudemax - Claude Code launcher that combines three unofficial fixes: | |\n| # | |\n| # 1. Restores extended-thinking summaries on Opus 4.7 / 4.8, where the | |\n| # \"Thinking\" section otherwise renders empty in the VS Code extension and | |\n| # headless -p/SDK. Done by injecting `--thinking-display summarized` into the | |\n| # launch args - the one lever that is NOT interactivity-gated. Edits nothing. | |\n| # 2. Restores the always-visible context-usage icon in the VS Code chat input. | |\n| # Recent extension builds (2.1.165+) hide that icon until you have used | |\n| # >50% of the context window; with the 1M window that is ~500k tokens, so it | |\n| # is effectively never shown. There is no env/CLI lever for this, so (unlike | |\n| # fix #1) this wrapper idempotently patches the extension's webview bundle on | |\n| # each launch, flipping the threshold so the icon shows at any usage level. | |\n| # Because it re-applies every launch, it survives extension updates. | |\n| # 3. Adds a single-click \"Copy as Markdown\" icon to every message (and a floating | |\n| # \"copy conversation\" icon) in the VS Code chat; the icon flips to a checkmark | |\n| # only when the copy truly lands. Like fix #2 there is no env/CLI lever, so this | |\n| # wrapper idempotently appends a self-contained block to the webview bundle | |\n| # (index.js + index.css) each launch; it fails safe (the controls simply do | |\n| # not appear if the markup moves) and survives extension updates. | |\n| # | |\n| # This single launcher carries every fix, each independently switchable by an | |\n| # environment variable (all on by default): CC_THINKING_DISPLAY=omitted (fix 1), | |\n| # CC_PATCH_CONTEXT_ICON=0 (fix 2), CC_PATCH_MD_COPY=0 (fix 3). E.g. for thinking | |\n| # summaries only, set CC_PATCH_CONTEXT_ICON=0 AND CC_PATCH_MD_COPY=0. | |\n| # | |\n| # NOTE: unlike fix #1, fixes #2 and #3 DO edit the extension's bundled webview | |\n| # files (#2 patches index.js in place; #3 appends a block to index.js + index.css). | |\n| # Those edits are idempotent and ownership-marked, snapshotted once to | |\n| # index.js.bak-cc-workarounds (emergency restore only), written atomically (a | |\n| # failed write leaves the original untouched), best-effort (it never blocks the | |\n| # launch), reconciled per file every launch, and toggle-able with | |\n| # CC_PATCH_CONTEXT_ICON=0 / CC_PATCH_MD_COPY=0 (or CC_WORKAROUNDS=0 / CC_RECONCILE=0). | |\n| # | |\n| # Use it: | |\n| # - VS Code (official \"Claude Code\" extension): set \"claudeCode.claudeProcessWrapper\" | |\n| # to the FULL path of this file, then reload the window. In a multi-root | |\n| # .code-workspace this setting is window-scoped, so put it in the workspace | |\n| # file's \"settings\" block (or User settings), not a folder .vscode/settings.json. | |\n| # - VS Code (third-party \"Claude Code Chat\"): set \"claudeCodeChat.executable.path\". | |\n| # - Terminal: run `claudemax` in place of `claude`. | |\n| # | |\n| # Toggle off (defaults in parentheses): | |\n| # export CC_THINKING_DISPLAY=omitted # hide thinking summaries (summarized) | |\n| # export CC_PATCH_CONTEXT_ICON=0 # leave the context-usage icon as-is (1) | |\n| # export CC_PATCH_MD_COPY=0 # no copy controls / webview append (1) | |\n| # export CC_WORKAROUNDS=0 # master: disable every fix (1) | |\n| # export CC_RECONCILE=0 # do not touch the webview bundle (1) | |\n| # export CC_SCRUB_ROUTING=1 # force the default Anthropic account (0) | |\n| # | |\n| # The real `claude` must be installed. This wrapper finds it automatically; if it | |\n| # cannot, set CLAUDE_REAL_BIN to the full path of your real claude binary. | |\n| set -euo pipefail | |\n| # --- Locate the real claude binary ----------------------------------------- | |\n| self=\"$(readlink -f \"$0\" 2>/dev/null || echo \"$0\")\" | |\n| # Process-wrapper convention: the official VS Code extension invokes the wrapper | |\n| # as <wrapper> <REAL_CLAUDE...> <args...>, passing the real CLI ahead of the | |\n| # args. <REAL_CLAUDE...> is either a single native-binary path (\".../claude\") or | |\n| # a node interpreter followed by the bundled cli.js (\".../node .../cli.js\"). | |\n| # Peel that off so it is not forwarded as a stray positional argument, and | |\n| # prefer it as the real claude. (Plain \"claudemax <args>\" use is unaffected: | |\n| # <args> never begins with an existing claude/node binary path.) | |\n| wrapper_bin=\"\" | |\n| if [ \"$#\" -gt 0 ] \\ | |\n| && printf '%s' \"$1\" | grep -Eqi '/claude(\\.exe|\\.cmd|\\.bat)?$' \\ | |\n| && [ -e \"$1\" ]; then | |\n| wrapper_bin=\"$1\" | |\n| shift | |\n| elif [ \"$#\" -ge 2 ] \\ | |\n| && printf '%s' \"$1\" | grep -Eqi '/node(\\.exe)?$' && [ -e \"$1\" ] \\ | |\n| && printf '%s' \"$2\" | grep -Eqi '\\.(c?js|mjs)$' && [ -e \"$2\" ]; then | |\n| # node + cli.js: exec node directly and keep cli.js as the first forwarded arg. | |\n| wrapper_bin=\"$1\" | |\n| shift | |\n| fi | |\n| REAL_CLAUDE=\"${CLAUDE_REAL_BIN:-}\" | |\n| if [ -z \"$REAL_CLAUDE\" ] && [ -n \"$wrapper_bin\" ]; then | |\n| REAL_CLAUDE=\"$wrapper_bin\" | |\n| fi | |\n| if [ -z \"$REAL_CLAUDE\" ]; then | |\n| for c in \\ | |\n| \"$HOME/.local/bin/claude\" \\ | |\n| /usr/local/bin/claude \\ | |\n| /usr/bin/claude \\ | |\n| /opt/homebrew/bin/claude \\ | |\n| \"$(command -v claude 2>/dev/null || true)\"; do | |\n| [ -n \"$c\" ] && [ -x \"$c\" ] || continue | |\n| [ \"$(readlink -f \"$c\" 2>/dev/null || echo \"$c\")\" = \"$self\" ] && continue | |\n| REAL_CLAUDE=\"$c\" | |\n| break | |\n| done | |\n| fi | |\n| [ -n \"$REAL_CLAUDE\" ] || { | |\n| echo \"claudemax: could not find the real 'claude' binary; set CLAUDE_REAL_BIN\" >&2 | |\n| exit 1 | |\n| } | |\n| # --- Behavior --------------------------------------------------------------- | |\n| # Set CC_THINKING_DISPLAY=omitted to hide thinking; default shows summaries. | |\n| DISPLAY_VALUE=\"${CC_THINKING_DISPLAY:-summarized}\" | |\n| case \"$DISPLAY_VALUE\" in | |\n| summarized|omitted) ;; | |\n| *) | |\n| echo \"claudemax: invalid CC_THINKING_DISPLAY=$DISPLAY_VALUE; using summarized\" >&2 | |\n| DISPLAY_VALUE=\"summarized\" | |\n| ;; | |\n| esac | |\n| # ===== FEATURE DEFAULTS (edit to taste; environment variables override) ===== | |\n| # Master switch: 0 disables every workaround (argument injection AND bundle | |\n| # patches) and reconcile reverts the webview to clean on this launch. When 1, | |\n| # the per-feature toggles below govern. | |\n| CC_WORKAROUNDS=\"${CC_WORKAROUNDS:-1}\" | |\n| # Emergency bundle bypass: 0 means do NOT read or write the webview bundle at all | |\n| # this launch (argument injection is unaffected). Leaves any existing patches in | |\n| # place without uninstalling. | |\n| CC_RECONCILE=\"${CC_RECONCILE:-1}\" | |\n| # context-icon bundle patch: 0 leaves the webview's context-usage icon unpatched. | |\n| CC_PATCH_CONTEXT_ICON=\"${CC_PATCH_CONTEXT_ICON:-1}\" | |\n| # (CC_THINKING_DISPLAY is handled above as DISPLAY_VALUE: summarized | omitted.) | |\n| # markdown copy/export bundle patch: 0 leaves the webview without the copy controls. | |\n| CC_PATCH_MD_COPY=\"${CC_PATCH_MD_COPY:-1}\" | |\n| # ============================================================================ | |\n| # --- Optional customizations ------------------------------------------------ | |\n| # | |\n| # Raise reasoning effort - longer, more detailed summaries. Uses more tokens: | |\n| # export CLAUDE_CODE_EFFORT_LEVEL=\"${CLAUDE_CODE_EFFORT_LEVEL:-xhigh}\" | |\n| # | |\n| # Auto mode - let a classifier pick the effort level per task. This is an | |\n| # ALTERNATIVE to a fixed effort level above (when auto mode is on, a fixed | |\n| # CLAUDE_CODE_EFFORT_LEVEL may be ignored). Another frequently-requested feature: | |\n| # export CLAUDE_CODE_ENABLE_AUTO_MODE=\"${CLAUDE_CODE_ENABLE_AUTO_MODE:-1}\" | |\n| # | |\n| # Longer network timeout for large requests: | |\n| # export API_TIMEOUT_MS=\"${API_TIMEOUT_MS:-600000}\" | |\n| # --- Routing scrub + local environment -------------------------------------- | |\n| # | |\n| # CC_SCRUB_ROUTING=1 clears third-party model-routing variables before launch so | |\n| # Claude Code always uses the default Anthropic account. Useful when you also run | |\n| # wrappers (e.g. a GLM launcher) that export ANTHROPIC_BASE_URL / | |\n| # ANTHROPIC_AUTH_TOKEN / *_MODEL to point Claude Code at a non-Anthropic model. | |\n| # Default 0: leave the environment as-is. | |\n| CC_SCRUB_ROUTING=\"${CC_SCRUB_ROUTING:-0}\" | |\n| if [ \"$CC_SCRUB_ROUTING\" != \"0\" ]; then | |\n| unset CLAUDE_CONFIG_DIR \\ | |\n| ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_MODEL \\ | |\n| ANTHROPIC_DEFAULT_OPUS_MODEL ANTHROPIC_DEFAULT_SONNET_MODEL \\ | |\n| ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL \\ | |\n| ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION \\ | |\n| ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION \\ | |\n| ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION \\ | |\n| ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES \\ | |\n| ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES \\ | |\n| ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES 2>/dev/null || true | |\n| fi | |\n| # Personal/local exports go between the markers below. They are a stable splice | |\n| # point: the Linux deploy step and the Windows build.ps1 inject a private env | |\n| # file here, so a personal build never hand-merges into the launcher body. | |\n| # Anything set here (effort level, API timeout, even routing) applies this launch | |\n| # and, coming after the scrub above, wins over it. | |\n| # >>> ccwa-local-env >>> | |\n| # --- Connection --- | |\n| export ANTHROPIC_BASE_URL=\"https://api.z.ai/api/anthropic\" | |\n| export ANTHROPIC_AUTH_TOKEN=\"your token\" | |\n| # --- Model mapping --- | |\n| export ANTHROPIC_MODEL=\"glm-5.2\" | |\n| export ANTHROPIC_DEFAULT_OPUS_MODEL=\"glm-5.2\" | |\n| export ANTHROPIC_DEFAULT_SONNET_MODEL=\"glm-5.2\" | |\n| export ANTHROPIC_DEFAULT_HAIKU_MODEL=\"glm-5.2\" | |\n| export CLAUDE_CODE_SUBAGENT_MODEL=\"glm-5.2\" | |\n| # --- Session isolation (run this alongside the default `claude` at the same time) --- | |\n| export CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude-glm}\" | |\n| # <<< ccwa-local-env <<< | |\n| # --- Inject the thinking-display fix into the launch args ------------------- | |\n| # | |\n| # Fire on a real agent invocation. Surfaces signal a real run differently: | |\n| # - the VS Code extension passes \"--max-thinking-tokens N\" (N > 0) plus the | |\n| # stream-json I/O flags, and does NOT pass \"--thinking adaptive\" or \"-p\"; | |\n| # - the SDK / older extensions pass \"--thinking adaptive\" (or \"enabled\"); | |\n| # - headless passes \"-p\" / \"--print\". | |\n| # | |\n| # Skip injection when: | |\n| # - thinking is explicitly disabled | |\n| # - --thinking-display is already present (no double-inject vs a patched extension) | |\n| # - CC_THINKING_DISPLAY=omitted | |\n| # - the command is a subcommand/probe such as mcp, config, or --version, | |\n| # which carries none of these markers | |\n| args=(\"$@\") | |\n| have_display=false | |\n| thinking_adaptive=false | |\n| thinking_disabled=false | |\n| print_mode=false | |\n| max_thinking_on=false | |\n| prev=\"\" | |\n| for a in \"$@\"; do | |\n| case \"$a\" in | |\n| --thinking-display|--thinking-display=*) | |\n| have_display=true | |\n| ;; | |\n| --thinking=adaptive|--thinking=enabled) | |\n| thinking_adaptive=true | |\n| ;; | |\n| --thinking=disabled) | |\n| thinking_disabled=true | |\n| ;; | |\n| --max-thinking-tokens=*) | |\n| v=\"${a#*=}\" | |\n| if [ -n \"$v\" ] && [ \"$v\" != \"0\" ]; then | |\n| max_thinking_on=true | |\n| fi | |\n| ;; | |\n| -p|--print) | |\n| print_mode=true | |\n| ;; | |\n| esac | |\n| if [ \"$prev\" = \"--thinking\" ]; then | |\n| case \"$a\" in | |\n| adaptive|enabled) | |\n| thinking_adaptive=true | |\n| ;; | |\n| disabled) | |\n| thinking_disabled=true | |\n| ;; | |\n| esac | |\n| fi | |\n| if [ \"$prev\" = \"--max-thinking-tokens\" ] && [ \"$a\" != \"0\" ]; then | |\n| max_thinking_on=true | |\n| fi | |\n| prev=\"$a\" | |\n| done | |\n| if [ \"$CC_WORKAROUNDS\" != \"0\" ] \\ | |\n| && [ \"$have_display\" = false ] \\ | |\n| && [ \"$thinking_disabled\" = false ] \\ | |\n| && [ \"$DISPLAY_VALUE\" != \"omitted\" ] \\ | |\n| && { [ \"$thinking_adaptive\" = true ] || [ \"$print_mode\" = true ] || [ \"$max_thinking_on\" = true ]; }; then | |\n| args+=(--thinking-display \"$DISPLAY_VALUE\") | |\n| fi | |\n| # --- Reconcile the webview bundle: apply enabled bundle-patch features, undo | |\n| # disabled ones, PER FILE, every launch --------------------------------- | |\n| # | |\n| # Generic engine (replaces the single hard-coded context-icon sed). Each | |\n| # bundle-patch feature registers, per target file, an idempotent + reversible | |\n| # (apply, undo) pair. Every applied edit carries an ownership MARKER, and undo | |\n| # keys off our own fingerprints (the MARKER, plus any legacy unmarked form an | |\n| # older version of this tool wrote), so the launcher reverses ONLY its own edits | |\n| # and never touches upstream code that merely resembles a patched value. | |\n| # | |\n| # Per-file reconcile (see TECHNICAL.md \"patch composition\"): | |\n| # C = current bytes with every KNOWN feature's undo applied in REVERSE order | |\n| # (the pristine bundle, regardless of which of our patches were present) | |\n| # D = C with every ENABLED feature's apply applied in FORWARD order | |\n| # write D only when it differs from the current bytes (idempotent) | |\n| # | |\n| # Best-effort: every step is guarded and the whole pass runs under `|| true`, so | |\n| # it can never block the launch. Writes go through a metadata-preserving temp | |\n| # (`cp -p`, portable - the GNU-only `--reference` is avoided so this also works | |\n| # on macOS/BSD) and an atomic `mv -f`; a failed step leaves the original | |\n| # untouched. | |\n| # | |\n| # context-icon feature - component `FJe` in webview/index.js: | |\n| # if(t===0)return null;if(c>=50)return null} | |\n| # -> if(c>=101)return null}/*ccwa-context-icon:t:c*/ | |\n| # `c` is \"% of context remaining\" (maxes at 100), so >=101 never fires. Removing | |\n| # the t===0 guard keeps the icon visible across a reload gap; it may briefly show | |\n| # 0% until the webview receives fresh usage data. The trailing | |\n| # /*ccwa-context-icon:<first-var>:<remaining-var>*/ is our ownership marker. | |\n| # Maintenance: this keys off the minified guard pair shape above, not the | |\n| # component name or exact minified variable names; if a future build changes that | |\n| # shape, apply no-ops loudly (a one-line warning) until the anchor here is updated. | |\n| # A bundle feature is enabled when the master switch is on AND its own toggle is | |\n| # on. CC_WORKAROUNDS=0 forces every feature off, so reconcile reverts to clean. | |\n| _cc_feature_enabled() { | |\n| [ \"$CC_WORKAROUNDS\" != \"0\" ] || return 1 | |\n| case \"$1\" in | |\n| context-icon) [ \"$CC_PATCH_CONTEXT_ICON\" != \"0\" ] ;; | |\n| md-copy) [ \"$CC_PATCH_MD_COPY\" != \"0\" ] ;; | |\n| *) return 1 ;; | |\n| esac | |\n| } | |\n| # apply/undo operate on a path in place. Each is a no-op when its target state is | |\n| # already present/absent, so chaining them is safe and idempotent. | |\n| _cc_apply_context_icon() { | |\n| local f=\"$1\" tmp count | |\n| if grep -q '/\\*ccwa-context-icon' \"$f\" 2>/dev/null; then return 0; fi # already marked | |\n| 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 ' ')\" | |\n| if [ \"$count\" = \"0\" ]; then | |\n| echo \"claudemax: context-icon anchor not found in $f (extension changed?); skipping\" >&2 | |\n| return 0 | |\n| fi | |\n| if [ \"$count\" != \"1\" ]; then return 0; fi # ambiguous (version changed) - skip | |\n| tmp=\"${f}.ccapply.$$\" | |\n| 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 \\ | |\n| && [ -s \"$tmp\" ] && grep -q '/\\*ccwa-context-icon' \"$tmp\" 2>/dev/null; then | |\n| cat \"$tmp\" > \"$f\" 2>/dev/null || true | |\n| fi | |\n| rm -f \"$tmp\" 2>/dev/null || true | |\n| } | |\n| _cc_undo_context_icon() { | |\n| # Revert our edit to the pristine upstream form. Recognized fingerprints are: | |\n| # the current metadata-marked form; the legacy bare (metadata-less) marker on | |\n| # arbitrary guard names (older var-agnostic write); and legacy bare/unmarked | |\n| # forms that older t/c-only versions wrote. Marked substitutions run first | |\n| # because bare strings are prefixes of marked strings; a final pass strips any | |\n| # leftover bare marker so apply (which exits early on ANY marker) is never | |\n| # wedged by an unrecognized form. We deliberately do NOT do a generic | |\n| # >=101->=50 rewrite: a bare >=101 guard with no marker is not necessarily ours, | |\n| # and rewriting it would corrupt upstream code that merely resembles a patched | |\n| # value (the ownership invariant above). Every form we actually write is covered | |\n| # by the scoped substitutions below. | |\n| local f=\"$1\" tmp | |\n| # Nothing of ours: no >=101 guard AND no leftover marker. The marker check is | |\n| # load-bearing - a file an older buggy undo left wedged (gate already reverted to | |\n| # >=50 but the bare marker still appended) has no >=101, yet the orphan strip | |\n| # below must still run or apply stays wedged on the surviving marker. | |\n| grep -qF '>=101)return null}' \"$f\" 2>/dev/null \\ | |\n| || grep -qF '/*ccwa-context-icon' \"$f\" 2>/dev/null \\ | |\n| || return 0 | |\n| tmp=\"${f}.ccundo.$$\" | |\n| 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' \\ | |\n| -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' \\ | |\n| -e 's#if(c>=101)return null}/\\*ccwa-context-icon\\*/#if(t===0)return null;if(c>=50)return null}#g' \\ | |\n| -e 's#if(t===0)return null;if(c>=101)return null}#if(t===0)return null;if(c>=50)return null}#g' \\ | |\n| -e 's#if(c>=101)return null}#if(t===0)return null;if(c>=50)return null}#g' \\ | |\n| -e 's#)return null}/\\*ccwa-context-icon\\*/#)return null}#g' \"$f\" > \"$tmp\" 2>/dev/null \\ | |\n| && [ -s \"$tmp\" ]; then | |\n| cat \"$tmp\" > \"$f\" 2>/dev/null || true | |\n| fi | |\n| rm -f \"$tmp\" 2>/dev/null || true | |\n| } | |\n| # md-copy feature - a large IIFE appended to webview/index.js plus matching CSS | |\n| # appended to webview/index.css, each bracketed by the sentinel | |\n| # /* cc-md-copy v1 */ ... /* /cc-md-copy v1 */ (its ownership marker). apply | |\n| # appends the block at END-OF-FILE; undo removes exactly that OPEN..CLOSE block | |\n| # (marker-scoped, keeps any bytes after CLOSE), so it composes with context-icon | |\n| # (an in-place swap elsewhere in index.js) regardless of ordering. The payload below is GENERATED from | |\n| # fixes/markdown-copy-export/webview-inject.{js,css} by tools/gen-embeds; do not | |\n| # edit it by hand (CI drift check: tools/gen-embeds --check). | |\n| # >>>CCWA-MD-COPY-EMBED>>> (generated by tools/gen-embeds; do not edit) | |\n| _cc_md_copy_js() { cat <<'CCMDCOPYJS' | |\n| /* cc-md-copy: per-message and whole-conversation copy (Markdown) for the | |\n| * Claude Code VS Code webview. Self-contained IIFE appended to webview/index.js. | |\n| * Each control is a single clipboard icon that flips to a checkmark for ~2s when a | |\n| * copy actually succeeds (no text label, no menu). Additive and read-only w.r.t. | |\n| * app state; keyed on stable CSS-module class prefixes, so it fails safe (controls | |\n| * simply do not appear) if a prefix moves. | |\n| * Exposes its pure functions for node unit tests; boot()s only in a real webview. */ | |\n| /* Leading ';' so that, appended after the bundle, this IIFE can never be parsed as | |\n| * a call on the bundle's final expression if it lacks a trailing semicolon (ASI | |\n| * safety across extension builds). */ | |\n| ;(function () { | |\n| \"use strict\"; | |\n| var CONTROL_PREFIX = \"cc-md-copy\"; // every injected node's class starts with this | |\n| var USER_BUBBLE = '[class*=\"userMessageContainer_\"]'; | |\n| // Assistant message wrapper. Verified on 2.1.170: the render emits exactly one | |\n| // `data-testid=\"assistant-message\"` div per assistant turn, with the rating | |\n| // widget and content blocks as its children. (The earlier `[data-message-rating]` | |\n| // was WRONG: that attribute sits on the nested rating control, which is also only | |\n| // rendered behind an experiment+analytics gate.) Re-pinned in Task 6. | |\n| var ASSISTANT_BUBBLE = '[data-testid=\"assistant-message\"]'; | |\n| var MESSAGES_CONTAINER = '[class*=\"messagesContainer_\"]'; // e.g. '[class*=\"timeline_\"]'; \"\" -> observe document.body | |\n| // Optional narrowing only. MUST be a single wrapper around ALL content blocks, | |\n| // not a per-block class (a turn has multiple blocks). \"\" -> use the bubble itself | |\n| // (already aggregates all blocks; sanitizeClone is the correctness gate). | |\n| var ASSISTANT_CONTENT = \"\"; | |\n| var FEEDBACK_MS = 2000; // how long the checkmark shows after a successful copy | |\n| // ---- HTML -> Markdown (DOM walk) ------------------------------------------- | |\n| // Uses only: nodeType, tagName, childNodes, textContent, getAttribute, className. | |\n| function htmlToMarkdown(root) { | |\n| // Longest run of consecutive backticks in s, so a code delimiter/fence can be | |\n| // chosen longer than anything inside it (else ``` in the content closes early). | |\n| function backtickRun(s) { | |\n| var max = 0, cur = 0; | |\n| for (var i = 0; i < s.length; i++) { | |\n| if (s.charAt(i) === \"`\") { cur++; if (cur > max) max = cur; } else cur = 0; | |\n| } | |\n| return max; | |\n| } | |\n| function fence(s, min) { var n = backtickRun(s) + 1; if (n < min) n = min; return new Array(n + 1).join(\"`\"); } | |\n| function inline(node) { | |\n| var out = \"\"; | |\n| var kids = node.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) { | |\n| var c = kids[i]; | |\n| if (c.nodeType === 3) { out += c.textContent || \"\"; continue; } | |\n| if (c.nodeType !== 1) continue; | |\n| var tag = (c.tagName || \"\").toUpperCase(); | |\n| if (tag === \"BR\") out += \"\\n\"; | |\n| else if (tag === \"STRONG\" || tag === \"B\") out += \"**\" + inline(c) + \"**\"; | |\n| else if (tag === \"EM\" || tag === \"I\") out += \"*\" + inline(c) + \"*\"; | |\n| else if (tag === \"DEL\" || tag === \"S\") out += \"~~\" + inline(c) + \"~~\"; | |\n| else if (tag === \"CODE\") { | |\n| var ct = c.textContent || \"\"; | |\n| var d = fence(ct, 1); | |\n| // CommonMark strips one leading+trailing space, so pad when an edge is a | |\n| // backtick to keep it from merging with the delimiter. | |\n| var p = (ct.charAt(0) === \"`\" || ct.charAt(ct.length - 1) === \"`\") ? \" \" : \"\"; | |\n| out += d + p + ct + p + d; | |\n| } | |\n| else if (tag === \"A\") { | |\n| var href = c.getAttribute ? c.getAttribute(\"href\") : null; | |\n| var t = inline(c); | |\n| out += href ? \"[\" + t + \"](\" + href + \")\" : t; | |\n| } else out += inline(c); // unknown inline wrapper: keep text, drop tag | |\n| } | |\n| return out; | |\n| } | |\n| function langOf(codeEl) { | |\n| var cls = \"\"; | |\n| if (codeEl) cls = (codeEl.getAttribute && codeEl.getAttribute(\"class\")) || codeEl.className || \"\"; | |\n| var m = /language-([A-Za-z0-9+#.\\-]+)/.exec(cls || \"\"); | |\n| return m ? m[1] : \"\"; | |\n| } | |\n| function findChildTag(node, tag) { | |\n| var kids = node.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) { | |\n| if (kids[i].nodeType === 1 && (kids[i].tagName || \"\").toUpperCase() === tag) return kids[i]; | |\n| } | |\n| return null; | |\n| } | |\n| function list(node, ordered, depth) { | |\n| var out = \"\", n = 1; | |\n| var kids = node.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) { | |\n| var li = kids[i]; | |\n| if (li.nodeType !== 1 || (li.tagName || \"\").toUpperCase() !== \"LI\") continue; | |\n| var marker = ordered ? n++ + \". \" : \"- \"; | |\n| var indent = new Array(depth + 1).join(\" \"); | |\n| var lead = \"\", nested = \"\"; | |\n| var lk = li.childNodes || []; | |\n| for (var j = 0; j < lk.length; j++) { | |\n| var ch = lk[j]; | |\n| var ct = ch.nodeType === 1 ? (ch.tagName || \"\").toUpperCase() : \"\"; | |\n| if (ct === \"UL\") nested += list(ch, false, depth + 1); | |\n| else if (ct === \"OL\") nested += list(ch, true, depth + 1); | |\n| else if (ch.nodeType === 3) lead += ch.textContent || \"\"; | |\n| else lead += inline(ch); | |\n| } | |\n| out += indent + marker + lead.trim() + \"\\n\" + nested; | |\n| } | |\n| return out; | |\n| } | |\n| function table(node) { | |\n| var rows = []; | |\n| (function collect(container) { | |\n| var kids = container.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) { | |\n| var c = kids[i]; | |\n| if (c.nodeType !== 1) continue; | |\n| var t = (c.tagName || \"\").toUpperCase(); | |\n| if (t === \"THEAD\" || t === \"TBODY\" || t === \"TFOOT\") collect(c); | |\n| else if (t === \"TR\") { | |\n| var cells = [], cc = c.childNodes || []; | |\n| for (var j = 0; j < cc.length; j++) { | |\n| var d = cc[j]; | |\n| if (d.nodeType !== 1) continue; | |\n| var dt = (d.tagName || \"\").toUpperCase(); | |\n| if (dt === \"TH\" || dt === \"TD\") cells.push(inline(d).trim()); | |\n| } | |\n| rows.push(cells); | |\n| } | |\n| } | |\n| })(node); | |\n| if (!rows.length) return \"\"; | |\n| var head = rows[0], body = rows.slice(1); | |\n| var sep = head.map(function () { return \"---\"; }); | |\n| var out = \"| \" + head.join(\" | \") + \" |\\n| \" + sep.join(\" | \") + \" |\\n\"; | |\n| for (var k = 0; k < body.length; k++) out += \"| \" + body[k].join(\" | \") + \" |\\n\"; | |\n| return out; | |\n| } | |\n| function block(node) { | |\n| var out = \"\"; | |\n| var kids = node.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) { | |\n| var c = kids[i]; | |\n| if (c.nodeType === 3) { if ((c.textContent || \"\").trim()) out += c.textContent; continue; } | |\n| if (c.nodeType !== 1) continue; | |\n| var tag = (c.tagName || \"\").toUpperCase(); | |\n| if (/^H[1-6]$/.test(tag)) out += new Array(+tag[1] + 1).join(\"#\") + \" \" + inline(c).trim() + \"\\n\\n\"; | |\n| else if (tag === \"P\") out += inline(c).trim() + \"\\n\\n\"; | |\n| else if (tag === \"UL\") out += list(c, false, 0) + \"\\n\"; | |\n| else if (tag === \"OL\") out += list(c, true, 0) + \"\\n\"; | |\n| else if (tag === \"PRE\") { | |\n| var code = findChildTag(c, \"CODE\"); | |\n| var lang = langOf(code || c); | |\n| var body = (code || c).textContent || \"\"; | |\n| var f = fence(body, 3); | |\n| out += f + lang + \"\\n\" + body.replace(/\\n$/, \"\") + \"\\n\" + f + \"\\n\\n\"; | |\n| } else if (tag === \"BLOCKQUOTE\") { | |\n| var inner = block(c).trim().split(\"\\n\").map(function (l) { return \"> \" + l; }).join(\"\\n\"); | |\n| out += inner + \"\\n\\n\"; | |\n| } else if (tag === \"DETAILS\") out += block(c).trim() + \"\\n\\n\"; | |\n| else if (tag === \"SUMMARY\") out += inline(c).trim() + \"\\n\\n\"; | |\n| else if (tag === \"HR\") out += \"---\\n\\n\"; | |\n| else if (tag === \"TABLE\") out += table(c) + \"\\n\"; | |\n| else if (tag === \"BR\") out += \"\\n\"; | |\n| else if (tag === \"STRONG\" || tag === \"B\" || tag === \"EM\" || tag === \"I\" || | |\n| tag === \"A\" || tag === \"CODE\" || tag === \"DEL\" || tag === \"S\") | |\n| out += inline(c) + \"\\n\\n\"; | |\n| else out += block(c); // unknown wrapper: recurse (drop tag, keep content) | |\n| } | |\n| return out; | |\n| } | |\n| // block() dispatches on each CHILD's tag, treating the passed node as a plain | |\n| // container. Wrap root in a one-off container so root's OWN tag is dispatched | |\n| // too: callers pass either the bubble container (its block children render) or | |\n| // a single block element like <pre>/<ul>/<table> (now handled, not flattened). | |\n| return block({ childNodes: [root] }).replace(/\\n{3,}/g, \"\\n\\n\").trim(); | |\n| } | |\n| // ---- pure helpers ---------------------------------------------------------- | |\n| function hasPrefix(node, prefix) { | |\n| if (node.nodeType !== 1 || typeof node.className !== \"string\") return false; | |\n| var parts = node.className.split(/\\s+/); | |\n| for (var i = 0; i < parts.length; i++) if (parts[i].indexOf(prefix) === 0) return true; | |\n| return false; | |\n| } | |\n| // Class-prefix hooks for non-content chrome that renders *inside* an assistant | |\n| // bubble (verified on 2.1.170; Task 6 re-pins these). Tool blocks are excluded | |\n| // from message copy; thinking summaries are visible content and must remain | |\n| // copyable. unknownContent_ is the renderer's fallback for unrecognized block | |\n| // types, so stripping it makes a *future* block type fail safe to excluded rather | |\n| // than leaking \"Unsupported content\" into the copy. Re-pin if a prefix moves. | |\n| var CHROME_PREFIXES = [\"toolUse_\", \"toolResult_\", \"toolReference_\", \"unknownContent_\"]; | |\n| // True for any node that must never appear in copied output: our own controls, | |\n| // the rating widget (`data-message-rating` + its \"Thanks for your feedback\" | |\n| // text), any button (copy-code chrome), and the excluded content blocks above. | |\n| function isChrome(node) { | |\n| if (node.nodeType !== 1) return false; | |\n| if ((node.tagName || \"\").toUpperCase() === \"BUTTON\") return true; | |\n| if (node.getAttribute && node.getAttribute(\"data-message-rating\") !== null) return true; | |\n| if (hasPrefix(node, CONTROL_PREFIX)) return true; | |\n| for (var i = 0; i < CHROME_PREFIXES.length; i++) if (hasPrefix(node, CHROME_PREFIXES[i])) return true; | |\n| return false; | |\n| } | |\n| // Deep-clone `contentNode`, then strip every chrome node so copied output is the | |\n| // message's text content only. This is a CORRECTNESS GATE, not cosmetic: the | |\n| // default content node is the whole bubble (all content-block siblings, so multi- | |\n| // block assistant turns are captured), and this strip-list is the only thing | |\n| // keeping the rating widget and excluded tool/fallback blocks out of the copy. | |\n| function sanitizeClone(contentNode) { | |\n| var clone = contentNode.cloneNode(true); | |\n| (function strip(node) { | |\n| var kids = Array.prototype.slice.call(node.childNodes || []); | |\n| for (var i = 0; i < kids.length; i++) { | |\n| var c = kids[i]; | |\n| if (c.nodeType === 1 && isChrome(c)) { node.removeChild(c); continue; } | |\n| if (c.nodeType === 1) strip(c); | |\n| } | |\n| })(clone); | |\n| return clone; | |\n| } | |\n| function hasCopyableContent(contentNode, role) { | |\n| function walk(node) { | |\n| if (!node) return false; | |\n| if (node.nodeType === 3) return !!(node.textContent || \"\").trim(); | |\n| if (node.nodeType !== 1) return false; | |\n| if (isChrome(node)) return false; | |\n| var kids = node.childNodes || []; | |\n| for (var i = 0; i < kids.length; i++) if (walk(kids[i])) return true; | |\n| return false; | |\n| } | |\n| return walk(contentNode); | |\n| } | |\n| function classifyBubble(node) { | |\n| if (node.nodeType !== 1) return null; | |\n| if (hasPrefix(node, \"userMessageContainer_\")) return \"user\"; | |\n| if (node.getAttribute && node.getAttribute(\"data-testid\") === \"assistant-message\") return \"assistant\"; | |\n| return null; | |\n| } | |\n| // Build the whole-conversation markdown from an ordered list of bubbles. | |\n| // `contentOf(bubble)` resolves the content node (default: the bubble itself, so | |\n| // every content block is included; sanitizeClone drops chrome); a default is | |\n| // provided for tests. | |\n| function conversationToMarkdown(bubbles, contentOf) { | |\n| contentOf = contentOf || function (b) { return b; }; | |\n| var parts = []; | |\n| for (var i = 0; i < bubbles.length; i++) { | |\n| var role = classifyBubble(bubbles[i]); | |\n| if (!role) continue; | |\n| var clean = sanitizeClone(contentOf(bubbles[i])); | |\n| var body = role === \"assistant\" ? htmlToMarkdown(clean) : (clean.textContent || \"\").trim(); | |\n| if (!body) continue; | |\n| parts.push((role === \"user\" ? \"## User\" : \"## Assistant\") + \"\\n\\n\" + body); | |\n| } | |\n| return parts.join(\"\\n\\n\") + (parts.length ? \"\\n\" : \"\"); | |\n| } | |\n| // ---- exports (node tests) / boot (real webview) ---------------------------- | |\n| if (typeof document !== \"undefined\") { | |\n| boot(); | |\n| } else if (typeof module !== \"undefined\" && module.exports) { | |\n| module.exports = { htmlToMarkdown: htmlToMarkdown, sanitizeClone: sanitizeClone, | |\n| classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown, | |\n| hasCopyableContent: hasCopyableContent, copyText: copyText }; | |\n| } | |\n| // ---- live-webview wiring (runs only when a document exists) ---------------- | |\n| function qs(node, sel) { try { return sel && node.querySelector ? node.querySelector(sel) : null; } catch (_) { return null; } } | |\n| function qsa(sel) { try { return Array.prototype.slice.call(document.querySelectorAll(sel)); } catch (_) { return []; } } | |\n| // The content node to convert/copy: the optional ASSISTANT_CONTENT wrapper if | |\n| // pinned and present, else the bubble itself. The bubble already contains every | |\n| // content-block sibling of a multi-block turn, and sanitizeClone strips the | |\n| // chrome (rating widget, tool/unknown blocks, buttons, our controls) | |\n| // either way -- so this is a narrowing, never the thing that guarantees | |\n| // correctness. | |\n| function contentNodeOf(bubble, role) { | |\n| if (role === \"assistant\" && ASSISTANT_CONTENT) { | |\n| var n = qs(bubble, ASSISTANT_CONTENT); | |\n| if (n) return n; | |\n| } | |\n| return bubble; | |\n| } | |\n| // Copy `s` via a synchronous execCommand(\"copy\") on an off-screen textarea, and | |\n| // report whether it actually happened. Done first (and synchronously) because it | |\n| // runs inside the click gesture and works whether or not the page is a secure | |\n| // context -- so it covers remote / code-server, where the async Clipboard API is | |\n| // simply absent. Restores the prior selection/focus so it is invisible. | |\n| function execCopy(s) { | |\n| try { | |\n| if (typeof document === \"undefined\" || !document.createElement) return false; | |\n| var prev = document.activeElement || null; | |\n| var sel = document.getSelection ? document.getSelection() : null; | |\n| var saved = (sel && sel.rangeCount) ? sel.getRangeAt(0) : null; | |\n| var ta = document.createElement(\"textarea\"); | |\n| ta.value = s; | |\n| ta.setAttribute(\"readonly\", \"\"); | |\n| ta.style.position = \"fixed\"; | |\n| ta.style.top = \"-1000px\"; | |\n| ta.style.left = \"0\"; | |\n| ta.style.opacity = \"0\"; | |\n| (document.body || document.documentElement).appendChild(ta); | |\n| ta.focus(); | |\n| ta.select(); | |\n| var ok = false; | |\n| try { ok = document.execCommand(\"copy\"); } catch (_) { ok = false; } | |\n| if (ta.parentNode) ta.parentNode.removeChild(ta); | |\n| if (saved && sel) { try { sel.removeAllRanges(); sel.addRange(saved); } catch (_) {} } | |\n| if (prev && prev.focus) { try { prev.focus(); } catch (_) {} } | |\n| return !!ok; | |\n| } catch (_) { return false; } | |\n| } | |\n| // Copy `text` and resolve to whether the copy ACTUALLY happened, so callers only | |\n| // show success on a real copy -- never a false \"copied\" (the original bug: | |\n| // navigator.clipboard was undefined in the webview, the code fell through to | |\n| // Promise.resolve(), and the UI claimed success while nothing was written). Empty | |\n| // text is a non-copy -> false. execCommand first (gesture-safe, secure-context- | |\n| // independent); the async Clipboard API is the fallback. Never throws. | |\n| function copyText(text) { | |\n| var s = (text == null) ? \"\" : String(text); | |\n| if (!s) return Promise.resolve(false); | |\n| if (execCopy(s)) return Promise.resolve(true); | |\n| try { | |\n| if (typeof navigator !== \"undefined\" && navigator.clipboard && navigator.clipboard.writeText) { | |\n| return navigator.clipboard.writeText(s).then( | |\n| function () { return true; }, | |\n| function () { return false; } | |\n| ); | |\n| } | |\n| } catch (_) {} | |\n| return Promise.resolve(false); | |\n| } | |\n| function bubbleMarkdown(bubble, role) { | |\n| var clean = sanitizeClone(contentNodeOf(bubble, role)); | |\n| return role === \"assistant\" ? htmlToMarkdown(clean) : (clean.textContent || \"\").trim(); | |\n| } | |\n| // Inline SVG icons (currentColor, ~14px). Set via innerHTML on our own buttons | |\n| // only; the markup never reaches copied content (sanitizeClone drops our nodes). | |\n| 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>'; | |\n| 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>'; | |\n| // Flip the button to a checkmark for FEEDBACK_MS, then restore. Idempotent across | |\n| // rapid clicks (any pending restore is cleared first). | |\n| function showCopied(btn) { | |\n| try { | |\n| if (btn.__ccTimer) clearTimeout(btn.__ccTimer); | |\n| btn.classList.add(CONTROL_PREFIX + \"-ok\"); | |\n| btn.innerHTML = ICON_CHECK; | |\n| btn.__ccTimer = setTimeout(function () { | |\n| try { btn.classList.remove(CONTROL_PREFIX + \"-ok\"); btn.innerHTML = ICON_COPY; } catch (_) {} | |\n| btn.__ccTimer = null; | |\n| }, FEEDBACK_MS); | |\n| } catch (_) {} | |\n| } | |\n| // Build a single control: one clipboard-icon button. `onCopy()` is invoked | |\n| // synchronously on click (so the copy stays inside the user gesture) and must | |\n| // return a Promise<boolean>; the checkmark shows only when it resolves true. All | |\n| // nodes carry the CONTROL_PREFIX class so sanitizeClone strips them from copies. | |\n| function buildControl(onCopy, title) { | |\n| var wrap = document.createElement(\"span\"); | |\n| wrap.className = CONTROL_PREFIX; | |\n| var btn = document.createElement(\"button\"); | |\n| btn.type = \"button\"; | |\n| btn.className = CONTROL_PREFIX + \"-btn\"; | |\n| btn.title = title || \"Copy as Markdown\"; | |\n| btn.setAttribute(\"aria-label\", btn.title); | |\n| btn.innerHTML = ICON_COPY; | |\n| var busy = false; | |\n| btn.addEventListener(\"click\", function (e) { | |\n| e.stopPropagation(); | |\n| if (busy) return; | |\n| busy = true; | |\n| var p; | |\n| try { p = onCopy(); } catch (_) { p = false; } | |\n| Promise.resolve(p).then( | |\n| function (ok) { busy = false; if (ok) showCopied(btn); }, | |\n| function () { busy = false; } | |\n| ); | |\n| }); | |\n| wrap.appendChild(btn); | |\n| return wrap; | |\n| } | |\n| function decorate(bubble) { | |\n| try { | |\n| var role = classifyBubble(bubble); | |\n| if (!role) return; | |\n| // Idempotent: keep exactly one control. A React re-render of the bubble can | |\n| // leave a stale control behind or transiently defeat an \"already decorated\" | |\n| // guard, which is what produced duplicate rows of buttons; prune any extras | |\n| // every sweep and only add one when none remain. | |\n| var existing = bubble.querySelectorAll ? bubble.querySelectorAll(\".\" + CONTROL_PREFIX) : null; | |\n| if (!hasCopyableContent(contentNodeOf(bubble, role), role)) { | |\n| if (existing && existing.length) { | |\n| for (var j = existing.length - 1; j >= 0; j--) { | |\n| if (existing[j] && existing[j].parentNode) existing[j].parentNode.removeChild(existing[j]); | |\n| } | |\n| } | |\n| return; | |\n| } | |\n| if (existing && existing.length) { | |\n| for (var i = existing.length - 1; i >= 1; i--) { | |\n| if (existing[i] && existing[i].parentNode) existing[i].parentNode.removeChild(existing[i]); | |\n| } | |\n| return; | |\n| } | |\n| var control = buildControl(function () { | |\n| return copyText(bubbleMarkdown(bubble, role)); | |\n| }, \"Copy as Markdown\"); | |\n| bubble.appendChild(control); | |\n| } catch (_) {} | |\n| } | |\n| function copyConversation() { | |\n| var bubbles = qsa(USER_BUBBLE + \",\" + ASSISTANT_BUBBLE); | |\n| return copyText(conversationToMarkdown(bubbles, function (b) { | |\n| return contentNodeOf(b, classifyBubble(b)); | |\n| })); | |\n| } | |\n| // A single floating \"Copy conversation\" icon, present only while a conversation | |\n| // is open (so it never clutters the history-list view). Pinned top-right by CSS, | |\n| // clear of the chat input at the bottom; the most-recent-prompt sticky header | |\n| // sits to its left. | |\n| function installConversationControl() { | |\n| try { | |\n| var existing = qs(document, \".\" + CONTROL_PREFIX + \"-conversation\"); | |\n| var hasMessages = qsa(USER_BUBBLE + \",\" + ASSISTANT_BUBBLE).length > 0; | |\n| if (!hasMessages) { | |\n| if (existing && existing.parentNode) existing.parentNode.removeChild(existing); | |\n| return; | |\n| } | |\n| if (existing) return; | |\n| var bar = document.createElement(\"div\"); | |\n| bar.className = CONTROL_PREFIX + \"-conversation\"; | |\n| bar.appendChild(buildControl(copyConversation, \"Copy conversation\")); | |\n| document.body.appendChild(bar); | |\n| } catch (_) {} | |\n| } | |\n| function sweep() { | |\n| var b = qsa(USER_BUBBLE + \",\" + ASSISTANT_BUBBLE); | |\n| for (var i = 0; i < b.length; i++) decorate(b[i]); | |\n| installConversationControl(); | |\n| } | |\n| function boot() { | |\n| try { | |\n| var target = (MESSAGES_CONTAINER && qs(document, MESSAGES_CONTAINER)) || document.body; | |\n| sweep(); | |\n| if (typeof MutationObserver === \"undefined\") return; | |\n| var obs = new MutationObserver(function () { sweep(); }); | |\n| obs.observe(target, { childList: true, subtree: true }); | |\n| } catch (_) {} | |\n| } | |\n| })(); | |\n| CCMDCOPYJS | |\n| } | |\n| _cc_md_copy_css() { cat <<'CCMDCOPYCSS' | |\n| .cc-md-copy { | |\n| display: inline-flex; | |\n| align-items: center; | |\n| vertical-align: middle; | |\n| margin-left: 6px; | |\n| } | |\n| .cc-md-copy-btn { | |\n| display: inline-flex; | |\n| align-items: center; | |\n| justify-content: center; | |\n| padding: 2px; | |\n| color: var(--vscode-foreground); | |\n| background: transparent; | |\n| border: none; | |\n| border-radius: 4px; | |\n| cursor: pointer; | |\n| opacity: 0.6; | |\n| } | |\n| .cc-md-copy-btn svg { | |\n| display: block; | |\n| width: 14px; | |\n| height: 14px; | |\n| } | |\n| .cc-md-copy-btn:hover { | |\n| opacity: 1; | |\n| background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15)); | |\n| } | |\n| /* Success state: the icon is a green checkmark for a moment after a real copy. */ | |\n| .cc-md-copy-btn.cc-md-copy-ok, | |\n| .cc-md-copy-btn.cc-md-copy-ok:hover { | |\n| opacity: 1; | |\n| color: var(--vscode-charts-green, var(--vscode-testing-iconPassed, #89d185)); | |\n| background: transparent; | |\n| } | |\n| /* Whole-conversation copy: a single floating icon pinned to the top-right corner, | |\n| clear of the chat input at the bottom. Shown only while a conversation is open | |\n| (the IIFE adds/removes it). Nudge top/right here if it crowds the sticky header. */ | |\n| .cc-md-copy-conversation { | |\n| position: fixed; | |\n| top: 26px; | |\n| right: 4px; | |\n| z-index: 30; | |\n| display: inline-flex; | |\n| padding: 2px; | |\n| background: var(--vscode-editorWidget-background); | |\n| border: 1px solid var(--vscode-widget-border, transparent); | |\n| border-radius: 6px; | |\n| opacity: 0.85; | |\n| } | |\n| .cc-md-copy-conversation .cc-md-copy { | |\n| margin-left: 0; | |\n| } | |\n| .cc-md-copy-conversation:hover { | |\n| opacity: 1; | |\n| } | |\n| CCMDCOPYCSS | |\n| } | |\n| # <<<CCWA-MD-COPY-EMBED<<< | |\n| _cc_md_copy_has() { grep -qF '/* cc-md-copy v1 */' \"$1\" 2>/dev/null; } | |\n| # Append our sentinel block (byte-identical to the node/python deliveries): | |\n| # \"\\n\" + OPEN + \"\\n\" + PAYLOAD + \"\\n\" + CLOSE + \"\\n\" | |\n| _cc_apply_md_copy() { # $1=file $2=js|css | |\n| local f=\"$1\" kind=\"$2\" tmp | |\n| _cc_md_copy_has \"$f\" && return 0 # already applied | |\n| tmp=\"${f}.ccmdapply.$$\" | |\n| if { cat \"$f\" \\ | |\n| && printf '\\n/* cc-md-copy v1 */\\n' \\ | |\n| && { if [ \"$kind\" = css ]; then _cc_md_copy_css; else _cc_md_copy_js; fi; } \\ | |\n| && printf '/* /cc-md-copy v1 */\\n'; } > \"$tmp\" 2>/dev/null \\ | |\n| && [ -s \"$tmp\" ] && _cc_md_copy_has \"$tmp\"; then | |\n| cat \"$tmp\" > \"$f\" 2>/dev/null || true | |\n| fi | |\n| rm -f \"$tmp\" 2>/dev/null || true | |\n| } | |\n| # Reverse transform: marker-scoped block removal (same algorithm as the node/python | |\n| # deliveries). Removes exactly our OPEN..CLOSE block plus the separator newline | |\n| # apply added, and KEEPS any bytes after CLOSE (prefix + suffix splice, not a | |\n| # truncate-to-EOF) - so undo is independent of file ordering and composes with a | |\n| # future end-of-file append feature. | |\n| _cc_undo_md_copy() { | |\n| local f=\"$1\" ooff coff cend size tmp | |\n| _cc_md_copy_has \"$f\" || return 0 # nothing of ours | |\n| ooff=\"$(grep -boF '/* cc-md-copy v1 */' \"$f\" 2>/dev/null | head -1 | cut -d: -f1)\" | |\n| coff=\"$(grep -boF '/* /cc-md-copy v1 */' \"$f\" 2>/dev/null | head -1 | cut -d: -f1)\" | |\n| [ -n \"$ooff\" ] && [ -n \"$coff\" ] && [ \"$coff\" -ge \"$ooff\" ] || return 0 # malformed -> leave intact | |\n| [ \"$ooff\" -gt 0 ] && ooff=$((ooff - 1)) # also remove the separator newline before OPEN | |\n| cend=$((coff + 20)) # 20 = byte length of CLOSE marker '/* /cc-md-copy v1 */' | |\n| size=\"$(wc -c < \"$f\" 2>/dev/null | tr -d ' ')\" | |\n| # drop the one trailing newline apply added, iff the byte after CLOSE is \"\\n\" | |\n| if [ -n \"$size\" ] && [ \"$cend\" -lt \"$size\" ] \\ | |\n| && [ \"$(tail -c \"+$((cend + 1))\" \"$f\" 2>/dev/null | head -c 1 | od -An -tu1 | tr -d ' ')\" = \"10\" ]; then | |\n| cend=$((cend + 1)) | |\n| fi | |\n| tmp=\"${f}.ccmdundo.$$\" | |\n| if { head -c \"$ooff\" \"$f\" 2>/dev/null | |\n| [ -n \"$size\" ] && [ \"$cend\" -lt \"$size\" ] && tail -c \"+$((cend + 1))\" \"$f\" 2>/dev/null | |\n| true; } > \"$tmp\" 2>/dev/null; then | |\n| cat \"$tmp\" > \"$f\" 2>/dev/null || true | |\n| fi | |\n| rm -f \"$tmp\" 2>/dev/null || true | |\n| } | |\n| # Shared tail for both file reconcilers: write `patched` if it differs from the | |\n| # live file, taking the one-time pristine snapshot (= `base`) on first change. | |\n| _cc_commit_reconciled() { # $1=f $2=base $3=patched | |\n| local f=\"$1\" base=\"$2\" patched=\"$3\" tmpmeta | |\n| if cmp -s \"$patched\" \"$f\"; then rm -f \"$base\" \"$patched\" 2>/dev/null || true; return 0; fi | |\n| if [ ! -e \"${f}.bak-cc-workarounds\" ]; then | |\n| if cp -p \"$f\" \"${f}.bak-cc-workarounds\" 2>/dev/null; then | |\n| cat \"$base\" > \"${f}.bak-cc-workarounds\" 2>/dev/null || true | |\n| fi | |\n| fi | |\n| tmpmeta=\"${f}.ccwrite.$$\" | |\n| if cp -p \"$f\" \"$tmpmeta\" 2>/dev/null && cat \"$patched\" > \"$tmpmeta\" 2>/dev/null; then | |\n| mv -f \"$tmpmeta\" \"$f\" 2>/dev/null || rm -f \"$tmpmeta\" 2>/dev/null || true | |\n| else | |\n| rm -f \"$tmpmeta\" 2>/dev/null || true | |\n| fi | |\n| rm -f \"$base\" \"$patched\" 2>/dev/null || true | |\n| } | |\n| # Reconcile webview/index.js. Registry (forward apply order): context-icon | |\n| # (in-place), then md-copy (append, registered LAST). Undo runs in REVERSE. | |\n| _cc_reconcile_index_js() { | |\n| local f=\"$1\" base patched | |\n| [ -f \"$f\" ] && [ -r \"$f\" ] || return 0 | |\n| base=\"${f}.ccbase.$$\" | |\n| patched=\"${f}.ccnew.$$\" | |\n| cp \"$f\" \"$base\" 2>/dev/null || { rm -f \"$base\" 2>/dev/null || true; return 0; } | |\n| # Clean base C = current with every KNOWN feature undone, REVERSE order. | |\n| _cc_undo_md_copy \"$base\" | |\n| _cc_undo_context_icon \"$base\" | |\n| # Desired D = C with every ENABLED feature applied, FORWARD order. | |\n| cp \"$base\" \"$patched\" 2>/dev/null || { rm -f \"$base\" \"$patched\" 2>/dev/null || true; return 0; } | |\n| if _cc_feature_enabled context-icon; then _cc_apply_context_icon \"$patched\"; fi | |\n| if _cc_feature_enabled md-copy; then _cc_apply_md_copy \"$patched\" js; fi | |\n| _cc_commit_reconciled \"$f\" \"$base\" \"$patched\" | |\n| } | |\n| # Reconcile webview/index.css. Registry: md-copy (append) only. | |\n| _cc_reconcile_index_css() { | |\n| local f=\"$1\" base patched | |\n| [ -f \"$f\" ] && [ -r \"$f\" ] || return 0 | |\n| base=\"${f}.ccbase.$$\" | |\n| patched=\"${f}.ccnew.$$\" | |\n| cp \"$f\" \"$base\" 2>/dev/null || { rm -f \"$base\" 2>/dev/null || true; return 0; } | |\n| _cc_undo_md_copy \"$base\" | |\n| cp \"$base\" \"$patched\" 2>/dev/null || { rm -f \"$base\" \"$patched\" 2>/dev/null || true; return 0; } | |\n| if _cc_feature_enabled md-copy; then _cc_apply_md_copy \"$patched\" css; fi | |\n| _cc_commit_reconciled \"$f\" \"$base\" \"$patched\" | |\n| } | |\n| _cc_reconcile() { | |\n| [ \"$CC_RECONCILE\" != \"0\" ] || return 0 # emergency bypass: touch nothing | |\n| local d extdir f | |\n| # Most precise target: walk up from REAL_CLAUDE to the extension root. | |\n| d=\"$(dirname \"$REAL_CLAUDE\" 2>/dev/null || echo \"\")\" | |\n| extdir=\"\" | |\n| while [ -n \"$d\" ] && [ \"$d\" != \"/\" ] && [ \"$d\" != \".\" ]; do | |\n| case \"${d##*/}\" in anthropic.claude-code-*) extdir=\"$d\"; break ;; esac | |\n| d=\"$(dirname \"$d\" 2>/dev/null || echo \"\")\" | |\n| done | |\n| if [ -n \"$extdir\" ]; then | |\n| _cc_reconcile_index_js \"$extdir/webview/index.js\" | |\n| _cc_reconcile_index_css \"$extdir/webview/index.css\" | |\n| fi | |\n| # Also cover any installed extension under this user's VS Code dirs (terminal | |\n| # launches, or when the real binary is the standalone CLI). Unmatched globs | |\n| # fall through harmlessly - _cc_reconcile_index_js skips non-files. | |\n| for f in \\ | |\n| \"$HOME\"/.vscode/extensions/anthropic.claude-code-*/webview/index.js \\ | |\n| \"$HOME\"/.vscode-insiders/extensions/anthropic.claude-code-*/webview/index.js \\ | |\n| \"$HOME\"/.vscode-server/extensions/anthropic.claude-code-*/webview/index.js \\ | |\n| \"$HOME\"/.vscode-server-insiders/extensions/anthropic.claude-code-*/webview/index.js; do | |\n| _cc_reconcile_index_js \"$f\" | |\n| _cc_reconcile_index_css \"${f%/index.js}/index.css\" | |\n| done | |\n| } | |\n| # Best-effort: invoking under `|| true` suspends `set -e` for the whole pass, so | |\n| # nothing here can block the launch (matches the previous safety model). | |\n| _cc_reconcile || true | |\n| # The `${args[@]+...}` form guards the empty-array case under `set -u`, | |\n| # including older Bash versions such as the default Bash on older macOS systems. | |\n| exec \"$REAL_CLAUDE\" ${args[@]+\"${args[@]}\"} |", "url": "https://wpnews.pro/news/launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features", "canonical_source": "https://gist.github.com/phase3dev/1f7ee1cb17151c19d538e053f4f548ca", "published_at": "2026-06-24 03:19:22+00:00", "updated_at": "2026-06-26 05:03:19.146323+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Claude Code", "VS Code", "Opus 4.7", "Opus 4.8", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features", "markdown": "https://wpnews.pro/news/launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features.md", "text": "https://wpnews.pro/news/launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features.txt", "jsonld": "https://wpnews.pro/news/launcher-to-run-glm-5-2-in-claude-code-harness-full-launcher-with-all-features.jsonld"}}