mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 01:49:13 +01:00
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
232 lines
7.0 KiB
TypeScript
232 lines
7.0 KiB
TypeScript
import type { WebsocketEvent, WebsocketRequest } from "@yaakapp-internal/models";
|
|
import { hexy } from "hexy";
|
|
import { useAtomValue } from "jotai";
|
|
import { useMemo, useState } from "react";
|
|
import { useFormatText } from "../hooks/useFormatText";
|
|
import {
|
|
activeWebsocketConnectionAtom,
|
|
activeWebsocketConnectionsAtom,
|
|
setPinnedWebsocketConnectionId,
|
|
useWebsocketEvents,
|
|
} from "../hooks/usePinnedWebsocketConnection";
|
|
import { useStateWithDeps } from "../hooks/useStateWithDeps";
|
|
import { languageFromContentType } from "../lib/contentType";
|
|
import { Button } from "./core/Button";
|
|
import { Editor } from "./core/Editor/LazyEditor";
|
|
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
|
import { EventViewerRow } from "./core/EventViewerRow";
|
|
import { HotkeyList } from "./core/HotkeyList";
|
|
import { Icon } from "./core/Icon";
|
|
import { LoadingIcon } from "./core/LoadingIcon";
|
|
import { HStack, VStack } from "./core/Stacks";
|
|
import { WebsocketStatusTag } from "./core/WebsocketStatusTag";
|
|
import { EmptyStateText } from "./EmptyStateText";
|
|
import { ErrorBoundary } from "./ErrorBoundary";
|
|
import { RecentWebsocketConnectionsDropdown } from "./RecentWebsocketConnectionsDropdown";
|
|
|
|
interface Props {
|
|
activeRequest: WebsocketRequest;
|
|
}
|
|
|
|
export function WebsocketResponsePane({ activeRequest }: Props) {
|
|
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
|
|
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
|
const [hexDumps, setHexDumps] = useState<Record<number, boolean>>({});
|
|
|
|
const activeConnection = useAtomValue(activeWebsocketConnectionAtom);
|
|
const connections = useAtomValue(activeWebsocketConnectionsAtom);
|
|
const events = useWebsocketEvents(activeConnection?.id ?? null);
|
|
|
|
if (activeConnection == null) {
|
|
return (
|
|
<HotkeyList hotkeys={["request.send", "model.create", "sidebar.focus", "url_bar.focus"]} />
|
|
);
|
|
}
|
|
|
|
const header = (
|
|
<HStack className="pl-3 mb-1 font-mono text-sm text-text-subtle">
|
|
<HStack space={2}>
|
|
{activeConnection.state !== "closed" && (
|
|
<LoadingIcon size="sm" className="text-text-subtlest" />
|
|
)}
|
|
<WebsocketStatusTag connection={activeConnection} />
|
|
<span>•</span>
|
|
<span>{events.length} Messages</span>
|
|
</HStack>
|
|
<HStack space={0.5} className="ml-auto">
|
|
<RecentWebsocketConnectionsDropdown
|
|
connections={connections}
|
|
activeConnection={activeConnection}
|
|
onPinnedConnectionId={setPinnedWebsocketConnectionId}
|
|
/>
|
|
</HStack>
|
|
</HStack>
|
|
);
|
|
|
|
return (
|
|
<ErrorBoundary name="Websocket Events">
|
|
<EventViewer
|
|
events={events}
|
|
getEventKey={(event) => event.id}
|
|
error={activeConnection.error}
|
|
header={header}
|
|
splitLayoutName="websocket_events"
|
|
defaultRatio={0.4}
|
|
renderRow={({ event, isActive, onClick }) => (
|
|
<WebsocketEventRow event={event} isActive={isActive} onClick={onClick} />
|
|
)}
|
|
renderDetail={({ event, index, onClose }) => (
|
|
<WebsocketEventDetail
|
|
event={event}
|
|
hexDump={hexDumps[index] ?? event.messageType === "binary"}
|
|
setHexDump={(v) => setHexDumps({ ...hexDumps, [index]: v })}
|
|
showLarge={showLarge}
|
|
showingLarge={showingLarge}
|
|
setShowLarge={setShowLarge}
|
|
setShowingLarge={setShowingLarge}
|
|
onClose={onClose}
|
|
/>
|
|
)}
|
|
/>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
function WebsocketEventRow({
|
|
event,
|
|
isActive,
|
|
onClick,
|
|
}: {
|
|
event: WebsocketEvent;
|
|
isActive: boolean;
|
|
onClick: () => void;
|
|
}) {
|
|
const { message: messageBytes, isServer, messageType } = event;
|
|
const message = messageBytes
|
|
? new TextDecoder("utf-8").decode(Uint8Array.from(messageBytes))
|
|
: "";
|
|
|
|
const iconColor =
|
|
messageType === "close" || messageType === "open" ? "secondary" : isServer ? "info" : "primary";
|
|
|
|
const icon =
|
|
messageType === "close" || messageType === "open"
|
|
? "info"
|
|
: isServer
|
|
? "arrow_big_down_dash"
|
|
: "arrow_big_up_dash";
|
|
|
|
const content =
|
|
messageType === "close" ? (
|
|
"Disconnected from server"
|
|
) : messageType === "open" ? (
|
|
"Connected to server"
|
|
) : message === "" ? (
|
|
<em className="italic text-text-subtlest">No content</em>
|
|
) : (
|
|
<span className="text-xs">{message.slice(0, 1000)}</span>
|
|
);
|
|
|
|
return (
|
|
<EventViewerRow
|
|
isActive={isActive}
|
|
onClick={onClick}
|
|
icon={<Icon color={iconColor} icon={icon} />}
|
|
content={content}
|
|
timestamp={event.createdAt}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function WebsocketEventDetail({
|
|
event,
|
|
hexDump,
|
|
setHexDump,
|
|
showLarge,
|
|
showingLarge,
|
|
setShowLarge,
|
|
setShowingLarge,
|
|
onClose,
|
|
}: {
|
|
event: WebsocketEvent;
|
|
hexDump: boolean;
|
|
setHexDump: (v: boolean) => void;
|
|
showLarge: boolean;
|
|
showingLarge: boolean;
|
|
setShowLarge: (v: boolean) => void;
|
|
setShowingLarge: (v: boolean) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const message = useMemo(() => {
|
|
if (hexDump) {
|
|
return event.message ? hexy(event.message) : "";
|
|
}
|
|
return event.message ? new TextDecoder("utf-8").decode(Uint8Array.from(event.message)) : "";
|
|
}, [event.message, hexDump]);
|
|
|
|
const language = languageFromContentType(null, message);
|
|
const formattedMessage = useFormatText({ language, text: message, pretty: true });
|
|
|
|
const title =
|
|
event.messageType === "close"
|
|
? "Connection Closed"
|
|
: event.messageType === "open"
|
|
? "Connection Open"
|
|
: `Message ${event.isServer ? "Received" : "Sent"}`;
|
|
|
|
const actions: EventDetailAction[] =
|
|
message !== ""
|
|
? [
|
|
{
|
|
key: "toggle-hexdump",
|
|
label: hexDump ? "Show Message" : "Show Hexdump",
|
|
onClick: () => setHexDump(!hexDump),
|
|
},
|
|
]
|
|
: [];
|
|
|
|
return (
|
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
|
<EventDetailHeader
|
|
title={title}
|
|
timestamp={event.createdAt}
|
|
actions={actions}
|
|
copyText={formattedMessage || undefined}
|
|
onClose={onClose}
|
|
/>
|
|
{!showLarge && event.message.length > 1000 * 1000 ? (
|
|
<VStack space={2} className="italic text-text-subtlest">
|
|
Message previews larger than 1MB are hidden
|
|
<div>
|
|
<Button
|
|
onClick={() => {
|
|
setShowingLarge(true);
|
|
setTimeout(() => {
|
|
setShowLarge(true);
|
|
setShowingLarge(false);
|
|
}, 500);
|
|
}}
|
|
isLoading={showingLarge}
|
|
color="secondary"
|
|
variant="border"
|
|
size="xs"
|
|
>
|
|
Try Showing
|
|
</Button>
|
|
</div>
|
|
</VStack>
|
|
) : event.message.length === 0 ? (
|
|
<EmptyStateText>No Content</EmptyStateText>
|
|
) : (
|
|
<Editor
|
|
language={language}
|
|
defaultValue={formattedMessage ?? ""}
|
|
wrapLines={false}
|
|
readOnly={true}
|
|
stateKey={null}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|