Files
openchamber/packages/ui/src/lib/sessionBtwMetadata.ts
T

121 lines
4.7 KiB
TypeScript
Raw Normal View History

import type { Session } from '@opencode-ai/sdk/v2';
import { getSessionMetadata, type SessionMetadataRecord } from '@/lib/sessionReviewMetadata';
/**
* Session-metadata contract for the `/btw` flow, mirroring the review-session
* link in `sessionReviewMetadata`:
*
* - The parent (the session `/btw` was typed into) carries
* `openchamber.btwSessionID` pointing at its active btw fork. The panel is
* derived from this link, so it appears only in the parent session and
* survives reloads.
* - The fork itself is marked `openchamber.kind = 'btw'` with
* `originalSessionID` (its parent) and `btwBoundaryMessageID` — the id of
* the last message cloned from the parent. Messages with a greater id are
* the fork's own tail and are what the panel renders. Message ids are
* server-generated ascending identifiers, so the boundary is a plain string
* comparison and immune to client clock skew.
*/
type BtwMetadata = {
kind?: string;
originalSessionID?: string;
btwSessionID?: string;
btwBoundaryMessageID?: string;
};
const getOpenChamberMetadata = (metadata: SessionMetadataRecord): BtwMetadata => {
const value = metadata.openchamber;
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
// SAFETY: session metadata is persisted, externally writable data; this is
// its parsing boundary. `BtwMetadata` only declares optional fields and
// every reader re-validates the field it consumes in `nonEmpty`.
return value as BtwMetadata;
};
const nonEmpty = (value: string | undefined): string | null =>
typeof value === 'string' && value.trim().length > 0 ? value : null;
/** The parent's link to its active btw fork, or null. */
export const getBtwSessionID = (session: Session | null | undefined): string | null =>
nonEmpty(getOpenChamberMetadata(getSessionMetadata(session)).btwSessionID);
export const isBtwSession = (session: Session | null | undefined): boolean =>
getOpenChamberMetadata(getSessionMetadata(session)).kind === 'btw'
&& Boolean(getBtwOriginalSessionID(session));
/** The fork's back-pointer to the session `/btw` was typed into. */
export const getBtwOriginalSessionID = (session: Session | null | undefined): string | null => {
const openchamber = getOpenChamberMetadata(getSessionMetadata(session));
return openchamber.kind === 'btw' ? nonEmpty(openchamber.originalSessionID) : null;
};
/**
* The id of the last message the fork inherited from the parent. `null` means
* the fork inherited nothing (empty parent) and every message is its own.
*/
export const getBtwBoundaryMessageID = (session: Session | null | undefined): string | null => {
const openchamber = getOpenChamberMetadata(getSessionMetadata(session));
return openchamber.kind === 'btw' ? nonEmpty(openchamber.btwBoundaryMessageID) : null;
};
export const withBtwSessionLink = (
metadata: SessionMetadataRecord,
btwSessionID: string,
): SessionMetadataRecord => ({
...metadata,
openchamber: {
...getOpenChamberMetadata(metadata),
btwSessionID,
},
});
/**
* Mark the fork as a btw session. The fork clones the parent's metadata
* wholesale (including review links or a stale `btwSessionID`), so the
* inherited `openchamber` object is replaced, not merged.
*/
export const withBtwSessionMarker = (
metadata: SessionMetadataRecord,
originalSessionID: string,
boundaryMessageID: string | null,
): SessionMetadataRecord => {
const openchamber: BtwMetadata = { kind: 'btw', originalSessionID };
if (boundaryMessageID) openchamber.btwBoundaryMessageID = boundaryMessageID;
return { ...metadata, openchamber };
};
/** Remove the btw marker so a promoted fork becomes a plain session. */
export const withoutBtwSessionMarker = (metadata: SessionMetadataRecord): SessionMetadataRecord => {
const openchamber = getOpenChamberMetadata(metadata);
if (openchamber.kind !== 'btw') return metadata;
const rest: BtwMetadata = { ...openchamber };
delete rest.kind;
delete rest.originalSessionID;
delete rest.btwBoundaryMessageID;
const next: SessionMetadataRecord = { ...metadata };
if (Object.keys(rest).length > 0) {
next.openchamber = rest;
} else {
delete next.openchamber;
}
return next;
};
/** Unlink the parent, but only if it still points at this fork. */
export const withoutBtwSessionLink = (
metadata: SessionMetadataRecord,
btwSessionID: string,
): SessionMetadataRecord => {
const openchamber = getOpenChamberMetadata(metadata);
if (openchamber.btwSessionID !== btwSessionID) return metadata;
const rest: BtwMetadata = { ...openchamber };
delete rest.btwSessionID;
const next: SessionMetadataRecord = { ...metadata };
if (Object.keys(rest).length > 0) {
next.openchamber = rest;
} else {
delete next.openchamber;
}
return next;
};