feat: add automatic review loop (#1840)
This commit is contained in:
committed by
GitHub
parent
45df19c3b2
commit
1f549e4525
@@ -11,17 +11,25 @@ import {
|
||||
withReviewSessionMarker,
|
||||
} from '@/lib/sessionReviewMetadata';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useAutoReviewStore, type AutoReviewRun } from '@/stores/useAutoReviewStore';
|
||||
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { optimisticSend, patchSessionMetadata, waitForConnectionOrThrow } from '@/sync/session-actions';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { getSyncMessages, getSyncParts, registerSessionDirectory } from '@/sync/sync-refs';
|
||||
import { getSyncMessages, getSyncParts, getSyncSessionStatus, registerSessionDirectory } from '@/sync/sync-refs';
|
||||
import { markPendingUserSendAnimation } from '@/lib/userSendAnimation';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
|
||||
const HANDOFF_TIMEOUT_MS = 180_000;
|
||||
const HANDOFF_POLL_MS = 400;
|
||||
const AUTO_REVIEW_POLL_MS = 300;
|
||||
const AUTO_REVIEW_MAX_ITERATIONS = 15;
|
||||
const AUTO_REVIEW_FINAL_MARKER = 'FINAL_REVIEW_STATUS: no_remaining_findings';
|
||||
const AUTO_REVIEW_FINAL_MARKER_NORMALIZED = AUTO_REVIEW_FINAL_MARKER.toLowerCase();
|
||||
const REVIEW_SESSION_TITLE = 'Review of workspace changes';
|
||||
const activeAutoReviewLoops = new Set<string>();
|
||||
const activeAutoReviewForwardKeys = new Set<string>();
|
||||
|
||||
type SessionModelContext = {
|
||||
providerID: string;
|
||||
@@ -36,6 +44,12 @@ type StartReviewFlowInput = SessionModelContext & {
|
||||
agentMentionName?: string;
|
||||
generateHandoff?: boolean;
|
||||
returnAfterHandoffRequest?: boolean;
|
||||
autoReview?: boolean;
|
||||
};
|
||||
|
||||
type AssistantTextMessage = {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const isMessageCompleted = (message: Message): boolean => {
|
||||
@@ -55,6 +69,234 @@ const getMessageRole = (message: Message): string => {
|
||||
return typeof role === 'string' ? role : '';
|
||||
};
|
||||
|
||||
const getMessageParentID = (message: Message): string | null => {
|
||||
const parentID = (message as { parentID?: unknown }).parentID;
|
||||
return typeof parentID === 'string' && parentID.trim().length > 0 ? parentID : null;
|
||||
};
|
||||
|
||||
const isCompactionCommandMessage = (message: Message, directory: string): boolean => {
|
||||
const parts = getSyncParts(message.id, directory);
|
||||
return parts.some((part) => {
|
||||
const type = (part as { type?: unknown }).type;
|
||||
if (type === 'compaction') return true;
|
||||
if (type !== 'text') return false;
|
||||
const text = (part as { text?: unknown }).text;
|
||||
return typeof text === 'string' && text.trim() === '/compact';
|
||||
});
|
||||
};
|
||||
|
||||
const getLatestAssistantTextMessage = (
|
||||
sessionID: string,
|
||||
directory: string,
|
||||
lastForwardedMessageID?: string,
|
||||
afterCreatedAt = 0,
|
||||
expectedParentID?: string,
|
||||
): AssistantTextMessage | null => {
|
||||
const messages = getSyncMessages(sessionID, directory);
|
||||
const compactionCommandIDs = new Set<string>();
|
||||
for (const message of messages) {
|
||||
if (isCompactionCommandMessage(message, directory)) {
|
||||
compactionCommandIDs.add(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (message.id === lastForwardedMessageID) return null;
|
||||
if (getMessageRole(message) !== 'assistant') continue;
|
||||
if (!isMessageCompleted(message)) continue;
|
||||
if (getMessageCreatedAt(message) < afterCreatedAt - 1000) continue;
|
||||
const parentID = getMessageParentID(message);
|
||||
if (!isExpectedAutoReviewAssistantParent(message, expectedParentID)) continue;
|
||||
if (parentID && compactionCommandIDs.has(parentID)) continue;
|
||||
const text = flattenAssistantTextParts(getSyncParts(message.id, directory)).trim();
|
||||
if (!text) continue;
|
||||
return { id: message.id, text };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isSessionIdle = (sessionID: string, directory: string): boolean => {
|
||||
const status = getSyncSessionStatus(sessionID, directory);
|
||||
return status?.type === 'idle';
|
||||
};
|
||||
|
||||
export const isAutoReviewRuntimeCurrent = (runtimeKey: string): boolean => runtimeKey === getRuntimeKey();
|
||||
|
||||
const stopRunForRuntimeMismatch = (run: AutoReviewRun): void => {
|
||||
useAutoReviewStore.getState().updateRun(run.originalSessionID, (current) => ({
|
||||
...current,
|
||||
status: 'stopped',
|
||||
error: 'Auto-review stopped because the runtime changed.',
|
||||
}));
|
||||
};
|
||||
|
||||
export const assertAutoReviewRuntimeStillCurrent = (expectedRuntimeKey?: string): void => {
|
||||
if (expectedRuntimeKey && !isAutoReviewRuntimeCurrent(expectedRuntimeKey)) {
|
||||
throw new Error('Auto-review stopped because the runtime changed.');
|
||||
}
|
||||
};
|
||||
|
||||
const isRuntimeChangeError = (error: unknown): boolean => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes('runtime changed');
|
||||
};
|
||||
|
||||
export const hasFinalReviewMarker = (text: string): boolean => {
|
||||
const lines = text.trim().split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
return lines.at(-1)?.toLowerCase() === AUTO_REVIEW_FINAL_MARKER_NORMALIZED;
|
||||
};
|
||||
|
||||
export const stripFinalReviewMarker = (text: string): string => {
|
||||
const lines = text.trimEnd().split('\n');
|
||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
|
||||
if (lines.at(-1)?.trim().toLowerCase() === AUTO_REVIEW_FINAL_MARKER_NORMALIZED) {
|
||||
lines.pop();
|
||||
}
|
||||
return lines.join('\n').trim();
|
||||
};
|
||||
|
||||
export const isExpectedAutoReviewAssistantParent = (message: Message, expectedParentID?: string): boolean => {
|
||||
if (!expectedParentID) return true;
|
||||
return getMessageParentID(message) === expectedParentID;
|
||||
};
|
||||
|
||||
export const getAutoReviewForwardKey = (run: AutoReviewRun, messageID: string): string => [
|
||||
run.runtimeKey,
|
||||
run.originalSessionID,
|
||||
run.phase,
|
||||
run.expectedAssistantParentID ?? '',
|
||||
messageID,
|
||||
].join(':');
|
||||
|
||||
export const claimAutoReviewForward = (run: AutoReviewRun, messageID: string): string | null => {
|
||||
const key = getAutoReviewForwardKey(run, messageID);
|
||||
if (activeAutoReviewForwardKeys.has(key)) return null;
|
||||
activeAutoReviewForwardKeys.add(key);
|
||||
return key;
|
||||
};
|
||||
|
||||
export const releaseAutoReviewForward = (key: string): void => {
|
||||
activeAutoReviewForwardKeys.delete(key);
|
||||
};
|
||||
|
||||
const autoReviewReviewerInstructions = (): Array<{ text: string; synthetic: true }> => [{
|
||||
synthetic: true,
|
||||
text: `This review is part of an automatic review loop. If there are no remaining issues, end your response with this exact final line:\n${AUTO_REVIEW_FINAL_MARKER}\nIf you found issues that require changes, do not include that final status line.`,
|
||||
}];
|
||||
|
||||
const runAutoReviewLoop = async (originalSessionID: string): Promise<void> => {
|
||||
while (true) {
|
||||
const run = useAutoReviewStore.getState().runsByOriginalSessionID[originalSessionID];
|
||||
if (!run || run.status !== 'running') return;
|
||||
if (!isAutoReviewRuntimeCurrent(run.runtimeKey)) {
|
||||
stopRunForRuntimeMismatch(run);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceSessionID = run.phase === 'waiting_for_reviewer' ? run.reviewSessionID : run.originalSessionID;
|
||||
if (!isSessionIdle(sourceSessionID, run.directory)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, AUTO_REVIEW_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
const latest = getLatestAssistantTextMessage(
|
||||
sourceSessionID,
|
||||
run.directory,
|
||||
run.lastForwardedMessageID,
|
||||
run.waitAfterCreatedAt,
|
||||
run.expectedAssistantParentID,
|
||||
);
|
||||
if (!latest) {
|
||||
await new Promise((resolve) => setTimeout(resolve, AUTO_REVIEW_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (run.phase === 'waiting_for_reviewer') {
|
||||
const forwardKey = claimAutoReviewForward(run, latest.id);
|
||||
if (!forwardKey) {
|
||||
await new Promise((resolve) => setTimeout(resolve, AUTO_REVIEW_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
if (!isAutoReviewRuntimeCurrent(run.runtimeKey)) {
|
||||
releaseAutoReviewForward(forwardKey);
|
||||
stopRunForRuntimeMismatch(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const waitAfterCreatedAt = Date.now();
|
||||
const isFinalReview = hasFinalReviewMarker(latest.text);
|
||||
const reviewFeedback = isFinalReview ? stripFinalReviewMarker(latest.text) : latest.text;
|
||||
const sentMessageID = await sendReviewFeedbackToOriginal(run.reviewSessionID, run.directory, reviewFeedback, run.runtimeKey);
|
||||
if (isFinalReview) {
|
||||
useAutoReviewStore.getState().completeRun(run.originalSessionID);
|
||||
return;
|
||||
}
|
||||
useAutoReviewStore.getState().updateRun(run.originalSessionID, (current) => ({
|
||||
...current,
|
||||
phase: 'waiting_for_implementer',
|
||||
lastForwardedMessageID: latest.id,
|
||||
expectedAssistantParentID: sentMessageID,
|
||||
waitAfterCreatedAt,
|
||||
}));
|
||||
} finally {
|
||||
releaseAutoReviewForward(forwardKey);
|
||||
}
|
||||
} else {
|
||||
if (run.iteration >= run.maxIterations) {
|
||||
useAutoReviewStore.getState().stopRun(run.originalSessionID);
|
||||
return;
|
||||
}
|
||||
const forwardKey = claimAutoReviewForward(run, latest.id);
|
||||
if (!forwardKey) {
|
||||
await new Promise((resolve) => setTimeout(resolve, AUTO_REVIEW_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
if (!isAutoReviewRuntimeCurrent(run.runtimeKey)) {
|
||||
releaseAutoReviewForward(forwardKey);
|
||||
stopRunForRuntimeMismatch(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const waitAfterCreatedAt = Date.now();
|
||||
const sentMessageID = await sendImplementationResponseToReviewer(run.originalSessionID, run.directory, latest.text, true, run.runtimeKey);
|
||||
useAutoReviewStore.getState().updateRun(run.originalSessionID, (current) => ({
|
||||
...current,
|
||||
phase: 'waiting_for_reviewer',
|
||||
iteration: current.iteration + 1,
|
||||
lastForwardedMessageID: latest.id,
|
||||
expectedAssistantParentID: sentMessageID,
|
||||
waitAfterCreatedAt,
|
||||
}));
|
||||
} finally {
|
||||
releaseAutoReviewForward(forwardKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoReviewRun = (run: AutoReviewRun): void => {
|
||||
useAutoReviewStore.getState().upsertRun(run);
|
||||
resumeAutoReviewRun(run.originalSessionID);
|
||||
};
|
||||
|
||||
export const resumeAutoReviewRun = (originalSessionID: string): void => {
|
||||
const run = useAutoReviewStore.getState().runsByOriginalSessionID[originalSessionID];
|
||||
if (!run || run.status !== 'running' || !isAutoReviewRuntimeCurrent(run.runtimeKey) || activeAutoReviewLoops.has(originalSessionID)) return;
|
||||
activeAutoReviewLoops.add(originalSessionID);
|
||||
void runAutoReviewLoop(run.originalSessionID).catch((error) => {
|
||||
console.error('[review-flow] auto-review loop failed', error);
|
||||
useAutoReviewStore.getState().updateRun(run.originalSessionID, (current) => ({
|
||||
...current,
|
||||
status: isRuntimeChangeError(error) ? 'stopped' : 'error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
}).finally(() => {
|
||||
activeAutoReviewLoops.delete(originalSessionID);
|
||||
});
|
||||
};
|
||||
|
||||
const waitForAssistantText = async (sessionID: string, directory: string, afterCreatedAt: number): Promise<string> => {
|
||||
const deadline = Date.now() + HANDOFF_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
@@ -118,7 +360,9 @@ const sendPlainMessage = async (
|
||||
text: string,
|
||||
modelContext?: SessionModelContext | null,
|
||||
additionalParts?: Array<{ text: string; synthetic?: boolean }>,
|
||||
): Promise<void> => {
|
||||
expectedRuntimeKey?: string,
|
||||
): Promise<string> => {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const resolved = modelContext ?? resolveModelContext(sessionID);
|
||||
if (!resolved) throw new Error('Select a model before sending review flow messages');
|
||||
const selection = useSelectionStore.getState();
|
||||
@@ -129,6 +373,7 @@ const sendPlainMessage = async (
|
||||
selection.saveAgentModelVariantForSession(sessionID, resolved.agent, resolved.providerID, resolved.modelID, resolved.variant);
|
||||
}
|
||||
markPendingUserSendAnimation(sessionID);
|
||||
let sentMessageID: string | null = null;
|
||||
await optimisticSend({
|
||||
sessionId: sessionID,
|
||||
content: text,
|
||||
@@ -136,19 +381,28 @@ const sendPlainMessage = async (
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.modelID,
|
||||
agent: resolved.agent,
|
||||
onMessageID: (messageID) => {
|
||||
sentMessageID = messageID;
|
||||
},
|
||||
beforeOptimisticInsert: () => assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey),
|
||||
onOptimisticInsert: () => requestChatForceScrollBottom(sessionID),
|
||||
send: (messageID) => opencodeClient.sendMessage({
|
||||
id: sessionID,
|
||||
directory,
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.modelID,
|
||||
agent: resolved.agent,
|
||||
variant: resolved.variant,
|
||||
text,
|
||||
additionalParts,
|
||||
messageId: messageID,
|
||||
}).then(() => undefined),
|
||||
send: (messageID) => {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
return opencodeClient.sendMessage({
|
||||
id: sessionID,
|
||||
directory,
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.modelID,
|
||||
agent: resolved.agent,
|
||||
variant: resolved.variant,
|
||||
text,
|
||||
additionalParts,
|
||||
messageId: messageID,
|
||||
}).then(() => undefined);
|
||||
},
|
||||
});
|
||||
if (!sentMessageID) throw new Error('Failed to prepare review flow message');
|
||||
return sentMessageID;
|
||||
};
|
||||
|
||||
const requestChatForceScrollBottom = (sessionId: string): void => {
|
||||
@@ -174,11 +428,14 @@ const getSessionOrNull = async (sessionID: string, directory: string): Promise<S
|
||||
}
|
||||
};
|
||||
|
||||
const createOrReuseReviewSession = async (originalSessionID: string, directory: string): Promise<Session> => {
|
||||
const createOrReuseReviewSession = async (originalSessionID: string, directory: string, expectedRuntimeKey?: string): Promise<Session> => {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const original = await opencodeClient.getSession(originalSessionID, directory);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const existingReviewID = getReviewSessionID(original);
|
||||
if (existingReviewID) {
|
||||
const existing = await getSessionOrNull(existingReviewID, directory);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
if (existing && isReviewSession(existing)) return existing;
|
||||
await patchSessionMetadata(originalSessionID, directory, (metadata) => {
|
||||
const next = { ...metadata };
|
||||
@@ -192,14 +449,18 @@ const createOrReuseReviewSession = async (originalSessionID: string, directory:
|
||||
});
|
||||
}
|
||||
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const review = await opencodeClient.createSession({
|
||||
title: REVIEW_SESSION_TITLE,
|
||||
metadata: withReviewSessionMarker({}, originalSessionID),
|
||||
}, directory);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
registerSessionDirectory(review.id, directory);
|
||||
try {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
await patchSessionMetadata(originalSessionID, directory, (metadata) => withReviewSessionLink(metadata, review.id));
|
||||
} catch (error) {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
await opencodeClient.deleteSession(review.id, directory).catch((deleteError) => {
|
||||
console.warn('[review-flow] failed to delete unlinked review session after link failure', deleteError);
|
||||
});
|
||||
@@ -211,6 +472,7 @@ const createOrReuseReviewSession = async (originalSessionID: string, directory:
|
||||
|
||||
export const startReviewFlow = async (input: StartReviewFlowInput): Promise<void> => {
|
||||
await waitForConnectionOrThrow();
|
||||
const expectedAutoReviewRuntimeKey = input.autoReview ? getRuntimeKey() : undefined;
|
||||
let reviewPrompt: string;
|
||||
|
||||
if (input.generateHandoff ?? true) {
|
||||
@@ -219,19 +481,38 @@ export const startReviewFlow = async (input: StartReviewFlowInput): Promise<void
|
||||
const startedAt = Date.now();
|
||||
await sendPlainMessage(input.originalSessionID, input.directory, visibleText, null, [
|
||||
{ text: instructionsText, synthetic: true },
|
||||
]);
|
||||
], expectedAutoReviewRuntimeKey);
|
||||
|
||||
const continueFromHandoff = async (): Promise<void> => {
|
||||
const handoff = await waitForAssistantText(input.originalSessionID, input.directory, startedAt);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedAutoReviewRuntimeKey);
|
||||
const handoffReviewPrompt = await renderMagicPrompt('session.reviewSession.visible', { handoff });
|
||||
const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory);
|
||||
await sendPlainMessage(reviewSession.id, input.directory, handoffReviewPrompt, {
|
||||
const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory, expectedAutoReviewRuntimeKey);
|
||||
const runtimeKey = expectedAutoReviewRuntimeKey ?? getRuntimeKey();
|
||||
const waitAfterCreatedAt = Date.now();
|
||||
const sentMessageID = await sendPlainMessage(reviewSession.id, input.directory, handoffReviewPrompt, {
|
||||
providerID: input.providerID,
|
||||
modelID: input.modelID,
|
||||
agent: input.agent,
|
||||
variant: input.variant,
|
||||
});
|
||||
openReviewSessionPanel(input.directory, reviewSession);
|
||||
}, input.autoReview ? autoReviewReviewerInstructions() : undefined, input.autoReview ? runtimeKey : undefined);
|
||||
if (input.autoReview) {
|
||||
startAutoReviewRun({
|
||||
originalSessionID: input.originalSessionID,
|
||||
reviewSessionID: reviewSession.id,
|
||||
directory: input.directory,
|
||||
runtimeKey,
|
||||
status: 'running',
|
||||
phase: 'waiting_for_reviewer',
|
||||
iteration: 0,
|
||||
maxIterations: AUTO_REVIEW_MAX_ITERATIONS,
|
||||
expectedAssistantParentID: sentMessageID,
|
||||
waitAfterCreatedAt,
|
||||
});
|
||||
}
|
||||
if (!input.autoReview) {
|
||||
openReviewSessionPanel(input.directory, reviewSession);
|
||||
}
|
||||
};
|
||||
|
||||
if (input.returnAfterHandoffRequest) {
|
||||
@@ -247,25 +528,46 @@ export const startReviewFlow = async (input: StartReviewFlowInput): Promise<void
|
||||
reviewPrompt = await renderMagicPrompt('session.reviewSessionWithoutHandoff.visible');
|
||||
}
|
||||
|
||||
const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory);
|
||||
await sendPlainMessage(reviewSession.id, input.directory, reviewPrompt, {
|
||||
const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory, expectedAutoReviewRuntimeKey);
|
||||
const runtimeKey = expectedAutoReviewRuntimeKey ?? getRuntimeKey();
|
||||
const waitAfterCreatedAt = Date.now();
|
||||
const sentMessageID = await sendPlainMessage(reviewSession.id, input.directory, reviewPrompt, {
|
||||
providerID: input.providerID,
|
||||
modelID: input.modelID,
|
||||
agent: input.agent,
|
||||
variant: input.variant,
|
||||
});
|
||||
openReviewSessionPanel(input.directory, reviewSession);
|
||||
}, input.autoReview ? autoReviewReviewerInstructions() : undefined, input.autoReview ? runtimeKey : undefined);
|
||||
if (input.autoReview) {
|
||||
startAutoReviewRun({
|
||||
originalSessionID: input.originalSessionID,
|
||||
reviewSessionID: reviewSession.id,
|
||||
directory: input.directory,
|
||||
runtimeKey,
|
||||
status: 'running',
|
||||
phase: 'waiting_for_reviewer',
|
||||
iteration: 0,
|
||||
maxIterations: AUTO_REVIEW_MAX_ITERATIONS,
|
||||
expectedAssistantParentID: sentMessageID,
|
||||
waitAfterCreatedAt,
|
||||
});
|
||||
}
|
||||
if (!input.autoReview) {
|
||||
openReviewSessionPanel(input.directory, reviewSession);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendReviewFeedbackToOriginal = async (reviewSessionID: string, directory: string, reviewFeedback: string): Promise<void> => {
|
||||
export const sendReviewFeedbackToOriginal = async (reviewSessionID: string, directory: string, reviewFeedback: string, expectedRuntimeKey?: string): Promise<string> => {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const reviewSession = await opencodeClient.getSession(reviewSessionID, directory);
|
||||
const originalSessionID = getOriginalSessionID(reviewSession);
|
||||
if (!originalSessionID) throw new Error('Original session is missing');
|
||||
const prompt = await renderMagicPrompt('session.reviewFeedbackToImplementer.visible', { review_feedback: reviewFeedback });
|
||||
await sendPlainMessage(originalSessionID, directory, prompt);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
return sendPlainMessage(originalSessionID, directory, prompt, undefined, undefined, expectedRuntimeKey);
|
||||
};
|
||||
|
||||
export const sendImplementationResponseToReviewer = async (originalSessionID: string, directory: string, implementationResponse: string): Promise<void> => {
|
||||
export const sendImplementationResponseToReviewer = async (originalSessionID: string, directory: string, implementationResponse: string, autoReview = false, expectedRuntimeKey?: string): Promise<string> => {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const originalSession = await opencodeClient.getSession(originalSessionID, directory);
|
||||
const reviewSessionID = getReviewSessionID(originalSession);
|
||||
if (!reviewSessionID) throw new Error('Review session is missing');
|
||||
@@ -273,12 +575,17 @@ export const sendImplementationResponseToReviewer = async (originalSessionID: st
|
||||
try {
|
||||
reviewSession = await opencodeClient.getSession(reviewSessionID, directory);
|
||||
} catch (error) {
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
await patchSessionMetadata(originalSessionID, directory, (metadata) => withoutReviewSessionLink(metadata, reviewSessionID));
|
||||
throw error;
|
||||
}
|
||||
const prompt = await renderMagicPrompt('session.implementationResponseToReviewer.visible', { implementation_response: implementationResponse });
|
||||
await sendPlainMessage(reviewSessionID, directory, prompt);
|
||||
openReviewSessionPanel(directory, reviewSession);
|
||||
assertAutoReviewRuntimeStillCurrent(expectedRuntimeKey);
|
||||
const sentMessageID = await sendPlainMessage(reviewSessionID, directory, prompt, undefined, autoReview ? autoReviewReviewerInstructions() : undefined, expectedRuntimeKey);
|
||||
if (!autoReview) {
|
||||
openReviewSessionPanel(directory, reviewSession);
|
||||
}
|
||||
return sentMessageID;
|
||||
};
|
||||
|
||||
export type ReviewTransferDirection = 'review-to-original' | 'original-to-review';
|
||||
|
||||
Reference in New Issue
Block a user