@aisa-one/cli 0.2.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -1
- package/LICENSE +21 -0
- package/README.md +163 -169
- package/dist/api.d.ts +10 -0
- package/dist/api.js +14 -3
- package/dist/catalog.d.ts +1 -18
- package/dist/catalog.js +1 -24
- package/dist/cli-error.d.ts +17 -0
- package/dist/cli-error.js +25 -0
- package/dist/cli-guidance.d.ts +14 -0
- package/dist/cli-guidance.js +55 -0
- package/dist/commands/account.d.ts +17 -0
- package/dist/commands/account.js +45 -1
- package/dist/commands/api.d.ts +0 -11
- package/dist/commands/api.js +6 -175
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +43 -9
- package/dist/commands/brand-logos.d.ts +6 -0
- package/dist/commands/brand-logos.js +21 -0
- package/dist/commands/configCmd.js +8 -3
- package/dist/commands/connect-shared.d.ts +136 -0
- package/dist/commands/connect-shared.js +143 -0
- package/dist/commands/connect-t2.d.ts +18 -0
- package/dist/commands/connect-t2.js +1599 -0
- package/dist/commands/connect-terminal.d.ts +65 -0
- package/dist/commands/connect-terminal.js +822 -0
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.js +3181 -0
- package/dist/commands/flow.d.ts +685 -0
- package/dist/commands/flow.js +697 -0
- package/dist/commands/install.d.ts +91 -0
- package/dist/commands/install.js +258 -0
- package/dist/commands/llm-config.d.ts +103 -0
- package/dist/commands/llm-config.js +452 -0
- package/dist/commands/manifest.d.ts +68 -0
- package/dist/commands/manifest.js +90 -0
- package/dist/commands/mcp.d.ts +79 -2
- package/dist/commands/mcp.js +262 -48
- package/dist/commands/oauth-login.d.ts +33 -0
- package/dist/commands/oauth-login.js +333 -0
- package/dist/commands/prompt.d.ts +96 -0
- package/dist/commands/prompt.js +397 -0
- package/dist/commands/serve-results.d.ts +61 -0
- package/dist/commands/serve-results.js +245 -0
- package/dist/commands/serve-signin.d.ts +28 -0
- package/dist/commands/serve-signin.js +90 -0
- package/dist/commands/signin-page.d.ts +49 -0
- package/dist/commands/signin-page.js +250 -0
- package/dist/commands/skills.js +87 -50
- package/dist/commands/tool-help.d.ts +69 -0
- package/dist/commands/tool-help.js +189 -0
- package/dist/commands/tool-input.d.ts +22 -0
- package/dist/commands/tool-input.js +220 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.js +307 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +92 -0
- package/dist/commands/vscode.d.ts +65 -0
- package/dist/commands/vscode.js +173 -0
- package/dist/commands/wrappers.d.ts +23 -0
- package/dist/commands/wrappers.js +102 -0
- package/dist/completion.js +3 -80
- package/dist/config.js +76 -2
- package/dist/constants.d.ts +78 -38
- package/dist/constants.js +80 -102
- package/dist/index.js +155 -357
- package/dist/json-text.d.ts +12 -0
- package/dist/json-text.js +60 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +59 -0
- package/dist/skills-registry.js +12 -3
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/browser.d.ts +19 -0
- package/dist/utils/browser.js +31 -0
- package/dist/utils/exec.d.ts +43 -0
- package/dist/utils/exec.js +61 -0
- package/dist/utils/http.d.ts +53 -0
- package/dist/utils/http.js +124 -0
- package/dist/utils/install-method.d.ts +36 -0
- package/dist/utils/install-method.js +48 -0
- package/dist/utils/journal.d.ts +57 -0
- package/dist/utils/journal.js +188 -0
- package/dist/utils/key-check.d.ts +20 -0
- package/dist/utils/key-check.js +24 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +116 -0
- package/dist/utils/width.d.ts +32 -0
- package/dist/utils/width.js +73 -0
- package/package.json +8 -4
- package/vscode-extension/README.md +6 -0
- package/vscode-extension/aisa-vscode.vsix +0 -0
- package/dist/commands/finance.d.ts +0 -17
- package/dist/commands/finance.js +0 -342
- package/dist/commands/run.d.ts +0 -28
- package/dist/commands/run.js +0 -182
- package/dist/commands/search.d.ts +0 -9
- package/dist/commands/search.js +0 -81
- package/dist/commands/twitter.d.ts +0 -138
- package/dist/commands/twitter.js +0 -374
- package/dist/commands/video.d.ts +0 -49
- package/dist/commands/video.js +0 -311
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
import { MCP_DEFAULT_SLUGS } from "../constants.js";
|
|
2
|
+
import { INSTALLERS } from "./install.js";
|
|
3
|
+
import { defaultModelsFor } from "./llm-config.js";
|
|
4
|
+
import { stripped } from "./mcp.js";
|
|
5
|
+
import { BRAND_LOGOS } from "./brand-logos.js";
|
|
6
|
+
import { STEP_AGENT, AGENT_ORDER, agentRank, AGENT_BADGE, AGENT_SIDE_TITLES, AGENT_NOTES, AGENT_HAVE_NOTES, installOffer, t, fill, LANGS, LANG_LABEL, NAV, STEP_TITLES, STEP_WELCOME, STEP_MODELS, STEP_CAPS, STEP_INSTALL, STEP_DONE, FILE_MODEL_NOTE, FILE_MODEL_FALLBACK, BACKUP_COPY, CATEGORY_BLURB, runtimeCopy, NEXT_WORD, SUPERSEDED, } from "./flow.js";
|
|
7
|
+
import { RED, RED_CTA, INK, PAPER, I, LOGO, CATEGORY_ICON, EXAMPLES, CLIENT_LOGOS, CODEX_FACE, CLAUDE_BOT, OPENCODE_MARK, } from "./connect-shared.js";
|
|
8
|
+
import { AISA_PROVIDER_ID } from "../constants.js";
|
|
9
|
+
import { VSCODE_MODELS } from "./vscode.js";
|
|
10
|
+
/**
|
|
11
|
+
* T2 — the guided six-step connect flow.
|
|
12
|
+
*
|
|
13
|
+
* One page, six panes, a narrow step rail on the left and the current step
|
|
14
|
+
* filling the rest of the screen. The shape is borrowed from the onboarding
|
|
15
|
+
* flows that feel effortless (Sapiom's six-step rail was the reference): the
|
|
16
|
+
* rail says where you are, the main area has room to actually explain the
|
|
17
|
+
* step instead of cramming everything into one sidebar.
|
|
18
|
+
*
|
|
19
|
+
* 1 Welcome what you are about to get
|
|
20
|
+
* 2 Your agent which tool to connect (install it if missing)
|
|
21
|
+
* 3 Models which models it runs on, and how easy it is to switch
|
|
22
|
+
* 4 Capabilities live data — a grid of capability areas, servers inside
|
|
23
|
+
* 5 Install sign in, install, wire up — animated, one tick at a time
|
|
24
|
+
* 6 Done congratulations, recap, balance, launch, try-it-now
|
|
25
|
+
*
|
|
26
|
+
* The install tab parks on step 5 and the success tab the process opens lands
|
|
27
|
+
* on step 6; both are this same page, hydrated from GET /status, so from
|
|
28
|
+
* either tab every other step remains one click away. Steps 1–4 turn
|
|
29
|
+
* read-only once the run has started.
|
|
30
|
+
*
|
|
31
|
+
* The T1 flow (renderPage / renderDone in connect.ts) is untouched; the
|
|
32
|
+
* server picks a template per run and both share the same endpoints.
|
|
33
|
+
*/
|
|
34
|
+
/** Providers shown on the Models step. Model names are from the live
|
|
35
|
+
* `aisa models` catalogue (2026-08-23); a representative few per provider. */
|
|
36
|
+
const PROVIDERS = [
|
|
37
|
+
{ id: "claude", name: "Claude", models: "Sonnet 5 · Opus 5 · Haiku 4.5" },
|
|
38
|
+
{ id: "openai", name: "OpenAI", models: "GPT-5.5 · GPT-5.3-codex · 5.4-mini" },
|
|
39
|
+
{ id: "gemini", name: "Gemini", models: "Gemini 3.5 Flash" },
|
|
40
|
+
{ id: "deepseek", name: "DeepSeek", models: "V4 Pro · V4 Flash · R1" },
|
|
41
|
+
{ id: "kimi", name: "Kimi", models: "K3 · K2.7-code · K2 thinking" },
|
|
42
|
+
{ id: "glm", name: "GLM (Zhipu)", models: "GLM-5.2 · GLM-5.1 · GLM-5" },
|
|
43
|
+
{ id: "qwen", name: "Qwen", models: "Qwen3.7 Max · Qwen3 Coder" },
|
|
44
|
+
{ id: "grok", name: "Grok (xAI)", models: "Grok 4.6 · Grok 4.5" },
|
|
45
|
+
];
|
|
46
|
+
/** Clients we do not connect yet but will, shown so the roadmap is visible. */
|
|
47
|
+
const COMING_SOON = [
|
|
48
|
+
{ id: "claude-ai", label: "Claude.ai", note: "connector" },
|
|
49
|
+
{ id: "chatgpt", label: "ChatGPT", note: "connector" },
|
|
50
|
+
];
|
|
51
|
+
/** One paragraph per agent on what connecting it means, for the Your-agent
|
|
52
|
+
* step's side column. */
|
|
53
|
+
/** Why the Models step has nothing to offer a file-configured client. */
|
|
54
|
+
/** Backup-mode consent copy, per client — the same contract T1 shows. */
|
|
55
|
+
/** The wordmark recoloured for the paper background: the white "sa" of the
|
|
56
|
+
* dark-background original becomes the ink colour via currentColor. */
|
|
57
|
+
const LOGO_INK = LOGO.replace(/#FFFFFF/g, "currentColor");
|
|
58
|
+
function normCategory(c) {
|
|
59
|
+
return /^search/i.test(c) ? "Search & Research" : c;
|
|
60
|
+
}
|
|
61
|
+
/** lucide:menu — same 2px round-cap line as the rest of the page's marks. */
|
|
62
|
+
const MENU_ICON = '<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">' +
|
|
63
|
+
'<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" ' +
|
|
64
|
+
'stroke-width="2" d="M4 5h16M4 12h16M4 19h16"/></svg>';
|
|
65
|
+
export function renderT2Page(servers, clients, token, keyed, canInstall,
|
|
66
|
+
/**
|
|
67
|
+
* Whether this machine still needs the AIsa CLI installed. Probed by the
|
|
68
|
+
* caller, never in here: this function's output is snapshot-tested byte
|
|
69
|
+
* for byte, and a live probe inside it froze one machine's answer into
|
|
70
|
+
* the baseline — green wherever the CLI happened to be installed, red on
|
|
71
|
+
* every CI runner (runs 74-84, 2026-09-03).
|
|
72
|
+
*/
|
|
73
|
+
needsCli, view,
|
|
74
|
+
/**
|
|
75
|
+
* Defaults to English so this signature could grow without moving a byte of
|
|
76
|
+
* the rendered page: the picker that will set it is a later step, and the
|
|
77
|
+
* snapshots guarding this refactor have to stay green in between.
|
|
78
|
+
*/
|
|
79
|
+
lang = "en") {
|
|
80
|
+
const T = (x) => t(x, lang);
|
|
81
|
+
/** A per-agent map of Text, flattened to the current language. */
|
|
82
|
+
const resolved = (m) => Object.fromEntries(Object.entries(m).map(([k, v]) => [k, t(v, lang)]));
|
|
83
|
+
const totalTools = servers.reduce((n, s) => n + s.toolCount, 0);
|
|
84
|
+
const cats = new Map();
|
|
85
|
+
for (const s of servers) {
|
|
86
|
+
const c = normCategory(s.category);
|
|
87
|
+
cats.set(c, [...(cats.get(c) ?? []), s]);
|
|
88
|
+
}
|
|
89
|
+
const ORDER = ["Search & Research", "Finance", "Social", "Sales"];
|
|
90
|
+
const rank = (c) => (ORDER.indexOf(c) === -1 ? 99 : ORDER.indexOf(c));
|
|
91
|
+
const catList = [...cats.entries()].sort((a, b) => rank(a[0]) - rank(b[0]));
|
|
92
|
+
// ── data handed to the page script ──
|
|
93
|
+
const SERVERS = servers.map((s) => ({
|
|
94
|
+
slug: s.slug,
|
|
95
|
+
endpoint: s.endpoint,
|
|
96
|
+
name: stripped(s.name),
|
|
97
|
+
category: normCategory(s.category),
|
|
98
|
+
toolCount: s.toolCount,
|
|
99
|
+
description: s.description,
|
|
100
|
+
}));
|
|
101
|
+
// Windsurf is not part of this flow (its file path stays in T1). claude.ai
|
|
102
|
+
// on the web is parked until AIsa is in the Connector Directory — the
|
|
103
|
+
// hand-off page below is ready, the card is not offered; see mcp.md T20.
|
|
104
|
+
const CLIENTS = clients
|
|
105
|
+
.map((c) => ({
|
|
106
|
+
id: c.id,
|
|
107
|
+
label: c.label,
|
|
108
|
+
kind: c.kind,
|
|
109
|
+
detected: c.detected,
|
|
110
|
+
detail: c.detail,
|
|
111
|
+
installable: !c.detected && Boolean(INSTALLERS[c.id]) && canInstall,
|
|
112
|
+
command: INSTALLERS[c.id]?.command ?? "",
|
|
113
|
+
}));
|
|
114
|
+
const MODEL_FOR = Object.fromEntries(clients.map((c) => [c.id, defaultModelsFor(c.id).model]));
|
|
115
|
+
// ── step 1: welcome ──
|
|
116
|
+
const logoStrip = PROVIDERS.slice(0, 6).map((p) => `<span class="blogo" title="${p.name}">${BRAND_LOGOS[p.id] ?? ""}</span>`).join("");
|
|
117
|
+
const welcome = `
|
|
118
|
+
<h1>${T(STEP_WELCOME.h1).replace("Every major model", "<em>Every major model</em>")}</h1>
|
|
119
|
+
<p class="lede">${T(STEP_WELCOME.lede).replace("models and the real", "models <b>and</b> the real")}</p>
|
|
120
|
+
<div class="feat">
|
|
121
|
+
<div class="ftile"><div class="fico">${I.sparkles}</div>
|
|
122
|
+
<h3>${T(STEP_WELCOME.tiles[0].h3)}</h3>
|
|
123
|
+
<p>${T(STEP_WELCOME.tiles[0].p)}</p>
|
|
124
|
+
<div class="strip">${logoStrip}</div></div>
|
|
125
|
+
<div class="ftile"><div class="fico">${I.finance}</div>
|
|
126
|
+
<h3>${T(STEP_WELCOME.tiles[1].h3)}</h3>
|
|
127
|
+
<p>${T(STEP_WELCOME.tiles[1].p)}</p></div>
|
|
128
|
+
<div class="ftile"><div class="fico">${I.terminal}</div>
|
|
129
|
+
<h3>${T(STEP_WELCOME.tiles[2].h3)}</h3>
|
|
130
|
+
<p>${T(STEP_WELCOME.tiles[2].p)}</p></div>
|
|
131
|
+
<div class="ftile"><div class="fico">${I.shield}</div>
|
|
132
|
+
<h3>${T(STEP_WELCOME.tiles[3].h3)}</h3>
|
|
133
|
+
<p>${T(STEP_WELCOME.tiles[3].p)}</p></div>
|
|
134
|
+
</div>`;
|
|
135
|
+
// ── step 2: your agent ──
|
|
136
|
+
// Order comes from the flow definition — the terminal numbers its menu from
|
|
137
|
+
// the same list, so "pick 2" cannot mean two different agents.
|
|
138
|
+
void AGENT_ORDER;
|
|
139
|
+
const shown = CLIENTS.filter((c) => c.detected || c.installable).sort((a, b) => agentRank(a.id) - agentRank(b.id));
|
|
140
|
+
const firstPick = shown.find((c) => c.detected) ?? shown[0];
|
|
141
|
+
const rest = CLIENTS.filter((c) => !c.detected && !c.installable);
|
|
142
|
+
const clientCard = (c, checked) => `
|
|
143
|
+
<label class="tile agent${checked ? " on" : ""}" data-cid="${c.id}">
|
|
144
|
+
<input type="radio" class="dot" name="client" value="${c.id}"${checked ? " checked" : ""}${c.installable ? ' data-install="1"' : ""}>
|
|
145
|
+
<span class="tlogo${c.id === "cursor" ? " mono" : ""}">${BRAND_LOGOS[c.id] ?? (c.id === "claude-desktop" ? BRAND_LOGOS["claude-ai"] : undefined) ?? CLIENT_LOGOS[c.id] ?? I.terminal}</span>
|
|
146
|
+
<span class="tbody"><span class="thead"><span class="tname">${c.label}</span></span>
|
|
147
|
+
<span class="tbrief" data-brief>${c.detected ? c.detail : installOffer(c.command ?? "", lang)}</span></span>
|
|
148
|
+
${c.kind === "web" ? `<span class="badge web end">${T(AGENT_BADGE.web)}</span>` : c.detected ? `<span class="badge ok end">${T(AGENT_BADGE.detected)}</span>` : `<span class="badge todo end" data-badge>${T(AGENT_BADGE.absent)}</span>`}</label>`;
|
|
149
|
+
const agentCards = shown.map((c) => clientCard(c, c === firstPick)).join("");
|
|
150
|
+
// Fixed order: Claude Desktop, ChatGPT, Cursor, VS Code — by how likely a
|
|
151
|
+
// reader is to care, not by which list they come from.
|
|
152
|
+
const chipOrder = ["claude-desktop", "claude-ai", "chatgpt", "cursor", "vscode"];
|
|
153
|
+
// (vscode only lands here when it has never been run on this machine)
|
|
154
|
+
const chips = [
|
|
155
|
+
...rest.map((c) => ({ id: c.id, html: `<span class="chip">${BRAND_LOGOS[c.id] ?? ""}${c.label} <i>${T(AGENT_BADGE.notFound)}</i></span>` })),
|
|
156
|
+
...COMING_SOON.map((c) => ({ id: c.id, html: `<span class="chip">${BRAND_LOGOS[c.id] ?? ""}${c.label} <i>${c.note} · ${T(AGENT_BADGE.soon)}</i></span>` })),
|
|
157
|
+
].sort((a, b) => chipOrder.indexOf(a.id) - chipOrder.indexOf(b.id));
|
|
158
|
+
const restChips = chips.length ? `<div class="soon">${chips.map((c) => c.html).join("")}</div>` : "";
|
|
159
|
+
const agent = `
|
|
160
|
+
<h1>${T(STEP_AGENT.question).replace("plug into", "<em>plug into</em>")}</h1>
|
|
161
|
+
<p class="lede">${T(STEP_AGENT.lede)}</p>
|
|
162
|
+
<div class="grid1">${agentCards}</div>
|
|
163
|
+
${restChips}
|
|
164
|
+
<div class="side">
|
|
165
|
+
<h3 id="agentNoteTitle">${T(AGENT_SIDE_TITLES.how)}</h3>
|
|
166
|
+
<p id="agentNote"></p>
|
|
167
|
+
<h3 id="agentHaveTitle">${T(AGENT_SIDE_TITLES.have)}</h3>
|
|
168
|
+
<p id="agentHave"></p>
|
|
169
|
+
</div>`;
|
|
170
|
+
// ── step 3: models ──
|
|
171
|
+
const providerTiles = PROVIDERS.map((p) => `<div class="ptile"><span class="blogo lg">${BRAND_LOGOS[p.id] ?? ""}</span>
|
|
172
|
+
<span class="pname">${p.name}</span><span class="pmodels">${p.models}</span></div>`).join("");
|
|
173
|
+
const models = `
|
|
174
|
+
<h1>${T(STEP_MODELS.h1).replace("one account", "<em>one account</em>")}</h1>
|
|
175
|
+
<p class="lede">${T(STEP_MODELS.lede)}</p>
|
|
176
|
+
<div class="pgrid">${providerTiles}</div>
|
|
177
|
+
<div class="callout">${I.sparkles}<div>${T(STEP_MODELS.callout)}</div></div>
|
|
178
|
+
<p class="rerun">${T(STEP_MODELS.rerun)}</p>
|
|
179
|
+
|
|
180
|
+
<h2>${T(STEP_MODELS.h2Prefix)}<span id="mClient">${T(STEP_MODELS.yourAgent)}</span>${T(STEP_MODELS.h2Suffix)}</h2>
|
|
181
|
+
<div id="mFresh" class="grid1 choice" style="display:none">
|
|
182
|
+
<label class="tile on"><input type="radio" class="dot" name="lmodeFresh" value="switch" checked>
|
|
183
|
+
<span class="tbody"><span class="thead"><span class="tname">${T(STEP_MODELS.freshSwitch.name)}</span><span class="badge rec">${T(STEP_MODELS.recommended)}</span></span>
|
|
184
|
+
<span class="tbrief">${T(STEP_MODELS.freshSwitch.brief).replace("{model}", '<b id="mModel"></b>')}</span></span></label>
|
|
185
|
+
<label class="tile"><input type="radio" class="dot" name="lmodeFresh" value="skip">
|
|
186
|
+
<span class="tbody"><span class="thead"><span class="tname">${T(STEP_MODELS.notNow.name)}</span></span>
|
|
187
|
+
<span class="tbrief">${T(STEP_MODELS.notNow.briefFresh)}</span></span></label>
|
|
188
|
+
</div>
|
|
189
|
+
<div id="mDetected" class="grid1 choice" style="display:none">
|
|
190
|
+
<label class="tile on"><input type="radio" class="dot" name="lmodeDet" value="backup" checked>
|
|
191
|
+
<span class="tbody"><span class="thead"><span class="tname">${T(STEP_MODELS.backup.name)}</span><span class="badge rec">${T(STEP_MODELS.recommended)}</span></span>
|
|
192
|
+
<span class="tbrief" id="mBackup"></span></span></label>
|
|
193
|
+
<label class="tile" id="mSwitch"><input type="radio" class="dot" name="lmodeDet" value="switch">
|
|
194
|
+
<span class="tbody"><span class="thead"><span class="tname">${T(STEP_MODELS.switchIt.name)}</span></span>
|
|
195
|
+
<span class="tbrief">${T(STEP_MODELS.switchIt.brief).replace("{model}", '<b id="mModel2"></b>')}</span></span></label>
|
|
196
|
+
<label class="tile"><input type="radio" class="dot" name="lmodeDet" value="skip">
|
|
197
|
+
<span class="tbody"><span class="thead"><span class="tname">${T(STEP_MODELS.notNow.name)}</span></span>
|
|
198
|
+
<span class="tbrief">${T(STEP_MODELS.notNow.briefDetected)}</span></span></label>
|
|
199
|
+
</div>
|
|
200
|
+
<div id="mFile" class="callout" style="display:none">${I.shield}<div id="mFileText"></div></div>
|
|
201
|
+
<div id="modelwarn" class="modelwarn" style="display:none">
|
|
202
|
+
<div class="mw-head">${T(STEP_MODELS.warn.head)}</div>
|
|
203
|
+
<div class="mw-body">${T(STEP_MODELS.warn.body)}</div>
|
|
204
|
+
<button type="button" class="mw-fix" id="modelfix">${T(STEP_MODELS.warn.fix)}</button>
|
|
205
|
+
</div>`;
|
|
206
|
+
// ── step 4: capabilities ──
|
|
207
|
+
const catTiles = catList
|
|
208
|
+
.map(([cat, list]) => {
|
|
209
|
+
const tools = list.reduce((n, s) => n + s.toolCount, 0);
|
|
210
|
+
return `<button type="button" class="ctile" data-cat="${cat}">
|
|
211
|
+
<span class="cico">${CATEGORY_ICON[cat] ?? I.sparkles}</span>
|
|
212
|
+
<span class="cname">${cat}</span>
|
|
213
|
+
<span class="cmeta">${fill(STEP_CAPS.tileMeta, lang, { n: list.length, noun: list.length > 1 ? T(STEP_CAPS.serversWord) : T(STEP_CAPS.serverWord), tools })}</span>
|
|
214
|
+
<span class="cblurb">${CATEGORY_BLURB[cat] ? T(CATEGORY_BLURB[cat]) : ""}</span>
|
|
215
|
+
<span class="csel" data-csel></span></button>`;
|
|
216
|
+
})
|
|
217
|
+
.join("");
|
|
218
|
+
const serverTiles = catList
|
|
219
|
+
.map(([cat, list]) => list
|
|
220
|
+
.map((s) => {
|
|
221
|
+
const checked = MCP_DEFAULT_SLUGS.includes(s.slug);
|
|
222
|
+
return `<label class="stile${checked ? " on" : ""}" data-cat="${cat}">
|
|
223
|
+
<input type="checkbox" name="server" value="${s.slug}"${checked ? " checked" : ""}>
|
|
224
|
+
<span class="tbody"><span class="thead"><span class="tname">${stripped(s.name)}</span>
|
|
225
|
+
<span class="badge">${s.toolCount} tools</span></span>
|
|
226
|
+
<span class="sdesc">${s.description}</span></span></label>`;
|
|
227
|
+
})
|
|
228
|
+
.join(""))
|
|
229
|
+
.join("");
|
|
230
|
+
const caps = `
|
|
231
|
+
<h1>${T(STEP_CAPS.h1).replace("reach", "<em>reach</em>")}</h1>
|
|
232
|
+
<p class="lede">${fill(STEP_CAPS.counts, lang, { areas: catList.length, servers: servers.length, tools: totalTools })} ${T(STEP_CAPS.ledeTail)}</p>
|
|
233
|
+
<div class="cgrid">${catTiles}</div>
|
|
234
|
+
<div class="spanel" id="spanel">
|
|
235
|
+
<div class="sphead"><h2 id="spTitle">${T(STEP_CAPS.pickArea)}</h2><button type="button" class="link" id="spAll">${T(STEP_CAPS.selectAll)}</button></div>
|
|
236
|
+
<div class="sgrid" id="sgrid">${serverTiles}</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="tally" id="tally"></div>`;
|
|
239
|
+
// ── step 5: install ──
|
|
240
|
+
const install = `
|
|
241
|
+
<h1 id="inTitle">${T(STEP_INSTALL.h1).replace("connect", "<em>connect</em>").replace("接线", "<em>接线</em>")}</h1>
|
|
242
|
+
<p class="lede" id="inLede">${T(STEP_INSTALL.lede)}</p>
|
|
243
|
+
<div class="backfrom" id="backfrom" hidden></div>
|
|
244
|
+
<div class="plan" id="plan"></div>
|
|
245
|
+
<div class="barwrap" id="barwrap" style="display:none"><div class="barfill" id="barfill"></div></div>
|
|
246
|
+
<div class="barnote" id="barnote"></div>
|
|
247
|
+
<div class="authnote">${I.shield}<div>${keyed ? T(STEP_INSTALL.authKeyed) : T(STEP_INSTALL.authFresh)}</div></div>
|
|
248
|
+
<div id="inResult" class="fine"></div>`;
|
|
249
|
+
// ── step 6: done (filled by the page script from /status) ──
|
|
250
|
+
const done = `<div id="doneBody"><div class="eyebrow">${T(STEP_DONE.eyebrow)}</div>
|
|
251
|
+
<h1>${T(STEP_DONE.h1)}</h1><p class="lede">${T(STEP_DONE.lede)}</p></div>`;
|
|
252
|
+
const rail = STEP_TITLES.map((s) => `<button type="button" class="rstep" data-step="${s.n}">
|
|
253
|
+
<span class="rn">${s.n}</span><span class="rt"><span class="rtitle">${T(s.title)}</span><span class="rsub">${T(s.sub)}</span></span></button>`).join("");
|
|
254
|
+
// Top-right, and it sticks: the choice is posted back to the CLI, which
|
|
255
|
+
// stores it, so the terminal beside this page switches too and the next run
|
|
256
|
+
// opens in the same language. A picker that only changed the page would
|
|
257
|
+
// leave the two surfaces disagreeing — the thing flow.ts exists to prevent.
|
|
258
|
+
// A two-button language toggle sat in the corner of every step, on a page
|
|
259
|
+
// whose whole job is to ask six questions — a seventh control, permanently
|
|
260
|
+
// lit, competing with them. It is a setting, not a step: behind a menu it
|
|
261
|
+
// is one click further away and stops being part of the conversation.
|
|
262
|
+
const langPicker = `<div class="more">
|
|
263
|
+
<button type="button" class="morebtn" id="morebtn" aria-haspopup="true" aria-expanded="false" aria-label="Menu">${MENU_ICON}</button>
|
|
264
|
+
<div class="moremenu" id="moremenu" hidden>${LANGS.map((code) => `<button type="button" class="lang${code === lang ? " on" : ""}" data-lang="${code}">${LANG_LABEL[code]}</button>`).join("")}</div>
|
|
265
|
+
</div>`;
|
|
266
|
+
const body = `
|
|
267
|
+
<div class="wrap">
|
|
268
|
+
<div id="superseded" class="ssup" style="display:none"><div class="ssbox">
|
|
269
|
+
<h2 id="sstitle">${T(SUPERSEDED.title)}</h2>
|
|
270
|
+
<p id="ssbody">${T(SUPERSEDED.body)} <b id="sscount">60</b> ${T(SUPERSEDED.seconds)}.</p>
|
|
271
|
+
<div class="sscta">
|
|
272
|
+
<p>${T(SUPERSEDED.console)} <a href="https://${SUPERSEDED.consoleDomain}?source=aisa_cli_connect" target="_blank" rel="noopener noreferrer">${SUPERSEDED.consoleDomain}</a>.</p>
|
|
273
|
+
<p>${T(SUPERSEDED.againBefore)} <code>${SUPERSEDED.againCmd}</code> ${T(SUPERSEDED.againAfter)}</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div></div>
|
|
276
|
+
<nav class="rail">
|
|
277
|
+
<a class="railhead" href="https://aisa.one/?source=aisa_cli_connect" target="_blank" rel="noopener noreferrer">${LOGO_INK}<span>Connect</span></a>
|
|
278
|
+
<div class="railsteps">${rail}</div>
|
|
279
|
+
</nav>
|
|
280
|
+
<section class="main">
|
|
281
|
+
<div class="content">
|
|
282
|
+
<div class="topnav"><button type="button" class="ghost" id="back">${T(NAV.back)}</button>${langPicker}</div>
|
|
283
|
+
<div class="pane" data-pane="1">${welcome}</div>
|
|
284
|
+
<div class="pane" data-pane="2">${agent}</div>
|
|
285
|
+
<div class="pane" data-pane="3">${models}</div>
|
|
286
|
+
<div class="pane" data-pane="4">${caps}</div>
|
|
287
|
+
<div class="pane" data-pane="5">${install}</div>
|
|
288
|
+
<div class="pane" data-pane="6">${done}</div>
|
|
289
|
+
<div class="navbar">
|
|
290
|
+
<button type="button" class="cta" id="next">${T(NEXT_WORD[1])}${I.arrow}</button>
|
|
291
|
+
<span class="navnote" id="navnote"></span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
</div>
|
|
296
|
+
<script>
|
|
297
|
+
(function () {
|
|
298
|
+
var TOKEN = ${JSON.stringify(token)};
|
|
299
|
+
// The sign-in tab sends the reader back here with this on the URL. Without
|
|
300
|
+
// it the return reads as an unexplained reload of a page they never left
|
|
301
|
+
// on purpose — they were redirected away by the sign-in and redirected
|
|
302
|
+
// home again, and neither move was theirs.
|
|
303
|
+
var CAME_BACK = /[?&]signedin=1/.test(location.search);
|
|
304
|
+
var VIEW = ${JSON.stringify(view)};
|
|
305
|
+
var SERVERS = ${JSON.stringify(SERVERS)};
|
|
306
|
+
var CLIENTS = ${JSON.stringify(CLIENTS)};
|
|
307
|
+
var MODEL_FOR = ${JSON.stringify(MODEL_FOR)};
|
|
308
|
+
var EXAMPLES = ${JSON.stringify(EXAMPLES)};
|
|
309
|
+
var BACKUP_COPY = ${JSON.stringify(resolved(BACKUP_COPY))};
|
|
310
|
+
var COPY = ${JSON.stringify(runtimeCopy(lang))};
|
|
311
|
+
var AGENT_NOTES = ${JSON.stringify(resolved(AGENT_NOTES))};
|
|
312
|
+
var HAVE_NOTES = ${JSON.stringify(resolved(AGENT_HAVE_NOTES))};
|
|
313
|
+
var FILE_MODEL_NOTE = ${JSON.stringify(resolved(FILE_MODEL_NOTE))};
|
|
314
|
+
var NEEDS_CLI = ${JSON.stringify(needsCli)};
|
|
315
|
+
var PROVIDER_ID = ${JSON.stringify(AISA_PROVIDER_ID)};
|
|
316
|
+
var ART = ${JSON.stringify({ codex: CODEX_FACE, claude: CLAUDE_BOT, opencode: OPENCODE_MARK })};
|
|
317
|
+
var LABEL = {}; CLIENTS.forEach(function (c) { LABEL[c.id] = c.label; });
|
|
318
|
+
var BY_SLUG = {}; SERVERS.forEach(function (s) { BY_SLUG[s.slug] = s; });
|
|
319
|
+
var ICON_COPY = ${JSON.stringify(I.copy)};
|
|
320
|
+
var ICON_CHECK = ${JSON.stringify(I.check)};
|
|
321
|
+
var VSCODE_LOGO = ${JSON.stringify(BRAND_LOGOS.vscode ?? "")};
|
|
322
|
+
var CURSOR_LOGO = ${JSON.stringify(BRAND_LOGOS.cursor ?? "")};
|
|
323
|
+
var CLAUDE_DT_LOGO = ${JSON.stringify(BRAND_LOGOS["claude-ai"] ?? "")};
|
|
324
|
+
var MODEL_COUNT = ${VSCODE_MODELS.length};
|
|
325
|
+
|
|
326
|
+
var $ = function (s, r) { return (r || document).querySelector(s); };
|
|
327
|
+
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
|
|
328
|
+
|
|
329
|
+
// ── navigation ──
|
|
330
|
+
var current = 1, unlocked = 1, locked = false, phase = "selecting";
|
|
331
|
+
var panes = $$(".pane"), rails = $$(".rstep");
|
|
332
|
+
var nextBtn = $("#next"), backBtn = $("#back"), navnote = $("#navnote");
|
|
333
|
+
var ARROW = nextBtn.innerHTML.replace(/^[^<]*/, "");
|
|
334
|
+
var NEXT_WORD = ${JSON.stringify(resolved(NEXT_WORD))};
|
|
335
|
+
|
|
336
|
+
// Switching reloads: the page is rendered by the CLI, so the new language
|
|
337
|
+
// arrives the same way the first one did. Storing it first means the
|
|
338
|
+
// terminal picks it up too.
|
|
339
|
+
// The menu: opens on click, closes on the next click anywhere else or on
|
|
340
|
+
// Escape. Nothing here is destructive, so it does not need to be modal —
|
|
341
|
+
// it needs to get out of the way, which is the opposite requirement.
|
|
342
|
+
(function () {
|
|
343
|
+
var btn = $("#morebtn"), menu = $("#moremenu");
|
|
344
|
+
if (!btn || !menu) return;
|
|
345
|
+
function setOpen(on) {
|
|
346
|
+
menu.hidden = !on;
|
|
347
|
+
btn.setAttribute("aria-expanded", on ? "true" : "false");
|
|
348
|
+
}
|
|
349
|
+
btn.addEventListener("click", function (e) {
|
|
350
|
+
e.stopPropagation();
|
|
351
|
+
setOpen(menu.hidden);
|
|
352
|
+
});
|
|
353
|
+
document.addEventListener("click", function (e) {
|
|
354
|
+
if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) setOpen(false);
|
|
355
|
+
});
|
|
356
|
+
document.addEventListener("keydown", function (e) {
|
|
357
|
+
if (e.key === "Escape" && !menu.hidden) setOpen(false);
|
|
358
|
+
});
|
|
359
|
+
})();
|
|
360
|
+
|
|
361
|
+
$$(".lang").forEach(function (b) {
|
|
362
|
+
b.addEventListener("click", function () {
|
|
363
|
+
if (b.classList.contains("on")) return;
|
|
364
|
+
fetch("/lang?token=" + TOKEN, { method: "POST",
|
|
365
|
+
headers: { "content-type": "application/json" },
|
|
366
|
+
body: JSON.stringify({ lang: b.dataset.lang }) })
|
|
367
|
+
.then(function () { location.reload(); })
|
|
368
|
+
.catch(function () { location.reload(); });
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ── the shared draft ──
|
|
373
|
+
// The terminal is looking at the same run. Publishing every change here is
|
|
374
|
+
// what lets it show what was ticked in this page, and pulling lets this
|
|
375
|
+
// page show what was answered there. Neither is the master; rev decides
|
|
376
|
+
// who was late and hands them the current state instead of accepting a
|
|
377
|
+
// stale copy. (No backticks in here: this whole script is a template
|
|
378
|
+
// literal, and one would end it.)
|
|
379
|
+
// The ready flag gates writing until this page has read what is there.
|
|
380
|
+
// Publishing first was self-defeating: the arrival publish bumped rev to
|
|
381
|
+
// match the server, so the very next poll saw "nothing new" and the page
|
|
382
|
+
// never adopted a run the terminal had already moved along. A surface has
|
|
383
|
+
// to read the shared state before it is allowed to speak for it.
|
|
384
|
+
var rev = 0, pushing = false, adopting = false, ready = false;
|
|
385
|
+
function publish(patch) {
|
|
386
|
+
if (!ready || pushing) return; pushing = true;
|
|
387
|
+
fetch("/select?token=" + TOKEN, { method: "POST",
|
|
388
|
+
headers: { "content-type": "application/json" },
|
|
389
|
+
body: JSON.stringify(Object.assign({ rev: rev }, patch)) })
|
|
390
|
+
.then(function (r) { return r.json(); })
|
|
391
|
+
.then(function (b) { if (typeof b.rev === "number") rev = b.rev; })
|
|
392
|
+
.catch(function () {})
|
|
393
|
+
.then(function () { pushing = false; });
|
|
394
|
+
}
|
|
395
|
+
function publishDraft(step) {
|
|
396
|
+
// Applying someone else's change must not be republished as if it were
|
|
397
|
+
// ours: that bumps rev for no reason and can bounce between surfaces.
|
|
398
|
+
if (adopting) return;
|
|
399
|
+
var c = chosenClient();
|
|
400
|
+
publish({ step: step, draft: {
|
|
401
|
+
clients: c ? [c.value] : [],
|
|
402
|
+
install: c && c.dataset.install ? [c.value] : [],
|
|
403
|
+
llmMode: llmMode(),
|
|
404
|
+
servers: pickedServers(),
|
|
405
|
+
} });
|
|
406
|
+
}
|
|
407
|
+
/** Apply what another surface chose, without echoing it straight back. */
|
|
408
|
+
function adopt(s) {
|
|
409
|
+
if (typeof s.rev !== "number" || s.rev === rev || !s.draft) return;
|
|
410
|
+
rev = s.rev;
|
|
411
|
+
adopting = true;
|
|
412
|
+
try {
|
|
413
|
+
var d = s.draft;
|
|
414
|
+
if (d.clients && d.clients[0]) {
|
|
415
|
+
var el = $('input[name="client"][value="' + d.clients[0] + '"]');
|
|
416
|
+
if (el && !el.checked) { el.checked = true; syncAgent(); }
|
|
417
|
+
}
|
|
418
|
+
if (d.llmMode) {
|
|
419
|
+
var m = $('input[name="lmodeDet"][value="' + d.llmMode + '"]')
|
|
420
|
+
|| $('input[name="lmodeFresh"][value="' + d.llmMode + '"]');
|
|
421
|
+
if (m && !m.checked) { m.checked = true; syncModels(); }
|
|
422
|
+
}
|
|
423
|
+
if (d.servers) {
|
|
424
|
+
var want = {}; d.servers.forEach(function (x) { want[x] = 1; });
|
|
425
|
+
$$('input[name="server"]').forEach(function (i) {
|
|
426
|
+
if (!!want[i.value] !== i.checked) { i.checked = !!want[i.value]; }
|
|
427
|
+
});
|
|
428
|
+
syncCaps();
|
|
429
|
+
}
|
|
430
|
+
// Answering in the terminal advances the run, so this page has to unlock
|
|
431
|
+
// as far as the run has gone — gating on the Next button alone meant the
|
|
432
|
+
// terminal could move to step 3 and this side would sit on step 1,
|
|
433
|
+
// ignoring the update as out of range.
|
|
434
|
+
//
|
|
435
|
+
// It moves this page in both directions. Escape over there rewinds the run
|
|
436
|
+
// and pushes the earlier step, and following only forward left that looking
|
|
437
|
+
// broken: the terminal printed "back to step 2" while this side stayed on
|
|
438
|
+
// 3. unlocked is a high-water mark, so arriving from behind never re-locks
|
|
439
|
+
// a step that was already reached — the rail stays clickable.
|
|
440
|
+
if (s.currentStep && s.currentStep !== current) {
|
|
441
|
+
unlocked = Math.max(unlocked, Math.min(s.currentStep, 5));
|
|
442
|
+
renderSteps();
|
|
443
|
+
go(s.currentStep);
|
|
444
|
+
}
|
|
445
|
+
} finally { adopting = false; }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// While choosing, watch the shared draft so a choice made in the terminal
|
|
449
|
+
// appears here. The run-progress poll below only starts after /apply, which
|
|
450
|
+
// is far too late: the whole point is to stay in step *before* anything is
|
|
451
|
+
// applied. Slower than that one (1.5s against 250ms) because a person
|
|
452
|
+
// ticking boxes is not a progress bar.
|
|
453
|
+
/**
|
|
454
|
+
* Enter progress mode for a run this page did not start.
|
|
455
|
+
*
|
|
456
|
+
* The install view only ever opened from this page's own apply, so a run
|
|
457
|
+
* driven from the terminal left step 5 unseen entirely — the page sat on
|
|
458
|
+
* step 4 through the whole install and only reappeared at step 6 when it
|
|
459
|
+
* was over. Watching the phase is what makes the step the user is actually
|
|
460
|
+
* living through visible on both surfaces.
|
|
461
|
+
*/
|
|
462
|
+
function enterProgress(s) {
|
|
463
|
+
if (locked) return;
|
|
464
|
+
lockSelections();
|
|
465
|
+
serverSteps = s.steps || [];
|
|
466
|
+
shown = {}; lastFlip = 0;
|
|
467
|
+
unlocked = 5;
|
|
468
|
+
renderSteps();
|
|
469
|
+
go(5);
|
|
470
|
+
if (!ticker) ticker = setInterval(tick, 250);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ── one status poller ──
|
|
474
|
+
//
|
|
475
|
+
// There used to be three, two of them running at the same time during
|
|
476
|
+
// selecting, all asking this one endpoint for different reasons. None of
|
|
477
|
+
// them ever stopped: a tab left open on a server that had gone kept asking
|
|
478
|
+
// once a second for as long as the tab stayed open, and kept a live
|
|
479
|
+
// countdown on screen for a page nothing was behind.
|
|
480
|
+
//
|
|
481
|
+
// So: one request, every consumer reads the same answer, the interval
|
|
482
|
+
// slackens when nothing is happening, failures back off, and enough of
|
|
483
|
+
// them in a row is taken to mean the run is over.
|
|
484
|
+
var lastStatus = null;
|
|
485
|
+
var pollFails = 0, pollTimer = null, pollStopped = false;
|
|
486
|
+
var POLL_GIVE_UP = 8;
|
|
487
|
+
|
|
488
|
+
function pollDelay(s) {
|
|
489
|
+
if (!s) return Math.min(30000, 1000 * Math.pow(2, pollFails));
|
|
490
|
+
if (s.phase !== "done" && s.phase !== "failed") return 1000;
|
|
491
|
+
if (s.superseded) return 1000;
|
|
492
|
+
// Idle on the results: the countdown only has to be right while it is
|
|
493
|
+
// on screen, and half an hour at one request a second is 1800 of them.
|
|
494
|
+
var left = s.closingAt ? (s.closingAt - Date.now()) / 1000 : 0;
|
|
495
|
+
return left <= 90 ? 1000 : 5000;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function schedule(s) {
|
|
499
|
+
if (pollStopped) return;
|
|
500
|
+
pollTimer = setTimeout(pollOnce, pollDelay(s));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Everything behind the dialog stops pretending to work.
|
|
505
|
+
*
|
|
506
|
+
* The dialog says the page is over; the button under it was still saying
|
|
507
|
+
* "Signing in…" with its spinner turning, which is the page arguing with
|
|
508
|
+
* itself. Whatever was in flight is not going to finish — the run that
|
|
509
|
+
* would have finished it is gone.
|
|
510
|
+
*/
|
|
511
|
+
function settle() {
|
|
512
|
+
if (ticker) { clearInterval(ticker); ticker = null; }
|
|
513
|
+
document.body.classList.add("settled");
|
|
514
|
+
if (nextBtn) { nextBtn.disabled = true; }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function giveUp() {
|
|
518
|
+
pollStopped = true;
|
|
519
|
+
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
520
|
+
// Say so rather than leaving a countdown ticking against nothing.
|
|
521
|
+
var box = $("#superseded");
|
|
522
|
+
if (box) {
|
|
523
|
+
box.style.display = "flex";
|
|
524
|
+
$("#sstitle").textContent = ${JSON.stringify(T(SUPERSEDED.expiringTitle))};
|
|
525
|
+
$("#ssbody").textContent = ${JSON.stringify(T(SUPERSEDED.closed))};
|
|
526
|
+
settle();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function showClosing(s) {
|
|
531
|
+
if (!s.closingAt) return;
|
|
532
|
+
var left = Math.max(0, Math.ceil((s.closingAt - Date.now()) / 1000));
|
|
533
|
+
// Only in the last minute for a page that is simply timing out: a
|
|
534
|
+
// countdown running for half an hour is not a warning, it is furniture.
|
|
535
|
+
// Being superseded says so immediately, because that one is news.
|
|
536
|
+
if (!s.superseded && left > 60) return;
|
|
537
|
+
var box = $("#superseded");
|
|
538
|
+
box.style.display = "flex";
|
|
539
|
+
settle();
|
|
540
|
+
$("#sstitle").textContent = s.superseded
|
|
541
|
+
? ${JSON.stringify(T(SUPERSEDED.title))}
|
|
542
|
+
: ${JSON.stringify(T(SUPERSEDED.expiringTitle))};
|
|
543
|
+
if (!s.superseded && !box.dataset.reworded) {
|
|
544
|
+
box.dataset.reworded = "1";
|
|
545
|
+
$("#ssbody").innerHTML =
|
|
546
|
+
${JSON.stringify(T(SUPERSEDED.expiringBody))} +
|
|
547
|
+
' <b id="sscount"></b> ' + ${JSON.stringify(T(SUPERSEDED.seconds))} + ".";
|
|
548
|
+
}
|
|
549
|
+
var c = $("#sscount");
|
|
550
|
+
if (c) c.textContent = String(left);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function pollOnce() {
|
|
554
|
+
fetch("/status?token=" + TOKEN)
|
|
555
|
+
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
|
556
|
+
.then(function (s) {
|
|
557
|
+
pollFails = 0;
|
|
558
|
+
lastStatus = s;
|
|
559
|
+
showClosing(s);
|
|
560
|
+
if (s.phase === "selecting") {
|
|
561
|
+
if (!locked) adopt(s);
|
|
562
|
+
} else {
|
|
563
|
+
phase = s.phase;
|
|
564
|
+
serverSteps = s.steps;
|
|
565
|
+
syncClientCard(s.steps);
|
|
566
|
+
// A run this page did not start: show the step being lived through.
|
|
567
|
+
if (!locked) enterProgress(s);
|
|
568
|
+
if (s.phase === "done" || s.phase === "failed") document.title = "✓ AIsa Connected";
|
|
569
|
+
}
|
|
570
|
+
schedule(s);
|
|
571
|
+
})
|
|
572
|
+
.catch(function () {
|
|
573
|
+
// Unreachable, or answering with an error. Either way the thing this
|
|
574
|
+
// page was talking to is not there right now.
|
|
575
|
+
if (++pollFails >= POLL_GIVE_UP) { giveUp(); return; }
|
|
576
|
+
schedule(null);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
pollOnce();
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Nothing to go back to while the machine is being changed.
|
|
583
|
+
*
|
|
584
|
+
* Locking the inputs was not enough: the rail still navigated, and leaving
|
|
585
|
+
* step 5 publishes the step it lands on — which drags the terminal out of a
|
|
586
|
+
* run that is halfway through writing files. The choices stop being choices
|
|
587
|
+
* the moment they are being acted on, and the rail should say so rather
|
|
588
|
+
* than accept a click and quietly do something else.
|
|
589
|
+
*
|
|
590
|
+
* It thaws on its own the moment the run settles, either way: a finished
|
|
591
|
+
* run and a failed one are both things you are allowed to look back at.
|
|
592
|
+
*/
|
|
593
|
+
function frozen() {
|
|
594
|
+
// Not the server's phase. In a dry run — and on a fast machine in a real
|
|
595
|
+
// one — the server reports "done" while the checklist here is still
|
|
596
|
+
// playing itself out a row at a time, and that visible stretch is
|
|
597
|
+
// exactly what a person means by "it is installing". Locking on the
|
|
598
|
+
// server's word thawed the rail in the middle of the animation.
|
|
599
|
+
//
|
|
600
|
+
// The honest signal is the results: locked when the run began, thawed
|
|
601
|
+
// when step 6 exists, which is the moment there is something to go back
|
|
602
|
+
// and look at.
|
|
603
|
+
return locked && unlocked < 6;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Repaint the rail on its own.
|
|
608
|
+
*
|
|
609
|
+
* It used to be painted only inside go(), so freezing after arriving at
|
|
610
|
+
* step 5 changed nothing on screen: the steps kept their pointer cursor
|
|
611
|
+
* and their hover, which is how a rail that refuses clicks still reads as
|
|
612
|
+
* clickable.
|
|
613
|
+
*/
|
|
614
|
+
function paintRail() {
|
|
615
|
+
rails.forEach(function (r) {
|
|
616
|
+
var k = Number(r.dataset.step);
|
|
617
|
+
r.classList.toggle("active", k === current);
|
|
618
|
+
r.classList.toggle("done", k < current || (k <= unlocked && k !== current && (locked || k < current)));
|
|
619
|
+
r.classList.toggle("open", k <= unlocked);
|
|
620
|
+
// Says it before the click rather than swallowing it afterwards.
|
|
621
|
+
r.classList.toggle("frozen", frozen() && k < 5);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function go(n) {
|
|
626
|
+
if (n < 1 || n > 6 || n > unlocked) return;
|
|
627
|
+
if (frozen() && n < 5) return;
|
|
628
|
+
current = n;
|
|
629
|
+
// Only the step, never the draft. Publishing what is ticked here on mere
|
|
630
|
+
// arrival made opening the page look like answering it: the terminal saw
|
|
631
|
+
// the default agent appear and moved on, crediting a choice the user had
|
|
632
|
+
// not made. A draft is published when someone changes something.
|
|
633
|
+
publish({ step: n });
|
|
634
|
+
panes.forEach(function (p) { p.classList.toggle("show", Number(p.dataset.pane) === n); });
|
|
635
|
+
paintRail();
|
|
636
|
+
backBtn.style.visibility = n === 1 ? "hidden" : "visible";
|
|
637
|
+
if (n === 5) { renderInstallPane(); }
|
|
638
|
+
else if (n === 6) { nextBtn.style.display = "none"; navnote.textContent = ""; revealCheck(); }
|
|
639
|
+
else { nextBtn.style.display = ""; nextBtn.disabled = false; nextBtn.innerHTML = (locked ? "Next " : NEXT_WORD[n]) + ARROW; navnote.textContent = ""; }
|
|
640
|
+
if (n === 5 && unlocked >= 6) { nextBtn.style.display = ""; nextBtn.innerHTML = "See your results " + ARROW; }
|
|
641
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
642
|
+
if (n === 2) syncAgent(); if (n === 3) syncModels(); if (n === 4) syncCaps();
|
|
643
|
+
}
|
|
644
|
+
var retryInFlight = false;
|
|
645
|
+
nextBtn.addEventListener("click", function (e) {
|
|
646
|
+
if (!nextBtn.dataset.retry) return;
|
|
647
|
+
// Not a step forward — a second attempt at the step that stopped.
|
|
648
|
+
e.stopImmediatePropagation();
|
|
649
|
+
e.preventDefault();
|
|
650
|
+
nextBtn.disabled = true;
|
|
651
|
+
nextBtn.textContent = "Signing in…";
|
|
652
|
+
delete nextBtn.dataset.retry;
|
|
653
|
+
retryInFlight = true;
|
|
654
|
+
// Whatever goes wrong, the button goes back to being a button. The first
|
|
655
|
+
// version turned into the words "Could not retry" and stayed there — a
|
|
656
|
+
// dead end where the whole point of this control is that there is a way
|
|
657
|
+
// out. It also said nothing about which of the two things went wrong.
|
|
658
|
+
var giveBack = function (why) {
|
|
659
|
+
retryInFlight = false;
|
|
660
|
+
nextBtn.disabled = false;
|
|
661
|
+
nextBtn.textContent = "Log in again";
|
|
662
|
+
nextBtn.dataset.retry = "1";
|
|
663
|
+
navnote.textContent = why;
|
|
664
|
+
};
|
|
665
|
+
fetch("/retry-signin?token=" + TOKEN, { method: "POST" })
|
|
666
|
+
.then(function (r) {
|
|
667
|
+
// A run started before this button existed has no such route. Saying
|
|
668
|
+
// "could not retry" for that is true and useless; the fix is a fresh
|
|
669
|
+
// run, and only this page knows to say so.
|
|
670
|
+
if (r.status === 404) throw new Error("old");
|
|
671
|
+
if (!r.ok) throw new Error("http");
|
|
672
|
+
return r.json();
|
|
673
|
+
})
|
|
674
|
+
.then(function (d) {
|
|
675
|
+
if (!d.ok) {
|
|
676
|
+
giveBack("This run is no longer waiting on a sign-in — run aisa connect again.");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Forget only the rows the run just reset. Clearing the whole map
|
|
680
|
+
// replayed the checklist from "Install the AIsa CLI", which is the
|
|
681
|
+
// same "it started over" the return-from-sign-in used to show — and
|
|
682
|
+
// just as untrue: those rows are still done.
|
|
683
|
+
(serverSteps || []).forEach(function (x) {
|
|
684
|
+
if (x.state !== "ok") delete shown[x.id];
|
|
685
|
+
});
|
|
686
|
+
lastFlip = Date.now();
|
|
687
|
+
unlocked = 5;
|
|
688
|
+
// Cleared once the run reports it is no longer waiting; until then the
|
|
689
|
+
// stale poll would keep re-offering the button.
|
|
690
|
+
var release = setInterval(function () {
|
|
691
|
+
if (lastStatus && !lastStatus.needsSignIn) { retryInFlight = false; clearInterval(release); }
|
|
692
|
+
}, 400);
|
|
693
|
+
document.body.classList.remove("settled");
|
|
694
|
+
if (!ticker) ticker = setInterval(tick, 250);
|
|
695
|
+
})
|
|
696
|
+
.catch(function (e) {
|
|
697
|
+
giveBack(e && e.message === "old"
|
|
698
|
+
? "This page belongs to an older run that cannot retry — run aisa connect again."
|
|
699
|
+
: "Could not reach the run — it may have exited. Run aisa connect again.");
|
|
700
|
+
});
|
|
701
|
+
}, true);
|
|
702
|
+
rails.forEach(function (r) { r.addEventListener("click", function () { go(Number(r.dataset.step)); }); });
|
|
703
|
+
backBtn.addEventListener("click", function () { go(current - 1); });
|
|
704
|
+
|
|
705
|
+
// ── selection state ──
|
|
706
|
+
function chosenClient() { return $('input[name="client"]:checked'); }
|
|
707
|
+
function clientId() { var c = chosenClient(); return c ? c.value : null; }
|
|
708
|
+
function installing() { var c = chosenClient(); return Boolean(c && c.dataset.install === "1"); }
|
|
709
|
+
function clientKind() { var id = clientId(); var c = CLIENTS.filter(function (x) { return x.id === id; })[0]; return c ? c.kind : "file"; }
|
|
710
|
+
function llmMode() {
|
|
711
|
+
if (!clientId()) return "skip";
|
|
712
|
+
if (clientKind() !== "cli" && clientId() !== "vscode") return "skip";
|
|
713
|
+
if (installing()) return ($('input[name="lmodeFresh"]:checked') || {}).value || "skip";
|
|
714
|
+
return ($('input[name="lmodeDet"]:checked') || {}).value || "skip";
|
|
715
|
+
}
|
|
716
|
+
function pickedServers() { return $$('input[name="server"]:checked').map(function (i) { return i.value; }); }
|
|
717
|
+
|
|
718
|
+
// ── step 2 ──
|
|
719
|
+
function syncAgent() {
|
|
720
|
+
var id = clientId();
|
|
721
|
+
$("#agentNoteTitle").textContent = id ? "How " + LABEL[id] + " connects" : "How it connects";
|
|
722
|
+
$("#agentNote").innerHTML = id ? (AGENT_NOTES[id] || "") : "Pick an agent above.";
|
|
723
|
+
var have = id && HAVE_NOTES[id];
|
|
724
|
+
$("#agentHaveTitle").style.display = have ? "" : "none";
|
|
725
|
+
$("#agentHave").style.display = have ? "" : "none";
|
|
726
|
+
if (have) $("#agentHave").innerHTML = have;
|
|
727
|
+
$$(".tile.agent").forEach(function (t) { t.classList.toggle("on", t.querySelector("input").checked); });
|
|
728
|
+
}
|
|
729
|
+
$$('input[name="client"]').forEach(function (r) { r.addEventListener("change", function () { syncAgent(); armed = false; publishDraft(current); }); });
|
|
730
|
+
|
|
731
|
+
// ── step 3 ──
|
|
732
|
+
var armed = false;
|
|
733
|
+
function syncModels() {
|
|
734
|
+
var id = clientId(), fresh = installing(), kind = clientKind();
|
|
735
|
+
$("#mClient").textContent = id ? LABEL[id] : "your agent";
|
|
736
|
+
$("#mModel").textContent = MODEL_FOR[id] || ""; $("#mModel2").textContent = MODEL_FOR[id] || "";
|
|
737
|
+
var vsc = id === "vscode";
|
|
738
|
+
$("#mFresh").style.display = kind === "cli" && fresh ? "" : "none";
|
|
739
|
+
$("#mDetected").style.display = (kind === "cli" && !fresh) || vsc ? "" : "none";
|
|
740
|
+
$("#mFile").style.display = kind !== "cli" && !vsc ? "" : "none";
|
|
741
|
+
// VS Code has no "switch": Copilot's models cannot be replaced, only joined.
|
|
742
|
+
$("#mSwitch").style.display = vsc ? "none" : "";
|
|
743
|
+
if (vsc && $('input[name="lmodeDet"][value="switch"]').checked) $('input[name="lmodeDet"][value="backup"]').checked = true;
|
|
744
|
+
if ((kind === "cli" && !fresh) || vsc) $("#mBackup").innerHTML = BACKUP_COPY[id] || "";
|
|
745
|
+
if (kind !== "cli" && !vsc) $("#mFileText").innerHTML = FILE_MODEL_NOTE[id] || ${JSON.stringify(T(FILE_MODEL_FALLBACK))};
|
|
746
|
+
$$(".choice .tile").forEach(function (t) { t.classList.toggle("on", t.querySelector("input").checked); });
|
|
747
|
+
updateWarn();
|
|
748
|
+
}
|
|
749
|
+
function updateWarn() {
|
|
750
|
+
var need = installing() && llmMode() === "skip";
|
|
751
|
+
$("#modelwarn").style.display = need ? "" : "none";
|
|
752
|
+
if (!need) armed = false;
|
|
753
|
+
}
|
|
754
|
+
$$('input[name="lmodeFresh"], input[name="lmodeDet"]').forEach(function (r) { r.addEventListener("change", function () { syncModels(); publishDraft(current); }); });
|
|
755
|
+
$("#modelfix").addEventListener("click", function () {
|
|
756
|
+
$('input[name="lmodeFresh"][value="switch"]').checked = true; syncModels();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// ── step 4 ──
|
|
760
|
+
var activeCat = null;
|
|
761
|
+
function syncCaps() {
|
|
762
|
+
var picked = pickedServers();
|
|
763
|
+
$$(".ctile").forEach(function (t) {
|
|
764
|
+
var cat = t.dataset.cat;
|
|
765
|
+
var n = SERVERS.filter(function (s) { return s.category === cat && picked.indexOf(s.slug) !== -1; }).length;
|
|
766
|
+
var sel = t.querySelector("[data-csel]");
|
|
767
|
+
sel.textContent = n ? n + " selected" : "";
|
|
768
|
+
t.classList.toggle("has", n > 0);
|
|
769
|
+
t.classList.toggle("active", cat === activeCat);
|
|
770
|
+
});
|
|
771
|
+
$$(".stile").forEach(function (t) {
|
|
772
|
+
t.classList.toggle("on", t.querySelector("input").checked);
|
|
773
|
+
t.style.display = t.dataset.cat === activeCat ? "" : "none";
|
|
774
|
+
});
|
|
775
|
+
$("#spanel").classList.toggle("open", Boolean(activeCat));
|
|
776
|
+
$("#spTitle").textContent = activeCat ? activeCat : "Pick an area above";
|
|
777
|
+
var tools = picked.reduce(function (n, s) { return n + (BY_SLUG[s] ? BY_SLUG[s].toolCount : 0); }, 0);
|
|
778
|
+
$("#tally").innerHTML = picked.length
|
|
779
|
+
? "<b>" + picked.length + " server" + (picked.length > 1 ? "s" : "") + "</b> · " + tools + " tools selected"
|
|
780
|
+
: COPY.nothingSelected;
|
|
781
|
+
}
|
|
782
|
+
$$(".ctile").forEach(function (t) { t.addEventListener("click", function () {
|
|
783
|
+
activeCat = activeCat === t.dataset.cat ? null : t.dataset.cat; syncCaps();
|
|
784
|
+
if (activeCat) $("#spanel").scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
785
|
+
}); });
|
|
786
|
+
$$('input[name="server"]').forEach(function (c) { c.addEventListener("change", function () { syncCaps(); publishDraft(current); }); });
|
|
787
|
+
$("#spAll").addEventListener("click", function () {
|
|
788
|
+
if (!activeCat) return;
|
|
789
|
+
var boxes = $$('.stile[data-cat="' + activeCat + '"] input');
|
|
790
|
+
var all = boxes.every(function (b) { return b.checked; });
|
|
791
|
+
boxes.forEach(function (b) { b.checked = !all; });
|
|
792
|
+
syncCaps();
|
|
793
|
+
});
|
|
794
|
+
// Open the area holding the default selection so the page never starts blank.
|
|
795
|
+
(function () { var first = $('input[name="server"]:checked'); if (first) activeCat = first.closest(".stile").dataset.cat; })();
|
|
796
|
+
|
|
797
|
+
// ── next button ──
|
|
798
|
+
nextBtn.addEventListener("click", function () {
|
|
799
|
+
if (current === 2 && !clientId()) { navnote.textContent = "Pick an agent first."; return; }
|
|
800
|
+
if (current === 3 && installing() && llmMode() === "skip" && !armed) {
|
|
801
|
+
armed = true; updateWarn();
|
|
802
|
+
var w = $("#modelwarn"); w.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
803
|
+
w.style.animation = "none"; void w.offsetWidth; w.style.animation = "mwshake .45s";
|
|
804
|
+
nextBtn.innerHTML = "Continue without a model " + ARROW; return;
|
|
805
|
+
}
|
|
806
|
+
if (current === 4 && !pickedServers().length) { navnote.textContent = "Pick at least one server."; return; }
|
|
807
|
+
if (current === 5) { if (unlocked >= 6) go(6); else start(); return; }
|
|
808
|
+
unlocked = Math.max(unlocked, current + 1);
|
|
809
|
+
go(current + 1);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// ── step 5: plan preview, start, animated progress ──
|
|
813
|
+
var serverSteps = null; // what the server reports
|
|
814
|
+
var shown = {}; // step id -> state we are displaying
|
|
815
|
+
var lastFlip = 0, ticker = null;
|
|
816
|
+
var STATE_WORD = { pending: "waiting", running: "working…", ok: "done", fail: "failed", skip: "skipped" };
|
|
817
|
+
var MIN_DWELL = 1800; // a step stays visibly "working" and then "done" at least this long
|
|
818
|
+
var LOW_DWELL = 3000; // the balance step, when the account is running low
|
|
819
|
+
|
|
820
|
+
function planPreview() {
|
|
821
|
+
var id = clientId(), rows = [];
|
|
822
|
+
if (installing()) rows.push(["Install " + LABEL[id], "through its official installer, no sudo"]);
|
|
823
|
+
rows.push(["Install the AIsa CLI", NEEDS_CLI ? "npm install -g @aisa-one/cli — the aisa command for balance, top-up and key rotation" : "the aisa command is already on this machine"]);
|
|
824
|
+
if (!${keyed}) rows.push(["Sign in to AIsa", "one browser approval — it issues your key"]);
|
|
825
|
+
var n = pickedServers().length;
|
|
826
|
+
rows.push([clientKind() === "web" ? "Prepare " + n + " connector URL" + (n === 1 ? "" : "s") : id === "cursor" ? "Prepare " + n + " Cursor install link" + (n === 1 ? "" : "s") : "Add " + n + " MCP server" + (n === 1 ? "" : "s"), clientKind() === "web" ? "for you to add in claude.ai" : id === "cursor" ? "one click each, confirmed inside Cursor" : "to " + LABEL[id]]);
|
|
827
|
+
var m = llmMode();
|
|
828
|
+
if (m === "switch") rows.push(["Point its models at AIsa", MODEL_FOR[id] + " by default; reversible"]);
|
|
829
|
+
if (m === "backup") rows.push([COPY.planBackup[id] || COPY.planBackup.other, COPY.planBackupNote]);
|
|
830
|
+
rows.push(["Check your AIsa balance", "so an empty account is never a surprise"]);
|
|
831
|
+
return rows.map(function (r) {
|
|
832
|
+
return "<div class='step pending'><span class='mark'></span><span class='body'><span class='lbl'>" + r[0] +
|
|
833
|
+
"</span><span class='det'>" + r[1] + "</span></span><span class='st'>planned</span></div>";
|
|
834
|
+
}).join("");
|
|
835
|
+
}
|
|
836
|
+
function renderInstallPane() {
|
|
837
|
+
if (serverSteps) { renderSteps(); return; }
|
|
838
|
+
$("#plan").innerHTML = planPreview();
|
|
839
|
+
nextBtn.style.display = ""; nextBtn.disabled = true;
|
|
840
|
+
nextBtn.innerHTML = (installing() ? "Install & connect " : "Connect ") + ARROW;
|
|
841
|
+
navnote.textContent = "Starting…";
|
|
842
|
+
// Arriving here is the decision: the plan shows for a beat, then runs.
|
|
843
|
+
// Only auto-start a run this page was navigated into by its own Next
|
|
844
|
+
// button. Arriving at step 5 because another surface said so is not
|
|
845
|
+
// consent: a terminal that publishes the step while still asking the user
|
|
846
|
+
// to confirm would have the run applied out from under it.
|
|
847
|
+
setTimeout(function () { if (current === 5 && !serverSteps && !locked && !adopting) start(); }, 500);
|
|
848
|
+
}
|
|
849
|
+
function renderSteps() {
|
|
850
|
+
var steps = serverSteps || [];
|
|
851
|
+
var rows = steps.map(function (s) {
|
|
852
|
+
var st = shown[s.id] || "pending";
|
|
853
|
+
var det = st === "pending" ? "" : (s.detail || "");
|
|
854
|
+
return "<div class='step " + st + "'><span class='mark'></span><span class='body'><span class='lbl'>" + s.label +
|
|
855
|
+
"</span>" + (det ? "<span class='det'>" + det + "</span>" : "") + "</span><span class='st'>" + STATE_WORD[st] + "</span></div>";
|
|
856
|
+
}).join("");
|
|
857
|
+
$("#plan").innerHTML = rows;
|
|
858
|
+
var settled = steps.filter(function (s) { return /ok|skip|fail/.test(shown[s.id] || ""); }).length;
|
|
859
|
+
var pct = steps.length ? Math.round(settled / steps.length * 100) : 0;
|
|
860
|
+
var anySkipped = steps.some(function (s) { return shown[s.id] === "skip"; });
|
|
861
|
+
$("#barwrap").style.display = "";
|
|
862
|
+
$("#barfill").style.width = (anySkipped ? Math.round((settled - steps.filter(function (s) { return shown[s.id] === "skip"; }).length) / steps.length * 100) : pct) + "%";
|
|
863
|
+
$("#barfill").classList.toggle("halted", anySkipped);
|
|
864
|
+
var running = steps.filter(function (s) { return shown[s.id] === "running"; })[0];
|
|
865
|
+
// Counts what ran, not what was listed: a halted run has rows nobody
|
|
866
|
+
// attempted, and calling those part of "5 of 5 · finished" is the bar
|
|
867
|
+
// congratulating the reader on a setup that did not happen.
|
|
868
|
+
var skipped = steps.filter(function (s) { return shown[s.id] === "skip"; }).length;
|
|
869
|
+
var ran = settled - skipped;
|
|
870
|
+
var total = steps.length - skipped;
|
|
871
|
+
$("#barnote").textContent = ran + " of " + total + " · " +
|
|
872
|
+
(running ? running.label : skipped ? "stopped" : pct === 100 ? "finished" : "…");
|
|
873
|
+
var BTN = { install: "Installing…", signin: "Signing in…", mcp: "Connecting…", llm: "Configuring models…", "llm-backup": "Configuring models…", auth: "Authorizing…", balance: "Finishing…" };
|
|
874
|
+
if (running) { nextBtn.disabled = true; nextBtn.textContent = BTN[running.id.split(":")[0]] || "Working…"; }
|
|
875
|
+
// The run stopped for want of a key, and the one thing that unblocks it
|
|
876
|
+
// is the thing that failed. Offering it here rather than in a line of
|
|
877
|
+
// prose is the difference between a dead end and a retry.
|
|
878
|
+
// retryInFlight, because the status poll that would clear needsSignIn is
|
|
879
|
+
// up to a second behind the click. Without it the button offered itself
|
|
880
|
+
// again in that gap, and the second press met a run that had already
|
|
881
|
+
// started and was told so — an answer that is correct and confusing.
|
|
882
|
+
else if (lastStatus && lastStatus.needsSignIn && !retryInFlight) {
|
|
883
|
+
nextBtn.disabled = false;
|
|
884
|
+
nextBtn.style.display = "";
|
|
885
|
+
nextBtn.textContent = "Log in again";
|
|
886
|
+
nextBtn.dataset.retry = "1";
|
|
887
|
+
} else if (nextBtn.dataset.retry) {
|
|
888
|
+
delete nextBtn.dataset.retry;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/** Said once: the run waits on it, and repeating it would say nothing new. */
|
|
892
|
+
var toldSignin = false;
|
|
893
|
+
function caughtUp() {
|
|
894
|
+
return (serverSteps || []).every(function (s) { return shown[s.id] === s.state; });
|
|
895
|
+
}
|
|
896
|
+
/** Digest of the visible detail text, so a change to it can be noticed. */
|
|
897
|
+
var lastDetails = "";
|
|
898
|
+
function tick() {
|
|
899
|
+
var now = Date.now();
|
|
900
|
+
var steps = serverSteps || [];
|
|
901
|
+
// A row's state is paced by this function; its detail is not — the run
|
|
902
|
+
// rewrites it whenever it has something to say, and the sign-in countdown
|
|
903
|
+
// is nothing but that. Repainting only on a state flip meant those
|
|
904
|
+
// updates went to a screen that never redrew: four seconds of counting,
|
|
905
|
+
// none of it displayed, and then the row jumped to done.
|
|
906
|
+
var details = steps.map(function (s) {
|
|
907
|
+
return (shown[s.id] || "pending") === "pending" ? "" : (s.detail || "");
|
|
908
|
+
}).join("\u0001");
|
|
909
|
+
if (details !== lastDetails) { lastDetails = details; renderSteps(); }
|
|
910
|
+
for (var i = 0; i < steps.length; i++) {
|
|
911
|
+
var s = steps[i], cur = shown[s.id] || "pending";
|
|
912
|
+
if (cur === s.state) continue;
|
|
913
|
+
var dwell = s.id === "balance" && lastStatus && lastStatus.balanceMicros !== null &&
|
|
914
|
+
lastStatus.balanceMicros !== undefined && lastStatus.balanceMicros <= 5e6 ? LOW_DWELL : MIN_DWELL;
|
|
915
|
+
// A skipped step is work that did not happen, so it does not get the
|
|
916
|
+
// moment of work below. Playing it through "working…" on its way to
|
|
917
|
+
// "skipped" is how a halted run looked like it was still going — the
|
|
918
|
+
// bar climbing to 5 of 5 while the reader had just been told nothing
|
|
919
|
+
// would be written.
|
|
920
|
+
if (cur === "pending" && s.state === "skip") {
|
|
921
|
+
shown[s.id] = "skip"; renderSteps(); continue;
|
|
922
|
+
}
|
|
923
|
+
if (cur === "pending" && (s.state === "running" || /ok|skip|fail/.test(s.state))) {
|
|
924
|
+
// Even an instant step gets a visible moment of work before its tick.
|
|
925
|
+
if (now - lastFlip < MIN_DWELL && lastFlip) break;
|
|
926
|
+
shown[s.id] = "running"; lastFlip = now; renderSteps();
|
|
927
|
+
// The run holds the sign-in here until this arrives. Anything else it
|
|
928
|
+
// does is its own business; this one row is the only one whose next
|
|
929
|
+
// move takes the reader out of the page, so it is the only one worth
|
|
930
|
+
// asking permission to leave from.
|
|
931
|
+
if (s.id === "signin" && !toldSignin) {
|
|
932
|
+
toldSignin = true;
|
|
933
|
+
fetch("/signin-shown?token=" + TOKEN, { method: "POST" }).catch(function () {});
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
if (cur === "running" && /ok|skip|fail/.test(s.state)) {
|
|
938
|
+
if (now - lastFlip < dwell) break;
|
|
939
|
+
shown[s.id] = s.state; lastFlip = now; renderSteps(); break;
|
|
940
|
+
}
|
|
941
|
+
break; // a running step stays running until the server settles it
|
|
942
|
+
}
|
|
943
|
+
if ((phase === "done" || phase === "failed") && caughtUp()) {
|
|
944
|
+
// One last beat on the final tick, then tell the process the checklist
|
|
945
|
+
// has been seen — only now may it open the results tab.
|
|
946
|
+
if (now - lastFlip < MIN_DWELL) return;
|
|
947
|
+
clearInterval(ticker); ticker = null;
|
|
948
|
+
finish();
|
|
949
|
+
fetch("/seen?token=" + TOKEN, { method: "POST" }).catch(function () {});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function finish() {
|
|
953
|
+
var failed = (serverSteps || []).filter(function (s) { return s.state === "fail"; }).length;
|
|
954
|
+
var waiting = lastStatus && lastStatus.needsSignIn;
|
|
955
|
+
// "Finished, with 1 issue" is the wrong word for a run that did not
|
|
956
|
+
// finish. Nothing after the sign-in was attempted, so the honest
|
|
957
|
+
// headline is what it is waiting for.
|
|
958
|
+
$("#inTitle").innerHTML = waiting
|
|
959
|
+
? "Waiting on <em>your sign-in</em>"
|
|
960
|
+
: failed ? "Finished, <em>with " + failed + " issue" + (failed > 1 ? "s" : "") + "</em>" : "All <em>connected</em>";
|
|
961
|
+
$("#inLede").textContent = waiting
|
|
962
|
+
? "AIsa could not sign you in, so nothing was written. Everything else is ready to run the moment it succeeds."
|
|
963
|
+
: failed ? COPY.ledeFailed : COPY.ledeAllRan;
|
|
964
|
+
unlocked = 6; rails.forEach(function (r) { r.classList.add("open"); });
|
|
965
|
+
paintRail(); // thawed: the results exist, so the earlier steps are readable again
|
|
966
|
+
renderSteps();
|
|
967
|
+
nextBtn.disabled = false; nextBtn.style.display = "";
|
|
968
|
+
// A run that stopped for want of a key has not finished, it is waiting.
|
|
969
|
+
// "See your results" would send the reader to a page describing a setup
|
|
970
|
+
// that did not happen; the only useful action is the one that failed.
|
|
971
|
+
if (lastStatus && lastStatus.needsSignIn) {
|
|
972
|
+
nextBtn.textContent = "Log in again";
|
|
973
|
+
nextBtn.dataset.retry = "1";
|
|
974
|
+
navnote.textContent = "Nothing was written — the rest of the setup runs once you are signed in.";
|
|
975
|
+
rails[4].classList.remove("done");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
nextBtn.innerHTML = "See your results " + ARROW;
|
|
979
|
+
navnote.textContent = VIEW === "start" && lastStatus && lastStatus.doneUrl ? "A results tab also opened on its own." : "";
|
|
980
|
+
renderDone();
|
|
981
|
+
rails[4].classList.add("done");
|
|
982
|
+
}
|
|
983
|
+
function lockSelections() {
|
|
984
|
+
locked = true;
|
|
985
|
+
paintRail();
|
|
986
|
+
$$('.pane[data-pane="2"] input, .pane[data-pane="3"] input, .pane[data-pane="4"] input').forEach(function (i) { i.disabled = true; });
|
|
987
|
+
$$(".ctile, #spAll").forEach(function (b) { b.disabled = false; });
|
|
988
|
+
document.body.classList.add("locked");
|
|
989
|
+
}
|
|
990
|
+
function start() {
|
|
991
|
+
var body = { servers: pickedServers(), clients: [clientId()], install: installing() ? [clientId()] : [], llmMode: llmMode() };
|
|
992
|
+
nextBtn.disabled = true; nextBtn.textContent = installing() ? "Installing…" : "Connecting…";
|
|
993
|
+
navnote.textContent = "";
|
|
994
|
+
lockSelections();
|
|
995
|
+
fetch("/apply", { method: "POST", headers: { "content-type": "application/json", "x-connect-token": TOKEN }, body: JSON.stringify(body) })
|
|
996
|
+
.then(function (r) { return r.json(); })
|
|
997
|
+
.then(function (d) { serverSteps = d.steps; shown = {}; lastFlip = 0; renderSteps(); if (!ticker) ticker = setInterval(tick, 250); })
|
|
998
|
+
.catch(function () {
|
|
999
|
+
// The run could not be started. Give the buttons back rather than
|
|
1000
|
+
// leaving the page frozen on "Connecting…" with nothing behind it.
|
|
1001
|
+
nextBtn.disabled = false;
|
|
1002
|
+
nextBtn.innerHTML = (installing() ? "Install & connect " : "Connect ") + ARROW;
|
|
1003
|
+
navnote.textContent = ${JSON.stringify(T(SUPERSEDED.closed))};
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function syncClientCard(steps) {
|
|
1007
|
+
(steps || []).forEach(function (s) {
|
|
1008
|
+
if (s.id.indexOf("install:") !== 0 || s.state !== "ok") return;
|
|
1009
|
+
var card = $('.tile.agent[data-cid="' + s.id.slice(8) + '"]'); if (!card) return;
|
|
1010
|
+
var b = card.querySelector("[data-badge]"); if (b) { b.className = "badge ok"; b.textContent = "✓ installed"; }
|
|
1011
|
+
var br = card.querySelector("[data-brief]"); if (br) br.innerHTML = "<b>Installed</b> — " + (s.detail || "ready");
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ── step 6 ──
|
|
1016
|
+
var doneRendered = false;
|
|
1017
|
+
// The tick after the headline draws itself only once the results pane is
|
|
1018
|
+
// actually on screen, a beat after it appears.
|
|
1019
|
+
function revealCheck() {
|
|
1020
|
+
if (doneRendered) return;
|
|
1021
|
+
var c = $(".h1check"); if (!c) return;
|
|
1022
|
+
doneRendered = true;
|
|
1023
|
+
setTimeout(function () { c.classList.add("show"); }, 350);
|
|
1024
|
+
}
|
|
1025
|
+
function fmtUsd(m) { return "$" + (m / 1e6).toFixed(2); }
|
|
1026
|
+
function renderDone() {
|
|
1027
|
+
var s = lastStatus; if (!s || !s.selection) return;
|
|
1028
|
+
doneRendered = false;
|
|
1029
|
+
var sel = s.selection, steps = s.steps || [];
|
|
1030
|
+
var id = sel.clients[0], name = LABEL[id] || id;
|
|
1031
|
+
var chosen = sel.servers.map(function (x) { return BY_SLUG[x]; }).filter(Boolean);
|
|
1032
|
+
var tools = chosen.reduce(function (n, x) { return n + x.toolCount; }, 0);
|
|
1033
|
+
var failed = steps.filter(function (x) { return x.state === "fail"; });
|
|
1034
|
+
var mcpFailed = failed.some(function (x) { return x.id === "mcp"; });
|
|
1035
|
+
var installed = steps.filter(function (x) { return x.id.indexOf("install:") === 0 && x.id !== "install:aisa-cli" && x.state === "ok"; })
|
|
1036
|
+
.map(function (x) { return LABEL[x.id.slice(8)] || x.id.slice(8); });
|
|
1037
|
+
var llmOk = steps.some(function (x) { return x.id === "llm" && x.state === "ok"; });
|
|
1038
|
+
var backup = sel.llmMode === "backup";
|
|
1039
|
+
var bin = id === "codex" ? (backup ? "codex-aisa" : "codex") : id === "claude-code" ? (backup ? "claude-aisa" : "claude") : id === "opencode" ? "opencode" : null;
|
|
1040
|
+
|
|
1041
|
+
var head = mcpFailed
|
|
1042
|
+
? "<div class='eyebrow'>Almost there</div><h1>Your agent is <em>not connected yet</em></h1><p class='lede'>The MCP entries could not be added to <b>" + name + "</b> — details below.</p>"
|
|
1043
|
+
: (installed.length
|
|
1044
|
+
? "<h1><em>Congratulations!</em> " + installed.join(" & ") + " is installed and armed with " + tools + " powerful tools<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'><b>" + installed.join(" & ") + "</b> is on this machine, signed in to AIsa" + (llmOk ? ", running on <b>" + MODEL_FOR[id] + "</b> through AIsa," : ",") + " with " + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " wired in — a complete setup, nothing else to configure.</p>"
|
|
1045
|
+
: "<h1><em>Congratulations!</em> Your agent just got " + tools + " powerful new tool" + (tools > 1 ? "s" : "") + ".<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'>" + (id === "claude-ai"
|
|
1046
|
+
? chosen.length + " connector URL" + (chosen.length > 1 ? "s are" : " is") + " ready for <b>Claude.ai</b> — add them below, it takes about a minute."
|
|
1047
|
+
: id === "cursor" && s.deeplinks && s.deeplinks.length
|
|
1048
|
+
? chosen.length + " install link" + (chosen.length > 1 ? "s are" : " is") + " ready for <b>Cursor</b> — one click each, below."
|
|
1049
|
+
: chosen.length + " AIsa MCP server" + (chosen.length > 1 ? "s are" : " is") + " now installed and signed in for <b>" + name + "</b> — nothing else to configure.") + "</p>");
|
|
1050
|
+
|
|
1051
|
+
var failBlock = failed.length ? "<div class='authnote warn'><div><b>" + failed.length + " step" + (failed.length > 1 ? "s" : "") + " did not complete:</b><ul>" +
|
|
1052
|
+
failed.map(function (x) { return "<li><b>" + x.label + "</b> — " + (x.detail || "failed") + "</li>"; }).join("") +
|
|
1053
|
+
"</ul>Fix the above, then run <code>npx @aisa-one/cli connect</code> again — it is safe to re-run.</div></div>" : "";
|
|
1054
|
+
var ICON = { ok: "✓", fail: "✕", skip: "–", pending: "·", running: "·" };
|
|
1055
|
+
var recap = "<h2>Everything you just gained</h2><div class='recap'><div class='rsum'>" + chosen.length + " capabilit" + (chosen.length === 1 ? "y" : "ies") + " · " + tools + " tools · " + name + "</div>" +
|
|
1056
|
+
// The balance has its own card right below, so its row stays out of the recap.
|
|
1057
|
+
steps.filter(function (x) { return x.id !== "balance"; }).map(function (x) { return "<div class='rrow " + x.state + "'><span class='rl'>" + x.label + "</span><span class='rd' title=\\"" + (x.detail || "").replace(/"/g, """) + "\\">" + (x.detail || "") + "</span><span class='ri'>" + ICON[x.state] + "</span></div>"; }).join("") + "</div>";
|
|
1058
|
+
var bal = s.balanceMicros, low = bal !== null && bal !== undefined && bal < 5e6;
|
|
1059
|
+
// The welcome-credit note is shown on every results page, whatever the
|
|
1060
|
+
// balance: it is the one place the top-up is explained.
|
|
1061
|
+
var gift = true;
|
|
1062
|
+
var balCard = "<div class='balcard" + (low || gift ? " low" : "") + "'><div><div class='balnum'>" + (bal === null || bal === undefined ? "—" : fmtUsd(bal)) + "</div><div class='ballbl'>AIsa balance</div></div><div class='balright'>" +
|
|
1063
|
+
(gift ? "<div class='giftnote'><b>AIsa has given you $1 to get started.</b> That covers your first few calls. Top up now so your agent never stops mid-task.</div>" : low ? "<div class='lownote'>Running a little low — a small top-up keeps your first calls flowing.</div>" : (bal === null || bal === undefined ? "<div class='lownote'>Could not read it just now — <code>aisa balance</code> will.</div>" : "")) +
|
|
1064
|
+
"<a class='cta sm' href='https://console.aisa.one/billing?source=aisa_cli' target='_blank' rel='noopener noreferrer'>Top up now →</a></div></div>";
|
|
1065
|
+
var fileNote = "";
|
|
1066
|
+
if (id === "claude-desktop") {
|
|
1067
|
+
fileNote = "<div class='vsccard'><div class='vschead'><span class='blogo lg'>" + CLAUDE_DT_LOGO + "</span><div><b>Claude Desktop is ready</b>" +
|
|
1068
|
+
"<div class='fine' style='margin:0'>" + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " (plus aisa-docs) are in its config — one restart loads them.</div></div>" +
|
|
1069
|
+
"<div class='termside'><button type='button' class='cta sm' id='launch'>Restart Claude Desktop →</button><span class='fine' id='launchnote'></span></div></div>" +
|
|
1070
|
+
"<div class='vscgrid'>" +
|
|
1071
|
+
"<div class='vscitem'><span class='srv'>Tools</span>After the restart, open the <b>tools menu</b> (🔧) in a chat — the <code>aisa-…</code> servers are there. Each runs as a small bridge that lives and dies with the app; no background service.</div>" +
|
|
1072
|
+
"<div class='vscitem'><span class='srv'>Across devices</span>Prefer one setup on every device? Add the same server URLs as <b>custom Connectors</b> under Settings → Connectors — they sync with claude.ai through your account.</div>" +
|
|
1073
|
+
"</div></div>";
|
|
1074
|
+
}
|
|
1075
|
+
if (id === "vscode") {
|
|
1076
|
+
var llmStep = steps.filter(function (x) { return x.id === "llm-backup"; })[0];
|
|
1077
|
+
var needsPaste = llmStep && /paste/.test(llmStep.detail || "");
|
|
1078
|
+
var modelsOn = sel.llmMode === "backup" && !needsPaste;
|
|
1079
|
+
fileNote = "<div class='vsccard'><div class='vschead'><span class='blogo lg'>" + VSCODE_LOGO + "</span><div><b>VS Code is ready</b>" +
|
|
1080
|
+
"<div class='fine' style='margin:0'>" + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + (modelsOn ? " and " + MODEL_COUNT + " AIsa models" : "") + " are in place — no reload needed.</div></div>" +
|
|
1081
|
+
"<div class='termside'><button type='button' class='cta sm' id='launch'>Open VS Code →</button><span class='fine' id='launchnote'></span></div></div>" +
|
|
1082
|
+
"<div class='vscgrid'>" +
|
|
1083
|
+
(modelsOn ? "<div class='vscitem'><span class='srv'>Models</span>Open <b>Chat</b> (<code>Ctrl+Cmd+I</code>), click the model name at the bottom-right of the input — the <b>AIsa</b> group lists Claude, GPT, DeepSeek, Kimi, GLM and Qwen. The AIsa extension already stored your key.</div>" : "") +
|
|
1084
|
+
"<div class='vscitem'><span class='srv'>Tools</span>Switch Chat to <b>Agent</b> mode and tick the <code>aisa-…</code> servers in the tools picker (🔧). They are listed under <b>Extensions → MCP Servers</b>; if one is missing, run <b>Reload Window</b> once.</div>" +
|
|
1085
|
+
"</div></div>";
|
|
1086
|
+
if (sel.llmMode === "backup" && needsPaste) {
|
|
1087
|
+
fileNote += "<h2>One paste in VS Code — your key</h2><div class='webcard'>" +
|
|
1088
|
+
"<p class='fine' style='margin:0 0 .6rem'>VS Code keeps model keys in its own encrypted store, which only its UI can write — so this is the one step it does not let us do for you.</p>" +
|
|
1089
|
+
"<ol class='websteps'><li>Open Chat (<code>Ctrl+Cmd+I</code>), click the <b>model name</b> at the bottom-right of the input, then <b>Manage Models…</b></li>" +
|
|
1090
|
+
"<li>Pick <b>AIsa</b> and paste your key when asked (it is stored securely by VS Code)</li>" +
|
|
1091
|
+
"<li>Back in the picker, choose any AIsa model — Claude, GPT, DeepSeek, Kimi, GLM, Qwen</li></ol>" +
|
|
1092
|
+
"<button type='button' class='cta sm' id='copykey'>Copy your AIsa key</button> <span class='fine' id='copykeynote'></span></div>";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (id === "cursor" && s.deeplinks && s.deeplinks.length) {
|
|
1096
|
+
fileNote = "<div class='vsccard'><div class='vschead'><span class='blogo lg mono'>" + CURSOR_LOGO + "</span><div><b>Cursor is ready</b>" +
|
|
1097
|
+
"<div class='fine' style='margin:0'>" + s.deeplinks.length + " install link" + (s.deeplinks.length > 1 ? "s" : "") + " below — one click each, confirmed inside Cursor.</div></div>" +
|
|
1098
|
+
"<div class='termside'><button type='button' class='cta sm' id='launch'>Open Cursor →</button><span class='fine' id='launchnote'></span></div></div>" +
|
|
1099
|
+
"<div class='vscgrid'>" +
|
|
1100
|
+
"<div class='vscitem'><span class='srv'>Tools</span>Each button opens Cursor with the entry ready; press <b>Install</b> there. Afterwards <b>Cursor Settings → MCP</b> lists them as <code>aisa-…</code>, and Agent mode can use them.</div>" +
|
|
1101
|
+
"<div class='vscitem'><span class='srv'>Models</span>Cursor keeps its own models for Tab and inline edits. For chat and Agent you can point it at AIsa by hand: <b>Settings → Models → Override OpenAI Base URL</b> = <code>https://api.aisa.one/v1</code> with your AIsa key.</div>" +
|
|
1102
|
+
"</div>" +
|
|
1103
|
+
s.deeplinks.map(function (d) {
|
|
1104
|
+
return "<div class='weburl'><div><span class='srv'>" + d.name + "</span><code>" + (BY_SLUG[d.slug] ? BY_SLUG[d.slug].endpoint : "") + "</code></div>" +
|
|
1105
|
+
"<a class='cta sm' href='" + d.url + "'>Add to Cursor →</a></div>";
|
|
1106
|
+
}).join("") + "</div>";
|
|
1107
|
+
}
|
|
1108
|
+
if (id === "claude-ai") {
|
|
1109
|
+
fileNote += "<h2>Finish in claude.ai — about a minute</h2><div class='webcard'>" +
|
|
1110
|
+
"<ol class='websteps'><li>Open <a class='lnk' href='https://claude.ai/settings/connectors' target='_blank' rel='noopener noreferrer'>claude.ai → Settings → Connectors</a></li>" +
|
|
1111
|
+
"<li>Click <b>Add custom connector</b>, paste a name and URL from below, then <b>Add</b></li>" +
|
|
1112
|
+
"<li>Press <b>Connect</b> and approve the AIsa sign-in — once per connector</li></ol>" +
|
|
1113
|
+
chosen.map(function (x) {
|
|
1114
|
+
return "<div class='weburl'><div><span class='srv'>aisa-" + x.slug + "</span><code>" + x.endpoint + "</code></div>" +
|
|
1115
|
+
"<button type='button' data-copy=\\"" + x.endpoint + "\\">" + ICON_COPY + " Copy URL</button></div>";
|
|
1116
|
+
}).join("") + "</div>";
|
|
1117
|
+
}
|
|
1118
|
+
var backupNote = backup && id === "vscode" ? "" : backup ? "<p class='rerun'><b>" + COPY.backupIntact + "</b> " + (id === "opencode" ? COPY.backupOpencode : COPY.backupBin.split("{bin}").join(bin)) + "</p>" : "";
|
|
1119
|
+
var model = MODEL_FOR[id] || "";
|
|
1120
|
+
var preview = id === "opencode"
|
|
1121
|
+
? "<pre class='termlogo oc'>" + ART.opencode + "</pre><div class='termline'>Welcome to <b>opencode</b></div><div class='termline dim'>model: <b>" + PROVIDER_ID + "/" + model + "</b> · via AIsa</div><div class='termline dim'>config: ~/.config/opencode/opencode.json</div>"
|
|
1122
|
+
: id === "codex"
|
|
1123
|
+
? "<pre class='termlogo codex'>" + ART.codex + "</pre><div class='termline'>Welcome to <b>Codex</b>, OpenAI's command-line coding agent</div><div class='termline dim'>model: <b>" + model + "</b> · via AIsa</div>"
|
|
1124
|
+
: "<pre class='termlogo claude'>" + ART.claude + "</pre><div class='termline'><b class='ccname'>Claude Code</b></div><div class='termline dim'>" + model + " · via AIsa</div><div class='termline accent'>Using " + model + " (from .claude/settings.json)</div>";
|
|
1125
|
+
var launch = !mcpFailed && bin ? "<div class='termcard'><div class='termwin'><div class='termbar'><span class='tdot r'></span><span class='tdot y'></span><span class='tdot g'></span></div><div class='termbody'>" + preview + "</div></div>" +
|
|
1126
|
+
"<div class='termside'><button type='button' class='cta sm' id='launch'>Launch " + bin + " →</button><span class='fine' id='launchnote'></span></div></div>" : "";
|
|
1127
|
+
var withEx = chosen.filter(function (x) { return EXAMPLES[x.slug]; });
|
|
1128
|
+
var cards = withEx.length === 1 ? EXAMPLES[withEx[0].slug].slice(0, 2).map(function (t) { return { slug: withEx[0].slug, text: t }; })
|
|
1129
|
+
: withEx.slice(0, 4).map(function (x) { return { slug: x.slug, text: EXAMPLES[x.slug][0] }; });
|
|
1130
|
+
var examples = cards.map(function (c) {
|
|
1131
|
+
return "<div class='example'><div><span class='srv'>aisa-" + c.slug + "</span><div class='txt'>" + c.text + "</div></div><button type='button' data-copy=\\"" + c.text.replace(/"/g, """) + "\\">" + ICON_COPY + " Copy</button></div>";
|
|
1132
|
+
}).join("");
|
|
1133
|
+
var more = SERVERS.length - chosen.length;
|
|
1134
|
+
var rest = mcpFailed ? failBlock + recap + balCard
|
|
1135
|
+
: "<p class='lede'>You are connected to <b>AIsa</b> — one account for all the well-known models and the live data behind them." + (more > 0 ? " " + more + " more MCP server" + (more > 1 ? "s are" : " is") + " one <code>npx @aisa-one/cli connect</code> away." : "") + " See your account dashboard at <a class='lnk' href='https://console.aisa.one' target='_blank' rel='noopener noreferrer'>console.aisa.one</a>.</p>" +
|
|
1136
|
+
failBlock + recap + balCard + fileNote + launch +
|
|
1137
|
+
"<h2>Try it now — paste one of these into " + name + (id === "claude-ai" || id === "cursor" ? " once the servers are added" : id === "claude-desktop" ? " after the restart" : "") + "</h2><div class='examples'>" + (examples || "<p class='fine'>Ask your agent to use any of the aisa-* MCP tools.</p>") + "</div>" +
|
|
1138
|
+
backupNote +
|
|
1139
|
+
"<p class='rerun'>${T(STEP_MODELS.rerun)}</p>";
|
|
1140
|
+
$("#doneBody").innerHTML = head + rest;
|
|
1141
|
+
$$("[data-copy]").forEach(function (b) { b.addEventListener("click", function () {
|
|
1142
|
+
navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () {
|
|
1143
|
+
b.textContent = "Copied ✓"; setTimeout(function () { b.innerHTML = ICON_COPY + " Copy"; }, 1600); });
|
|
1144
|
+
}); });
|
|
1145
|
+
var ck = $("#copykey"); if (ck) ck.addEventListener("click", function () {
|
|
1146
|
+
fetch("/key?token=" + TOKEN).then(function (r) { return r.json(); }).then(function (d) {
|
|
1147
|
+
if (!d.key) throw 0;
|
|
1148
|
+
return navigator.clipboard.writeText(d.key);
|
|
1149
|
+
}).then(function () { ck.textContent = "Copied ✓"; setTimeout(function () { ck.textContent = "Copy your AIsa key"; }, 1600); })
|
|
1150
|
+
.catch(function () { $("#copykeynote").innerHTML = COPY.noKeyStored; });
|
|
1151
|
+
});
|
|
1152
|
+
var lb = $("#launch"); if (lb) lb.addEventListener("click", function () {
|
|
1153
|
+
lb.disabled = true;
|
|
1154
|
+
fetch("/launch?token=" + TOKEN, { method: "POST" }).then(function (r) { if (!r.ok) throw 0; lb.textContent = id === "vscode" ? "✓ VS Code opened" : id === "cursor" ? "✓ Cursor opened" : id === "claude-desktop" ? "✓ Restarting…" : "✓ Opened in Terminal"; })
|
|
1155
|
+
.catch(function () { lb.style.display = "none"; $("#launchnote").className = "fine told"; $("#launchnote").innerHTML = id === "vscode" || id === "cursor" || id === "claude-desktop" ? COPY.cannotStartApp.split("{name}").join(name) : COPY.cannotOpenTerminal.split("{bin}").join(bin); });
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// ── hydrate: a run may already be in flight (the done tab, or a reload) ──
|
|
1160
|
+
function hydrate(s) {
|
|
1161
|
+
var sel = s.selection; if (!sel) return false;
|
|
1162
|
+
var c = $('input[name="client"][value="' + sel.clients[0] + '"]'); if (c) c.checked = true;
|
|
1163
|
+
$$('input[name="server"]').forEach(function (i) { i.checked = sel.servers.indexOf(i.value) !== -1; });
|
|
1164
|
+
var fresh = sel.install.length > 0;
|
|
1165
|
+
var r = $('input[name="' + (fresh ? "lmodeFresh" : "lmodeDet") + '"][value="' + sel.llmMode + '"]'); if (r) r.checked = true;
|
|
1166
|
+
var first = $('input[name="server"]:checked'); if (first) activeCat = first.closest(".stile").dataset.cat;
|
|
1167
|
+
syncAgent(); syncModels(); syncCaps(); lockSelections();
|
|
1168
|
+
phase = s.phase; serverSteps = s.steps; lastStatus = s;
|
|
1169
|
+
var settled = phase === "done" || phase === "failed";
|
|
1170
|
+
// Coming back from the sign-in tab is not the same as arriving late.
|
|
1171
|
+
//
|
|
1172
|
+
// The reader left this page for exactly one row — the sign-in — and that
|
|
1173
|
+
// row's outcome is the only thing they went away to get. Adopting it in
|
|
1174
|
+
// silence with everything else meant the trip they had just taken had no
|
|
1175
|
+
// visible result: the checklist was simply further along than they left
|
|
1176
|
+
// it, and the one line that says the key is theirs went by unread.
|
|
1177
|
+
//
|
|
1178
|
+
// So the return resumes at the sign-in: everything above it is adopted
|
|
1179
|
+
// at once (no replay from the top), the sign-in itself lands with its own
|
|
1180
|
+
// tick and its own sentence, and only then does the run move on. This
|
|
1181
|
+
// holds even for a run that finished while they were away — the rows are
|
|
1182
|
+
// paced out rather than snapped to a finished checklist.
|
|
1183
|
+
var resumeAt = -1;
|
|
1184
|
+
if (CAME_BACK) {
|
|
1185
|
+
(s.steps || []).forEach(function (x, i) { if (resumeAt < 0 && x.id === "signin") resumeAt = i; });
|
|
1186
|
+
// Consume the marker: a manual reload later is an arrival, not a return,
|
|
1187
|
+
// and should not replay a sign-in that happened minutes ago.
|
|
1188
|
+
if (history.replaceState) history.replaceState(null, "", location.pathname);
|
|
1189
|
+
}
|
|
1190
|
+
if ((settled || VIEW === "done") && resumeAt < 0) {
|
|
1191
|
+
serverSteps.forEach(function (x) { shown[x.id] = x.state; });
|
|
1192
|
+
unlocked = 6; renderSteps(); finish();
|
|
1193
|
+
syncClientCard(s.steps);
|
|
1194
|
+
} else {
|
|
1195
|
+
// Catch up to the run; do not re-perform it.
|
|
1196
|
+
//
|
|
1197
|
+
// The shown map starts empty on every page load, so a page arriving in
|
|
1198
|
+
// the middle of a run replayed the whole checklist from the top at one
|
|
1199
|
+
// row every 1.8s — which is what the tab coming back from a sign-in
|
|
1200
|
+
// did: the reader watched Install the AIsa CLI start again and could
|
|
1201
|
+
// only conclude the setup had restarted.
|
|
1202
|
+
//
|
|
1203
|
+
// The pacing exists to make a first viewing legible. A page that
|
|
1204
|
+
// arrives late has missed it, not earned the right to see it — so
|
|
1205
|
+
// everything already settled is adopted at once and the animation picks
|
|
1206
|
+
// up at the row that is actually working. The one exception is the
|
|
1207
|
+
// return from the sign-in (resumeAt), which is not an arrival: see the
|
|
1208
|
+
// note where resumeAt is computed.
|
|
1209
|
+
if (CAME_BACK) {
|
|
1210
|
+
var bf = $("#backfrom");
|
|
1211
|
+
if (bf) {
|
|
1212
|
+
// Says the one thing the checklist cannot: the trip is over and
|
|
1213
|
+
// this is the same setup they left. The receipt — that a key now
|
|
1214
|
+
// exists and where it lives — belongs to the sign-in row a few
|
|
1215
|
+
// lines below, which lands with its own tick and holds still long
|
|
1216
|
+
// enough to be read. Saying it in both places twice over, two
|
|
1217
|
+
// lines apart, is not emphasis; it is noise.
|
|
1218
|
+
bf.innerHTML = "\u2713 Signed in \u2014 picking up where your setup left off.";
|
|
1219
|
+
bf.hidden = false;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var caughtUp = false;
|
|
1223
|
+
(serverSteps || []).forEach(function (x, i) {
|
|
1224
|
+
if (caughtUp) return;
|
|
1225
|
+
// On a return: adopt down to and including the sign-in, then stop.
|
|
1226
|
+
// The rows after it are the ones still worth watching happen.
|
|
1227
|
+
if (resumeAt >= 0 && i > resumeAt) { caughtUp = true; return; }
|
|
1228
|
+
if (/ok|skip|fail/.test(x.state)) { shown[x.id] = x.state; return; }
|
|
1229
|
+
if (x.state === "running") { shown[x.id] = "running"; caughtUp = true; }
|
|
1230
|
+
});
|
|
1231
|
+
// The sign-in result is on screen the moment the page paints. Start the
|
|
1232
|
+
// dwell clock here so it stays there: the next row cannot begin until a
|
|
1233
|
+
// full MIN_DWELL has passed, which is the beat the reader needs in order
|
|
1234
|
+
// to read that the key is theirs before the checklist moves again.
|
|
1235
|
+
if (resumeAt >= 0) lastFlip = Date.now();
|
|
1236
|
+
unlocked = 5; renderSteps(); if (!ticker) ticker = setInterval(tick, 250);
|
|
1237
|
+
}
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
fetch("/status?token=" + TOKEN).then(function (r) { return r.json(); }).then(function (s) {
|
|
1241
|
+
if (hydrate(s)) { go(VIEW === "done" || s.phase === "done" || s.phase === "failed" ? (VIEW === "done" ? 6 : 5) : 5); return; }
|
|
1242
|
+
// Land where the run is, not where a run would start. hydrate only knows
|
|
1243
|
+
// about a run already applying; a run still being chosen — in the
|
|
1244
|
+
// terminal, say — was answered here with go(1), throwing away the step
|
|
1245
|
+
// the shared draft had already reached.
|
|
1246
|
+
if (s.phase === "selecting") { adopt(s); ready = true; }
|
|
1247
|
+
go(Math.max(current, 1));
|
|
1248
|
+
}).catch(function () { ready = true; go(1); });
|
|
1249
|
+
})();
|
|
1250
|
+
</script>`;
|
|
1251
|
+
return shellT2(view === "done" ? "✓ AIsa Connected" : "AIsa Connect", body);
|
|
1252
|
+
}
|
|
1253
|
+
// ── T2 shell: same brand tokens as T1, a rail + main layout ─────────────────
|
|
1254
|
+
function shellT2(title, body) {
|
|
1255
|
+
return `<!doctype html>
|
|
1256
|
+
<html><head><meta charset="utf-8"><title>${title}</title>
|
|
1257
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1258
|
+
<!--
|
|
1259
|
+
This page's own address carries the run token, and the page links out to
|
|
1260
|
+
aisa.one and console.aisa.one. rel="noopener" does not suppress a referrer —
|
|
1261
|
+
only noreferrer does — so without this, clicking any of them posted
|
|
1262
|
+
"http://127.0.0.1:PORT/?token=…" into someone else's access log. Same shape
|
|
1263
|
+
as the /cli/auth page handing its authorization code to analytics.
|
|
1264
|
+
-->
|
|
1265
|
+
<meta name="referrer" content="no-referrer">
|
|
1266
|
+
<style>
|
|
1267
|
+
:root { --paper: ${PAPER}; --ink: #1c1b1a; --muted: #6d6a66; --line: #e7e4df; --card: #ffffff;
|
|
1268
|
+
--red: ${RED}; --red-cta: ${RED_CTA}; --bar: ${INK}; --tint: #fdf1ef; --ok: #2e7d43; --warn: #f59e0b; }
|
|
1269
|
+
@media (prefers-color-scheme: dark) {
|
|
1270
|
+
:root { --paper: #141312; --ink: #f0eeeb; --muted: #9b9792; --line: #2c2a27; --card: #1d1c1a;
|
|
1271
|
+
--tint: #2a1917; --ok: #57b06f; }
|
|
1272
|
+
}
|
|
1273
|
+
* { box-sizing: border-box; margin: 0; }
|
|
1274
|
+
html { scroll-behavior: smooth; }
|
|
1275
|
+
body { background: var(--paper); color: var(--ink);
|
|
1276
|
+
font: 18px/1.6 Inter, "Inter Fallback", "PingFang SC", ui-sans-serif, system-ui, sans-serif;
|
|
1277
|
+
background-image: radial-gradient(color-mix(in srgb, var(--muted) 22%, transparent) 1px, transparent 1px);
|
|
1278
|
+
background-size: 22px 22px; }
|
|
1279
|
+
.wrap { display: grid; grid-template-columns: 408px minmax(0, 1fr); min-height: 100vh; }
|
|
1280
|
+
/* The rail: same paper as the main area, one hairline between them. The
|
|
1281
|
+
wordmark sits at the top; the six steps float at the vertical centre so
|
|
1282
|
+
the space above and below them is equal at any window height. */
|
|
1283
|
+
.rail { border-right: 1px solid var(--line); padding: 1.6rem 2.2rem; position: sticky; top: 0;
|
|
1284
|
+
height: 100vh; display: flex; flex-direction: column; }
|
|
1285
|
+
.railhead { display: flex; align-items: flex-end; gap: .6rem; color: var(--ink); padding: 0 .6rem;
|
|
1286
|
+
text-decoration: none; transition: opacity .15s ease; }
|
|
1287
|
+
.railhead:hover { opacity: .72; }
|
|
1288
|
+
.railhead svg { width: 89px; height: auto; display: block; }
|
|
1289
|
+
.railhead span { font-weight: 600; font-size: 1.28rem; line-height: 1; color: var(--muted); padding-bottom: 4px; }
|
|
1290
|
+
/* Sits a little above centre: the gap below is noticeably larger than the gap above. */
|
|
1291
|
+
.railsteps { margin: auto 0; display: flex; flex-direction: column; gap: .3rem; padding-bottom: 22vh; }
|
|
1292
|
+
.rstep { display: flex; gap: .8rem; align-items: center; text-align: left; background: transparent;
|
|
1293
|
+
border: 0; border-radius: 8px; padding: .7rem .7rem; font: inherit; color: var(--muted);
|
|
1294
|
+
cursor: default; opacity: .55; position: relative; }
|
|
1295
|
+
.rstep.open { opacity: 1; cursor: pointer; }
|
|
1296
|
+
.rstep.open:hover { background: color-mix(in srgb, var(--tint) 60%, transparent); }
|
|
1297
|
+
/* While the run is writing to the machine: still legible, plainly not a
|
|
1298
|
+
control. No hover, because a hover that leads nowhere is a small lie. */
|
|
1299
|
+
.backfrom { display: flex; align-items: center; gap: .55rem; margin: 0 0 1rem;
|
|
1300
|
+
padding: .62rem .85rem; border: 1px solid #bfe0c8; border-radius: 10px;
|
|
1301
|
+
background: #f2faf4; color: #216c37; font-size: .9rem; }
|
|
1302
|
+
.backfrom[hidden] { display: none; }
|
|
1303
|
+
.rstep.frozen { cursor: default; opacity: .45; }
|
|
1304
|
+
.rstep.frozen:hover { background: transparent; }
|
|
1305
|
+
.rstep .rn { flex: none; width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--line);
|
|
1306
|
+
display: flex; align-items: center; justify-content: center; font-size: .82rem; font-weight: 700; }
|
|
1307
|
+
.rstep .rtitle { display: block; font-weight: 700; font-size: 1.08rem; color: var(--ink); }
|
|
1308
|
+
.rstep .rsub { display: block; font-size: .86rem; color: var(--muted); }
|
|
1309
|
+
.rstep.active { background: var(--card); box-shadow: 0 1px 0 var(--line), 0 0 0 1px var(--line); }
|
|
1310
|
+
.rstep.active .rn { background: var(--red); border-color: var(--red); color: #fff; }
|
|
1311
|
+
.rstep.done .rn { background: var(--ok); border-color: var(--ok); color: #fff; font-size: 0; }
|
|
1312
|
+
.rstep.done .rn::after { content: "\\2713"; font-size: .85rem; }
|
|
1313
|
+
/* The main area centres its content both ways; panes are capped so lines
|
|
1314
|
+
stay readable on a wide screen. */
|
|
1315
|
+
.main { display: flex; align-items: center; justify-content: center; padding: 2rem 4rem; }
|
|
1316
|
+
.content { width: 100%; max-width: 980px; }
|
|
1317
|
+
.ssup { position: fixed; inset: 0; background: rgba(13,13,11,.72); z-index: 99;
|
|
1318
|
+
display: flex; align-items: center; justify-content: center; backdrop-filter: blur(3px); }
|
|
1319
|
+
.ssbox { background: #fff; border: 1px solid var(--line); border-radius: 14px;
|
|
1320
|
+
padding: 1.8rem 2rem; max-width: 30rem; box-shadow: 0 18px 50px rgba(0,0,0,.25); }
|
|
1321
|
+
.ssbox h2 { margin: 0 0 .6rem; font-size: 1.15rem; }
|
|
1322
|
+
.ssbox p { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
1323
|
+
.ssbox #sscount { color: var(--red); font-variant-numeric: tabular-nums; font-size: 1.1rem; }
|
|
1324
|
+
/* Set apart from the sentence above, because it is not about what just
|
|
1325
|
+
happened — it is the two things still worth knowing afterwards. */
|
|
1326
|
+
.sscta { margin: 1.05rem 0 0; padding: .9rem 0 0; border-top: 1px solid var(--line); }
|
|
1327
|
+
.sscta p { margin: 0 0 .38rem; font-size: .9rem; line-height: 1.55; }
|
|
1328
|
+
.sscta p:last-child { margin-bottom: 0; }
|
|
1329
|
+
.sscta a { color: var(--red-cta); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1330
|
+
text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--red-cta) 38%, transparent); }
|
|
1331
|
+
.sscta a:hover { border-bottom-color: var(--red-cta); }
|
|
1332
|
+
.sscta code { font-size: .88rem; background: var(--paper); border: 1px solid var(--line);
|
|
1333
|
+
border-radius: 5px; padding: .06rem .34rem; }
|
|
1334
|
+
/* Nothing behind the dialog is still happening. The spinner on a running
|
|
1335
|
+
step is the loudest of these: it says work is under way at the same
|
|
1336
|
+
moment the dialog says the run is gone. */
|
|
1337
|
+
.settled .step.running .mark { animation: none; border-top-color: var(--line); opacity: .5; }
|
|
1338
|
+
.settled .barfill { transition: none; }
|
|
1339
|
+
.settled #next { opacity: .5; }
|
|
1340
|
+
.topnav { min-height: 3rem; margin-bottom: 2.4rem; display: flex; align-items: center; justify-content: space-between; }
|
|
1341
|
+
.more { position: relative; display: inline-flex; }
|
|
1342
|
+
.morebtn { display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
|
|
1343
|
+
border: 1px solid var(--line); border-radius: 9px; background: #fff; color: var(--muted); cursor: pointer; }
|
|
1344
|
+
.morebtn:hover, .morebtn[aria-expanded="true"] { color: var(--ink); border-color: var(--muted); }
|
|
1345
|
+
.moremenu { position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
|
|
1346
|
+
display: flex; flex-direction: column; min-width: 132px; padding: 4px;
|
|
1347
|
+
border: 1px solid var(--line); border-radius: 10px; background: #fff;
|
|
1348
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.08); }
|
|
1349
|
+
.moremenu[hidden] { display: none; }
|
|
1350
|
+
/* No border of its own — the menu already draws one, and a box inside a
|
|
1351
|
+
box is the shape nothing else on this page has. A row here is text that
|
|
1352
|
+
lights up under the cursor. */
|
|
1353
|
+
.moremenu .lang { display: flex; justify-content: space-between; align-items: center;
|
|
1354
|
+
border: 0; background: none; cursor: pointer; font: inherit; font-size: .86rem;
|
|
1355
|
+
color: var(--muted); text-align: left; padding: 8px 10px; border-radius: 6px; line-height: 1.4; }
|
|
1356
|
+
.moremenu .lang:hover { background: var(--paper); color: var(--ink); }
|
|
1357
|
+
.moremenu .lang.on { color: var(--ink); font-weight: 600; }
|
|
1358
|
+
.moremenu .lang.on::after { content: "✓"; opacity: .55; font-weight: 400; }
|
|
1359
|
+
.topnav .ghost { padding: .65rem 1.3rem; font-size: 1rem; }
|
|
1360
|
+
.pane { display: none; animation: fade .25s ease; }
|
|
1361
|
+
.pane.show { display: block; }
|
|
1362
|
+
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
1363
|
+
@media (max-width: 960px) {
|
|
1364
|
+
.wrap { grid-template-columns: 1fr; }
|
|
1365
|
+
.rail { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
1366
|
+
.railsteps { flex-direction: row; flex-wrap: wrap; margin: .8rem 0 0; padding: 0; }
|
|
1367
|
+
.rail .rsub { display: none; }
|
|
1368
|
+
.main { padding: 1.6rem 5% 4rem; align-items: flex-start; }
|
|
1369
|
+
}
|
|
1370
|
+
.eyebrow { display: flex; align-items: center; gap: .55rem; color: var(--muted); font-size: .74rem;
|
|
1371
|
+
font-weight: 600; letter-spacing: .14em; text-transform: uppercase; }
|
|
1372
|
+
.eyebrow::before { content: ""; width: 26px; height: 3px; background: var(--red); }
|
|
1373
|
+
h1 { font-size: 2.45rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.15; margin: .5rem 0 .6rem; }
|
|
1374
|
+
h1 em { font-style: normal; color: var(--red); }
|
|
1375
|
+
h2 { font-size: 1.15rem; font-weight: 700; margin: 2rem 0 .8rem; }
|
|
1376
|
+
h3 { font-size: 1rem; font-weight: 700; margin: 0 0 .3rem; }
|
|
1377
|
+
.lede { color: var(--muted); font-size: 1.08rem; }
|
|
1378
|
+
.fine { color: var(--muted); font-size: .86rem; margin-top: .9rem; }
|
|
1379
|
+
/* When the launch button fails it is replaced by this, and it stops being
|
|
1380
|
+
fine print: it is the only thing left telling the reader what to do. */
|
|
1381
|
+
#launchnote.told { font-size: 1rem; color: var(--ink); }
|
|
1382
|
+
#launchnote.told code { font-size: .95rem; }
|
|
1383
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em;
|
|
1384
|
+
background: color-mix(in srgb, var(--muted) 12%, transparent); padding: .1em .35em; border-radius: 4px; }
|
|
1385
|
+
.badge { font-size: .72rem; font-weight: 700; padding: .16rem .6rem; border-radius: 99px;
|
|
1386
|
+
border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }
|
|
1387
|
+
.badge.ok { background: var(--ok); border-color: var(--ok); color: #fff; }
|
|
1388
|
+
.badge.todo { background: var(--warn); border-color: var(--warn); color: #fff; }
|
|
1389
|
+
.badge.rec { background: var(--red); border-color: var(--red); color: #fff; }
|
|
1390
|
+
.badge.web { background: var(--ink); border-color: var(--ink); color: #fff; }
|
|
1391
|
+
.webcard { border: 1px solid var(--line); border-radius: 12px; background: var(--card); padding: 1rem 1.2rem; margin: 0 0 1.4rem; }
|
|
1392
|
+
.websteps { margin: 0 0 .9rem 1.2rem; font-size: .98rem; } .websteps li { margin: .25rem 0; }
|
|
1393
|
+
.weburl { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .6rem 0; border-top: 1px dashed var(--line); }
|
|
1394
|
+
.weburl .srv { display: block; color: var(--red); font-weight: 600; font-size: .74rem; letter-spacing: .06em; text-transform: uppercase; }
|
|
1395
|
+
.weburl code { font-size: .9rem; overflow-wrap: anywhere; }
|
|
1396
|
+
.weburl button { flex: none; display: inline-flex; align-items: center; gap: .35rem; font: inherit; font-size: .8rem; font-weight: 600;
|
|
1397
|
+
color: var(--ink); background: transparent; border: 1px solid var(--line); border-radius: 6px; padding: .35rem .7rem; cursor: pointer; }
|
|
1398
|
+
.weburl button:hover { border-color: var(--red); color: var(--red); }
|
|
1399
|
+
/* welcome */
|
|
1400
|
+
.feat { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin: 1.8rem 0 1rem; }
|
|
1401
|
+
.ftile { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 1.3rem 1.4rem; }
|
|
1402
|
+
.ftile p { color: var(--muted); font-size: .95rem; }
|
|
1403
|
+
.fico { width: 38px; height: 38px; border-radius: 10px; background: var(--tint); color: var(--red);
|
|
1404
|
+
display: flex; align-items: center; justify-content: center; margin-bottom: .8rem; }
|
|
1405
|
+
.fico svg { width: 20px; height: 20px; }
|
|
1406
|
+
.strip { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .9rem; }
|
|
1407
|
+
.blogo { display: inline-flex; width: 26px; height: 26px; align-items: center; justify-content: center; }
|
|
1408
|
+
.blogo svg { width: 100%; height: 100%; }
|
|
1409
|
+
.blogo.lg { width: 36px; height: 36px; }
|
|
1410
|
+
/* Single-colour marks drawn for light backgrounds (Cursor) flip in dark mode. */
|
|
1411
|
+
@media (prefers-color-scheme: dark) { .mono svg { filter: invert(1); } }
|
|
1412
|
+
/* tiles (agents, model choice) */
|
|
1413
|
+
.two { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 2rem; margin-top: 1.6rem; align-items: start; }
|
|
1414
|
+
@media (max-width: 960px) { .two { grid-template-columns: 1fr; } }
|
|
1415
|
+
.side { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 1.2rem 1.4rem;
|
|
1416
|
+
font-size: .95rem; color: var(--muted); margin-top: 1.4rem; }
|
|
1417
|
+
.grid1 { display: grid; grid-template-columns: 1fr; gap: .8rem; margin-top: 1.6rem; }
|
|
1418
|
+
.choice.grid1 { margin-top: .4rem; }
|
|
1419
|
+
.choice .tile { align-items: center; }
|
|
1420
|
+
.choice .tbody { flex: 1; min-width: 0; }
|
|
1421
|
+
.choice .thead { width: 100%; }
|
|
1422
|
+
.badge.rec { margin-left: auto; }
|
|
1423
|
+
.side h3 { color: var(--ink); margin-top: 1rem; } .side h3:first-child { margin-top: 0; }
|
|
1424
|
+
.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem; }
|
|
1425
|
+
.grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .8rem; }
|
|
1426
|
+
@media (max-width: 720px) { .grid2, .grid3, .feat { grid-template-columns: 1fr; } }
|
|
1427
|
+
.tile { display: flex; gap: .8rem; align-items: flex-start; background: var(--card); border: 1px solid var(--line);
|
|
1428
|
+
border-left: 3px solid var(--line); border-radius: 10px; padding: 1rem 1.1rem; cursor: pointer;
|
|
1429
|
+
transition: border-color .15s, box-shadow .15s; }
|
|
1430
|
+
.tile:hover { border-color: color-mix(in srgb, var(--red) 45%, var(--line)); }
|
|
1431
|
+
.tile.on { border-left-color: var(--red); background: color-mix(in srgb, var(--tint) 55%, var(--card)); }
|
|
1432
|
+
.tile input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
1433
|
+
.tile input.dot { position: static; opacity: 1; width: 20px; height: 20px; margin-top: .3rem; accent-color: var(--red-cta); flex: none; }
|
|
1434
|
+
.tile.agent { align-items: center; }
|
|
1435
|
+
.badge.end { margin-left: auto; flex: none; }
|
|
1436
|
+
.tlogo { flex: none; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; }
|
|
1437
|
+
.tlogo svg { width: 100%; height: 100%; }
|
|
1438
|
+
.tbody { min-width: 0; } .thead { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
1439
|
+
.tname { font-weight: 700; } .tbrief { display: block; color: var(--muted); font-size: .9rem; margin-top: .2rem; }
|
|
1440
|
+
.choice { margin-top: .4rem; }
|
|
1441
|
+
.soon { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: 1rem; }
|
|
1442
|
+
.chip { display: inline-flex; align-items: center; gap: .4rem; font-size: .82rem; color: var(--muted);
|
|
1443
|
+
border: 1px dashed var(--line); border-radius: 99px; padding: .3rem .75rem; }
|
|
1444
|
+
.chip svg { width: 16px; height: 16px; }
|
|
1445
|
+
body.locked .tile, body.locked .stile { cursor: default; }
|
|
1446
|
+
body.locked .tile:not(.on), body.locked .stile:not(.on) { opacity: .45; }
|
|
1447
|
+
/* models */
|
|
1448
|
+
.pgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .8rem; margin: 1.6rem 0 1.2rem; }
|
|
1449
|
+
@media (max-width: 720px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
|
|
1450
|
+
.ptile { display: grid; grid-template-columns: 40px 1fr; grid-template-rows: auto auto; column-gap: .8rem;
|
|
1451
|
+
align-items: center; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem; }
|
|
1452
|
+
.ptile .blogo { grid-row: 1 / 3; }
|
|
1453
|
+
.pname { font-weight: 700; } .pmodels { font-size: .8rem; color: var(--muted); }
|
|
1454
|
+
.callout { display: flex; gap: .8rem; align-items: flex-start; background: var(--card); border: 1px solid var(--line);
|
|
1455
|
+
border-radius: 10px; padding: 1rem 1.1rem; color: var(--muted); font-size: .95rem; margin: 1rem 0; }
|
|
1456
|
+
.callout svg { flex: none; color: var(--red); margin-top: .15rem; }
|
|
1457
|
+
.callout b { color: var(--ink); }
|
|
1458
|
+
.rerun { margin: 1rem 0 0; font-size: 1.02rem; color: var(--muted); }
|
|
1459
|
+
.rerun + .rerun { margin-top: .35rem; }
|
|
1460
|
+
.rerun code { font-size: 1.05em; color: var(--ink); }
|
|
1461
|
+
.modelwarn { margin-top: 1rem; border: 2px solid var(--warn); border-radius: 10px;
|
|
1462
|
+
background: color-mix(in srgb, var(--warn) 12%, var(--card)); padding: .9rem 1rem; }
|
|
1463
|
+
.mw-head { font-weight: 800; color: color-mix(in srgb, #b45309 60%, var(--ink)); margin-bottom: .3rem; }
|
|
1464
|
+
.mw-body { font-size: .92rem; color: color-mix(in srgb, #92400e 45%, var(--ink)); }
|
|
1465
|
+
.mw-fix { margin-top: .7rem; background: var(--red-cta); color: #fff; border: 0; border-radius: 6px;
|
|
1466
|
+
padding: .5rem 1.2rem; font: inherit; font-weight: 700; cursor: pointer; }
|
|
1467
|
+
@keyframes mwshake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
|
|
1468
|
+
/* capabilities: the area grid, then the servers of the open area */
|
|
1469
|
+
.cgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .9rem; margin: 1.6rem 0 1rem; }
|
|
1470
|
+
.ctile { position: relative; display: flex; flex-direction: column; gap: .25rem; align-items: flex-start; text-align: left;
|
|
1471
|
+
background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 1.2rem 1.2rem 1.1rem;
|
|
1472
|
+
font: inherit; color: var(--ink); cursor: pointer; min-height: 168px; transition: border-color .15s, transform .15s; }
|
|
1473
|
+
.ctile:hover { border-color: color-mix(in srgb, var(--red) 45%, var(--line)); transform: translateY(-1px); }
|
|
1474
|
+
.ctile.active { border-color: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent); }
|
|
1475
|
+
.ctile.has { border-left: 3px solid var(--ok); }
|
|
1476
|
+
.cico { width: 40px; height: 40px; border-radius: 10px; background: var(--tint); color: var(--red);
|
|
1477
|
+
display: flex; align-items: center; justify-content: center; margin-bottom: .4rem; }
|
|
1478
|
+
.cico svg { width: 20px; height: 20px; }
|
|
1479
|
+
.cname { font-weight: 800; font-size: 1.05rem; }
|
|
1480
|
+
.cmeta { font-size: .78rem; color: var(--muted); }
|
|
1481
|
+
.cblurb { font-size: .86rem; color: var(--muted); margin-top: .3rem; }
|
|
1482
|
+
.csel { position: absolute; top: .8rem; right: .9rem; font-size: .74rem; font-weight: 700; color: var(--ok); }
|
|
1483
|
+
.spanel { display: none; border: 1px solid var(--line); border-radius: 12px; background: color-mix(in srgb, var(--card) 60%, var(--paper));
|
|
1484
|
+
padding: 1rem 1.2rem 1.2rem; }
|
|
1485
|
+
.spanel.open { display: block; animation: fade .2s ease; }
|
|
1486
|
+
.sphead { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
1487
|
+
.sphead h2 { margin: .2rem 0 .8rem; }
|
|
1488
|
+
.link { background: none; border: 0; color: var(--red); font: inherit; font-size: .85rem; font-weight: 600; cursor: pointer; }
|
|
1489
|
+
.sgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: .8rem; }
|
|
1490
|
+
.stile { display: flex; gap: .7rem; align-items: flex-start; background: var(--card); border: 1px solid var(--line);
|
|
1491
|
+
border-left: 3px solid var(--line); border-radius: 10px; padding: .9rem 1rem; cursor: pointer; }
|
|
1492
|
+
.stile.on { border-left-color: var(--red); background: color-mix(in srgb, var(--tint) 55%, var(--card)); }
|
|
1493
|
+
.stile input { width: 18px; height: 18px; margin-top: .2rem; accent-color: var(--red-cta); flex: none; }
|
|
1494
|
+
.sdesc { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
|
|
1495
|
+
color: var(--muted); font-size: .88rem; margin-top: .2rem; }
|
|
1496
|
+
.tally { margin-top: 1rem; font-size: .95rem; color: var(--muted); }
|
|
1497
|
+
.tally b { color: var(--ink); }
|
|
1498
|
+
/* install */
|
|
1499
|
+
.plan { margin-top: 1.6rem; background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: .4rem 1.2rem; }
|
|
1500
|
+
.step { display: flex; align-items: flex-start; gap: .8rem; padding: .8rem .2rem; border-bottom: 1px dashed var(--line);
|
|
1501
|
+
font-size: 1.02rem; opacity: .5; transition: opacity .3s; }
|
|
1502
|
+
.step:last-child { border-bottom: 0; }
|
|
1503
|
+
.step.running, .step.ok, .step.fail { opacity: 1; }
|
|
1504
|
+
.step .lbl { display: block; font-weight: 600; } .step .det { display: block; color: var(--muted); font-size: .86rem; margin-top: .1rem; }
|
|
1505
|
+
.step .st { margin-left: auto; font-size: .8rem; font-weight: 600; color: var(--muted); white-space: nowrap; padding-left: .6rem; }
|
|
1506
|
+
.step.ok .st { color: var(--ok); } .step.fail .st { color: var(--red); }
|
|
1507
|
+
.step .mark { flex: none; width: 22px; height: 22px; margin-top: .15rem; border-radius: 50%; border: 2px solid var(--line);
|
|
1508
|
+
display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; color: #fff;
|
|
1509
|
+
transition: background .25s, border-color .25s; }
|
|
1510
|
+
.step.running .mark { border-color: var(--red); border-top-color: transparent; animation: r .8s linear infinite; }
|
|
1511
|
+
.step.ok .mark { background: var(--ok); border-color: var(--ok); animation: pop .35s ease; }
|
|
1512
|
+
.step.ok .mark::after { content: "\\2713"; }
|
|
1513
|
+
.step.ok .st, .step.fail .st { animation: fade .4s ease; }
|
|
1514
|
+
.step.fail .mark { background: var(--red); border-color: var(--red); } .step.fail .mark::after { content: "\\2715"; }
|
|
1515
|
+
.step.skip .mark { border-style: dotted; }
|
|
1516
|
+
@keyframes r { to { transform: rotate(360deg); } }
|
|
1517
|
+
@keyframes pop { 0% { transform: scale(.6); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } }
|
|
1518
|
+
.barwrap { height: 5px; background: var(--line); border-radius: 99px; overflow: hidden; margin: 1rem 0 .3rem; }
|
|
1519
|
+
.barfill { height: 100%; width: 0; background: var(--red); border-radius: 99px; transition: width .4s ease; }
|
|
1520
|
+
.barnote { color: var(--muted); font-size: .82rem; min-height: 1.2em; }
|
|
1521
|
+
.authnote { display: flex; gap: .7rem; align-items: flex-start; background: var(--card); border: 1px solid var(--line);
|
|
1522
|
+
border-radius: 10px; padding: 1rem 1.1rem; color: var(--muted); font-size: .92rem; margin-top: 1.2rem; }
|
|
1523
|
+
.authnote svg { flex: none; margin-top: .1rem; color: var(--red); } .authnote b { color: var(--ink); }
|
|
1524
|
+
.authnote.warn { border-color: var(--warn); } .authnote ul { margin: .4rem 0 .4rem 1.1rem; }
|
|
1525
|
+
/* nav */
|
|
1526
|
+
.navbar { display: flex; flex-direction: column; align-items: center; gap: .6rem; margin-top: 2.4rem; }
|
|
1527
|
+
.navnote { color: var(--red); font-size: .9rem; min-height: 1.2em; }
|
|
1528
|
+
.cta { display: inline-flex; align-items: center; justify-content: center; gap: .6rem; background: var(--red-cta); color: #fff;
|
|
1529
|
+
border: none; border-radius: 6px; font: inherit; font-weight: 600; font-size: 1.08rem; padding: .85rem 1.9rem; cursor: pointer;
|
|
1530
|
+
text-decoration: none; }
|
|
1531
|
+
.cta:hover { background: color-mix(in srgb, var(--red-cta) 88%, black); }
|
|
1532
|
+
.cta:disabled { opacity: .55; cursor: default; }
|
|
1533
|
+
.cta.sm { font-size: .95rem; padding: .6rem 1.3rem; }
|
|
1534
|
+
.ghost { background: transparent; border: 1px solid var(--line); color: var(--ink); border-radius: 6px; font: inherit;
|
|
1535
|
+
font-weight: 600; padding: .8rem 1.2rem; cursor: pointer; }
|
|
1536
|
+
.ghost:hover { border-color: var(--red); color: var(--red); }
|
|
1537
|
+
/* done */
|
|
1538
|
+
/* The tick sits at the end of the headline, headline-sized, and pops in
|
|
1539
|
+
once the pane is on screen. */
|
|
1540
|
+
.h1check { display: inline-flex; vertical-align: middle; margin-left: .45rem; width: .85em; height: .85em;
|
|
1541
|
+
border-radius: 50%; background: var(--red); color: #fff; align-items: center; justify-content: center;
|
|
1542
|
+
transform: scale(0); opacity: 0; }
|
|
1543
|
+
.h1check svg { width: .53em; height: .53em; }
|
|
1544
|
+
.h1check.show { animation: tickpop .5s cubic-bezier(.2,1.4,.4,1) forwards; }
|
|
1545
|
+
@keyframes tickpop { 0% { transform: scale(0); opacity: 0; } 70% { transform: scale(1.18); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
|
|
1546
|
+
.recap, .balcard, .launch { border: 1px solid var(--line); border-radius: 12px; background: var(--card); padding: 1rem 1.2rem; margin: 0 0 1.2rem; }
|
|
1547
|
+
.rsum { font-weight: 600; padding-bottom: .5rem; border-bottom: 1px dashed var(--line); margin-bottom: .5rem; }
|
|
1548
|
+
.rrow { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .8rem; align-items: center;
|
|
1549
|
+
min-height: 2.9rem; padding: .3rem 0; border-bottom: 1px solid var(--line); }
|
|
1550
|
+
.rrow:last-child { border-bottom: 0; }
|
|
1551
|
+
/* Every row the same height: the detail is one line, clipped, full text on hover. */
|
|
1552
|
+
.rrow .rl { font-weight: 700; white-space: nowrap; }
|
|
1553
|
+
.rrow .rd { color: var(--muted); font-size: .92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1554
|
+
.rrow { height: 2.9rem; }
|
|
1555
|
+
.rrow .ri { font-weight: 800; }
|
|
1556
|
+
.rrow.ok .ri { color: var(--ok); } .rrow.fail .ri { color: var(--red); } .rrow.skip .ri { color: #9ca3af; }
|
|
1557
|
+
.balcard, .launch { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
|
1558
|
+
.balcard.low { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, var(--card)); }
|
|
1559
|
+
.balnum { font-size: 1.6rem; font-weight: 800; } .ballbl { font-size: .8rem; color: var(--muted); }
|
|
1560
|
+
.balright { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
|
1561
|
+
.giftnote { font-size: 1rem; color: color-mix(in srgb, #b45309 70%, var(--ink)); max-width: 30rem; }
|
|
1562
|
+
.giftnote b { color: var(--red); }
|
|
1563
|
+
.lownote { font-size: .9rem; color: color-mix(in srgb, #b45309 70%, var(--ink)); max-width: 26rem; }
|
|
1564
|
+
.launch .fine { margin-top: .2rem; }
|
|
1565
|
+
/* The launch card: a believable little terminal with the agent's own art. */
|
|
1566
|
+
.termcard { display: flex; justify-content: space-between; align-items: center; gap: 1.2rem; border: 1px solid var(--line);
|
|
1567
|
+
border-radius: 12px; background: var(--card); padding: 1rem 1.2rem; margin: 0 0 1.4rem; flex-wrap: wrap; }
|
|
1568
|
+
.termwin { background: #0d0d0b; border-radius: 10px; overflow: hidden; flex: 1 1 24rem; min-width: 0; box-shadow: inset 0 0 0 1px #262622; }
|
|
1569
|
+
.termbar { display: flex; gap: .38rem; padding: .5rem .7rem; background: #1a1a17; }
|
|
1570
|
+
.tdot { width: .62rem; height: .62rem; border-radius: 50%; }
|
|
1571
|
+
.tdot.r { background: #ff5f57; } .tdot.y { background: #febc2e; } .tdot.g { background: #28c840; }
|
|
1572
|
+
.termbody { padding: .6rem 1rem .8rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .74rem; line-height: 1.25; }
|
|
1573
|
+
.termlogo { margin: 0 0 .45rem; font-size: .3rem; line-height: 1.1; overflow-x: auto; }
|
|
1574
|
+
.termlogo.codex { color: #33d17a; }
|
|
1575
|
+
.termlogo.oc { color: #fafafa; font-size: .54rem; line-height: 1.15; }
|
|
1576
|
+
.termlogo.claude { color: #e07b54; font-size: .48rem; line-height: 1; }
|
|
1577
|
+
.termline { color: #d8d8d2; padding: .08rem 0; overflow-wrap: anywhere; } .termline b { color: #fff; }
|
|
1578
|
+
.termline.dim { color: #8a8a82; } .termline.accent { border-left: 2px solid #33d17a; padding-left: .5rem; }
|
|
1579
|
+
.ccname { color: #33d17a !important; }
|
|
1580
|
+
.termside { display: flex; flex-direction: column; align-items: flex-end; gap: .5rem; flex: none; }
|
|
1581
|
+
a.lnk { color: var(--red); text-decoration: none; font-weight: 600; }
|
|
1582
|
+
a.lnk:hover { text-decoration: underline; }
|
|
1583
|
+
.vsccard { border: 1px solid var(--line); border-radius: 12px; background: var(--card); padding: 1.1rem 1.3rem; margin: 0 0 1.4rem; }
|
|
1584
|
+
.vschead { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap; }
|
|
1585
|
+
.vschead > div:nth-child(2) { flex: 1; min-width: 14rem; font-size: 1.05rem; }
|
|
1586
|
+
.vscgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: .9rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px dashed var(--line); }
|
|
1587
|
+
.vsccard .weburl:first-of-type { margin-top: .6rem; }
|
|
1588
|
+
.vscitem { font-size: .95rem; color: var(--muted); } .vscitem b, .vscitem code { color: var(--ink); }
|
|
1589
|
+
.vscitem .srv { display: block; color: var(--red); font-weight: 600; font-size: .74rem; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .25rem; }
|
|
1590
|
+
.examples { display: grid; gap: .8rem; }
|
|
1591
|
+
.example { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.1rem; display: flex; gap: .9rem; align-items: flex-start; }
|
|
1592
|
+
.example .srv { color: var(--red); font-weight: 600; font-size: .74rem; letter-spacing: .06em; text-transform: uppercase; display: block; margin-bottom: .25rem; }
|
|
1593
|
+
.example button { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: .35rem; font: inherit; font-size: .8rem;
|
|
1594
|
+
font-weight: 600; color: var(--ink); background: transparent; border: 1px solid var(--line); border-radius: 6px; padding: .35rem .7rem; cursor: pointer; }
|
|
1595
|
+
.example button:hover { border-color: var(--red); color: var(--red); }
|
|
1596
|
+
</style></head><body>
|
|
1597
|
+
${body}
|
|
1598
|
+
</body></html>`;
|
|
1599
|
+
}
|