Files
yaak-mountain-loop/apps/yaak-proxy/components/FirstRun.tsx
T
Gregory SchierandClaude Fable 5.2 35058cc3be Yaak Proxy: prose pass over the UI copy
Apply the approved wording table: labels stay short, explanations are one
full sentence about the user's traffic, and detail moves behind an info
icon on the first-run card, the body banners, the Settings Helper row and
the rejected card. One word per thing: traffic and flows, recorded,
truncated, domains, searches, the CLI, Retry.

Backend messages that reach the window change with them, with the tests
and harness fixtures that assert on the old text. The two working files
for the pass are removed from the branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:41:49 -07:00

213 lines
7.7 KiB
TypeScript

import { Button, Icon, IconTooltip, InlineCode, LoadingIcon } from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { ReactNode } from "react";
import { useRpcMutation } from "../hooks/useRpcMutation";
import { encryptingPercent } from "../lib/encrypting";
import { proxyStateAtom } from "../lib/store";
import { useCopyEnvironment, useProxySetup, useSystemProxySwitch } from "./ProxyStatus";
type StepState = "todo" | "next" | "wait" | "done" | "fail";
/**
* The list's empty state until traffic arrives: turn on the system proxy, then see traffic. The
* certificate is trusted at the first Decrypt, not here, as nothing needs it before. Not
* dismissable and nothing remembered; the first flow replaces it. Copy environment beside it
* serves someone who wants one terminal proxied and nothing system-wide.
*/
export function FirstRun() {
const proxyState = useAtomValue(proxyStateAtom);
const setup = useProxySetup();
const { systemProxy, setSystemProxy } = setup;
const systemSwitch = useSystemProxySwitch(setup);
const execute = useRpcMutation("execute_action");
const retry = useRpcMutation("take_over");
const copyEnvironment = useCopyEnvironment();
const running = proxyState?.state === "running";
// Changed only by the process that owns the engine, so not from here while the CLI does.
const otherOwner = proxyState?.otherOwner != null;
const recording = proxyState?.recording !== false;
const systemSupported = systemProxy.data?.supported !== false;
const systemOn = systemProxy.data?.enabled === true;
const systemState: StepState = setSystemProxy.isPending
? "wait"
: systemOn
? "done"
: systemSwitch.failed
? "fail"
: "next";
const ready = systemOn || !systemSupported;
const encrypting = !running && !otherOwner ? encryptingPercent(proxyState) : null;
const trafficState: StepState =
encrypting != null ? "wait" : !running && !otherOwner ? "fail" : ready ? "wait" : "todo";
return (
<div className="grid place-items-center h-full px-10 pb-16 pt-6">
<div className="w-full max-w-[40rem]">
<div className="text-xl font-semibold mb-1.5">Set up Yaak Proxy</div>
<div className="text-text-subtle text-sm leading-relaxed mb-4">
{systemSupported
? "Traffic from every app on this Mac will be recorded."
: "Traffic appears here as apps send it."}
</div>
<div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle">
{systemSupported && (
<Step
n={1}
state={systemState}
title={systemOn ? "System proxy overridden" : "Override system proxy"}
description={
otherOwner && !systemOn
? proxyState?.otherOwner?.owner === "cli"
? "Take over from the CLI to change this."
: "Quit the other Yaak Proxy to use this one."
: systemSwitch.description
}
action={
(systemState === "next" || systemState === "fail") && running ? (
<Button
size="2xs"
variant={systemState === "fail" ? "border" : "solid"}
color={systemState === "fail" ? "default" : "primary"}
onClick={() => setSystemProxy.mutate({ enabled: true })}
>
{systemState === "fail" ? "Retry" : "Enable"}
</Button>
) : null
}
/>
)}
<Step
n={systemSupported ? 2 : 1}
state={trafficState}
title={
encrypting != null
? `Encrypting recorded traffic, ${encrypting}`
: trafficState === "wait"
? "Waiting for traffic"
: "See traffic"
}
description={
encrypting != null
? "Recording will start once encryption finishes."
: trafficState === "fail"
? (retry.error?.message ?? proxyState?.startError ?? "The proxy failed to start.")
: !recording
? "Recording is paused."
: trafficState === "wait"
? "Open a website or send a request to begin."
: "Flows appear here as apps send requests."
}
action={
trafficState === "fail" ? (
<Button
size="2xs"
variant="border"
isLoading={retry.isPending}
onClick={() => retry.mutate({})}
>
Retry
</Button>
) : !recording && running ? (
<Button
size="2xs"
color="primary"
onClick={() => execute.mutate({ scope: "global", action: "recording_toggle" })}
>
Resume Recording
</Button>
) : null
}
/>
</div>
<div className="flex items-center gap-3 mt-4 text-sm text-text-subtle">
<span>To proxy a single terminal instead, copy the environment.</span>
<Button
size="2xs"
variant="border"
className="ml-auto shrink-0"
leftSlot={<Icon icon="square_terminal" size="sm" />}
onClick={() => void copyEnvironment()}
>
Copy Environment
</Button>
</div>
<div className="mt-2 text-sm text-text-subtle">
Agents can connect through <InlineCode>yaak mcp</InlineCode>.
</div>
<div className="flex items-center gap-1.5 mt-2 text-sm text-text-subtle">
Recorded traffic is encrypted on disk.
<IconTooltip
iconSize="sm"
content="The key is kept in the system keychain, so the system may ask to let Yaak Proxy use it."
/>
</div>
</div>
</div>
);
}
function Step({
n,
state,
title,
description,
action,
}: {
n: number;
state: StepState;
title: string;
description: ReactNode;
action: ReactNode;
}) {
return (
<div className="grid grid-cols-[1.4rem_minmax(0,1fr)_auto] gap-x-3 items-center px-4 py-3">
<StepMark n={n} state={state} />
<div className="min-w-0">
<div className={classNames("text-sm", state === "todo" ? "text-text-subtle" : "text-text")}>
{title}
</div>
<div
className={classNames(
"text-xs mt-0.5",
state === "fail" ? "text-danger" : "text-text-subtle",
)}
>
{description}
</div>
</div>
<div>{action}</div>
</div>
);
}
function StepMark({ n, state }: { n: number; state: StepState }) {
const base = "w-[1.4rem] h-[1.4rem] rounded-full grid place-items-center text-xs";
switch (state) {
case "done":
return (
<span className={classNames(base, "bg-success/15 text-success")}>
<Icon icon="check" size="sm" />
</span>
);
case "fail":
return (
<span className={classNames(base, "bg-danger/15 text-danger")}>
<Icon icon="x" size="sm" />
</span>
);
case "wait":
return (
<span className={classNames(base, "text-text-subtle")}>
<LoadingIcon size="sm" />
</span>
);
case "next":
return <span className={classNames(base, "bg-primary text-white font-semibold")}>{n}</span>;
case "todo":
return <span className={classNames(base, "border border-border text-text-subtle")}>{n}</span>;
}
}