|
| 1 | +#!/usr/bin/env bash |
| 2 | +# claude-context - Claude Code launcher that restores the always-visible |
| 3 | +# context-usage icon in the VS Code chat input. |
| 4 | +# |
| 5 | +# Recent extension builds (2.1.165+) hide that icon until you have used >50% of |
| 6 | +# the context window; with the 1M context window that is ~500k tokens, so it is |
| 7 | +# effectively never shown. There is no env/CLI lever for this, so this wrapper |
| 8 | +# idempotently patches the extension's webview bundle on each launch, flipping |
| 9 | +# the threshold so the icon shows at any usage level. Because it re-applies every |
| 10 | +# launch, it survives extension updates. |
| 11 | +# |
| 12 | +# Context-icon-only variant. For the thinking-summaries fix too, use `claudemax` |
| 13 | +# (both fixes combined); for thinking alone, use `claude-think`. All three are |
| 14 | +# drop-in process wrappers and differ only in what they inject/patch. |
| 15 | +# |
| 16 | +# NOTE: this DOES edit the extension's bundled webview/index.js. That edit is |
| 17 | +# idempotent, backed up once to index.js.bak-context-icon, written atomically (a |
| 18 | +# failed write leaves the original untouched), best-effort (it never blocks the |
| 19 | +# launch), and toggle-able with CC_PATCH_CONTEXT_ICON=0. |
| 20 | +# |
| 21 | +# Use it: |
| 22 | +# - VS Code (official "Claude Code" extension): set "claudeCode.claudeProcessWrapper" |
| 23 | +# to the FULL path of this file, then reload the window. In a multi-root |
| 24 | +# .code-workspace this setting is window-scoped, so put it in the workspace |
| 25 | +# file's "settings" block (or User settings), not a folder .vscode/settings.json. |
| 26 | +# - VS Code (third-party "Claude Code Chat"): set "claudeCodeChat.executable.path". |
| 27 | +# - Terminal: run `claude-context` in place of `claude`. |
| 28 | +# |
| 29 | +# First-run note: the wrapper patches index.js on disk when the CLI is spawned, |
| 30 | +# which can be AFTER the webview already loaded the old bundle. So the first time |
| 31 | +# you enable this you may need two reloads: reload (the spawn patches the file), |
| 32 | +# then reload again (the webview loads the patched bundle). Subsequent windows |
| 33 | +# and post-update launches are already patched on disk. |
| 34 | +# |
| 35 | +# Toggle off: |
| 36 | +# export CC_PATCH_CONTEXT_ICON=0 # leave the extension webview untouched |
| 37 | +# |
| 38 | +# Default: |
| 39 | +# CC_PATCH_CONTEXT_ICON=1 |
| 40 | +# |
| 41 | +# The real `claude` must be installed. This wrapper finds it automatically; if it |
| 42 | +# cannot, set CLAUDE_REAL_BIN to the full path of your real claude binary. |
| 43 | + |
| 44 | +set -euo pipefail |
| 45 | + |
| 46 | +# --- Locate the real claude binary ----------------------------------------- |
| 47 | + |
| 48 | +self="$(readlink -f "$0" 2>/dev/null || echo "$0")" |
| 49 | + |
| 50 | +# Process-wrapper convention: the official VS Code extension invokes the wrapper |
| 51 | +# as <wrapper> <REAL_CLAUDE...> <args...>, passing the real CLI ahead of the |
| 52 | +# args. <REAL_CLAUDE...> is either a single native-binary path (".../claude") or |
| 53 | +# a node interpreter followed by the bundled cli.js (".../node .../cli.js"). |
| 54 | +# Peel that off so it is not forwarded as a stray positional argument, and |
| 55 | +# prefer it as the real claude. (Plain "claude-context <args>" use is unaffected: |
| 56 | +# <args> never begins with an existing claude/node binary path.) |
| 57 | +wrapper_bin="" |
| 58 | +if [ "$#" -gt 0 ] \ |
| 59 | + && printf '%s' "$1" | grep -Eqi '/claude(\.exe|\.cmd|\.bat)?$' \ |
| 60 | + && [ -e "$1" ]; then |
| 61 | + wrapper_bin="$1" |
| 62 | + shift |
| 63 | +elif [ "$#" -ge 2 ] \ |
| 64 | + && printf '%s' "$1" | grep -Eqi '/node(\.exe)?$' && [ -e "$1" ] \ |
| 65 | + && printf '%s' "$2" | grep -Eqi '\.(c?js|mjs)$' && [ -e "$2" ]; then |
| 66 | + # node + cli.js: exec node directly and keep cli.js as the first forwarded arg. |
| 67 | + wrapper_bin="$1" |
| 68 | + shift |
| 69 | +fi |
| 70 | + |
| 71 | +REAL_CLAUDE="${CLAUDE_REAL_BIN:-}" |
| 72 | +if [ -z "$REAL_CLAUDE" ] && [ -n "$wrapper_bin" ]; then |
| 73 | + REAL_CLAUDE="$wrapper_bin" |
| 74 | +fi |
| 75 | + |
| 76 | +if [ -z "$REAL_CLAUDE" ]; then |
| 77 | + for c in \ |
| 78 | + "$HOME/.local/bin/claude" \ |
| 79 | + /usr/local/bin/claude \ |
| 80 | + /usr/bin/claude \ |
| 81 | + /opt/homebrew/bin/claude \ |
| 82 | + "$(command -v claude 2>/dev/null || true)"; do |
| 83 | + |
| 84 | + [ -n "$c" ] && [ -x "$c" ] || continue |
| 85 | + [ "$(readlink -f "$c" 2>/dev/null || echo "$c")" = "$self" ] && continue |
| 86 | + |
| 87 | + REAL_CLAUDE="$c" |
| 88 | + break |
| 89 | + done |
| 90 | +fi |
| 91 | + |
| 92 | +[ -n "$REAL_CLAUDE" ] || { |
| 93 | + echo "claude-context: could not find the real 'claude' binary; set CLAUDE_REAL_BIN" >&2 |
| 94 | + exit 1 |
| 95 | +} |
| 96 | + |
| 97 | +# --- Restore the always-visible context-usage icon (patches the webview) ---- |
| 98 | +# |
| 99 | +# Best-effort and must never block the launch: every step is guarded, writes go |
| 100 | +# through a temp file with an atomic rename (a failed write leaves the original |
| 101 | +# untouched), and the whole routine runs under `|| true`. |
| 102 | +# |
| 103 | +# What it changes - component `FJe` in webview/index.js: |
| 104 | +# if(c>=50)return null -> if(c>=101)return null |
| 105 | +# `c` is "% of context remaining"; it maxes at 100, so >=101 never fires and the |
| 106 | +# icon renders whenever a context window is known (the t===0 "no session yet" |
| 107 | +# guard is left intact). Idempotent, and re-applied each launch, so an extension |
| 108 | +# update that reinstalls a fresh bundle is re-patched on the next launch. |
| 109 | +# |
| 110 | +# Maintenance note: the patch keys off the stable string `>=50)return null}`, not |
| 111 | +# the minified component name. If a future extension build changes that exact |
| 112 | +# substring, the routine safely no-ops (the icon goes missing again) until the |
| 113 | +# anchor here is updated. |
| 114 | + |
| 115 | +CC_PATCH_CONTEXT_ICON="${CC_PATCH_CONTEXT_ICON:-1}" |
| 116 | + |
| 117 | +_cc_patch_index_js() { |
| 118 | + local f="$1" tmp tmp2 |
| 119 | + [ -f "$f" ] && [ -w "$f" ] || return 0 |
| 120 | + if grep -q '>=101)return null}' "$f" 2>/dev/null; then return 0; fi # already patched |
| 121 | + if ! grep -q '>=50)return null}' "$f" 2>/dev/null; then return 0; fi # gate absent (version changed) |
| 122 | + if [ ! -e "$f.bak-context-icon" ]; then cp -p "$f" "$f.bak-context-icon" 2>/dev/null || true; fi |
| 123 | + tmp="${f}.ccpatch.$$" |
| 124 | + tmp2="${f}.ccpatch2.$$" |
| 125 | + # `cp -p` preserves mode/owner portably (the GNU-only `--reference` is avoided |
| 126 | + # so this also works on macOS/BSD): copy the original to a temp, sed into a |
| 127 | + # second temp, copy that back over the metadata-preserving temp, then atomically |
| 128 | + # replace the original. A failed/partial step leaves the original untouched. |
| 129 | + cp -p "$f" "$tmp" 2>/dev/null || { rm -f "$tmp" 2>/dev/null || true; return 0; } |
| 130 | + if sed 's/>=50)return null}/>=101)return null}/g' "$f" > "$tmp2" 2>/dev/null \ |
| 131 | + && [ -s "$tmp2" ] \ |
| 132 | + && grep -q '>=101)return null}' "$tmp2" 2>/dev/null; then |
| 133 | + cat "$tmp2" > "$tmp" && mv -f "$tmp" "$f" || true |
| 134 | + fi |
| 135 | + rm -f "$tmp" "$tmp2" 2>/dev/null || true |
| 136 | +} |
| 137 | + |
| 138 | +_cc_restore_context_icon() { |
| 139 | + local d extdir f |
| 140 | + # Most precise target: walk up from the real claude path the extension handed |
| 141 | + # us (its bundled resources/native-binary/claude) to the extension root. |
| 142 | + d="$(dirname "$REAL_CLAUDE" 2>/dev/null || echo "")" |
| 143 | + extdir="" |
| 144 | + while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "." ]; do |
| 145 | + case "${d##*/}" in |
| 146 | + anthropic.claude-code-*) extdir="$d"; break ;; |
| 147 | + esac |
| 148 | + d="$(dirname "$d" 2>/dev/null || echo "")" |
| 149 | + done |
| 150 | + if [ -n "$extdir" ]; then _cc_patch_index_js "$extdir/webview/index.js"; fi |
| 151 | + |
| 152 | + # Also cover any installed extension under this user's VS Code dirs (terminal |
| 153 | + # launches, or when the real binary is the standalone CLI). Unmatched globs |
| 154 | + # fall through harmlessly - _cc_patch_index_js skips non-files. |
| 155 | + for f in \ |
| 156 | + "$HOME"/.vscode-server/extensions/anthropic.claude-code-*/webview/index.js \ |
| 157 | + "$HOME"/.vscode/extensions/anthropic.claude-code-*/webview/index.js \ |
| 158 | + "$HOME"/.vscode-server-insiders/extensions/anthropic.claude-code-*/webview/index.js; do |
| 159 | + _cc_patch_index_js "$f" |
| 160 | + done |
| 161 | +} |
| 162 | + |
| 163 | +if [ "$CC_PATCH_CONTEXT_ICON" != "0" ]; then |
| 164 | + _cc_restore_context_icon || true |
| 165 | +fi |
| 166 | + |
| 167 | +# The `${@+...}` form guards the empty-args case under `set -u`, including older |
| 168 | +# Bash versions such as the default Bash on older macOS systems. |
| 169 | +exec "$REAL_CLAUDE" ${@+"$@"} |
0 commit comments