Files
openchamber/packages/ui/src/lib/sessionReviewMetadata.ts
T
Bohdan Triapitsyn 1f9769a932 feat: add session review handoff flow
Introduce a desktop/web-only /handoff-review flow that generates a handoff from the active implementation session, creates or reuses a separate review session in the same directory, and links the pair through hidden OpenChamber session metadata.

Add review flow orchestration, metadata helpers, magic prompts, localized command/action labels, session metadata create/update support, and assistant message transfer actions for sending reviewer feedback back to the implementer or implementation responses back to the reviewer.

Review sessions are ordinary sessions, not child sessions. The flow avoids exposing session IDs or routing metadata to agents, hides review controls on mobile and VS Code, hides unrelated assistant actions inside review sessions, cleans up stale metadata where possible, and uses the optimistic send path so cross-session sends scroll like normal composer messages.
2026-06-07 01:22:40 +03:00

83 lines
2.6 KiB
TypeScript

import type { Session } from '@opencode-ai/sdk/v2';
export type SessionMetadataRecord = Record<string, unknown>;
type OpenChamberMetadata = {
kind?: 'review';
originalSessionID?: string;
reviewSessionID?: string;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value && typeof value === 'object' && !Array.isArray(value));
export const getSessionMetadata = (session: Session | null | undefined): SessionMetadataRecord => {
const metadata = (session as (Session & { metadata?: unknown }) | null | undefined)?.metadata;
return isRecord(metadata) ? metadata : {};
};
const getOpenChamberMetadata = (metadata: SessionMetadataRecord): OpenChamberMetadata => {
const value = metadata.openchamber;
return isRecord(value) ? value as OpenChamberMetadata : {};
};
export const getReviewSessionID = (session: Session | null | undefined): string | null => {
const value = getOpenChamberMetadata(getSessionMetadata(session)).reviewSessionID;
return typeof value === 'string' && value.trim().length > 0 ? value : null;
};
export const getOriginalSessionID = (session: Session | null | undefined): string | null => {
const value = getOpenChamberMetadata(getSessionMetadata(session)).originalSessionID;
return typeof value === 'string' && value.trim().length > 0 ? value : null;
};
export const isReviewSession = (session: Session | null | undefined): boolean =>
getOpenChamberMetadata(getSessionMetadata(session)).kind === 'review' && Boolean(getOriginalSessionID(session));
export const withReviewSessionLink = (
metadata: SessionMetadataRecord,
reviewSessionID: string,
): SessionMetadataRecord => {
const current = getOpenChamberMetadata(metadata);
return {
...metadata,
openchamber: {
...current,
reviewSessionID,
},
};
};
export const withReviewSessionMarker = (
metadata: SessionMetadataRecord,
originalSessionID: string,
): SessionMetadataRecord => {
const current = getOpenChamberMetadata(metadata);
return {
...metadata,
openchamber: {
...current,
kind: 'review' as const,
originalSessionID,
},
};
};
export const withoutReviewSessionLink = (
metadata: SessionMetadataRecord,
reviewSessionID: string,
): SessionMetadataRecord => {
const current = getOpenChamberMetadata(metadata);
if (current.reviewSessionID !== reviewSessionID) return metadata;
const restOpenChamber = { ...current };
delete restOpenChamber.reviewSessionID;
const next: SessionMetadataRecord = { ...metadata };
if (Object.keys(restOpenChamber).length > 0) {
next.openchamber = restOpenChamber;
} else {
delete next.openchamber;
}
return next;
};