* fix(cta): fall back to textarea+execCommand when Clipboard API rejects
The "Setup a new Claude Code" CTA fetches /auth/tokens, parses the JSON
response, renders the setup script, THEN calls
`navigator.clipboard.writeText()`. Modern browsers (Safari, Firefox, and
Chrome on stricter configurations) reject `writeText` with
NotAllowedError when transient user activation has been consumed by an
intervening `await` — which is exactly the case here. Users perceived
this as "the browser blocked the copy" and got the manual-paste fallback
modal even though the textarea + `document.execCommand('copy')` path
WOULD have worked synchronously without needing fresh user activation.
`copyToClipboard` now:
- prefers the modern Clipboard API (unchanged for the happy path)
- on writeText rejection, falls back to `copyViaTextarea` instead of
surfacing the rejection to the caller's catch block.
`copyViaTextarea` is the previously-inline textarea fallback factored
out into a named helper, with two small hardening touches:
- `readonly` + `tabindex=-1` so the hidden textarea doesn't steal
focus or pop the virtual keyboard on mobile.
- explicit `setSelectionRange(0, text.length)` to belt-and-braces the
selection on iOS Safari (where `.select()` alone sometimes selects
zero chars on touch-focused textareas).
Only the CTA button needed this — the Step-1 install-command and the
connector-copy buttons all call `writeText` synchronously inside the
click handler (no awaits in between), so they keep their existing
user-gesture context and didn't hit the same rejection. No template
changes there.
* refactor(home): fold Atlassian MCP registration into connectors block
The standalone "Register the Atlassian MCP server" step (was step 6 in
the unified setup script) moves INTO the Atlassian connector's prompt
body so all Atlassian-related setup lives in one logical group. Same
intent that #247 carried for connectors, applied one level deeper:
the hosted Remote MCP registration is part of "set up Atlassian", not
its own ungrouped step.
What changed:
- `app/web/connector_prompts.py` — the Atlassian prompt's step 5
replaces the speculative "Register the on-demand Atlassian MCP under
.claude/mcp/atlassian" line with the actual hosted Remote MCP
registration: `claude mcp add --transport sse atlassian
https://mcp.atlassian.com/v1/sse || true`. The `|| true` keeps re-runs
idempotent and the body explains the OAuth-on-first-use contract.
Both /home's Atlassian tile and the inlined setup-script Atlassian
sub-block emit this line — single source of truth holds.
- `app/web/setup_instructions.py` — `_mcp_servers_block` deleted; the
`mcp_servers` step is removed from `_step_numbers`; resolve_lines no
longer calls it.
- Renumbering: install (1), init (2), catalog (3), preflight (4),
marketplace (5), diagnose (6), connectors (7), confirm (8). Was:
6 = mcp_servers, 7 = diagnose, 8 = connectors, 9 = confirm.
- `tests/test_setup_instructions.py` — Confirm step 9→8, Connect 8→7,
diagnose 7→6, mcp_servers references dropped.
`test_step_numbering_with_connectors_step` now asserts
`"mcp_servers" not in steps`. Stray-Confirm assertion lists shift
by one position.
- `tests/test_setup_page_unified.py` + `tests/test_web_ui.py` — same
step-number shifts in the rendered /setup preview assertions.
The `claude mcp add` line is still the Atlassian Remote-MCP path that
the 2026-05-10 init-report Fix C added — only its position in the
flow changes. /home Atlassian tile copying continues to install the
MCP too (the prompt body the tile pastes contains the same line).
112 tests pass.
* feat(atlassian): operator-overrideable base URL via AGNES_ATLASSIAN_BASE_URL
Adds an env var / YAML key the operator (Terraform module, customer-VM
template, OSS instance.yaml) can set to bake the Atlassian Cloud site
root into the connector prompt — so end users don't have to guess /
paste their org's `https://<myorg>.atlassian.net`.
When set, the Atlassian connector prompt (rendered on both /home tile
and inlined into the setup-script step 7 Atlassian sub-block) replaces
step 1's "Ask me for my Atlassian Cloud site URL and email" with a
one-line note that the URL is already provisioned by the operator and
asks only for the email. Step 4's helper-script body has the
`BASE_URL='<the site URL I gave you>'` placeholder substituted with
the literal value. When unset (empty), the existing "ask the user"
flow remains — no regression for OSS instances.
Resolution + normalization in `get_atlassian_base_url()`:
- env `AGNES_ATLASSIAN_BASE_URL` > yaml `instance.atlassian.base_url` > ""
- strips trailing slash + trailing `/wiki` so the canonical value is
the bare site root. Matches the per-user helper script's
normalization at storage time (atlassian_prompt step 4 guard 2), so
the literal baked in by the operator stays consistent with what the
user's helper script would have computed from their input.
Plumbing:
- `app/instance_config.py`: new `get_atlassian_base_url()` resolver.
- `app/web/connector_prompts.py`:
- `atlassian_prompt(*, base_url: str = "")` — string-replace two
explicit placeholder phrases when base_url is truthy; otherwise
return the prompt unchanged.
- `all_connector_prompts(..., atlassian_base_url: str = "")` —
forwards the kwarg.
- `app/web/router.py` (`_build_context`): reads
`get_atlassian_base_url()` and passes it through to
`all_connector_prompts(...)` so both the /home tile context AND the
inlined-script `resolve_lines(...)` call use the same value.
- `src/welcome_template.py` (`compute_default_agent_prompt`): same
threading via the existing import-on-demand path.
Tests (`tests/test_home_route_resolution.py`):
- `get_atlassian_base_url` resolver: default empty, env override,
trailing-slash strip, trailing-`/wiki` strip.
- `atlassian_prompt(base_url=...)`: literal URL baked in, ask-step
removed, placeholder replaced, operator-baked-in copy appears.
- `atlassian_prompt(base_url="")`: existing ask-the-user flow
unchanged.
- `all_connector_prompts(atlassian_base_url=...)`: kwarg threads
through to the rendered atlassian prompt.
135 tests pass.
* feat(asana): register hosted Asana Remote MCP in connector prompt
The Asana connector prompt only stored a PAT in the OS keychain + ran
a curl verify against /api/1.0/users/me. That set Claude Code up for
direct `curl` calls but didn't actually wire Asana into Claude's tool
list — so the user couldn't ask Claude to "find my open Asana tasks"
and have it work. Symmetric oversight to the Atlassian connector's
original speculative `.claude/mcp/atlassian` line that this branch
already replaced with `claude mcp add --transport sse atlassian
https://mcp.atlassian.com/v1/sse`.
Adds a new step 5 that registers Asana's hosted Remote MCP:
claude mcp add --transport http asana https://mcp.asana.com/mcp || true
This is the V2 endpoint (streamable HTTP transport, launched February
2026). The V1 SSE endpoint at https://mcp.asana.com/sse was deprecated
2026-05-11 (today) and must NOT be used — calling it out explicitly
in the prompt body so a future operator who finds an old reference
doesn't paste the dead URL. OAuth is handled by Claude Code at first
use, same model as the Atlassian MCP step.
The PAT stored in step 3 stays for direct `curl` calls (precheck +
ad-hoc scripts) — the MCP path uses its own OAuth grant, not the PAT.
Old step 5 (revoke instructions) renumbers to step 6 and adds the
`claude mcp remove asana` cleanup hint.
Same single-source-of-truth invariant holds: /home Asana tile + the
inlined Asana sub-block in the setup script (step 7 connectors) both
emit identical text from `asana_prompt()`.
71 tests pass.
* feat(asana): drive MCP OAuth login + end-to-end validation post-register
`claude mcp add --transport http asana ...` only registers the
server in Claude Code's local config — it does NOT trigger OAuth.
The browser tab opens the first time any `mcp__asana__*` tool gets
invoked. So the previous step 5 left a user looking at a "registered"
MCP that, in practice, hadn't authed yet and would fail on first
real use. Same blind spot Atlassian's prompt also has, but Asana was
the one called out in the latest review pass.
Adds a new step 6 between MCP registration (step 5) and the revoke
instructions (now step 7):
a. Tell the user verbatim what's about to happen — a low-impact
read through the MCP will pop the OAuth browser tab; sign in
with the same account whose PAT they stored in step 3 and
approve. Frames the OAuth as one-time so users don't wait
for it on every later call.
b. Drive an actual MCP read. Don't prescribe the exact tool name
because the Asana MCP's exposed surface (`mcp__asana__*`) is
versioned upstream and we don't want to pin to a name that
gets renamed. Instead: tell Claude to pick the lightest read
from its surfaced tool list (users-me / list-workspaces /
equivalent). Document the recovery path when Claude Code
times out waiting for the OAuth tool use: `claude mcp list`
to confirm registration before retrying.
c. Print a single one-line proof that combines wiring + auth:
"Asana MCP connected as <name> — <N> workspace(s) visible."
Explicit anti-echo callout for tokens, task content, comments.
On failure, surface the exact Claude-Code error and stop —
no silent pass.
d. Sanity-check that the MCP OAuth identity and the PAT identity
reference the same Asana account. Easy mistake to make when
the user has multiple Asana accounts — flag only on mismatch,
keep quiet when they match. Recovery: `claude mcp remove asana
&& claude logout asana` then redo step 5.
Step 7 (revoke) absorbs both the keychain delete + the
`claude logout asana` line so users have a single place to undo
everything.
43 tests pass.
* fix(init): clear stale CA env vars on Windows before any TLS handshake
Reported by the 2026-05-11 Windows test pass: after `agnes init` the
gws connector failed with `UnknownIssuer` TLS errors because
`SSL_CERT_FILE` and `REQUESTS_CA_BUNDLE` were still set in Windows
User scope pointing at `C:\Users\localadmin\.config\agnes\ca-bundle.pem`
— a file that did not exist on the test host. Past Agnes installs
(the setup-prompt trust block + older bootstrap helpers) write those
pointers when they materialize a combined Agnes-CA bundle; when the
bundle file later disappears (re-init on a new VM, machine swap, the
~/.agnes dir wiped), the pointers go stale and every native Windows
TLS handshake fails before Agnes itself runs. SSL_CERT_FILE in
particular REPLACES (not appends to) the trust store, so a stale
pointer is silently catastrophic.
`agnes init` now clears stale pointers in two layers before the first
server roundtrip:
1. Current-process env (os.environ) — what the immediately-following
`api_get` to /api/catalog/tables actually reads. Without this, init
itself blows up before it gets to step 2.
2. Windows User-scope env via PowerShell
`[Environment]::SetEnvironmentVariable(name, $null, 'User')` — what
every future shell + every native tool (gws, claude.exe, pip, uv)
inherits. The 2026-05-11 reporter expected this exact cleanup
("init was supposed to clear these but they persisted").
The cleanup is best-effort and conservative:
- Only deletes a var when its value points at a path that does NOT
exist on disk. Intentional operator config (e.g. SSL_CERT_FILE
pointing at a corp certifi bundle) stays put.
- PowerShell missing / restricted execution policy / WSL-without-pwsh:
swallowed silently. The current-process leg still runs, which
unblocks init even on hosts where the User-scope leg cannot fire.
Tests (`tests/test_init_ca_cleanup.py`, 6 cases):
- Stale pointers → removed from process env.
- Real-path pointers → preserved.
- Non-Windows hosts: PowerShell is not invoked.
- Windows hosts: PowerShell IS invoked with a script that checks
all three vars + uses Test-Path + SetEnvironmentVariable.
- PowerShell FileNotFoundError: cleanup swallows it, does not raise.
- `_is_windows_host()` reflects sys.platform.
* refactor(asana): MCP-first flow — drop PAT storage, precheck via `claude mcp list`
The Asana hosted MCP at https://mcp.asana.com/mcp authenticates via
OAuth (Claude Code holds the grant; browser tab pops on first tool
use). The earlier prompt walked the user through creating + keychain-
storing an Asana Personal Access Token AND registering the MCP — two
parallel auth surfaces for one connector. Once the MCP works, the PAT
has no consumer: the precheck/verify steps that used `curl
$BASE/api/1.0/users/me` are just redundant proof that Asana itself is
reachable, which the OAuth handshake already establishes.
Removed:
- Step 0 keychain probe + curl verify against /users/me with PAT.
- Step 1 open developer-console / create PAT.
- Step 2 click "+ New access token", warn shown-ONCE.
- Step 3 helper-script for keychain-storage (per-OS bodies: macOS
`security add-generic-password`, Linux `secret-tool store`, Windows
`cmdkey /generic`).
- Step 4 PAT-side `users/me` verify.
- Step 5's split that kept the PAT around for direct curl scripts.
- Step 6d's "MCP vs PAT identity sanity check" — there is no PAT
anymore, nothing to mismatch against.
New flow (3 steps total):
- Step 0 precheck: `claude mcp list | grep ^asana` — if found, the
server is registered AND Claude Code is holding its OAuth grant
(otherwise prior failure would have removed it); print
"Asana MCP already registered — skipping setup" and stop. Tells the
user the explicit reset command (`claude mcp remove asana && claude
logout asana`) so a re-register stays one paste.
- Step 1: `claude mcp add --transport http asana
https://mcp.asana.com/mcp` — no `|| true` because step 0 should have
caught the "already exists" case. Step explains the V2-vs-V1
endpoint distinction (V1 SSE deprecated 2026-05-11) and the
abort-clean recovery if the precheck somehow missed the existing
server.
- Step 2: same OAuth + low-impact-read validation pattern as before.
- Step 3: revoke instructions (mcp remove + logout + Asana-side app
revoke at app.asana.com/Settings → Apps).
Both surfaces (the /home Asana tile and the inlined Asana sub-block
in the setup script's step 7) emit the new text from the same
asana_prompt() — single-source-of-truth invariant intact.
77 tests pass.
287 lines
11 KiB
Django/Jinja
287 lines
11 KiB
Django/Jinja
{# Shared "Setup a new Claude Code" behaviour.
|
|
|
|
Provides the JS + CSS that drives any page's "Setup a new Claude Code"
|
|
button. The button itself is rendered by the consuming page (because
|
|
each page wraps it in its own card chrome — dashboard's blue CTA card,
|
|
/home's hero, /install's section). The contract is:
|
|
|
|
- A button with `onclick="setupNewClaude(this)"` and ideally id
|
|
`setupClaudeBtn` so the visible-state transitions read sensibly.
|
|
- Optional `<div id="setupClaudeError">` for inline error reporting.
|
|
If absent, errors fall back to `alert()`.
|
|
|
|
The clipboard payload is single-sourced through
|
|
`_claude_setup_instructions.jinja` (preview_mode=False), which emits
|
|
`SETUP_INSTRUCTIONS_TEMPLATE` + `renderSetupInstructions(server, token)`.
|
|
|
|
Pages that ALSO render the read-only preview (e.g. via
|
|
`{% include "_claude_setup_instructions.jinja" %}` with preview_mode=True
|
|
in their own template tree) just show the same lines under
|
|
`<pre class="setup-preview-pre">`. The JS here doesn't touch that pre.
|
|
#}
|
|
<style>
|
|
.setup-fallback-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.45);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
.setup-fallback-modal {
|
|
background: var(--surface, #fff);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
max-width: 720px;
|
|
width: calc(100% - 32px);
|
|
max-height: calc(100vh - 64px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.3);
|
|
}
|
|
.setup-fallback-modal h4 {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
color: var(--text-primary, #111827);
|
|
}
|
|
.setup-fallback-modal p {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: var(--text-secondary, #6B7280);
|
|
}
|
|
.setup-fallback-modal textarea {
|
|
flex: 1;
|
|
min-height: 260px;
|
|
font-family: var(--font-mono, ui-monospace, "SF Mono", Consolas, monospace);
|
|
font-size: 12px;
|
|
padding: 10px;
|
|
border: 1px solid var(--border, #E5E7EB);
|
|
border-radius: 8px;
|
|
background: var(--background, #F9FAFB);
|
|
color: var(--text-primary, #111827);
|
|
resize: vertical;
|
|
}
|
|
.setup-fallback-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
.setup-fallback-actions button {
|
|
font-family: var(--font-primary, "Inter", -apple-system, BlinkMacSystemFont, sans-serif);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border, #E5E7EB);
|
|
background: var(--surface, #fff);
|
|
color: var(--text-primary, #111827);
|
|
cursor: pointer;
|
|
}
|
|
.setup-fallback-actions button.primary {
|
|
background: var(--primary, #0073D1);
|
|
color: #FFF;
|
|
border-color: var(--primary, #0073D1);
|
|
}
|
|
|
|
.setup-error {
|
|
margin-top: 12px;
|
|
padding: 10px 14px;
|
|
background: rgba(234, 88, 12, 0.12);
|
|
border-left: 3px solid #EA580C;
|
|
border-radius: 6px;
|
|
color: #FFF;
|
|
font-size: 13px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
if (window.__setupCtaWired) return;
|
|
window.__setupCtaWired = true;
|
|
|
|
{% include "_claude_setup_instructions.jinja" %}
|
|
|
|
// Synchronous textarea + execCommand('copy'). The fallback path the
|
|
// Clipboard API replaced — still functional in all major browsers and,
|
|
// critically, NOT subject to the Clipboard API's transient-activation
|
|
// window. We use it whenever (a) the modern Clipboard API is
|
|
// unavailable / non-secure context, OR (b) it rejected at write time
|
|
// (NotAllowedError — Safari/Firefox enforce this when user-gesture is
|
|
// consumed across an `await fetch()`, which is exactly what
|
|
// setupNewClaude does between the button click and the copy call).
|
|
function copyViaTextarea(text) {
|
|
return new Promise(function (resolve, reject) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.left = '-9999px';
|
|
ta.style.top = '-9999px';
|
|
// readOnly + tabindex=-1 stops the keyboard focus stealing and
|
|
// virtual-keyboard popups on mobile.
|
|
ta.setAttribute('readonly', '');
|
|
ta.setAttribute('tabindex', '-1');
|
|
document.body.appendChild(ta);
|
|
try {
|
|
ta.focus();
|
|
ta.select();
|
|
ta.setSelectionRange(0, text.length);
|
|
var ok = false;
|
|
try { ok = document.execCommand('copy'); } catch (_e) { ok = false; }
|
|
if (ok) { resolve(); } else { reject(new Error('execCommand copy returned false')); }
|
|
} finally {
|
|
document.body.removeChild(ta);
|
|
}
|
|
});
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
// Prefer the modern Clipboard API but fall back to the textarea
|
|
// path on rejection — NOT just when the API is missing. Recent
|
|
// browsers reject `writeText` with NotAllowedError when transient
|
|
// user activation has been consumed by an intervening `await`
|
|
// (the /auth/tokens fetch in setupNewClaude does exactly that).
|
|
// Without this fallback the catch in setupNewClaude pops the
|
|
// manual-paste modal, which users perceive as "browser blocked
|
|
// the copy" — when actually the textarea path would have worked.
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text).catch(function (_err) {
|
|
return copyViaTextarea(text);
|
|
});
|
|
}
|
|
return copyViaTextarea(text);
|
|
}
|
|
|
|
function defaultTokenName() {
|
|
var stamp = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
return "Claude Code — " + stamp;
|
|
}
|
|
|
|
function showSetupFallback(instructions) {
|
|
var overlay = document.createElement('div');
|
|
overlay.className = 'setup-fallback-overlay';
|
|
overlay.innerHTML =
|
|
'<div class="setup-fallback-modal" role="dialog" aria-modal="true" aria-labelledby="setupFallbackTitle">' +
|
|
'<h4 id="setupFallbackTitle">Copy these setup instructions</h4>' +
|
|
'<p>Your browser blocked automatic clipboard access. Select all, copy, then paste into Claude Code.</p>' +
|
|
'<textarea readonly></textarea>' +
|
|
'<div class="setup-fallback-actions">' +
|
|
'<button type="button" data-action="close">Close</button>' +
|
|
'<button type="button" class="primary" data-action="select">Select all</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(overlay);
|
|
var ta = overlay.querySelector('textarea');
|
|
ta.value = instructions;
|
|
ta.focus();
|
|
ta.select();
|
|
overlay.addEventListener('click', function (ev) {
|
|
if (ev.target === overlay) { document.body.removeChild(overlay); }
|
|
});
|
|
overlay.querySelector('[data-action="close"]').addEventListener('click', function () {
|
|
document.body.removeChild(overlay);
|
|
});
|
|
overlay.querySelector('[data-action="select"]').addEventListener('click', function () {
|
|
ta.focus();
|
|
ta.select();
|
|
});
|
|
}
|
|
|
|
async function setupNewClaude(btn) {
|
|
var errEl = document.getElementById('setupClaudeError');
|
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
var origText = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Generating token…';
|
|
try {
|
|
var resp = await fetch('/auth/tokens', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: defaultTokenName(),
|
|
expires_in_days: 90,
|
|
}),
|
|
});
|
|
if (resp.status === 401) {
|
|
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
|
|
return;
|
|
}
|
|
if (!resp.ok) {
|
|
var detail = 'HTTP ' + resp.status;
|
|
try {
|
|
var body = await resp.json();
|
|
if (body && body.detail) { detail = body.detail; }
|
|
} catch (_) { /* non-JSON */ }
|
|
throw new Error(detail);
|
|
}
|
|
var data = await resp.json();
|
|
if (!data || !data.token) {
|
|
throw new Error('Server did not return a token.');
|
|
}
|
|
var serverUrl = window.location.origin;
|
|
var instructions = renderSetupInstructions(serverUrl, data.token);
|
|
|
|
// Replace the inline preview's placeholder with the real token so
|
|
// users can see what got copied (and re-copy by hand if the
|
|
// clipboard write failed). The placeholder span lives inside the
|
|
// shared `_claude_setup_instructions.jinja` preview-mode block;
|
|
// not every page renders that block, so guard for absence.
|
|
var placeholders = document.querySelectorAll('.placeholder-token');
|
|
placeholders.forEach(function (span) {
|
|
span.textContent = data.token;
|
|
span.classList.remove('placeholder-token');
|
|
span.classList.add('token-revealed');
|
|
span.setAttribute(
|
|
'aria-label',
|
|
'Generated personal access token — already copied to your clipboard'
|
|
);
|
|
// Intentionally do NOT auto-open the parent <details>. The
|
|
// primary path is "Copied! Paste into Claude Code"; the
|
|
// manual-paste preview is a secondary affordance and should
|
|
// stay collapsed unless the user opens it themselves.
|
|
});
|
|
|
|
try {
|
|
await copyToClipboard(instructions);
|
|
btn.textContent = 'Copied! Paste into Claude Code';
|
|
btn.classList.add('copied');
|
|
setTimeout(function () {
|
|
btn.textContent = origText;
|
|
btn.classList.remove('copied');
|
|
btn.disabled = false;
|
|
}, 3000);
|
|
// Notify any host page that wants to chain a follow-up UI
|
|
// (e.g. /home's P0-2 "where to paste" 3-step modal). The
|
|
// include itself does nothing with the event — listeners
|
|
// that don't exist are no-ops.
|
|
try {
|
|
document.dispatchEvent(new CustomEvent('agnes:setup-script-copied'));
|
|
} catch (_e) { /* CustomEvent ctor missing — IE-era, ignore */ }
|
|
} catch (clipErr) {
|
|
btn.textContent = origText;
|
|
btn.disabled = false;
|
|
showSetupFallback(instructions);
|
|
}
|
|
} catch (err) {
|
|
btn.textContent = origText;
|
|
btn.disabled = false;
|
|
if (errEl) {
|
|
errEl.textContent = 'Setup failed: ' + (err && err.message ? err.message : err);
|
|
errEl.style.display = 'block';
|
|
} else {
|
|
alert('Setup failed: ' + (err && err.message ? err.message : err));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expose globally so inline onclick handlers can reach them.
|
|
window.copyToClipboard = copyToClipboard;
|
|
window.defaultTokenName = defaultTokenName;
|
|
window.showSetupFallback = showSetupFallback;
|
|
window.setupNewClaude = setupNewClaude;
|
|
window.renderSetupInstructions = renderSetupInstructions;
|
|
window.SETUP_INSTRUCTIONS_TEMPLATE = SETUP_INSTRUCTIONS_TEMPLATE;
|
|
})();
|
|
</script>
|