Skip to content

Commit 148b49f

Browse files
committed
Clarify guided integrations setup
1 parent 2289879 commit 148b49f

4 files changed

Lines changed: 131 additions & 52 deletions

File tree

web/app/(console)/integrations/page-content.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ assertIncludes("integrations page has terminal category", source, "Terminal");
1919
assertIncludes("integrations page has AI agents category", source, "AI agents");
2020
assertIncludes("integrations page has editors category", source, "Editors");
2121
assertIncludes("integrations page tracks active setup category", source, "activeToolCategory");
22-
assertIncludes("integrations page shows Stint CLI connected state", source, "Yes, Stint CLI is connected");
22+
assertIncludes("integrations page has guided terminal action", source, "Install Stint");
23+
assertIncludes("integrations page has guided agent action", source, "Install agent plugin");
24+
assertIncludes("integrations page has guided editor action", source, "Install editor plugin");
25+
assertIncludes("integrations page models connection status", source, "connectionStatus");
26+
assertIncludes("integrations page shows not connected state", source, "Not connected yet");
27+
assertIncludes("integrations page shows pending check-in state", source, "Waiting for first check-in");
28+
assertIncludes("integrations page shows connected state", source, "Stint is connected");
2329
assertIncludes("integrations page makes integration cards selectable", source, "setSelectedIntegration");
2430
assertIncludes("integrations page updates the hash for selected setup cards", source, 'window.history.replaceState(null, "", `#${recipeId}`)');
2531
assertIncludes("integrations page exposes selected instructions region", source, "integration-instructions");
@@ -60,6 +66,7 @@ assertExcludes("integrations page removes status tiles", source, "StatusTile");
6066
assertExcludes("integrations page removes screenshot previews", source, "<img");
6167
assertExcludes("integrations page does not lead with catalog jargon", source, "Integration catalog");
6268
assertExcludes("integrations page removes right-side detail panel", source, "DetailPanel");
69+
assertExcludes("integrations page does not use copy setup as primary source text", source, "Copy setup, run it once");
6370

6471
assertExcludes("integrations page does not ask users to build Stint CLI", source, "make stint");
6572
assertExcludes("integrations page does not expose bin-prefixed setup commands", source, "bin/stint");

web/app/(console)/integrations/page.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ assert.match(source, /AI agents/);
1616
assert.match(source, /Editors/);
1717
assert.match(source, /listUserAgents/);
1818
assert.match(source, /last_seen_at/);
19-
assert.match(source, /Copy setup/);
19+
assert.match(source, /Install Stint/);
20+
assert.match(source, /Install agent plugin/);
21+
assert.match(source, /Install editor plugin/);
22+
assert.match(source, /connectionStatus/);
23+
assert.match(source, /Not connected yet/);
24+
assert.match(source, /Waiting for first check-in/);
25+
assert.match(source, /Stint is connected/);
2026
assert.match(source, /Verify connection/);
2127
assert.match(source, /setLatestKey/);
2228
assert.match(source, /copyText/);
@@ -33,6 +39,7 @@ assert.doesNotMatch(source, /StatusTile/);
3339
assert.doesNotMatch(source, /Integration catalog/);
3440
assert.doesNotMatch(source, /DetailPanel/);
3541
assert.doesNotMatch(source, /shell-cli-config/);
42+
assert.doesNotMatch(source, /Copy setup, run it once/);
3643
assert.match(recipes, /VS Code/);
3744
assert.match(recipes, /JetBrains/);
3845
assert.match(recipes, /Vim\/Neovim/);

web/app/(console)/integrations/page.tsx

