diff --git a/README.md b/README.md index 590c7fc..41318aa 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,35 @@ Not affiliated with or endorsed by Anthropic. A future Claude Code update could ## Workarounds -1. **Empty thinking summaries (Opus 4.7 / 4.8)** [updated 2026-06-08]. +1. **Empty thinking summaries (Opus 4.7 / 4.8)** [updated 2026-06-10]. Thinking summaries render empty in the VS Code extension and headless `-p`/SDK paths, even with `showThinkingSummaries` enabled. Fix via the launcher (recommended), a one-line extension patch, or a local proxy. -> [details](#workaround-1-thinking-summaries) -2. **Missing context-usage icon (1M context window)** [updated 2026-06-08]. + A populated Thinking summary in the VS Code chat instead of an empty block + +2. **Missing context-usage icon (1M context window)** [updated 2026-06-10]. The context-usage pie in the chat input is hidden until you have used more than 50% of the context window. With the 1M window that is about 500,000 tokens, so it is effectively never shown. Fix via the launcher (re-patches the webview on each launch), or a standalone patcher script. -> [details](#workaround-2-context-usage-icon) -3. **No markdown copy / export of chat** [added 2026-06-09]. + The context-usage icon and tooltip in the VS Code chat input, showing 51 percent context used and 49 percent remaining until auto-compact + +3. **No markdown copy / export of chat** [updated 2026-06-10]. The chat cannot copy a whole message or the whole conversation as Markdown, and has no transcript export. Fix via the launcher (adds copy controls, re-applied each launch), a standalone patcher, or a standalone session exporter CLI. -> [details](fixes/markdown-copy-export/README.md) + Per-message copy controls added to the VS Code chat + +## Quick start + +Two downloads, one per platform: + +* **Linux / macOS:** [`launcher/claudemax`](launcher/claudemax) - the bash launcher, no build needed. +* **Windows:** `claudemax.exe` from the [Releases](../../releases) page. + +Put it on your PATH, point the VS Code "Claude Code" extension's `claudeCode.claudeProcessWrapper` setting at its full path, and reload the window. That enables every fix above. Terminal use, per-fix toggles, and the full wiring are under [The launcher](#the-launcher) and each workaround's section. + ## The launcher The recommended fix for everything is one small launcher that wraps the real `claude` binary. It is a drop-in process wrapper carrying every fix in this repo; each fix is on by default and independently switchable with an environment variable, so the same artifact serves "I want everything" and "I want only X" without editing code and without recompiling. @@ -72,13 +87,13 @@ Extended-thinking summaries stopped appearing with Opus 4.7 and remain unavailab * * -There are three workarounds: +There are technically three workarounds; only the launcher is maintained: -* **Option 1: Launcher (recommended).** A small wrapper that launches Claude Code and adds the missing flag. It fixes the VS Code extension and headless CLI, and it survives Claude Code updates. -* **Option 2: One-line patch.** A direct edit to one line of the VS Code extension. This only fixes VS Code and must be re-applied after each extension update. -* **Option 3: Local proxy (advanced).** A localhost proxy that can fix all surfaces at once. This is powerful but untested and is documented for users who want to evaluate it. +1. **Launcher (recommended).** A small wrapper that launches Claude Code and adds the missing flag. It fixes the VS Code extension and headless CLI, and it survives Claude Code updates. +2. **One-line `extension.js` patch.** A direct edit to one line of the VS Code extension; see [TECHNICAL.md](TECHNICAL.md#option-2-extensionjs-patch). VS Code only, and must be re-applied after each extension update. Maintained through `2.1.172`; not maintained after. +3. **Local proxy (advanced).** A localhost proxy that fixes all surfaces at the wire level; see [TECHNICAL.md](TECHNICAL.md#option-3-local-proxy-design). Powerful but untested. Not maintained. -## Option 1: Launcher (recommended) +## Launcher The launcher starts the real `claude` binary and appends the missing `--thinking-display summarized` flag. It does not modify Claude Code files, so it continues working after updates. The same wrapper fixes both the VS Code extension and headless CLI. @@ -139,7 +154,7 @@ The same result is achieved with the compiled `.exe`. ### Want only the thinking fix? -Set `CC_PATCH_CONTEXT_ICON=0` to leave the webview untouched and inject only the thinking-display flag. The launcher reads `CC_THINKING_DISPLAY`: +Set `CC_PATCH_CONTEXT_ICON=0` and `CC_PATCH_MD_COPY=0` to leave the webview untouched and inject only the thinking-display flag. The launcher reads `CC_THINKING_DISPLAY`: * unset or `summarized`: show thinking summaries, which is the default * `omitted`: hide thinking summaries @@ -152,7 +167,7 @@ When Claude Code starts a real agent run it puts one of these markers on the com The launcher inspects the arguments and, when it detects a real run via any of those markers, appends `--thinking-display summarized` before handing off to the real `claude` binary. The official extension also launches the wrapper with the real CLI path as a leading argument (a "process wrapper" convention); the launcher detects and consumes that path so it is not forwarded as a stray positional. See [TECHNICAL.md](TECHNICAL.md) for more. -### Why Option 1 is recommended +### Why the launcher is recommended 1. It survives updates because it does not edit Claude Code files (for the thinking fix; the context-icon fix re-applies on each launch). 2. It fixes both the VS Code extension and headless `claude -p` or SDK runs. @@ -162,56 +177,14 @@ The launcher inspects the arguments and, when it detects a real run via any of t 6. Every fix can be toggled with one environment variable. 7. It provides one place to configure effort level, auto mode, timeouts, or model routing. See the commented customization section in the script and the [Side note](#side-note-launching-claude-code-with-third-party-models). -## Option 2: One-line `extension.js` patch (VS Code only) - -If you only use the VS Code extension and accept reapplying the change after updates, you can patch the extension directly. The fix is a single line: - -```js -// from: -if(l.type!=="disabled"&&l.display)B.push("--thinking-display",l.display) -// to: -if(l.type!=="disabled")B.push("--thinking-display",l.display||"summarized") -``` - -> The extension is minified, so the array variable name varies by build (`B` in 2.0.x, `q` in 2.1.16x, and so on). Match the surrounding text and keep whatever variable name your build uses; [`fixes/thinking-summaries/patch-extension.sh`](fixes/thinking-summaries/patch-extension.sh) does this automatically. This version fragility is one reason Option 1 is preferred. - -### Automatic patching on Linux, macOS, WSL, or Git Bash - -[`fixes/thinking-summaries/patch-extension.sh`](fixes/thinking-summaries/patch-extension.sh) finds every installed Claude Code extension, backs each one up, and applies the patch: +## Other approaches (not maintained) -```sh -./fixes/thinking-summaries/patch-extension.sh # patch and create .bak backups -./fixes/thinking-summaries/patch-extension.sh --dry-run # preview only, change nothing -./fixes/thinking-summaries/patch-extension.sh --revert # restore backups -``` - -Reload the VS Code window after patching. Re-run the patch after every extension update because updates replace the extension folder and remove the change. - -### Manual patching on any OS - -Find the extension's `extension.js` file, back it up, replace the line shown above, save the file, and reload VS Code. Common locations: - -* Linux/macOS: `~/.vscode/extensions/anthropic.claude-code-*/` -* Windows: `%USERPROFILE%\.vscode\extensions\anthropic.claude-code-*\` - -### Why Option 1 is preferred over Option 2 - -1. Option 2 must be re-applied after every extension update. -2. Option 2 fixes VS Code only. Headless `claude -p` and SDK runs still come back empty. - -## Option 3: Local proxy (advanced, untested) - -A localhost proxy can add the missing field to every request, fixing VS Code, CLI, and SDK runs without editing files or reapplying patches. This works because each surface honors `ANTHROPIC_BASE_URL`. - -This is a working starting point, not a turnkey fix. It also sits in the path of your live auth token, so review the security notes before relying on it. - -```sh -node fixes/thinking-summaries/proxy.js # listens on http://127.0.0.1:8788 -export ANTHROPIC_BASE_URL=http://127.0.0.1:8788 # set this where Claude launches -claude ... # for VS Code, set it for the extension host, then reload -``` +The one-line `extension.js` patch and the local proxy are documented in full - +with their trade-offs and the runnable scripts - in TECHNICAL.md. Neither is +maintained; the launcher is the supported fix. -Security: The proxy sees your live auth token. It binds to `127.0.0.1` only, never `0.0.0.0`, and does not log headers or bodies. Unset `ANTHROPIC_BASE_URL` to return directly to Anthropic. See [`fixes/thinking-summaries/proxy.js`](fixes/thinking-summaries/proxy.js) and [TECHNICAL.md](TECHNICAL.md#option-3-local-proxy-design) for details and caveats. +* [Option 2: extension.js patch](TECHNICAL.md#option-2-extensionjs-patch) - VS Code only, and must be re-applied after every extension update. Script: [`fixes/thinking-summaries/patch-extension.sh`](fixes/thinking-summaries/patch-extension.sh) (`--revert`, `--dry-run`). Maintained through `2.1.172`; not maintained after. +* [Option 3: local proxy](TECHNICAL.md#option-3-local-proxy-design) - surface-agnostic (VS Code + CLI + SDK) but untested, and it sits in the path of your live auth token. Script: [`fixes/thinking-summaries/proxy.js`](fixes/thinking-summaries/proxy.js). Not maintained. --- @@ -232,19 +205,19 @@ There is no environment variable or CLI flag for this threshold, so the fix is a The launcher carries this fix on by default. Install and wire it up exactly like Workaround 1 (copy [`launcher/claudemax`](launcher/claudemax) to `~/.local/bin`, point `claudeCode.claudeProcessWrapper` at it, reload). On Windows, download `claudemax.exe` from [Releases](../../releases). To get only the context-icon fix and skip thinking injection, set `CC_THINKING_DISPLAY=omitted`. -On each launch the wrapper reconciles the extension's `webview/index.js`, flipping the hidden threshold so the icon shows at any usage level. Because it re-applies every launch, an extension auto-update that reinstalls a fresh bundle is re-patched on the next launch. +On each launch the wrapper reconciles the extension's `webview/index.js`, removing the startup hide guard and flipping the hidden threshold so the icon shows at any usage level. Because it re-applies every launch, an extension auto-update that reinstalls a fresh bundle is re-patched on the next launch. > First-run note: the wrapper patches `index.js` on disk when the CLI is spawned, which can be **after** the webview already loaded the old bundle. The first time you enable it you may need **two reloads**: reload once (the spawn patches the file), then reload again (the webview loads the patched bundle). Later windows and post-update launches are already patched on disk. ### What it changes -In the indicator component, the render gate is `if (c >= 50) return null`, where `c` is the percent of context **remaining**. So the icon renders only when less than 50% remains (more than 50% used). The fix flips the threshold and tags the edit with an ownership marker: +In the indicator component, the render gates are `if (t === 0) return null` and `if (c >= 50) return null`, where `t` is the known context window and `c` is the percent of context **remaining**. So the icon renders only after the webview knows a session and less than 50% remains (more than 50% used). The fix removes the startup guard, flips the threshold, and tags the edit with an ownership marker: ```text -if(c>=50)return null -> if(c>=101)return null}/*ccwa-context-icon*/ +if(t===0)return null;if(c>=50)return null} -> if(c>=101)return null}/*ccwa-context-icon:t:c*/ ``` -`c` maxes at 100, so `c >= 101` is never true and the gate never hides the icon. The separate `if (t === 0) return null` guard (no context window known yet) is left intact, so nothing renders before a session exists. The edit is anchored on the stable string `>=50)return null}`, not on the minified component name, which changes between builds. The trailing `/*ccwa-context-icon*/` marks the edit as ours, so the launcher only ever reverses its own change. +`c` maxes at 100, so `c >= 101` is never true and the gate never hides the icon. Removing `if (t === 0) return null` keeps the icon visible after a window reload while usage data is still being repopulated; during that gap it can briefly show `0%`. The edit is anchored on the minified guard-pair shape above, not on the component name or exact minified variable names, which change between builds. The trailing `/*ccwa-context-icon::*/` marker stores the matched names so the launcher can reverse only its own change back to the same pristine variable names. ### Turn the context-icon fix on or off @@ -257,8 +230,8 @@ The launcher reads `CC_PATCH_CONTEXT_ICON`: Unlike Workaround 1, this fix edits the extension's bundled `webview/index.js`. The edit is made safe: -* **Idempotent** - it skips a file that is already in the desired state, and skips (rather than guesses) if the `>=50)return null}` anchor is absent because the extension changed. -* **Ownership-marked** - the edit carries a `/*ccwa-context-icon*/` marker; the launcher reverses only its own marked edit and never touches upstream code that merely resembles a patched value. +* **Idempotent** - it skips a file that is already in the desired state, and skips (rather than guesses) if the combined guard shape is absent because the extension changed. +* **Ownership-marked** - the edit carries a `/*ccwa-context-icon::*/` marker; the launcher reverses only its own marked edit and never touches upstream code that merely resembles a patched value. Older `/*ccwa-context-icon*/` markers from prior versions are still recognized and normalized. * **Snapshotted once** - a whole-file pristine snapshot `index.js.bak-cc-workarounds` is written the first time the file is rewritten, for emergency manual restore only; routine reconcile never reads it. * **Atomic** - the change is written to a temp file and moved into place only after it is verified, so a failed or partial write leaves the original untouched. * **Best-effort** - every step is guarded; a read-only file, a renamed bundle, or a missing tool simply no-ops and never blocks the launch. @@ -279,7 +252,7 @@ After patching, reload the webview (Command Palette -> "Developer: Reload Window ## Known limitations 1. **Coarse glyph.** The pie is a 3-state gauge, not a continuous fill: it only changes appearance at roughly 62.5% and 87% used. The precise percentage is in the hover tooltip and in `/context`, not in the glyph itself. Making the pie a fine-grained gauge would require new SVG geometry, not a one-line patch, so it is out of scope. -2. **Transient 0% right after a reload.** The icon reads from a usage store that resets to zero on a window reload and is repopulated by the next assistant turn. Immediately after reloading a continued conversation, before any new turn, the tooltip can briefly read "0% context used"; it self-corrects to the true value after the next turn. (`/context` is unaffected - it queries the CLI directly.) If you would rather hide the icon while the store is empty than show a transient 0%, change the icon's `if(t===0)return null` to `if(t<=0)return null` in `webview/index.js`; the icon then stays hidden until the first turn populates real numbers. This is a manual, optional tweak and is not applied by default. +2. **Transient 0% right after a reload.** The icon reads from a usage store that resets to zero on a window reload and is repopulated by the next assistant turn. Immediately after reloading a continued conversation, before any new turn, the tooltip can briefly read "0% context used"; it self-corrects to the true value after the next turn. (`/context` is unaffected - it queries the CLI directly.) This is intentional: showing a temporary 0% icon is preferable to hiding the icon for the whole reload gap. --- @@ -317,12 +290,12 @@ Setup is otherwise identical to Option 1. This is unrelated to the fixes above. | [`launcher/claudemax`](launcher/claudemax) | both | Unified launcher (Linux/macOS), env-toggled. | | [`launcher/claudemax.win.js`](launcher/claudemax.win.js) | both | Windows source for `claudemax.exe`. | | [`launcher/README.md`](launcher/README.md) | both | Wiring, the toggle table, the VS Code env-setting how-to, the build command. | -| [`fixes/thinking-summaries/patch-extension.sh`](fixes/thinking-summaries/patch-extension.sh) | thinking | Option 2 idempotent `extension.js` patch with `--revert` and `--dry-run`. | -| [`fixes/thinking-summaries/proxy.js`](fixes/thinking-summaries/proxy.js) | thinking | Option 3 localhost proxy. Advanced and untested. | +| [`fixes/thinking-summaries/patch-extension.sh`](fixes/thinking-summaries/patch-extension.sh) | thinking | Option 2 `extension.js` patch (`--revert`, `--dry-run`). Unmaintained; see TECHNICAL.md. | +| [`fixes/thinking-summaries/proxy.js`](fixes/thinking-summaries/proxy.js) | thinking | Option 3 localhost proxy. Advanced, untested, unmaintained. | | [`fixes/thinking-summaries/test-thinking-display.sh`](fixes/thinking-summaries/test-thinking-display.sh) | thinking | Live A/B test showing that the flag is the relevant lever. | | [`fixes/context-icon/fix-context-icon.py`](fixes/context-icon/fix-context-icon.py) | context icon | Option 2 standalone webview patcher with `--revert`. | | [`fixes/markdown-copy-export/add-md-copy.py`](fixes/markdown-copy-export/add-md-copy.py) | markdown copy | Standalone webview patcher (sentinel block, reverse-transform `--revert`). | -| [`fixes/markdown-copy-export/cc-export.py`](fixes/markdown-copy-export/cc-export.py) | markdown copy | Standalone session exporter (markdown/text, `--open`). | +| [`fixes/markdown-copy-export/cc-export.py`](fixes/markdown-copy-export/cc-export.py) | markdown copy | Standalone session exporter (markdown or plain text). | | [`fixes/markdown-copy-export/webview-inject.js`](fixes/markdown-copy-export/webview-inject.js) | markdown copy | Single source of the appended copy-controls IIFE. | | [`tools/gen-embeds`](tools/gen-embeds) | markdown copy | Generates the embedded payload into the launcher + patcher; `--check` drift gate. | | [`TECHNICAL.md`](TECHNICAL.md) | both | Full root-cause analysis, the reconcile model, and design notes. | @@ -342,9 +315,10 @@ pkg launcher/claudemax.win.js --targets node18-win-x64 --output claudemax.exe ## Compatibility -Confirmed on Opus 4.7 and Opus 4.8 with VS Code extension `2.1.169` (native-binary CLI), via the `claudeCode.claudeProcessWrapper` setting, on Windows 11 and Ubuntu 24.04. +Confirmed on Opus 4.7 and Opus 4.8 with VS Code extension builds `2.1.169` through `2.1.172` (native-binary CLI), via the `claudeCode.claudeProcessWrapper` setting, on Windows 11 and Ubuntu 24.04. -* **Thinking fix:** earlier builds (`2.1.165` / `2.1.167`) signaled thinking with `--thinking adaptive`; `2.1.169` uses `--max-thinking-tokens` on the VS Code path. The launcher keys off either, plus `-p`/`--print` for headless. The `--thinking-display` flag and the request field are stable levers; the Option 2 minified strings can change between releases (the script matches generically and skips if not found). -* **Context-icon fix:** the `>50% used` gate appeared around `2.1.165` (absent in `2.1.131` / `2.1.128`). The patch anchors on the stable substring `>=50)return null}`; if a future build changes that exact string, the launcher safely no-ops and the anchor needs updating. +* **Thinking fix:** earlier builds (`2.1.165` / `2.1.167`) signaled thinking with `--thinking adaptive`; `2.1.169` and later use `--max-thinking-tokens` on the VS Code path. The launcher keys off either, plus `-p`/`--print` for headless. The `--thinking-display` flag and the request field are stable levers; the Option 2 minified strings can change between releases (the script matches generically and skips if not found). +* **Context-icon fix:** the `>50% used` gate has been observed with different minified names (`Z/U` in `2.1.108` and `2.1.131`, `t/c` in `2.1.170` and `2.1.172`). The patch matches the guard-pair shape with variable-name captures and records the matched names in its ownership marker, so it patches and reverts cleanly regardless of the names; if a future build changes the guard shape itself, the launcher safely no-ops and the anchor needs updating. +* **Markdown copy/export fix:** the copy controls are appended to the webview bundle and re-applied each launch. The injection is anchored on stable structural markers and skips cleanly if the bundle shape changes. Behavior may change in future Claude Code releases. diff --git a/TECHNICAL.md b/TECHNICAL.md index 2be784d..a6d904a 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -96,6 +96,8 @@ The launcher intentionally sets no environment of its own by default; it only ad ### Option 2: extension.js patch +> **Unmaintained.** Documented for reference. VS Code only, and must be re-applied after every extension update; the launcher (Option 1) is the supported fix. + A one-line change so the non-interactive spawn always forwards the flag: ```js @@ -113,6 +115,8 @@ Toggle idea (untested): changing the line to `l.display || (process.env.CC_THINK ### Option 3: local proxy (design) +> **Unmaintained and untested.** A design and a working starting point, not a turnkey fix; it sits in the path of your live auth token. The launcher (Option 1) is the supported fix. + The most thorough fix: a small localhost forward proxy that injects the field at the wire level, so it is surface-agnostic (VS Code + CLI + SDK), needs no install edits, and survives updates. It works because every Claude Code surface resolves the API host as: ``` @@ -172,17 +176,17 @@ function FJe({usedTokens:e, contextWindow:t, onCompact:i, buttonClassName:n}) { } ``` -`c` is the percent of context *remaining*, so `c >= 50` hides the icon whenever at least half the window is free, i.e. it only appears once more than 50% is used. With a 1M window that is 500k tokens. Older bundles (`2.1.131`, `2.1.128`) do not contain this gate; it appeared around `2.1.165`, which matches users' recollection that the icon used to be visible. +`c` is the percent of context *remaining*, so `c >= 50` hides the icon whenever at least half the window is free, i.e. it only appears once more than 50% is used. With a 1M window that is 500k tokens. The same guard shape is present in the locally inspected `2.1.131` (`Z/U`) and `2.1.170` (`t/c`) bundles, with different minified names. The `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` env var that circulates in the issue threads only shrinks the window to 200k so 50% (100k) is reached sooner. It does not touch the threshold and forces giving up the 1M window, so it is not a real fix. ## The fix ```text -if(c>=50)return null -> if(c>=101)return null}/*ccwa-context-icon*/ +if(t===0)return null;if(c>=50)return null} -> if(c>=101)return null}/*ccwa-context-icon:t:c*/ ``` -`c` is in `[0, 100]`, so `c >= 101` is never true and the gate never hides the icon. The separate `if (t === 0) return null` guard is left intact, so nothing renders before a context window is known. Using `>=101` (rather than deleting the line) is the smallest, most legible, greppable, reversible change, and it preserves the surrounding structure for a clean string substitution. The patch is anchored on the literal `>=50)return null}`, which is stable across builds even though the minified names around it are not, and which occurs exactly once in `2.1.169`. The trailing `/*ccwa-context-icon*/` is an ownership marker: the launcher's reconcile reverses only the marked form, so it can never corrupt upstream code that merely matches a patched value, and a pre-existing unmarked patch from an older launcher is left as-is and re-marked on the next fresh bundle. +`c` is in `[0, 100]`, so `c >= 101` is never true and the gate never hides the icon. Removing `if (t === 0) return null` keeps the icon visible across window-reload gaps where the webview has not repopulated usage data yet; the tooltip can briefly read `0%` until the next usage event arrives. Using `>=101` (rather than deleting the line) is the smallest, most legible, greppable, reversible threshold change, while removing the adjacent startup guard addresses the reload case explicitly. The patch is anchored on the combined guard shape `if(===0)return null;if(>=50)return null}`, not the exact minified names. The trailing `/*ccwa-context-icon::*/` is an ownership marker that stores the matched names: the launcher's reconcile reverses only known patch fingerprints, so it can never corrupt unrelated upstream code; pre-existing legacy patched forms are normalized to pristine and re-applied on the next launch. There is no integrity or subresource check on the webview bundle (the only `sha256` references in `extension.js` belong to a bundled crypto library), so an edited `index.js` loads normally. @@ -197,8 +201,8 @@ The wrapper discovers `index.js` two ways: The edit is made safe: -* Idempotent: writes only when the recomputed bytes differ, and skips (rather than guesses) if the `>=50)return null}` anchor is absent because the extension changed. -* Ownership-marked: the edit carries a `/*ccwa-context-icon*/` marker; reconcile reverses only its own marked edit and never touches upstream code that merely resembles a patched value. +* Idempotent: writes only when the recomputed bytes differ, and skips (rather than guesses) if the combined guard shape is absent because the extension changed. +* Ownership-marked: the edit carries a `/*ccwa-context-icon::*/` marker; reconcile reverses only its own marked edit and never touches upstream code that merely resembles a patched value. Older `/*ccwa-context-icon*/` forms are treated as legacy fingerprints. * Atomic: written to a temp file and moved into place only after it is verified non-empty and actually patched, so a failed or partial write cannot corrupt the bundle. * Metadata-preserving via `cp -p` (portable; the GNU-only `chmod`/`chown --reference` is avoided so it also works on macOS/BSD). The Windows launcher writes with `fs.writeFileSync` + `fs.renameSync`, inheriting the parent directory's ACLs (it does not preserve the file mode - the one intentional bash/node asymmetry). * Fully guarded so it never blocks the launch (a read-only file, a renamed bundle, or a missing tool simply no-ops). @@ -251,9 +255,9 @@ React.createElement(FJe, { }); ``` -`usageData` initializes to all zeros and is only filled by usage events that arrive during an assistant turn. There is no seeding from `get_claude_state` on resume, so immediately after a window reload of a continued conversation, before any new turn, the store is still `{0,0,0,0}`: `usedTokens = 0` (the tooltip reads "0% used") and `contextWindow = 0 - 0 - 13000 = -13000` (negative, so the `t === 0` guard does not fire and, after the patch, the icon still renders at 0%). This self-corrects on the next turn, when a usage event fills the store with the real totals. `/context` does not use this store; it queries the CLI directly, so it always shows the true number. +`usageData` initializes to all zeros and is only filled by usage events that arrive during an assistant turn. There is no seeding from `get_claude_state` on resume, so immediately after a window reload of a continued conversation, before any new turn, the store is still empty: the tooltip can read "0% used" until a usage event fills the store with the real totals. The patch removes the `t === 0` hide guard so this state still renders an icon instead of hiding it for the whole reload gap. `/context` does not use this store; it queries the CLI directly, so it always shows the true number. -If showing a transient 0% is undesirable, changing `if(t===0)return null` to `if(t<=0)return null` hides the icon while the store is empty (`t` is negative then) and shows it with the correct value after the first turn. This is documented as an optional manual tweak and is not applied by default. +The transient `0%` is deliberate: it is the same stale-data symptom the extension can already show, and it is less confusing than no icon at all after a reload. ### The glyph is a coarse 3-state gauge @@ -269,4 +273,4 @@ The pie button's `onClick` is `onCompact`: clicking the icon triggers compaction ## Compatibility -Confirmed on VS Code extension `2.1.169` (native-binary CLI) on Windows 11 and Ubuntu 24.04. The `>50% used` gate appeared around `2.1.165` (absent in `2.1.131` / `2.1.128`). The patch keys off the stable substring `>=50)return null}`, not the minified component name; if a future build changes that exact substring, the launcher safely no-ops (the icon goes missing again) until the anchor is updated. The standalone [`fix-context-icon.py`](fixes/context-icon/fix-context-icon.py) applies the same change directly and supports `--revert`. +Confirmed on VS Code extension `2.1.169` (native-binary CLI) on Windows 11 and Ubuntu 24.04. The same guard shape has been observed with different minified names (`Z/U` in `2.1.131`, `t/c` in `2.1.170`). The patch keys off that shape, not the minified component name or exact variable names; if a future build changes the shape, the launcher safely no-ops (the icon goes missing again) until the anchor is updated. The standalone [`fix-context-icon.py`](fixes/context-icon/fix-context-icon.py) applies the same change directly and supports `--revert`. diff --git a/fixes/context-icon/README.md b/fixes/context-icon/README.md index ad27254..e1015b1 100644 --- a/fixes/context-icon/README.md +++ b/fixes/context-icon/README.md @@ -5,8 +5,9 @@ Restores the always-visible context-usage icon in the VS Code chat input. Extension builds 2.1.165+ hide that icon until you have used more than 50% of the context window. With the 1M context window that is ~500k tokens, so it is -effectively never shown. This fix flips the threshold so the icon renders -whenever a context window is known, at any usage level. +effectively never shown. This fix removes the startup hide guard and flips the +threshold so the icon renders at any usage level, including the reload gap before +fresh usage data arrives. ## Standalone usage @@ -29,12 +30,16 @@ launcher reverts our edit on the next launch. ## Maintenance Contract -- Anchors / selectors: `>=50)return null}` (component `FJe` in `webview/index.js`). -- Ownership marker: `/*ccwa-context-icon*/`. Apply rewrites `>=50)return null}` -> - `>=101)return null}/*ccwa-context-icon*/`; undo reverses only that marked form. -- Failure mode if an anchor moves: if the anchor string changes, apply no-ops with - a one-line warning (the icon goes missing again) until the anchor is updated. A - bare upstream `>=101)return null}` with no marker is never touched. +- Anchors / selectors: `if(===0)return null;if(>=50)return null}` + (component `FJe` in `webview/index.js`). The identifier names are captured, not + hardcoded. +- Ownership marker: `/*ccwa-context-icon::*/`. Apply + rewrites the combined guard to `if(>=101)return null}`; + undo uses the marker metadata to restore the pristine combined guard with the + same variable names. Older `/*ccwa-context-icon*/` markers are recognized as + legacy fingerprints. +- Failure mode if an anchor moves: if the guard shape changes, apply no-ops with a + one-line warning (the icon goes missing again) until the anchor is updated. - Launcher registry entry: feature id `context-icon`, file `webview/index.js`, apply = marked swap, undo = reverse of the marked form only. - Test fixture: `tests/test_reconcile.py` (launcher engine, both platforms) and diff --git a/fixes/context-icon/fix-context-icon.py b/fixes/context-icon/fix-context-icon.py index 6191517..1520489 100755 --- a/fixes/context-icon/fix-context-icon.py +++ b/fixes/context-icon/fix-context-icon.py @@ -17,14 +17,20 @@ } With the 1M context window, 50% used = 500,000 tokens, so the icon stays hidden -for virtually an entire normal session. This script flips the threshold so the -icon is visible whenever a context window is known (t>0), at any usage level. +for virtually an entire normal session. This script removes the startup guard and +flips the threshold so the icon stays visible across reload gaps and then +self-corrects when fresh usage data arrives. - if(c>=50)return null -> if(c>=101)return null}/*ccwa-context-icon*/ (marked; c maxes at 100) + if(t===0)return null;if(c>=50)return null} + -> if(c>=101)return null}/*ccwa-context-icon:t:c*/ (marked; c maxes at 100) -The (t===0) guard is left intact. In a resumed window, the webview can still show -a transient 0% before the first fresh response updates context metadata. After -that first response the icon stays visible. +The minified variable names are not stable across builds; the patcher matches the +guard shape with any ASCII JS identifier pair and stores the matched names in the +marker so undo can restore the same pristine names. + +In a resumed window, the webview can still show a transient 0% before the first +fresh response updates context metadata. After that first response the icon +corrects. SAFE & REVERSIBLE ----------------- @@ -48,14 +54,54 @@ """ import glob import os +import re import shutil import sys import tempfile -OLD = ">=50)return null}" -NEW = ">=101)return null}/*ccwa-context-icon*/" +IDENT = r"[A-Za-z_$][A-Za-z0-9_$]*" +OLD_RE = re.compile(rf"if\(({IDENT})===0\)return null;if\(({IDENT})>=50\)return null\}}") +MARKED_RE = re.compile( + rf"if\(({IDENT})>=101\)return null\}}/\*ccwa-context-icon:({IDENT}):\1\*/" +) +# Legacy bare (metadata-less) marker on arbitrary guard names: an older +# var-agnostic write could leave the both-guards >=101 form + bare marker on a +# non-t/c build (e.g. Z/U). Recognize it by shape, not the fixed t/c. +LEGACY_NEW_RE = re.compile( + rf"if\(({IDENT})===0\)return null;if\(({IDENT})>=101\)return null\}}/\*ccwa-context-icon\*/" +) +# Strip any leftover bare marker so patch_file (which only re-applies on the +# pristine guard pair) is never wedged by an unrecognized bare-marked form. +ORPHAN_MARKER_RE = re.compile(r"\)return null\}/\*ccwa-context-icon\*/") +OLD = "if(t===0)return null;if(c>=50)return null}" +NEW_BARE = "if(c>=101)return null}" +NEW_LEGACY = NEW_BARE + "/*ccwa-context-icon*/" +NEW = "if(c>=101)return null}/*ccwa-context-icon:t:c*/" +LEGACY_BARE = "if(t===0)return null;if(c>=101)return null}" BACKUP_SUFFIX = ".bak-context-icon" + +def old_guard(first_var, remaining_var): + return f"if({first_var}===0)return null;if({remaining_var}>=50)return null}}" + + +def marked_guard(first_var, remaining_var): + return ( + f"if({remaining_var}>=101)return null}}" + f"/*ccwa-context-icon:{first_var}:{remaining_var}*/" + ) + + +def undo_known_patches(data): + data = MARKED_RE.sub(lambda m: old_guard(m.group(2), m.group(1)), data) + data = LEGACY_NEW_RE.sub(lambda m: old_guard(m.group(1), m.group(2)), data) + data = ( + data.replace(NEW_LEGACY, OLD) + .replace(LEGACY_BARE, OLD) + .replace(NEW_BARE, OLD) + ) + return ORPHAN_MARKER_RE.sub(")return null}", data) + DISCOVERY_GLOBS = [ os.path.expanduser("~/.vscode/extensions/anthropic.claude-code-*/webview/index.js"), os.path.expanduser("~/.vscode-server/extensions/anthropic.claude-code-*/webview/index.js"), @@ -103,18 +149,22 @@ def write_atomic_preserving_metadata(path, text): def patch_file(path): with open(path, "r", encoding="utf-8", newline="") as f: data = f.read() - if NEW in data: + if MARKED_RE.search(data): return "already-patched" - n = data.count(OLD) + data = undo_known_patches(data) + matches = list(OLD_RE.finditer(data)) + n = len(matches) if n == 0: return "gate-not-found (extension version changed? re-inspect FJe in index.js)" if n > 1: return f"ambiguous ({n} matches) — skipped for safety" + match = matches[0] + patched = data[: match.start()] + marked_guard(match.group(1), match.group(2)) + data[match.end() :] backup = path + BACKUP_SUFFIX if not os.path.exists(backup): with open(backup, "w", encoding="utf-8", newline="") as b: b.write(data) - write_atomic_preserving_metadata(path, data.replace(OLD, NEW, 1)) + write_atomic_preserving_metadata(path, patched) return "PATCHED" diff --git a/fixes/markdown-copy-export/README.md b/fixes/markdown-copy-export/README.md index 98ae7f0..9ea4116 100644 --- a/fixes/markdown-copy-export/README.md +++ b/fixes/markdown-copy-export/README.md @@ -4,10 +4,11 @@ The Claude Code VS Code chat has no way to copy a whole message or the whole conversation as Markdown (only code blocks have a copy button). This adds a -per-message copy control (Markdown primary, plain text secondary) on every user -and assistant message, a "copy entire conversation" control, and a standalone -CLI that exports a session transcript to Markdown/plain text or opens the raw -`.jsonl`. Affects the VS Code extension webview; the CLI is independent of it. +single-click copy icon (Markdown) on every user and assistant message - it flips +to a checkmark only when the copy actually lands - plus a floating "copy +conversation" icon, and a standalone CLI that exports a session transcript to +Markdown or plain text. Affects the VS Code extension webview; the CLI is +independent of it. ## Standalone usage @@ -27,7 +28,6 @@ Session exporter (independent of the webview): python3 fixes/markdown-copy-export/cc-export.py --format text python3 fixes/markdown-copy-export/cc-export.py --include-thinking --include-tools python3 fixes/markdown-copy-export/cc-export.py --session ID -o out.md - python3 fixes/markdown-copy-export/cc-export.py --open # open raw .jsonl in VS Code ## Launcher toggle @@ -39,10 +39,13 @@ controls and reverts ours on the next launch. `CC_WORKAROUNDS=0` reverts it too. - Anchors / selectors: user bubble `[class*="userMessageContainer_"]`; assistant bubble `[data-testid="assistant-message"]` (NOT `[data-message-rating]` — that is the nested, experiment+analytics-gated rating widget, which the sanitizer strips); - chrome strip-prefixes `toolUse_`/`toolResult_`/`toolReference_`/`thinking_`/ - `unknownContent_` plus `[data-message-rating]` and `button`; - `navigator.clipboard.writeText` (proven to work via the built-in copy-code - button). Optional refinements pinned at install: the messages container and the + chrome strip-prefixes `toolUse_`/`toolResult_`/`toolReference_`/ + `unknownContent_` plus `[data-message-rating]` and `button`; visible + `thinking_` summaries are content and remain copyable; + clipboard write via a synchronous `document.execCommand("copy")` first + (gesture-safe and works without a secure context, e.g. remote / code-server), + falling back to `navigator.clipboard.writeText`; the icon only flips to a + checkmark when a copy actually succeeds. Optional refinements pinned at install: the messages container and the single all-content wrapper, if any (see the inject source constants `MESSAGES_CONTAINER` / `ASSISTANT_CONTENT`). When a bundle update renames the bubble anchor or a chrome hook, the Phase-9 selector guard fails loudly; re-pin diff --git a/fixes/markdown-copy-export/add-md-copy.py b/fixes/markdown-copy-export/add-md-copy.py index 2958e3e..50c4290 100644 --- a/fixes/markdown-copy-export/add-md-copy.py +++ b/fixes/markdown-copy-export/add-md-copy.py @@ -31,8 +31,8 @@ BACKUP_SUFFIX = ".bak-md-copy" # >>>CCWA-MD-COPY-EMBED>>> (generated by tools/gen-embeds; do not edit) -INJECT_JS = base64.b64decode("LyogY2MtbWQtY29weTogcGVyLW1lc3NhZ2UgYW5kIHdob2xlLWNvbnZlcnNhdGlvbiBjb3B5IChtYXJrZG93bi9wbGFpbikgZm9yIHRoZQogKiBDbGF1ZGUgQ29kZSBWUyBDb2RlIHdlYnZpZXcuIFNlbGYtY29udGFpbmVkIElJRkUgYXBwZW5kZWQgdG8gd2Vidmlldy9pbmRleC5qcy4KICogQWRkaXRpdmUgYW5kIHJlYWQtb25seSB3LnIudC4gYXBwIHN0YXRlOyBrZXllZCBvbiBzdGFibGUgQ1NTLW1vZHVsZSBjbGFzcwogKiBwcmVmaXhlcywgc28gaXQgZmFpbHMgc2FmZSAoY29udHJvbHMgc2ltcGx5IGRvIG5vdCBhcHBlYXIpIGlmIGEgcHJlZml4IG1vdmVzLgogKiBFeHBvc2VzIGl0cyBwdXJlIGZ1bmN0aW9ucyBmb3Igbm9kZSB1bml0IHRlc3RzOyBib290KClzIG9ubHkgaW4gYSByZWFsIHdlYnZpZXcuICovCihmdW5jdGlvbiAoKSB7CiAgInVzZSBzdHJpY3QiOwoKICB2YXIgQ09OVFJPTF9QUkVGSVggPSAiY2MtbWQtY29weSI7IC8vIGV2ZXJ5IGluamVjdGVkIG5vZGUncyBjbGFzcyBzdGFydHMgd2l0aCB0aGlzCiAgdmFyIFVTRVJfQlVCQkxFID0gJ1tjbGFzcyo9InVzZXJNZXNzYWdlQ29udGFpbmVyXyJdJzsKICAvLyBBc3Npc3RhbnQgbWVzc2FnZSB3cmFwcGVyLiBWZXJpZmllZCBvbiAyLjEuMTcwOiB0aGUgcmVuZGVyIGVtaXRzIGV4YWN0bHkgb25lCiAgLy8gYGRhdGEtdGVzdGlkPSJhc3Npc3RhbnQtbWVzc2FnZSJgIGRpdiBwZXIgYXNzaXN0YW50IHR1cm4sIHdpdGggdGhlIHJhdGluZwogIC8vIHdpZGdldCBhbmQgY29udGVudCBibG9ja3MgYXMgaXRzIGNoaWxkcmVuLiAoVGhlIGVhcmxpZXIgYFtkYXRhLW1lc3NhZ2UtcmF0aW5nXWAKICAvLyB3YXMgV1JPTkc6IHRoYXQgYXR0cmlidXRlIHNpdHMgb24gdGhlIG5lc3RlZCByYXRpbmcgY29udHJvbCwgd2hpY2ggaXMgYWxzbyBvbmx5CiAgLy8gcmVuZGVyZWQgYmVoaW5kIGFuIGV4cGVyaW1lbnQrYW5hbHl0aWNzIGdhdGUuKSBSZS1waW5uZWQgaW4gVGFzayA2LgogIHZhciBBU1NJU1RBTlRfQlVCQkxFID0gJ1tkYXRhLXRlc3RpZD0iYXNzaXN0YW50LW1lc3NhZ2UiXSc7CiAgdmFyIE1FU1NBR0VTX0NPTlRBSU5FUiA9ICdbY2xhc3MqPSJtZXNzYWdlc0NvbnRhaW5lcl8iXSc7IC8vIGUuZy4gJ1tjbGFzcyo9InRpbWVsaW5lXyJdJzsgIiIgLT4gb2JzZXJ2ZSBkb2N1bWVudC5ib2R5CiAgLy8gT3B0aW9uYWwgbmFycm93aW5nIG9ubHkuIE1VU1QgYmUgYSBzaW5nbGUgd3JhcHBlciBhcm91bmQgQUxMIGNvbnRlbnQgYmxvY2tzLAogIC8vIG5vdCBhIHBlci1ibG9jayBjbGFzcyAoYSB0dXJuIGhhcyBtdWx0aXBsZSBibG9ja3MpLiAiIiAtPiB1c2UgdGhlIGJ1YmJsZSBpdHNlbGYKICAvLyAoYWxyZWFkeSBhZ2dyZWdhdGVzIGFsbCBibG9ja3M7IHNhbml0aXplQ2xvbmUgaXMgdGhlIGNvcnJlY3RuZXNzIGdhdGUpLgogIHZhciBBU1NJU1RBTlRfQ09OVEVOVCA9ICIiOwogIHZhciBGRUVEQkFDS19NUyA9IDE4MDA7CgogIC8vIC0tLS0gSFRNTCAtPiBNYXJrZG93biAoRE9NIHdhbGspIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICAvLyBVc2VzIG9ubHk6IG5vZGVUeXBlLCB0YWdOYW1lLCBjaGlsZE5vZGVzLCB0ZXh0Q29udGVudCwgZ2V0QXR0cmlidXRlLCBjbGFzc05hbWUuCiAgZnVuY3Rpb24gaHRtbFRvTWFya2Rvd24ocm9vdCkgewogICAgLy8gTG9uZ2VzdCBydW4gb2YgY29uc2VjdXRpdmUgYmFja3RpY2tzIGluIHMsIHNvIGEgY29kZSBkZWxpbWl0ZXIvZmVuY2UgY2FuIGJlCiAgICAvLyBjaG9zZW4gbG9uZ2VyIHRoYW4gYW55dGhpbmcgaW5zaWRlIGl0IChlbHNlIGBgYCBpbiB0aGUgY29udGVudCBjbG9zZXMgZWFybHkpLgogICAgZnVuY3Rpb24gYmFja3RpY2tSdW4ocykgewogICAgICB2YXIgbWF4ID0gMCwgY3VyID0gMDsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgaWYgKHMuY2hhckF0KGkpID09PSAiYCIpIHsgY3VyKys7IGlmIChjdXIgPiBtYXgpIG1heCA9IGN1cjsgfSBlbHNlIGN1ciA9IDA7CiAgICAgIH0KICAgICAgcmV0dXJuIG1heDsKICAgIH0KICAgIGZ1bmN0aW9uIGZlbmNlKHMsIG1pbikgeyB2YXIgbiA9IGJhY2t0aWNrUnVuKHMpICsgMTsgaWYgKG4gPCBtaW4pIG4gPSBtaW47IHJldHVybiBuZXcgQXJyYXkobiArIDEpLmpvaW4oImAiKTsgfQogICAgZnVuY3Rpb24gaW5saW5lKG5vZGUpIHsKICAgICAgdmFyIG91dCA9ICIiOwogICAgICB2YXIga2lkcyA9IG5vZGUuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBraWRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgdmFyIGMgPSBraWRzW2ldOwogICAgICAgIGlmIChjLm5vZGVUeXBlID09PSAzKSB7IG91dCArPSBjLnRleHRDb250ZW50IHx8ICIiOyBjb250aW51ZTsgfQogICAgICAgIGlmIChjLm5vZGVUeXBlICE9PSAxKSBjb250aW51ZTsKICAgICAgICB2YXIgdGFnID0gKGMudGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKTsKICAgICAgICBpZiAodGFnID09PSAiQlIiKSBvdXQgKz0gIlxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJTVFJPTkciIHx8IHRhZyA9PT0gIkIiKSBvdXQgKz0gIioqIiArIGlubGluZShjKSArICIqKiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiRU0iIHx8IHRhZyA9PT0gIkkiKSBvdXQgKz0gIioiICsgaW5saW5lKGMpICsgIioiOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIkRFTCIgfHwgdGFnID09PSAiUyIpIG91dCArPSAifn4iICsgaW5saW5lKGMpICsgIn5+IjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJDT0RFIikgewogICAgICAgICAgdmFyIGN0ID0gYy50ZXh0Q29udGVudCB8fCAiIjsKICAgICAgICAgIHZhciBkID0gZmVuY2UoY3QsIDEpOwogICAgICAgICAgLy8gQ29tbW9uTWFyayBzdHJpcHMgb25lIGxlYWRpbmcrdHJhaWxpbmcgc3BhY2UsIHNvIHBhZCB3aGVuIGFuIGVkZ2UgaXMgYQogICAgICAgICAgLy8gYmFja3RpY2sgdG8ga2VlcCBpdCBmcm9tIG1lcmdpbmcgd2l0aCB0aGUgZGVsaW1pdGVyLgogICAgICAgICAgdmFyIHAgPSAoY3QuY2hhckF0KDApID09PSAiYCIgfHwgY3QuY2hhckF0KGN0Lmxlbmd0aCAtIDEpID09PSAiYCIpID8gIiAiIDogIiI7CiAgICAgICAgICBvdXQgKz0gZCArIHAgKyBjdCArIHAgKyBkOwogICAgICAgIH0KICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJBIikgewogICAgICAgICAgdmFyIGhyZWYgPSBjLmdldEF0dHJpYnV0ZSA/IGMuZ2V0QXR0cmlidXRlKCJocmVmIikgOiBudWxsOwogICAgICAgICAgdmFyIHQgPSBpbmxpbmUoYyk7CiAgICAgICAgICBvdXQgKz0gaHJlZiA/ICJbIiArIHQgKyAiXSgiICsgaHJlZiArICIpIiA6IHQ7CiAgICAgICAgfSBlbHNlIG91dCArPSBpbmxpbmUoYyk7IC8vIHVua25vd24gaW5saW5lIHdyYXBwZXI6IGtlZXAgdGV4dCwgZHJvcCB0YWcKICAgICAgfQogICAgICByZXR1cm4gb3V0OwogICAgfQogICAgZnVuY3Rpb24gbGFuZ09mKGNvZGVFbCkgewogICAgICB2YXIgY2xzID0gIiI7CiAgICAgIGlmIChjb2RlRWwpIGNscyA9IChjb2RlRWwuZ2V0QXR0cmlidXRlICYmIGNvZGVFbC5nZXRBdHRyaWJ1dGUoImNsYXNzIikpIHx8IGNvZGVFbC5jbGFzc05hbWUgfHwgIiI7CiAgICAgIHZhciBtID0gL2xhbmd1YWdlLShbQS1aYS16MC05KyMuXC1dKykvLmV4ZWMoY2xzIHx8ICIiKTsKICAgICAgcmV0dXJuIG0gPyBtWzFdIDogIiI7CiAgICB9CiAgICBmdW5jdGlvbiBmaW5kQ2hpbGRUYWcobm9kZSwgdGFnKSB7CiAgICAgIHZhciBraWRzID0gbm9kZS5jaGlsZE5vZGVzIHx8IFtdOwogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIHsKICAgICAgICBpZiAoa2lkc1tpXS5ub2RlVHlwZSA9PT0gMSAmJiAoa2lkc1tpXS50YWdOYW1lIHx8ICIiKS50b1VwcGVyQ2FzZSgpID09PSB0YWcpIHJldHVybiBraWRzW2ldOwogICAgICB9CiAgICAgIHJldHVybiBudWxsOwogICAgfQogICAgZnVuY3Rpb24gbGlzdChub2RlLCBvcmRlcmVkLCBkZXB0aCkgewogICAgICB2YXIgb3V0ID0gIiIsIG4gPSAxOwogICAgICB2YXIga2lkcyA9IG5vZGUuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBraWRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgdmFyIGxpID0ga2lkc1tpXTsKICAgICAgICBpZiAobGkubm9kZVR5cGUgIT09IDEgfHwgKGxpLnRhZ05hbWUgfHwgIiIpLnRvVXBwZXJDYXNlKCkgIT09ICJMSSIpIGNvbnRpbnVlOwogICAgICAgIHZhciBtYXJrZXIgPSBvcmRlcmVkID8gbisrICsgIi4gIiA6ICItICI7CiAgICAgICAgdmFyIGluZGVudCA9IG5ldyBBcnJheShkZXB0aCArIDEpLmpvaW4oIiAgIik7CiAgICAgICAgdmFyIGxlYWQgPSAiIiwgbmVzdGVkID0gIiI7CiAgICAgICAgdmFyIGxrID0gbGkuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgICBmb3IgKHZhciBqID0gMDsgaiA8IGxrLmxlbmd0aDsgaisrKSB7CiAgICAgICAgICB2YXIgY2ggPSBsa1tqXTsKICAgICAgICAgIHZhciBjdCA9IGNoLm5vZGVUeXBlID09PSAxID8gKGNoLnRhZ05hbWUgfHwgIiIpLnRvVXBwZXJDYXNlKCkgOiAiIjsKICAgICAgICAgIGlmIChjdCA9PT0gIlVMIikgbmVzdGVkICs9IGxpc3QoY2gsIGZhbHNlLCBkZXB0aCArIDEpOwogICAgICAgICAgZWxzZSBpZiAoY3QgPT09ICJPTCIpIG5lc3RlZCArPSBsaXN0KGNoLCB0cnVlLCBkZXB0aCArIDEpOwogICAgICAgICAgZWxzZSBpZiAoY2gubm9kZVR5cGUgPT09IDMpIGxlYWQgKz0gY2gudGV4dENvbnRlbnQgfHwgIiI7CiAgICAgICAgICBlbHNlIGxlYWQgKz0gaW5saW5lKGNoKTsKICAgICAgICB9CiAgICAgICAgb3V0ICs9IGluZGVudCArIG1hcmtlciArIGxlYWQudHJpbSgpICsgIlxuIiArIG5lc3RlZDsKICAgICAgfQogICAgICByZXR1cm4gb3V0OwogICAgfQogICAgZnVuY3Rpb24gdGFibGUobm9kZSkgewogICAgICB2YXIgcm93cyA9IFtdOwogICAgICAoZnVuY3Rpb24gY29sbGVjdChjb250YWluZXIpIHsKICAgICAgICB2YXIga2lkcyA9IGNvbnRhaW5lci5jaGlsZE5vZGVzIHx8IFtdOwogICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwga2lkcy5sZW5ndGg7IGkrKykgewogICAgICAgICAgdmFyIGMgPSBraWRzW2ldOwogICAgICAgICAgaWYgKGMubm9kZVR5cGUgIT09IDEpIGNvbnRpbnVlOwogICAgICAgICAgdmFyIHQgPSAoYy50YWdOYW1lIHx8ICIiKS50b1VwcGVyQ2FzZSgpOwogICAgICAgICAgaWYgKHQgPT09ICJUSEVBRCIgfHwgdCA9PT0gIlRCT0RZIiB8fCB0ID09PSAiVEZPT1QiKSBjb2xsZWN0KGMpOwogICAgICAgICAgZWxzZSBpZiAodCA9PT0gIlRSIikgewogICAgICAgICAgICB2YXIgY2VsbHMgPSBbXSwgY2MgPSBjLmNoaWxkTm9kZXMgfHwgW107CiAgICAgICAgICAgIGZvciAodmFyIGogPSAwOyBqIDwgY2MubGVuZ3RoOyBqKyspIHsKICAgICAgICAgICAgICB2YXIgZCA9IGNjW2pdOwogICAgICAgICAgICAgIGlmIChkLm5vZGVUeXBlICE9PSAxKSBjb250aW51ZTsKICAgICAgICAgICAgICB2YXIgZHQgPSAoZC50YWdOYW1lIHx8ICIiKS50b1VwcGVyQ2FzZSgpOwogICAgICAgICAgICAgIGlmIChkdCA9PT0gIlRIIiB8fCBkdCA9PT0gIlREIikgY2VsbHMucHVzaChpbmxpbmUoZCkudHJpbSgpKTsKICAgICAgICAgICAgfQogICAgICAgICAgICByb3dzLnB1c2goY2VsbHMpOwogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSkobm9kZSk7CiAgICAgIGlmICghcm93cy5sZW5ndGgpIHJldHVybiAiIjsKICAgICAgdmFyIGhlYWQgPSByb3dzWzBdLCBib2R5ID0gcm93cy5zbGljZSgxKTsKICAgICAgdmFyIHNlcCA9IGhlYWQubWFwKGZ1bmN0aW9uICgpIHsgcmV0dXJuICItLS0iOyB9KTsKICAgICAgdmFyIG91dCA9ICJ8ICIgKyBoZWFkLmpvaW4oIiB8ICIpICsgIiB8XG58ICIgKyBzZXAuam9pbigiIHwgIikgKyAiIHxcbiI7CiAgICAgIGZvciAodmFyIGsgPSAwOyBrIDwgYm9keS5sZW5ndGg7IGsrKykgb3V0ICs9ICJ8ICIgKyBib2R5W2tdLmpvaW4oIiB8ICIpICsgIiB8XG4iOwogICAgICByZXR1cm4gb3V0OwogICAgfQogICAgZnVuY3Rpb24gYmxvY2sobm9kZSkgewogICAgICB2YXIgb3V0ID0gIiI7CiAgICAgIHZhciBraWRzID0gbm9kZS5jaGlsZE5vZGVzIHx8IFtdOwogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIHsKICAgICAgICB2YXIgYyA9IGtpZHNbaV07CiAgICAgICAgaWYgKGMubm9kZVR5cGUgPT09IDMpIHsgaWYgKChjLnRleHRDb250ZW50IHx8ICIiKS50cmltKCkpIG91dCArPSBjLnRleHRDb250ZW50OyBjb250aW51ZTsgfQogICAgICAgIGlmIChjLm5vZGVUeXBlICE9PSAxKSBjb250aW51ZTsKICAgICAgICB2YXIgdGFnID0gKGMudGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKTsKICAgICAgICBpZiAoL15IWzEtNl0kLy50ZXN0KHRhZykpIG91dCArPSBuZXcgQXJyYXkoK3RhZ1sxXSArIDEpLmpvaW4oIiMiKSArICIgIiArIGlubGluZShjKS50cmltKCkgKyAiXG5cbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiUCIpIG91dCArPSBpbmxpbmUoYykudHJpbSgpICsgIlxuXG4iOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIlVMIikgb3V0ICs9IGxpc3QoYywgZmFsc2UsIDApICsgIlxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJPTCIpIG91dCArPSBsaXN0KGMsIHRydWUsIDApICsgIlxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJQUkUiKSB7CiAgICAgICAgICB2YXIgY29kZSA9IGZpbmRDaGlsZFRhZyhjLCAiQ09ERSIpOwogICAgICAgICAgdmFyIGxhbmcgPSBsYW5nT2YoY29kZSB8fCBjKTsKICAgICAgICAgIHZhciBib2R5ID0gKGNvZGUgfHwgYykudGV4dENvbnRlbnQgfHwgIiI7CiAgICAgICAgICB2YXIgZiA9IGZlbmNlKGJvZHksIDMpOwogICAgICAgICAgb3V0ICs9IGYgKyBsYW5nICsgIlxuIiArIGJvZHkucmVwbGFjZSgvXG4kLywgIiIpICsgIlxuIiArIGYgKyAiXG5cbiI7CiAgICAgICAgfSBlbHNlIGlmICh0YWcgPT09ICJCTE9DS1FVT1RFIikgewogICAgICAgICAgdmFyIGlubmVyID0gYmxvY2soYykudHJpbSgpLnNwbGl0KCJcbiIpLm1hcChmdW5jdGlvbiAobCkgeyByZXR1cm4gIj4gIiArIGw7IH0pLmpvaW4oIlxuIik7CiAgICAgICAgICBvdXQgKz0gaW5uZXIgKyAiXG5cbiI7CiAgICAgICAgfSBlbHNlIGlmICh0YWcgPT09ICJIUiIpIG91dCArPSAiLS0tXG5cbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiVEFCTEUiKSBvdXQgKz0gdGFibGUoYykgKyAiXG4iOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIkJSIikgb3V0ICs9ICJcbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiU1RST05HIiB8fCB0YWcgPT09ICJCIiB8fCB0YWcgPT09ICJFTSIgfHwgdGFnID09PSAiSSIgfHwKICAgICAgICAgICAgICAgICB0YWcgPT09ICJBIiB8fCB0YWcgPT09ICJDT0RFIiB8fCB0YWcgPT09ICJERUwiIHx8IHRhZyA9PT0gIlMiKQogICAgICAgICAgb3V0ICs9IGlubGluZShjKSArICJcblxuIjsKICAgICAgICBlbHNlIG91dCArPSBibG9jayhjKTsgLy8gdW5rbm93biB3cmFwcGVyOiByZWN1cnNlIChkcm9wIHRhZywga2VlcCBjb250ZW50KQogICAgICB9CiAgICAgIHJldHVybiBvdXQ7CiAgICB9CiAgICAvLyBibG9jaygpIGRpc3BhdGNoZXMgb24gZWFjaCBDSElMRCdzIHRhZywgdHJlYXRpbmcgdGhlIHBhc3NlZCBub2RlIGFzIGEgcGxhaW4KICAgIC8vIGNvbnRhaW5lci4gV3JhcCByb290IGluIGEgb25lLW9mZiBjb250YWluZXIgc28gcm9vdCdzIE9XTiB0YWcgaXMgZGlzcGF0Y2hlZAogICAgLy8gdG9vOiBjYWxsZXJzIHBhc3MgZWl0aGVyIHRoZSBidWJibGUgY29udGFpbmVyIChpdHMgYmxvY2sgY2hpbGRyZW4gcmVuZGVyKSBvcgogICAgLy8gYSBzaW5nbGUgYmxvY2sgZWxlbWVudCBsaWtlIDxwcmU+Lzx1bD4vPHRhYmxlPiAobm93IGhhbmRsZWQsIG5vdCBmbGF0dGVuZWQpLgogICAgcmV0dXJuIGJsb2NrKHsgY2hpbGROb2RlczogW3Jvb3RdIH0pLnJlcGxhY2UoL1xuezMsfS9nLCAiXG5cbiIpLnRyaW0oKTsKICB9CgogIC8vIC0tLS0gcHVyZSBoZWxwZXJzIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICBmdW5jdGlvbiBoYXNQcmVmaXgobm9kZSwgcHJlZml4KSB7CiAgICBpZiAobm9kZS5ub2RlVHlwZSAhPT0gMSB8fCB0eXBlb2Ygbm9kZS5jbGFzc05hbWUgIT09ICJzdHJpbmciKSByZXR1cm4gZmFsc2U7CiAgICB2YXIgcGFydHMgPSBub2RlLmNsYXNzTmFtZS5zcGxpdCgvXHMrLyk7CiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHBhcnRzLmxlbmd0aDsgaSsrKSBpZiAocGFydHNbaV0uaW5kZXhPZihwcmVmaXgpID09PSAwKSByZXR1cm4gdHJ1ZTsKICAgIHJldHVybiBmYWxzZTsKICB9CgogIC8vIENsYXNzLXByZWZpeCBob29rcyBmb3Igbm9uLWNvbnRlbnQgY2hyb21lIHRoYXQgcmVuZGVycyAqaW5zaWRlKiBhbiBhc3Npc3RhbnQKICAvLyBidWJibGUgKHZlcmlmaWVkIG9uIDIuMS4xNzA7IFRhc2sgNiByZS1waW5zIHRoZXNlKS4gdG9vbCovdGhpbmtpbmdfIGFyZSB0aGUgdjEKICAvLyBleGNsdXNpb25zOyB1bmtub3duQ29udGVudF8gaXMgdGhlIHJlbmRlcmVyJ3MgZmFsbGJhY2sgZm9yIHVucmVjb2duaXplZCBibG9jawogIC8vIHR5cGVzLCBzbyBzdHJpcHBpbmcgaXQgbWFrZXMgYSAqZnV0dXJlKiBibG9jayB0eXBlIGZhaWwgc2FmZSB0byBleGNsdWRlZCByYXRoZXIKICAvLyB0aGFuIGxlYWtpbmcgIlVuc3VwcG9ydGVkIGNvbnRlbnQiIGludG8gdGhlIGNvcHkuIFJlLXBpbiBpZiBhIHByZWZpeCBtb3Zlcy4KICB2YXIgQ0hST01FX1BSRUZJWEVTID0gWyJ0b29sVXNlXyIsICJ0b29sUmVzdWx0XyIsICJ0b29sUmVmZXJlbmNlXyIsICJ0aGlua2luZ18iLCAidW5rbm93bkNvbnRlbnRfIl07CgogIC8vIFRydWUgZm9yIGFueSBub2RlIHRoYXQgbXVzdCBuZXZlciBhcHBlYXIgaW4gY29waWVkIG91dHB1dDogb3VyIG93biBjb250cm9scywKICAvLyB0aGUgcmF0aW5nIHdpZGdldCAoYGRhdGEtbWVzc2FnZS1yYXRpbmdgICsgaXRzICJUaGFua3MgZm9yIHlvdXIgZmVlZGJhY2siCiAgLy8gdGV4dCksIGFueSBidXR0b24gKGNvcHktY29kZSBjaHJvbWUpLCBhbmQgdGhlIGV4Y2x1ZGVkIGNvbnRlbnQgYmxvY2tzIGFib3ZlLgogIGZ1bmN0aW9uIGlzQ2hyb21lKG5vZGUpIHsKICAgIGlmIChub2RlLm5vZGVUeXBlICE9PSAxKSByZXR1cm4gZmFsc2U7CiAgICBpZiAoKG5vZGUudGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKSA9PT0gIkJVVFRPTiIpIHJldHVybiB0cnVlOwogICAgaWYgKG5vZGUuZ2V0QXR0cmlidXRlICYmIG5vZGUuZ2V0QXR0cmlidXRlKCJkYXRhLW1lc3NhZ2UtcmF0aW5nIikgIT09IG51bGwpIHJldHVybiB0cnVlOwogICAgaWYgKGhhc1ByZWZpeChub2RlLCBDT05UUk9MX1BSRUZJWCkpIHJldHVybiB0cnVlOwogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBDSFJPTUVfUFJFRklYRVMubGVuZ3RoOyBpKyspIGlmIChoYXNQcmVmaXgobm9kZSwgQ0hST01FX1BSRUZJWEVTW2ldKSkgcmV0dXJuIHRydWU7CiAgICByZXR1cm4gZmFsc2U7CiAgfQoKICAvLyBEZWVwLWNsb25lIGBjb250ZW50Tm9kZWAsIHRoZW4gc3RyaXAgZXZlcnkgY2hyb21lIG5vZGUgc28gY29waWVkIG91dHB1dCBpcyB0aGUKICAvLyBtZXNzYWdlJ3MgdGV4dCBjb250ZW50IG9ubHkuIFRoaXMgaXMgYSBDT1JSRUNUTkVTUyBHQVRFLCBub3QgY29zbWV0aWM6IHRoZQogIC8vIGRlZmF1bHQgY29udGVudCBub2RlIGlzIHRoZSB3aG9sZSBidWJibGUgKGFsbCBjb250ZW50LWJsb2NrIHNpYmxpbmdzLCBzbyBtdWx0aS0KICAvLyBibG9jayBhc3Npc3RhbnQgdHVybnMgYXJlIGNhcHR1cmVkKSwgYW5kIHRoaXMgc3RyaXAtbGlzdCBpcyB0aGUgb25seSB0aGluZwogIC8vIGtlZXBpbmcgdGhlIHJhdGluZyB3aWRnZXQgYW5kIHYxLWV4Y2x1ZGVkIGJsb2NrcyBvdXQgb2YgdGhlIGNvcHkuCiAgZnVuY3Rpb24gc2FuaXRpemVDbG9uZShjb250ZW50Tm9kZSkgewogICAgdmFyIGNsb25lID0gY29udGVudE5vZGUuY2xvbmVOb2RlKHRydWUpOwogICAgKGZ1bmN0aW9uIHN0cmlwKG5vZGUpIHsKICAgICAgdmFyIGtpZHMgPSAobm9kZS5jaGlsZE5vZGVzIHx8IFtdKS5zbGljZSgpOwogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIHsKICAgICAgICB2YXIgYyA9IGtpZHNbaV07CiAgICAgICAgaWYgKGMubm9kZVR5cGUgPT09IDEgJiYgaXNDaHJvbWUoYykpIHsgbm9kZS5yZW1vdmVDaGlsZChjKTsgY29udGludWU7IH0KICAgICAgICBpZiAoYy5ub2RlVHlwZSA9PT0gMSkgc3RyaXAoYyk7CiAgICAgIH0KICAgIH0pKGNsb25lKTsKICAgIHJldHVybiBjbG9uZTsKICB9CgogIGZ1bmN0aW9uIGNsYXNzaWZ5QnViYmxlKG5vZGUpIHsKICAgIGlmIChub2RlLm5vZGVUeXBlICE9PSAxKSByZXR1cm4gbnVsbDsKICAgIGlmIChoYXNQcmVmaXgobm9kZSwgInVzZXJNZXNzYWdlQ29udGFpbmVyXyIpKSByZXR1cm4gInVzZXIiOwogICAgaWYgKG5vZGUuZ2V0QXR0cmlidXRlICYmIG5vZGUuZ2V0QXR0cmlidXRlKCJkYXRhLXRlc3RpZCIpID09PSAiYXNzaXN0YW50LW1lc3NhZ2UiKSByZXR1cm4gImFzc2lzdGFudCI7CiAgICByZXR1cm4gbnVsbDsKICB9CgogIC8vIEJ1aWxkIHRoZSB3aG9sZS1jb252ZXJzYXRpb24gbWFya2Rvd24gZnJvbSBhbiBvcmRlcmVkIGxpc3Qgb2YgYnViYmxlcy4KICAvLyBgY29udGVudE9mKGJ1YmJsZSlgIHJlc29sdmVzIHRoZSBjb250ZW50IG5vZGUgKGRlZmF1bHQ6IHRoZSBidWJibGUgaXRzZWxmLCBzbwogIC8vIGV2ZXJ5IGNvbnRlbnQgYmxvY2sgaXMgaW5jbHVkZWQ7IHNhbml0aXplQ2xvbmUgZHJvcHMgY2hyb21lKTsgYSBkZWZhdWx0IGlzCiAgLy8gcHJvdmlkZWQgZm9yIHRlc3RzLgogIGZ1bmN0aW9uIGNvbnZlcnNhdGlvblRvTWFya2Rvd24oYnViYmxlcywgY29udGVudE9mKSB7CiAgICBjb250ZW50T2YgPSBjb250ZW50T2YgfHwgZnVuY3Rpb24gKGIpIHsgcmV0dXJuIGI7IH07CiAgICB2YXIgcGFydHMgPSBbXTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgYnViYmxlcy5sZW5ndGg7IGkrKykgewogICAgICB2YXIgcm9sZSA9IGNsYXNzaWZ5QnViYmxlKGJ1YmJsZXNbaV0pOwogICAgICBpZiAoIXJvbGUpIGNvbnRpbnVlOwogICAgICB2YXIgY2xlYW4gPSBzYW5pdGl6ZUNsb25lKGNvbnRlbnRPZihidWJibGVzW2ldKSk7CiAgICAgIHZhciBib2R5ID0gcm9sZSA9PT0gImFzc2lzdGFudCIgPyBodG1sVG9NYXJrZG93bihjbGVhbikgOiAoY2xlYW4udGV4dENvbnRlbnQgfHwgIiIpLnRyaW0oKTsKICAgICAgaWYgKCFib2R5KSBjb250aW51ZTsKICAgICAgcGFydHMucHVzaCgocm9sZSA9PT0gInVzZXIiID8gIiMjIFVzZXIiIDogIiMjIEFzc2lzdGFudCIpICsgIlxuXG4iICsgYm9keSk7CiAgICB9CiAgICByZXR1cm4gcGFydHMuam9pbigiXG5cbiIpICsgKHBhcnRzLmxlbmd0aCA/ICJcbiIgOiAiIik7CiAgfQoKICAvLyAtLS0tIGV4cG9ydHMgKG5vZGUgdGVzdHMpIC8gYm9vdCAocmVhbCB3ZWJ2aWV3KSAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiAgaWYgKHR5cGVvZiBkb2N1bWVudCAhPT0gInVuZGVmaW5lZCIpIHsKICAgIGJvb3QoKTsKICB9IGVsc2UgaWYgKHR5cGVvZiBtb2R1bGUgIT09ICJ1bmRlZmluZWQiICYmIG1vZHVsZS5leHBvcnRzKSB7CiAgICBtb2R1bGUuZXhwb3J0cyA9IHsgaHRtbFRvTWFya2Rvd246IGh0bWxUb01hcmtkb3duLCBzYW5pdGl6ZUNsb25lOiBzYW5pdGl6ZUNsb25lLAogICAgICAgICAgICAgICAgICAgICAgIGNsYXNzaWZ5QnViYmxlOiBjbGFzc2lmeUJ1YmJsZSwgY29udmVyc2F0aW9uVG9NYXJrZG93bjogY29udmVyc2F0aW9uVG9NYXJrZG93biB9OwogIH0KCiAgLy8gLS0tLSBsaXZlLXdlYnZpZXcgd2lyaW5nIChydW5zIG9ubHkgd2hlbiBhIGRvY3VtZW50IGV4aXN0cykgLS0tLS0tLS0tLS0tLS0tLQogIGZ1bmN0aW9uIHFzKG5vZGUsIHNlbCkgeyB0cnkgeyByZXR1cm4gc2VsICYmIG5vZGUucXVlcnlTZWxlY3RvciA/IG5vZGUucXVlcnlTZWxlY3RvcihzZWwpIDogbnVsbDsgfSBjYXRjaCAoXykgeyByZXR1cm4gbnVsbDsgfSB9CiAgZnVuY3Rpb24gcXNhKHNlbCkgeyB0cnkgeyByZXR1cm4gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbChzZWwpKTsgfSBjYXRjaCAoXykgeyByZXR1cm4gW107IH0gfQoKICAvLyBUaGUgY29udGVudCBub2RlIHRvIGNvbnZlcnQvY29weTogdGhlIG9wdGlvbmFsIEFTU0lTVEFOVF9DT05URU5UIHdyYXBwZXIgaWYKICAvLyBwaW5uZWQgYW5kIHByZXNlbnQsIGVsc2UgdGhlIGJ1YmJsZSBpdHNlbGYuIFRoZSBidWJibGUgYWxyZWFkeSBjb250YWlucyBldmVyeQogIC8vIGNvbnRlbnQtYmxvY2sgc2libGluZyBvZiBhIG11bHRpLWJsb2NrIHR1cm4sIGFuZCBzYW5pdGl6ZUNsb25lIHN0cmlwcyB0aGUKICAvLyBjaHJvbWUgKHJhdGluZyB3aWRnZXQsIHRvb2wvdGhpbmtpbmcvdW5rbm93biBibG9ja3MsIGJ1dHRvbnMsIG91ciBjb250cm9scykKICAvLyBlaXRoZXIgd2F5IC0tIHNvIHRoaXMgaXMgYSBuYXJyb3dpbmcsIG5ldmVyIHRoZSB0aGluZyB0aGF0IGd1YXJhbnRlZXMKICAvLyBjb3JyZWN0bmVzcy4KICBmdW5jdGlvbiBjb250ZW50Tm9kZU9mKGJ1YmJsZSwgcm9sZSkgewogICAgaWYgKHJvbGUgPT09ICJhc3Npc3RhbnQiICYmIEFTU0lTVEFOVF9DT05URU5UKSB7CiAgICAgIHZhciBuID0gcXMoYnViYmxlLCBBU1NJU1RBTlRfQ09OVEVOVCk7CiAgICAgIGlmIChuKSByZXR1cm4gbjsKICAgIH0KICAgIHJldHVybiBidWJibGU7CiAgfQoKICBmdW5jdGlvbiBjb3B5VGV4dCh0ZXh0KSB7CiAgICB0cnkgewogICAgICBpZiAobmF2aWdhdG9yLmNsaXBib2FyZCAmJiBuYXZpZ2F0b3IuY2xpcGJvYXJkLndyaXRlVGV4dCkgcmV0dXJuIG5hdmlnYXRvci5jbGlwYm9hcmQud3JpdGVUZXh0KHRleHQpOwogICAgfSBjYXRjaCAoXykge30KICAgIHJldHVybiBQcm9taXNlLnJlc29sdmUoKTsgLy8gYmVzdC1lZmZvcnQ7IG5ldmVyIHRocm93IGludG8gdGhlIGFwcAogIH0KCiAgZnVuY3Rpb24gZmxhc2hGZWVkYmFjayhob3N0KSB7CiAgICB0cnkgewogICAgICB2YXIgZmIgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJzcGFuIik7CiAgICAgIGZiLmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1mZWVkYmFjayI7CiAgICAgIGZiLnRleHRDb250ZW50ID0gIkNvcGllZCI7CiAgICAgIGhvc3QuYXBwZW5kQ2hpbGQoZmIpOwogICAgICBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHsgaWYgKGZiICYmIGZiLnBhcmVudE5vZGUpIGZiLnBhcmVudE5vZGUucmVtb3ZlQ2hpbGQoZmIpOyB9LCBGRUVEQkFDS19NUyk7CiAgICB9IGNhdGNoIChfKSB7fQogIH0KCiAgZnVuY3Rpb24gYnViYmxlTWFya2Rvd24oYnViYmxlLCByb2xlKSB7CiAgICB2YXIgY2xlYW4gPSBzYW5pdGl6ZUNsb25lKGNvbnRlbnROb2RlT2YoYnViYmxlLCByb2xlKSk7CiAgICByZXR1cm4gcm9sZSA9PT0gImFzc2lzdGFudCIgPyBodG1sVG9NYXJrZG93bihjbGVhbikgOiAoY2xlYW4udGV4dENvbnRlbnQgfHwgIiIpLnRyaW0oKTsKICB9CiAgZnVuY3Rpb24gYnViYmxlUGxhaW4oYnViYmxlLCByb2xlKSB7CiAgICByZXR1cm4gKHNhbml0aXplQ2xvbmUoY29udGVudE5vZGVPZihidWJibGUsIHJvbGUpKS50ZXh0Q29udGVudCB8fCAiIikudHJpbSgpOwogIH0KCiAgLy8gQnVpbGQgYSBzaW5nbGUgY29udHJvbDogYSBwcmltYXJ5ICJDb3B5IiAobWFya2Rvd24pIHBsdXMgYSBzbWFsbCBjYXJldCB0aGF0CiAgLy8gdG9nZ2xlcyBhIG1lbnUgd2l0aCAiQ29weSBhcyBwbGFpbiB0ZXh0Ii4gQWxsIG5vZGVzIGNhcnJ5IHRoZSBDT05UUk9MX1BSRUZJWAogIC8vIGNsYXNzIHNvIHNhbml0aXplQ2xvbmUgcmVtb3ZlcyB0aGVtIGZyb20gYW55IGNvcGllZCBjb250ZW50LgogIGZ1bmN0aW9uIGJ1aWxkQ29udHJvbChvbk1hcmtkb3duLCBvblBsYWluKSB7CiAgICB2YXIgd3JhcCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInNwYW4iKTsKICAgIHdyYXAuY2xhc3NOYW1lID0gQ09OVFJPTF9QUkVGSVg7CiAgICB2YXIgcHJpbWFyeSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImJ1dHRvbiIpOwogICAgcHJpbWFyeS50eXBlID0gImJ1dHRvbiI7CiAgICBwcmltYXJ5LmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1idG4iOwogICAgcHJpbWFyeS50aXRsZSA9ICJDb3B5IGFzIE1hcmtkb3duIjsKICAgIHByaW1hcnkudGV4dENvbnRlbnQgPSAiQ29weSI7CiAgICBwcmltYXJ5LmFkZEV2ZW50TGlzdGVuZXIoImNsaWNrIiwgZnVuY3Rpb24gKGUpIHsgZS5zdG9wUHJvcGFnYXRpb24oKTsgb25NYXJrZG93bihwcmltYXJ5KTsgfSk7CiAgICB2YXIgY2FyZXQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJidXR0b24iKTsKICAgIGNhcmV0LnR5cGUgPSAiYnV0dG9uIjsKICAgIGNhcmV0LmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1jYXJldCI7CiAgICBjYXJldC50aXRsZSA9ICJDb3B5IG9wdGlvbnMiOwogICAgY2FyZXQudGV4dENvbnRlbnQgPSAi4pa+IjsgLy8gYmxhY2sgZG93bi1wb2ludGluZyBzbWFsbCB0cmlhbmdsZQogICAgdmFyIG1lbnUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJzcGFuIik7CiAgICBtZW51LmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1tZW51IjsKICAgIG1lbnUuc3R5bGUuZGlzcGxheSA9ICJub25lIjsKICAgIHZhciBwbGFpbiA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImJ1dHRvbiIpOwogICAgcGxhaW4udHlwZSA9ICJidXR0b24iOwogICAgcGxhaW4uY2xhc3NOYW1lID0gQ09OVFJPTF9QUkVGSVggKyAiLWJ0biI7CiAgICBwbGFpbi50ZXh0Q29udGVudCA9ICJDb3B5IGFzIHBsYWluIHRleHQiOwogICAgcGxhaW4uYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCBmdW5jdGlvbiAoZSkgeyBlLnN0b3BQcm9wYWdhdGlvbigpOyBtZW51LnN0eWxlLmRpc3BsYXkgPSAibm9uZSI7IG9uUGxhaW4ocGxhaW4pOyB9KTsKICAgIG1lbnUuYXBwZW5kQ2hpbGQocGxhaW4pOwogICAgY2FyZXQuYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCBmdW5jdGlvbiAoZSkgewogICAgICBlLnN0b3BQcm9wYWdhdGlvbigpOwogICAgICBtZW51LnN0eWxlLmRpc3BsYXkgPSBtZW51LnN0eWxlLmRpc3BsYXkgPT09ICJub25lIiA/ICJpbmxpbmUtYmxvY2siIDogIm5vbmUiOwogICAgfSk7CiAgICB3cmFwLmFwcGVuZENoaWxkKHByaW1hcnkpOwogICAgd3JhcC5hcHBlbmRDaGlsZChjYXJldCk7CiAgICB3cmFwLmFwcGVuZENoaWxkKG1lbnUpOwogICAgcmV0dXJuIHdyYXA7CiAgfQoKICBmdW5jdGlvbiBkZWNvcmF0ZShidWJibGUpIHsKICAgIHRyeSB7CiAgICAgIHZhciByb2xlID0gY2xhc3NpZnlCdWJibGUoYnViYmxlKTsKICAgICAgaWYgKCFyb2xlKSByZXR1cm47CiAgICAgIGlmIChxcyhidWJibGUsICIuIiArIENPTlRST0xfUFJFRklYKSkgcmV0dXJuOyAvLyBhbHJlYWR5IGRlY29yYXRlZAogICAgICB2YXIgY29udHJvbCA9IGJ1aWxkQ29udHJvbCgKICAgICAgICBmdW5jdGlvbiAoaG9zdCkgeyBjb3B5VGV4dChidWJibGVNYXJrZG93bihidWJibGUsIHJvbGUpKS50aGVuKGZ1bmN0aW9uICgpIHsgZmxhc2hGZWVkYmFjayhjb250cm9sKTsgfSk7IH0sCiAgICAgICAgZnVuY3Rpb24gKGhvc3QpIHsgY29weVRleHQoYnViYmxlUGxhaW4oYnViYmxlLCByb2xlKSkudGhlbihmdW5jdGlvbiAoKSB7IGZsYXNoRmVlZGJhY2soY29udHJvbCk7IH0pOyB9CiAgICAgICk7CiAgICAgIGJ1YmJsZS5hcHBlbmRDaGlsZChjb250cm9sKTsKICAgIH0gY2F0Y2ggKF8pIHt9CiAgfQoKICBmdW5jdGlvbiBjb3B5Q29udmVyc2F0aW9uKGZvcm1hdCkgewogICAgdmFyIGJ1YmJsZXMgPSBxc2EoVVNFUl9CVUJCTEUgKyAiLCIgKyBBU1NJU1RBTlRfQlVCQkxFKTsKICAgIGlmIChmb3JtYXQgPT09ICJ0ZXh0IikgewogICAgICB2YXIgbGluZXMgPSBbXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBidWJibGVzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgdmFyIHJvbGUgPSBjbGFzc2lmeUJ1YmJsZShidWJibGVzW2ldKTsKICAgICAgICBpZiAoIXJvbGUpIGNvbnRpbnVlOwogICAgICAgIHZhciBib2R5ID0gYnViYmxlUGxhaW4oYnViYmxlc1tpXSwgcm9sZSk7CiAgICAgICAgaWYgKGJvZHkpIGxpbmVzLnB1c2goYm9keSk7CiAgICAgIH0KICAgICAgcmV0dXJuIGNvcHlUZXh0KGxpbmVzLmpvaW4oIlxuXG4iKSArIChsaW5lcy5sZW5ndGggPyAiXG4iIDogIiIpKTsKICAgIH0KICAgIHJldHVybiBjb3B5VGV4dChjb252ZXJzYXRpb25Ub01hcmtkb3duKGJ1YmJsZXMsIGZ1bmN0aW9uIChiKSB7CiAgICAgIHJldHVybiBjb250ZW50Tm9kZU9mKGIsIGNsYXNzaWZ5QnViYmxlKGIpKTsKICAgIH0pKTsKICB9CgogIGZ1bmN0aW9uIGluc3RhbGxDb252ZXJzYXRpb25Db250cm9sKCkgewogICAgdHJ5IHsKICAgICAgaWYgKHFzKGRvY3VtZW50LCAiLiIgKyBDT05UUk9MX1BSRUZJWCArICItY29udmVyc2F0aW9uIikpIHJldHVybjsKICAgICAgdmFyIGJhciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImRpdiIpOwogICAgICBiYXIuY2xhc3NOYW1lID0gQ09OVFJPTF9QUkVGSVggKyAiLWNvbnZlcnNhdGlvbiI7CiAgICAgIHZhciBjb250cm9sID0gYnVpbGRDb250cm9sKAogICAgICAgIGZ1bmN0aW9uICgpIHsgY29weUNvbnZlcnNhdGlvbigibWFya2Rvd24iKS50aGVuKGZ1bmN0aW9uICgpIHsgZmxhc2hGZWVkYmFjayhiYXIpOyB9KTsgfSwKICAgICAgICBmdW5jdGlvbiAoKSB7IGNvcHlDb252ZXJzYXRpb24oInRleHQiKS50aGVuKGZ1bmN0aW9uICgpIHsgZmxhc2hGZWVkYmFjayhiYXIpOyB9KTsgfQogICAgICApOwogICAgICBjb250cm9sLnRpdGxlID0gIkNvcHkgZW50aXJlIGNvbnZlcnNhdGlvbiI7CiAgICAgIGJhci5hcHBlbmRDaGlsZChjb250cm9sKTsKICAgICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChiYXIpOyAvLyBmaXhlZC1wb3NpdGlvbiB2aWEgQ1NTOyBwbGFjZW1lbnQgcmVmaW5lZCBpbiBUYXNrIDYKICAgIH0gY2F0Y2ggKF8pIHt9CiAgfQoKICBmdW5jdGlvbiBzd2VlcCgpIHsgdmFyIGIgPSBxc2EoVVNFUl9CVUJCTEUgKyAiLCIgKyBBU1NJU1RBTlRfQlVCQkxFKTsgZm9yICh2YXIgaSA9IDA7IGkgPCBiLmxlbmd0aDsgaSsrKSBkZWNvcmF0ZShiW2ldKTsgfQoKICBmdW5jdGlvbiBib290KCkgewogICAgdHJ5IHsKICAgICAgdmFyIHRhcmdldCA9IChNRVNTQUdFU19DT05UQUlORVIgJiYgcXMoZG9jdW1lbnQsIE1FU1NBR0VTX0NPTlRBSU5FUikpIHx8IGRvY3VtZW50LmJvZHk7CiAgICAgIHN3ZWVwKCk7CiAgICAgIGluc3RhbGxDb252ZXJzYXRpb25Db250cm9sKCk7CiAgICAgIGlmICh0eXBlb2YgTXV0YXRpb25PYnNlcnZlciA9PT0gInVuZGVmaW5lZCIpIHJldHVybjsKICAgICAgdmFyIG9icyA9IG5ldyBNdXRhdGlvbk9ic2VydmVyKGZ1bmN0aW9uICgpIHsgc3dlZXAoKTsgfSk7CiAgICAgIG9icy5vYnNlcnZlKHRhcmdldCwgeyBjaGlsZExpc3Q6IHRydWUsIHN1YnRyZWU6IHRydWUgfSk7CiAgICB9IGNhdGNoIChfKSB7fQogIH0KfSkoKTsK").decode("utf-8") -INJECT_CSS = base64.b64decode("LmNjLW1kLWNvcHkgewogIGRpc3BsYXk6IGlubGluZS1mbGV4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgZ2FwOiAycHg7CiAgdmVydGljYWwtYWxpZ246IG1pZGRsZTsKICBtYXJnaW4tbGVmdDogNnB4Owp9Ci5jYy1tZC1jb3B5LWJ0biwKLmNjLW1kLWNvcHktY2FyZXQgewogIGZvbnQ6IGluaGVyaXQ7CiAgZm9udC1zaXplOiAxMXB4OwogIGxpbmUtaGVpZ2h0OiAxLjQ7CiAgcGFkZGluZzogMXB4IDZweDsKICBjb2xvcjogdmFyKC0tdnNjb2RlLWZvcmVncm91bmQpOwogIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50OwogIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLXZzY29kZS13aWRnZXQtYm9yZGVyLCB0cmFuc3BhcmVudCk7CiAgYm9yZGVyLXJhZGl1czogNHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBvcGFjaXR5OiAwLjY1Owp9Ci5jYy1tZC1jb3B5LWJ0bjpob3ZlciwKLmNjLW1kLWNvcHktY2FyZXQ6aG92ZXIgewogIG9wYWNpdHk6IDE7CiAgYmFja2dyb3VuZDogdmFyKC0tdnNjb2RlLXRvb2xiYXItaG92ZXJCYWNrZ3JvdW5kLCByZ2JhKDEyOCwgMTI4LCAxMjgsIDAuMTUpKTsKfQouY2MtbWQtY29weS1tZW51IHsKICBwb3NpdGlvbjogcmVsYXRpdmU7CiAgbWFyZ2luLWxlZnQ6IDRweDsKICBwYWRkaW5nOiAycHg7CiAgYmFja2dyb3VuZDogdmFyKC0tdnNjb2RlLW1lbnUtYmFja2dyb3VuZCwgdmFyKC0tdnNjb2RlLWVkaXRvcldpZGdldC1iYWNrZ3JvdW5kKSk7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tdnNjb2RlLW1lbnUtYm9yZGVyLCB2YXIoLS12c2NvZGUtd2lkZ2V0LWJvcmRlciwgdHJhbnNwYXJlbnQpKTsKICBib3JkZXItcmFkaXVzOiA0cHg7CiAgei1pbmRleDogNTsKfQouY2MtbWQtY29weS1mZWVkYmFjayB7CiAgbWFyZ2luLWxlZnQ6IDZweDsKICBmb250LXNpemU6IDExcHg7CiAgb3BhY2l0eTogMC44NTsKICBjb2xvcjogdmFyKC0tdnNjb2RlLWZvcmVncm91bmQpOwp9Ci5jYy1tZC1jb3B5LWNvbnZlcnNhdGlvbiB7CiAgcG9zaXRpb246IGZpeGVkOwogIHJpZ2h0OiAxNnB4OwogIGJvdHRvbTogNTZweDsKICB6LWluZGV4OiAxMDsKICBwYWRkaW5nOiAycHg7CiAgYmFja2dyb3VuZDogdmFyKC0tdnNjb2RlLWVkaXRvcldpZGdldC1iYWNrZ3JvdW5kKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS12c2NvZGUtd2lkZ2V0LWJvcmRlciwgdHJhbnNwYXJlbnQpOwogIGJvcmRlci1yYWRpdXM6IDZweDsKICBvcGFjaXR5OiAwLjg1Owp9Ci5jYy1tZC1jb3B5LWNvbnZlcnNhdGlvbjpob3ZlciB7CiAgb3BhY2l0eTogMTsKfQo=").decode("utf-8") +INJECT_JS = base64.b64decode("LyogY2MtbWQtY29weTogcGVyLW1lc3NhZ2UgYW5kIHdob2xlLWNvbnZlcnNhdGlvbiBjb3B5IChNYXJrZG93bikgZm9yIHRoZQogKiBDbGF1ZGUgQ29kZSBWUyBDb2RlIHdlYnZpZXcuIFNlbGYtY29udGFpbmVkIElJRkUgYXBwZW5kZWQgdG8gd2Vidmlldy9pbmRleC5qcy4KICogRWFjaCBjb250cm9sIGlzIGEgc2luZ2xlIGNsaXBib2FyZCBpY29uIHRoYXQgZmxpcHMgdG8gYSBjaGVja21hcmsgZm9yIH4ycyB3aGVuIGEKICogY29weSBhY3R1YWxseSBzdWNjZWVkcyAobm8gdGV4dCBsYWJlbCwgbm8gbWVudSkuIEFkZGl0aXZlIGFuZCByZWFkLW9ubHkgdy5yLnQuCiAqIGFwcCBzdGF0ZTsga2V5ZWQgb24gc3RhYmxlIENTUy1tb2R1bGUgY2xhc3MgcHJlZml4ZXMsIHNvIGl0IGZhaWxzIHNhZmUgKGNvbnRyb2xzCiAqIHNpbXBseSBkbyBub3QgYXBwZWFyKSBpZiBhIHByZWZpeCBtb3Zlcy4KICogRXhwb3NlcyBpdHMgcHVyZSBmdW5jdGlvbnMgZm9yIG5vZGUgdW5pdCB0ZXN0czsgYm9vdCgpcyBvbmx5IGluIGEgcmVhbCB3ZWJ2aWV3LiAqLwovKiBMZWFkaW5nICc7JyBzbyB0aGF0LCBhcHBlbmRlZCBhZnRlciB0aGUgYnVuZGxlLCB0aGlzIElJRkUgY2FuIG5ldmVyIGJlIHBhcnNlZCBhcwogKiBhIGNhbGwgb24gdGhlIGJ1bmRsZSdzIGZpbmFsIGV4cHJlc3Npb24gaWYgaXQgbGFja3MgYSB0cmFpbGluZyBzZW1pY29sb24gKEFTSQogKiBzYWZldHkgYWNyb3NzIGV4dGVuc2lvbiBidWlsZHMpLiAqLwo7KGZ1bmN0aW9uICgpIHsKICAidXNlIHN0cmljdCI7CgogIHZhciBDT05UUk9MX1BSRUZJWCA9ICJjYy1tZC1jb3B5IjsgLy8gZXZlcnkgaW5qZWN0ZWQgbm9kZSdzIGNsYXNzIHN0YXJ0cyB3aXRoIHRoaXMKICB2YXIgVVNFUl9CVUJCTEUgPSAnW2NsYXNzKj0idXNlck1lc3NhZ2VDb250YWluZXJfIl0nOwogIC8vIEFzc2lzdGFudCBtZXNzYWdlIHdyYXBwZXIuIFZlcmlmaWVkIG9uIDIuMS4xNzA6IHRoZSByZW5kZXIgZW1pdHMgZXhhY3RseSBvbmUKICAvLyBgZGF0YS10ZXN0aWQ9ImFzc2lzdGFudC1tZXNzYWdlImAgZGl2IHBlciBhc3Npc3RhbnQgdHVybiwgd2l0aCB0aGUgcmF0aW5nCiAgLy8gd2lkZ2V0IGFuZCBjb250ZW50IGJsb2NrcyBhcyBpdHMgY2hpbGRyZW4uIChUaGUgZWFybGllciBgW2RhdGEtbWVzc2FnZS1yYXRpbmddYAogIC8vIHdhcyBXUk9ORzogdGhhdCBhdHRyaWJ1dGUgc2l0cyBvbiB0aGUgbmVzdGVkIHJhdGluZyBjb250cm9sLCB3aGljaCBpcyBhbHNvIG9ubHkKICAvLyByZW5kZXJlZCBiZWhpbmQgYW4gZXhwZXJpbWVudCthbmFseXRpY3MgZ2F0ZS4pIFJlLXBpbm5lZCBpbiBUYXNrIDYuCiAgdmFyIEFTU0lTVEFOVF9CVUJCTEUgPSAnW2RhdGEtdGVzdGlkPSJhc3Npc3RhbnQtbWVzc2FnZSJdJzsKICB2YXIgTUVTU0FHRVNfQ09OVEFJTkVSID0gJ1tjbGFzcyo9Im1lc3NhZ2VzQ29udGFpbmVyXyJdJzsgLy8gZS5nLiAnW2NsYXNzKj0idGltZWxpbmVfIl0nOyAiIiAtPiBvYnNlcnZlIGRvY3VtZW50LmJvZHkKICAvLyBPcHRpb25hbCBuYXJyb3dpbmcgb25seS4gTVVTVCBiZSBhIHNpbmdsZSB3cmFwcGVyIGFyb3VuZCBBTEwgY29udGVudCBibG9ja3MsCiAgLy8gbm90IGEgcGVyLWJsb2NrIGNsYXNzIChhIHR1cm4gaGFzIG11bHRpcGxlIGJsb2NrcykuICIiIC0+IHVzZSB0aGUgYnViYmxlIGl0c2VsZgogIC8vIChhbHJlYWR5IGFnZ3JlZ2F0ZXMgYWxsIGJsb2Nrczsgc2FuaXRpemVDbG9uZSBpcyB0aGUgY29ycmVjdG5lc3MgZ2F0ZSkuCiAgdmFyIEFTU0lTVEFOVF9DT05URU5UID0gIiI7CiAgdmFyIEZFRURCQUNLX01TID0gMjAwMDsgLy8gaG93IGxvbmcgdGhlIGNoZWNrbWFyayBzaG93cyBhZnRlciBhIHN1Y2Nlc3NmdWwgY29weQoKICAvLyAtLS0tIEhUTUwgLT4gTWFya2Rvd24gKERPTSB3YWxrKSAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiAgLy8gVXNlcyBvbmx5OiBub2RlVHlwZSwgdGFnTmFtZSwgY2hpbGROb2RlcywgdGV4dENvbnRlbnQsIGdldEF0dHJpYnV0ZSwgY2xhc3NOYW1lLgogIGZ1bmN0aW9uIGh0bWxUb01hcmtkb3duKHJvb3QpIHsKICAgIC8vIExvbmdlc3QgcnVuIG9mIGNvbnNlY3V0aXZlIGJhY2t0aWNrcyBpbiBzLCBzbyBhIGNvZGUgZGVsaW1pdGVyL2ZlbmNlIGNhbiBiZQogICAgLy8gY2hvc2VuIGxvbmdlciB0aGFuIGFueXRoaW5nIGluc2lkZSBpdCAoZWxzZSBgYGAgaW4gdGhlIGNvbnRlbnQgY2xvc2VzIGVhcmx5KS4KICAgIGZ1bmN0aW9uIGJhY2t0aWNrUnVuKHMpIHsKICAgICAgdmFyIG1heCA9IDAsIGN1ciA9IDA7CiAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgcy5sZW5ndGg7IGkrKykgewogICAgICAgIGlmIChzLmNoYXJBdChpKSA9PT0gImAiKSB7IGN1cisrOyBpZiAoY3VyID4gbWF4KSBtYXggPSBjdXI7IH0gZWxzZSBjdXIgPSAwOwogICAgICB9CiAgICAgIHJldHVybiBtYXg7CiAgICB9CiAgICBmdW5jdGlvbiBmZW5jZShzLCBtaW4pIHsgdmFyIG4gPSBiYWNrdGlja1J1bihzKSArIDE7IGlmIChuIDwgbWluKSBuID0gbWluOyByZXR1cm4gbmV3IEFycmF5KG4gKyAxKS5qb2luKCJgIik7IH0KICAgIGZ1bmN0aW9uIGlubGluZShub2RlKSB7CiAgICAgIHZhciBvdXQgPSAiIjsKICAgICAgdmFyIGtpZHMgPSBub2RlLmNoaWxkTm9kZXMgfHwgW107CiAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwga2lkcy5sZW5ndGg7IGkrKykgewogICAgICAgIHZhciBjID0ga2lkc1tpXTsKICAgICAgICBpZiAoYy5ub2RlVHlwZSA9PT0gMykgeyBvdXQgKz0gYy50ZXh0Q29udGVudCB8fCAiIjsgY29udGludWU7IH0KICAgICAgICBpZiAoYy5ub2RlVHlwZSAhPT0gMSkgY29udGludWU7CiAgICAgICAgdmFyIHRhZyA9IChjLnRhZ05hbWUgfHwgIiIpLnRvVXBwZXJDYXNlKCk7CiAgICAgICAgaWYgKHRhZyA9PT0gIkJSIikgb3V0ICs9ICJcbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiU1RST05HIiB8fCB0YWcgPT09ICJCIikgb3V0ICs9ICIqKiIgKyBpbmxpbmUoYykgKyAiKioiOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIkVNIiB8fCB0YWcgPT09ICJJIikgb3V0ICs9ICIqIiArIGlubGluZShjKSArICIqIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJERUwiIHx8IHRhZyA9PT0gIlMiKSBvdXQgKz0gIn5+IiArIGlubGluZShjKSArICJ+fiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiQ09ERSIpIHsKICAgICAgICAgIHZhciBjdCA9IGMudGV4dENvbnRlbnQgfHwgIiI7CiAgICAgICAgICB2YXIgZCA9IGZlbmNlKGN0LCAxKTsKICAgICAgICAgIC8vIENvbW1vbk1hcmsgc3RyaXBzIG9uZSBsZWFkaW5nK3RyYWlsaW5nIHNwYWNlLCBzbyBwYWQgd2hlbiBhbiBlZGdlIGlzIGEKICAgICAgICAgIC8vIGJhY2t0aWNrIHRvIGtlZXAgaXQgZnJvbSBtZXJnaW5nIHdpdGggdGhlIGRlbGltaXRlci4KICAgICAgICAgIHZhciBwID0gKGN0LmNoYXJBdCgwKSA9PT0gImAiIHx8IGN0LmNoYXJBdChjdC5sZW5ndGggLSAxKSA9PT0gImAiKSA/ICIgIiA6ICIiOwogICAgICAgICAgb3V0ICs9IGQgKyBwICsgY3QgKyBwICsgZDsKICAgICAgICB9CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiQSIpIHsKICAgICAgICAgIHZhciBocmVmID0gYy5nZXRBdHRyaWJ1dGUgPyBjLmdldEF0dHJpYnV0ZSgiaHJlZiIpIDogbnVsbDsKICAgICAgICAgIHZhciB0ID0gaW5saW5lKGMpOwogICAgICAgICAgb3V0ICs9IGhyZWYgPyAiWyIgKyB0ICsgIl0oIiArIGhyZWYgKyAiKSIgOiB0OwogICAgICAgIH0gZWxzZSBvdXQgKz0gaW5saW5lKGMpOyAvLyB1bmtub3duIGlubGluZSB3cmFwcGVyOiBrZWVwIHRleHQsIGRyb3AgdGFnCiAgICAgIH0KICAgICAgcmV0dXJuIG91dDsKICAgIH0KICAgIGZ1bmN0aW9uIGxhbmdPZihjb2RlRWwpIHsKICAgICAgdmFyIGNscyA9ICIiOwogICAgICBpZiAoY29kZUVsKSBjbHMgPSAoY29kZUVsLmdldEF0dHJpYnV0ZSAmJiBjb2RlRWwuZ2V0QXR0cmlidXRlKCJjbGFzcyIpKSB8fCBjb2RlRWwuY2xhc3NOYW1lIHx8ICIiOwogICAgICB2YXIgbSA9IC9sYW5ndWFnZS0oW0EtWmEtejAtOSsjLlwtXSspLy5leGVjKGNscyB8fCAiIik7CiAgICAgIHJldHVybiBtID8gbVsxXSA6ICIiOwogICAgfQogICAgZnVuY3Rpb24gZmluZENoaWxkVGFnKG5vZGUsIHRhZykgewogICAgICB2YXIga2lkcyA9IG5vZGUuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBraWRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgaWYgKGtpZHNbaV0ubm9kZVR5cGUgPT09IDEgJiYgKGtpZHNbaV0udGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKSA9PT0gdGFnKSByZXR1cm4ga2lkc1tpXTsKICAgICAgfQogICAgICByZXR1cm4gbnVsbDsKICAgIH0KICAgIGZ1bmN0aW9uIGxpc3Qobm9kZSwgb3JkZXJlZCwgZGVwdGgpIHsKICAgICAgdmFyIG91dCA9ICIiLCBuID0gMTsKICAgICAgdmFyIGtpZHMgPSBub2RlLmNoaWxkTm9kZXMgfHwgW107CiAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwga2lkcy5sZW5ndGg7IGkrKykgewogICAgICAgIHZhciBsaSA9IGtpZHNbaV07CiAgICAgICAgaWYgKGxpLm5vZGVUeXBlICE9PSAxIHx8IChsaS50YWdOYW1lIHx8ICIiKS50b1VwcGVyQ2FzZSgpICE9PSAiTEkiKSBjb250aW51ZTsKICAgICAgICB2YXIgbWFya2VyID0gb3JkZXJlZCA/IG4rKyArICIuICIgOiAiLSAiOwogICAgICAgIHZhciBpbmRlbnQgPSBuZXcgQXJyYXkoZGVwdGggKyAxKS5qb2luKCIgICIpOwogICAgICAgIHZhciBsZWFkID0gIiIsIG5lc3RlZCA9ICIiOwogICAgICAgIHZhciBsayA9IGxpLmNoaWxkTm9kZXMgfHwgW107CiAgICAgICAgZm9yICh2YXIgaiA9IDA7IGogPCBsay5sZW5ndGg7IGorKykgewogICAgICAgICAgdmFyIGNoID0gbGtbal07CiAgICAgICAgICB2YXIgY3QgPSBjaC5ub2RlVHlwZSA9PT0gMSA/IChjaC50YWdOYW1lIHx8ICIiKS50b1VwcGVyQ2FzZSgpIDogIiI7CiAgICAgICAgICBpZiAoY3QgPT09ICJVTCIpIG5lc3RlZCArPSBsaXN0KGNoLCBmYWxzZSwgZGVwdGggKyAxKTsKICAgICAgICAgIGVsc2UgaWYgKGN0ID09PSAiT0wiKSBuZXN0ZWQgKz0gbGlzdChjaCwgdHJ1ZSwgZGVwdGggKyAxKTsKICAgICAgICAgIGVsc2UgaWYgKGNoLm5vZGVUeXBlID09PSAzKSBsZWFkICs9IGNoLnRleHRDb250ZW50IHx8ICIiOwogICAgICAgICAgZWxzZSBsZWFkICs9IGlubGluZShjaCk7CiAgICAgICAgfQogICAgICAgIG91dCArPSBpbmRlbnQgKyBtYXJrZXIgKyBsZWFkLnRyaW0oKSArICJcbiIgKyBuZXN0ZWQ7CiAgICAgIH0KICAgICAgcmV0dXJuIG91dDsKICAgIH0KICAgIGZ1bmN0aW9uIHRhYmxlKG5vZGUpIHsKICAgICAgdmFyIHJvd3MgPSBbXTsKICAgICAgKGZ1bmN0aW9uIGNvbGxlY3QoY29udGFpbmVyKSB7CiAgICAgICAgdmFyIGtpZHMgPSBjb250YWluZXIuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIHsKICAgICAgICAgIHZhciBjID0ga2lkc1tpXTsKICAgICAgICAgIGlmIChjLm5vZGVUeXBlICE9PSAxKSBjb250aW51ZTsKICAgICAgICAgIHZhciB0ID0gKGMudGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKTsKICAgICAgICAgIGlmICh0ID09PSAiVEhFQUQiIHx8IHQgPT09ICJUQk9EWSIgfHwgdCA9PT0gIlRGT09UIikgY29sbGVjdChjKTsKICAgICAgICAgIGVsc2UgaWYgKHQgPT09ICJUUiIpIHsKICAgICAgICAgICAgdmFyIGNlbGxzID0gW10sIGNjID0gYy5jaGlsZE5vZGVzIHx8IFtdOwogICAgICAgICAgICBmb3IgKHZhciBqID0gMDsgaiA8IGNjLmxlbmd0aDsgaisrKSB7CiAgICAgICAgICAgICAgdmFyIGQgPSBjY1tqXTsKICAgICAgICAgICAgICBpZiAoZC5ub2RlVHlwZSAhPT0gMSkgY29udGludWU7CiAgICAgICAgICAgICAgdmFyIGR0ID0gKGQudGFnTmFtZSB8fCAiIikudG9VcHBlckNhc2UoKTsKICAgICAgICAgICAgICBpZiAoZHQgPT09ICJUSCIgfHwgZHQgPT09ICJURCIpIGNlbGxzLnB1c2goaW5saW5lKGQpLnRyaW0oKSk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgcm93cy5wdXNoKGNlbGxzKTsKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0pKG5vZGUpOwogICAgICBpZiAoIXJvd3MubGVuZ3RoKSByZXR1cm4gIiI7CiAgICAgIHZhciBoZWFkID0gcm93c1swXSwgYm9keSA9IHJvd3Muc2xpY2UoMSk7CiAgICAgIHZhciBzZXAgPSBoZWFkLm1hcChmdW5jdGlvbiAoKSB7IHJldHVybiAiLS0tIjsgfSk7CiAgICAgIHZhciBvdXQgPSAifCAiICsgaGVhZC5qb2luKCIgfCAiKSArICIgfFxufCAiICsgc2VwLmpvaW4oIiB8ICIpICsgIiB8XG4iOwogICAgICBmb3IgKHZhciBrID0gMDsgayA8IGJvZHkubGVuZ3RoOyBrKyspIG91dCArPSAifCAiICsgYm9keVtrXS5qb2luKCIgfCAiKSArICIgfFxuIjsKICAgICAgcmV0dXJuIG91dDsKICAgIH0KICAgIGZ1bmN0aW9uIGJsb2NrKG5vZGUpIHsKICAgICAgdmFyIG91dCA9ICIiOwogICAgICB2YXIga2lkcyA9IG5vZGUuY2hpbGROb2RlcyB8fCBbXTsKICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBraWRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgdmFyIGMgPSBraWRzW2ldOwogICAgICAgIGlmIChjLm5vZGVUeXBlID09PSAzKSB7IGlmICgoYy50ZXh0Q29udGVudCB8fCAiIikudHJpbSgpKSBvdXQgKz0gYy50ZXh0Q29udGVudDsgY29udGludWU7IH0KICAgICAgICBpZiAoYy5ub2RlVHlwZSAhPT0gMSkgY29udGludWU7CiAgICAgICAgdmFyIHRhZyA9IChjLnRhZ05hbWUgfHwgIiIpLnRvVXBwZXJDYXNlKCk7CiAgICAgICAgaWYgKC9eSFsxLTZdJC8udGVzdCh0YWcpKSBvdXQgKz0gbmV3IEFycmF5KCt0YWdbMV0gKyAxKS5qb2luKCIjIikgKyAiICIgKyBpbmxpbmUoYykudHJpbSgpICsgIlxuXG4iOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIlAiKSBvdXQgKz0gaW5saW5lKGMpLnRyaW0oKSArICJcblxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJVTCIpIG91dCArPSBsaXN0KGMsIGZhbHNlLCAwKSArICJcbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiT0wiKSBvdXQgKz0gbGlzdChjLCB0cnVlLCAwKSArICJcbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiUFJFIikgewogICAgICAgICAgdmFyIGNvZGUgPSBmaW5kQ2hpbGRUYWcoYywgIkNPREUiKTsKICAgICAgICAgIHZhciBsYW5nID0gbGFuZ09mKGNvZGUgfHwgYyk7CiAgICAgICAgICB2YXIgYm9keSA9IChjb2RlIHx8IGMpLnRleHRDb250ZW50IHx8ICIiOwogICAgICAgICAgdmFyIGYgPSBmZW5jZShib2R5LCAzKTsKICAgICAgICAgIG91dCArPSBmICsgbGFuZyArICJcbiIgKyBib2R5LnJlcGxhY2UoL1xuJC8sICIiKSArICJcbiIgKyBmICsgIlxuXG4iOwogICAgICAgIH0gZWxzZSBpZiAodGFnID09PSAiQkxPQ0tRVU9URSIpIHsKICAgICAgICAgIHZhciBpbm5lciA9IGJsb2NrKGMpLnRyaW0oKS5zcGxpdCgiXG4iKS5tYXAoZnVuY3Rpb24gKGwpIHsgcmV0dXJuICI+ICIgKyBsOyB9KS5qb2luKCJcbiIpOwogICAgICAgICAgb3V0ICs9IGlubmVyICsgIlxuXG4iOwogICAgICAgIH0gZWxzZSBpZiAodGFnID09PSAiREVUQUlMUyIpIG91dCArPSBibG9jayhjKS50cmltKCkgKyAiXG5cbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiU1VNTUFSWSIpIG91dCArPSBpbmxpbmUoYykudHJpbSgpICsgIlxuXG4iOwogICAgICAgIGVsc2UgaWYgKHRhZyA9PT0gIkhSIikgb3V0ICs9ICItLS1cblxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJUQUJMRSIpIG91dCArPSB0YWJsZShjKSArICJcbiI7CiAgICAgICAgZWxzZSBpZiAodGFnID09PSAiQlIiKSBvdXQgKz0gIlxuIjsKICAgICAgICBlbHNlIGlmICh0YWcgPT09ICJTVFJPTkciIHx8IHRhZyA9PT0gIkIiIHx8IHRhZyA9PT0gIkVNIiB8fCB0YWcgPT09ICJJIiB8fAogICAgICAgICAgICAgICAgIHRhZyA9PT0gIkEiIHx8IHRhZyA9PT0gIkNPREUiIHx8IHRhZyA9PT0gIkRFTCIgfHwgdGFnID09PSAiUyIpCiAgICAgICAgICBvdXQgKz0gaW5saW5lKGMpICsgIlxuXG4iOwogICAgICAgIGVsc2Ugb3V0ICs9IGJsb2NrKGMpOyAvLyB1bmtub3duIHdyYXBwZXI6IHJlY3Vyc2UgKGRyb3AgdGFnLCBrZWVwIGNvbnRlbnQpCiAgICAgIH0KICAgICAgcmV0dXJuIG91dDsKICAgIH0KICAgIC8vIGJsb2NrKCkgZGlzcGF0Y2hlcyBvbiBlYWNoIENISUxEJ3MgdGFnLCB0cmVhdGluZyB0aGUgcGFzc2VkIG5vZGUgYXMgYSBwbGFpbgogICAgLy8gY29udGFpbmVyLiBXcmFwIHJvb3QgaW4gYSBvbmUtb2ZmIGNvbnRhaW5lciBzbyByb290J3MgT1dOIHRhZyBpcyBkaXNwYXRjaGVkCiAgICAvLyB0b286IGNhbGxlcnMgcGFzcyBlaXRoZXIgdGhlIGJ1YmJsZSBjb250YWluZXIgKGl0cyBibG9jayBjaGlsZHJlbiByZW5kZXIpIG9yCiAgICAvLyBhIHNpbmdsZSBibG9jayBlbGVtZW50IGxpa2UgPHByZT4vPHVsPi88dGFibGU+IChub3cgaGFuZGxlZCwgbm90IGZsYXR0ZW5lZCkuCiAgICByZXR1cm4gYmxvY2soeyBjaGlsZE5vZGVzOiBbcm9vdF0gfSkucmVwbGFjZSgvXG57Myx9L2csICJcblxuIikudHJpbSgpOwogIH0KCiAgLy8gLS0tLSBwdXJlIGhlbHBlcnMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQogIGZ1bmN0aW9uIGhhc1ByZWZpeChub2RlLCBwcmVmaXgpIHsKICAgIGlmIChub2RlLm5vZGVUeXBlICE9PSAxIHx8IHR5cGVvZiBub2RlLmNsYXNzTmFtZSAhPT0gInN0cmluZyIpIHJldHVybiBmYWxzZTsKICAgIHZhciBwYXJ0cyA9IG5vZGUuY2xhc3NOYW1lLnNwbGl0KC9ccysvKTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgcGFydHMubGVuZ3RoOyBpKyspIGlmIChwYXJ0c1tpXS5pbmRleE9mKHByZWZpeCkgPT09IDApIHJldHVybiB0cnVlOwogICAgcmV0dXJuIGZhbHNlOwogIH0KCiAgLy8gQ2xhc3MtcHJlZml4IGhvb2tzIGZvciBub24tY29udGVudCBjaHJvbWUgdGhhdCByZW5kZXJzICppbnNpZGUqIGFuIGFzc2lzdGFudAogIC8vIGJ1YmJsZSAodmVyaWZpZWQgb24gMi4xLjE3MDsgVGFzayA2IHJlLXBpbnMgdGhlc2UpLiBUb29sIGJsb2NrcyBhcmUgZXhjbHVkZWQKICAvLyBmcm9tIG1lc3NhZ2UgY29weTsgdGhpbmtpbmcgc3VtbWFyaWVzIGFyZSB2aXNpYmxlIGNvbnRlbnQgYW5kIG11c3QgcmVtYWluCiAgLy8gY29weWFibGUuIHVua25vd25Db250ZW50XyBpcyB0aGUgcmVuZGVyZXIncyBmYWxsYmFjayBmb3IgdW5yZWNvZ25pemVkIGJsb2NrCiAgLy8gdHlwZXMsIHNvIHN0cmlwcGluZyBpdCBtYWtlcyBhICpmdXR1cmUqIGJsb2NrIHR5cGUgZmFpbCBzYWZlIHRvIGV4Y2x1ZGVkIHJhdGhlcgogIC8vIHRoYW4gbGVha2luZyAiVW5zdXBwb3J0ZWQgY29udGVudCIgaW50byB0aGUgY29weS4gUmUtcGluIGlmIGEgcHJlZml4IG1vdmVzLgogIHZhciBDSFJPTUVfUFJFRklYRVMgPSBbInRvb2xVc2VfIiwgInRvb2xSZXN1bHRfIiwgInRvb2xSZWZlcmVuY2VfIiwgInVua25vd25Db250ZW50XyJdOwoKICAvLyBUcnVlIGZvciBhbnkgbm9kZSB0aGF0IG11c3QgbmV2ZXIgYXBwZWFyIGluIGNvcGllZCBvdXRwdXQ6IG91ciBvd24gY29udHJvbHMsCiAgLy8gdGhlIHJhdGluZyB3aWRnZXQgKGBkYXRhLW1lc3NhZ2UtcmF0aW5nYCArIGl0cyAiVGhhbmtzIGZvciB5b3VyIGZlZWRiYWNrIgogIC8vIHRleHQpLCBhbnkgYnV0dG9uIChjb3B5LWNvZGUgY2hyb21lKSwgYW5kIHRoZSBleGNsdWRlZCBjb250ZW50IGJsb2NrcyBhYm92ZS4KICBmdW5jdGlvbiBpc0Nocm9tZShub2RlKSB7CiAgICBpZiAobm9kZS5ub2RlVHlwZSAhPT0gMSkgcmV0dXJuIGZhbHNlOwogICAgaWYgKChub2RlLnRhZ05hbWUgfHwgIiIpLnRvVXBwZXJDYXNlKCkgPT09ICJCVVRUT04iKSByZXR1cm4gdHJ1ZTsKICAgIGlmIChub2RlLmdldEF0dHJpYnV0ZSAmJiBub2RlLmdldEF0dHJpYnV0ZSgiZGF0YS1tZXNzYWdlLXJhdGluZyIpICE9PSBudWxsKSByZXR1cm4gdHJ1ZTsKICAgIGlmIChoYXNQcmVmaXgobm9kZSwgQ09OVFJPTF9QUkVGSVgpKSByZXR1cm4gdHJ1ZTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgQ0hST01FX1BSRUZJWEVTLmxlbmd0aDsgaSsrKSBpZiAoaGFzUHJlZml4KG5vZGUsIENIUk9NRV9QUkVGSVhFU1tpXSkpIHJldHVybiB0cnVlOwogICAgcmV0dXJuIGZhbHNlOwogIH0KCiAgLy8gRGVlcC1jbG9uZSBgY29udGVudE5vZGVgLCB0aGVuIHN0cmlwIGV2ZXJ5IGNocm9tZSBub2RlIHNvIGNvcGllZCBvdXRwdXQgaXMgdGhlCiAgLy8gbWVzc2FnZSdzIHRleHQgY29udGVudCBvbmx5LiBUaGlzIGlzIGEgQ09SUkVDVE5FU1MgR0FURSwgbm90IGNvc21ldGljOiB0aGUKICAvLyBkZWZhdWx0IGNvbnRlbnQgbm9kZSBpcyB0aGUgd2hvbGUgYnViYmxlIChhbGwgY29udGVudC1ibG9jayBzaWJsaW5ncywgc28gbXVsdGktCiAgLy8gYmxvY2sgYXNzaXN0YW50IHR1cm5zIGFyZSBjYXB0dXJlZCksIGFuZCB0aGlzIHN0cmlwLWxpc3QgaXMgdGhlIG9ubHkgdGhpbmcKICAvLyBrZWVwaW5nIHRoZSByYXRpbmcgd2lkZ2V0IGFuZCBleGNsdWRlZCB0b29sL2ZhbGxiYWNrIGJsb2NrcyBvdXQgb2YgdGhlIGNvcHkuCiAgZnVuY3Rpb24gc2FuaXRpemVDbG9uZShjb250ZW50Tm9kZSkgewogICAgdmFyIGNsb25lID0gY29udGVudE5vZGUuY2xvbmVOb2RlKHRydWUpOwogICAgKGZ1bmN0aW9uIHN0cmlwKG5vZGUpIHsKICAgICAgdmFyIGtpZHMgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChub2RlLmNoaWxkTm9kZXMgfHwgW10pOwogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIHsKICAgICAgICB2YXIgYyA9IGtpZHNbaV07CiAgICAgICAgaWYgKGMubm9kZVR5cGUgPT09IDEgJiYgaXNDaHJvbWUoYykpIHsgbm9kZS5yZW1vdmVDaGlsZChjKTsgY29udGludWU7IH0KICAgICAgICBpZiAoYy5ub2RlVHlwZSA9PT0gMSkgc3RyaXAoYyk7CiAgICAgIH0KICAgIH0pKGNsb25lKTsKICAgIHJldHVybiBjbG9uZTsKICB9CgogIGZ1bmN0aW9uIGhhc0NvcHlhYmxlQ29udGVudChjb250ZW50Tm9kZSwgcm9sZSkgewogICAgZnVuY3Rpb24gd2Fsayhub2RlKSB7CiAgICAgIGlmICghbm9kZSkgcmV0dXJuIGZhbHNlOwogICAgICBpZiAobm9kZS5ub2RlVHlwZSA9PT0gMykgcmV0dXJuICEhKG5vZGUudGV4dENvbnRlbnQgfHwgIiIpLnRyaW0oKTsKICAgICAgaWYgKG5vZGUubm9kZVR5cGUgIT09IDEpIHJldHVybiBmYWxzZTsKICAgICAgaWYgKGlzQ2hyb21lKG5vZGUpKSByZXR1cm4gZmFsc2U7CiAgICAgIHZhciBraWRzID0gbm9kZS5jaGlsZE5vZGVzIHx8IFtdOwogICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGtpZHMubGVuZ3RoOyBpKyspIGlmICh3YWxrKGtpZHNbaV0pKSByZXR1cm4gdHJ1ZTsKICAgICAgcmV0dXJuIGZhbHNlOwogICAgfQogICAgcmV0dXJuIHdhbGsoY29udGVudE5vZGUpOwogIH0KCiAgZnVuY3Rpb24gY2xhc3NpZnlCdWJibGUobm9kZSkgewogICAgaWYgKG5vZGUubm9kZVR5cGUgIT09IDEpIHJldHVybiBudWxsOwogICAgaWYgKGhhc1ByZWZpeChub2RlLCAidXNlck1lc3NhZ2VDb250YWluZXJfIikpIHJldHVybiAidXNlciI7CiAgICBpZiAobm9kZS5nZXRBdHRyaWJ1dGUgJiYgbm9kZS5nZXRBdHRyaWJ1dGUoImRhdGEtdGVzdGlkIikgPT09ICJhc3Npc3RhbnQtbWVzc2FnZSIpIHJldHVybiAiYXNzaXN0YW50IjsKICAgIHJldHVybiBudWxsOwogIH0KCiAgLy8gQnVpbGQgdGhlIHdob2xlLWNvbnZlcnNhdGlvbiBtYXJrZG93biBmcm9tIGFuIG9yZGVyZWQgbGlzdCBvZiBidWJibGVzLgogIC8vIGBjb250ZW50T2YoYnViYmxlKWAgcmVzb2x2ZXMgdGhlIGNvbnRlbnQgbm9kZSAoZGVmYXVsdDogdGhlIGJ1YmJsZSBpdHNlbGYsIHNvCiAgLy8gZXZlcnkgY29udGVudCBibG9jayBpcyBpbmNsdWRlZDsgc2FuaXRpemVDbG9uZSBkcm9wcyBjaHJvbWUpOyBhIGRlZmF1bHQgaXMKICAvLyBwcm92aWRlZCBmb3IgdGVzdHMuCiAgZnVuY3Rpb24gY29udmVyc2F0aW9uVG9NYXJrZG93bihidWJibGVzLCBjb250ZW50T2YpIHsKICAgIGNvbnRlbnRPZiA9IGNvbnRlbnRPZiB8fCBmdW5jdGlvbiAoYikgeyByZXR1cm4gYjsgfTsKICAgIHZhciBwYXJ0cyA9IFtdOwogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBidWJibGVzLmxlbmd0aDsgaSsrKSB7CiAgICAgIHZhciByb2xlID0gY2xhc3NpZnlCdWJibGUoYnViYmxlc1tpXSk7CiAgICAgIGlmICghcm9sZSkgY29udGludWU7CiAgICAgIHZhciBjbGVhbiA9IHNhbml0aXplQ2xvbmUoY29udGVudE9mKGJ1YmJsZXNbaV0pKTsKICAgICAgdmFyIGJvZHkgPSByb2xlID09PSAiYXNzaXN0YW50IiA/IGh0bWxUb01hcmtkb3duKGNsZWFuKSA6IChjbGVhbi50ZXh0Q29udGVudCB8fCAiIikudHJpbSgpOwogICAgICBpZiAoIWJvZHkpIGNvbnRpbnVlOwogICAgICBwYXJ0cy5wdXNoKChyb2xlID09PSAidXNlciIgPyAiIyMgVXNlciIgOiAiIyMgQXNzaXN0YW50IikgKyAiXG5cbiIgKyBib2R5KTsKICAgIH0KICAgIHJldHVybiBwYXJ0cy5qb2luKCJcblxuIikgKyAocGFydHMubGVuZ3RoID8gIlxuIiA6ICIiKTsKICB9CgogIC8vIC0tLS0gZXhwb3J0cyAobm9kZSB0ZXN0cykgLyBib290IChyZWFsIHdlYnZpZXcpIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICBpZiAodHlwZW9mIGRvY3VtZW50ICE9PSAidW5kZWZpbmVkIikgewogICAgYm9vdCgpOwogIH0gZWxzZSBpZiAodHlwZW9mIG1vZHVsZSAhPT0gInVuZGVmaW5lZCIgJiYgbW9kdWxlLmV4cG9ydHMpIHsKICAgIG1vZHVsZS5leHBvcnRzID0geyBodG1sVG9NYXJrZG93bjogaHRtbFRvTWFya2Rvd24sIHNhbml0aXplQ2xvbmU6IHNhbml0aXplQ2xvbmUsCiAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NpZnlCdWJibGU6IGNsYXNzaWZ5QnViYmxlLCBjb252ZXJzYXRpb25Ub01hcmtkb3duOiBjb252ZXJzYXRpb25Ub01hcmtkb3duLAogICAgICAgICAgICAgICAgICAgICAgIGhhc0NvcHlhYmxlQ29udGVudDogaGFzQ29weWFibGVDb250ZW50LCBjb3B5VGV4dDogY29weVRleHQgfTsKICB9CgogIC8vIC0tLS0gbGl2ZS13ZWJ2aWV3IHdpcmluZyAocnVucyBvbmx5IHdoZW4gYSBkb2N1bWVudCBleGlzdHMpIC0tLS0tLS0tLS0tLS0tLS0KICBmdW5jdGlvbiBxcyhub2RlLCBzZWwpIHsgdHJ5IHsgcmV0dXJuIHNlbCAmJiBub2RlLnF1ZXJ5U2VsZWN0b3IgPyBub2RlLnF1ZXJ5U2VsZWN0b3Ioc2VsKSA6IG51bGw7IH0gY2F0Y2ggKF8pIHsgcmV0dXJuIG51bGw7IH0gfQogIGZ1bmN0aW9uIHFzYShzZWwpIHsgdHJ5IHsgcmV0dXJuIEFycmF5LnByb3RvdHlwZS5zbGljZS5jYWxsKGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoc2VsKSk7IH0gY2F0Y2ggKF8pIHsgcmV0dXJuIFtdOyB9IH0KCiAgLy8gVGhlIGNvbnRlbnQgbm9kZSB0byBjb252ZXJ0L2NvcHk6IHRoZSBvcHRpb25hbCBBU1NJU1RBTlRfQ09OVEVOVCB3cmFwcGVyIGlmCiAgLy8gcGlubmVkIGFuZCBwcmVzZW50LCBlbHNlIHRoZSBidWJibGUgaXRzZWxmLiBUaGUgYnViYmxlIGFscmVhZHkgY29udGFpbnMgZXZlcnkKICAvLyBjb250ZW50LWJsb2NrIHNpYmxpbmcgb2YgYSBtdWx0aS1ibG9jayB0dXJuLCBhbmQgc2FuaXRpemVDbG9uZSBzdHJpcHMgdGhlCiAgLy8gY2hyb21lIChyYXRpbmcgd2lkZ2V0LCB0b29sL3Vua25vd24gYmxvY2tzLCBidXR0b25zLCBvdXIgY29udHJvbHMpCiAgLy8gZWl0aGVyIHdheSAtLSBzbyB0aGlzIGlzIGEgbmFycm93aW5nLCBuZXZlciB0aGUgdGhpbmcgdGhhdCBndWFyYW50ZWVzCiAgLy8gY29ycmVjdG5lc3MuCiAgZnVuY3Rpb24gY29udGVudE5vZGVPZihidWJibGUsIHJvbGUpIHsKICAgIGlmIChyb2xlID09PSAiYXNzaXN0YW50IiAmJiBBU1NJU1RBTlRfQ09OVEVOVCkgewogICAgICB2YXIgbiA9IHFzKGJ1YmJsZSwgQVNTSVNUQU5UX0NPTlRFTlQpOwogICAgICBpZiAobikgcmV0dXJuIG47CiAgICB9CiAgICByZXR1cm4gYnViYmxlOwogIH0KCiAgLy8gQ29weSBgc2AgdmlhIGEgc3luY2hyb25vdXMgZXhlY0NvbW1hbmQoImNvcHkiKSBvbiBhbiBvZmYtc2NyZWVuIHRleHRhcmVhLCBhbmQKICAvLyByZXBvcnQgd2hldGhlciBpdCBhY3R1YWxseSBoYXBwZW5lZC4gRG9uZSBmaXJzdCAoYW5kIHN5bmNocm9ub3VzbHkpIGJlY2F1c2UgaXQKICAvLyBydW5zIGluc2lkZSB0aGUgY2xpY2sgZ2VzdHVyZSBhbmQgd29ya3Mgd2hldGhlciBvciBub3QgdGhlIHBhZ2UgaXMgYSBzZWN1cmUKICAvLyBjb250ZXh0IC0tIHNvIGl0IGNvdmVycyByZW1vdGUgLyBjb2RlLXNlcnZlciwgd2hlcmUgdGhlIGFzeW5jIENsaXBib2FyZCBBUEkgaXMKICAvLyBzaW1wbHkgYWJzZW50LiBSZXN0b3JlcyB0aGUgcHJpb3Igc2VsZWN0aW9uL2ZvY3VzIHNvIGl0IGlzIGludmlzaWJsZS4KICBmdW5jdGlvbiBleGVjQ29weShzKSB7CiAgICB0cnkgewogICAgICBpZiAodHlwZW9mIGRvY3VtZW50ID09PSAidW5kZWZpbmVkIiB8fCAhZG9jdW1lbnQuY3JlYXRlRWxlbWVudCkgcmV0dXJuIGZhbHNlOwogICAgICB2YXIgcHJldiA9IGRvY3VtZW50LmFjdGl2ZUVsZW1lbnQgfHwgbnVsbDsKICAgICAgdmFyIHNlbCA9IGRvY3VtZW50LmdldFNlbGVjdGlvbiA/IGRvY3VtZW50LmdldFNlbGVjdGlvbigpIDogbnVsbDsKICAgICAgdmFyIHNhdmVkID0gKHNlbCAmJiBzZWwucmFuZ2VDb3VudCkgPyBzZWwuZ2V0UmFuZ2VBdCgwKSA6IG51bGw7CiAgICAgIHZhciB0YSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInRleHRhcmVhIik7CiAgICAgIHRhLnZhbHVlID0gczsKICAgICAgdGEuc2V0QXR0cmlidXRlKCJyZWFkb25seSIsICIiKTsKICAgICAgdGEuc3R5bGUucG9zaXRpb24gPSAiZml4ZWQiOwogICAgICB0YS5zdHlsZS50b3AgPSAiLTEwMDBweCI7CiAgICAgIHRhLnN0eWxlLmxlZnQgPSAiMCI7CiAgICAgIHRhLnN0eWxlLm9wYWNpdHkgPSAiMCI7CiAgICAgIChkb2N1bWVudC5ib2R5IHx8IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudCkuYXBwZW5kQ2hpbGQodGEpOwogICAgICB0YS5mb2N1cygpOwogICAgICB0YS5zZWxlY3QoKTsKICAgICAgdmFyIG9rID0gZmFsc2U7CiAgICAgIHRyeSB7IG9rID0gZG9jdW1lbnQuZXhlY0NvbW1hbmQoImNvcHkiKTsgfSBjYXRjaCAoXykgeyBvayA9IGZhbHNlOyB9CiAgICAgIGlmICh0YS5wYXJlbnROb2RlKSB0YS5wYXJlbnROb2RlLnJlbW92ZUNoaWxkKHRhKTsKICAgICAgaWYgKHNhdmVkICYmIHNlbCkgeyB0cnkgeyBzZWwucmVtb3ZlQWxsUmFuZ2VzKCk7IHNlbC5hZGRSYW5nZShzYXZlZCk7IH0gY2F0Y2ggKF8pIHt9IH0KICAgICAgaWYgKHByZXYgJiYgcHJldi5mb2N1cykgeyB0cnkgeyBwcmV2LmZvY3VzKCk7IH0gY2F0Y2ggKF8pIHt9IH0KICAgICAgcmV0dXJuICEhb2s7CiAgICB9IGNhdGNoIChfKSB7IHJldHVybiBmYWxzZTsgfQogIH0KCiAgLy8gQ29weSBgdGV4dGAgYW5kIHJlc29sdmUgdG8gd2hldGhlciB0aGUgY29weSBBQ1RVQUxMWSBoYXBwZW5lZCwgc28gY2FsbGVycyBvbmx5CiAgLy8gc2hvdyBzdWNjZXNzIG9uIGEgcmVhbCBjb3B5IC0tIG5ldmVyIGEgZmFsc2UgImNvcGllZCIgKHRoZSBvcmlnaW5hbCBidWc6CiAgLy8gbmF2aWdhdG9yLmNsaXBib2FyZCB3YXMgdW5kZWZpbmVkIGluIHRoZSB3ZWJ2aWV3LCB0aGUgY29kZSBmZWxsIHRocm91Z2ggdG8KICAvLyBQcm9taXNlLnJlc29sdmUoKSwgYW5kIHRoZSBVSSBjbGFpbWVkIHN1Y2Nlc3Mgd2hpbGUgbm90aGluZyB3YXMgd3JpdHRlbikuIEVtcHR5CiAgLy8gdGV4dCBpcyBhIG5vbi1jb3B5IC0+IGZhbHNlLiBleGVjQ29tbWFuZCBmaXJzdCAoZ2VzdHVyZS1zYWZlLCBzZWN1cmUtY29udGV4dC0KICAvLyBpbmRlcGVuZGVudCk7IHRoZSBhc3luYyBDbGlwYm9hcmQgQVBJIGlzIHRoZSBmYWxsYmFjay4gTmV2ZXIgdGhyb3dzLgogIGZ1bmN0aW9uIGNvcHlUZXh0KHRleHQpIHsKICAgIHZhciBzID0gKHRleHQgPT0gbnVsbCkgPyAiIiA6IFN0cmluZyh0ZXh0KTsKICAgIGlmICghcykgcmV0dXJuIFByb21pc2UucmVzb2x2ZShmYWxzZSk7CiAgICBpZiAoZXhlY0NvcHkocykpIHJldHVybiBQcm9taXNlLnJlc29sdmUodHJ1ZSk7CiAgICB0cnkgewogICAgICBpZiAodHlwZW9mIG5hdmlnYXRvciAhPT0gInVuZGVmaW5lZCIgJiYgbmF2aWdhdG9yLmNsaXBib2FyZCAmJiBuYXZpZ2F0b3IuY2xpcGJvYXJkLndyaXRlVGV4dCkgewogICAgICAgIHJldHVybiBuYXZpZ2F0b3IuY2xpcGJvYXJkLndyaXRlVGV4dChzKS50aGVuKAogICAgICAgICAgZnVuY3Rpb24gKCkgeyByZXR1cm4gdHJ1ZTsgfSwKICAgICAgICAgIGZ1bmN0aW9uICgpIHsgcmV0dXJuIGZhbHNlOyB9CiAgICAgICAgKTsKICAgICAgfQogICAgfSBjYXRjaCAoXykge30KICAgIHJldHVybiBQcm9taXNlLnJlc29sdmUoZmFsc2UpOwogIH0KCiAgZnVuY3Rpb24gYnViYmxlTWFya2Rvd24oYnViYmxlLCByb2xlKSB7CiAgICB2YXIgY2xlYW4gPSBzYW5pdGl6ZUNsb25lKGNvbnRlbnROb2RlT2YoYnViYmxlLCByb2xlKSk7CiAgICByZXR1cm4gcm9sZSA9PT0gImFzc2lzdGFudCIgPyBodG1sVG9NYXJrZG93bihjbGVhbikgOiAoY2xlYW4udGV4dENvbnRlbnQgfHwgIiIpLnRyaW0oKTsKICB9CgogIC8vIElubGluZSBTVkcgaWNvbnMgKGN1cnJlbnRDb2xvciwgfjE0cHgpLiBTZXQgdmlhIGlubmVySFRNTCBvbiBvdXIgb3duIGJ1dHRvbnMKICAvLyBvbmx5OyB0aGUgbWFya3VwIG5ldmVyIHJlYWNoZXMgY29waWVkIGNvbnRlbnQgKHNhbml0aXplQ2xvbmUgZHJvcHMgb3VyIG5vZGVzKS4KICB2YXIgSUNPTl9DT1BZID0gJzxzdmcgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgYXJpYS1oaWRkZW49InRydWUiPjxyZWN0IHg9IjkiIHk9IjkiIHdpZHRoPSIxMyIgaGVpZ2h0PSIxMyIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PHBhdGggZD0iTTUgMTVINGEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg5YTIgMiAwIDAgMSAyIDJ2MSI+PC9wYXRoPjwvc3ZnPic7CiAgdmFyIElDT05fQ0hFQ0sgPSAnPHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGFyaWEtaGlkZGVuPSJ0cnVlIj48cG9seWxpbmUgcG9pbnRzPSIyMCA2IDkgMTcgNCAxMiI+PC9wb2x5bGluZT48L3N2Zz4nOwoKICAvLyBGbGlwIHRoZSBidXR0b24gdG8gYSBjaGVja21hcmsgZm9yIEZFRURCQUNLX01TLCB0aGVuIHJlc3RvcmUuIElkZW1wb3RlbnQgYWNyb3NzCiAgLy8gcmFwaWQgY2xpY2tzIChhbnkgcGVuZGluZyByZXN0b3JlIGlzIGNsZWFyZWQgZmlyc3QpLgogIGZ1bmN0aW9uIHNob3dDb3BpZWQoYnRuKSB7CiAgICB0cnkgewogICAgICBpZiAoYnRuLl9fY2NUaW1lcikgY2xlYXJUaW1lb3V0KGJ0bi5fX2NjVGltZXIpOwogICAgICBidG4uY2xhc3NMaXN0LmFkZChDT05UUk9MX1BSRUZJWCArICItb2siKTsKICAgICAgYnRuLmlubmVySFRNTCA9IElDT05fQ0hFQ0s7CiAgICAgIGJ0bi5fX2NjVGltZXIgPSBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHsKICAgICAgICB0cnkgeyBidG4uY2xhc3NMaXN0LnJlbW92ZShDT05UUk9MX1BSRUZJWCArICItb2siKTsgYnRuLmlubmVySFRNTCA9IElDT05fQ09QWTsgfSBjYXRjaCAoXykge30KICAgICAgICBidG4uX19jY1RpbWVyID0gbnVsbDsKICAgICAgfSwgRkVFREJBQ0tfTVMpOwogICAgfSBjYXRjaCAoXykge30KICB9CgogIC8vIEJ1aWxkIGEgc2luZ2xlIGNvbnRyb2w6IG9uZSBjbGlwYm9hcmQtaWNvbiBidXR0b24uIGBvbkNvcHkoKWAgaXMgaW52b2tlZAogIC8vIHN5bmNocm9ub3VzbHkgb24gY2xpY2sgKHNvIHRoZSBjb3B5IHN0YXlzIGluc2lkZSB0aGUgdXNlciBnZXN0dXJlKSBhbmQgbXVzdAogIC8vIHJldHVybiBhIFByb21pc2U8Ym9vbGVhbj47IHRoZSBjaGVja21hcmsgc2hvd3Mgb25seSB3aGVuIGl0IHJlc29sdmVzIHRydWUuIEFsbAogIC8vIG5vZGVzIGNhcnJ5IHRoZSBDT05UUk9MX1BSRUZJWCBjbGFzcyBzbyBzYW5pdGl6ZUNsb25lIHN0cmlwcyB0aGVtIGZyb20gY29waWVzLgogIGZ1bmN0aW9uIGJ1aWxkQ29udHJvbChvbkNvcHksIHRpdGxlKSB7CiAgICB2YXIgd3JhcCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInNwYW4iKTsKICAgIHdyYXAuY2xhc3NOYW1lID0gQ09OVFJPTF9QUkVGSVg7CiAgICB2YXIgYnRuID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiYnV0dG9uIik7CiAgICBidG4udHlwZSA9ICJidXR0b24iOwogICAgYnRuLmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1idG4iOwogICAgYnRuLnRpdGxlID0gdGl0bGUgfHwgIkNvcHkgYXMgTWFya2Rvd24iOwogICAgYnRuLnNldEF0dHJpYnV0ZSgiYXJpYS1sYWJlbCIsIGJ0bi50aXRsZSk7CiAgICBidG4uaW5uZXJIVE1MID0gSUNPTl9DT1BZOwogICAgdmFyIGJ1c3kgPSBmYWxzZTsKICAgIGJ0bi5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsIGZ1bmN0aW9uIChlKSB7CiAgICAgIGUuc3RvcFByb3BhZ2F0aW9uKCk7CiAgICAgIGlmIChidXN5KSByZXR1cm47CiAgICAgIGJ1c3kgPSB0cnVlOwogICAgICB2YXIgcDsKICAgICAgdHJ5IHsgcCA9IG9uQ29weSgpOyB9IGNhdGNoIChfKSB7IHAgPSBmYWxzZTsgfQogICAgICBQcm9taXNlLnJlc29sdmUocCkudGhlbigKICAgICAgICBmdW5jdGlvbiAob2spIHsgYnVzeSA9IGZhbHNlOyBpZiAob2spIHNob3dDb3BpZWQoYnRuKTsgfSwKICAgICAgICBmdW5jdGlvbiAoKSB7IGJ1c3kgPSBmYWxzZTsgfQogICAgICApOwogICAgfSk7CiAgICB3cmFwLmFwcGVuZENoaWxkKGJ0bik7CiAgICByZXR1cm4gd3JhcDsKICB9CgogIGZ1bmN0aW9uIGRlY29yYXRlKGJ1YmJsZSkgewogICAgdHJ5IHsKICAgICAgdmFyIHJvbGUgPSBjbGFzc2lmeUJ1YmJsZShidWJibGUpOwogICAgICBpZiAoIXJvbGUpIHJldHVybjsKICAgICAgLy8gSWRlbXBvdGVudDoga2VlcCBleGFjdGx5IG9uZSBjb250cm9sLiBBIFJlYWN0IHJlLXJlbmRlciBvZiB0aGUgYnViYmxlIGNhbgogICAgICAvLyBsZWF2ZSBhIHN0YWxlIGNvbnRyb2wgYmVoaW5kIG9yIHRyYW5zaWVudGx5IGRlZmVhdCBhbiAiYWxyZWFkeSBkZWNvcmF0ZWQiCiAgICAgIC8vIGd1YXJkLCB3aGljaCBpcyB3aGF0IHByb2R1Y2VkIGR1cGxpY2F0ZSByb3dzIG9mIGJ1dHRvbnM7IHBydW5lIGFueSBleHRyYXMKICAgICAgLy8gZXZlcnkgc3dlZXAgYW5kIG9ubHkgYWRkIG9uZSB3aGVuIG5vbmUgcmVtYWluLgogICAgICB2YXIgZXhpc3RpbmcgPSBidWJibGUucXVlcnlTZWxlY3RvckFsbCA/IGJ1YmJsZS5xdWVyeVNlbGVjdG9yQWxsKCIuIiArIENPTlRST0xfUFJFRklYKSA6IG51bGw7CiAgICAgIGlmICghaGFzQ29weWFibGVDb250ZW50KGNvbnRlbnROb2RlT2YoYnViYmxlLCByb2xlKSwgcm9sZSkpIHsKICAgICAgICBpZiAoZXhpc3RpbmcgJiYgZXhpc3RpbmcubGVuZ3RoKSB7CiAgICAgICAgICBmb3IgKHZhciBqID0gZXhpc3RpbmcubGVuZ3RoIC0gMTsgaiA+PSAwOyBqLS0pIHsKICAgICAgICAgICAgaWYgKGV4aXN0aW5nW2pdICYmIGV4aXN0aW5nW2pdLnBhcmVudE5vZGUpIGV4aXN0aW5nW2pdLnBhcmVudE5vZGUucmVtb3ZlQ2hpbGQoZXhpc3Rpbmdbal0pOwogICAgICAgICAgfQogICAgICAgIH0KICAgICAgICByZXR1cm47CiAgICAgIH0KICAgICAgaWYgKGV4aXN0aW5nICYmIGV4aXN0aW5nLmxlbmd0aCkgewogICAgICAgIGZvciAodmFyIGkgPSBleGlzdGluZy5sZW5ndGggLSAxOyBpID49IDE7IGktLSkgewogICAgICAgICAgaWYgKGV4aXN0aW5nW2ldICYmIGV4aXN0aW5nW2ldLnBhcmVudE5vZGUpIGV4aXN0aW5nW2ldLnBhcmVudE5vZGUucmVtb3ZlQ2hpbGQoZXhpc3RpbmdbaV0pOwogICAgICAgIH0KICAgICAgICByZXR1cm47CiAgICAgIH0KICAgICAgdmFyIGNvbnRyb2wgPSBidWlsZENvbnRyb2woZnVuY3Rpb24gKCkgewogICAgICAgIHJldHVybiBjb3B5VGV4dChidWJibGVNYXJrZG93bihidWJibGUsIHJvbGUpKTsKICAgICAgfSwgIkNvcHkgYXMgTWFya2Rvd24iKTsKICAgICAgYnViYmxlLmFwcGVuZENoaWxkKGNvbnRyb2wpOwogICAgfSBjYXRjaCAoXykge30KICB9CgogIGZ1bmN0aW9uIGNvcHlDb252ZXJzYXRpb24oKSB7CiAgICB2YXIgYnViYmxlcyA9IHFzYShVU0VSX0JVQkJMRSArICIsIiArIEFTU0lTVEFOVF9CVUJCTEUpOwogICAgcmV0dXJuIGNvcHlUZXh0KGNvbnZlcnNhdGlvblRvTWFya2Rvd24oYnViYmxlcywgZnVuY3Rpb24gKGIpIHsKICAgICAgcmV0dXJuIGNvbnRlbnROb2RlT2YoYiwgY2xhc3NpZnlCdWJibGUoYikpOwogICAgfSkpOwogIH0KCiAgLy8gQSBzaW5nbGUgZmxvYXRpbmcgIkNvcHkgY29udmVyc2F0aW9uIiBpY29uLCBwcmVzZW50IG9ubHkgd2hpbGUgYSBjb252ZXJzYXRpb24KICAvLyBpcyBvcGVuIChzbyBpdCBuZXZlciBjbHV0dGVycyB0aGUgaGlzdG9yeS1saXN0IHZpZXcpLiBQaW5uZWQgdG9wLXJpZ2h0IGJ5IENTUywKICAvLyBjbGVhciBvZiB0aGUgY2hhdCBpbnB1dCBhdCB0aGUgYm90dG9tOyB0aGUgbW9zdC1yZWNlbnQtcHJvbXB0IHN0aWNreSBoZWFkZXIKICAvLyBzaXRzIHRvIGl0cyBsZWZ0LgogIGZ1bmN0aW9uIGluc3RhbGxDb252ZXJzYXRpb25Db250cm9sKCkgewogICAgdHJ5IHsKICAgICAgdmFyIGV4aXN0aW5nID0gcXMoZG9jdW1lbnQsICIuIiArIENPTlRST0xfUFJFRklYICsgIi1jb252ZXJzYXRpb24iKTsKICAgICAgdmFyIGhhc01lc3NhZ2VzID0gcXNhKFVTRVJfQlVCQkxFICsgIiwiICsgQVNTSVNUQU5UX0JVQkJMRSkubGVuZ3RoID4gMDsKICAgICAgaWYgKCFoYXNNZXNzYWdlcykgewogICAgICAgIGlmIChleGlzdGluZyAmJiBleGlzdGluZy5wYXJlbnROb2RlKSBleGlzdGluZy5wYXJlbnROb2RlLnJlbW92ZUNoaWxkKGV4aXN0aW5nKTsKICAgICAgICByZXR1cm47CiAgICAgIH0KICAgICAgaWYgKGV4aXN0aW5nKSByZXR1cm47CiAgICAgIHZhciBiYXIgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJkaXYiKTsKICAgICAgYmFyLmNsYXNzTmFtZSA9IENPTlRST0xfUFJFRklYICsgIi1jb252ZXJzYXRpb24iOwogICAgICBiYXIuYXBwZW5kQ2hpbGQoYnVpbGRDb250cm9sKGNvcHlDb252ZXJzYXRpb24sICJDb3B5IGNvbnZlcnNhdGlvbiIpKTsKICAgICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChiYXIpOwogICAgfSBjYXRjaCAoXykge30KICB9CgogIGZ1bmN0aW9uIHN3ZWVwKCkgewogICAgdmFyIGIgPSBxc2EoVVNFUl9CVUJCTEUgKyAiLCIgKyBBU1NJU1RBTlRfQlVCQkxFKTsKICAgIGZvciAodmFyIGkgPSAwOyBpIDwgYi5sZW5ndGg7IGkrKykgZGVjb3JhdGUoYltpXSk7CiAgICBpbnN0YWxsQ29udmVyc2F0aW9uQ29udHJvbCgpOwogIH0KCiAgZnVuY3Rpb24gYm9vdCgpIHsKICAgIHRyeSB7CiAgICAgIHZhciB0YXJnZXQgPSAoTUVTU0FHRVNfQ09OVEFJTkVSICYmIHFzKGRvY3VtZW50LCBNRVNTQUdFU19DT05UQUlORVIpKSB8fCBkb2N1bWVudC5ib2R5OwogICAgICBzd2VlcCgpOwogICAgICBpZiAodHlwZW9mIE11dGF0aW9uT2JzZXJ2ZXIgPT09ICJ1bmRlZmluZWQiKSByZXR1cm47CiAgICAgIHZhciBvYnMgPSBuZXcgTXV0YXRpb25PYnNlcnZlcihmdW5jdGlvbiAoKSB7IHN3ZWVwKCk7IH0pOwogICAgICBvYnMub2JzZXJ2ZSh0YXJnZXQsIHsgY2hpbGRMaXN0OiB0cnVlLCBzdWJ0cmVlOiB0cnVlIH0pOwogICAgfSBjYXRjaCAoXykge30KICB9Cn0pKCk7Cg==").decode("utf-8") +INJECT_CSS = base64.b64decode("LmNjLW1kLWNvcHkgewogIGRpc3BsYXk6IGlubGluZS1mbGV4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgdmVydGljYWwtYWxpZ246IG1pZGRsZTsKICBtYXJnaW4tbGVmdDogNnB4Owp9Ci5jYy1tZC1jb3B5LWJ0biB7CiAgZGlzcGxheTogaW5saW5lLWZsZXg7CiAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjsKICBwYWRkaW5nOiAycHg7CiAgY29sb3I6IHZhcigtLXZzY29kZS1mb3JlZ3JvdW5kKTsKICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudDsKICBib3JkZXI6IG5vbmU7CiAgYm9yZGVyLXJhZGl1czogNHB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBvcGFjaXR5OiAwLjY7Cn0KLmNjLW1kLWNvcHktYnRuIHN2ZyB7CiAgZGlzcGxheTogYmxvY2s7CiAgd2lkdGg6IDE0cHg7CiAgaGVpZ2h0OiAxNHB4Owp9Ci5jYy1tZC1jb3B5LWJ0bjpob3ZlciB7CiAgb3BhY2l0eTogMTsKICBiYWNrZ3JvdW5kOiB2YXIoLS12c2NvZGUtdG9vbGJhci1ob3ZlckJhY2tncm91bmQsIHJnYmEoMTI4LCAxMjgsIDEyOCwgMC4xNSkpOwp9Ci8qIFN1Y2Nlc3Mgc3RhdGU6IHRoZSBpY29uIGlzIGEgZ3JlZW4gY2hlY2ttYXJrIGZvciBhIG1vbWVudCBhZnRlciBhIHJlYWwgY29weS4gKi8KLmNjLW1kLWNvcHktYnRuLmNjLW1kLWNvcHktb2ssCi5jYy1tZC1jb3B5LWJ0bi5jYy1tZC1jb3B5LW9rOmhvdmVyIHsKICBvcGFjaXR5OiAxOwogIGNvbG9yOiB2YXIoLS12c2NvZGUtY2hhcnRzLWdyZWVuLCB2YXIoLS12c2NvZGUtdGVzdGluZy1pY29uUGFzc2VkLCAjODlkMTg1KSk7CiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7Cn0KLyogV2hvbGUtY29udmVyc2F0aW9uIGNvcHk6IGEgc2luZ2xlIGZsb2F0aW5nIGljb24gcGlubmVkIHRvIHRoZSB0b3AtcmlnaHQgY29ybmVyLAogICBjbGVhciBvZiB0aGUgY2hhdCBpbnB1dCBhdCB0aGUgYm90dG9tLiBTaG93biBvbmx5IHdoaWxlIGEgY29udmVyc2F0aW9uIGlzIG9wZW4KICAgKHRoZSBJSUZFIGFkZHMvcmVtb3ZlcyBpdCkuIE51ZGdlIHRvcC9yaWdodCBoZXJlIGlmIGl0IGNyb3dkcyB0aGUgc3RpY2t5IGhlYWRlci4gKi8KLmNjLW1kLWNvcHktY29udmVyc2F0aW9uIHsKICBwb3NpdGlvbjogZml4ZWQ7CiAgdG9wOiAyNnB4OwogIHJpZ2h0OiA0cHg7CiAgei1pbmRleDogMzA7CiAgZGlzcGxheTogaW5saW5lLWZsZXg7CiAgcGFkZGluZzogMnB4OwogIGJhY2tncm91bmQ6IHZhcigtLXZzY29kZS1lZGl0b3JXaWRnZXQtYmFja2dyb3VuZCk7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tdnNjb2RlLXdpZGdldC1ib3JkZXIsIHRyYW5zcGFyZW50KTsKICBib3JkZXItcmFkaXVzOiA2cHg7CiAgb3BhY2l0eTogMC44NTsKfQouY2MtbWQtY29weS1jb252ZXJzYXRpb24gLmNjLW1kLWNvcHkgewogIG1hcmdpbi1sZWZ0OiAwOwp9Ci5jYy1tZC1jb3B5LWNvbnZlcnNhdGlvbjpob3ZlciB7CiAgb3BhY2l0eTogMTsKfQo=").decode("utf-8") # << stdout (markdown) python3 cc-export.py --session ID # a specific session id python3 cc-export.py --format text # plain text python3 cc-export.py --include-thinking --include-tools python3 cc-export.py -o out.md # write to a file - python3 cc-export.py --open # open the raw transcript in VS Code python3 cc-export.py --cwd /path/to/proj # resolve as if launched from there The JSONL is an internal format that can change, so each row's shape is validated @@ -21,7 +20,6 @@ import glob import json import os -import subprocess import sys import unicodedata @@ -157,7 +155,6 @@ def main(argv=None, config=None, env=None): ap.add_argument("--format", choices=("markdown", "text"), default="markdown") ap.add_argument("--include-thinking", action="store_true") ap.add_argument("--include-tools", action="store_true") - ap.add_argument("--open", action="store_true", help="open the raw .jsonl in VS Code") ap.add_argument("-o", "--output", default=None, help="write to FILE (default: stdout)") args = ap.parse_args(argv) @@ -168,15 +165,6 @@ def main(argv=None, config=None, env=None): sys.stderr.write("cc-export: no matching session transcript found\n") return 1 - if args.open: - editor = env.get("CC_EDITOR", "code") - try: - subprocess.run([editor, path], check=False, env=env) - except FileNotFoundError: - sys.stderr.write(f"cc-export: editor '{editor}' not found on PATH\n") - return 1 - return 0 - out = render( read_rows(path), fmt=args.format, diff --git a/fixes/markdown-copy-export/webview-inject.css b/fixes/markdown-copy-export/webview-inject.css index 21ce251..321e5a2 100644 --- a/fixes/markdown-copy-export/webview-inject.css +++ b/fixes/markdown-copy-export/webview-inject.css @@ -1,54 +1,55 @@ .cc-md-copy { display: inline-flex; align-items: center; - gap: 2px; vertical-align: middle; margin-left: 6px; } -.cc-md-copy-btn, -.cc-md-copy-caret { - font: inherit; - font-size: 11px; - line-height: 1.4; - padding: 1px 6px; +.cc-md-copy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px; color: var(--vscode-foreground); background: transparent; - border: 1px solid var(--vscode-widget-border, transparent); + border: none; border-radius: 4px; cursor: pointer; - opacity: 0.65; + opacity: 0.6; } -.cc-md-copy-btn:hover, -.cc-md-copy-caret:hover { +.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)); } -.cc-md-copy-menu { - position: relative; - margin-left: 4px; - padding: 2px; - background: var(--vscode-menu-background, var(--vscode-editorWidget-background)); - border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border, transparent)); - border-radius: 4px; - z-index: 5; -} -.cc-md-copy-feedback { - margin-left: 6px; - font-size: 11px; - opacity: 0.85; - color: var(--vscode-foreground); +/* 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; - right: 16px; - bottom: 56px; - z-index: 10; + 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; } diff --git a/fixes/markdown-copy-export/webview-inject.js b/fixes/markdown-copy-export/webview-inject.js index 947d691..b6a096c 100644 --- a/fixes/markdown-copy-export/webview-inject.js +++ b/fixes/markdown-copy-export/webview-inject.js @@ -1,9 +1,14 @@ -/* cc-md-copy: per-message and whole-conversation copy (markdown/plain) for the +/* 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. - * 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. + * 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. */ -(function () { +/* 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 @@ -19,7 +24,7 @@ // 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 = 1800; + 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. @@ -146,7 +151,9 @@ } 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 === "HR") out += "---\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" || @@ -172,11 +179,12 @@ } // Class-prefix hooks for non-content chrome that renders *inside* an assistant - // bubble (verified on 2.1.170; Task 6 re-pins these). tool*/thinking_ are the v1 - // exclusions; unknownContent_ is the renderer's fallback for unrecognized block + // 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_", "thinking_", "unknownContent_"]; + 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" @@ -194,11 +202,11 @@ // 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 v1-excluded blocks out of the copy. + // 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 = (node.childNodes || []).slice(); + 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; } @@ -208,6 +216,19 @@ 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"; @@ -238,7 +259,8 @@ boot(); } else if (typeof module !== "undefined" && module.exports) { module.exports = { htmlToMarkdown: htmlToMarkdown, sanitizeClone: sanitizeClone, - classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown }; + classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown, + hasCopyableContent: hasCopyableContent, copyText: copyText }; } // ---- live-webview wiring (runs only when a document exists) ---------------- @@ -248,7 +270,7 @@ // 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/thinking/unknown blocks, buttons, our controls) + // 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) { @@ -259,64 +281,107 @@ return bubble; } - function copyText(text) { + // 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 (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text); - } catch (_) {} - return Promise.resolve(); // best-effort; never throw into the app + 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; } } - function flashFeedback(host) { + // 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 { - var fb = document.createElement("span"); - fb.className = CONTROL_PREFIX + "-feedback"; - fb.textContent = "Copied"; - host.appendChild(fb); - setTimeout(function () { if (fb && fb.parentNode) fb.parentNode.removeChild(fb); }, FEEDBACK_MS); + 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(); } - function bubblePlain(bubble, role) { - return (sanitizeClone(contentNodeOf(bubble, role)).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 = ''; + var ICON_CHECK = ''; + + // 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: a primary "Copy" (markdown) plus a small caret that - // toggles a menu with "Copy as plain text". All nodes carry the CONTROL_PREFIX - // class so sanitizeClone removes them from any copied content. - function buildControl(onMarkdown, onPlain) { + // 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; 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 primary = document.createElement("button"); - primary.type = "button"; - primary.className = CONTROL_PREFIX + "-btn"; - primary.title = "Copy as Markdown"; - primary.textContent = "Copy"; - primary.addEventListener("click", function (e) { e.stopPropagation(); onMarkdown(primary); }); - var caret = document.createElement("button"); - caret.type = "button"; - caret.className = CONTROL_PREFIX + "-caret"; - caret.title = "Copy options"; - caret.textContent = "▾"; // black down-pointing small triangle - var menu = document.createElement("span"); - menu.className = CONTROL_PREFIX + "-menu"; - menu.style.display = "none"; - var plain = document.createElement("button"); - plain.type = "button"; - plain.className = CONTROL_PREFIX + "-btn"; - plain.textContent = "Copy as plain text"; - plain.addEventListener("click", function (e) { e.stopPropagation(); menu.style.display = "none"; onPlain(plain); }); - menu.appendChild(plain); - caret.addEventListener("click", function (e) { + 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(); - menu.style.display = menu.style.display === "none" ? "inline-block" : "none"; + 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(primary); - wrap.appendChild(caret); - wrap.appendChild(menu); + wrap.appendChild(btn); return wrap; } @@ -324,54 +389,69 @@ try { var role = classifyBubble(bubble); if (!role) return; - if (qs(bubble, "." + CONTROL_PREFIX)) return; // already decorated - var control = buildControl( - function (host) { copyText(bubbleMarkdown(bubble, role)).then(function () { flashFeedback(control); }); }, - function (host) { copyText(bubblePlain(bubble, role)).then(function () { flashFeedback(control); }); } - ); + // 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(format) { + function copyConversation() { var bubbles = qsa(USER_BUBBLE + "," + ASSISTANT_BUBBLE); - if (format === "text") { - var lines = []; - for (var i = 0; i < bubbles.length; i++) { - var role = classifyBubble(bubbles[i]); - if (!role) continue; - var body = bubblePlain(bubbles[i], role); - if (body) lines.push(body); - } - return copyText(lines.join("\n\n") + (lines.length ? "\n" : "")); - } 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 { - if (qs(document, "." + CONTROL_PREFIX + "-conversation")) return; + 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"; - var control = buildControl( - function () { copyConversation("markdown").then(function () { flashFeedback(bar); }); }, - function () { copyConversation("text").then(function () { flashFeedback(bar); }); } - ); - control.title = "Copy entire conversation"; - bar.appendChild(control); - document.body.appendChild(bar); // fixed-position via CSS; placement refined in Task 6 + 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]); } + 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(); - installConversationControl(); if (typeof MutationObserver === "undefined") return; var obs = new MutationObserver(function () { sweep(); }); obs.observe(target, { childList: true, subtree: true }); diff --git a/launcher/claudemax b/launcher/claudemax index 2b03efe..285d2f4 100755 --- a/launcher/claudemax +++ b/launcher/claudemax @@ -12,8 +12,9 @@ # 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 per-message and whole-conversation "Copy" controls (Markdown / plain -# text) to the VS Code chat. Like fix #2 there is no env/CLI lever, so this +# 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. @@ -45,6 +46,7 @@ # 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. @@ -143,6 +145,35 @@ CC_PATCH_MD_COPY="${CC_PATCH_MD_COPY:-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 DeepSeek 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 >>> +# <<< ccwa-local-env <<< + # --- Inject the thinking-display fix into the launch args ------------------- # # Fire on a real agent invocation. Surfaces signal a real run differently: @@ -237,13 +268,15 @@ fi # untouched. # # context-icon feature - component `FJe` in webview/index.js: -# if(c>=50)return null -> if(c>=101)return null}/*ccwa-context-icon*/ -# `c` is "% of context remaining" (maxes at 100), so >=101 never fires and the -# icon renders whenever a context window is known; the t===0 "no session yet" -# guard is left intact. The trailing /*ccwa-context-icon*/ is our ownership -# marker. Maintenance: this keys off the stable string ">=50)return null}", not -# the minified component name; if a future build changes that substring, apply -# no-ops loudly (a one-line warning) until the anchor here is updated. +# 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::*/ 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. @@ -260,34 +293,48 @@ _cc_feature_enabled() { # 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 -o '>=50)return null}' "$f" 2>/dev/null || true) | wc -l | tr -d ' ')" + 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#>=50)return null}#>=101)return null}/*ccwa-context-icon*/#' "$f" > "$tmp" 2>/dev/null \ - && [ -s "$tmp" ] && grep -q '/\*ccwa-context-icon\*/' "$tmp" 2>/dev/null; then + 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. Two ownership fingerprints are - # recognized: the current MARKED form, and the legacy BARE form that older - # launcher/standalone versions wrote before the marker existed. `>=101)return - # null}` is dead upstream code (c maxes at 100), so it appears only as our own - # output; adopting it lets a legacy install revert and upgrade cleanly instead - # of warning on every launch. The MARKED substitution runs first because the - # bare string is a prefix of the marked one. + # 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 - grep -qF '>=101)return null}' "$f" 2>/dev/null || return 0 # nothing of ours + # 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#>=101)return null}/\*ccwa-context-icon\*/#>=50)return null}#g' \ - -e 's#>=101)return null}#>=50)return null}#g' "$f" > "$tmp" 2>/dev/null \ + 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 @@ -304,12 +351,17 @@ _cc_undo_context_icon() { # 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/plain) for the +/* 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. - * 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. + * 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. */ -(function () { +/* 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 @@ -325,7 +377,7 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' // 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 = 1800; + 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. @@ -452,7 +504,9 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' } 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 === "HR") out += "---\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" || @@ -478,11 +532,12 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' } // Class-prefix hooks for non-content chrome that renders *inside* an assistant - // bubble (verified on 2.1.170; Task 6 re-pins these). tool*/thinking_ are the v1 - // exclusions; unknownContent_ is the renderer's fallback for unrecognized block + // 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_", "thinking_", "unknownContent_"]; + 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" @@ -500,11 +555,11 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' // 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 v1-excluded blocks out of the copy. + // 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 = (node.childNodes || []).slice(); + 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; } @@ -514,6 +569,19 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' 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"; @@ -544,7 +612,8 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' boot(); } else if (typeof module !== "undefined" && module.exports) { module.exports = { htmlToMarkdown: htmlToMarkdown, sanitizeClone: sanitizeClone, - classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown }; + classifyBubble: classifyBubble, conversationToMarkdown: conversationToMarkdown, + hasCopyableContent: hasCopyableContent, copyText: copyText }; } // ---- live-webview wiring (runs only when a document exists) ---------------- @@ -554,7 +623,7 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' // 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/thinking/unknown blocks, buttons, our controls) + // 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) { @@ -565,64 +634,107 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' return bubble; } - function copyText(text) { + // 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 (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text); - } catch (_) {} - return Promise.resolve(); // best-effort; never throw into the app + 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; } } - function flashFeedback(host) { + // 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 { - var fb = document.createElement("span"); - fb.className = CONTROL_PREFIX + "-feedback"; - fb.textContent = "Copied"; - host.appendChild(fb); - setTimeout(function () { if (fb && fb.parentNode) fb.parentNode.removeChild(fb); }, FEEDBACK_MS); + 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(); } - function bubblePlain(bubble, role) { - return (sanitizeClone(contentNodeOf(bubble, role)).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 = ''; + var ICON_CHECK = ''; + + // 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: a primary "Copy" (markdown) plus a small caret that - // toggles a menu with "Copy as plain text". All nodes carry the CONTROL_PREFIX - // class so sanitizeClone removes them from any copied content. - function buildControl(onMarkdown, onPlain) { + // 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; 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 primary = document.createElement("button"); - primary.type = "button"; - primary.className = CONTROL_PREFIX + "-btn"; - primary.title = "Copy as Markdown"; - primary.textContent = "Copy"; - primary.addEventListener("click", function (e) { e.stopPropagation(); onMarkdown(primary); }); - var caret = document.createElement("button"); - caret.type = "button"; - caret.className = CONTROL_PREFIX + "-caret"; - caret.title = "Copy options"; - caret.textContent = "▾"; // black down-pointing small triangle - var menu = document.createElement("span"); - menu.className = CONTROL_PREFIX + "-menu"; - menu.style.display = "none"; - var plain = document.createElement("button"); - plain.type = "button"; - plain.className = CONTROL_PREFIX + "-btn"; - plain.textContent = "Copy as plain text"; - plain.addEventListener("click", function (e) { e.stopPropagation(); menu.style.display = "none"; onPlain(plain); }); - menu.appendChild(plain); - caret.addEventListener("click", function (e) { + 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(); - menu.style.display = menu.style.display === "none" ? "inline-block" : "none"; + 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(primary); - wrap.appendChild(caret); - wrap.appendChild(menu); + wrap.appendChild(btn); return wrap; } @@ -630,54 +742,69 @@ _cc_md_copy_js() { cat <<'CCMDCOPYJS' try { var role = classifyBubble(bubble); if (!role) return; - if (qs(bubble, "." + CONTROL_PREFIX)) return; // already decorated - var control = buildControl( - function (host) { copyText(bubbleMarkdown(bubble, role)).then(function () { flashFeedback(control); }); }, - function (host) { copyText(bubblePlain(bubble, role)).then(function () { flashFeedback(control); }); } - ); + // 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(format) { + function copyConversation() { var bubbles = qsa(USER_BUBBLE + "," + ASSISTANT_BUBBLE); - if (format === "text") { - var lines = []; - for (var i = 0; i < bubbles.length; i++) { - var role = classifyBubble(bubbles[i]); - if (!role) continue; - var body = bubblePlain(bubbles[i], role); - if (body) lines.push(body); - } - return copyText(lines.join("\n\n") + (lines.length ? "\n" : "")); - } 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 { - if (qs(document, "." + CONTROL_PREFIX + "-conversation")) return; + 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"; - var control = buildControl( - function () { copyConversation("markdown").then(function () { flashFeedback(bar); }); }, - function () { copyConversation("text").then(function () { flashFeedback(bar); }); } - ); - control.title = "Copy entire conversation"; - bar.appendChild(control); - document.body.appendChild(bar); // fixed-position via CSS; placement refined in Task 6 + 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]); } + 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(); - installConversationControl(); if (typeof MutationObserver === "undefined") return; var obs = new MutationObserver(function () { sweep(); }); obs.observe(target, { childList: true, subtree: true }); @@ -690,54 +817,55 @@ _cc_md_copy_css() { cat <<'CCMDCOPYCSS' .cc-md-copy { display: inline-flex; align-items: center; - gap: 2px; vertical-align: middle; margin-left: 6px; } -.cc-md-copy-btn, -.cc-md-copy-caret { - font: inherit; - font-size: 11px; - line-height: 1.4; - padding: 1px 6px; +.cc-md-copy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px; color: var(--vscode-foreground); background: transparent; - border: 1px solid var(--vscode-widget-border, transparent); + border: none; border-radius: 4px; cursor: pointer; - opacity: 0.65; + opacity: 0.6; +} +.cc-md-copy-btn svg { + display: block; + width: 14px; + height: 14px; } -.cc-md-copy-btn:hover, -.cc-md-copy-caret:hover { +.cc-md-copy-btn:hover { opacity: 1; background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15)); } -.cc-md-copy-menu { - position: relative; - margin-left: 4px; - padding: 2px; - background: var(--vscode-menu-background, var(--vscode-editorWidget-background)); - border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border, transparent)); - border-radius: 4px; - z-index: 5; -} -.cc-md-copy-feedback { - margin-left: 6px; - font-size: 11px; - opacity: 0.85; - color: var(--vscode-foreground); +/* 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; - right: 16px; - bottom: 56px; - z-index: 10; + 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; } diff --git a/launcher/claudemax.win.js b/launcher/claudemax.win.js index 6f38ffe..deec5a1 100644 --- a/launcher/claudemax.win.js +++ b/launcher/claudemax.win.js @@ -13,8 +13,9 @@ // 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 per-message and whole-conversation "Copy" controls (Markdown / plain -// text) to the VS Code chat. Like fix #2 there is no env/CLI lever, so this +// 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. @@ -46,6 +47,7 @@ // set CC_PATCH_MD_COPY=0 no copy controls / webview append (default: 1) // set CC_WORKAROUNDS=0 master: disable every fix (default: 1) // set CC_RECONCILE=0 do not touch the webview bundle (default: 1) +// set CC_SCRUB_ROUTING=1 force the default Anthropic account (default: 0) // // The real `claude` must be installed. This wrapper finds it automatically // (native install `claude.exe` or npm `claude.cmd`); if it cannot, set the @@ -232,21 +234,55 @@ if (!claude) { // --- Restore the always-visible context-usage icon (patches the webview) ---- // // Idempotent edit to component `FJe` in the extension's webview/index.js: -// if(c>=50)return null -> if(c>=101)return null -// `c` is "% of context remaining"; it maxes at 100, so >=101 never fires and the -// icon renders whenever a context window is known (the t===0 "no session yet" -// guard is left intact). Best-effort: every step is wrapped so it can never block -// the launch; a one-time backup is made and the write goes through a temp + rename -// so a failed write leaves the original untouched. Re-applied each launch, so an -// extension update that reinstalls a fresh bundle is re-patched next launch. +// 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"; it 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. Best-effort: every step is +// wrapped so it can never block the launch; a one-time backup is made and the +// write goes through a temp + rename so a failed write leaves the original +// untouched. Re-applied each launch, so an extension update that reinstalls a +// fresh bundle is re-patched next launch. // -// Maintenance note: this keys off the stable string ">=50)return null}", not the -// minified component name. If a future build changes that exact substring, the -// routine safely no-ops until the anchor here is updated. -const ICON_OLD = ">=50)return null}"; -const ICON_MARKER = "/*ccwa-context-icon*/"; -const ICON_BARE = ">=101)return null}"; // legacy unmarked form (older launcher/standalone) -const ICON_NEW = ICON_BARE + ICON_MARKER; +// Maintenance note: this keys off the minified guard pair shape below, not the +// component name or exact minified variable names. If a future build changes that +// shape, the routine safely no-ops until the anchor here is updated. +const ICON_IDENT = "[A-Za-z_$][A-Za-z0-9_$]*"; +const ICON_OLD_RE = new RegExp( + "if\\((" + ICON_IDENT + ")===0\\)return null;if\\((" + ICON_IDENT + ")>=50\\)return null\\}", + "g" +); +const ICON_MARKED_RE = new RegExp( + "if\\((" + ICON_IDENT + ")>=101\\)return null\\}/\\*ccwa-context-icon:(" + + ICON_IDENT + + "):\\1\\*/", + "g" +); +const ICON_OLD = "if(t===0)return null;if(c>=50)return null}"; +const ICON_LEGACY_MARKER = "/*ccwa-context-icon*/"; +const ICON_BARE = "if(c>=101)return null}"; +const ICON_NEW = "if(c>=101)return null}/*ccwa-context-icon:t:c*/"; +const ICON_LEGACY_NEW_CURRENT = ICON_BARE + ICON_LEGACY_MARKER; +const ICON_LEGACY_BARE = "if(t===0)return null;if(c>=101)return null}"; +// Legacy bare (metadata-less) marker on arbitrary guard names: an older +// var-agnostic write could leave the both-guards >=101 form + bare marker on a +// non-t/c build (e.g. Z/U). Match it by shape, not the fixed t/c. +const ICON_LEGACY_NEW_RE = new RegExp( + "if\\((" + + ICON_IDENT + + ")===0\\)return null;if\\((" + + ICON_IDENT + + ")>=101\\)return null\\}/\\*ccwa-context-icon\\*/", + "g" +); + +function iconOld(firstVar, remainingVar) { + return `if(${firstVar}===0)return null;if(${remainingVar}>=50)return null}`; +} + +function iconNew(firstVar, remainingVar) { + return `if(${remainingVar}>=101)return null}/*ccwa-context-icon:${firstVar}:${remainingVar}*/`; +} // Bundle-patch feature registry. Each feature is idempotent (apply/undo are // no-ops when their target state already holds) and reversible; undo keys off @@ -254,26 +290,44 @@ const ICON_NEW = ICON_BARE + ICON_MARKER; // older version wrote), so it reverses ONLY our own edits. Order matters: apply // runs forward, undo runs in reverse. function applyContextIcon(data) { - if (data.indexOf(ICON_MARKER) !== -1) return data; // already applied - const n = data.split(ICON_OLD).length - 1; + if (ICON_MARKED_RE.test(data)) { + ICON_MARKED_RE.lastIndex = 0; + return data; // already applied + } + ICON_MARKED_RE.lastIndex = 0; + const base = undoContextIcon(data); + const matches = Array.from(base.matchAll(ICON_OLD_RE)); + const n = matches.length; if (n === 0) { console.error( "claudemax: context-icon anchor not found (extension changed?); skipping" ); - return data; + return base; } - if (n !== 1) return data; // ambiguous (version changed) - skip - return data.replace(ICON_OLD, ICON_NEW); + if (n !== 1) return base; // ambiguous (version changed) - skip + return base.replace(ICON_OLD_RE, (_, firstVar, remainingVar) => + iconNew(firstVar, remainingVar) + ); } function undoContextIcon(data) { - // Revert our edit to the pristine upstream form. Two ownership fingerprints - // are recognized: the current MARKED form, and the legacy BARE form older - // versions wrote before the marker existed. ICON_BARE is dead upstream code - // (c maxes at 100), so it appears only as our own output; adopting it lets a - // legacy install revert/upgrade cleanly. Marked must go first: ICON_BARE is a - // prefix of ICON_NEW. - return data.split(ICON_NEW).join(ICON_OLD).split(ICON_BARE).join(ICON_OLD); + // Revert our edit to the pristine upstream form. Recognized fingerprints are: + // the current metadata marker, and legacy marked/bare forms that older + // versions wrote with fixed t/c names. Marked forms must go first because bare + // strings are prefixes of marked strings. + return data + .replace(ICON_MARKED_RE, (_, remainingVar, firstVar) => + iconOld(firstVar, remainingVar) + ) + .replace(ICON_LEGACY_NEW_RE, (_, firstVar, remainingVar) => + iconOld(firstVar, remainingVar) + ) + .split(ICON_LEGACY_NEW_CURRENT).join(ICON_OLD) + .split(ICON_LEGACY_BARE).join(ICON_OLD) + .split(ICON_BARE).join(ICON_OLD) + // Strip any leftover bare marker so apply is never wedged by an unrecognized + // form (parity with the bash launcher's final undo pass). + .replace(/\)return null\}\/\*ccwa-context-icon\*\//g, ")return null}"); } function contextIconEnabled() { @@ -291,8 +345,8 @@ function contextIconEnabled() { const MD_OPEN = "/* cc-md-copy v1 */"; const MD_CLOSE = "/* /cc-md-copy v1 */"; // >>>CCWA-MD-COPY-EMBED>>> (generated by tools/gen-embeds; do not edit) -const MD_COPY_JS = "/* cc-md-copy: per-message and whole-conversation copy (markdown/plain) for the\n * Claude Code VS Code webview. Self-contained IIFE appended to webview/index.js.\n * Additive and read-only w.r.t. app state; keyed on stable CSS-module class\n * prefixes, so it fails safe (controls 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(function () {\n \"use strict\";\n\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 = 1800;\n\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 === \"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
/
    / (now handled, not flattened).\n return block({ childNodes: [root] }).replace(/\\n{3,}/g, \"\\n\\n\").trim();\n }\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\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*/thinking_ are the v1\n // exclusions; 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_\", \"thinking_\", \"unknownContent_\"];\n\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\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 v1-excluded blocks out of the copy.\n function sanitizeClone(contentNode) {\n var clone = contentNode.cloneNode(true);\n (function strip(node) {\n var kids = (node.childNodes || []).slice();\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\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\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\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 }\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\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/thinking/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\n function copyText(text) {\n try {\n if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);\n } catch (_) {}\n return Promise.resolve(); // best-effort; never throw into the app\n }\n\n function flashFeedback(host) {\n try {\n var fb = document.createElement(\"span\");\n fb.className = CONTROL_PREFIX + \"-feedback\";\n fb.textContent = \"Copied\";\n host.appendChild(fb);\n setTimeout(function () { if (fb && fb.parentNode) fb.parentNode.removeChild(fb); }, FEEDBACK_MS);\n } catch (_) {}\n }\n\n function bubbleMarkdown(bubble, role) {\n var clean = sanitizeClone(contentNodeOf(bubble, role));\n return role === \"assistant\" ? htmlToMarkdown(clean) : (clean.textContent || \"\").trim();\n }\n function bubblePlain(bubble, role) {\n return (sanitizeClone(contentNodeOf(bubble, role)).textContent || \"\").trim();\n }\n\n // Build a single control: a primary \"Copy\" (markdown) plus a small caret that\n // toggles a menu with \"Copy as plain text\". All nodes carry the CONTROL_PREFIX\n // class so sanitizeClone removes them from any copied content.\n function buildControl(onMarkdown, onPlain) {\n var wrap = document.createElement(\"span\");\n wrap.className = CONTROL_PREFIX;\n var primary = document.createElement(\"button\");\n primary.type = \"button\";\n primary.className = CONTROL_PREFIX + \"-btn\";\n primary.title = \"Copy as Markdown\";\n primary.textContent = \"Copy\";\n primary.addEventListener(\"click\", function (e) { e.stopPropagation(); onMarkdown(primary); });\n var caret = document.createElement(\"button\");\n caret.type = \"button\";\n caret.className = CONTROL_PREFIX + \"-caret\";\n caret.title = \"Copy options\";\n caret.textContent = \"\u25be\"; // black down-pointing small triangle\n var menu = document.createElement(\"span\");\n menu.className = CONTROL_PREFIX + \"-menu\";\n menu.style.display = \"none\";\n var plain = document.createElement(\"button\");\n plain.type = \"button\";\n plain.className = CONTROL_PREFIX + \"-btn\";\n plain.textContent = \"Copy as plain text\";\n plain.addEventListener(\"click\", function (e) { e.stopPropagation(); menu.style.display = \"none\"; onPlain(plain); });\n menu.appendChild(plain);\n caret.addEventListener(\"click\", function (e) {\n e.stopPropagation();\n menu.style.display = menu.style.display === \"none\" ? \"inline-block\" : \"none\";\n });\n wrap.appendChild(primary);\n wrap.appendChild(caret);\n wrap.appendChild(menu);\n return wrap;\n }\n\n function decorate(bubble) {\n try {\n var role = classifyBubble(bubble);\n if (!role) return;\n if (qs(bubble, \".\" + CONTROL_PREFIX)) return; // already decorated\n var control = buildControl(\n function (host) { copyText(bubbleMarkdown(bubble, role)).then(function () { flashFeedback(control); }); },\n function (host) { copyText(bubblePlain(bubble, role)).then(function () { flashFeedback(control); }); }\n );\n bubble.appendChild(control);\n } catch (_) {}\n }\n\n function copyConversation(format) {\n var bubbles = qsa(USER_BUBBLE + \",\" + ASSISTANT_BUBBLE);\n if (format === \"text\") {\n var lines = [];\n for (var i = 0; i < bubbles.length; i++) {\n var role = classifyBubble(bubbles[i]);\n if (!role) continue;\n var body = bubblePlain(bubbles[i], role);\n if (body) lines.push(body);\n }\n return copyText(lines.join(\"\\n\\n\") + (lines.length ? \"\\n\" : \"\"));\n }\n return copyText(conversationToMarkdown(bubbles, function (b) {\n return contentNodeOf(b, classifyBubble(b));\n }));\n }\n\n function installConversationControl() {\n try {\n if (qs(document, \".\" + CONTROL_PREFIX + \"-conversation\")) return;\n var bar = document.createElement(\"div\");\n bar.className = CONTROL_PREFIX + \"-conversation\";\n var control = buildControl(\n function () { copyConversation(\"markdown\").then(function () { flashFeedback(bar); }); },\n function () { copyConversation(\"text\").then(function () { flashFeedback(bar); }); }\n );\n control.title = \"Copy entire conversation\";\n bar.appendChild(control);\n document.body.appendChild(bar); // fixed-position via CSS; placement refined in Task 6\n } catch (_) {}\n }\n\n function sweep() { var b = qsa(USER_BUBBLE + \",\" + ASSISTANT_BUBBLE); for (var i = 0; i < b.length; i++) decorate(b[i]); }\n\n function boot() {\n try {\n var target = (MESSAGES_CONTAINER && qs(document, MESSAGES_CONTAINER)) || document.body;\n sweep();\n installConversationControl();\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"; -const MD_COPY_CSS = ".cc-md-copy {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n vertical-align: middle;\n margin-left: 6px;\n}\n.cc-md-copy-btn,\n.cc-md-copy-caret {\n font: inherit;\n font-size: 11px;\n line-height: 1.4;\n padding: 1px 6px;\n color: var(--vscode-foreground);\n background: transparent;\n border: 1px solid var(--vscode-widget-border, transparent);\n border-radius: 4px;\n cursor: pointer;\n opacity: 0.65;\n}\n.cc-md-copy-btn:hover,\n.cc-md-copy-caret:hover {\n opacity: 1;\n background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15));\n}\n.cc-md-copy-menu {\n position: relative;\n margin-left: 4px;\n padding: 2px;\n background: var(--vscode-menu-background, var(--vscode-editorWidget-background));\n border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border, transparent));\n border-radius: 4px;\n z-index: 5;\n}\n.cc-md-copy-feedback {\n margin-left: 6px;\n font-size: 11px;\n opacity: 0.85;\n color: var(--vscode-foreground);\n}\n.cc-md-copy-conversation {\n position: fixed;\n right: 16px;\n bottom: 56px;\n z-index: 10;\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:hover {\n opacity: 1;\n}\n"; +const MD_COPY_JS = "/* 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\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\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
    /
      /
    (now handled, not flattened).\n return block({ childNodes: [root] }).replace(/\\n{3,}/g, \"\\n\\n\").trim();\n }\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\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\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\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\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\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\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\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\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\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\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\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\n function bubbleMarkdown(bubble, role) {\n var clean = sanitizeClone(contentNodeOf(bubble, role));\n return role === \"assistant\" ? htmlToMarkdown(clean) : (clean.textContent || \"\").trim();\n }\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 = '';\n var ICON_CHECK = '';\n\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\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; 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\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\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\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\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\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"; +const MD_COPY_CSS = ".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"; // <<>> ccwa-local-env >>> +// <<< 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 diff --git a/media/context-icon.png b/media/context-icon.png new file mode 100644 index 0000000..6901e4c Binary files /dev/null and b/media/context-icon.png differ diff --git a/media/markdown.png b/media/markdown.png new file mode 100644 index 0000000..cac13e3 Binary files /dev/null and b/media/markdown.png differ diff --git a/media/thinking.png b/media/thinking.png new file mode 100644 index 0000000..db38b26 Binary files /dev/null and b/media/thinking.png differ diff --git a/tests/test_md_clipboard.py b/tests/test_md_clipboard.py new file mode 100644 index 0000000..983b24d --- /dev/null +++ b/tests/test_md_clipboard.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""copyText honesty tests: the control may only report success when a copy +ACTUALLY happened. Regression for the original bug, where navigator.clipboard was +absent in the webview, the code fell through to Promise.resolve(), and the UI said +"Copied" while nothing was written. + +copyText tries a synchronous execCommand("copy") first (gesture-safe and +secure-context-independent, so it works over remote / code-server), then falls +back to the async Clipboard API. It resolves to a boolean: true ONLY on a real +copy. Empty text is never a copy. Run under node with mocked document/navigator +globals (no jsdom).""" +import json +import pathlib +import subprocess +import unittest + +REPO = pathlib.Path(__file__).resolve().parents[1] +INJECT_JS = REPO / "fixes" / "markdown-copy-export" / "webview-inject.js" + +# Mocks are installed AFTER require() so the module takes its node-export branch +# (document is undefined at load -> it does not boot()); copyText reads the globals +# at call time. exec_ok / write controls each path; `captured` records the text +# each mechanism received so we can assert the real payload, not just the verdict. +HARNESS = """ +const M = require(%(mod)s); +const cfg = %(cfg)s; +const captured = { exec: null, api: null }; +// defineProperty (not assignment): require() already ran with document undefined so +// the module took its export branch; and node's built-in `navigator` global is a +// getter (no setter), so a plain `global.navigator =` would silently no-op. +const DOC = { + activeElement: null, + getSelection: () => null, + createElement: (t) => ({ + tagName: t, value: "", style: {}, parentNode: null, + setAttribute() {}, focus() {}, + select() { captured.exec = this.value; }, + }), + documentElement: {}, + execCommand: () => cfg.exec_ok, +}; +DOC.body = { appendChild(n) { n.parentNode = DOC.body; }, removeChild(n) { n.parentNode = null; } }; +const NAV = cfg.clipboard === "absent" ? {} : { clipboard: { writeText: (s) => { + captured.api = s; + return cfg.clipboard === "resolve" ? Promise.resolve() : Promise.reject(new Error("blocked")); +} } }; +Object.defineProperty(globalThis, "document", { value: DOC, configurable: true, writable: true }); +Object.defineProperty(globalThis, "navigator", { value: NAV, configurable: true, writable: true }); +Promise.resolve(M.copyText(cfg.text)).then((ok) => { + process.stdout.write(JSON.stringify({ ok, captured })); +}); +""" + + +def run(text, exec_ok, clipboard): + cfg = {"text": text, "exec_ok": exec_ok, "clipboard": clipboard} + script = HARNESS % {"mod": json.dumps(str(INJECT_JS)), "cfg": json.dumps(cfg)} + res = subprocess.run(["node", "-e", script], cwd=REPO, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + assert res.returncode == 0, res.stderr + return json.loads(res.stdout) + + +class CopyTextHonestyTests(unittest.TestCase): + def test_empty_text_is_never_a_copy(self): + out = run("", exec_ok=True, clipboard="resolve") + self.assertFalse(out["ok"]) + self.assertIsNone(out["captured"]["exec"]) # never even attempted + self.assertIsNone(out["captured"]["api"]) + + def test_execcommand_path_succeeds_and_carries_the_text(self): + out = run("hello world", exec_ok=True, clipboard="resolve") + self.assertTrue(out["ok"]) + self.assertEqual(out["captured"]["exec"], "hello world") + self.assertIsNone(out["captured"]["api"]) # API never needed + + def test_falls_back_to_clipboard_api_when_execcommand_fails(self): + out = run("payload", exec_ok=False, clipboard="resolve") + self.assertTrue(out["ok"]) + self.assertEqual(out["captured"]["api"], "payload") + + def test_no_false_success_when_clipboard_absent_and_exec_fails(self): + # The exact original-bug environment: no Clipboard API, execCommand unusable. + out = run("payload", exec_ok=False, clipboard="absent") + self.assertFalse(out["ok"]) + + def test_no_false_success_when_clipboard_api_rejects(self): + out = run("payload", exec_ok=False, clipboard="reject") + self.assertFalse(out["ok"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_md_converter.py b/tests/test_md_converter.py index befdd68..227129e 100644 --- a/tests/test_md_converter.py +++ b/tests/test_md_converter.py @@ -110,6 +110,15 @@ def test_fenced_code_with_triple_backticks_uses_longer_fence(self): self.assertIn("````md", out) self.assertTrue(out.strip().endswith("````")) + def test_details_summary_separates_thinking_header_from_body(self): + out = convert( + "el('details',{},[" + " el('summary',{},[txt('Thought for 2s')])," + " el('div',{},[el('p',{},[txt('visible thinking summary')])])," + "])" + ) + self.assertIn("Thought for 2s\n\nvisible thinking summary", out) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_md_export.py b/tests/test_md_export.py index ed1390c..4d052c5 100644 --- a/tests/test_md_export.py +++ b/tests/test_md_export.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -"""Tests for cc-export.py: session resolution, markdown/text rendering, opt-in -thinking/tools, and --open. Synthetic JSONL pins the documented row schema -without needing a real transcript.""" +"""Tests for cc-export.py: session resolution, markdown/text rendering, and +opt-in thinking/tools. Synthetic JSONL pins the documented row schema without +needing a real transcript.""" import importlib.util import json import os import pathlib -import stat import tempfile import unittest @@ -113,34 +112,11 @@ def test_resolve_missing_returns_none(self): self.mod.resolve_session(config=str(config), cwd=str(cwd), session_id="ghost") ) - def test_open_invokes_editor_with_resolved_path(self): - # Stub `code` on PATH; assert main(--open) calls it with the jsonl path. - with tempfile.TemporaryDirectory() as td: - config = pathlib.Path(td) / "config" - projects = config / "projects" - cwd = pathlib.Path(td) / "work" / "proj" - cwd.mkdir(parents=True) - key = self.mod.project_key_for_cwd(str(cwd)) - sess = write_session(projects, key, "s1", [ROWS[0]]) - bindir = pathlib.Path(td) / "bin" - bindir.mkdir() - capture = pathlib.Path(td) / "opened.txt" - code = bindir / "code" - code.write_text( - "#!/usr/bin/env bash\nprintf '%s' \"$1\" > \"$CC_OPEN_CAPTURE\"\n", - encoding="utf-8", - ) - code.chmod(code.stat().st_mode | stat.S_IXUSR) - env = dict(os.environ) - env["PATH"] = str(bindir) + os.pathsep + env["PATH"] - env["CC_OPEN_CAPTURE"] = str(capture) - rc = self.mod.main( - ["--open", "--cwd", str(cwd)], - config=str(config), - env=env, - ) - self.assertEqual(rc, 0) - self.assertEqual(capture.read_text(encoding="utf-8"), str(sess)) + def test_open_flag_is_removed(self): + # The "open the raw .jsonl in the editor" feature was dropped; --open must + # no longer be a recognized argument (argparse exits nonzero on unknown opt). + with self.assertRaises(SystemExit): + self.mod.main(["--open"], config="/nonexistent", env=dict(os.environ)) def test_fence_uses_longer_delimiter_when_body_has_backticks(self): # a tool/thinking payload that itself contains a ``` run must not close the diff --git a/tests/test_md_inject.py b/tests/test_md_inject.py index 0652871..5404860 100644 --- a/tests/test_md_inject.py +++ b/tests/test_md_inject.py @@ -31,8 +31,8 @@ class InjectHelperTests(unittest.TestCase): def test_sanitize_strips_all_chrome_and_asserts_clean(self): # Fixture shaped like a real assistant bubble: a text content block plus the # sibling chrome the live DOM puts inside it (our controls, the rating - # widget, and the v1-excluded tool/thinking/unknown blocks). Sanitize must - # remove ALL of it and leave the original untouched. + # widget, and excluded tool/unknown blocks). Sanitize must remove all chrome, + # keep visible thinking summaries, and leave the original untouched. out = run_node( "const content = el('div',{'data-testid':'assistant-message'},[" " el('p',{},[txt('keep me')])," @@ -40,7 +40,7 @@ def test_sanitize_strips_all_chrome_and_asserts_clean(self): " el('div',{class:'cc-md-copy-feedback'},[txt('Copied')])," " el('div',{class:'toolUse_uq5aLg'},[txt('ran a tool')])," " el('div',{class:'toolResult_uq5aLg'},[txt('tool output')])," - " el('div',{class:'thinking_aHyQPQ'},[txt('secret thoughts')])," + " el('div',{class:'thinking_aHyQPQ'},[txt('visible thinking summary')])," " el('div',{class:'unknownContent_uq5aLg'},[txt('Unsupported content')])," " el('div',{'data-message-rating':'0'},[txt('Thanks for your feedback')])," "]);" @@ -54,23 +54,75 @@ def test_sanitize_strips_all_chrome_and_asserts_clean(self): " if(tag==='BUTTON')acc.button++;" " if(c.getAttribute&&c.getAttribute('data-message-rating')!==null)acc.rating++;" " if(/(^|\\s)(toolUse_|toolResult_|toolReference_)/.test(cls))acc.tool++;" - " if(/(^|\\s)thinking_/.test(cls))acc.thinking++;" " if(/(^|\\s)unknownContent_/.test(cls))acc.unknown++;" " residue(c, acc);}return acc;}" - "const left = residue(clean,{button:0,rating:0,tool:0,thinking:0,unknown:0});" + "const left = residue(clean,{button:0,rating:0,tool:0,unknown:0});" "const origIntact = (content.childNodes.length === 8);" "process.stdout.write(JSON.stringify({cleanMd, left, origIntact}));" ) data = json.loads(out) self.assertIn("keep me", data["cleanMd"]) + self.assertIn("visible thinking summary", data["cleanMd"]) for leak in ["Copy", "Copied", "ran a tool", "tool output", - "secret thoughts", "Unsupported content", "Thanks for your feedback"]: + "Unsupported content", "Thanks for your feedback"]: self.assertNotIn(leak, data["cleanMd"]) # assert-clean: zero chrome nodes remain after sanitize - self.assertEqual(data["left"], {"button": 0, "rating": 0, "tool": 0, "thinking": 0, "unknown": 0}) + self.assertEqual(data["left"], {"button": 0, "rating": 0, "tool": 0, "unknown": 0}) # sanitize works on a clone; the original is untouched self.assertTrue(data["origIntact"]) + def test_copyable_content_keeps_thinking_but_skips_tool_only_turns(self): + out = run_node( + "const thinking = el('div',{'data-testid':'assistant-message'},[" + " el('details',{class:'thinking_aHyQPQ'},[" + " el('summary',{},[txt('Thought for 2s')])," + " el('div',{class:'thinkingContent_aHyQPQ'},[el('p',{},[txt('visible thinking summary')])])," + " ])," + "]);" + "const toolOnly = el('div',{'data-testid':'assistant-message'},[" + " el('div',{class:'toolUse_uq5aLg'},[txt('Bash')])," + "]);" + "process.stdout.write(JSON.stringify([" + " M.hasCopyableContent(thinking, 'assistant')," + " M.hasCopyableContent(toolOnly, 'assistant')" + "]));" + ) + self.assertEqual(json.loads(out), [True, False]) + + def test_sanitize_accepts_browser_nodelist_childnodes(self): + out = run_node( + "function nodeList(items){" + " var list={length:items.length,item:function(i){return this[i]||null;}};" + " for(var i=0;i=50 gate +# restored but the bare marker left behind (renamed Z/U vars). undo must strip the +# orphan marker so apply re-patches; otherwise apply early-exits on the marker and +# the icon stays hidden. +ALT_WEDGED = ALT_OLD + LEGACY_MARKER +# Upstream code that merely RESEMBLES a patched value: a bare >=101 guard on a +# renamed var, with NO marker and NO ===0 prefix. We never write this, so undo must +# leave it untouched (ownership invariant), never rewrite it to >=50. +UNOWNED_BARE101 = "if(U>=101)return null}" BAK = ".bak-cc-workarounds" MD_OPEN = "/* cc-md-copy v1 */" MD_CLOSE = "/* /cc-md-copy v1 */" @@ -86,6 +103,18 @@ def test_apply_writes_marked_form_and_pristine_backup(self): self.assertTrue(bak.exists()) self.assertEqual(bak.read_text(encoding="utf-8"), f"before {OLD} after") + def test_apply_accepts_renamed_minified_guard_vars(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_OLD} after") + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_MARKED} after") + self.assertNotIn("anchor not found", res.stderr) + bak = idx.with_name(idx.name + BAK) + self.assertTrue(bak.exists()) + self.assertEqual(bak.read_text(encoding="utf-8"), f"before {ALT_OLD} after") + def test_reconcile_is_idempotent_and_leaves_no_temp_files(self): with tempfile.TemporaryDirectory() as td: home = pathlib.Path(td) @@ -110,6 +139,15 @@ def test_disabling_feature_reverts_only_that_feature(self): self.assertEqual(res.returncode, 0, res.stderr) self.assertEqual(idx.read_text(encoding="utf-8"), f"before {OLD} after") + def test_disabling_feature_reverts_renamed_var_marker_to_same_vars(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_MARKED} after") + res = self._run(td, home, env_extra={"CC_PATCH_CONTEXT_ICON": "0"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_OLD} after") + self.assertNotIn("anchor not found", res.stderr) + def test_master_switch_reverts_all_and_injects_nothing(self): with tempfile.TemporaryDirectory() as td: home = pathlib.Path(td) @@ -123,11 +161,10 @@ def test_master_switch_reverts_all_and_injects_nothing(self): def test_legacy_bare_patch_is_upgraded_to_marked_when_enabled(self): # A bundle left by the OLD launcher/standalone carries the bare, - # unmarked >=101 form. `c` maxes at 100, so >=101 is dead upstream code - # that only ever appears as our own output; reconcile adopts it as a - # legacy fingerprint, upgrading it to the marked form and capturing the - # correct pristine (>=50) snapshot - without the spurious "anchor not - # found" warning the marker-only path used to emit on every launch. + # unmarked >=101 form while still keeping the old t===0 guard. Reconcile + # adopts it as a legacy fingerprint, upgrading it to the current marked + # form and capturing the correct pristine combined guard - without the + # spurious "anchor not found" warning the marker-only path used to emit. with tempfile.TemporaryDirectory() as td: home = pathlib.Path(td) idx = make_extension(home, f"before {BARE101} after") @@ -139,6 +176,82 @@ def test_legacy_bare_patch_is_upgraded_to_marked_when_enabled(self): self.assertTrue(bak.exists()) self.assertEqual(bak.read_text(encoding="utf-8"), f"before {OLD} after") + def test_legacy_marked_patch_is_upgraded_to_show_icon_during_reload_gap(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {LEGACY_MARKED} after") + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {MARKED} after") + bak = idx.with_name(idx.name + BAK) + self.assertTrue(bak.exists()) + self.assertEqual(bak.read_text(encoding="utf-8"), f"before {OLD} after") + + def test_legacy_current_marked_patch_is_upgraded_to_metadata_marker(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {LEGACY_CURRENT_MARKED} after") + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {MARKED} after") + self.assertNotIn("anchor not found", res.stderr) + + def test_legacy_bare_marked_patch_with_renamed_vars_is_upgraded(self): + # A bare (metadata-less) marker on a non-t/c build (Z/U). Undo must match + # the guard pair by shape and strip the marker so apply re-patches; the + # t/c-only fallback otherwise leaves the marker, and apply then exits early + # on it, committing the original >=50 gate (icon stays hidden). + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_LEGACY_MARKED} after") + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_MARKED} after") + self.assertNotIn("anchor not found", res.stderr) + + def test_legacy_bare_marked_patch_with_renamed_vars_reverts_when_disabled(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_LEGACY_MARKED} after") + res = self._run(td, home, env_extra={"CC_PATCH_CONTEXT_ICON": "0"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_OLD} after") + self.assertNotIn("anchor not found", res.stderr) + + def test_already_wedged_bare_marker_is_healed_and_repatched(self): + # An OLD buggy undo could leave the file already wedged: the >=50 gate + # restored but the bare marker still appended (renamed Z/U vars). undo's + # early-out must not skip the orphan-marker strip, or apply early-exits on + # the leftover marker (icon stays hidden). The launcher must heal this on + # the next run: strip the orphan marker, then re-patch to the marked form. + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_WEDGED} after") + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_MARKED} after") + + def test_already_wedged_bare_marker_reverts_to_pristine_when_disabled(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {ALT_WEDGED} after") + res = self._run(td, home, env_extra={"CC_PATCH_CONTEXT_ICON": "0"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {ALT_OLD} after") + + def test_unowned_bare_101_guard_is_left_untouched(self): + # Ownership invariant: a bare >=101 guard with no marker and no ===0 prefix + # is not ours (we always write a marker). undo must NOT rewrite it to >=50 - + # that corrupts upstream code that merely resembles a patched value. + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + original = f"before {UNOWNED_BARE101} after" + idx = make_extension(home, original) + res = self._run(td, home) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), original) + self.assertNotIn(">=50)return null}", idx.read_text(encoding="utf-8")) + def test_legacy_bare_patch_is_reverted_when_feature_disabled(self): # Migration-table promise: disabling the fix reverts our edit. A legacy # bare patch must revert to pristine just like a marked one does. @@ -150,6 +263,15 @@ def test_legacy_bare_patch_is_reverted_when_feature_disabled(self): self.assertEqual(idx.read_text(encoding="utf-8"), f"before {OLD} after") self.assertNotIn("anchor not found", res.stderr) + def test_legacy_marked_patch_is_reverted_when_feature_disabled(self): + with tempfile.TemporaryDirectory() as td: + home = pathlib.Path(td) + idx = make_extension(home, f"before {LEGACY_MARKED} after") + res = self._run(td, home, env_extra={"CC_PATCH_CONTEXT_ICON": "0"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(idx.read_text(encoding="utf-8"), f"before {OLD} after") + self.assertNotIn("anchor not found", res.stderr) + def test_legacy_bare_patch_is_reverted_by_master_switch(self): with tempfile.TemporaryDirectory() as td: home = pathlib.Path(td) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 33560d8..65c9c0b 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -12,8 +12,17 @@ REPO = pathlib.Path(__file__).resolve().parents[1] -OLD_ICON = ">=50)return null}" -NEW_ICON = ">=101)return null}" +OLD_ICON = "if(t===0)return null;if(c>=50)return null}" +NEW_ICON = "if(c>=101)return null}/*ccwa-context-icon:t:c*/" +ALT_OLD_ICON = "if(Z===0)return null;if(U>=50)return null}" +ALT_NEW_ICON = "if(U>=101)return null}/*ccwa-context-icon:Z:U*/" +ALT_LEGACY_MARKED_ICON = "if(Z===0)return null;if(U>=101)return null}/*ccwa-context-icon*/" +# Already-wedged state: pristine >=50 gate restored but the bare marker left behind +# (renamed Z/U vars). undo must strip the orphan marker so patch_file re-applies. +ALT_WEDGED_ICON = "if(Z===0)return null;if(U>=50)return null}/*ccwa-context-icon*/" +# Upstream code that resembles a patched value: bare >=101, no marker, no ===0 +# prefix. Not ours - undo must leave it untouched (never rewrite to >=50). +UNOWNED_BARE101_ICON = "if(U>=101)return null}" def run(cmd, *, env=None, cwd=REPO, timeout=10): @@ -184,6 +193,115 @@ def test_windows_thinking_launchers_resolve_cmd_shims_without_shell(self): ], ) + # CC_SCRUB_ROUTING clears third-party model-routing env vars before launch so + # Claude Code lands on the default Anthropic account. Default off (env intact). + ROUTING_KEYS = [ + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "CLAUDE_CONFIG_DIR", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + ] + + @staticmethod + def _routing_env(td): + return { + "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", + "ANTHROPIC_AUTH_TOKEN": "secret-token", + "CLAUDE_CONFIG_DIR": str(pathlib.Path(td) / "cfg"), + "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro", + } + + @unittest.skipIf(os.name == "nt", "POSIX Bash launcher test") + def test_bash_launcher_scrubs_routing_env_only_when_enabled(self): + keys = self.ROUTING_KEYS + with tempfile.TemporaryDirectory() as td: + fake = pathlib.Path(td) / "claude" + capture = pathlib.Path(td) / "env.json" + fake.write_text( + "#!/usr/bin/env bash\n" + "python3 - <<'PY'\n" + "import json, os\n" + "keys = json.loads(os.environ['CAPTURE_KEYS'])\n" + "data = {}\n" + "for k in keys:\n" + " data[k] = os.environ.get(k)\n" + "open(os.environ['CAPTURE_ENV'], 'w').write(json.dumps(data))\n" + "PY\n", + encoding="utf-8", + ) + fake.chmod(fake.stat().st_mode | stat.S_IXUSR) + + routing = self._routing_env(td) + base = { + "HOME": td, # keep reconcile away from real webview bundles + "CC_RECONCILE": "0", # do not read or write any bundle this launch + "CLAUDE_REAL_BIN": str(fake), + "CAPTURE_ENV": str(capture), + "CAPTURE_KEYS": json.dumps(keys), + **routing, + } + launcher = str(REPO / "launcher" / "claudemax") + + res = run([launcher], env=base) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(json.loads(capture.read_text(encoding="utf-8")), routing) + + res = run([launcher], env={**base, "CC_SCRUB_ROUTING": "1"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual( + json.loads(capture.read_text(encoding="utf-8")), + {k: None for k in keys}, + ) + + def test_windows_launcher_scrubs_routing_env_only_when_enabled(self): + keys = self.ROUTING_KEYS + with tempfile.TemporaryDirectory() as td: + temp = pathlib.Path(td) + capture = temp / "env.json" + cli = temp / "cli.js" + cli.write_text( + "const fs = require('fs');\n" + "const keys = JSON.parse(process.env.CAPTURE_KEYS);\n" + "const out = {};\n" + "for (const k of keys) out[k] = (k in process.env) ? process.env[k] : null;\n" + "fs.writeFileSync(process.env.CAPTURE_ENV, JSON.stringify(out));\n", + encoding="utf-8", + ) + shim = make_fake_cmd_shim(td, cli) + + routing = self._routing_env(td) + base = { + "HOME": td, + "USERPROFILE": td, + "CC_RECONCILE": "0", + "CLAUDE_REAL_BIN": str(shim), + "CAPTURE_ENV": str(capture), + "CAPTURE_KEYS": json.dumps(keys), + **routing, + } + launcher = str(REPO / "launcher" / "claudemax.win.js") + + res = run(["node", launcher], env=base) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual(json.loads(capture.read_text(encoding="utf-8")), routing) + + res = run(["node", launcher], env={**base, "CC_SCRUB_ROUTING": "1"}) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertEqual( + json.loads(capture.read_text(encoding="utf-8")), + {k: None for k in keys}, + ) + + def test_launchers_expose_local_env_injection_anchor(self): + # The marker pair is a stable contract: the Linux deploy step and the + # Windows build.ps1 splice a private env file between these lines. + for name in ("claudemax", "claudemax.win.js"): + with self.subTest(launcher=name): + src = (REPO / "launcher" / name).read_text(encoding="utf-8") + self.assertIn("CC_SCRUB_ROUTING", src) + self.assertIn(">>> ccwa-local-env >>>", src) + self.assertIn("<<< ccwa-local-env <<<", src) + class ProxyRegressionTests(unittest.TestCase): def test_proxy_exports_header_filters_that_strip_hop_by_hop_headers(self): @@ -255,12 +373,78 @@ def test_fix_context_icon_atomic_replace_preserves_metadata_and_docs_limitation( self.assertEqual(after.st_uid, before.st_uid) self.assertEqual(after.st_gid, before.st_gid) patched_text = target.read_text(encoding="utf-8") - self.assertIn("/*ccwa-context-icon*/", patched_text) + self.assertIn("/*ccwa-context-icon", patched_text) self.assertEqual(patched_text, f"before {mod.NEW} after") self.assertTrue((pathlib.Path(str(target) + mod.BACKUP_SUFFIX)).exists()) # Idempotent: a second patch is a no-op. self.assertEqual(mod.patch_file(str(target)), "already-patched") + def test_fix_context_icon_patches_renamed_minified_guard_vars(self): + spec = importlib.util.spec_from_file_location( + "fix_context_icon", REPO / "fixes" / "context-icon" / "fix-context-icon.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + with tempfile.TemporaryDirectory() as td: + target = pathlib.Path(td) / "index.js" + target.write_text(f"before {ALT_OLD_ICON} after", encoding="utf-8") + + self.assertEqual(mod.patch_file(str(target)), "PATCHED") + self.assertEqual(target.read_text(encoding="utf-8"), f"before {ALT_NEW_ICON} after") + backup = pathlib.Path(str(target) + mod.BACKUP_SUFFIX) + self.assertEqual(backup.read_text(encoding="utf-8"), f"before {ALT_OLD_ICON} after") + self.assertEqual(mod.patch_file(str(target)), "already-patched") + + def test_fix_context_icon_upgrades_renamed_bare_marked_patch(self): + # A bundle left by an older var-agnostic patcher carries a BARE + # (metadata-less) marker on non-t/c guard vars. undo_known_patches must + # recognize it by shape, not the fixed t/c, so patch_file re-applies and + # upgrades it to the metadata marker rather than reporting gate-not-found. + spec = importlib.util.spec_from_file_location( + "fix_context_icon", REPO / "fixes" / "context-icon" / "fix-context-icon.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + with tempfile.TemporaryDirectory() as td: + target = pathlib.Path(td) / "index.js" + target.write_text(f"before {ALT_LEGACY_MARKED_ICON} after", encoding="utf-8") + self.assertEqual(mod.patch_file(str(target)), "PATCHED") + self.assertEqual(target.read_text(encoding="utf-8"), f"before {ALT_NEW_ICON} after") + + def test_fix_context_icon_heals_already_wedged_bare_marker(self): + # An older buggy undo path could leave the file wedged: the >=50 gate + # restored but the bare marker still appended (renamed Z/U vars). + # undo_known_patches must strip the orphan marker so patch_file re-applies + # rather than treating the leftover marker as already-patched. + spec = importlib.util.spec_from_file_location( + "fix_context_icon", REPO / "fixes" / "context-icon" / "fix-context-icon.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + with tempfile.TemporaryDirectory() as td: + target = pathlib.Path(td) / "index.js" + target.write_text(f"before {ALT_WEDGED_ICON} after", encoding="utf-8") + self.assertEqual(mod.patch_file(str(target)), "PATCHED") + self.assertEqual(target.read_text(encoding="utf-8"), f"before {ALT_NEW_ICON} after") + + def test_fix_context_icon_leaves_unowned_bare_101_untouched(self): + # Ownership invariant: a bare >=101 guard with no marker and no ===0 prefix + # is not ours. undo must not rewrite it to >=50; patch_file reports the + # anchor as missing and leaves the file unchanged. + spec = importlib.util.spec_from_file_location( + "fix_context_icon", REPO / "fixes" / "context-icon" / "fix-context-icon.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + with tempfile.TemporaryDirectory() as td: + target = pathlib.Path(td) / "index.js" + original = f"before {UNOWNED_BARE101_ICON} after" + target.write_text(original, encoding="utf-8") + self.assertIn("gate-not-found", mod.patch_file(str(target))) + self.assertEqual(target.read_text(encoding="utf-8"), original) + def test_patch_extension_avoids_bash4_mapfile(self): source = (REPO / "fixes" / "thinking-summaries" / "patch-extension.sh").read_text(encoding="utf-8") self.assertNotIn("mapfile", source)