Split codebase (#455)

This commit is contained in:
Gregory Schier
2026-05-07 15:50:10 -07:00
committed by GitHub
parent d2dc719cc6
commit 10559c8f4f
742 changed files with 7686 additions and 3249 deletions

View File

@@ -0,0 +1,31 @@
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
import { Button, type ButtonProps } from "@yaakapp-internal/ui";
import { useCallback } from "react";
import { useRpcMutation } from "../hooks/useRpcMutation";
import { useActionMetadata } from "../hooks/useActionMetadata";
type ActionButtonProps = Omit<ButtonProps, "onClick" | "children"> & {
action: ActionInvocation;
/** Override the label from metadata */
children?: React.ReactNode;
};
export function ActionButton({ action, children, ...props }: ActionButtonProps) {
const meta = useActionMetadata(action);
const { mutate, isPending } = useRpcMutation("execute_action");
const onClick = useCallback(() => {
mutate(action);
}, [action, mutate]);
return (
<Button
{...props}
disabled={props.disabled || isPending}
isLoading={isPending}
onClick={onClick}
>
{children ?? meta?.label ?? "…"}
</Button>
);
}

View File

@@ -0,0 +1,29 @@
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
import { IconButton, type IconButtonProps } from "@yaakapp-internal/ui";
import { useCallback } from "react";
import { useRpcMutation } from "../hooks/useRpcMutation";
import { useActionMetadata } from "../hooks/useActionMetadata";
type ActionIconButtonProps = Omit<IconButtonProps, "onClick" | "title"> & {
action: ActionInvocation;
title?: string;
};
export function ActionIconButton({ action, ...props }: ActionIconButtonProps) {
const meta = useActionMetadata(action);
const { mutate, isPending } = useRpcMutation("execute_action");
const onClick = useCallback(() => {
mutate(action);
}, [action, mutate]);
return (
<IconButton
{...props}
title={props.title ?? meta?.label ?? "…"}
disabled={props.disabled || isPending}
isLoading={isPending}
onClick={onClick}
/>
);
}

View File

@@ -0,0 +1,77 @@
import type { HttpExchange, ProxyHeader } from "@yaakapp-internal/proxy-lib";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
TruncatedWideTableCell,
} from "@yaakapp-internal/ui";
import classNames from "classnames";
interface Props {
exchanges: HttpExchange[];
style?: React.CSSProperties;
className?: string;
}
export function ExchangesTable({ exchanges, style, className }: Props) {
if (exchanges.length === 0) {
return <p className="text-text-subtlest text-sm">No traffic yet</p>;
}
return (
<div className={className} style={style}>
<Table scrollable className="px-2">
<TableHead>
<TableRow>
<TableHeaderCell>Method</TableHeaderCell>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{exchanges.map((ex) => (
<TableRow key={ex.id}>
<TableCell className="font-mono text-2xs">{ex.method}</TableCell>
<TruncatedWideTableCell className="font-mono text-2xs">
{ex.url}
</TruncatedWideTableCell>
<TableCell>
<StatusBadge status={ex.resStatus} error={ex.error} />
</TableCell>
<TableCell className="text-text-subtle text-xs">
{getContentType(ex.resHeaders)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
function StatusBadge({ status, error }: { status: number | null; error: string | null }) {
if (error) return <span className="text-xs text-danger">Error</span>;
if (status == null) return <span className="text-xs text-text-subtlest"></span>;
const color =
status >= 500
? "text-danger"
: status >= 400
? "text-warning"
: status >= 300
? "text-notice"
: "text-success";
return <span className={classNames("text-xs font-mono", color)}>{status}</span>;
}
function getContentType(headers: ProxyHeader[]): string {
const ct = headers.find((h) => h.name.toLowerCase() === "content-type")?.value;
if (ct == null) return "—";
// Strip parameters (e.g. "; charset=utf-8")
return ct.split(";")[0]?.trim() ?? ct;
}

View File

@@ -0,0 +1,146 @@
import { HeaderSize, IconButton, SidebarLayout, SplitLayout } from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { useLocalStorage } from "react-use";
import { useRpcQueryWithEvent } from "../hooks/useRpcQueryWithEvent";
import { getOsType } from "../lib/tauri";
import { ActionIconButton } from "./ActionIconButton";
import { ExchangesTable } from "./ExchangesTable";
import { filteredExchangesAtom, Sidebar } from "./Sidebar";
export function ProxyLayout() {
const os = getOsType();
const exchanges = useAtomValue(filteredExchangesAtom);
const [sidebarWidth, setSidebarWidth] = useLocalStorage("sidebar_width", 250);
const [sidebarHidden, setSidebarHidden] = useLocalStorage("sidebar_hidden", false);
const [floatingSidebarHidden, setFloatingSidebarHidden] = useLocalStorage(
"floating_sidebar_hidden",
true,
);
const [floating, setFloating] = useState(false);
const { data: proxyState } = useRpcQueryWithEvent("get_proxy_state", {}, "proxy_state_changed");
const isRunning = proxyState?.state === "running";
const isHidden = floating ? (floatingSidebarHidden ?? true) : (sidebarHidden ?? false);
return (
<div
className={classNames(
"h-full w-full grid grid-rows-[auto_1fr]",
os === "linux" && "border border-border-subtle",
)}
>
<HeaderSize
data-tauri-drag-region
size="lg"
osType={os}
hideWindowControls={false}
useNativeTitlebar={false}
interfaceScale={1}
className="x-theme-appHeader bg-surface"
>
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full pointer-events-none">
<div className="flex items-center pl-1 pointer-events-auto">
<IconButton
size="sm"
title="Toggle sidebar"
icon={isHidden ? "left_panel_hidden" : "left_panel_visible"}
iconColor="secondary"
onClick={() => {
if (floating) {
setFloatingSidebarHidden(!floatingSidebarHidden);
} else {
setSidebarHidden(!sidebarHidden);
}
}}
/>
</div>
<div
data-tauri-drag-region
className="pointer-events-none flex items-center text-sm px-2"
>
Yaak Proxy
</div>
<div className="flex items-center gap-1 pr-1 pointer-events-auto">
{isRunning ? (
<>
<span className="text-2xs text-success">Running :9090</span>
<ActionIconButton
action={{ scope: "global", action: "proxy_stop" }}
icon="circle_stop"
iconColor="secondary"
size="sm"
/>
</>
) : (
<ActionIconButton
action={{ scope: "global", action: "proxy_start" }}
icon="circle_play"
iconColor="secondary"
size="sm"
/>
)}
</div>
</div>
</HeaderSize>
<SidebarLayout
width={sidebarWidth ?? 250}
onWidthChange={setSidebarWidth}
hidden={sidebarHidden ?? false}
onHiddenChange={setSidebarHidden}
floatingHidden={floatingSidebarHidden ?? true}
onFloatingHiddenChange={setFloatingSidebarHidden}
onFloatingChange={setFloating}
sidebar={
floating ? (
<div
className={classNames(
"x-theme-sidebar",
"h-full bg-surface border-r border-border-subtle",
"grid grid-rows-[auto_1fr]",
)}
>
<HeaderSize
hideControls
size="lg"
className="border-transparent pl-1"
osType={os}
hideWindowControls={false}
useNativeTitlebar={false}
interfaceScale={1}
>
<IconButton
size="sm"
title="Toggle sidebar"
icon="left_panel_visible"
iconColor="secondary"
onClick={() => setFloatingSidebarHidden(true)}
/>
</HeaderSize>
<Sidebar />
</div>
) : (
<Sidebar />
)
}
>
<SplitLayout
storageKey="proxy_detail"
layout="vertical"
defaultRatio={0.4}
firstSlot={({ style }) => (
<ExchangesTable exchanges={exchanges} style={style} className="overflow-auto" />
)}
secondSlot={({ style }) => (
<div
style={style}
className="p-3 text-text-subtlest text-sm border-t border-border-subtle"
>
Select a request to view details
</div>
)}
/>
</SidebarLayout>
</div>
);
}

View File

@@ -0,0 +1,219 @@
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
import type { TreeNode } from "@yaakapp-internal/ui";
import { selectedIdsFamily, Tree } from "@yaakapp-internal/ui";
import { atom, useAtomValue } from "jotai";
import { atomFamily } from "jotai/utils";
import { useCallback } from "react";
import { httpExchangesAtom } from "../lib/store";
/** A node in the sidebar tree — either a domain or a path segment. */
export type SidebarItem = {
id: string;
label: string;
exchangeIds: string[];
};
const collapsedAtom = atomFamily((_treeId: string) => atom<Record<string, boolean>>({}));
export const SIDEBAR_TREE_ID = "proxy-sidebar";
const sidebarTreeAtom = atom<TreeNode<SidebarItem>>((get) => {
const exchanges = get(httpExchangesAtom);
return buildTree(exchanges);
});
/** Exchanges filtered by the currently selected sidebar node(s). */
export const filteredExchangesAtom = atom((get) => {
const exchanges = get(httpExchangesAtom);
const tree = get(sidebarTreeAtom);
const selectedIds = get(selectedIdsFamily(SIDEBAR_TREE_ID));
// Nothing selected or root selected → show all
if (selectedIds.length === 0 || selectedIds.includes("root")) {
return exchanges;
}
// Collect exchange IDs from all selected nodes
const allowedIds = new Set<string>();
const nodeMap = new Map<string, SidebarItem>();
collectNodes(tree, nodeMap);
for (const selectedId of selectedIds) {
const node = nodeMap.get(selectedId);
if (node) {
for (const id of node.exchangeIds) {
allowedIds.add(id);
}
}
}
return exchanges.filter((ex) => allowedIds.has(ex.id));
});
function collectNodes(node: TreeNode<SidebarItem>, map: Map<string, SidebarItem>) {
map.set(node.item.id, node.item);
for (const child of node.children ?? []) {
collectNodes(child, map);
}
}
/**
* Build a domain → path-segment trie from a flat list of exchanges.
*
* Example: Given URLs
* GET https://api.example.com/v1/users
* GET https://api.example.com/v1/users/123
* POST https://api.example.com/v1/orders
*
* Produces:
* api.example.com
* /v1
* /users
* /123
* /orders
*/
function buildTree(exchanges: HttpExchange[]): TreeNode<SidebarItem> {
const root: SidebarItem = { id: "root", label: "All Traffic", exchangeIds: [] };
const rootNode: TreeNode<SidebarItem> = {
item: root,
parent: null,
depth: 0,
children: [],
draggable: false,
};
// Intermediate trie structure for building
type TrieNode = {
id: string;
label: string;
exchangeIds: string[];
children: Map<string, TrieNode>;
};
const domainMap = new Map<string, TrieNode>();
for (const ex of exchanges) {
let hostname: string;
let segments: string[];
try {
const url = new URL(ex.url);
hostname = url.host;
segments = url.pathname.split("/").filter(Boolean);
} catch {
hostname = ex.url;
segments = [];
}
// Get or create domain node
let domainNode = domainMap.get(hostname);
if (!domainNode) {
domainNode = {
id: `domain:${hostname}`,
label: hostname,
exchangeIds: [],
children: new Map(),
};
domainMap.set(hostname, domainNode);
}
domainNode.exchangeIds.push(ex.id);
// Walk path segments
let current = domainNode;
const pathSoFar: string[] = [];
for (const seg of segments) {
pathSoFar.push(seg);
let child = current.children.get(seg);
if (!child) {
child = {
id: `path:${hostname}/${pathSoFar.join("/")}`,
label: `/${seg}`,
exchangeIds: [],
children: new Map(),
};
current.children.set(seg, child);
}
child.exchangeIds.push(ex.id);
current = child;
}
}
// Convert trie to TreeNode structure
function toTreeNode(
trie: TrieNode,
parent: TreeNode<SidebarItem>,
depth: number,
): TreeNode<SidebarItem> {
const node: TreeNode<SidebarItem> = {
item: {
id: trie.id,
label: trie.label,
exchangeIds: trie.exchangeIds,
},
parent,
depth,
children: [],
draggable: false,
};
const sortedChildren = [...trie.children.values()].sort((a, b) =>
a.label.localeCompare(b.label),
);
for (const child of sortedChildren) {
node.children?.push(toTreeNode(child, node, depth + 1));
}
return node;
}
// Add a "Domains" folder between root and domain nodes
const allExchangeIds = exchanges.map((ex) => ex.id);
const domainsFolder: TreeNode<SidebarItem> = {
item: { id: "domains", label: "Domains", exchangeIds: allExchangeIds },
parent: rootNode,
depth: 1,
children: [],
draggable: false,
};
const sortedDomains = [...domainMap.values()].sort((a, b) => a.label.localeCompare(b.label));
for (const domain of sortedDomains) {
domainsFolder.children?.push(toTreeNode(domain, domainsFolder, 2));
}
rootNode.children?.push(domainsFolder);
return rootNode;
}
function ItemInner({ item }: { item: SidebarItem }) {
const count = item.exchangeIds.length;
return (
<div className="flex items-center gap-2 w-full min-w-0">
<span className="truncate">{item.label}</span>
{count > 0 && <span className="ml-auto text-text-subtlest text-2xs shrink-0">{count}</span>}
</div>
);
}
export function Sidebar() {
const tree = useAtomValue(sidebarTreeAtom);
const treeId = SIDEBAR_TREE_ID;
const getItemKey = useCallback(
(item: SidebarItem) => `${item.id}:${item.exchangeIds.length}`,
[],
);
return (
<aside className="x-theme-sidebar bg-surface h-full w-full min-w-0 overflow-y-auto border-r border-border-subtle">
<div className="pt-2 text-xs">
<Tree
treeId={treeId}
collapsedAtom={collapsedAtom(treeId)}
className="px-2 pb-10"
root={tree}
getItemKey={getItemKey}
ItemInner={ItemInner}
/>
</div>
</aside>
);
}

View File

@@ -0,0 +1,2 @@
// Hardcode font size for now. In the future, this could be configurable.
document.documentElement.style.fontSize = "15px";

View File

@@ -0,0 +1,32 @@
import type { ActionInvocation, ActionMetadata } from "@yaakapp-internal/proxy-lib";
import { useEffect, useState } from "react";
import { rpc } from "../lib/rpc";
/** Look up metadata for a specific action invocation. */
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
export function useActionMetadata(action: ActionInvocation): ActionMetadata | null {
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
const [meta, setMeta] = useState<ActionMetadata | null>(null);
useEffect(() => {
void getActions().then((actions) => {
const match = actions.find(
([inv]) => inv.scope === action.scope && inv.action === action.action,
);
setMeta(match?.[1] ?? null);
});
}, [action]);
return meta;
}
let cachedActions: [ActionInvocation, ActionMetadata][] | null = null;
/** Fetch and cache all action metadata. */
async function getActions(): Promise<[ActionInvocation, ActionMetadata][]> {
if (!cachedActions) {
const { actions } = await rpc("list_actions", {});
cachedActions = actions;
}
return cachedActions;
}

View File

@@ -0,0 +1,15 @@
import type { RpcEventSchema } from "@yaakapp-internal/proxy-lib";
import { useEffect } from "react";
import { listen } from "../lib/rpc";
/**
* Subscribe to an RPC event. Cleans up automatically on unmount.
*/
export function useRpcEvent<K extends keyof RpcEventSchema>(
event: K & string,
callback: (payload: RpcEventSchema[K]) => void,
) {
useEffect(() => {
return listen(event, callback);
}, [event, callback]);
}

View File

@@ -0,0 +1,18 @@
import { type UseMutationOptions, useMutation } from "@tanstack/react-query";
import type { RpcSchema } from "@yaakapp-internal/proxy-lib";
import { minPromiseMillis } from "@yaakapp-internal/ui";
import type { Req, Res } from "../lib/rpc";
import { rpc } from "../lib/rpc";
/**
* React Query mutation wrapper for RPC commands.
*/
export function useRpcMutation<K extends keyof RpcSchema>(
cmd: K,
opts?: Omit<UseMutationOptions<Res<K>, Error, Req<K>>, "mutationFn">,
) {
return useMutation<Res<K>, Error, Req<K>>({
mutationFn: (payload) => minPromiseMillis(rpc(cmd, payload)),
...opts,
});
}

View File

@@ -0,0 +1,20 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { RpcSchema } from "@yaakapp-internal/proxy-lib";
import type { Req, Res } from "../lib/rpc";
import { rpc } from "../lib/rpc";
/**
* React Query wrapper for RPC commands.
* Automatically caches by [cmd, payload] and supports all useQuery options.
*/
export function useRpcQuery<K extends keyof RpcSchema>(
cmd: K,
payload: Req<K>,
opts?: Omit<UseQueryOptions<Res<K>>, "queryKey" | "queryFn">,
) {
return useQuery<Res<K>>({
queryKey: [cmd, payload],
queryFn: () => rpc(cmd, payload),
...opts,
});
}

View File

@@ -0,0 +1,23 @@
import { type UseQueryOptions, useQueryClient } from "@tanstack/react-query";
import type { RpcEventSchema, RpcSchema } from "@yaakapp-internal/proxy-lib";
import type { Req, Res } from "../lib/rpc";
import { useRpcEvent } from "./useRpcEvent";
import { useRpcQuery } from "./useRpcQuery";
/**
* Combines useRpcQuery with an event listener that invalidates the query
* whenever the specified event fires, keeping data fresh automatically.
*/
export function useRpcQueryWithEvent<
K extends keyof RpcSchema,
E extends keyof RpcEventSchema,
>(cmd: K, payload: Req<K>, event: E, opts?: Omit<UseQueryOptions<Res<K>>, "queryKey" | "queryFn">) {
const queryClient = useQueryClient();
const query = useRpcQuery(cmd, payload, opts);
useRpcEvent(event, () => {
void queryClient.invalidateQueries({ queryKey: [cmd, payload] });
});
return query;
}

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak Proxy</title>
<style>
html,
body {
background-color: white;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: #1b1a29;
}
}
</style>
</head>
<body class="text-base">
<div id="root"></div>
<script type="module" src="/font-size.ts"></script>
<script type="module" src="/lib/theme.ts"></script>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
export function fireAndForget(promise: Promise<unknown>) {
promise.catch((err: unknown) => {
console.error("Unhandled async error:", err);
});
}

View File

@@ -0,0 +1,63 @@
import type { ActionInvocation, ActionMetadata } from "@yaakapp-internal/proxy-lib";
import { rpc } from "./rpc";
type ActionBinding = {
invocation: ActionInvocation;
meta: ActionMetadata;
keys: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean };
};
/** Parse a hotkey string like "Ctrl+Shift+P" into its parts. */
function parseHotkey(hotkey: string): ActionBinding["keys"] {
const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
return {
ctrl: parts.includes("ctrl") || parts.includes("control"),
shift: parts.includes("shift"),
alt: parts.includes("alt"),
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command"),
key:
parts.filter(
(p) => !["ctrl", "control", "shift", "alt", "meta", "cmd", "command"].includes(p),
)[0] ?? "",
};
}
function matchesEvent(binding: ActionBinding["keys"], e: KeyboardEvent): boolean {
return (
e.ctrlKey === binding.ctrl &&
e.shiftKey === binding.shift &&
e.altKey === binding.alt &&
e.metaKey === binding.meta &&
e.key.toLowerCase() === binding.key
);
}
/** Fetch all actions from Rust and register a global keydown listener. */
export async function initHotkeys(): Promise<() => void> {
const { actions } = await rpc("list_actions", {});
const bindings: ActionBinding[] = actions
.filter(
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
(entry): entry is [ActionInvocation, ActionMetadata & { defaultHotkey: string }] =>
entry[1].defaultHotkey != null,
)
.map(([invocation, meta]) => ({
invocation,
meta,
keys: parseHotkey(meta.defaultHotkey),
}));
function onKeyDown(e: KeyboardEvent) {
for (const binding of bindings) {
if (matchesEvent(binding.keys, e)) {
e.preventDefault();
void rpc("execute_action", binding.invocation);
return;
}
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}

View File

@@ -0,0 +1,17 @@
import type { RpcEventSchema, RpcSchema } from "@yaakapp-internal/proxy-lib";
import { command, subscribe } from "./tauri";
export type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
export type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
export async function rpc<K extends keyof RpcSchema>(cmd: K, payload: Req<K>): Promise<Res<K>> {
return command<Res<K>>("rpc", { cmd, payload });
}
/** Subscribe to a backend event. Returns an unsubscribe function. */
export function listen<K extends keyof RpcEventSchema>(
event: K & string,
callback: (payload: RpcEventSchema[K]) => void,
): () => void {
return subscribe<RpcEventSchema[K]>(event, callback);
}

View File

@@ -0,0 +1,11 @@
import { createModelStore } from "@yaakapp-internal/model-store";
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
type ProxyModels = {
http_exchange: HttpExchange;
};
export const { dataAtom, applyChange, replaceAll, listAtom, orderedListAtom } =
createModelStore<ProxyModels>(["http_exchange"]);
export const httpExchangesAtom = orderedListAtom("http_exchange", "createdAt", "desc");

View File

@@ -0,0 +1,30 @@
import { invoke } from "@tauri-apps/api/core";
import { listen as tauriListen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { type as tauriOsType } from "@tauri-apps/plugin-os";
/** Call a Tauri command. */
export function command<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
return invoke(cmd, args) as Promise<T>;
}
/** Subscribe to a Tauri event. Returns an unsubscribe function. */
export function subscribe<T>(event: string, callback: (payload: T) => void): () => void {
let unsub: (() => void) | null = null;
tauriListen<T>(event, (e) => callback(e.payload))
.then((fn) => {
unsub = fn;
})
.catch(console.error);
return () => unsub?.();
}
/** Show the current webview window. */
export function showWindow(): Promise<void> {
return getCurrentWebviewWindow().show();
}
/** Get the current OS type (e.g. "macos", "linux", "windows"). */
export function getOsType() {
return tauriOsType();
}

View File

@@ -0,0 +1,35 @@
import { setWindowTheme } from "@yaakapp-internal/mac-window";
import {
applyThemeToDocument,
defaultDarkTheme,
defaultLightTheme,
getCSSAppearance,
platformFromUserAgent,
setPlatformOnDocument,
subscribeToPreferredAppearance,
type Appearance,
} from "@yaakapp-internal/theme";
import { showWindow } from "./tauri";
setPlatformOnDocument(platformFromUserAgent(navigator.userAgent));
// Apply a quick initial theme based on CSS media query
let preferredAppearance: Appearance = getCSSAppearance();
applyTheme(preferredAppearance);
// Then subscribe to accurate OS appearance detection and changes
subscribeToPreferredAppearance((a) => {
preferredAppearance = a;
applyTheme(preferredAppearance);
});
// Show window after initial theme is applied (window starts hidden to prevent flash)
showWindow().catch(console.error);
function applyTheme(appearance: Appearance) {
const theme = appearance === "dark" ? defaultDarkTheme : defaultLightTheme;
applyThemeToDocument(theme);
if (theme.base.surface != null) {
setWindowTheme(theme.base.surface);
}
}

92
apps/yaak-proxy/main.css Normal file
View File

@@ -0,0 +1,92 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html,
body,
#root {
@apply w-full h-full overflow-hidden text-text bg-surface;
}
:root {
--font-family-interface: "";
--font-family-editor: "";
}
:root {
font-variant-ligatures: none;
}
html[data-platform="linux"] {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
::selection {
@apply bg-selection;
}
:not(a),
:not(input):not(textarea),
:not(input):not(textarea)::after,
:not(input):not(textarea)::before {
@apply select-none cursor-default;
}
input,
textarea {
&::placeholder {
@apply text-placeholder;
}
}
a,
a[href] * {
@apply cursor-pointer !important;
}
table th {
@apply text-left;
}
:not(iframe) {
&::-webkit-scrollbar,
&::-webkit-scrollbar-corner {
@apply w-[8px] h-[8px] bg-transparent;
}
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply bg-text-subtlest rounded-[4px] opacity-20;
}
&::-webkit-scrollbar-thumb:hover {
@apply opacity-40 !important;
}
}
.hide-scrollbars {
&::-webkit-scrollbar-corner,
&::-webkit-scrollbar {
@apply hidden !important;
}
}
.rtl {
direction: rtl;
}
:root {
color-scheme: light dark;
--transition-duration: 100ms ease-in-out;
--color-white: 255 100% 100%;
--color-black: 255 0% 0%;
}
}

44
apps/yaak-proxy/main.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createStore, Provider } from "jotai";
import { LazyMotion, MotionConfig } from "motion/react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ProxyLayout } from "./components/ProxyLayout";
import { listen, rpc } from "./lib/rpc";
import { initHotkeys } from "./lib/hotkeys";
import { applyChange, dataAtom, replaceAll } from "./lib/store";
import "./main.css";
const queryClient = new QueryClient();
const jotaiStore = createStore();
// Load initial models from the database
void rpc("list_models", {}).then((res) => {
jotaiStore.set(dataAtom, (prev) => replaceAll(prev, "http_exchange", res.httpExchanges));
});
// Register hotkeys from action metadata
void initHotkeys();
// Subscribe to model change events from the backend
void listen("model_write", (payload) => {
jotaiStore.set(dataAtom, (prev) =>
applyChange(prev, "http_exchange", payload.model, payload.change),
);
});
const motionFeatures = () => import("framer-motion").then((mod) => mod.domAnimation);
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<Provider store={jotaiStore}>
<LazyMotion strict features={motionFeatures}>
<MotionConfig transition={{ duration: 0.1 }}>
<ProxyLayout />
</MotionConfig>
</LazyMotion>
</Provider>
</QueryClientProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,36 @@
{
"name": "@yaakapp/yaak-proxy",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vp dev --force",
"build": "vp build",
"lint": "tsc --noEmit"
},
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-os": "^2.3.2",
"@yaakapp-internal/model-store": "^1.0.0",
"@yaakapp-internal/proxy-lib": "^1.0.0",
"@yaakapp-internal/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0",
"classnames": "^2.5.1",
"jotai": "^2.18.0",
"motion": "^12.4.7",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
"@types/babel__core": "^7.20.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"typescript": "^5.8.3",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
"vite-plus": "^0.1.20"
}
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require("@tailwindcss/nesting")(require("postcss-nesting")),
require("tailwindcss"),
require("autoprefixer"),
],
};

View File

@@ -0,0 +1,7 @@
const sharedConfig = require("@yaakapp-internal/tailwind-config");
/** @type {import('tailwindcss').Config} */
module.exports = {
...sharedConfig,
content: ["./**/*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"],
};

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"useDefineForClassFields": true,
"allowJs": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"],
"@yaakapp-internal/theme/*": ["../../packages/theme/src/*"],
"@yaakapp-internal/ui": ["../../packages/ui/src/index.ts"],
"@yaakapp-internal/ui/*": ["../../packages/ui/src/*"]
}
},
"include": ["."],
"exclude": ["vite.config.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
},
"include": ["vite.config.ts"]
}

1
apps/yaak-proxy/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite-plus/client" />

View File

@@ -0,0 +1,16 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
build: {
outDir: "../../dist/apps/yaak-proxy",
emptyOutDir: true,
},
clearScreen: false,
server: {
port: parseInt(process.env.YAAK_PROXY_DEV_PORT ?? "2420", 10),
strictPort: true,
},
envPrefix: ["VITE_", "TAURI_"],
});