mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
64062af9a1
A Guided Review can now leave Plannotator: as a single self-contained HTML file that renders exactly like the in-app guide, as an encrypted-by-default share link on guides.show, or authored by any agent through the new guide CLI. Highlights: packages/guide-viewer extracted from review-editor at the injection seam (read-only host, no third renderer); guides.show Worker with R2-backed share storage, per-IP rate limiting on creation, delete tokens hashed at rest, and 128-bit ids; portable exports pin the viewer by SRI hash with budget and manifest gates in PR CI and at deploy; two-runtime parity across Bun and Pi verified; v0.27.x saved guides load unchanged. Retention is indefinite by explicit decision, to revisit with the lean sharing refactor. Decision record: adr/decisions/007-portable-guided-reviews-20260815.md
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
import React, { useCallback, useMemo } from 'react';
|
|
import { GuideHostProvider, type GuideDiffRendererContext, type GuideHostValue } from '@plannotator/guide-viewer/host';
|
|
import { useReviewState } from '../../dock/ReviewStateContext';
|
|
import { AllFilesCodeView, type AllFilesCodeViewProps } from '../AllFilesCodeView';
|
|
|
|
/**
|
|
* The review app's guide host: adapts ReviewState into the narrow contract the
|
|
* packaged guide chain consumes (`@plannotator/guide-viewer/host`), and names
|
|
* `AllFilesCodeView` as the diff renderer with the full in-app wiring
|
|
* (annotations, staging, viewed, search, AI markers).
|
|
*
|
|
* This is the only place review state reaches the guide chain. The portable
|
|
* viewer provides the same contract with a read-only renderer, so both hosts
|
|
* render byte-identical guides (decision record D2).
|
|
*/
|
|
type InAppRendererProps = Omit<
|
|
AllFilesCodeViewProps,
|
|
| 'files'
|
|
| 'fileScrollTarget'
|
|
| 'fileOrder'
|
|
| 'mountCollapsed'
|
|
| 'initialScrollPosition'
|
|
| 'onScrollPositionChange'
|
|
| 'onFileCollapsedChange'
|
|
| 'isActive'
|
|
| 'allowScrollChaining'
|
|
>;
|
|
|
|
export const ReviewGuideHost: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const state = useReviewState();
|
|
|
|
const getDiffRendererProps = useCallback(
|
|
({ focused }: GuideDiffRendererContext): InAppRendererProps => ({
|
|
diffStyle: state.diffStyle,
|
|
diffOverflow: state.diffOverflow,
|
|
diffIndicators: state.diffIndicators,
|
|
lineDiffType: state.lineDiffType,
|
|
disableLineNumbers: state.disableLineNumbers,
|
|
disableBackground: state.disableBackground,
|
|
expandUnchanged: state.expandUnchanged,
|
|
fontFamily: state.fontFamily,
|
|
fontSize: state.fontSize,
|
|
annotations: state.allAnnotations,
|
|
selectedAnnotationId: state.selectedAnnotationId,
|
|
scrollTargetAnnotation: state.scrollTargetAnnotation,
|
|
// Only the focused card is the selection target; a shared pending
|
|
// selection painted on every mounted card would highlight the wrong file.
|
|
pendingSelection: focused ? state.pendingSelection : null,
|
|
reviewBase: state.reviewBase,
|
|
reviewSnapshotId: state.feedbackDiffContext?.snapshotId,
|
|
onLineSelection: state.onLineSelection,
|
|
onAddAnnotationForFile: state.onAddAnnotationForFile,
|
|
onEditAnnotation: state.onEditAnnotation,
|
|
onSelectAnnotation: state.onSelectAnnotation,
|
|
onDeleteAnnotation: state.onDeleteAnnotation,
|
|
onAddFileCommentForFile: state.onAddFileCommentForFile,
|
|
viewedFiles: state.viewedFiles,
|
|
onToggleViewed: state.onToggleViewed,
|
|
showViewedControls: state.showViewedControls,
|
|
stagedFiles: state.stagedFiles,
|
|
onStage: state.onStage,
|
|
canStageFiles: state.canStageFiles,
|
|
showStageControls: state.showStageControls,
|
|
canStagePath: state.canStagePath,
|
|
stagingFile: state.stagingFile,
|
|
stageError: state.stageError,
|
|
prUrl: state.prMetadata?.url,
|
|
prDiffScope: state.prDiffScope,
|
|
searchQuery: state.isSearchPending ? '' : state.debouncedSearchQuery,
|
|
searchMatches: state.searchMatches,
|
|
activeSearchMatchId: state.activeSearchMatchId,
|
|
activeSearchMatch: state.allFilesActiveSearchMatch,
|
|
onCodeNavRequest: state.onCodeNavRequest,
|
|
aiAvailable: state.aiAvailable,
|
|
onAskAIForFile: state.onAskAIForFile,
|
|
isAILoading: state.isAILoading,
|
|
onViewAIResponse: state.onViewAIResponse,
|
|
aiMessages: state.aiMessages,
|
|
onClickAIMarker: state.onClickAIMarker,
|
|
getAIHistoryForFile: state.getAIHistoryForFile,
|
|
}),
|
|
[state],
|
|
);
|
|
|
|
const value = useMemo<GuideHostValue<InAppRendererProps>>(
|
|
() => ({
|
|
files: state.files,
|
|
DiffRenderer: AllFilesCodeView,
|
|
getDiffRendererProps,
|
|
revealFile: state.guideRevealFile,
|
|
onRevealFile: state.onGuideRevealFile,
|
|
activeSearchMatch: state.allFilesActiveSearchMatch,
|
|
}),
|
|
[state.files, getDiffRendererProps, state.guideRevealFile, state.onGuideRevealFile, state.allFilesActiveSearchMatch],
|
|
);
|
|
|
|
return <GuideHostProvider value={value}>{children}</GuideHostProvider>;
|
|
};
|