Files
yaak-mountain-loop/apps/yaak-client/lib/requestVersions.ts
T
Gregory Schier 7bd159b0b1 feat(client): offer the request a response was sent from
When the selected response's version no longer matches the live request, the
response header grows a "Request Changed" control with View Diff and Restore.
It follows the GraphQL editor's shape — a state-labelled dropdown button rather
than a new icon in the action row — because the label is the whole point: it
has to say *that* something is different before it offers to do anything about
it. Hidden when the two agree, and for responses recorded before versioning
existed, which have no version and never will.

The diff reuses the Git dialog's DiffViewer over YAML renderings of the two
documents, so a request diff reads like a commit diff.

Restore flushes pending edits first (the backend restores from the database, so
a debounced keystroke would otherwise land on top of it) and calls
wasUpdatedExternally afterwards, because the write came from this window and
the store's echo suppression would leave open editors showing the old content.

EditSessionTracker turns the boundaries only the frontend can see — switching
requests, losing focus, closing, falling idle for 60s — into snapshot calls. It
tracks no content of its own: content addressing already decides whether a
boundary had anything behind it, which is what keeps this a timer and two
assignments instead of a change-tracking system.
2026-09-05 21:20:20 -07:00

51 lines
2.0 KiB
TypeScript

import { flushAllPendingPatches } from "@yaakapp-internal/models";
import type { ModelPayload, ModelVersion, ModelVersionReason } from "@yaakapp-internal/models";
import { platform } from "@yaakapp-internal/platform";
import { EditSessionTracker } from "./editSessionTracker";
import { activeRequestIdAtom } from "../hooks/useActiveRequestId";
import { jotaiStore } from "./jotai";
import { rpc } from "./rpc";
const REQUEST_MODELS = ["http_request", "grpc_request", "websocket_request"];
/**
* Ask the backend to capture a request's current content.
*
* Quiet by design: a version that fails to write is not worth a toast, because
* every caller below is reacting to the user leaving rather than asking for
* anything.
*/
export function snapshotRequestVersion(requestId: string, reason: ModelVersionReason) {
// Edits reach the database on a debounce, so flush before asking for a
// version of what is in it
flushAllPendingPatches();
rpc<ModelVersion>("models_snapshot_request", { requestId, reason }).catch((err: unknown) => {
console.warn("Failed to snapshot request version", err);
});
}
export function initRequestVersionSnapshots() {
const tracker = new EditSessionTracker(snapshotRequestVersion);
platform.listen<ModelPayload[]>("model_writes", (payloads) => {
for (const payload of payloads) {
if (payload.change.type !== "upsert") continue;
if (!REQUEST_MODELS.includes(payload.model.model)) continue;
tracker.noteEdit(payload.model.id);
}
});
tracker.noteActiveRequest(jotaiStore.get(activeRequestIdAtom));
jotaiStore.sub(activeRequestIdAtom, () => {
tracker.noteActiveRequest(jotaiStore.get(activeRequestIdAtom));
});
platform.window.onFocusChanged((focused) => {
if (!focused) tracker.noteBoundary();
});
// Closing is the last boundary there is. Nothing can be awaited here, but the
// write is already on its way and the backend outlives the window.
window.addEventListener("beforeunload", () => tracker.noteBoundary());
}