Lines changed: 103 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,19 @@ const toolCategories: {
3737
description: string;
3838
setupTitle: string;
3939
setupBody: string;
40+
primaryAction: string;
4041
primaryRecipeId: string;
4142
recipeIds: readonly string[];
4243
}[] = [
4344
{
4445
id: "terminal",
4546
label: "Terminal",
4647
badge: "Recommended",
47-
description: "Install Stint once for terminal, AI agent, and editor activity.",
48-
setupTitle: "Terminal setup",
48+
description: "Set up terminal, AI agent, and editor activity in one place.",
49+
setupTitle: "Install Stint",
4950
setupBody:
50-
"Copy one command. It creates your key, installs Stint, writes config, and checks the connection.",
51+
"One command creates your key, installs Stint, writes config, and checks the connection.",
52+
primaryAction: "Install Stint",
5153
primaryRecipeId: "stint-cli-config",
5254
recipeIds: ["stint-cli-config"],
5355
},
@@ -56,9 +58,10 @@ const toolCategories: {
5658
label: "AI agents",
5759
badge: "Codex and Claude",
5860
description: "Track coding sessions from Codex or Claude Code.",
59-
setupTitle: "AI agent setup",
61+
setupTitle: "Install agent plugin",
6062
setupBody:
61-
"Choose your agent below. Stint shows the marketplace plugin first, with CLI setup as the fallback.",
63+
"Choose Codex or Claude Code below, then install the Stint marketplace plugin.",
64+
primaryAction: "Install agent plugin",
6265
primaryRecipeId: "codex-config",
6366
recipeIds: ["codex-config", "claude-code-config", "stint-cli-config"],
6467
},
@@ -67,9 +70,10 @@ const toolCategories: {
6770
label: "Editors",
6871
badge: "VS Code, JetBrains, Vim",
6972
description: "Use familiar editor plugins with your Stint endpoint and key.",
70-
setupTitle: "Editor setup",
73+
setupTitle: "Install editor plugin",
7174
setupBody:
72-
"Choose your editor below. Existing WakaTime-compatible plugins can send activity to Stint.",
75+
"Choose your editor below, then point the plugin at your Stint endpoint and key.",
76+
primaryAction: "Install editor plugin",
7377
primaryRecipeId: "vscode-config",
7478
recipeIds: ["vscode-config", "jetbrains-config", "vim-config"],
7579
},
@@ -107,6 +111,10 @@ function IntegrationsContent() {
107111
const agentRows = userAgents.data?.data ?? [];
108112
const recentStintAgent = agentRows.find((agent) => isStintAgent(agent));
109113
const stintCLIConnected = Boolean(recentStintAgent);
114+
const latestGeneratedKeyUsed = Boolean(
115+
latestKeyId &&
116+
keys.data?.data.some((key) => key.id === latestKeyId && key.last_used_at),
117+
);
110118
const configs = useMemo(
111119
() => integrationConfigs(apiURL, displayKey),
112120
[apiURL, displayKey],
@@ -118,6 +126,24 @@ function IntegrationsContent() {
118126
const activeCategory =
119127
toolCategories.find((category) => category.id === activeToolCategory) ??
120128
toolCategories[0];
129+
const connectionStatus =
130+
stintCLIConnected ||
131+
latestGeneratedKeyUsed ||
132+
validateMessage === "Stint is connected"
133+
? "Stint is connected"
134+
: setupMessage || validateMessage === "Checking connection"
135+
? "Waiting for first check-in"
136+
: validateMessage === "No check-in yet"
137+
? "No check-in yet"
138+
: "Not connected yet";
139+
const connectionDetail =
140+
connectionStatus === "Stint is connected"
141+
? `Last Stint check-in${recentStintAgent?.last_seen_at ? `: ${formatLastSeen(recentStintAgent.last_seen_at)}` : " found."}`
142+
: connectionStatus === "Waiting for first check-in"
143+
? "Run the copied setup command, then verify the connection."
144+
: connectionStatus === "No check-in yet"
145+
? "No Stint check-in has arrived yet. Run setup, then try again."
146+
: "Install Stint, then use Verify connection to confirm it is sending activity.";
121147
const visibleClients = clients.filter((client) =>
122148
activeCategory.recipeIds.includes(client.recipeId),
123149
);
@@ -185,10 +211,21 @@ function IntegrationsContent() {
185211
"generated-setup",
186212
stintConfiguredInstallCommand(apiURL, apiKey),
187213
);
188-
setSetupMessage("Setup command copied with your Stint key.");
214+
setSetupMessage("Waiting for first check-in");
215+
};
216+
const openPrimarySetup = () => {
217+
setSelectedIntegration(activeCategory.primaryRecipeId);
218+
window.history.replaceState(null, "", `#${activeCategory.primaryRecipeId}`);
219+
};
220+
const runPrimaryAction = () => {
221+
if (activeToolCategory === "terminal") {
222+
void copyGeneratedSetup();
223+
return;
224+
}
225+
openPrimarySetup();
189226
};
190227
const validateConnection = async () => {
191-
setValidateMessage("Checking for a Stint CLI check-in...");
228+
setValidateMessage("Checking connection");
192229
const [agentsResult, keysResult] = await Promise.all([
193230
userAgents.refetch(),
194231
keys.refetch(),
@@ -203,9 +240,7 @@ function IntegrationsContent() {
203240
(agentsResult.data?.data ?? []).some((agent) => isStintAgent(agent)) ||
204241
generatedKeyUsed;
205242
setValidateMessage(
206-
connected
207-
? "Yes, Stint CLI is connected."
208-
: "No Stint CLI check-in yet. Run the copied command, then verify again.",
243+
connected ? "Stint is connected" : "No check-in yet",
209244
);
210245
};
211246

@@ -271,37 +306,44 @@ function IntegrationsContent() {
271306
<p className="mb-3 max-w-2xl text-sm leading-6 text-zinc-300">
272307
{activeCategory.setupBody}
273308
</p>
274-
<p className="mt-3 text-sm text-zinc-400">
275-
{validateMessage ||
276-
setupMessage ||
277-
(stintCLIConnected
278-
? `Yes, Stint CLI is connected${recentStintAgent?.last_seen_at ? ` · ${formatLastSeen(recentStintAgent.last_seen_at)}` : ""}.`
279-
: activeToolCategory === "terminal"
280-
? "Copy setup, run it once, then verify the connection."
281-
: "Pick a setup option below. Use Verify connection after you run Stint.")}
282-
</p>
309+
<div className="mt-4 rounded border border-line bg-ink p-3">
310+
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-zinc-500">
311+
Connection status
312+
</div>
313+
<div className="mt-1 text-sm font-semibold text-zinc-100">
314+
{connectionStatus}
315+
</div>
316+
<p className="mt-1 text-sm leading-5 text-zinc-500">
317+
{connectionDetail}
318+
</p>
319+
</div>
283320
<div className="mt-4 flex flex-col gap-2 sm:flex-row">
284-
{activeToolCategory === "terminal" ? (
285-
<button
286-
className="inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-md bg-accent px-3 text-sm font-semibold text-ink hover:bg-sky-300 disabled:cursor-not-allowed disabled:opacity-60"
287-
type="button"
288-
onClick={() => {
289-
void copyGeneratedSetup();
290-
}}
291-
disabled={createIntegrationKey.isPending}
292-
>
293-
{copied === "generated-setup" ? (
321+
<button
322+
className="inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-md bg-accent px-3 text-sm font-semibold text-ink hover:bg-sky-300 disabled:cursor-not-allowed disabled:opacity-60"
323+
type="button"
324+
onClick={runPrimaryAction}
325+
disabled={
326+
activeToolCategory === "terminal" &&
327+
createIntegrationKey.isPending
328+
}
329+
>
330+
{activeToolCategory === "terminal" ? (
331+
copied === "generated-setup" ? (
294332
<Check size={15} />
295333
) : (
296334
<Clipboard size={15} />
297-
)}
298-
{createIntegrationKey.isPending
299-
? "Creating..."
300-
: copied === "generated-setup"
301-
? "Copied"
302-
: "Copy setup"}
303-
</button>
304-
) : null}
335+
)
336+
) : (
337+
<ArrowRight size={15} />
338+
)}
339+
{activeToolCategory === "terminal" &&
340+
createIntegrationKey.isPending
341+
? "Creating..."
342+
: activeToolCategory === "terminal" &&
343+
copied === "generated-setup"
344+
? "Copied"
345+
: activeCategory.primaryAction}
346+
</button>
305347
<button
306348
className="inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-md border border-line px-3 text-sm text-zinc-200 hover:border-accent/50 hover:bg-white/5 disabled:opacity-60"
307349
type="button"
@@ -431,6 +473,10 @@ function SetupDisclosure({
431473
copied: boolean;
432474
onCopy: () => void;
433475
}) {
476+
const [open, setOpen] = useState(false);
477+
const disclosureLabel =
478+
config.id === "stint-cli-config" ? "Show command" : "Show setup details";
479+
434480
return (
435481
<div
436482
id="integration-instructions"
@@ -440,16 +486,23 @@ function SetupDisclosure({
440486
<span id={config.id} className="sr-only">
441487
{config.name}
442488
</span>
443-
<details>
444-
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-sm font-medium text-zinc-100">
445-
Setup details
446-
<CopyButton
447-
id={config.id}
448-
label="Copy"
449-
copied={copied}
450-
onCopy={onCopy}
451-
/>
452-
</summary>
489+
<div className="flex items-center justify-between gap-3">
490+
<button
491+
className="text-left text-sm font-medium text-zinc-100 hover:text-accent focus:outline-none focus:ring-2 focus:ring-accent/60"
492+
type="button"
493+
aria-expanded={open}
494+
onClick={() => setOpen((current) => !current)}
495+
>
496+
{open ? "Hide setup details" : disclosureLabel}
497+
</button>
498+
<CopyButton
499+
id={config.id}
500+
label="Copy"
501+
copied={copied}
502+
onCopy={onCopy}
503+
/>
504+
</div>
505+
{open ? (
453506
<div className="mt-4 space-y-3">
454507
{config.options.map((option) => (
455508
<SetupOptionCard key={option.title} option={option} />
@@ -464,7 +517,7 @@ function SetupDisclosure({
464517
</div>
465518
) : null}
466519
</div>
467-
</details>
520+
) : null}
468521
</div>
469522
);
470523
}

web/e2e/integrations.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,34 @@ test("integration names reveal full setup instructions", async ({ page }) => {
3434
await expect(page.getByRole("heading", { name: "Connect Stint" })).toBeVisible();
3535
await expect(page.getByRole("heading", { name: "Choose where you code" })).toBeVisible();
3636
await expect(page.getByRole("button", { name: /Terminal/ })).toHaveAttribute("aria-pressed", "true");
37+
await expect(page.getByRole("button", { name: "Install Stint" })).toBeVisible();
38+
await expect(page.getByText("Not connected yet")).toBeVisible();
3739

3840
const stintCard = page.getByRole("button", { name: "Show Stint CLI integration instructions" });
3941
await expect(stintCard).toContainText("Open");
42+
await expect(page.locator("#integration-instructions")).not.toContainText("curl -fsSL");
43+
await page.getByText("Show command").click();
44+
await expect(page.locator("#integration-instructions")).toContainText("curl -fsSL");
4045

4146
for (const recipe of recipes) {
4247
if (recipe.name === "Codex" || recipe.name === "Claude Code") {
4348
await page.getByRole("button", { name: /AI agents/ }).click();
4449
await expect(page.getByRole("button", { name: /AI agents/ })).toHaveAttribute("aria-pressed", "true");
50+
await expect(page.getByRole("button", { name: "Install agent plugin" })).toBeVisible();
4551
} else if (recipe.name === "VS Code" || recipe.name === "JetBrains" || recipe.name === "Vim/Neovim") {
4652
await page.getByRole("button", { name: /Editors/ }).click();
4753
await expect(page.getByRole("button", { name: /Editors/ })).toHaveAttribute("aria-pressed", "true");
54+
await expect(page.getByRole("button", { name: "Install editor plugin" })).toBeVisible();
4855
} else {
4956
await page.getByRole("button", { name: /Terminal/ }).click();
5057
await expect(page.getByRole("button", { name: /Terminal/ })).toHaveAttribute("aria-pressed", "true");
58+
await expect(page.getByRole("button", { name: "Install Stint" })).toBeVisible();
5159
}
5260
await page.getByRole("button", { name: `Show ${recipe.name} integration instructions` }).click();
61+
const setupToggle = page.locator("#integration-instructions button[aria-expanded]").first();
62+
if ((await setupToggle.getAttribute("aria-expanded")) !== "true") {
63+
await setupToggle.click();
64+
}
5365
await expect(page.locator("#integration-instructions")).toContainText(recipe.expected);
5466
if (recipe.name === "Codex" || recipe.name === "Claude Code") {
5567
await expect(page.locator("#integration-instructions")).toContainText("Choose Stint marketplace plugin");

0 commit comments

Comments
 (0)