mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
cb2b264303
An authored guide's generator.engine is the harness the agent ran in (claude-code, codex-cli, …), which the in-app engine map does not know, so the header printed the raw id. Map the common ones; unknown ids still render as written. Viewer rebuilt and published, manifest synced.
98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
import React, { useCallback, useMemo, useState } from 'react';
|
|
import type { GuideSnapshot } from '@plannotator/core/guide-format';
|
|
import { GUIDE_ENGINE_LABEL, type CodeGuideData } from '@plannotator/core/guide';
|
|
import { parseDiffToFiles } from './diffParser';
|
|
import { GuideHostProvider, type GuideDiffRendererContext, type GuideDiffRendererProps } from './host';
|
|
import { GuideView } from './GuideView';
|
|
import type { DiffFile } from './types';
|
|
|
|
/**
|
|
* Renders a portable Guided Review snapshot: the guide chain over the exact
|
|
* diff it was generated against, with in-session reviewed toggles and no
|
|
* server behind it. Hosts inject the diff renderer (Plannotator's
|
|
* `AllFilesCodeView` in `readOnly` mode) so the portable file and the in-app
|
|
* guide are the same components (decision record D2, D3, D4).
|
|
*
|
|
* This component is deliberately theme-agnostic: the host mounts it inside
|
|
* its ThemeProvider / stylesheet of choice.
|
|
*/
|
|
export interface GuideViewerProps<P extends object> {
|
|
snapshot: GuideSnapshot;
|
|
DiffRenderer: React.ComponentType<GuideDiffRendererProps & P>;
|
|
/** Extra renderer props (a read-only host passes no-op annotation wiring). */
|
|
getDiffRendererProps: (context: GuideDiffRendererContext) => P;
|
|
/** Optional line under the guide title naming what the guide is of (repo, PR link, engine). */
|
|
sourceLine?: React.ReactNode;
|
|
/** Host-provided controls rendered top-right of the guide header (e.g. a theme toggle). */
|
|
headerActions?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Engines an agent-authored guide names in `generator.engine` (the harness the
|
|
* agent ran in), beyond the in-app job engines `GUIDE_ENGINE_LABEL` knows.
|
|
* Anything unlisted renders as written.
|
|
*/
|
|
const AUTHORED_ENGINE_LABEL: Record<string, string> = {
|
|
'claude-code': 'Claude Code',
|
|
'codex-cli': 'Codex CLI',
|
|
'gemini-cli': 'Gemini CLI',
|
|
'copilot-cli': 'Copilot CLI',
|
|
'kiro-cli': 'Kiro CLI',
|
|
amp: 'Amp',
|
|
droid: 'Droid',
|
|
};
|
|
|
|
/**
|
|
* Human label for the guide's generator, mirroring the in-app "generated by …"
|
|
* wording: the engine's display name plus the model as recorded, when the
|
|
* producer knew it (self-reported by the agent from its harness, or the model
|
|
* a Plannotator job was launched with).
|
|
*/
|
|
function engineLabel(engine: string | undefined, model: string | undefined): string | undefined {
|
|
if (!engine) return model || undefined;
|
|
const label = (GUIDE_ENGINE_LABEL as Record<string, string>)[engine] ?? AUTHORED_ENGINE_LABEL[engine] ?? engine;
|
|
return model ? `${label} · ${model}` : label;
|
|
}
|
|
|
|
export function GuideViewer<P extends object>({ snapshot, DiffRenderer, getDiffRendererProps, sourceLine, headerActions, className }: GuideViewerProps<P>) {
|
|
const files = useMemo<DiffFile[]>(() => parseDiffToFiles(snapshot.review.rawPatch), [snapshot.review.rawPatch]);
|
|
const guide = useMemo<CodeGuideData>(
|
|
() => ({
|
|
title: snapshot.guide.title,
|
|
intent: snapshot.guide.intent,
|
|
sections: snapshot.guide.sections,
|
|
unplacedFiles: snapshot.guide.unplacedFiles,
|
|
reviewed: [...snapshot.guide.reviewed],
|
|
}),
|
|
[snapshot.guide],
|
|
);
|
|
const [reviewed, setReviewed] = useState<boolean[]>(() => [...snapshot.guide.reviewed]);
|
|
const [focusedFile, setFocusedFile] = useState<string | null>(null);
|
|
const toggleReviewed = useCallback((index: number) => {
|
|
setReviewed((prev) => prev.map((value, i) => (i === index ? !value : value)));
|
|
}, []);
|
|
|
|
const host = useMemo(
|
|
() => ({ files, DiffRenderer, getDiffRendererProps }),
|
|
[files, DiffRenderer, getDiffRendererProps],
|
|
);
|
|
|
|
return (
|
|
<GuideHostProvider value={host}>
|
|
<div className={className}>
|
|
<GuideView
|
|
guide={guide}
|
|
reviewed={reviewed}
|
|
onToggleReviewed={toggleReviewed}
|
|
engineLabel={engineLabel(snapshot.generator?.engine, snapshot.generator?.model)}
|
|
focusedFile={focusedFile}
|
|
onFocusFile={setFocusedFile}
|
|
sourceLine={sourceLine}
|
|
headerActions={headerActions}
|
|
/>
|
|
</div>
|
|
</GuideHostProvider>
|
|
);
|
|
}
|