agnes-the-ai-analyst/app/web/templates/_claude_setup_cta.jinja
Vojtech a46b9dc928
/home install-hero polish: license link contrast, auto-mode reorder, Shift+Tab guidance (#243)
* Make /home install-hero links readable against blue background

The Claude license-options link added in the previous commit inherited
the default `<a>` style (`var(--hp-primary)` blue), which renders as
blue-on-blue and is unreadable inside the blue install-hero. Add a
scoped `.install-hero a` rule that uses white with an underline
(matching the existing lead-paragraph contrast pattern) so any link
nested in the hero stays legible.

* Reorder /home install flow: auto-mode is now Step 2, Agnes install becomes Step 3

Step 3 (was Step 2) pastes a ~20-command bash bootstrap into a fresh
Claude Code session. Without auto-mode enabled first, each Bash/edit
command needs a manual approve click — bad UX for first-time users.

Move auto-mode from the outside-hero `<details>` reference block into
the install-hero as a real Step 2, between "install Claude Code" and
"install Agnes". Content is the persistent `acceptEdits` snippet
(write to ~/.claude/settings.json) plus a one-liner pointing at
Shift+Tab for users who are already inside a running Claude Code
session. YOLO mode for full Bash auto-approve stays on
/setup-advanced behind the existing link.

The outside-hero `setup-collapsible[data-section="step3"]` block is
dropped — auto-mode is no longer reference content, it's a real
install step, and duplicating it would just diverge over time.
Onboarded users no longer see the auto-mode block at all (consistent
with Steps 1 + 3 also hiding post-onboarding).

Completion banner copy updated: "Step 1, 2 & 3 done — Claude Code
installed, auto-mode set, Agnes ready". Dashboard CTA partial and
other templates don't reference step numbers for this flow, so no
adaptation needed there.

* Simplify /home Step 2 to Shift+Tab only — drop the JSON snippet

Operator pointed out two issues with the prior Step 2:

1. The settings.json snippet is redundant. Claude Code's first
   Shift+Tab cycle to auto-accept mode already prompts the user
   whether to persist it as default — Claude writes the config
   itself, no manual file edit needed.

2. The snippet only showed the POSIX path `~/.claude/settings.json`,
   which doesn't translate to native Windows.

Replace the snippet + copy button with a plain Shift+Tab instruction,
explicitly call out the first-time "make this the default?" prompt,
and note that Claude handles the config write itself — same flow on
macOS / Linux / WSL / Windows. Adds a fallback line for users who
already closed the post-OAuth session.

* Tighten /home Step 2 install-note to two paragraphs

Operator: drop the 'Claude writes the setting itself, so this works
the same on macOS / Linux / WSL / Windows...' line plus the
'auto-approves file edits going forward; Bash commands stay gated
— that's the safe default' line. Both were filler — the make-default
prompt already implies persistence, and gated Bash is the obvious
default users won't be surprised by.

Result: paragraph 1 carries Shift+Tab + first-time make-default
say-yes + closed-session fallback in one breath; paragraph 2 keeps
the verbatim YOLO link. Same affordances, less vertical space.
2026-05-11 16:46:58 +00:00

255 lines
9.6 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" %}
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '-9999px';
document.body.appendChild(ta);
ta.focus();
ta.select();
return new Promise(function (resolve, reject) {
document.execCommand('copy') ? resolve() : reject();
document.body.removeChild(ta);
});
}
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>