Files
yaak-mountain-loop/apps/yaak-proxy/components/FirstRun.tsx
T
Gregory SchierandClaude Opus 5.5 c78732ecaf Polish Yaak Proxy's flow list, sidebar and header
Columns keep fixed widths, Path included, and the list scrolls sideways
with the header in step instead of dropping columns or capping Host.
Resize handles sit on every column boundary, a double-click resets one,
and a third click on a sorted header goes back to start time, with empty
values sorting lowest. The search box takes up to 44rem and wraps over
the list while focused, its completions as wide as it. The list/detail
handle fills the gutter. Native title tooltips become Yaak's tooltip,
shared buttons gain an opt-in for it, and folder and program rows in the
sidebar get Reveal in Finder and Copy path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:45:46 -07:00

302 lines
11 KiB
TypeScript

import {
Button,
CONTROL_TOOLTIP_DELAY,
Icon,
InlineCode,
LoadingIcon,
Tooltip,
} from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { ReactNode } from "react";
import { useDecryptList, useEditDecryptList } from "../hooks/useDecryptList";
import { useRpcMutation } from "../hooks/useRpcMutation";
import { everythingEdit } from "../lib/decrypt";
import { decryptStep } from "../lib/firstRun";
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: trust the certificate, turn on the system proxy,
* choose what to decrypt, then see traffic. 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 { caStatus, trust, systemProxy, setSystemProxy } = setup;
const systemSwitch = useSystemProxySwitch(setup);
const execute = useRpcMutation("execute_action");
const retry = useRpcMutation("take_over");
const copyEnvironment = useCopyEnvironment();
const decryptList = useDecryptList();
const saveDecryptList = useEditDecryptList();
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 trusted = caStatus.data?.trusted === true;
const systemSupported = systemProxy.data?.supported !== false;
const systemOn = systemProxy.data?.enabled === true;
const trustState: StepState = trusted
? "done"
: trust.isPending
? "wait"
: trust.error != null
? "fail"
: "next";
const systemState: StepState = setSystemProxy.isPending
? "wait"
: systemOn
? "done"
: systemSwitch.failed
? "fail"
: trusted
? "next"
: "todo";
const decrypt = decryptStep(decryptList, trusted);
const everything = decrypt.choice === "everything";
const setEverything = (on: boolean) => saveDecryptList.mutate(everythingEdit(on));
const ready = trusted && (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 ? "Four" : "Three"} steps, once. Then every app on this Mac sends its
traffic here.
</div>
<div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle">
<Step
n={1}
state={trustState}
title={trusted ? "Certificate trusted" : "Trust the certificate"}
description={
trusted
? "Yaak Proxy CA, in your login keychain."
: trustState === "wait"
? "Waiting for your password in the macOS prompt."
: trustState === "fail"
? (trust.error?.message ?? "The certificate was not trusted.")
: "So HTTPS reads as plain text. macOS asks for your login password once."
}
action={
trustState === "next" || trustState === "fail" ? (
<Button
size="2xs"
variant={trustState === "fail" ? "border" : "solid"}
color={trustState === "fail" ? "default" : "primary"}
onClick={() => trust.mutate({})}
>
{trustState === "fail" ? "Try Again" : "Trust Certificate"}
</Button>
) : null
}
/>
{systemSupported && (
<Step
n={2}
state={systemState}
title={systemOn ? "System proxy on" : "Turn on the system proxy"}
description={
otherOwner && !systemOn
? proxyState?.otherOwner?.owner === "cli"
? "The CLI owns the proxy. Take over to turn this on from here."
: "Another Yaak Proxy window owns the engine. Quit it 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" ? "Try Again" : "Turn On"}
</Button>
) : null
}
/>
)}
<Step
n={systemSupported ? 3 : 2}
state={decrypt.state}
title="What to decrypt"
description={
everything ? (
<Tooltip
wrap
content="Except the built-in exclusions"
delay={CONTROL_TOOLTIP_DELAY}
>
<span>Everything</span>
</Tooltip>
) : (
<Tooltip wrap content="From a flow or the sidebar" delay={CONTROL_TOOLTIP_DELAY}>
<span>Pick as they appear</span>
</Tooltip>
)
}
action={
everything ? (
<Button
size="2xs"
variant="border"
isLoading={saveDecryptList.isPending}
onClick={() => setEverything(false)}
>
Pick As They Appear
</Button>
) : (
<Button
size="2xs"
variant="border"
color={decrypt.state === "next" ? "primary" : "default"}
isLoading={saveDecryptList.isPending}
onClick={() => setEverything(true)}
>
Everything
</Button>
)
}
/>
<Step
n={systemSupported ? 4 : 3}
state={trafficState}
title={
encrypting != null
? `Encrypting stored traffic, ${encrypting}`
: trafficState === "wait"
? "Waiting for traffic"
: "See traffic"
}
description={
encrypting != null
? "Traffic captured before this version is being encrypted on disk. Recording starts when it's done."
: trafficState === "fail"
? (retry.error?.message ?? proxyState?.startError ?? "The proxy has not started.")
: !recording
? "Recording is paused, so traffic passes through without being stored."
: trafficState === "wait"
? "Open a website or send a request, and its flow appears here."
: "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>Only need one terminal or one app? Copy the environment instead.</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">
Working with an agent? <InlineCode>yaak mcp</InlineCode> gives it the proxy's flows and
actions.
</div>
<div className="mt-2 text-sm text-text-subtle">
Captured traffic is encrypted on disk with a key kept in your keychain, so macOS 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>;
}
}