Documentation
¶
Index ¶
- Constants
- Variables
- func AgentRepliesPath(csvPath string) string
- func AppliedPath(csvPath string) string
- func EventsPath(csvPath string) string
- func HiddenSuggestionPath(csvPath string) string
- func LLMStatusPath(csvPath string) string
- func LoadQuizAnswers(csvPath string) map[string]QuizAnswer
- func NewQuizID() string
- func NewSuggestionID() string
- func ProcessedPath(csvPath string) string
- func QuizAnswersPath(csvPath string) string
- func QuizPath(csvPath string) string
- func QuizThreadID(quizID, questionID string) string
- func ReenqueuePath(csvPath string) string
- func RevertedPath(csvPath string) string
- func ReviewerRepliesPath(csvPath string) string
- func SessionPath(csvPath string) string
- func SessionScope(csvPath string) string
- func SplitQuizThreadID(s string) (quizID, questionID string, ok bool)
- func SuggestionDecisionPath(csvPath string) string
- func SuggestionPath(csvPath string) string
- func ValidateQuiz(q Quiz) error
- func VersionsDir(csvPath string) string
- func WriteSessionScope(csvPath, singleFile string) error
- type Area
- type BlockAnn
- type Checkpoint
- type Comment
- func (c Comment) AnchorEdited() bool
- func (c Comment) AnchorMoved() bool
- func (c Comment) AnchorOutdated() bool
- func (c Comment) IsAreaLevel() bool
- func (c Comment) IsFileLevel() bool
- func (c Comment) IsRegionLevel() bool
- func (c Comment) IsTextLevel() bool
- func (c Comment) LineSpan() string
- func (c Comment) QueueState() string
- func (c Comment) Snippet() string
- type CommentAnchor
- type EventStream
- type FileHistoryEntry
- type FileRef
- type FileVersion
- type HiddenSuggestion
- type KeyBinding
- type LLMStatus
- type PrereviewController
- func (c *PrereviewController) AcceptSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) AddComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) AnswerQuestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CancelReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CheckpointBaseline()
- func (c *PrereviewController) ClearFlash(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ClearSelection(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ClearSuggestionDecision(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CloseFiles(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CloseMoreMenu(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CloseSearch(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CloseTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CursorDown(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CursorUp(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CycleScheme(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) CycleTheme(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) DeleteComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) DiffVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) DismissQuizNav(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) DismissUndo(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) EditComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) EndSession(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) EnqueueComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ExitVersionView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) FocusComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) HideComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) HideSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) HideSuggestionGroup(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) JumpToComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) JumpToQuizLine(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) JumpToReadFrontier(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) JumpToSearchResult(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) JumpToSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) LLMStatusChanged(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) Mount(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) NavigateToHeading(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) NextComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) NextFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) OnConnect(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) OpenFileComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) OpenReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) OpenSearch(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) OpenTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) PickPrompt(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) PostReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) PrevComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) PrevFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) Quit(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ReanchorComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ReenqueueComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RefreshDiff(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RejectSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ReportViewport(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RequestQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RequestRevert(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RestoreVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ResumeAgent(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) RetakeQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SaveDraft(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SaveReplyDraft(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectBlock(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectImageArea(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectLine(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectRegion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SelectText(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetBase(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetFileFilter(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetFileViewMode(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetHTMLView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetMarkdownView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetProxyURL(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetSearchQuery(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SetURLHash(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ShowHiddenSuggestions(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) SweepGoneFiles()
- func (c *PrereviewController) ToggleAgentPause(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleAnnotations(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleCommentList(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleFileScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleFileView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleFiles(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleFocusMode(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleKeyboardHelp(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleMarks(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleMoreMenu(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleQueueScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleRawHTML(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleRawMarkdown(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleRegionSelect(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleRow(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleSearchScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleShowResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleSuggestions(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ToggleViewed(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) UndoDelete(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) UnhideAllResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) ViewVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
- func (c *PrereviewController) WatchLLMStatus(stop <-chan struct{}, poll time.Duration)
- type PrereviewState
- func (s PrereviewState) AgentWorking() bool
- func (s PrereviewState) AgentWorkingLabel() string
- func (s PrereviewState) AllRegionComments() []Comment
- func (s PrereviewState) AnnotationAcceptedLines() map[string]bool
- func (s PrereviewState) AnnotationCountLines() map[string]int
- func (s PrereviewState) AnnotationOpenLines() map[string]bool
- func (s PrereviewState) AreaComments() []Comment
- func (s PrereviewState) AwaitingAgent() map[string]bool
- func (s PrereviewState) AwaitingApplyCount() int
- func (s PrereviewState) AwaitingLines() map[string]bool
- func (s PrereviewState) AwaitingReplyCount() int
- func (s PrereviewState) BinaryKind() string
- func (s PrereviewState) BlockAnnotations() map[string]BlockAnn
- func (s PrereviewState) BlockDiffStatus() map[string]string
- func (s PrereviewState) CSVBasename() string
- func (s PrereviewState) ChangedFilesCount() int
- func (s PrereviewState) CommentCount() int
- func (s PrereviewState) CommentCountLines() map[string]int
- func (s PrereviewState) CommentOpenLines() map[string]bool
- func (s PrereviewState) CommentsByEndLine() map[int][]Comment
- func (s PrereviewState) CurrentQuiz() *Quiz
- func (s PrereviewState) DataMode() string
- func (s PrereviewState) DataScheme() string
- func (s PrereviewState) DecisionCount() int
- func (s PrereviewState) DecisionsBySuggestion() map[string]SuggestionDecision
- func (s PrereviewState) DoneCount() int
- func (s PrereviewState) DraftCount() int
- func (s PrereviewState) FileBrowserHTML() template.HTML
- func (s PrereviewState) FileComments() []Comment
- func (s PrereviewState) FileHeadAnnotationCount() int
- func (s PrereviewState) FileHeadHasOpen() bool
- func (s PrereviewState) FileLevelComments() []Comment
- func (s PrereviewState) FileQuizItems() []QuizItem
- func (s PrereviewState) FileSuggestions() []Suggestion
- func (s PrereviewState) FileTree() []*TreeNode
- func (s PrereviewState) FilteredFileNodes() []*TreeNode
- func (s PrereviewState) FilteredFiles() []gitdiff.FileEntry
- func (s PrereviewState) FocusedComment() *Comment
- func (s PrereviewState) HasMarks() bool
- func (s PrereviewState) HasQueue() bool
- func (s PrereviewState) HasQuiz() bool
- func (s PrereviewState) HasReadFrontier() bool
- func (s PrereviewState) HiddenResolvedCount() int
- func (s PrereviewState) HiddenSuggestionCount() int
- func (s PrereviewState) IsHTML() bool
- func (s PrereviewState) IsMarkdown() bool
- func (s PrereviewState) KeyBindings() []KeyBinding
- func (s PrereviewState) KeyHint() map[string]string
- func (s PrereviewState) LineDisplay() map[string]template.HTML
- func (s PrereviewState) NextScheme() string
- func (s PrereviewState) NextSchemeLabel() string
- func (s PrereviewState) NextThemeMode() string
- func (s PrereviewState) NextThemeModeLabel() string
- func (s PrereviewState) OutdatedCount() int
- func (s PrereviewState) QueueHiddenCount() int
- func (s PrereviewState) QueueItems() []QueueItem
- func (s PrereviewState) QueueScopeLabel() string
- func (s PrereviewState) QueuedCount() int
- func (s PrereviewState) QuizAnsweredCount() int
- func (s PrereviewState) QuizByEndLine() map[int][]QuizItem
- func (s PrereviewState) QuizComplete() bool
- func (s PrereviewState) QuizCorrectCount() int
- func (s PrereviewState) QuizCountLines() map[string]int
- func (s PrereviewState) QuizItems() []QuizItem
- func (s PrereviewState) QuizNavLabel() string
- func (s PrereviewState) QuizOpenLines() map[string]bool
- func (s PrereviewState) QuizPending() bool
- func (s PrereviewState) QuizQuestionCount() int
- func (s PrereviewState) QuizResults() []StreamQuiz
- func (s PrereviewState) QuizScoreLabel() string
- func (s PrereviewState) ReadFrontierKey() string
- func (s PrereviewState) ReadPercent() int
- func (s PrereviewState) RegionComments() []Comment
- func (s PrereviewState) RenderedHTML() []gitdiff.HTMLBlock
- func (s PrereviewState) RenderedHTMLDoc() string
- func (s PrereviewState) RenderedHeadings() []gitdiff.Heading
- func (s PrereviewState) RenderedMarkdown() []gitdiff.MarkdownBlock
- func (s PrereviewState) ResolvedCount() int
- func (s PrereviewState) Revealing() bool
- func (s PrereviewState) RowToggled() map[string]bool
- func (s PrereviewState) SchemeLabel() string
- func (s PrereviewState) ScopeFileCount() int
- func (s PrereviewState) ScopeViewedCount() int
- func (s PrereviewState) ScrollHeadingBlockKey() string
- func (s PrereviewState) SearchScopeCount() int
- func (s PrereviewState) SearchScopeLabel() string
- func (s PrereviewState) SearchTruncated() bool
- func (s PrereviewState) SelectedFileBase() string
- func (s PrereviewState) SelectedFileDir() string
- func (s PrereviewState) SelectedLines() map[int]bool
- func (s PrereviewState) SelectionEmpty() bool
- func (s PrereviewState) SelectionEndMax() int
- func (s PrereviewState) SelectionLabel() string
- func (s PrereviewState) ShowQuizNav() bool
- func (s PrereviewState) ShowRenderedHTML() bool
- func (s PrereviewState) ShowRenderedMarkdown() bool
- func (s PrereviewState) SuggestionAcceptedLines() map[string]bool
- func (s PrereviewState) SuggestionCount() int
- func (s PrereviewState) SuggestionCountLines() map[string]int
- func (s PrereviewState) SuggestionGroups() map[string]SuggestionGroupInfo
- func (s PrereviewState) SuggestionOpenLines() map[string]bool
- func (s PrereviewState) SuggestionsByEndLine() map[int][]Suggestion
- func (s PrereviewState) ThemeModeLabel() string
- func (s PrereviewState) Threads() map[string][]ThreadEntry
- func (s PrereviewState) URLHash() string
- func (s PrereviewState) ViewedCount() int
- func (s PrereviewState) VisibleComments() []Comment
- func (s PrereviewState) VisibleLines() []gitdiff.DiffLine
- type ProcessedMark
- type Prompt
- type Question
- type QueueItem
- type Quiz
- type QuizAnswer
- type QuizItem
- type SearchHit
- type StreamComment
- type StreamDecision
- type StreamEvent
- type StreamQuiz
- type StreamReply
- type Suggestion
- type SuggestionDecision
- type SuggestionGroupInfo
- type ThreadEntry
- type TreeNode
- type UIPrefs
- type VersionListItem
- type VersionStore
- func (s *VersionStore) Blob(sha string) ([]byte, error)
- func (s *VersionStore) Checkpoint(refs []FileRef, trigger, changelog string) (cp Checkpoint, created bool, err error)
- func (s *VersionStore) Checkpoints() ([]Checkpoint, error)
- func (s *VersionStore) FileHistory(path string) ([]FileHistoryEntry, error)
- func (s *VersionStore) Restore(path string, seq int) ([]byte, error)
- type VersionSummary
Constants ¶
const ( LLMStateWorking = "working" LLMStateDone = "done" )
LLM status values written by the agent into llm-status.json's "state" field. Anything else (including "") is treated as idle.
const ( // QuizFileName is the agent-written, append-only quiz file under .prereview/. // Durable across launches, deduped by quiz id (last write wins) so the agent // can revise a quiz by re-appending the same id. QuizFileName = "quiz.jsonl" // QuizAnswersFileName is the server-written record of the reviewer's answers. // Separate file because quiz.jsonl is the agent's; one writer per file. QuizAnswersFileName = "quiz-answers.jsonl" )
quiz.go wires the #191 comprehension quiz: the reviewer asks for a quiz about the current diff, the agent authors it, and prereview renders it so answering becomes retrieval practice instead of a rubber-stamped `accept`.
It is the same two-channel shape as suggestions, with the same ownership rule (see thread.go: never one file with two writers):
- .prereview/quiz.jsonl is AGENT-owned and append-only (written by `prereview quiz`, never rewritten by the server), so it cannot race comments.csv. Durable across launches — openStore does NOT reset it.
- .prereview/quiz-answers.jsonl is SERVER-owned: the reviewer's answers, rewritten atomically when they answer or retake.
The two are merged at load, exactly like agent-replies.jsonl + reviewer-replies.jsonl.
Where the quality contract is enforced
The prompt that produces a quiz is GUIDANCE and is user-replaceable (see LoadQuizPrompts), so no guarantee may depend on its text. The contract is enforced downstream instead, and binds every quiz whatever prompt produced it:
ValidateQuiz (here, called by the CLI verb) — structure: known probe, >= 2
options, answer index in range, non-empty explanation, side set.
applyQuiz (the server, which holds the diff) — grounding: the cited line
range actually resolves in the file under review.
That split is what makes a user-authored prompt safe: a sloppy one yields a boring quiz, never a structurally invalid or hallucinated-anchor one.
const ( ProbeChangeType = "change-type" ProbeLocalization = "localization" ProbeConsequence = "consequence" ProbeRationale = "rationale" ProbeDecision = "decision" )
The probe taxonomy. The first three are adapted from the CodeReviewQA benchmark's reasoning steps (change type recognition / change localisation / solution identification); the last two are prereview's own.
ProbeDecision is the one that targets rubber-stamping specifically: the other four ask whether the reviewer understood what IS in the diff, while `decision` asks whether they noticed something they might have OBJECTED to — an unrequested dependency, a changed default, a widened interface, a skipped edge case. It is diff-versus-intent rather than diff-internal, which is why only the agent that did the work can author it.
const ( QuestionKindLine = "line" // a line range in the diff (the default) QuestionKindText = "text" // a character range within a line QuestionKindFile = "file" // the change as a whole, or something absent from it QuestionKindArea = "area" // a rectangle on an image QuestionKindRegion = "region" // a box on a live page (external mode) )
Question anchor kinds — the SAME vocabulary comments use (see csv/schema.go), so a quiz question is anchored exactly like every other annotation in prereview rather than inventing a parallel notion of "where this points".
v1 renders `line` and `file`; `text`, `area` and `region` are accepted and stored so that adding their UI later is pure rendering work and never a migration of quizzes already on disk.
const ( // CommentsFileName is the CSV the review server writes (the on-disk source of // truth) and the `prereview comments` reader parses. CommentsFileName = "comments.csv" // EventsFileName is the durable, append-only event log written only in // --agent mode and reset per launch (openStore); each line is a seq-stamped // StreamEvent consumed by `prereview events`. EventsFileName = "events.jsonl" )
Store-layout filenames under .prereview/ that the CLI subcommands resolve. Centralised so the server and the read subcommands (events, comments) agree on one location — mirroring ProcessedFileName / SuggestionFileName.
const ( // AgentRepliesFileName is the agent-written, append-only thread log. Durable // across relaunch (openStore does not reset it), like processed.jsonl. AgentRepliesFileName = "agent-replies.jsonl" // ReviewerRepliesFileName is the server-written thread log (reviewer-reply // phase). Forward-declared so the merge below is complete from the start. ReviewerRepliesFileName = "reviewer-replies.jsonl" )
const ( AuthorAgent = "agent" AuthorReviewer = "reviewer" )
Thread entry authors.
const ( VersionTriggerBaseline = "baseline" VersionTriggerLLMDone = "llm-done" VersionTriggerRollback = "rollback" // FileEdit fires when a reviewed file changes on disk outside a status batch // (the agent edited without going working→done, or wrote no status at all), so // the reviewer still gets a version with the diff of what changed. It carries // no changelog — the diff is the deterministic record. Ordered after LLMDone in // the watcher, so a done-message version wins when both apply (SHA no-op). VersionTriggerFileEdit = "file-edit" )
Version triggers recorded on each Checkpoint. Baseline is the v0 snapshot of the working tree at server start; LLMDone fires when the agent finishes a batch (status working→done); Rollback is written by RestoreVersion so a rollback is itself a new, append-only version (history is never rewritten).
const AppliedFileName = "applied.jsonl"
AppliedFileName is the fixed name of the agent-written, append-only applied-marks file.
const ( // FileHeadRowKey is the row key for the file-head annotation group: file-level // comments, image-area comments, and quiz questions with no line. They render // above the diff rather than in a row, so they had no badge and no way to be // collapsed at all — for comments as much as for quizzes. Giving the group a // key lets it reuse toggleRow / ToggledRows / RowToggled unchanged, instead of // growing a second collapse mechanism beside the one that already works. FileHeadRowKey = "file" )
The two default visibilities a row can have (#165): a row with any OPEN annotation shows its cards inline; a row whose work is all done/accepted collapses them behind the badge. ToggledRows stores which of these the reviewer was flipping away from.
const HiddenSuggestionFileName = "hidden-suggestions.jsonl"
HiddenSuggestionFileName is the fixed name of the server-owned hidden-set file under .prereview/. Durable across launches (openStore does NOT reset it).
const LLMStatusFileName = "llm-status.json"
LLMStatusFileName is the fixed name of the status file the agent writes under .prereview/ and the server watches. It is the INBOUND counterpart to events.jsonl: where events.jsonl carries snapshots server→agent, llm-status.json carries the agent's "what am I doing" echo agent→server, so the review UI can show live status across every open tab. Reset on each launch by openStore.
const LLMStatusPollInterval = 750 * time.Millisecond
LLMStatusPollInterval is how often the server stats the status file. It trades UI latency against wakeups; ~0.75s feels live without busy-polling, and the stat is cheap. Mirrors the polling shape of the existing DONE marker rather than adding an fsnotify dependency.
const PausedMarkerName = "paused"
PausedMarkerName is the .prereview/ marker that records "the agent is paused". Introduced by rollback (#90 Phase 1.3): a restore force-pauses so the agent stops applying while the reviewer re-steers, preventing desync with the LLM's own edit stack. M2's continuous-drain reads this to gate emission; in M1 it drives the paused banner. Reset on launch by openStore, like the other markers.
const ProcessedFileName = "processed.jsonl"
ProcessedFileName is the fixed name of the agent-written, append-only markers file under .prereview/. Durable across launches (openStore does NOT reset it).
const ReenqueuedFileName = "reenqueued.jsonl"
ReenqueuedFileName holds the server-owned "un-processed" tombstones (#119): a reviewer who re-enqueues a done comment appends its id here. A comment counts as done only while its processed-marks OUTNUMBER its re-enqueue marks, so a process → re-enqueue → re-process cycle resolves correctly (each re-enqueue cancels one process mark). Append-only, same one-id-per-line shape as processed.jsonl.
const RevertedFileName = "reverted.jsonl"
RevertedFileName is the agent-written, append-only revert-ack file: one {"id":…} line each time the agent RESTORES an applied suggestion's original text to disk (via `prereview reverted <id>`), the mirror of applied.jsonl. See loadAppliedSet for how the two counts net out to the live "applied" state.
const SessionFileName = "session.json"
SessionFileName records the CURRENT session's review scope inside the store (#171).
The .prereview/ store is keyed to a DIRECTORY (for a single-file review, the file's parent — see resolveTarget), so it is shared by every file ever reviewed from there. The server knows the session's scope in memory, but the agent subcommands don't: they are handed `--out <dir>` and nothing else, so `prereview comments` / `done` would list the whole directory's rows while the reviewer's UI shows one file. This file is how that scope reaches them.
It is a PER-SESSION file: openStore removes it every launch (like the event log, the agent-status file and the paused marker), and the server rewrites it only in single-file mode. That reset is load-bearing — a leftover from an earlier single-file run would otherwise scope a later DIRECTORY review down to one file: the same bug, inverted.
const SuggestionDecisionFileName = "suggestion-decisions.jsonl"
SuggestionDecisionFileName is the fixed name of the server-owned decisions file under .prereview/. Durable across launches (openStore does NOT reset it).
const SuggestionFileName = "suggestions.jsonl"
SuggestionFileName is the fixed name of the agent-written, append-only suggestions file under .prereview/. Durable across launches.
Variables ¶
var ErrVersionTombstone = errors.New("version is a tombstone (file absent at checkpoint)")
ErrVersionTombstone is returned by Restore when the requested version is a tombstone (the file did not exist at that checkpoint) — the caller restores "deleted" by removing the working file rather than writing bytes.
Functions ¶
func AgentRepliesPath ¶ added in v0.23.0
AgentRepliesPath / ReviewerRepliesPath resolve the two sidecars for a store whose CSV lives at csvPath — alongside processed.jsonl, so subcommand + Mount agree.
func AppliedPath ¶ added in v0.23.0
AppliedPath returns the applied-marks path for a store whose CSV lives at csvPath — alongside processed.jsonl / suggestions.jsonl.
func EventsPath ¶ added in v0.22.0
EventsPath returns the event-log path for a store whose CSV lives at csvPath — i.e. <csv dir>/events.jsonl, alongside processed.jsonl and llm-status.json.
func HiddenSuggestionPath ¶ added in v0.20.0
HiddenSuggestionPath returns the hidden-set path for a store whose CSV lives at csvPath — i.e. <csv dir>/hidden-suggestions.jsonl.
func LLMStatusPath ¶ added in v0.16.0
LLMStatusPath returns the status-file path for a store whose CSV lives at csvPath — i.e. <csv dir>/llm-status.json, the same .prereview/ directory that holds events.jsonl. Centralised so the launch reset (store.go), the watcher, and Mount all agree on one location, including single-file reviews where the store dir is the file's parent.
func LoadQuizAnswers ¶ added in v0.25.0
func LoadQuizAnswers(csvPath string) map[string]QuizAnswer
LoadQuizAnswers reads the reviewer's answers for the store whose CSV lives at csvPath, keyed "<quizID>\x00<questionID>". A Choice of -1 (a cleared answer from a retake) is dropped at load, so callers see only live answers.
func NewQuizID ¶ added in v0.25.0
func NewQuizID() string
NewQuizID mints a stable, sortable id when the agent omits one. Exported because the CLI lives in the main package (mirrors NewSuggestionID).
func NewSuggestionID ¶ added in v0.17.0
func NewSuggestionID() string
NewSuggestionID mints a stable, sortable suggestion ID for the `suggest` subcommand when the agent omits one (same scheme as comment IDs). Exported because the CLI lives in the main package.
func ProcessedPath ¶ added in v0.16.0
ProcessedPath returns the markers-file path for a store whose CSV lives at csvPath — i.e. <csv dir>/processed.jsonl, alongside llm-status.json. Centralised so the subcommand (main package), the watcher, and Mount all agree on one location, including single-file reviews where the store dir is the file's parent.
func QuizAnswersPath ¶ added in v0.25.0
QuizAnswersPath returns <csv dir>/quiz-answers.jsonl.
func QuizPath ¶ added in v0.25.0
QuizPath returns <csv dir>/quiz.jsonl. Centralised so the subcommand (main package), the watcher, and Mount all agree on one location.
func QuizThreadID ¶ added in v0.25.0
QuizThreadID is a question's identity for THREADS. Question ids are only unique within their quiz ("q1" appears in every quiz), while a thread target id is global — so the two are joined. Colon-separated because it is typed by a human or an agent on the `prereview reply` command line, unlike the NUL-joined internal answer key.
func ReenqueuePath ¶ added in v0.18.0
ReenqueuePath is the re-enqueue tombstone file alongside processed.jsonl.
func RevertedPath ¶ added in v0.23.0
RevertedPath returns the revert-marks path for a store whose CSV lives at csvPath.
func ReviewerRepliesPath ¶ added in v0.23.0
func SessionPath ¶ added in v0.24.0
SessionPath returns the session-scope file living beside the review's CSV.
func SessionScope ¶ added in v0.24.0
SessionScope reports the file this session's review is scoped to, or "" when the review spans the whole store (a directory / git review, or a store written by a version of prereview that predates this file).
Every failure reads as unscoped. That is the safe direction: an unreadable scope shows the reviewer MORE than they asked for, which is the pre-#171 behaviour — never less, which would look like their comments had vanished.
func SplitQuizThreadID ¶ added in v0.25.0
SplitQuizThreadID reverses QuizThreadID; ok is false when s is not a composite.
func SuggestionDecisionPath ¶ added in v0.17.0
SuggestionDecisionPath returns the decisions-file path for a store whose CSV lives at csvPath — i.e. <csv dir>/suggestion-decisions.jsonl.
func SuggestionPath ¶ added in v0.17.0
SuggestionPath returns the suggestions-file path for a store whose CSV lives at csvPath — i.e. <csv dir>/suggestions.jsonl, alongside processed.jsonl. Centralised so the subcommand (main package), the watcher, and Mount all agree on one location.
func ValidateQuiz ¶ added in v0.25.0
ValidateQuiz enforces the STRUCTURAL half of the question contract. It lives here rather than in the CLI because it is the contract, not a CLI convenience: it must reject a bad quiz identically whether a built-in or a user-authored prompt produced it, and the review package is where that property is tested.
It deliberately does NOT check that line ranges resolve — only the server holds the diff. That half is applyQuiz's job.
Errors name the offending question so a failure is actionable rather than a bare "invalid", matching `prereview done`'s fail-loudly-with-the-id convention.
func VersionsDir ¶ added in v0.18.0
VersionsDir returns the version-store directory for a store whose CSV lives at csvPath — i.e. <csv dir>/versions, alongside events.jsonl and llm-status.json. Centralised (like LLMStatusPath) so launch wiring and any future caller agree on one location, including single-file reviews where the store dir is the file's parent.
func WriteSessionScope ¶ added in v0.24.0
WriteSessionScope records singleFile as this session's scope. A "" scope writes nothing: a directory review narrows nothing, and the absent file is exactly what SessionScope reads as "unscoped".
Types ¶
type Area ¶
type Area struct {
X float64 `json:"x"`
Y float64 `json:"y"`
W float64 `json:"w"`
H float64 `json:"h"`
}
Area is the rectangle for a kind=area comment, expressed as 0..1 fractions of the host image's rendered (== natural-uniformly-scaled) dimensions. Persisted alongside the Comment in the CSV's `area` column as a JSON blob; the LLM consuming the CSV can scale these to pixels using the image's natural dimensions if it wants. Zero-value (W=0, H=0) is the "no area selected" sentinel.
func (Area) Empty ¶
Empty reports whether the area carries no rectangle (zero-value). Used by the controller (validate non-empty before persisting) and the template (only render the pending-overlay when set).
func (Area) JSON ¶
JSON encodes a as the compact JSON blob persisted in the CSV's `area` column. Returns "" for the zero value so the column stays empty for non-area rows.
type BlockAnn ¶ added in v0.23.1
BlockAnn is the per-Markdown-block annotation summary (#165) — the md-view has no gutter, so each block carries ONE state badge at its top-right: Total annotations, coloured yellow when Open (unresolved comment / undecided suggestion), accentuated yellow when Accepted (an accepted-but-unapplied suggestion, nothing open), else green. Open wins over Accepted, matching the diff-row ladder.
type Checkpoint ¶ added in v0.18.0
type Checkpoint struct {
Seq int `json:"seq"`
TS time.Time `json:"ts"`
Trigger string `json:"trigger"`
Files []FileVersion `json:"files"`
// Changelog is the agent's AI-authored "what changed" for this version (#155) —
// a one-line description captured from the agent's status message at the llm-done
// checkpoint. Empty for baseline/rollback and for any batch where the agent wrote
// no message; the panel falls back to a +add/−del stat then. Non-derived input, so
// it IS stored (unlike the diff stat). JSON tag stays "summary" (no migration).
Changelog string `json:"summary,omitempty"`
}
Checkpoint is one append-only entry in the timeline. Seq is its 0-based position (also its index in the log). Files lists every scope file's state at that moment.
type Comment ¶
type Comment struct {
ID string `json:"id"`
File string `json:"file"`
FromLine int `json:"from_line"`
ToLine int `json:"to_line"`
Side string `json:"side"`
// FromCol/ToCol delimit the selected character range for kind=text
// comments: a half-open [FromCol, ToCol) offset in *raw line*
// coordinates (0-based rune count into the line's Content, matching
// the rendered .content textContent — see gitdiff textcontent_test).
// FromCol binds FromLine, ToCol binds ToLine; interior lines are fully
// covered. Zero for every other kind (a line comment covers whole
// lines, so it needs no column offsets).
FromCol int `json:"from_col"`
ToCol int `json:"to_col"`
Body string `json:"body"`
Created time.Time `json:"created"`
// Resolved marks the comment as "addressed; keep as history". The skill
// should act only on unresolved comments. Toggled via ResolveComment.
Resolved bool `json:"resolved"`
// Hidden is a reviewer-only VIEW flag (issue #88): an individually re-hidden
// RESOLVED comment stays out of the diff + overview even when "Show resolved"
// is on, without turning the whole resolved group back off. Set by
// HideComment, cleared in bulk by UnhideAllResolved. Only meaningful for
// resolved comments (commentHiddenFromView ignores it otherwise). Durable in
// the CSV `hidden` column; ignored by the skill/handoff.
Hidden bool `json:"hidden"`
// Processed marks that the agent has addressed this comment (issue #88): it
// drives the "worked on" badge. Derived, NOT persisted in the CSV — set by
// applyProcessed from the agent-written .prereview/processed.jsonl markers on
// every Mount and on each markers-file change (see processed.go).
Processed bool `json:"processed"`
// Draft is the not-yet-enqueued flag (#119): a reviewer's in-progress note
// that is kept OUT of the agent's actionable snapshot until they enqueue it.
// The default (false) means enqueued/active — "save auto-enqueues" — so the
// five comment-creation paths need no change. Persisted INVERTED as the CSV
// `enqueued` column (enqueued == !Draft); legacy rows (no column) read as
// active. Set by materializeDraft (unsaved-text recovery); cleared by
// EnqueueComment. There is no manual "move to draft" — a comment only ever
// becomes a draft via unsaved-text recovery.
Draft bool `json:"draft"`
// Anchor is the content fingerprint captured at create/edit time so
// the comment can be re-located when the file changes (see anchor.go).
// AnchorStatus is "ok" | "moved" | "outdated" (empty == ok for
// legacy pre-migration comments).
Anchor CommentAnchor `json:"anchor"`
AnchorStatus string `json:"anchor_status"`
// Kind is the comment-shape vocabulary: "line" (or "" for legacy /
// pre-migration comments) for line-anchored comments —
// FromLine/ToLine are meaningful — "file" for whole-file comments
// where line numbers are zero and the anchor is empty, "area" for
// image-overlay annotations where Area carries the rectangle, and
// "region" for live-site annotations (--external mode) where Area
// carries the rectangle and URL carries the page.
Kind string `json:"kind"`
// Area is the rectangle for kind=area (fraction of the image) and
// kind=region (fraction of the live page's document) comments.
// Zero-value for line / file rows.
Area Area `json:"area"`
// URL is the proxied page (app-relative: pathname+query, no proxy
// origin since the proxy port is random per run) a kind=region
// comment is anchored to. Empty for every file-based kind.
URL string `json:"url"`
}
Comment is one row in the CSV output (and one entry in state).
func (Comment) AnchorEdited ¶ added in v0.25.0
AnchorEdited reports that exactly the commented lines were edited in place (the recorded text changed but its surrounding context is intact) — a deterministic "likely addressed" hint. Unlike outdated it stays ACTIONABLE: edited is not proof the point was correctly handled, so it is badged, never hidden, until the reviewer resolves it.
func (Comment) AnchorMoved ¶
AnchorMoved reports that the comment was auto-shifted to follow its content after the file changed (purely informational in the UI).
func (Comment) AnchorOutdated ¶
AnchorOutdated reports that re-location could not confidently place the comment — its line numbers no longer point at the intended content and a human (or the skill) must re-anchor or resolve it. File-level and area-level comments never go outdated (no anchor).
func (Comment) IsAreaLevel ¶
IsAreaLevel reports whether this comment overlays an image region (kind="area" with a populated Area). Parallel to IsFileLevel; both share the "no anchor to drift, skip in relocate" contract.
func (Comment) IsFileLevel ¶
IsFileLevel reports whether this comment applies to the whole file rather than a line range. The CSV-side persistence reads `kind=file` and FromLine=0; this method is the single in-process predicate so callers (anchor.relocate, template ranges, skill exports) don't re-implement the test.
func (Comment) IsRegionLevel ¶
IsRegionLevel reports whether this comment annotates a region of a live page in --external mode (kind="region" with a populated Area + URL). Parallel to IsAreaLevel; shares the "no anchor to drift, skip in relocate" contract.
func (Comment) IsTextLevel ¶ added in v0.15.0
IsTextLevel reports whether this comment anchors to a character range (kind="text" with FromCol/ToCol delimiting the selected substring). Unlike file/area/region, a text comment DOES drift — it reuses the line-anchor relocate machinery, so it is not skipped there.
func (Comment) LineSpan ¶
LineSpan returns "L42" for single-line, "L42-L48" for ranges, "file" for whole-file comments, "area" for image-area comments, and "region" for live-site comments — used in the template badge and the composer label, so every kind renders a recognisable span.
func (Comment) QueueState ¶ added in v0.18.0
QueueState classifies a comment for the queue view. Resolved and outdated comments have left the queue (the human closed them / the anchor vanished) and return "" — the panel skips them. The #164 unread-reply reopen is layered ON TOP of this by reopenIfReplied at the state level (this method has no thread access), so a replied-on resolved/outdated/done comment counts as "queued" again.
type CommentAnchor ¶
type CommentAnchor struct {
Text string `json:"text"` // normalized join of FromLine..ToLine
Before []string `json:"before,omitempty"` // <=3 normalized lines before FromLine (source order)
After []string `json:"after,omitempty"` // <=3 normalized lines after ToLine (source order)
// Snippet is the exact selected substring for kind=text comments
// (raw, un-normalized, spanning FromCol..ToCol across FromLine..ToLine).
// Empty for every other kind. Used to re-locate the character range
// after edits and to disambiguate a substring that repeats on a line.
Snippet string `json:"snippet,omitempty"`
}
CommentAnchor is the content fingerprint captured when a comment is created/edited. Serialized as the opaque `anchor` JSON CSV column — the skill must not parse it.
func (CommentAnchor) Empty ¶
func (a CommentAnchor) Empty() bool
Empty reports whether there is nothing to re-locate against (legacy pre-migration comments, or a capture that found no content).
func (CommentAnchor) JSON ¶
func (a CommentAnchor) JSON() string
JSON serializes the anchor for the opaque `anchor` CSV column. Empty anchor → "" (keeps legacy/zero rows clean).
type EventStream ¶
type EventStream struct {
// contains filtered or unexported fields
}
EventStream emits the --agent mode JSON event log: one JSON object per line, written to both a live channel (stdout, polled by the consuming LLM) and an append-only durable mirror (.prereview/events.jsonl, for replay after a context reset). It is the single writer of these events — controller actions call it under its mutex, so emitted lines are naturally ordered and carry a monotonic seq. A nil *EventStream is never emitted to; callers gate on agent mode before constructing one.
func NewEventStream ¶
func NewEventStream(out io.Writer, filePath string) *EventStream
NewEventStream returns an emitter targeting out (live) and filePath (durable mirror). filePath may be "" to disable the durable mirror (tests).
func (*EventStream) EmitEnd ¶ added in v0.22.0
func (e *EventStream) EmitEnd(ts time.Time) error
EmitEnd emits the single terminator — the only event the consumer loop should treat as "stop".
func (*EventStream) EmitReady ¶
func (e *EventStream) EmitReady(repo, csvPath string, paused, skillUpdated bool, ts time.Time) error
EmitReady announces the session is live. Emitted once, after the READY/REPO/STORE stdout preamble, so the preamble parse is never interleaved with JSON.
func (*EventStream) EmitSnapshot ¶ added in v0.22.0
func (e *EventStream) EmitSnapshot(comments []Comment, suggestions []Suggestion, decided map[string]SuggestionDecision, threadByID map[string][]ThreadEntry, applied map[string]bool, quizzes []StreamQuiz, paused bool, ts time.Time) error
EmitSnapshot emits a full actionable snapshot — one per queue mutation (or the final EndSession flush): the open comments AND the reviewer's decisions on the LLM's suggestions. Both snapshots are always non-nil slices so their keys are always present (`[]` when nothing is actionable). decided is the fingerprint-matched decision map (state.DecisionsBySuggestion) — only decided, non-outdated suggestions ship.
type FileHistoryEntry ¶ added in v0.18.0
type FileHistoryEntry struct {
Seq int
TS time.Time
Trigger string
SHA string
Changelog string // the checkpoint's changelog (#155), for the llm-done trigger
}
FileHistoryEntry is one point in a single file's timeline: the checkpoint at which the file took on content SHA. Emitted only for checkpoints where the file actually changed (or first appeared).
type FileRef ¶ added in v0.18.0
FileRef identifies one file to snapshot: Path is the stable timeline key (repo-relative, slash-separated) and AbsPath is where to read the bytes now. Keeping the two separate lets the timeline key stay stable across absolute path differences (single-file review stores by basename, etc.).
type FileVersion ¶ added in v0.18.0
FileVersion is one file's recorded state within a Checkpoint. SHA == "" is a tombstone: the file was absent (deleted) when the checkpoint was taken. A file's previous content is derivable from the prior checkpoint, so it isn't stored here.
type HiddenSuggestion ¶ added in v0.20.0
type HiddenSuggestion struct {
SuggestionID string `json:"suggestion_id"`
Fingerprint string `json:"fingerprint"`
}
HiddenSuggestion marks one suggestion hidden from view, pinned to the content it was hidden against so a later revision un-hides it automatically.
type KeyBinding ¶ added in v0.14.0
type KeyBinding struct {
// Keys are the raw KeyboardEvent.key values that trigger the action. Each
// key emits its own hidden binding element (lvt-key matches one key), so a
// shortcut with both a letter and an arrow (e.g. {"j", "ArrowDown"}) wires
// two elements to the same Action.
Keys []string
// Display is how the keys read in the help overlay (e.g. "j ↓"), kept
// separate from Keys because the raw event.key value ("ArrowDown") isn't
// what we want to show a human.
Display string
// Action is the controller method dispatched on keydown. Empty means the
// shortcut is handled elsewhere and only documented here: Enter saves via
// the composer form, and Esc has its own global window binding in the
// template (a child of <body> so it works in both repo and external mode,
// without the skip-when-typing guard so it cancels the composer
// mid-typing). Help-only rows emit no hidden element.
Action string
// Label describes what the shortcut does, shown in the help overlay.
Label string
// Button, when set, is the form-button name (e.g. "addComment") that this
// shortcut also triggers indirectly, so KeyHint can surface the key on that
// button even though the shortcut isn't wired as a window Action. Used for
// the composer's Save (Mod+Enter → the composer form) and Cancel (Esc → the
// global clearSelection binding): both fire from their own template bindings,
// not from the keyBindings loop, so their rows carry Button instead of Action.
Button string
// StreamOnly limits a binding to --agent mode. An agent-only shortcut is
// filtered out of the live window bindings AND the help overlay when the
// session isn't in agent mode, so a repo-only reviewer never sees a key for a
// control (the agent-queue Pause/Resume) that doesn't exist for them.
StreamOnly bool
}
KeyBinding is one row of the keyboard map. It is the single source of truth for a shortcut: the template renders BOTH the hidden `lvt-on:window:keydown` elements (for bindings with a non-empty Action) and the rows of the keyboard-help overlay from the same slice, so the live bindings and their documentation can never drift apart.
type LLMStatus ¶ added in v0.16.0
type LLMStatus struct {
// State is "working" while the agent applies a snapshot batch, "done" once it
// has finished and reported back. "" (missing/blank) means idle.
State string `json:"state"`
// Message is an optional human-readable detail shown in the status pill,
// e.g. "Applying 5 comments".
Message string `json:"message,omitempty"`
// UpdatedAt is an RFC3339 timestamp the agent stamps on each write. The
// watcher uses the file's mtime for change detection; this is a secondary
// human/debug field.
UpdatedAt string `json:"updated_at,omitempty"`
}
LLMStatus is the inbound signal the agent writes to <STORE>/llm-status.json so the running review server can show what the agent is doing. It is the reverse of the outbound event stream (stream.go): the agent is the writer, the server the reader.
type PrereviewController ¶
type PrereviewController struct {
// RepoPath, Base, CSVPath are set once by main.go and are read-only.
// CSVWriter is a goroutine-safe serializer over CSVPath.
RepoPath string
Base string
CSVPath string
// UIPrefsPath is the per-user view-prefs file (see uiprefs.go): the durable
// home for theme/mode/focus/file-view/raw/show-resolved so they survive a
// server relaunch, not just a reload. Set once by main.go; "" disables
// durable prefs (session-only) and is never an error. NOT per-repo — these
// prefs mean the same thing in every repo.
UIPrefsPath string
// NoGit is true when the path was a single file or a directory with no
// .git: the file list and per-file diff are synthesized from the
// filesystem (gitdiff.ListFilesNoGit / LoadDiffNoGit) instead of git,
// the base picker is suppressed, and Base is unused. SingleFile, when
// non-empty (single-file mode), is the only reviewable file —
// RepoPath is its parent directory.
NoGit bool
SingleFile string
// ExternalMode is true under `prereview --external <url>`: the session
// fronts a live local website through the reverse proxy instead of a
// repo. ProxyBaseURL is the second-origin URL the UI iframes; TargetURL
// is the upstream shown to the user. Set once by main.go.
ExternalMode bool
ProxyBaseURL string
TargetURL string
// Version is the build version (main.version; "dev" for source
// builds) surfaced into state for the footer.
Version string
CSVWriter *csv.Writer
// AgentMode is true under `prereview --agent`: prereview streams the review
// queue as JSON events (stdout + .prereview/events.jsonl) and the UI shows
// the Queue (Pause/Resume) + "End session" (which emits the terminating
// end event) instead of "Quit". Set once by main.go.
AgentMode bool
// PromptsDir is the user's "ask for suggestions" prompt overlay directory
// (~/.config/prereview/prompts), set once by main.go. Read every Mount and
// merged with the embedded built-ins in LoadPrompts. "" = built-ins only.
PromptsDir string
// QuizzesDir is the user overlay for #191 quiz prompts
// (~/.config/prereview/quizzes). Read every Mount and merged with the embedded
// built-ins in LoadQuizPrompts. "" = built-ins only. Separate from PromptsDir
// because the two libraries answer with different verbs.
QuizzesDir string
// Emitter is the agent-mode JSON event log writer (stdout +
// .prereview/events.jsonl). Non-nil only in agent mode; the emit path and
// EndSession guard on nil so non-agent sessions emit nothing.
Emitter *EventStream
// Versions is the artifact version store (#90): content-addressed snapshots
// of the reviewed files as the LLM edits them on disk, enabling view / diff /
// rollback of uncommitted work. Non-nil only in repo / single-file mode
// (external mode has no file scope). Every caller nil-guards it, so a failed
// init just disables versioning rather than breaking the review.
Versions *VersionStore
// ShutdownReq receives a struct{} when the user clicks Quit. main.go
// listens for it and triggers graceful HTTP shutdown.
ShutdownReq chan<- struct{}
// contains filtered or unexported fields
}
PrereviewController holds singleton dependencies. State is cloned per session by the framework — never store per-user data on the controller.
func (*PrereviewController) AcceptSuggestion ¶ added in v0.17.0
func (c *PrereviewController) AcceptSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
AcceptSuggestion records an "accept" AND auto-rejects every OTHER suggestion in the same artifact-area group (#117): alternatives for the same text are mutually exclusive, like a radio button, so the reviewer no longer rejects the rest by hand. The sibling rejects override whatever verdict they had and are marked Auto (so clearing the accept re-opens the group — see ClearSuggestionDecision). All batched into one persist.
func (*PrereviewController) AddComment ¶
func (c *PrereviewController) AddComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
AddComment validates body+selection, appends a Comment, writes the CSV atomically, and clears selection + draft for the next round. Save enqueues (the comment is actionable); the draft path is materializeDraft.
func (*PrereviewController) AnswerQuestion ¶ added in v0.25.0
func (c *PrereviewController) AnswerQuestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
AnswerQuestion records the reviewer's choice for one question and reveals its explanation. Answers are the reviewer's, so they go in the server-owned file — never into the agent's append-only quiz.jsonl.
An out-of-range choice is rejected rather than stored: the index is used to read Options in the template, so a bad one would render nothing (or panic a future caller) with no clue why.
func (*PrereviewController) CancelReply ¶ added in v0.23.0
func (c *PrereviewController) CancelReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CancelReply closes the reply form without posting.
func (*PrereviewController) CheckpointBaseline ¶ added in v0.18.0
func (c *PrereviewController) CheckpointBaseline()
CheckpointBaseline records the v0 snapshot of the review scope. Exported for the launch wiring (server.go) to call once at startup, before the agent edits anything. Idempotent-ish: a second call with an unchanged scope is skipped by the store, so it never duplicates the baseline.
func (*PrereviewController) ClearFlash ¶ added in v0.14.0
func (c *PrereviewController) ClearFlash(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ClearFlash dismisses the transient status toast (auto-clicked after a few seconds, or via the toast's manual dismiss button).
func (*PrereviewController) ClearSelection ¶
func (c *PrereviewController) ClearSelection(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ClearSelection wipes the line selection and any draft. Bound to a "Cancel" button next to the composer and to ESC keydown on the body.
func (*PrereviewController) ClearSuggestionDecision ¶ added in v0.17.0
func (c *PrereviewController) ClearSuggestionDecision(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ClearSuggestionDecision removes a recorded decision (undo), returning the suggestion to undecided. If the cleared decision was an ACCEPT, the whole group it auto-rejected re-opens too (#117): clear every Auto reject on a sibling in the same group, so undoing the accept restores the group to undecided rather than leaving it permanently all-rejected. Batched into one persist.
func (*PrereviewController) CloseFiles ¶
func (c *PrereviewController) CloseFiles(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CloseFiles unconditionally hides the file drawer. Distinct from ToggleFiles because the backdrop tap should only close, never open.
func (*PrereviewController) CloseMoreMenu ¶
func (c *PrereviewController) CloseMoreMenu(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CloseMoreMenu is the explicit close action — bound to the menu backdrop so tapping outside dismisses without toggling the open state to "true" on a subsequent click.
func (*PrereviewController) CloseSearch ¶ added in v0.16.0
func (c *PrereviewController) CloseSearch(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CloseSearch hides the palette (the close button; Esc also closes it via ClearSelection). Keeps the query so a reopen resumes where the user left off.
func (*PrereviewController) CloseTOC ¶
func (c *PrereviewController) CloseTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CloseTOC dismisses the mobile TOC overlay. Bound to two things: the backdrop tap (close-without-jump) and the click on a heading link inside the overlay (close-and-jump — the <a href="#…"> performs the native anchor scroll, this action closes the overlay in the same gesture). Browser-level anchor navigation is unaffected because event-delegation.ts only preventDefault's submit and drag events.
func (*PrereviewController) CursorDown ¶ added in v0.14.0
func (c *PrereviewController) CursorDown(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CursorDown / CursorUp move the keyboard line cursor through the diff (bound to ArrowDown / ArrowUp). The cursor line button is highlighted, scrolled into view, and focused, so Enter activates it (→ the line composer). j/k still switch files.
func (*PrereviewController) CursorUp ¶ added in v0.14.0
func (c *PrereviewController) CursorUp(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
func (*PrereviewController) CycleScheme ¶ added in v0.14.0
func (c *PrereviewController) CycleScheme(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CycleScheme advances the curated color scheme (the Theme axis, issue from the UI-overhaul plan), cycling through gitdiff.Schemes in registry order. Persisted per-user; the page re-renders with the new data-scheme attribute and the cascade re-skins chrome + diff — no JS, no CSS refetch (/syntax.css carries every scheme). Like CycleTheme, leaves the overflow menu open so a click from inside it can cycle again in place.
func (*PrereviewController) CycleTheme ¶ added in v0.14.0
func (c *PrereviewController) CycleTheme(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
CycleTheme advances the Light/Dark/System color mode (issue #60), cycling System → Light → Dark → System. Persisted per-user; the page re-renders with the new data-mode attribute (omitted for System) and the cascade does the rest — no JS, no CSS refetch (/syntax.css already carries both modes). The overflow menu is left open so a click from inside it can cycle again in place; the toolbar button is the primary control.
func (*PrereviewController) DeleteComment ¶
func (c *PrereviewController) DeleteComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
DeleteComment removes the named comment, rewrites the CSV, and stashes the deleted comment in state.LastDeletedComment so the user can undo for the remainder of the session (or until another mutation).
func (*PrereviewController) DiffVersion ¶ added in v0.18.0
func (c *PrereviewController) DiffVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
DiffVersion shows a prior version diffed against the CURRENT working-tree file ("Diff vs current") — a read-only view mode like ViewVersion, but the pane renders the line diff (via gitdiff.DiffContents, a pure-Go Myers diff over the two blobs — no git). A tombstone (the file didn't exist at that version) diffs as an all-addition against nothing. No disk change.
func (*PrereviewController) DismissQuizNav ¶ added in v0.25.0
func (c *PrereviewController) DismissQuizNav(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
DismissQuizNav puts the navigator away for this session.
func (*PrereviewController) DismissUndo ¶ added in v0.15.0
func (c *PrereviewController) DismissUndo(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
DismissUndo clears the undo affordance WITHOUT restoring the comment — the deletion stands. This backs the undo toast's ✕ button, the manual counterpart to UndoDelete's restore. Mirror of ClearFlash: a one-field clear with no persistence (the CSV already reflects the deletion).
func (*PrereviewController) EditComment ¶
func (c *PrereviewController) EditComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
EditComment seeds the composer with an existing comment's body + line range so the user can rewrite it. The original comment stays in state.Comments — AddComment detects EditingCommentID and updates in place rather than appending. This keeps Cancel non-destructive: if the user opens Edit and changes their mind, the original survives.
func (*PrereviewController) EndSession ¶
func (c *PrereviewController) EndSession(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
EndSession is the agent-mode terminator. It first flushes a final snapshot snapshot — so comments left since the last emitted snapshot still reach the consumer (dedup-by-id makes a redundant snapshot harmless, and the alternative would silently strand them in the CSV but never the stream) — then emits the single end event (the only event the consumer loop treats as "stop") and shuts the server down on the same delay as Quit, so the framework renders the "session ended" banner before the WebSocket is torn down. The LLM's background job completing is the second, redundant terminator.
func (*PrereviewController) EnqueueComment ¶ added in v0.18.0
func (c *PrereviewController) EnqueueComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
EnqueueComment is the per-card "Enqueue" / "Re-enqueue" action.
func (*PrereviewController) ExitVersionView ¶ added in v0.18.0
func (c *PrereviewController) ExitVersionView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ExitVersionView leaves the read-only historical view and returns to the live diff. No disk change.
func (*PrereviewController) FocusComment ¶
func (c *PrereviewController) FocusComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
FocusComment marks a region annotation as the one to locate: its on-page pin renders highlighted and the client (via the beacon) navigates the iframe to its page + scrolls it into view. FocusSeq bumps every call so re-tapping the same annotation re-fires the client even with an unchanged id.
func (*PrereviewController) HideComment ¶ added in v0.16.0
func (c *PrereviewController) HideComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
HideComment individually re-hides a RESOLVED comment (issue #88): it stays out of the diff + overview even while "Show resolved" is on, without turning the whole resolved group back off. A view-only flag — the comment is untouched otherwise (still resolved, still in the CSV, already excluded from the handoff). Recovered in bulk via UnhideAllResolved. Mirrors ToggleResolved's persist-and-rollback shape.
func (*PrereviewController) HideSuggestion ¶ added in v0.20.0
func (c *PrereviewController) HideSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
HideSuggestion hides a single suggestion from view (pinned to its current content, so a later revision un-hides it). A pure declutter — the suggestion stays open and still reaches the LLM on hand-off.
func (*PrereviewController) HideSuggestionGroup ¶ added in v0.20.0
func (c *PrereviewController) HideSuggestionGroup(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
HideSuggestionGroup hides every alternative in the same artifact-area group as the given suggestion (#117) in one atomic write — the "hide these alternatives" affordance. A lone suggestion (no group) hides just itself.
func (*PrereviewController) JumpToComment ¶
func (c *PrereviewController) JumpToComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
JumpToComment closes the all-comments view, selects the comment's file, and sets ScrollToCommentID so the framework's `lvt-fx:scroll="into-view"` directive on the matching inline comment scrolls it into view on the next render. Pure declarative wiring — no custom app-level JS.
func (*PrereviewController) JumpToQuizLine ¶ added in v0.25.0
func (c *PrereviewController) JumpToQuizLine(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
JumpToQuizLine leaves the quiz and scrolls to the lines a question cites, so "go look at the code" is one tap. It deliberately closes the quiz view (like every other jump handler) rather than trying to show both: the reviewer asked to see the code, and toggling back is one tap the other way.
func (*PrereviewController) JumpToReadFrontier ¶ added in v0.20.0
func (c *PrereviewController) JumpToReadFrontier(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
JumpToReadFrontier is the "resume reading" action: a one-shot scroll to the furthest-read element, so the reviewer lands back where they left off.
func (*PrereviewController) JumpToSearchResult ¶ added in v0.16.0
func (c *PrereviewController) JumpToSearchResult(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
JumpToSearchResult selects the hit's file and, for a content hit, reveals the full raw file and scrolls to the matched line (RevealFile + CursorKey — mirrors JumpToComment). A filename hit (no line) just opens the file. Closes the palette.
func (*PrereviewController) JumpToSuggestion ¶ added in v0.23.0
func (c *PrereviewController) JumpToSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
JumpToSuggestion selects the file an accepted suggestion lives in, from its row in the queue panel (#159). It stops at file selection — suggestions have no per-item scroll target today (unlike ScrollToCommentID), so the reviewer lands on the file with the inline suggestion box visible; a precise scroll can ride along with the collapse-to-badge work.
func (*PrereviewController) LLMStatusChanged ¶ added in v0.16.0
func (c *PrereviewController) LLMStatusChanged(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
LLMStatusChanged is dispatched by the watcher (via Session.TriggerAction) to every open tab when the status file changes. Fan-out re-runs this handler against each tab's OWN state, so it must re-read the file itself rather than rely on any payload.
func (*PrereviewController) Mount ¶
func (c *PrereviewController) Mount(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
Mount runs on every HTTP GET and WebSocket connect. It rebuilds the file list from `git diff` so the user sees current state without restarting the binary.
func (*PrereviewController) NavigateToHeading ¶
func (c *PrereviewController) NavigateToHeading(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
NavigateToHeading is the server-side bookkeeping for a TOC heading link click. It dismisses both the mobile TOC overlay AND the all-comments overview, then records ScrollToHeadingID so the framework's `lvt-fx:scroll="into-view"` directive on the matching MarkdownBlock scrolls the section into view on the next render — the same declarative pattern JumpToComment uses for comments.
Fixes issue #12: previously the TOC link only closed the overlay (closeTOC); from inside all-comments view the heading was never in the DOM, so the native anchor scroll missed and the user stayed stuck on the comments overview.
data-id on the link supplies the slugified heading id (matches the `id="..."` goldmark's WithAutoHeadingID writes into the rendered HTML and that ExtractHeadings reads back).
func (*PrereviewController) NextComment ¶ added in v0.14.0
func (c *PrereviewController) NextComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
NextComment jumps to the next comment in the all-comments order, wrapping from the last back to the first. Keyboard counterpart to clicking a comment in the overview.
func (*PrereviewController) NextFile ¶
func (c *PrereviewController) NextFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
NextFile selects the next file in state.Files relative to SelectedFile. Wraps to the first file from the last. If no file is selected, picks the first file. Falls back to no-op for an empty file list.
func (*PrereviewController) OnConnect ¶ added in v0.16.0
func (c *PrereviewController) OnConnect(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
OnConnect captures the connecting group's Session so the background llm-status watcher can fan updates out to every open tab. It also refreshes the status fields from the file: LLMState is not persisted, so the restored connect-time state has it blank, and the connect render must reflect the current file (the same value the initial GET/SSR rendered) or a tab that opened mid-work would desync from its own server-rendered DOM.
func (*PrereviewController) OpenFileComment ¶
func (c *PrereviewController) OpenFileComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
OpenFileComment opens the composer in "comment on whole file" mode. Distinct from SelectLine in that no line range is involved; the resulting Comment persists with Kind="file", FromLine=0, ToLine=0. Clears any pending line selection / edit / re-anchor so the composer renders once at the file head rather than twice. Mirrors the markdown raw-toggle's "close the overflow menu" behaviour so mobile clicks don't leave the menu hanging open over the composer.
func (*PrereviewController) OpenReply ¶ added in v0.23.0
func (c *PrereviewController) OpenReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
OpenReply opens the inline reply form on the given comment/suggestion, draft empty.
func (*PrereviewController) OpenSearch ¶ added in v0.16.0
func (c *PrereviewController) OpenSearch(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
OpenSearch shows the search palette. Bound to the Mod+k keybinding and the toolbar magnifier. Closes the overflow menu so the two overlays don't stack. Leaves SearchQuery/SearchHits intact so reopening shows the last search.
func (*PrereviewController) OpenTOC ¶
func (c *PrereviewController) OpenTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
OpenTOC opens the mobile Table-of-Contents overlay. Bound to the "Table of contents" entry in the 3-dots menu, so the menu must close at the same time — otherwise the dropdown stays drawn over the overlay it just summoned. Desktop never renders this entry; the TOC is a permanent right sidebar there.
func (*PrereviewController) PickPrompt ¶ added in v0.23.0
func (c *PrereviewController) PickPrompt(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
PickPrompt opens a file-level comment PRE-FILLED with a chosen "ask for suggestions" prompt (#147): it mirrors OpenFileComment, then seeds DraftBody with the prompt's body, so the reviewer sees the instruction in the composer, can tweak/scope it, and hits Save — creating an ordinary kind=file comment the agent answers with `prereview suggest`. An unknown slug is a no-op (a stale picker click).
func (*PrereviewController) PostReply ¶ added in v0.23.0
func (c *PrereviewController) PostReply(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
PostReply records the reviewer's reply on a comment/suggestion (#149): appends a reviewer ThreadEntry to the server-owned reviewer-replies.jsonl, reloads the threads so the card shows it immediately, and re-arms the snapshot emit so the agent's watch sees it. It does NOT touch Resolved — a reply is a steer, not a reopen; the unread-reply overlay (threadActionable) re-surfaces a resolved comment on the wire.
func (*PrereviewController) PrevComment ¶ added in v0.14.0
func (c *PrereviewController) PrevComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
PrevComment jumps to the previous comment, wrapping from the first to the last.
func (*PrereviewController) PrevFile ¶
func (c *PrereviewController) PrevFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
PrevFile selects the previous file. Wraps to the last file from the first.
func (*PrereviewController) Quit ¶
func (c *PrereviewController) Quit(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
Quit gracefully shuts the HTTP server down. The actual shutdown is dispatched on a delay so the framework gets to render `Quitting=true` back to the client before the WebSocket is torn down — otherwise the browser sees a sudden disconnect with no UI feedback.
func (*PrereviewController) ReanchorComment ¶
func (c *PrereviewController) ReanchorComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ReanchorComment starts re-placing an outdated comment: it jumps to the comment's file and arms ReanchorCommentID, but deliberately does NOT pre-seed the (stale) line selection — the user must pick the new location. The body is preserved in the composer. The next Save (AddComment, ReanchorCommentID branch) re-points the comment and re-captures its content anchor. This is the only sanctioned way to move an outdated comment; Edit is hidden for outdated comments precisely so it can't silently re-anchor against stale content.
func (*PrereviewController) ReenqueueComment ¶ added in v0.18.0
func (c *PrereviewController) ReenqueueComment(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ReenqueueComment is the queue-panel ↺ action — the same unified path.
func (*PrereviewController) RefreshDiff ¶ added in v0.16.0
func (c *PrereviewController) RefreshDiff(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RefreshDiff rebuilds the file list + diff (picking up the agent's edits, since loadDiffCached invalidates on mtime) and re-anchors comments, then clears the pending-refresh nudge. It reuses Mount so the rebuild logic lives in exactly one place.
func (*PrereviewController) RejectSuggestion ¶ added in v0.17.0
func (c *PrereviewController) RejectSuggestion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RejectSuggestion records a "reject" verdict.
func (*PrereviewController) ReportViewport ¶ added in v0.20.0
func (c *PrereviewController) ReportViewport(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ReportViewport records the client's read-progress report for the selected file: bottomKey advances the high-water mark (furthest new-side line seen), topKey becomes the scroll-restore target for the next time this file is opened. Both are per-file. A report with no selected file, or with unparseable keys, is a no-op. Never emits or persists to disk — pure session view-state.
func (*PrereviewController) RequestQuiz ¶ added in v0.25.0
func (c *PrereviewController) RequestQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RequestQuiz asks the agent for a quiz about the selected file, by SAVING a file-level comment whose body is the quiz prompt.
It deliberately sends immediately rather than pre-filling the composer the way PickPrompt does (#147). "Quiz me" should be one tap — this tool is mostly used from a phone — and unlike a suggestions prompt there is rarely anything to tweak. The cost is that the request is VISIBLE in the reviewer's own queue: an earlier design hoped to hide it with Comment.Hidden, but that flag only applies to RESOLVED comments ("Hidden is meaningless on them", state.go), so there is no way to hide it and no reason to invent one. A saved Prompt comment already behaves exactly this way, and the visible row doubles as a record of what was asked, with the agent's reply threaded under it.
func (*PrereviewController) RequestRevert ¶ added in v0.23.0
func (c *PrereviewController) RequestRevert(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RequestRevert asks the agent to UNDO an already-applied accept (#159 M4.2): it sets the Revert flag on the suggestion's accept decision, so the next snapshot carries it to the agent as verdict="revert". The agent restores the original text on disk and acks with `prereview reverted <id>` — that drops the suggestion out of the applied set, at which point DecisionsBySuggestion filters the now-revert-complete decision back to undecided. prereview still never writes the file itself; the agent does.
func (*PrereviewController) RestoreVersion ¶ added in v0.18.0
func (c *PrereviewController) RestoreVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RestoreVersion rolls the selected file back to a prior version — the payoff of the whole milestone. It performs the anti-desync sequence in order: (1) force-pause the agent so it stops applying while the reviewer re-steers (this is what prevents desync with the LLM's own /rewind checkpoint stack), (2) write the version's bytes to disk (or delete the file for a tombstone), (3) record a new rollback checkpoint so history stays append-only (never rewritten), and (4) return to the live diff + nudge a refresh. The rollback is itself a new version, so it too is undoable.
func (*PrereviewController) ResumeAgent ¶ added in v0.18.0
func (c *PrereviewController) ResumeAgent(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ResumeAgent clears the pause and releases the drain: the batch accumulated while paused ships on the next emit. Bound to the "Resume — send batch" button on the batching banner (the manual Pause is the only thing that sets it now — rollback no longer force-pauses, see RestoreVersion).
func (*PrereviewController) RetakeQuiz ¶ added in v0.25.0
func (c *PrereviewController) RetakeQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
RetakeQuiz clears every answer for the current quiz so the reviewer can run it again — retrieval practice works by repetition, and a quiz you can only take once is a test, not a study tool.
func (*PrereviewController) SaveDraft ¶
func (c *PrereviewController) SaveDraft(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SaveDraft updates DraftBody as the user types. Bound to the textarea's change event so the draft survives reconnect (state has lvt:"persist").
func (*PrereviewController) SaveReplyDraft ¶ added in v0.23.0
func (c *PrereviewController) SaveReplyDraft(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SaveReplyDraft autosaves the reply textarea so it survives a reconnect.
func (*PrereviewController) SelectBlock ¶
func (c *PrereviewController) SelectBlock(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectBlock selects a whole source block in one shot: a rendered- Markdown block, a region drawn over the rendered-HTML preview, or a region drawn over the code view (issue #26 region comments). A block IS a range, so unlike SelectLine's two-click anchor/extend, this sets the full source line span at once — for the previews, data-from/data-to are the real source lines the client's region-select directive resolved the drawn box to. The existing composer/AddComment flow then anchors the comment to those lines, so it round-trips with the raw view and the CSV unchanged.
`side` is optional and defaults to "new" (rendered Markdown/HTML have no diff sides; deep-link line numbers are post-diff). The code-view region path passes the side of the box's first matched row so a comment on a deleted ("old") row anchors correctly.
A block, like a line, is ONE conversation (#174): CLICKING a rendered block that already carries a comment OPENS that thread instead of composing a second one on top of it — see openThreadOnBlock. A brand-new comment on that block is still reachable by selecting a phrase inside it (kind=text, data-surface="block").
func (*PrereviewController) SelectFile ¶
func (c *PrereviewController) SelectFile(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectFile loads the diff for the file path supplied as a hidden form input (`name="path"`). Resets any line selection. Auto-closes the mobile file drawer so tapping a file goes straight to the diff — the user doesn't have to also close the drawer manually.
func (*PrereviewController) SelectImageArea ¶
func (c *PrereviewController) SelectImageArea(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectImageArea opens the composer in "area" mode with a captured rectangle. Fired by the client's lvt-fx:area-select directive on pointerup with the final 0..1-fraction coords. Sets CommentMode + SelectionArea and clears any pending line/file selection so the area composer is the only one visible.
func (*PrereviewController) SelectLine ¶
func (c *PrereviewController) SelectLine(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectLine implements two-click range selection. data-line and data-side arrive from <button lvt-on:click="selectLine" data-line="N" data-side="new">.
- First click on a line: anchor = end = N.
- Second click on a different line: end = N (range complete).
- Third click: reseat as a new anchor.
Side is captured on the first click and locked thereafter so a range can't accidentally span sides of the diff.
EXCEPT when the line already carries a comment (#174): a line is ONE conversation, so clicking it OPENS that thread rather than starting a second comment on top of it — see openThreadOnLine. Further input belongs in the thread as a reply. A separate comment on an already-commented line is still reachable by selecting text on it (kind=text).
func (*PrereviewController) SelectRegion ¶
func (c *PrereviewController) SelectRegion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectRegion arms the composer for a region annotation on the current proxied page: a rectangle in document-fraction coordinates (computed client-side from the drag box + the beacon's scroll/document metrics), dispatched from the overlay's lvt-fx:region-select="selectRegion" data-surface="page" directive. Mirrors SelectImageArea but anchors to CurrentURL instead of a file.
func (*PrereviewController) SelectText ¶ added in v0.15.0
func (c *PrereviewController) SelectText(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SelectText opens the composer for a CHARACTER range (a word / phrase / span across lines), dispatched from the client's lvt-fx:text-select directive when a native selection settles inside the diff. Payload: from_line/from_col + to_line/to_col (rune offsets, doc-ordered so from precedes to), side, and the exact selected text. It reuses the line composer's placement — SelectionAnchor /End are the line span, so the existing SelectionEndMax gate renders the composer under the range's last line — and adds the columns + snippet that distinguish a kind=text comment from a whole-line one.
func (*PrereviewController) SetBase ¶
func (c *PrereviewController) SetBase(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetBase changes the comparison ref. The picker is a dropdown of refs we enumerated this Mount (HEAD~N presets + local/remote branches), so the value is already a valid ref. The rev-parse check stays as cheap defense against a race (a branch deleted between Mount and select); on a miss we just no-op and keep the current base.
On success, rebuilds the file list against the new base. If the previously selected file no longer exists in the new file list, SelectedFile is cleared.
func (*PrereviewController) SetFileFilter ¶
func (c *PrereviewController) SetFileFilter(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetFileFilter updates the search filter applied to the file drawer. Bound to the search input via lvt-on:input with a debounce modifier.
func (*PrereviewController) SetFileViewMode ¶
func (c *PrereviewController) SetFileViewMode(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetFileViewMode is the setter counterpart of ToggleFileView for the desktop radio group (Diff / File). Reads form field `view`; "file" → FileView true, anything else (incl. "diff") → false.
func (*PrereviewController) SetHTMLView ¶
func (c *PrereviewController) SetHTMLView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetHTMLView is the idempotent setter behind the HTML Preview/Raw radio group. Mirrors SetMarkdownView: "raw" → line view, anything else → iframe preview.
func (*PrereviewController) SetMarkdownView ¶
func (c *PrereviewController) SetMarkdownView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetMarkdownView is the idempotent setter behind the desktop radio group (Rendered / Raw). Reads form field `view`; anything other than "raw" resolves to rendered. Unlike ToggleRawMarkdown, clicking the already-active radio is a no-op for state — the value reflects the final mode, not a flip.
func (*PrereviewController) SetProxyURL ¶
func (c *PrereviewController) SetProxyURL(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetProxyURL records the proxied page the iframe is currently showing, reported by the injected beacon on load / pushState / popstate. It scopes which region annotations render (RegionComments) and where new ones anchor. Navigating to a different page drops any in-progress region composer — it belonged to the page we just left.
func (*PrereviewController) SetSearchQuery ¶ added in v0.16.0
func (c *PrereviewController) SetSearchQuery(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetSearchQuery updates the live query and recomputes the hit list. Bound to the palette input via lvt-on:input + a debounce modifier (mirrors SetFileFilter).
func (*PrereviewController) SetURLHash ¶
func (c *PrereviewController) SetURLHash(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
SetURLHash dispatches from the client's url-hash directive when `location.hash` changes (page-load with a hash, address-bar edit, back-button, permalink click). Parses the hash via gitdiff.ParseHash and updates state.SelectedFile + selection range + anchor; loads the diff if the path changed and resolves to a known file. Tolerant of bogus hashes (an unrelated `#confirm-delete-xyz` dialog hash resolves to a non-existent file and is silently dropped) — the existing dialog/popover/details hash machinery in setupHashLink handles those independently.
On a successful path match, also clears any in-progress composer state from the previous file (mirror of SelectFile), since the user just navigated.
func (*PrereviewController) ShowHiddenSuggestions ¶ added in v0.20.0
func (c *PrereviewController) ShowHiddenSuggestions(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ShowHiddenSuggestions clears the SELECTED file's hides (the "show N hidden" recovery), bringing them back into view. Other files' hides are untouched, so the count next to the affordance stays scoped to the page. Mirrors UnhideAllResolved for comments.
func (*PrereviewController) SweepGoneFiles ¶ added in v0.24.0
func (c *PrereviewController) SweepGoneFiles()
SweepGoneFiles flags every comment whose file no longer exists as outdated, ONCE at startup, and self-heals the CSV.
relocateAll already does this, but it only runs on a live path (a snapshot emit, opening the all-comments view). The CLI readers — `prereview comments` / `done` — read the CSV directly, so until something relocated, they kept handing the agent work on files that no longer exist while the UI had already stopped showing it. Doing the sweep at launch means the store is TRUE from the first moment and every reader agrees.
Cheap: one os.Stat per commented file, no git.
func (*PrereviewController) ToggleAgentPause ¶ added in v0.18.0
func (c *PrereviewController) ToggleAgentPause(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleAgentPause flips the agent between paused and running (#119). Pausing holds the drain so the reviewer can batch up work; resuming releases it and re-arms the emit so the accumulated batch ships in one snapshot.
func (*PrereviewController) ToggleAnnotations ¶
func (c *PrereviewController) ToggleAnnotations(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleAnnotations opens/closes the --external annotations sidebar (collapsed by default so the live site gets full width). Pure flip.
func (*PrereviewController) ToggleCommentList ¶
func (c *PrereviewController) ToggleCommentList(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleCommentList flips between the diff viewer and the all-comments overview pane. Bound to the "N comments" entry in the overflow menu. Closes the menu so the user sees the result immediately.
func (*PrereviewController) ToggleFileScope ¶
func (c *PrereviewController) ToggleFileScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleFileScope flips the drawer file list between changed-only (default) and all tracked files. See PrereviewState.ShowAllFiles and scopedFiles. Lives in the drawer, so no overflow-menu interaction.
func (*PrereviewController) ToggleFileView ¶
func (c *PrereviewController) ToggleFileView(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleFileView flips between diff-overlay mode (default) and plain file-view mode. See PrereviewState.FileView. Closes the overflow menu so the effect on the diff is immediately visible.
func (*PrereviewController) ToggleFiles ¶
func (c *PrereviewController) ToggleFiles(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleFiles flips the mobile file-drawer's open state. Bound to the hamburger button and to the drawer's backdrop "close" form.
func (*PrereviewController) ToggleFocusMode ¶
func (c *PrereviewController) ToggleFocusMode(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleFocusMode flips the distraction-free reading view. When on, the desktop file drawer (left) and TOC sidebar (right) are hidden so the center reading surface gets the full width. Persisted per-user. Closes the overflow menu so the effect is visible immediately (the menu entry is the mobile control; the desktop control is a toolbar button).
func (*PrereviewController) ToggleKeyboardHelp ¶ added in v0.14.0
func (c *PrereviewController) ToggleKeyboardHelp(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleKeyboardHelp opens/closes the keyboard-shortcuts help overlay. Triggered by the "?" key and the toolbar help button (both dispatch toggleKeyboardHelp); Esc closes it via ClearSelection. Closes the overflow menu so the two overlays don't stack.
func (*PrereviewController) ToggleMarks ¶ added in v0.23.0
func (c *PrereviewController) ToggleMarks(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleMarks flips whether the per-line comment/suggestion count badges (#151) and their inline cards are shown in the diff. Off by default (badges visible); when on, the diff reads clean so a reviewer can scan the raw code. Persisted per-user. Closes the overflow menu so the effect is visible immediately.
func (*PrereviewController) ToggleMoreMenu ¶
func (c *PrereviewController) ToggleMoreMenu(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleMoreMenu opens/closes the 3-dots overflow menu in the top bar. Mirrors the file-drawer pattern: state-driven boolean + CSS class toggle. No JS. Backdrop tap submits CloseMoreMenu.
func (*PrereviewController) ToggleQueueScope ¶ added in v0.24.0
func (c *PrereviewController) ToggleQueueScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleQueueScope switches the queue panel between THIS FILE's work (the default — the queue is about the document in front of you) and the whole review's (#171). Persisted per-user, so the choice survives the relaunch-per-review workflow.
A view filter only: the agent is always handed every actionable item in the review, so flipping this can never strand work it would otherwise have picked up.
func (*PrereviewController) ToggleQuiz ¶ added in v0.25.0
func (c *PrereviewController) ToggleQuiz(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleQuiz flips between the diff viewer and the comprehension-quiz view for the selected file. Mirrors ToggleCommentList: closes the overflow menu so the result is visible immediately, and is not persisted — reopening the browser starts back in the diff.
func (*PrereviewController) ToggleRawHTML ¶
func (c *PrereviewController) ToggleRawHTML(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleRawHTML is the .html/.htm equivalent of ToggleRawMarkdown: flips the iframe preview off and the syntax-highlighted line view on (and back). Closes the overflow menu so the change is visible on mobile.
func (*PrereviewController) ToggleRawMarkdown ¶
func (c *PrereviewController) ToggleRawMarkdown(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleRawMarkdown flips a Markdown file between the rendered view (default) and the raw line view. Closes the overflow menu so the effect is immediately visible on mobile.
func (*PrereviewController) ToggleRegionSelect ¶
func (c *PrereviewController) ToggleRegionSelect(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleRegionSelect flips the "draw a box to comment" overlay for the current preview on/off. Bound to the "Select region" toggle button. Off by default so one-finger gestures scroll; on, the parent-document overlay (lvt-fx:region-select) captures a drag and resolves it to a pixel rect (image) or a source line range (rendered HTML / code).
func (*PrereviewController) ToggleResolved ¶
func (c *PrereviewController) ToggleResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleResolved flips the Resolved flag on the named comment and rewrites the CSV. Unlike DeleteComment, this keeps the comment as a historical record; the skill should treat resolved comments as "addressed" and only act on unresolved ones.
func (*PrereviewController) ToggleRow ¶ added in v0.24.0
func (c *PrereviewController) ToggleRow(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleRow flips one row's annotation cards away from their default visibility (#174) — the count badge's click. Green badge: peek the done cards (what it always did). Yellow badge: collapse the open comment/suggestion behind the badge, so it can be set aside and come back to later without being resolved.
Server-side on purpose: the old `peek-done` was a client-only class the server never emitted, so any re-render morphed it away and silently re-collapsed a card the reviewer had just opened (#173). Now the server owns it and the morph converges.
The row key is the badge's own: "<line>-<side>" in the diff, "MB-<start>-<end>" in the md-view. Deleting rather than storing false keeps the persisted map to just the rows the reviewer actually flipped.
func (*PrereviewController) ToggleSearchScope ¶ added in v0.16.0
func (c *PrereviewController) ToggleSearchScope(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleSearchScope flips the search between the changed set (default) and all files, recomputing against the new scope.
func (*PrereviewController) ToggleShowResolved ¶
func (c *PrereviewController) ToggleShowResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleShowResolved flips whether resolved comments are visible in the inline diff and the all-comments overview. Default off — resolved comments add noise once they're handled. Bound to an entry in the overflow menu. Closes the menu so the user can immediately see the effect on the diff.
func (*PrereviewController) ToggleSuggestions ¶ added in v0.17.0
func (c *PrereviewController) ToggleSuggestions(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleSuggestions shows/hides the inline LLM suggestion boxes for the current view (issue #98). A declutter toggle only — it never touches the underlying suggestions. Session-scoped (HideSuggestions is lvt:"persist"), so it isn't a durable per-user pref; closes the overflow menu so the effect is visible on mobile.
func (*PrereviewController) ToggleTOC ¶ added in v0.24.3
func (c *PrereviewController) ToggleTOC(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleTOC flips the desktop table-of-contents sidebar (the right column). Bound to the bar's contents button and the "t" shortcut. Independent of the file drawer (left) and of Focus mode, which hides both at once (#137). A durable view preference like Focus mode, so it's saved to disk to survive a relaunch.
func (*PrereviewController) ToggleViewed ¶
func (c *PrereviewController) ToggleViewed(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ToggleViewed flips the "reviewed" flag for the file passed via the hidden `path` input. Bound to a per-file checkbox/button in the drawer.
func (*PrereviewController) UndoDelete ¶
func (c *PrereviewController) UndoDelete(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
UndoDelete restores the most recently deleted comment to state.Comments and rewrites the CSV. No-op if LastDeletedComment is nil (the undo affordance shouldn't even render in that case, but defending in depth).
func (*PrereviewController) UnhideAllResolved ¶ added in v0.16.0
func (c *PrereviewController) UnhideAllResolved(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
UnhideAllResolved clears the individual-hide flag on every comment at once — the recovery for HideComment, surfaced as "Unhide N" next to the Show-resolved toggle. Rewrites the CSV; rolls back the in-memory flags on a write error so disk and memory stay in sync.
func (*PrereviewController) ViewVersion ¶ added in v0.18.0
func (c *PrereviewController) ViewVersion(state PrereviewState, ctx *livetemplate.Context) (PrereviewState, error)
ViewVersion opens a historical version of the selected file read-only in the right pane (the store hands back the exact bytes; RenderBytesAsFile turns them into the same *FileDiff the live pane renders). It's a view MODE, not a file write — nothing on disk changes. Selection/compose affordances are cleared because historical line coords don't match disk (see the template's !ViewingVersion gate).
func (*PrereviewController) WatchLLMStatus ¶ added in v0.16.0
func (c *PrereviewController) WatchLLMStatus(stop <-chan struct{}, poll time.Duration)
WatchLLMStatus polls the status file and, whenever it changes, fans a LLMStatusChanged action out to every connected tab of the current session. One goroutine per server (not per connection): the status file is a single global signal and TriggerAction already fans out to all tabs, so a per-connection watcher would multiply re-renders by the number of tabs. It runs only in skill/stream mode (the only modes with an agent writing status) and returns when stop is closed (server shutdown).
type PrereviewState ¶
type PrereviewState struct {
// Session identity — set once by main.go, never mutated.
RepoPath string `json:"repo_path" lvt:"persist"`
Base string `json:"base" lvt:"persist"`
StartedAt string `json:"started_at" lvt:"persist"`
CSVPath string `json:"csv_path" lvt:"persist"`
Version string `json:"version" lvt:"persist"`
// External (proxy) mode — `prereview --external <url>`. When true the
// session reviews a live local website framed through the reverse proxy
// instead of files in a repo: there is no git diff and no file list, and
// comments anchor to URL + region rather than file + line.
// ProxyBaseURL is the same-host second-origin URL the UI iframes;
// TargetURL is the upstream the proxy fronts (shown to the user);
// CurrentURL is the proxied page currently in the iframe, reported by the
// injected beacon and used to scope which annotations are shown/placed.
ExternalMode bool `json:"external_mode" lvt:"persist"`
ProxyBaseURL string `json:"proxy_base_url" lvt:"persist"`
TargetURL string `json:"target_url" lvt:"persist"`
CurrentURL string `json:"current_url" lvt:"persist"`
// File navigation.
Files []gitdiff.FileEntry `json:"files"`
SelectedFile string `json:"selected_file" lvt:"persist"`
CurrentDiff *gitdiff.FileDiff `json:"current_diff"`
// SingleFile is the session's review SCOPE: in single-file mode it is the one
// reviewable file, and "" in a directory / git review (which scopes to nothing).
// Mirrored from the controller every Mount — the controller owns it.
//
// Deliberately NOT lvt:"persist": a persisted copy could ride a session store
// into a relaunch pointed at a DIFFERENT file and silently scope the review to
// the wrong document. Disk (the CLI target) is the only source of truth.
//
// It is not SelectedFile: that is a cursor the reviewer moves, and a directory
// review's all-comments view is SUPPOSED to span every file. See inScope.
SingleFile string `json:"single_file"`
// Artifact versioning (#90). Versions is the selected file's version
// timeline (newest first), populated from the version store each Mount /
// version action for the Versions panel. When ViewingVersion is true the
// right pane shows a historical version (VersionViewSeq) of SelectedFile
// read-only instead of the live diff — CurrentDiff holds the rendered
// historical content. ViewingVersion is deliberately NOT lvt:"persist": it
// is a transient view mode that every live-diff rebuild (Mount, SelectFile,
// RefreshDiff) must clear, so a reconnect or background re-render lands back
// on the live diff rather than stale history mislabeled as current.
Versions []VersionListItem `json:"versions"`
ViewingVersion bool `json:"viewing_version"`
VersionViewSeq int `json:"version_view_seq"`
// VersionViewDiff distinguishes the two historical views: false = the version's
// content read-only ("View"), true = that version diffed against the current
// file ("Diff vs current"). Only meaningful while ViewingVersion.
VersionViewDiff bool `json:"version_view_diff"`
// AgentPaused mirrors the .prereview/paused marker: a rollback force-pauses
// the agent so it stops applying while the reviewer re-steers (#90). Read
// from disk each Mount; M2's continuous drain gates emission on it.
AgentPaused bool `json:"agent_paused"`
// Two-click selection (anchor → end). 0 = nothing selected; first
// click sets both to the same line; second click moves end; third
// click reseats anchor.
SelectionAnchor int `json:"selection_anchor" lvt:"persist"`
SelectionEnd int `json:"selection_end" lvt:"persist"`
SelectionSide string `json:"selection_side" lvt:"persist"` // "new"|"old"|"both"
// SelectionFromCol/ToCol/Text hold the in-progress CHARACTER range for a
// kind=text comment (composer CommentMode == "text"): rune offsets into
// FromLine/ToLine and the exact selected substring, set by SelectText
// (dispatched from the client's lvt-fx:text-select directive) and cleared
// by AddComment / ClearSelection / SelectFile. Persisted so the composer
// survives a WebSocket reconnect mid-compose.
SelectionFromCol int `json:"selection_from_col" lvt:"persist"`
SelectionToCol int `json:"selection_to_col" lvt:"persist"`
SelectionText string `json:"selection_text" lvt:"persist"`
// Comment composer.
DraftBody string `json:"draft_body" lvt:"persist"`
// CommentMode flags whether the open composer is targeting a line
// range ("" — the historical default, paired with SelectionAnchor),
// the whole file ("file" — SelectionAnchor stays 0), or an image
// region ("area" — paired with SelectionArea). Persisted so a
// WebSocket reconnect mid-composer doesn't drop the user back to
// the file head.
CommentMode string `json:"comment_mode" lvt:"persist"`
// SelectionArea holds the in-progress rectangle for a kind=area
// comment: 0..1 fractions of the image's rendered size. Set by
// SelectImageArea (dispatched from the client's lvt-fx:area-select
// directive on pointerup) and cleared by AddComment / ClearSelection
// / SelectFile. Persisted so the pending overlay re-appears after a
// WebSocket reconnect.
SelectionArea Area `json:"selection_area" lvt:"persist"`
// RegionSelectArmed gates the "draw a box to comment" overlay over a
// preview (image / rendered HTML / code). Off by default so a one-
// finger gesture scrolls the page normally; the "Select region" toggle
// flips it on, the overlay then captures the drag, and capturing a
// region (SelectImageArea / SelectBlock) disarms it again so the
// composer shows and scrolling returns. Persisted so a reconnect
// mid-selection doesn't silently disarm the overlay.
RegionSelectArmed bool `json:"region_select_armed" lvt:"persist"`
// Comments accumulated during this session.
Comments []Comment `json:"comments"`
// Suggestions are the LLM's proposed edits (issue #98), loaded from the
// agent-owned .prereview/suggestions.jsonl on every Mount (like Comments from
// the CSV — not lvt:"persist"; the file is the source of truth). Rendered
// inline as suggestion boxes on the selected file.
Suggestions []Suggestion `json:"suggestions"`
// HideSuggestions toggles the inline suggestion boxes off for the current
// view (a declutter, so the reviewer can read the doc without the proposed
// edits overlaid). Session-scoped (survives reconnect via persist); does not
// affect the underlying suggestions.
HideSuggestions bool `json:"hide_suggestions" lvt:"persist"`
// Decisions are the reviewer's recorded verdicts on suggestions (issue #98
// Phase 2), loaded from the server-owned .prereview/suggestion-decisions.jsonl
// every Mount (the file is the source of truth — not lvt:"persist"). Overlaid
// onto suggestions by ID + content fingerprint in DecisionsBySuggestion.
Decisions []SuggestionDecision `json:"decisions"`
// Applied is the set of accepted-suggestion ids the agent has WRITTEN to disk
// (#159), loaded from the agent-owned .prereview/applied.jsonl every Mount (not
// lvt:"persist"). Flips an accepted suggestion's card from "accepted (pending
// apply)" to "applied", and drops it from the agent's actionable snapshot.
Applied map[string]bool `json:"applied"`
// Hidden is the reviewer's hidden-from-view suggestion set, loaded from the
// server-owned .prereview/hidden-suggestions.jsonl every Mount (the file is
// the source of truth — not lvt:"persist"). A pure view filter applied in
// visibleSuggestions, fingerprint-gated so a revised suggestion reappears.
Hidden []HiddenSuggestion `json:"hidden_suggestions"`
// ThreadEntries are the #149 conversation entries on comments/suggestions —
// the agent's "what I did" notes (and, later, reviewer replies) — loaded from
// the append-only thread sidecars every Mount (the files are the source of
// truth — not lvt:"persist"). Grouped by target ID in Threads() for render.
ThreadEntries []ThreadEntry `json:"thread_entries"`
// Prompts is the #147 "ask for suggestions" library — built-in templates plus the
// user's ~/.config/prereview/prompts/ overlay — loaded every Mount (not
// lvt:"persist"). Rendered in the file-header picker; picking one pre-fills the
// file-comment composer.
Prompts []Prompt `json:"prompts"`
// QuizPrompts is the #191 quiz-prompt library — built-ins plus the user's
// ~/.config/prereview/quizzes/ overlay — loaded every Mount like Prompts.
// Drives the file-header "Quiz me" control: one prompt renders a plain
// one-tap button, several render a picker.
QuizPrompts []Prompt `json:"quiz_prompts"`
// ReplyingID is the comment/suggestion whose inline reply form is open (#149;
// mirrors EditingCommentID); ReplyDraft holds the in-progress reply so it
// survives reconnects. Empty = no reply form open.
ReplyingID string `json:"replying_id" lvt:"persist"`
ReplyDraft string `json:"reply_draft" lvt:"persist"`
// UI status.
LastSaved string `json:"last_saved"`
// EnqueueTick is a monotonic counter bumped ONLY when a comment genuinely
// enters the queued set (a new saved comment, or an explicit (re)enqueue) —
// never on edit / resolve / delete / reconnect. It drives the Queue button's
// one-shot "just queued" pulse (#129): the button's id embeds this tick, so a
// bump changes the id, morphdom recreates the node, and the client's
// lvt-fx:animate scan re-fires (the effect is once-per-node-identity, so a
// persistent button would otherwise animate only once). Persisted so a
// reconnect restores the same tick → same id → no spurious pulse.
EnqueueTick int `json:"enqueue_tick" lvt:"persist"`
// LLMState mirrors the agent's inbound status signal
// (.prereview/llm-status.json): "working" while the agent applies a snapshot
// batch, "done" once finished, "" idle. LLMMessage is the optional detail
// shown in the pill. Written by the agent (skill), watched by the server, and
// pushed to every open tab via the llm-status watcher → LLMStatusChanged
// fan-out; also refreshed from the file on each connect so a late/reconnecting
// tab shows current status. Not persisted — the file is the source of truth
// (like Comments), re-read every connect. (The file also carries updated_at;
// it's a debug field, not surfaced in the UI, so it isn't mirrored here.)
LLMState string `json:"llm_state"`
LLMMessage string `json:"llm_message"`
// PendingRefresh is set when the agent's status transitions working→done:
// the agent just edited files, so this tab's diff is now stale. It drives a
// non-intrusive "Changes applied — Refresh diff" affordance the user clicks
// to reload (RefreshDiff), preserving scroll + any in-progress draft until
// then. Per-connection and NOT persisted: a reconnect re-runs Mount, which
// rebuilds the diff fresh, so the nudge is moot after a reload.
PendingRefresh bool `json:"pending_refresh"`
// SeenReviewedGen is the controller's reviewedGen this tab last rebuilt its
// diff at (set in Mount). LLMStatusChanged nudges PendingRefresh when the
// controller's reviewedGen has moved past it — i.e. the reviewed file was
// edited on disk since this tab last rendered, with no agent command. Not
// persisted; Mount resets it.
SeenReviewedGen int64 `json:"-"`
// Mobile drawer visibility. Persisted so a reconnect mid-drawer doesn't
// surprise the user with a closed drawer. The desktop CSS ignores this
// field (sidebar is always visible above 900px).
FileDrawerOpen bool `json:"file_drawer_open" lvt:"persist"`
// TocCollapsed hides the desktop table-of-contents sidebar (the right
// column, rendered only for Markdown with headings). Independent of the
// left drawer and of Focus mode — #137 wants per-side collapse control.
// A durable per-user view preference like FocusMode (its per-side twin),
// sourced from disk via uiprefs so it survives the relaunch-per-review
// workflow; the desktop CSS ignores it below 900px, where the TOC is a
// modal, not a column.
TocCollapsed bool `json:"toc_collapsed"`
// AnnoDrawerOpen toggles the --external annotations sidebar. Collapsed by
// default (zero value) so the framed live site gets the full width —
// especially on a phone; the header "Annotations (N)" button opens it.
// Persisted so the choice survives a reconnect.
AnnoDrawerOpen bool `json:"anno_drawer_open" lvt:"persist"`
// FocusedCommentID is the region annotation the user tapped in the
// sidebar; its on-page pin renders highlighted, and the client asks the
// iframe (via the beacon) to navigate to its page + scroll it into view.
// FocusSeq changes on every focus so re-tapping the same annotation
// re-triggers the client even when the id is unchanged.
FocusedCommentID string `json:"focused_comment_id" lvt:"persist"`
FocusSeq int `json:"focus_seq" lvt:"persist"`
// AgentMode is mirrored from the controller (set by --agent flag) into state
// in Mount so the template can branch the top-bar controls between the Queue
// (Pause/Resume) + "End session" agent UI and the standalone "Quit" button.
// In agent mode each queue mutation emits a JSON snapshot and End
// session emits the terminating end event. Not persisted — the
// controller is the source of truth; Mount refreshes it every connect.
AgentMode bool `json:"agent_mode"`
// NoGit is mirrored from the controller (set when the path is a single
// file or a non-git directory) into state in Mount so the template
// can hide the base/branch picker — there are no refs to compare
// against. Not persisted; the controller is the source of truth.
NoGit bool `json:"no_git"`
// Quitting flips true when the user clicks Quit. The template renders
// a "Server stopping…" banner; ~250ms later the HTTP server actually
// shuts down (giving the framework time to flush the render).
Quitting bool `json:"quitting"`
// SessionEnded flips true when the user clicks "End session" in stream
// mode. Like Quitting it precedes a delayed graceful shutdown, but the
// banner wording differs ("session ended") and EndSession also emits the
// terminating end stream event before shutting down.
SessionEnded bool `json:"session_ended"`
// EditingCommentID is set when the user has tapped Edit on an existing
// comment. The composer label changes to "Editing comment on L28"
// instead of "Comment on L28" so it's clear the next save replaces
// rather than appends. Cleared by AddComment, ClearSelection.
// Persisted so the edit mode survives a WebSocket reconnect — iPhone
// Safari drops the WS aggressively on tab/app switch. Without persist,
// AddComment would see EditingCommentID="" after the reconnect and
// append a new comment instead of updating in place.
EditingCommentID string `json:"editing_comment_id" lvt:"persist"`
// ReanchorCommentID is set when the user taps "Re-anchor here" on an
// outdated comment. The next line/range they select + Save re-points
// that comment (and re-captures its content anchor) instead of
// appending. Mutually exclusive with EditingCommentID. Persisted for
// the same reconnect-resilience reason. Cleared by AddComment /
// ClearSelection.
ReanchorCommentID string `json:"reanchor_comment_id" lvt:"persist"`
// LastDeletedComment holds the most recently deleted comment so the
// user can undo. Cleared by ANY other mutation (add, edit, another
// delete, hand off, quit) so the undo affordance can't surprise the
// user later with state from minutes ago.
LastDeletedComment *Comment `json:"last_deleted_comment"`
// ViewedFiles is a per-session set of files the user has marked as
// "reviewed" (GitHub PR convention). Persisted so the state survives
// browser refresh; not written to CSV (this is UX state, not a comment).
ViewedFiles map[string]bool `json:"viewed_files" lvt:"persist"`
// Read progress (#128), keyed by file path. ReadThrough is the furthest
// new-side line number the reviewer has scrolled past (a high-water mark →
// lines at/above it render "read"). LastReadTopKey is the topmost visible line
// key at the last report → the scroll-restore target when the file is
// re-opened. Both are reported by the client's lvt-fx:viewport-report directive
// and persisted so they survive a reconnect within the review session. See
// readprogress.go.
ReadThrough map[string]int `json:"read_through" lvt:"persist"`
LastReadTopKey map[string]string `json:"last_read_top_key" lvt:"persist"`
// ScrollToReadKey is the transient scroll-restore target: SelectFile sets it to
// the file's LastReadTopKey so the browser re-opens where the reviewer left off.
// NOT persisted — a one-render nudge (like ScrollToCommentID), cleared once the
// reviewer scrolls (ReportViewport) so a re-render can't yank them back.
ScrollToReadKey string `json:"scroll_to_read_key"`
// FileFilter is the case-insensitive substring filter for the file
// drawer. Persisted so a refresh doesn't drop the filter.
FileFilter string `json:"file_filter" lvt:"persist"`
// ShowAllFiles controls the drawer scope. Default (false) lists
// only files that differ from the base — the common review case,
// and the only sane default on a large repo. When there are zero
// changed files (clean tree) the scope falls back to all so the
// list isn't empty. true forces the full tracked-file list so the
// user can comment on unchanged files too.
ShowAllFiles bool `json:"show_all_files" lvt:"persist"`
// ShowAllComments toggles the all-comments overview pane (replaces the
// diff viewer). Not persisted — closing/reopening the browser starts
// back in the diff view.
ShowAllComments bool `json:"show_all_comments"`
// ShowQuiz toggles the #191 comprehension-quiz view for the selected file
// (replaces the diff viewer, like ShowAllComments). Not persisted, and reset
// by every navigation action for the same reason: a view that survives a jump
// to another file would strand the reviewer somewhere they didn't ask to be.
ShowQuiz bool `json:"show_quiz"`
// Quizzes are the agent-written comprehension quizzes (.prereview/quiz.jsonl),
// and QuizAnswers the reviewer's responses keyed "<quizID>\x00<questionID>".
// Neither is persisted — the files are the source of truth, re-read every
// Mount and on each watcher tick, exactly like Comments and Suggestions.
Quizzes []Quiz `json:"quizzes"`
QuizAnswers map[string]QuizAnswer `json:"quiz_answers"`
// automatically whenever the file has a quiz — discoverability has been this
// feature's recurring failure, so it is not behind a menu — but it must be
// possible to put away while reading the diff.
QuizNavDismissed bool `json:"quiz_nav_dismissed"`
// QuizRequestedFile is the file a "Quiz me" request was just sent for, so the
// control can show "Quiz requested…" instead of inviting a second tap. Cleared
// as soon as a quiz for that file arrives (applyQuiz).
//
// It exists because the first version gave NO feedback: the request saved a
// comment, nothing visibly changed, and a real reviewer on a phone tapped again
// and queued a duplicate. A UI hint, deliberately not persisted — worst case a
// reconnect re-offers the button, which is merely the old behaviour.
QuizRequestedFile string `json:"quiz_requested_file"`
// ScrollToCommentID, when non-empty for one render, drives the
// `lvt-fx:scroll="into-view"` directive on the matching inline-comment.
// Set by JumpToComment; the framework's one-shot guard
// (data-lvt-iv-done) prevents repeated scrolls on subsequent renders.
ScrollToCommentID string `json:"scroll_to_comment_id"`
// ScrollToHeadingID, when non-empty, drives the
// `lvt-fx:scroll="into-view"` directive on the MarkdownBlock that
// contains the heading with this ID. Set by NavigateToHeading (TOC
// click) so a heading clicked from inside the all-comments overview
// actually lands the user on that section once ShowAllComments
// flips back off — the all-comments view replaces the md-view in the
// DOM, so the rendered markdown is fresh on return and the framework's
// data-lvt-iv-done guard hasn't been set yet (issue #12).
ScrollToHeadingID string `json:"scroll_to_heading_id"`
// URLHashScrollAnchor persists the `h-<anchor>` target from a
// deep-link URL across renders so the URL bar keeps `:h-<anchor>`
// even after the scroll completed (shareable link stays valid).
// Used by `state.URLHash()`. For markdown, the scroll itself routes
// through the ScrollToHeadingID + ScrollHeadingBlockKey machinery —
// this field only feeds URL serialisation. (HTML preview renders in
// one iframe, so it has no block-level scroll target.) Cleared by
// ClearSelection, by clicking a line, and by SelectFile.
URLHashScrollAnchor string `json:"url_hash_scroll_anchor" lvt:"persist"`
// ShowResolved, when true, includes resolved comments in the inline
// comment stream + all-comments view. Default false so the viewer
// focuses on what's still actionable. A durable per-user view pref: NOT
// lvt:"persist" — it lives in the on-disk prefs file (see uiprefs.go) so it
// survives a server relaunch, not just a reload; applyUIPrefs reloads it on
// every Mount/OnConnect.
ShowResolved bool `json:"show_resolved"`
// MoreMenuOpen drives the 3-dots overflow menu in the top bar where
// secondary controls (All comments, Show resolved) live on narrow
// viewports. Not persisted — closing across a reconnect is the right
// default; nobody expects an overflow menu to survive a refresh.
MoreMenuOpen bool `json:"more_menu_open"`
// TOCOpen drives the mobile Table-of-Contents overlay. On desktop
// the TOC is always a right sidebar (no state needed); on narrow
// viewports it lives behind a 3-dots menu item that flips this flag,
// rendering the heading list as a full-screen overlay. Not persisted
// for the same reason as MoreMenuOpen.
TOCOpen bool `json:"toc_open"`
// KeyHelpOpen drives the keyboard-shortcuts help overlay, toggled by
// the "?" key or the toolbar help button (both dispatch
// toggleKeyboardHelp). Esc closes it via ClearSelection. Not persisted —
// a refresh shouldn't reopen a help panel.
KeyHelpOpen bool `json:"key_help_open"`
// SearchOpen drives the cmd+k search palette (issue #91), toggled by the
// Mod+k binding / the toolbar magnifier (openSearch) and closed by Esc
// (ClearSelection) or the close button. SearchQuery is the live-debounced
// text; SearchScopeAll switches between the changed set (default) and all
// files; SearchHits is the computed result list (filled by the controller —
// a zero-arg state method can't reach loadDiffCached). None persisted: a
// reopened tab shouldn't resurrect a stale palette (mirrors KeyHelpOpen).
SearchOpen bool `json:"search_open"`
SearchQuery string `json:"search_query"`
SearchScopeAll bool `json:"search_scope_all"`
SearchHits []SearchHit `json:"search_hits"`
// RevealFile is a search-jump's transient "show this file's full RAW source"
// override: JumpToSearchResult sets it to the hit's path so the exact matched
// line exists in the DOM to scroll to — even in an unchanged region diff-view
// would fold, and even for Markdown/HTML (which otherwise render as blocks /
// preview with no L<old>-<new> rows). Honoured by Revealing() → VisibleLines
// (full file) + ShowRenderedMarkdown/ShowRenderedHTML (fall to line view). It
// deliberately does NOT touch the durable FileView pref. Cleared on SelectFile
// so it never leaks onto later navigation. Not persisted.
RevealFile string `json:"reveal_file"`
// CursorKey is the data-key ("L<old>-<new>") of the diff line the keyboard
// line cursor is on. ArrowUp/ArrowDown move it (CursorUp/CursorDown); the
// matching line button is highlighted, scrolled into view, and focused
// (lvt-autofocus) so Enter activates it → the line composer. Empty = no
// cursor yet (first arrow press seeds it). Not persisted — a transient
// navigation aid. Reset when the selected file changes.
CursorKey string `json:"cursor_key"`
// Flash is a transient status message shown as an auto-dismissing toast —
// e.g. pressing "r" (Show resolved) when there are no resolved comments,
// where toggling would otherwise do nothing visible. Set by the action,
// cleared by clearFlash (auto after a few seconds, or manual dismiss). Not
// persisted; a refresh shouldn't resurrect a stale notice.
Flash string `json:"flash"`
// FileView, when true, turns off the diff overlay: deleted lines are
// hidden, +/- gutter markers disappear, and add/del row coloring is
// dropped. The user sees the file as it currently exists in the
// working tree. Equivalent to GitHub's "View file" toggle. A durable
// per-user view pref (see uiprefs.go / ShowResolved) — not lvt:"persist".
// Defaults false (diff is the primary reviewing mode).
FileView bool `json:"file_view"`
// RawMarkdown shows a .md/.markdown file as the raw line view
// instead of the rendered default. Durable per-user view pref (uiprefs.go).
// Defaults false: Markdown renders by default; the user toggles to raw to
// see the source lines. Non-Markdown files ignore this.
RawMarkdown bool `json:"raw_markdown"`
// RawHTML is the .html/.htm equivalent of RawMarkdown: when true the
// viewer shows the syntax-highlighted source instead of the
// sandboxed-iframe preview. Durable per-user view pref (uiprefs.go).
// Defaults false. Independent of RawMarkdown so a user's preference for one
// format doesn't drag the other along. Non-HTML files ignore this.
RawHTML bool `json:"raw_html"`
// FocusMode, when true, hides both desktop side columns (the file
// drawer on the left and the TOC sidebar on the right) so the center
// reading surface gets the full width — a distraction-free reading
// view for long docs/diffs. Desktop-only in effect: the hiding CSS
// lives behind the ≥900px media query; on mobile the columns are
// already overlays/modals, so the flag is a harmless no-op there.
// Durable per-user view pref (uiprefs.go), like FileView.
FocusMode bool `json:"focus_mode"`
// HideMarks, when true, hides the per-line comment/suggestion count badges
// (#151) AND their inline cards in the diff, so a reviewer can read the raw
// diff clean. Toggled from the overflow menu (ToggleMarks); focus mode hides
// them too (CSS). Applies on all viewports (unlike FocusMode's side-column
// hiding, which is desktop-only). Durable per-user view pref (uiprefs.go).
HideMarks bool `json:"hide_marks"`
// ToggledRows flips a row's annotation cards away from their DEFAULT visibility
// (#174). Keyed by the same row key the badge renders under: "<line>-<side>" in the
// diff, "MB-<start>-<end>" in the md-view.
//
// The default comes from the annotation's state (#165): an OPEN annotation shows
// inline, a done/accepted one collapses behind its badge. Toggling a row inverts
// that — so the green badge keeps its existing "peek the done cards" behaviour, and
// the yellow badge gains the one it was missing: collapse an open comment/suggestion
// out of the way to come back to it later, without resolving it.
//
// SERVER state, not a client-only class. The previous `peek-done` was toggled purely
// in the browser, and since the server never emitted it, any re-render morphed it
// away — silently re-collapsing a card the reviewer had just opened (#173). The
// client still toggles optimistically for instant feedback on a phone; the server
// emits the authoritative class and the morph converges on it.
//
// lvt:"persist" (session), NOT durable on disk: it survives a reload/reconnect, but a
// relaunch starts clean. A collapsed-but-OPEN annotation is still unresolved work —
// carrying yesterday's hidden pile into a new session would quietly conceal it.
//
// View-only: a collapsed annotation is still queued for the agent. This changes what
// the reviewer sees, never what the agent is handed.
//
// The VALUE is the row's default state when the reviewer toggled it (rowStateOpen /
// rowStateCollapsed). A toggle is a flip AWAY FROM a specific default, so it expires the
// moment that default changes — see RowToggled. Without this, peeking a done card and
// then undoing its verdict left the row still "flipped", which now meant HIDE: the
// suggestion the reviewer was working on vanished from under them.
ToggledRows map[string]string `json:"toggled_rows" lvt:"persist"`
// QueueGlobal widens the queue panel from THIS FILE's work to the whole review's
// (#171). Default false: the queue answers "what is happening to the document in
// front of me", which is what it's for in a single-file or doc review. Flip it in
// a many-file repo review to see the review-wide backlog and jump across files.
//
// A VIEW toggle only — it never changes what the agent is handed. The snapshot
// always carries every actionable item in the review (see EmitSnapshot), so work
// queued on another file is never stranded by the reviewer's choice of filter.
// Durable per-user view pref (uiprefs.go).
QueueGlobal bool `json:"queue_global"`
// ThemeMode is the Light/Dark/System color-mode preference (issue #60),
// cycled by the toolbar toggle. "" means System (the default): the page
// omits the data-mode attribute and follows the OS via prefers-color-scheme
// (see the Solarized-dark block + /syntax.css). "light"/"dark" force the
// mode regardless of the OS. Durable per-user view pref (uiprefs.go).
// Orthogonal to SchemeName.
ThemeMode string `json:"theme_mode"`
// SchemeName is the curated color-scheme preference — the Theme axis,
// orthogonal to ThemeMode's Light/Dark/System. "" (and any unregistered
// value) means the default scheme, gitdiff.Schemes[0] (Solarized). Cycled
// by the toolbar theme picker; the page re-renders with the new data-scheme
// attribute on .theme-root and the cascade re-skins chrome + diff —
// /syntax.css already carries every registered scheme × mode, so there is
// no JS and no CSS refetch. Durable per-user view pref (uiprefs.go).
SchemeName string `json:"scheme_name"`
// BaseChoices populates the base-picker dropdown. Computed in
// Mount: ["HEAD", "HEAD~1", "HEAD~5", <local branches…>] plus the
// current state.Base if it isn't already in the list (so custom
// refs typed via the freeform fallback still appear as the
// selected option). Not persisted — recomputed each Mount so newly
// created branches show up without a process restart.
BaseChoices []string `json:"base_choices"`
}
PrereviewState is the per-session state cloned by livetemplate. Fields tagged `lvt:"persist"` survive WebSocket reconnects (browser refresh, etc.) so the user doesn't lose their selected file or comment draft if the page reloads.
func (PrereviewState) AgentWorking ¶ added in v0.18.0
func (s PrereviewState) AgentWorking() bool
AgentWorking mirrors the llm-status echo: true while the agent is applying a batch. Drives the "in progress" state of the queue (the queued set is being worked) and a live pill.
func (PrereviewState) AgentWorkingLabel ¶ added in v0.24.3
func (s PrereviewState) AgentWorkingLabel() string
AgentWorkingLabel is the text for the live "working" pill/toast — non-empty only while the agent is working (llm-status=working). It prefers the agent's own status message; with none, it tells the reviewer what is happening: applying their reply(ies) when any still await a response (#164 secondary — the reviewer wants to see their reply being applied), else the generic handoff message. Empty when the agent is idle, so the caller can gate on it directly.
func (PrereviewState) AllRegionComments ¶
func (s PrereviewState) AllRegionComments() []Comment
AllRegionComments returns every visible region annotation for the --external sidebar, in creation order. Unlike RegionComments it is NOT scoped to the current page, so the sidebar can show annotations across every page the user has visited.
func (PrereviewState) AnnotationAcceptedLines ¶ added in v0.23.1
func (s PrereviewState) AnnotationAcceptedLines() map[string]bool
AnnotationAcceptedLines reports, per row, whether the row has any accepted-but-unapplied suggestion (#165) — the badge is accentuated yellow (else it falls through to open→yellow or done→green). Only suggestions have an accepted state. The template checks it AFTER AnnotationOpenLines, so open wins on a mixed row.
func (PrereviewState) AnnotationCountLines ¶ added in v0.23.1
func (s PrereviewState) AnnotationCountLines() map[string]int
AnnotationCountLines is the per-row TOTAL — comments + suggestions — driving the one unified right-margin badge (#165: a single badge per line, not one per kind).
func (PrereviewState) AnnotationOpenLines ¶ added in v0.23.1
func (s PrereviewState) AnnotationOpenLines() map[string]bool
AnnotationOpenLines reports, per row, whether ANY annotation is open (unresolved comment / undecided suggestion) — the unified badge is yellow.
func (PrereviewState) AreaComments ¶
func (s PrereviewState) AreaComments() []Comment
AreaComments returns the selected file's visible image-area comments (Kind == "area") in creation order. Rendered as semi-transparent rectangle overlays inside the image wrapper, with paired list entries for body + Resolve/Edit/Delete actions in the file-comments section. Same shape as FileLevelComments — parallel iteration.
func (PrereviewState) AwaitingAgent ¶ added in v0.23.0
func (s PrereviewState) AwaitingAgent() map[string]bool
AwaitingAgent maps each target ID whose thread ends with a REVIEWER entry — the reviewer replied and is waiting on the agent (#149) — so a card can badge itself "awaiting agent". Zero-arg for the framework; nil when none.
func (PrereviewState) AwaitingApplyCount ¶ added in v0.24.0
func (s PrereviewState) AwaitingApplyCount() int
AwaitingApplyCount is how many suggestions the reviewer has ACCEPTED but the agent has not yet written to the file (#171).
Accepting only records a verdict — prereview never writes user files (#103); the agent applies the edit and acks with `prereview applied <id>`. That works while an agent is looping on `prereview watch`, but accept an edit after its turn has ended and nothing ever applies it: the verdict sits in suggestion-decisions.jsonl, the file is untouched, and the card has already collapsed to an amber badge (#165) that is trivially missed. The document silently never becomes clean.
This is the count that makes that state impossible to ignore — surfaced in the queue and warned about on End session. Revert-aware, because s.Applied nets reverted.jsonl: undoing an applied edit correctly puts it back in the awaiting-apply pile.
Deliberately scoped to the whole REVIEW, not the current file — unlike the rest of the queue panel (#171). It is a "you are about to lose work" guard, not a work list: a count that only saw the file you happen to be looking at would cheerfully let you end the session with unapplied accepts stranded on another file, which is the exact failure it exists to prevent.
func (PrereviewState) AwaitingLines ¶ added in v0.23.0
func (s PrereviewState) AwaitingLines() map[string]bool
AwaitingLines reports, per line-row ("<line>-<side>"), whether any comment or suggestion rendered there is awaiting the agent (the reviewer replied last, #149) — so the #151 count badge on that row can show an unread dot. Derived from the same filtered ByEndLine maps as the badges, gated by AwaitingAgent. Zero-arg; nil when nothing is awaiting.
func (PrereviewState) AwaitingReplyCount ¶ added in v0.24.3
func (s PrereviewState) AwaitingReplyCount() int
AwaitingReplyCount tallies, across the WHOLE REVIEW, every reviewer reply still awaiting an agent response — the trailing run of reviewer entries on each comment/suggestion thread (#164). It is a per-REPLY counter, not per-comment: three replies in a row on one comment count as three, so the reviewer sees each reply register, and the tally drops to zero for a thread the moment the agent replies.
reopenIfReplied separately reopens a replied-on comment as "queued" in the per-file counts, but QueuedCount/HasQueue are per-file (#171): reply on a comment, then look at another file, and that work vanishes from the toolbar. So — like AwaitingApplyCount, and for the same "don't strand cross-file work" reason — this tally is deliberately whole-review and surfaced independently of the per-file HasQueue gate, so replies are always visible on the Queue badge whatever file you are on. A draft never has a thread, so it never contributes.
func (PrereviewState) BinaryKind ¶
func (s PrereviewState) BinaryKind() string
BinaryKind classifies an IsBinary FileDiff by extension so the viewer can render a real preview (<img>, <iframe>, <video>, <audio>) instead of the bare "Binary file — cannot display" message. Returns "" for formats with no in-browser viewer (e.g. .zip, .so, unknown ext) so the template falls back to the cannot-display copy. Extensions must be a subset of staticAllowedExt in main.go — the static fallback only serves what's on that allowlist, and an <img>/<iframe> pointing at a 404 would just show a broken icon.
func (PrereviewState) BlockAnnotations ¶ added in v0.23.1
func (s PrereviewState) BlockAnnotations() map[string]BlockAnn
BlockAnnotations aggregates the selected file's inline annotations into each rendered Markdown block by source line range, keyed "MB-<start>-<end>" (the template's $mbkey). Zero-arg; nil when none.
func (PrereviewState) BlockDiffStatus ¶ added in v0.22.0
func (s PrereviewState) BlockDiffStatus() map[string]string
BlockDiffStatus maps a rendered-Markdown block's data-key ("MB-<start>-<end>") to a diff-highlight class: "added" when every new-side line the block spans is an addition, "changed" when the block spans a mix of added and context lines. Blocks that span only unchanged (context) lines are omitted, so `{{index $.BlockDiffStatus $mbkey}}` yields "" and no class is applied.
This is what makes issue #110 — "diff highlighting should also show up in preview mode" — work: the code view colours each add/del line green/red, and this surfaces the same add signal on the rendered blocks. Only ADDED/CHANGED content is representable here: the preview renders the NEW side of the file, so a purely-deleted region has no block to mark (a documented gap, not a bug — interleaving old content would be a different feature).
Zero-arg by design (livetemplate only auto-invokes NumIn()==0 state methods), mirroring LineDisplay / ScrollHeadingBlockKey: the per-block predicate is precomputed into a map the template looks up by key.
Returns nil (no highlighting) when the rendered view isn't showing, when the reviewer picked File over Diff (the toolbar's Diff/File toggle — File mode shows the plain document, exactly as it drops the add/del colouring in the code view via `.code.file-view`), when a historical version is being viewed (block ranges are in the version's coordinates while CurrentDiff.Lines is the live diff — matching them is meaningless), or for a wholly-added / wholly-deleted file (Note "file added" / "file deleted"): the code view drops its green wall there too (`.code.pure-add`), so the preview stays consistent.
func (PrereviewState) CSVBasename ¶
func (s PrereviewState) CSVBasename() string
CSVBasename returns just the filename portion of CSVPath — useful for compact toast/banner display where the full repo path is noise.
func (PrereviewState) ChangedFilesCount ¶
func (s PrereviewState) ChangedFilesCount() int
ChangedFilesCount is how many files differ from the base. Zero-arg so the template can label the scope toggle without computing.
func (PrereviewState) CommentCount ¶ added in v0.24.0
func (s PrereviewState) CommentCount() int
CommentCount is the number of in-scope comments — the all-comments view's header total. Zero-arg because livetemplate only pre-computes zero-arg methods; the template can't do `len` over a scoped slice itself without one.
func (PrereviewState) CommentCountLines ¶ added in v0.23.0
func (s PrereviewState) CommentCountLines() map[string]int
CommentCountLines reports, per line-row (keyed "<toLine>-<side>", e.g. "4-new"), HOW MANY comments render on that row — driving the #151 right-margin count badge (and, via count>0, the #136 presence marks). Derived from the SAME filtered map the cards render from (CommentsByEndLine), so the badge's number always equals the cards actually rendered — side, individually-hidden, resolved filter, and file scope are all inherited. Zero-arg so the framework pre-computes it (a method WITH an arg silently breaks rendering); the row looks itself up as {{index $.CommentCountLines (printf "%d-%s" $ln $lside)}}. Nil when none, so a missing key indexes to 0. (SuggestionCountLines is its counterpart below.)
func (PrereviewState) CommentOpenLines ¶ added in v0.23.1
func (s PrereviewState) CommentOpenLines() map[string]bool
CommentOpenLines reports, per line-row, whether the row has any OPEN (unresolved) comment (#165). Drives the badge colour: yellow when open work remains, else green (all resolved). "Yellow wins on a mixed line" falls out — one open comment flags it.
func (PrereviewState) CommentsByEndLine ¶
func (s PrereviewState) CommentsByEndLine() map[int][]Comment
CommentsByEndLine groups the current comments by their ToLine — the line the comment trails. The template renders each line N, then inlines any comment whose ToLine == N right after it (GitHub-mobile-style). Zero-arg for the same reason as SelectedLines: the livetemplate framework only pre-computes zero-arg methods into the data map.
Restricted to the currently-selected file. Resolved comments are filtered out when ShowResolved is false so the diff stays focused on open issues.
func (PrereviewState) CurrentQuiz ¶ added in v0.25.0
func (s PrereviewState) CurrentQuiz() *Quiz
CurrentQuiz returns the quiz for the selected file, or nil. The agent can revise a quiz by re-appending its id (the loader keeps the last write), and can also submit a genuinely new one for the same file — in which case the newest wins, since that is the one the reviewer just asked for.
func (PrereviewState) DataMode ¶ added in v0.14.0
func (s PrereviewState) DataMode() string
DataMode is the value for the .theme-root data-mode attribute: "light" or "dark" when the mode is forced, "" for System (in which case the template omits the attribute and prefers-color-scheme drives the theme). Zero-arg so the page template can emit `{{with .DataMode}} data-mode="{{.}}"{{end}}`.
func (PrereviewState) DataScheme ¶ added in v0.14.0
func (s PrereviewState) DataScheme() string
DataScheme is the value for the .theme-root data-scheme attribute: the persisted SchemeName when it names a registered scheme, else the default (gitdiff.Schemes[0]). Always non-empty so the wrapper always carries a scheme — mirrors DataMode's fall-through-to-default contract. Zero-arg so the page template can emit `data-scheme="{{.DataScheme}}"`.
func (PrereviewState) DecisionCount ¶ added in v0.17.0
func (s PrereviewState) DecisionCount() int
DecisionCount is the number of current suggestions carrying a live (fingerprint- matching) decision — drives the "N decisions recorded" status. Zero-arg.
func (PrereviewState) DecisionsBySuggestion ¶ added in v0.17.0
func (s PrereviewState) DecisionsBySuggestion() map[string]SuggestionDecision
DecisionsBySuggestion maps each CURRENT in-scope suggestion's ID to its recorded decision, but ONLY when the decision's fingerprint still matches the suggestion's content. A same-id revision (new proposed text) changes the fingerprint, so its stale decision drops and the suggestion reads as undecided again; orphan decisions (the suggestion is gone) are dropped too. Zero-arg so the framework pre-computes it; the suggestionCard looks up its own ID via {{index $.DecisionsBySuggestion .ID}}.
Keyed off scopedSuggestions, so DecisionCount (its len) counts only this review's decisions — and the by-ID lookups it serves are all made from already-scoped cards.
func (PrereviewState) DoneCount ¶ added in v0.18.0
func (s PrereviewState) DoneCount() int
DoneCount is the number of comments the agent marked worked-on + suggestions it applied.
func (PrereviewState) DraftCount ¶ added in v0.18.0
func (s PrereviewState) DraftCount() int
DraftCount is the number of held (not-yet-enqueued) comments.
func (PrereviewState) FileBrowserHTML ¶ added in v0.13.0
func (s PrereviewState) FileBrowserHTML() template.HTML
FileBrowserHTML renders the drawer file browser (folder tree when unfiltered, flat path list while filtering) for injection into prereview.tmpl. Zero-arg so the template can emit `{{.FileBrowserHTML}}`; re-executed each render so selection / viewed / filter stay current.
func (PrereviewState) FileComments ¶
func (s PrereviewState) FileComments() []Comment
FileComments returns the selected file's visible LINE-anchored comments (honoring ShowResolved) as a flat slice. Zero-arg so the rendered-Markdown view can, per block, show the comments whose ToLine falls in that block's source range — the line-view path uses CommentsByEndLine (exact-line map) instead.
File-level (Kind=="file") and area-level (Kind=="area") comments are excluded here so they don't accidentally try to anchor at line 0 inside a block range. They're rendered in their own sections via FileLevelComments() and AreaComments().
func (PrereviewState) FileHeadAnnotationCount ¶ added in v0.25.0
func (s PrereviewState) FileHeadAnnotationCount() int
FileHeadAnnotationCount is how many annotations sit at the file head — the number the shared row badge shows there. Counts file-level and area comments alongside the head quiz questions, because they all collapse together.
func (PrereviewState) FileHeadHasOpen ¶ added in v0.25.0
func (s PrereviewState) FileHeadHasOpen() bool
FileHeadHasOpen reports whether anything at the file head is still open work, which is what makes the shared badge read "open" rather than "done".
func (PrereviewState) FileLevelComments ¶
func (s PrereviewState) FileLevelComments() []Comment
FileLevelComments returns the selected file's visible whole-file comments (Kind == "file") in creation order. Rendered in a dedicated section above the per-line body so reviewers see "comments on the file itself" before any line-anchored feedback.
func (PrereviewState) FileQuizItems ¶ added in v0.25.0
func (s PrereviewState) FileQuizItems() []QuizItem
FileQuizItems are the questions that have no valid position inside the diff, so they render at the file head — exactly where a kind=file COMMENT renders.
Two groups land here, for different reasons:
- kind=file: about the change as a whole, or about something absent from it. It never had a line, by design.
- UNGROUNDED: it claims a line the diff does not contain, so there is nowhere to anchor it. These MUST still render. Anchoring questions inline means an unresolvable anchor has no home, and quietly dropping it would HIDE a hallucinated question instead of surfacing it — undoing the entire point of the grounding check. The reviewer needs to see that the agent asked about code that isn't there.
func (PrereviewState) FileSuggestions ¶ added in v0.17.0
func (s PrereviewState) FileSuggestions() []Suggestion
FileSuggestions returns the selected file's suggestions as a flat slice (nil when toggled off), so the rendered-Markdown view can, per block, show the suggestions whose ToLine falls in that block's source range — the parallel of FileComments for the block view. The line/code view uses SuggestionsByEndLine.
func (PrereviewState) FileTree ¶ added in v0.13.0
func (s PrereviewState) FileTree() []*TreeNode
FileTree builds the directory tree from the scoped file list (so the Changed/All scope toggle applies exactly as it does to FilteredFiles). The search filter is intentionally NOT applied here: the template shows the flat FilteredFiles list while a filter is active and the tree only when it's empty.
Zero-arg so the framework pre-computes it once per render and the template can range `$.FileTree`. Returns the top-level nodes (depth 0).
func (PrereviewState) FilteredFileNodes ¶ added in v0.13.0
func (s PrereviewState) FilteredFileNodes() []*TreeNode
FilteredFileNodes wraps FilteredFiles as flat leaf TreeNodes so the flat (filter-active) list and the tree can share one `fileRow` template. Name is the full path here — the flat list shows full paths so a search reveals where each match lives; the tree sets Name to just the final segment.
Zero-arg so the template can range `$.FilteredFileNodes`.
func (PrereviewState) FilteredFiles ¶
func (s PrereviewState) FilteredFiles() []gitdiff.FileEntry
FilteredFiles returns the scoped files (see scopedFiles) further narrowed by FileFilter (case-insensitive substring match against path). Zero-arg so the framework pre-computes it once per render and the template can iterate `$.FilteredFiles`.
func (PrereviewState) FocusedComment ¶
func (s PrereviewState) FocusedComment() *Comment
FocusedComment returns the region annotation the user tapped to locate (FocusedCommentID), or nil. The template reads its URL + Area to tell the client which page to show and where to scroll the iframe.
func (PrereviewState) HasMarks ¶ added in v0.23.0
func (s PrereviewState) HasMarks() bool
HasMarks reports whether the selected file has any per-line comment/suggestion badges — i.e. whether the "Hide annotations" toggle (ToggleMarks) has anything to act on. Gates that menu entry. Reads the same memoized count maps the badges use.
func (PrereviewState) HasQueue ¶ added in v0.18.0
func (s PrereviewState) HasQueue() bool
HasQueue reports whether there's anything to show in the queue panel (any draft/queued/done comment or accepted/applied suggestion). Gates the toolbar indicator so it stays hidden on an empty review.
func (PrereviewState) HasQuiz ¶ added in v0.25.0
func (s PrereviewState) HasQuiz() bool
HasQuiz reports whether the selected file has a quiz to show — the gate for offering the quiz entry in the menus at all.
func (PrereviewState) HasReadFrontier ¶ added in v0.20.0
func (s PrereviewState) HasReadFrontier() bool
HasReadFrontier reports whether there's a meaningful "resume reading" jump to offer — some progress, but not the whole file read. Gates the Resume affordance. Zero-arg.
func (PrereviewState) HiddenResolvedCount ¶ added in v0.16.0
func (s PrereviewState) HiddenResolvedCount() int
HiddenResolvedCount returns how many in-scope resolved comments have been individually re-hidden — drives the "Unhide N" affordance next to the Show-resolved toggle.
func (PrereviewState) HiddenSuggestionCount ¶ added in v0.20.0
func (s PrereviewState) HiddenSuggestionCount() int
HiddenSuggestionCount is how many of the SELECTED file's suggestions are currently hidden (fingerprint-matching), driving the "N hidden · show" affordance. Scoped to the selected file so the count matches what a reviewer could reveal on the page they're looking at. Zero-arg.
func (PrereviewState) IsHTML ¶
func (s PrereviewState) IsHTML() bool
IsHTML reports whether the selected file is an HTML file (.html/.htm). Drives the Preview/Raw toolbar toggle and the iframe branch in the viewer template, parallel to IsMarkdown.
func (PrereviewState) IsMarkdown ¶
func (s PrereviewState) IsMarkdown() bool
IsMarkdown reports whether the selected file is Markdown.
func (PrereviewState) KeyBindings ¶ added in v0.14.0
func (s PrereviewState) KeyBindings() []KeyBinding
KeyBindings exposes the keymap to the template. Zero-arg so livetemplate's evaluator auto-invokes it; the template ranges over it for both the hidden window bindings and the help overlay. StreamOnly rows are dropped outside --agent mode so a repo-only reviewer gets neither a live binding nor a help row for the agent-queue Pause/Resume shortcut (there's no queue for them).
func (PrereviewState) KeyHint ¶ added in v0.16.0
func (s PrereviewState) KeyHint() map[string]string
KeyHint maps a button name to its Display key, so a button that a shortcut fires can surface the same key in its label (issue #89 / #118) — {{with index $.KeyHint "toggleFocusMode"}}<kbd>{{.}}</kbd>{{end}}. Keyed by both Action (the window-bound rows: nextFile, toggleFocusMode, …) and Button (rows fired from their own template binding: Save via Mod+Enter → "addComment", Cancel via Esc → "clearSelection"). Plain Enter — documented in the help overlay but with no toolbar button — has neither, so it never leaks a chip. Single-sourced from the same keyBindings slice as the live bindings and the help overlay, so a chip can never show a stale key.
Not filtered by AgentMode (unlike KeyBindings): a hint for a control that isn't rendered (the agent-only Pause button in repo mode) has no button to attach to, so it's harmless — and leaving it in keeps the lookup a pure map over the keymap. Zero-arg so livetemplate auto-invokes it (a method-with-arg would silently break rendering).
func (PrereviewState) LineDisplay ¶ added in v0.15.0
func (s PrereviewState) LineDisplay() map[string]template.HTML
LineDisplay maps each visible diff line's key ("L<old>-<new>", matching the template's $lkey) to its rendered content with every character-range (kind=text) comment wrapped in a <mark class="comment-span">. Zero-arg so livetemplate pre-computes it into the data map (it only pre-computes zero-arg methods — a method taking the line as an argument does NOT render); the template indexes it with {{index $.LineDisplay $lkey}}, mirroring how CommentsByEndLine / SelectedLines are consumed. (OldNum,NewNum) is unique per line — adds are (0,n), dels (m,0), ctx (m,n) — so the key never collides.
func (PrereviewState) NextScheme ¶ added in v0.14.0
func (s PrereviewState) NextScheme() string
NextScheme is the scheme the picker advances to from the current one, cycling gitdiff.Schemes in registry order and wrapping back to the first. Drives both the CycleScheme action and the picker's "switch to" label.
func (PrereviewState) NextSchemeLabel ¶ added in v0.14.0
func (s PrereviewState) NextSchemeLabel() string
func (PrereviewState) NextThemeMode ¶ added in v0.14.0
func (s PrereviewState) NextThemeMode() string
NextThemeMode is the mode the toolbar toggle advances to from the current one, cycling System → Light → Dark → System. Drives both the CycleTheme action and the toggle's label/icon ("show what a click switches TO").
func (PrereviewState) NextThemeModeLabel ¶ added in v0.14.0
func (s PrereviewState) NextThemeModeLabel() string
func (PrereviewState) OutdatedCount ¶
func (s PrereviewState) OutdatedCount() int
OutdatedCount returns how many non-resolved in-scope comments could not be confidently re-anchored (their line numbers no longer point at the intended content) — drives the header "N need re-anchoring" hint so drift is discoverable without opening every file.
func (PrereviewState) QueueHiddenCount ¶ added in v0.24.0
func (s PrereviewState) QueueHiddenCount() int
QueueHiddenCount is how many queue rows the CURRENT filter is hiding — the work that exists elsewhere in the review. Zero when Global (nothing is hidden) and zero in a single-file review (there is nowhere else). It is what stops the per-file default from concealing a backlog: the switch advertises what is behind it.
func (PrereviewState) QueueItems ¶ added in v0.18.0
func (s PrereviewState) QueueItems() []QueueItem
QueueItems returns the queue rows ordered by lifecycle — queued (remaining) first, then done, then drafts — so the panel leads with what still needs the agent's attention. Resolved/outdated comments and un-queued suggestions are excluded (QueueState "") — UNLESS the reviewer replied on one last (#164), which reopens it as "queued" work via reopenIfReplied.
func (PrereviewState) QueueScopeLabel ¶ added in v0.24.0
func (s PrereviewState) QueueScopeLabel() string
QueueScopeLabel names what the queue is currently showing — the switch's label, and the honest answer to "why is this count different from the one I saw a second ago".
func (PrereviewState) QueuedCount ¶ added in v0.18.0
func (s PrereviewState) QueuedCount() int
QueuedCount is the number of enqueued-but-not-yet-done comments + accepted-but- not-yet-applied suggestions — the "remaining" work the agent still has to pick up.
func (PrereviewState) QuizAnsweredCount ¶ added in v0.25.0
func (s PrereviewState) QuizAnsweredCount() int
QuizAnsweredCount is how many of them have been answered.
func (PrereviewState) QuizByEndLine ¶ added in v0.25.0
func (s PrereviewState) QuizByEndLine() map[int][]QuizItem
QuizByEndLine groups the current quiz's LINE-anchored questions by the line they end on, so the diff view can render each one inline right under the code it asks about — the same shape CommentsByEndLine and SuggestionsByEndLine use.
This is what makes a quiz question just another annotation. The first version put the quiz on its own screen and then had to reproduce the cited code inside it, which was a worse re-implementation of the diff view; anchoring the question to the line makes the code its own context.
func (PrereviewState) QuizComplete ¶ added in v0.25.0
func (s PrereviewState) QuizComplete() bool
QuizComplete reports that every question has been answered.
func (PrereviewState) QuizCorrectCount ¶ added in v0.25.0
func (s PrereviewState) QuizCorrectCount() int
QuizCorrectCount is how many were answered correctly.
func (PrereviewState) QuizCountLines ¶ added in v0.25.0
func (s PrereviewState) QuizCountLines() map[string]int
QuizCountLines counts the quiz questions anchored to each row, keyed "<line>-<side>" exactly like CommentCountLines — so a question contributes to the row's annotation badge and can be collapsed with everything else on that line. A quiz question is an annotation; it collapses like one.
func (PrereviewState) QuizItems ¶ added in v0.25.0
func (s PrereviewState) QuizItems() []QuizItem
QuizItems is the current quiz's questions with the reviewer's progress folded in. Empty when there is no quiz, so the template can range over it directly.
func (PrereviewState) QuizNavLabel ¶ added in v0.25.0
func (s PrereviewState) QuizNavLabel() string
QuizNavLabel summarises the navigator's state, e.g. "2/5 answered · 1 right".
Progress and score are DIFFERENT fractions, and an earlier version showed "1/5 answered" beside a "0/5" chip — two ratios side by side that looked like they should agree. They are one phrase now, and the chip says what it does ("Overview") rather than carrying a competing number.
func (PrereviewState) QuizOpenLines ¶ added in v0.25.0
func (s PrereviewState) QuizOpenLines() map[string]bool
QuizOpenLines marks rows carrying an UNANSWERED question — open work, so the badge reads open (yellow) rather than done, matching an unresolved comment.
func (PrereviewState) QuizPending ¶ added in v0.25.0
func (s PrereviewState) QuizPending() bool
QuizPending reports that a "Quiz me" request for the selected file is awaiting an answer, so the control shows a disabled "Quiz requested…" rather than inviting a second tap that would queue a duplicate.
func (PrereviewState) QuizQuestionCount ¶ added in v0.25.0
func (s PrereviewState) QuizQuestionCount() int
QuizQuestionCount is the number of questions in the current quiz.
func (PrereviewState) QuizResults ¶ added in v0.25.0
func (s PrereviewState) QuizResults() []StreamQuiz
QuizResults reports every quiz's outcome for the agent snapshot (#191).
It is ADVISORY: it never gates a verdict, it just lets the agent tell "accepted after a comprehension check" from "accepted without one". That distinction is the whole point — an accept records that the reviewer clicked, and this is the only signal that says whether they also understood.
Unlike the view helpers it walks every quiz, not just the selected file's, since the snapshot is not scoped to whatever file happens to be open — but it IS scoped to the review (inScope), the same as scopedComments and scopedSuggestions. Without that, a single-file review sharing a pre-#199 store reported the sibling file's quizzes as this file's comprehension record.
func (PrereviewState) QuizScoreLabel ¶ added in v0.25.0
func (s PrereviewState) QuizScoreLabel() string
QuizScoreLabel renders the running score for the quiz header, e.g. "2/5".
func (PrereviewState) ReadFrontierKey ¶ added in v0.20.0
func (s PrereviewState) ReadFrontierKey() string
ReadFrontierKey returns the data-key of the element at the read frontier — the furthest-read line/block — for the CURRENT view (#128), so a "resume reading" jump can scroll straight to where the reviewer left off. It matches the exact key the template stamps on that element (a code line "L<old>-<new>" or a rendered-markdown block "MB-<start>-<end>"), so lvt-fx:scroll finds it. Empty when nothing's read or no diff is loaded. Zero-arg.
func (PrereviewState) ReadPercent ¶ added in v0.20.0
func (s PrereviewState) ReadPercent() int
ReadPercent is how far through the selected file the reviewer has read (0..100): the read high-water mark (ReadThrough — the furthest new-side line scrolled past) as a fraction of the file's line count. Drives the top reading-progress bar (#128). Zero-arg; 0 when nothing's been read or the file length is unknown. Because it's the high-water mark, it only grows — scrolling back up doesn't shrink it (you've still reviewed that far).
func (PrereviewState) RegionComments ¶
func (s PrereviewState) RegionComments() []Comment
RegionComments returns the visible region annotations (kind="region") for the CURRENT proxied page (--external mode), in creation order. Rendered as the re-pin overlay markers + paired list entries. Scoped by CurrentURL (the page the iframe is on) rather than SelectedFile, since external mode has no file. Zero-arg so the template can range over it.
func (PrereviewState) RenderedHTML ¶
func (s PrereviewState) RenderedHTML() []gitdiff.HTMLBlock
RenderedHTML is the block list for the rendered HTML view (nil unless ShowRenderedHTML). Each block carries its real source line range so comments stay line-accurate across rendered and raw views — same contract as RenderedMarkdown. The preview itself renders in a single iframe (RenderedHTMLDoc); these ranges drive the comments list below it.
func (PrereviewState) RenderedHTMLDoc ¶
func (s PrereviewState) RenderedHTMLDoc() string
RenderedHTMLDoc is the preview document for the iframe srcdoc (empty unless ShowRenderedHTML). The whole file rendered with real-document fidelity; the client wires clicks inside it back to a block via the data-from/data-to ranges.
func (PrereviewState) RenderedHeadings ¶
func (s PrereviewState) RenderedHeadings() []gitdiff.Heading
RenderedHeadings returns the TOC entries for the current Markdown file, filtered to h1–h3 (deeper levels create visual noise without adding navigational value for typical docs). Returns nil when the rendered view isn't showing OR when there are fewer than two headings — a TOC with one entry is pointless and just steals horizontal real estate from the prose.
func (PrereviewState) RenderedMarkdown ¶
func (s PrereviewState) RenderedMarkdown() []gitdiff.MarkdownBlock
RenderedMarkdown is the block list for the rendered view (nil unless ShowRenderedMarkdown). Each block carries its real source line range so comments stay line-accurate across rendered and raw views.
func (PrereviewState) ResolvedCount ¶
func (s PrereviewState) ResolvedCount() int
ResolvedCount returns how many of the in-scope comments are resolved — useful for "(N resolved hidden)" status copy. Counts every resolved comment, individually-hidden or not (it gates the "Show resolved" toggle's visibility).
func (PrereviewState) Revealing ¶ added in v0.16.0
func (s PrereviewState) Revealing() bool
Revealing reports whether the currently-selected file is being shown as its full RAW source because a search-jump set RevealFile to it — see RevealFile. Zero-arg so VisibleLines / ShowRendered* can gate on it.
func (PrereviewState) RowToggled ¶ added in v0.24.1
func (s PrereviewState) RowToggled() map[string]bool
RowToggled reports which rows currently render flipped away from their default — the `row-toggled` class the templates emit. Zero-arg so livetemplate pre-computes it.
A toggle EXPIRES when the row's default changes underneath it. A toggle means "flip THIS row away from THIS default"; once the default is no longer that, the instruction is stale and re-applying it inverts the reviewer's intent. Concretely: peek a done suggestion (the row is collapsed-by-default, so the toggle means SHOW), then click Undo — the suggestion goes back to undecided, the row's default becomes open, and the stale toggle would now mean HIDE, vanishing the card the reviewer is working on. Same for reopening a resolved comment. Dropping the stale entry restores the default view, which is what the reviewer wants to see the moment the row's state changes.
func (PrereviewState) SchemeLabel ¶ added in v0.14.0
func (s PrereviewState) SchemeLabel() string
SchemeLabel / NextSchemeLabel are the human names (gitdiff.Scheme.Label) of the current and next schemes, for the picker's tooltip ("Theme: Solarized — switch to Gruvbox") and the overflow-menu item.
func (PrereviewState) ScopeFileCount ¶ added in v0.14.0
func (s PrereviewState) ScopeFileCount() int
ScopeFileCount / ScopeViewedCount drive the "X/Y viewed" toolbar progress so it counts against the files actually in the review SCOPE (what the drawer shows) — changed-only by default. So a repo with 144 tracked files and 1 changed reads "0/1 viewed", not "0/144". Falls back to all files when the scope is all (clean tree or ShowAllFiles), consistent with scopedFiles. Zero-arg so the template can read them directly.
func (PrereviewState) ScopeViewedCount ¶ added in v0.14.0
func (s PrereviewState) ScopeViewedCount() int
func (PrereviewState) ScrollHeadingBlockKey ¶
func (s PrereviewState) ScrollHeadingBlockKey() string
ScrollHeadingBlockKey returns the `data-key` (e.g. "MB-5-10") of the MarkdownBlock containing the heading currently targeted by ScrollToHeadingID — or "" when no scroll is requested or the heading is not found. The template compares this against each block's own data-key to gate a `lvt-fx:scroll="into-view"` directive; see NavigateToHeading and issue #12.
Zero-arg by design: livetemplate's template evaluator (livetemplate/internal/parse/eval.go callMethod) only auto-invokes methods with NumIn() == 0, so a "does this block match" predicate taking the block's range as arguments isn't reachable from the template. Precomputing the matching key here and comparing with the builtin `eq`/`printf` keeps the hot path in Go.
func (PrereviewState) SearchScopeCount ¶ added in v0.16.0
func (s PrereviewState) SearchScopeCount() int
SearchScopeCount is the number of files the current search scope covers — changed files by default, or every file when SearchScopeAll. Drives the scope toggle's count. Zero-arg for the template.
func (PrereviewState) SearchScopeLabel ¶ added in v0.16.0
func (s PrereviewState) SearchScopeLabel() string
SearchScopeLabel is the human label for the current search scope toggle.
func (PrereviewState) SearchTruncated ¶ added in v0.16.0
func (s PrereviewState) SearchTruncated() bool
SearchTruncated reports whether the result list hit the cap (so the palette can note "showing first N"). Zero-arg for the template.
func (PrereviewState) SelectedFileBase ¶ added in v0.20.0
func (s PrereviewState) SelectedFileBase() string
SelectedFileBase / SelectedFileDir split the selected file's path for the file-head: the basename is shown bold and always in full (it identifies the file), the directory prefix is muted and truncates when the header is tight, so a long/deep path never wraps into a multi-line block that shoves the action chips off-screen. Dir includes a trailing "/"; empty for a top-level file. Zero-arg state methods (safe — a method on the nested *FileDiff wouldn't render).
func (PrereviewState) SelectedFileDir ¶ added in v0.20.0
func (s PrereviewState) SelectedFileDir() string
func (PrereviewState) SelectedLines ¶
func (s PrereviewState) SelectedLines() map[int]bool
SelectedLines returns a set of line numbers currently selected. Zero-arg so the livetemplate framework eagerly pre-computes it once per render (the framework only pre-computes zero-arg methods, so a SelectionContains(n) helper would not be callable from the template). The template membership-tests with `{{index $.SelectedLines $n}}` — index returns the zero value (false) for missing keys, which is exactly what we want for unselected lines.
func (PrereviewState) SelectionEmpty ¶
func (s PrereviewState) SelectionEmpty() bool
SelectionEmpty reports whether nothing is currently selected.
func (PrereviewState) SelectionEndMax ¶
func (s PrereviewState) SelectionEndMax() int
SelectionEndMax returns max(SelectionAnchor, SelectionEnd) — the line after which the inline composer should render. Zero means "no selection, don't render the composer". Order-independent so the user can pick anchor=10 → end=5 and the composer still lands after line 10.
func (PrereviewState) SelectionLabel ¶
func (s PrereviewState) SelectionLabel() string
SelectionLabel returns the human-readable form of the current selection (e.g. "L42" or "L42-L48"; for a text selection "L42:6-12" or "L42:6-L48:3"). Empty string when nothing is selected. Kept consistent with Comment.LineSpan so the composer heading matches the saved card's badge.
func (PrereviewState) ShowQuizNav ¶ added in v0.25.0
func (s PrereviewState) ShowQuizNav() bool
ShowQuizNav gates the navigator: it appears whenever the open file has a quiz, unless dismissed or the overview is already showing (where it would duplicate the list it sits above).
func (PrereviewState) ShowRenderedHTML ¶
func (s PrereviewState) ShowRenderedHTML() bool
ShowRenderedHTML is true when the viewer should swap the line view for the inline-blocks view: an HTML file, not toggled to raw, with at least one rendered block. Mirrors ShowRenderedMarkdown — a deleted / empty file falls through to the line view (showing the diff) instead of an empty preview pane.
func (PrereviewState) ShowRenderedMarkdown ¶
func (s PrereviewState) ShowRenderedMarkdown() bool
ShowRenderedMarkdown is true when the viewer should show the rendered-Markdown blocks instead of the line view: a Markdown file, not toggled to raw, with at least one rendered block. Zero-arg so the template can branch on `$.ShowRenderedMarkdown`.
func (PrereviewState) SuggestionAcceptedLines ¶ added in v0.23.1
func (s PrereviewState) SuggestionAcceptedLines() map[string]bool
SuggestionAcceptedLines reports, per line-row, whether the row has any ACCEPTED-but- unapplied suggestion (#165) — drives the accentuated-yellow "accepted" badge. Loses to "open" on a mixed row (an undecided item is more urgent), which the template's is-open→is-accepted→is-done ladder enforces.
func (PrereviewState) SuggestionCount ¶ added in v0.17.0
func (s PrereviewState) SuggestionCount() int
SuggestionCount is the number of suggestions on the selected file, used to label the toolbar toggle and gate its visibility. Counts regardless of the HideSuggestions toggle (it's the total available, not the shown count).
func (PrereviewState) SuggestionCountLines ¶ added in v0.23.0
func (s PrereviewState) SuggestionCountLines() map[string]int
SuggestionCountLines counts ALL suggestions per row (#165) — open + accepted. The badge colour (yellow if any open, else green) comes from SuggestionOpenLines; the accepted ones collapse to that badge via CSS rather than being excluded here.
func (PrereviewState) SuggestionGroups ¶ added in v0.20.0
func (s PrereviewState) SuggestionGroups() map[string]SuggestionGroupInfo
SuggestionGroups maps each suggestion ID that belongs to a multi-member alternatives group (same File/Side/range/OriginalText) to its position + group size, so the card can show "N of M" + a shared visual — a clear signal that the suggestions are alternatives, not independent edits. Zero-arg so the framework pre-computes it; the card looks up {{index $.SuggestionGroups .ID}}. Computed over the SAME visible set the views render (hidden suggestions don't count), so the "N of M" never over- or under-states what the reviewer can see.
func (PrereviewState) SuggestionOpenLines ¶ added in v0.23.1
func (s PrereviewState) SuggestionOpenLines() map[string]bool
SuggestionOpenLines reports, per line-row, whether the row has any UNDECIDED suggestion (#165) — drives the yellow "open" badge. An accepted-pending suggestion is NOT open (it gets the accentuated-yellow badge via SuggestionAcceptedLines); an applied/rejected one is done (green).
func (PrereviewState) SuggestionsByEndLine ¶ added in v0.17.0
func (s PrereviewState) SuggestionsByEndLine() map[int][]Suggestion
SuggestionsByEndLine renders ALL of the selected file's suggestions inline (#165); an applied suggestion collapses to a green count badge via CSS, not by exclusion here (so the reviewer can still peek it open for the M4 status/revert controls).
func (PrereviewState) ThemeModeLabel ¶ added in v0.14.0
func (s PrereviewState) ThemeModeLabel() string
ThemeModeLabel / NextThemeModeLabel are the human names of the current and next modes, for the toggle's tooltip ("Theme: System — switch to Light") and the overflow-menu item. (The button's icon is chosen by an explicit ladder in the template, since Go templates can't take a dynamic {{template}} name.)
func (PrereviewState) Threads ¶ added in v0.23.0
func (s PrereviewState) Threads() map[string][]ThreadEntry
Threads groups the loaded conversation entries (#149) by target ID (a comment or suggestion ID), so a card renders its own thread with {{index $.Threads .ID}}. Entries arrive already sorted (loadThreads), so each group is chronological. Zero-arg so the framework pre-computes it; nil when there are no threads.
func (PrereviewState) URLHash ¶
func (s PrereviewState) URLHash() string
URLHash returns the canonical hash string for the current state, suitable for placement in `data-lvt-url-hash` on the body. Returns "" when no file is selected (the directive then no-ops on mirror). Order of precedence: SelectedFile + SelectionAnchor/End (line range is the most specific target a user is viewing) > SelectedFile + URLHashScrollAnchor > SelectedFile alone. The line-range form matches the gutter `<a>` permalinks; the anchor form survives a markdown TOC click or an HTML deep link until the user moves.
func (PrereviewState) ViewedCount ¶
func (s PrereviewState) ViewedCount() int
ViewedCount returns the number of files in state.Files marked as viewed. Zero-arg so the template can show "N of M reviewed" without computing.
func (PrereviewState) VisibleComments ¶
func (s PrereviewState) VisibleComments() []Comment
VisibleComments returns the in-scope Comments filtered by the resolved/hidden view rule. Zero-arg so the framework eagerly evaluates and the template iterates the filtered list directly. Note there is no `if ShowResolved { return all }` fast-path: an individually-hidden resolved comment must stay out even when the group is shown.
func (PrereviewState) VisibleLines ¶
func (s PrereviewState) VisibleLines() []gitdiff.DiffLine
VisibleLines is the line set the viewer renders for the selected file, per the current mode:
- File view (FileView == true): the entire current working-tree file — every line that exists on the new side (add + ctx), deletions excluded since they aren't in the file anymore. No diff, no folds.
- Diff view (FileView == false): a real diff — only changed lines plus 3 lines of context, long unchanged gaps replaced by fold markers (see gitdiff.CollapseToHunks). An unchanged file has no diff, so CollapseToHunks returns it whole.
Zero-arg so the framework pre-computes it once per render and the template ranges `$.VisibleLines`. Line numbers are identical across modes, so comments anchored in one mode resolve in the other.
type ProcessedMark ¶ added in v0.16.0
ProcessedMark is one line of an agent-owned marks file (processed.jsonl for done'd comments, applied.jsonl for applied suggestions): the target ID the agent acted on plus an informational timestamp (the server only uses ID).
type Prompt ¶ added in v0.23.0
Prompt is one "ask for suggestions" template. Slug is the stable id (the filename without .md); Title is shown in the picker; Body pre-fills the file-comment composer and, once saved, is the instruction the agent acts on.
func LoadPrompts ¶ added in v0.23.0
LoadPrompts returns the embedded built-in prompts overlaid with the user's library at userDir. A user file with the same slug (filename) OVERRIDES the built-in; new slugs are added. Sorted by title for a stable picker. Tolerant by design: a missing user dir, or an unreadable / title-only file, is skipped — the picker must never break a review.
func LoadQuizPrompts ¶ added in v0.25.0
LoadQuizPrompts returns the embedded built-in quiz prompts overlaid with the user's library at userDir (~/.config/prereview/quizzes/*.md), sharing the suggestions library's loader and its rules: same "# Title" + body format, a user file OVERRIDES a built-in of the same slug, and anything unreadable is skipped rather than failing.
User-authored quiz prompts are first-class on purpose. That is only safe because a prompt is guidance and never the guarantee — ValidateQuiz and the server's grounding check bind whatever prompt produced the quiz, so a custom prompt can make a quiz boring but not structurally invalid or hallucinated.
type Question ¶ added in v0.25.0
type Question struct {
ID string `json:"id"`
Kind string `json:"kind"` // line (default) | text | file | area | region
Probe string `json:"probe"`
Prompt string `json:"prompt"`
Options []string `json:"options"`
Answer int `json:"answer"` // 0-based index into Options
Why string `json:"why"`
// Anchor, by kind — the same fields the CSV carries for comments.
FromLine int `json:"from_line"` // line/text
ToLine int `json:"to_line"`
FromCol int `json:"from_col"` // text
ToCol int `json:"to_col"`
Side string `json:"side"` // "new" (default) | "old"
Area *Area `json:"area,omitempty"` // area: {x,y,w,h} 0..1 fractions
URL string `json:"url,omitempty"` // region: the live page
// AnchorStatus is DERIVED at load time by applyQuiz against the live diff —
// kept off the JSON so the append-only file stays the agent's pure input.
AnchorStatus string `json:"-"`
}
Question is one multiple-choice item. Answer indexes into Options; Why is the explanation revealed after answering (immediate explanatory feedback is the headline recommendation in the retrieval-practice literature, so it is required, not optional).
FromLine/ToLine/Side ground the question in the diff. Side is load-bearing and NOT redundant with the line numbers: a modified line exists on BOTH the old and new side, so a line number alone is ambiguous — the same reason Comment carries a side column. Without it the "jump to the cited code" link can land on the wrong row of a modified pair.
FromLine == 0 means DELIBERATELY ANCHORLESS and is legal only for ProbeDecision, whose subject can be an omission ("chose not to add a test for the error path") with no lines to point at. Every other probe must anchor.
func (Question) AnchorLabel ¶ added in v0.25.0
AnchorLabel is the badge for a question's line reference: "L42", "L42-L48", or empty when the question is deliberately about something absent.
func (Question) Anchorless ¶ added in v0.25.0
Anchorless reports that the question carries no line anchor. It is now a property of the KIND (file/area/region) rather than a from_line==0 sentinel, so "this question is about the change as a whole, or about something absent from it" is stated in the same vocabulary comments use.
Still distinct from Ungrounded: anchorless is a declared position, ungrounded is a claimed line that does not resolve.
func (Question) EndLine ¶ added in v0.25.0
EndLine is the line a question trails — the row its inline card renders under. Clamped up to FromLine so a malformed to_line (reachable only from a hand-appended quiz.jsonl line; NormalizeQuiz fixes the write path) never underflows the row key.
func (Question) Jumpable ¶ added in v0.25.0
Jumpable reports whether this question offers a "jump to the code" link — only when it both claims an anchor AND that anchor resolved in the current diff. An ungrounded question must never offer a jump: the link would land on code that has nothing to do with the question.
func (Question) LineAnchored ¶ added in v0.25.0
LineAnchored reports that the question points at a line range — the kinds the grounding check applies to.
func (Question) LineGrounded ¶ added in v0.25.0
LineGrounded reports that the question is anchored to a line AND that line resolves in the current diff — i.e. it renders inline under its code rather than at the file head. Its negation is the head set: unanchored kinds and hallucinated (ungrounded) anchors alike lose their inline home.
NOT the same as Jumpable, which is stricter (AnchorStatus == "ok"): an unset AnchorStatus (grounding not yet run) counts as grounded here but is not jumpable, matching every site that partitions head-vs-line by !Ungrounded().
func (Question) Ungrounded ¶ added in v0.25.0
Ungrounded reports that the question claimed a line range that does not resolve in the current diff — a hallucinated anchor, rendered with a warning.
type QueueItem ¶ added in v0.18.0
type QueueItem struct {
ID string
Kind string // queueKindComment | queueKindSuggestion
File string
Line int // new-side line (0 for file/region/area comments)
Body string // the comment text / suggestion note, shown truncated by CSS
State string // queueDraft | queueQueued | queueDone
}
QueueItem is one row of the queue panel — a presentation view over a comment or an accepted suggestion (Kind distinguishes them so the row can render the ✦ marker and route its jump to the right target).
type Quiz ¶ added in v0.25.0
type Quiz struct {
ID string `json:"id"`
File string `json:"file"`
Prompt string `json:"prompt,omitempty"` // slug of the quiz prompt used
Fingerprint string `json:"fingerprint,omitempty"`
Questions []Question `json:"questions"`
CreatedAt string `json:"created_at,omitempty"`
}
Quiz is one generated comprehension quiz about one file's diff.
The JSON tags are the agent-facing schema written to quiz.jsonl by `prereview quiz`. Fingerprint pins the quiz to the diff it was generated from so a later edit can mark it stale rather than silently quizzing the reviewer on code that has since changed.
func LoadQuizzes ¶ added in v0.25.0
LoadQuizzes reads the agent-owned quizzes for the store whose CSV lives at csvPath.
func NormalizeQuiz ¶ added in v0.25.0
NormalizeQuiz fills the defaults the agent may omit, so a payload round-trips through the loader unchanged. Applied by the CLI verb before validation.
type QuizAnswer ¶ added in v0.25.0
type QuizAnswer struct {
QuizID string `json:"quiz_id"`
QuestionID string `json:"question_id"`
Choice int `json:"choice"`
At int64 `json:"at"` // UnixNano, matching ThreadEntry
}
QuizAnswer is one reviewer response, in the server-owned answers file. Choice indexes into the question's Options; -1 means cleared (a retake).
type QuizItem ¶ added in v0.25.0
type QuizItem struct {
Question
// QuizID is carried on the row so the card partial is SELF-CONTAINED: it can
// build its answer form without reaching for root state, which keeps it
// renderable from the diff view, the file head and the overview alike.
QuizID string
// Num is the question's 1-based position, for the navigator badge. (An earlier
// Index field was removed as dead code; the navigator gave it a purpose.)
Num int
// ThreadID is this question's global identity for the #149 conversation
// machinery, and Thread the conversation so far. Carried on the row so the card
// partial stays self-contained.
ThreadID string
Thread []ThreadEntry
Replying bool
ReplyDraft string
Answered bool
Choice int // the option the reviewer picked; meaningless unless Answered
Correct bool
}
QuizItem is one question plus the reviewer's progress on it: a flattened view row, so the template needs no lookups and no per-row logic.
type SearchHit ¶ added in v0.16.0
type SearchHit struct {
File string `json:"file"`
Kind string `json:"kind"` // "file" | "line"
OldNum int `json:"old_num"`
NewNum int `json:"new_num"`
Line template.HTML `json:"line"` // empty for Kind == "file"
}
SearchHit is one row in the palette: either a file-PATH match (Kind "file", no line) or a line-CONTENT match (Kind "line") with the 1-based diff line numbers and the matched line pre-escaped + <mark>-highlighted.
type StreamComment ¶
type StreamComment struct {
ID string `json:"id"`
Kind string `json:"kind"`
File string `json:"file"`
FromLine int `json:"from_line"`
ToLine int `json:"to_line"`
FromCol int `json:"from_col"`
ToCol int `json:"to_col"`
Side string `json:"side"`
Body string `json:"body"`
URL string `json:"url"`
Area *Area `json:"area"`
CreatedAt string `json:"created_at"`
AnchorStatus string `json:"anchor_status"`
// Text is the exact selected substring for kind=text comments (the phrase
// the reviewer highlighted). Lets the consuming LLM see WHAT was commented
// on — essential for rendered-view (Preview) comments, which anchor at
// line level (no columns) so the phrase is the only sub-line signal.
Text string `json:"text,omitempty"`
// Thread is the #149 conversation on this comment: the agent's prior replies
// and the reviewer's follow-ups, oldest first. Present when non-empty so the
// agent has the full exchange — and can tell a fresh comment (no thread) from a
// reviewer reply it must respond to (the last entry's author is "reviewer").
Thread []StreamReply `json:"thread,omitempty"`
}
StreamComment is the consumer-facing shape of a Comment: the CSV fields minus the opaque `anchor` fingerprint (the consumer must not parse it) and minus `resolved` (a snapshot is pre-filtered to actionable rows). Area is a nested object (or null) rather than a JSON-in-a-string blob, so the consumer never parses nested JSON.
func LoadComments ¶ added in v0.22.0
func LoadComments(csvPath string, all bool) ([]StreamComment, error)
LoadComments reads the review's comments.csv at csvPath and returns the comments as StreamComment — the SAME JSON shape the live snapshot emits, so the `prereview comments` reader and the stream never diverge. With all=false it returns only the actionable set (unresolved, non-outdated, non-draft — what the agent should act on); with all=true, every comment. The returned slice is always non-nil (JSON-encodes as `[]`, never `null`).
Scoped to the session's file (#171) — comments.csv is shared by every file reviewed from the store's directory, and `prereview comments` / `done` must not list work from a file the reviewer isn't reviewing. Unscoped when the store holds no session scope (a directory review, or a pre-#171 store).
This is the CLI's read path, NOT the server's: the server reads the CSV through loadCommentsFromDisk, which stays UNSCOPED on purpose because its result is the buffer persist() rewrites comments.csv from. Scoping that one would delete every other file's rows from disk.
type StreamDecision ¶ added in v0.17.0
type StreamDecision struct {
ID string `json:"id"`
File string `json:"file"`
FromLine int `json:"from_line"`
ToLine int `json:"to_line"`
Side string `json:"side"`
Verdict string `json:"verdict"` // accept | reject | revert (#159 M4.2, wire-only)
Note string `json:"note,omitempty"`
Original string `json:"original"`
Proposed string `json:"proposed"`
AnchorStatus string `json:"anchor_status"`
// Thread is the #149 conversation on this suggestion (see StreamComment.Thread).
Thread []StreamReply `json:"thread,omitempty"`
}
StreamDecision is the consumer-facing shape of a reviewer's decision on one LLM suggestion: the verdict + note joined with the suggestion's content and location, so the LLM has everything it needs to act without reading any file itself. Emitted only for decided, non-outdated suggestions (see actionableDecisions).
type StreamEvent ¶
type StreamEvent struct {
Event string `json:"event"`
Seq int `json:"seq"`
Ts string `json:"ts"`
Repo string `json:"repo,omitempty"` // ready
CSV string `json:"csv,omitempty"` // ready
// Comments is a pointer so a snapshot always serializes the key — `[]` when
// empty, never absent — while ready/end (nil) omit it. A consumer
// keying event["comments"] on a snapshot never chokes on a missing field.
Comments *[]StreamComment `json:"comments,omitempty"` // snapshot
// Suggestions carries the reviewer's decisions on the LLM's suggested edits
// (issue #98): the decided, non-outdated suggestions the LLM should act on —
// apply accepts, rework revises, drop rejects. Same pointer convention as
// Comments: always present on a snapshot (`[]` when none), absent elsewhere.
Suggestions *[]StreamDecision `json:"suggestions,omitempty"` // snapshot
// Paused reports that the reviewer paused the queue (batching): the agent's
// `watch` will block until resume, which then delivers one coalesced snapshot.
Paused bool `json:"paused,omitempty"`
// Quizzes reports the reviewer's comprehension-quiz results (#191) — one row
// per quiz the agent generated. ADVISORY ONLY: it never gates anything, it
// tells the agent whether a file was accepted after a comprehension check or
// without one. Omitted entirely when no quiz exists, so the snapshot shape is
// unchanged for every review that doesn't use the feature.
Quizzes []StreamQuiz `json:"quizzes,omitempty"` // ready / snapshot
// SkillUpdated is set on the `ready` event when this launch refreshed the
// installed prereview skill to match the (possibly self-updated) binary — so
// the agent's loaded skill is now stale. The agent should re-read the skill
// from its install path before continuing and tell the user to reload it.
SkillUpdated bool `json:"skill_updated,omitempty"` // ready
}
StreamEvent is one line of the event log. Per-event-type fields are omitempty so each event carries only what it needs; the leading event/seq/ts are always present and, being struct fields, keep their declared order (a map would sort them alphabetically).
func (StreamEvent) CommentList ¶
func (e StreamEvent) CommentList() []StreamComment
CommentList returns the event's comment snapshot, or nil for events that carry none (ready / end).
func (StreamEvent) DecisionList ¶ added in v0.17.0
func (e StreamEvent) DecisionList() []StreamDecision
DecisionList returns the event's suggestion-decision snapshot, or nil for events that carry none.
type StreamQuiz ¶ added in v0.25.0
type StreamQuiz struct {
File string `json:"file"`
QuizID string `json:"quiz_id"`
Taken bool `json:"taken"`
Score int `json:"score"`
Total int `json:"total"`
}
StreamQuiz is one quiz's outcome as the agent sees it. Taken is false when the reviewer has not answered anything yet — distinct from a 0/N score, which means they answered and got them all wrong.
type StreamReply ¶ added in v0.23.0
type StreamReply struct {
Author string `json:"author"`
Body string `json:"body"`
At string `json:"at"`
}
StreamReply is one consumer-facing thread entry (#149): who said it, what, when. At is RFC3339 (not the internal nanoseconds) to match CreatedAt's convention.
type Suggestion ¶ added in v0.17.0
type Suggestion struct {
ID string `json:"id"`
File string `json:"file"`
FromLine int `json:"from_line"`
ToLine int `json:"to_line"`
Side string `json:"side"` // "new" (default) | "old" | "both"
OriginalText string `json:"original"`
ProposedText string `json:"proposed"`
Note string `json:"note,omitempty"` // optional rationale from the LLM
// Anchor/AnchorStatus are DERIVED at load time from OriginalText + the live
// file (not part of the agent's payload) — the drift fingerprint. Kept off
// the JSON so the append-only file stays the agent's pure input.
Anchor CommentAnchor `json:"-"`
AnchorStatus string `json:"-"`
}
Suggestion is one proposed edit: replace the OriginalText currently at [FromLine,ToLine] (new side) with ProposedText. It is rendered inline as a suggestion box, visually distinct from a comment.
The JSON tags are the agent-facing schema written to suggestions.jsonl by `prereview suggest` (snake_case, mirroring the CSV column names). FromLine/ ToLine are the LLM's hint; the authoritative fingerprint is OriginalText, which is re-located in the live file on every load (see relocateSuggestion), so a suggestion drifts exactly like a comment and goes `outdated` when its target text is gone or ambiguous — it is never silently misplaced onto the wrong content.
func LoadSuggestions ¶ added in v0.23.0
func LoadSuggestions(csvPath string) []Suggestion
LoadSuggestions reads the agent-owned suggestions for the store whose CSV lives at csvPath. Exported so the `reply` subcommand can validate a target id against suggestion ids — a reply targets a comment OR a suggestion.
func (Suggestion) AnchorMoved ¶ added in v0.17.0
func (s Suggestion) AnchorMoved() bool
AnchorMoved reports that the suggestion was auto-shifted to follow its target text after the file changed (informational). Parallels Comment.AnchorMoved.
func (Suggestion) AnchorOutdated ¶ added in v0.17.0
func (s Suggestion) AnchorOutdated() bool
AnchorOutdated reports that the suggestion's OriginalText could no longer be confidently located — the reviewer/LLM must re-derive it. Parallels Comment.AnchorOutdated.
func (Suggestion) LineSpan ¶ added in v0.17.0
func (s Suggestion) LineSpan() string
LineSpan renders "L42" / "L42-L48" for the card badge, matching Comment's line-level span (suggestions are always line-anchored).
type SuggestionDecision ¶ added in v0.17.0
type SuggestionDecision struct {
SuggestionID string `json:"suggestion_id"`
Verdict string `json:"verdict"` // accept | reject
Note string `json:"note,omitempty"`
// Auto marks a reject that was a SIDE EFFECT of accepting another alternative
// in the same group (#117), as opposed to a reject the reviewer clicked. It
// lets ClearSuggestionDecision re-open the whole group when the accept is
// undone (radio-button semantics). Invisible to the LLM — a reject is a reject.
Auto bool `json:"auto,omitempty"`
// Revert marks that the reviewer asked to UNDO an already-applied accept (#159
// M4.2): the verdict stays "accept" but the agent must restore the original text
// on disk. Set by RequestRevert, surfaced to the agent as verdict="revert", and
// self-clears once the agent acks (`reverted` drops the suggestion out of the
// applied set → DecisionsBySuggestion filters the now-revert-complete decision
// back to undecided).
Revert bool `json:"revert,omitempty"`
Fingerprint string `json:"fingerprint"` // of the suggestion content decided on
Created time.Time `json:"created"`
}
SuggestionDecision is the reviewer's recorded verdict on one suggestion.
type SuggestionGroupInfo ¶ added in v0.20.0
type SuggestionGroupInfo struct {
Index int // 1-based position within the group, in submission order
Total int // group size; only groups of >1 members are surfaced
}
SuggestionGroupInfo positions a suggestion within its group of same-area alternatives (#117).
type ThreadEntry ¶ added in v0.23.0
type ThreadEntry struct {
TargetID string `json:"target_id"`
Author string `json:"author"`
Body string `json:"body"`
At int64 `json:"at"`
}
ThreadEntry is one message in a comment's or suggestion's thread. At is a nanosecond timestamp (time.Now().UnixNano()): the agent CLI and the server share the same box + clock, so an agent reply is naturally later than the reviewer prompt it answers, and (At, author) sorts stably.
func (ThreadEntry) When ¶ added in v0.23.0
func (e ThreadEntry) When() string
When renders At as a compact wall-clock time for the card (e.g. "3:04 PM").
type TreeNode ¶ added in v0.13.0
type TreeNode struct {
Name string // last path segment, the row label
Path string // full path from repo root (the directory path for dirs)
IsDir bool // directory vs file leaf
Depth int // 0 for top-level entries, +1 per level — drives indentation
Children []*TreeNode // directories first, then files, each alphabetical
// Entry is the underlying file row for a leaf; nil for directories.
Entry *gitdiff.FileEntry
// Resolved per-node display state.
IsSelected bool // leaf path == SelectedFile
IsViewed bool // leaf path is in ViewedFiles
DefaultOpen bool // dir initial expansion: contains a changed or the selected file
// Roll-ups. A leaf mirrors its own file; a directory aggregates its whole
// subtree. Added/Deleted clamp negative (binary/unknown) child values to 0.
CommentCount int
Added int
Deleted int
HasChanged bool // leaf changed vs base, or any changed descendant for a dir
}
TreeNode is one node in the file-browser tree: a directory (IsDir, with Children) or a file leaf (Entry set). The recursive template renders nested <details>/<summary> for directories and the existing file row for leaves.
All display state (IsSelected, IsViewed, DefaultOpen) and roll-up counts are resolved here at build time so the recursive template needs no access to the root state ($) — Go templates drop $ when a template calls itself with a child node as the new dot.
func (*TreeNode) DepthStyle ¶ added in v0.13.0
DepthStyle is the inline style carrying the node's nesting depth as a CSS custom property the stylesheet multiplies into a left-indent. Typed template.CSS so html/template injects it verbatim (a bare {{.Depth}} in a style="" attribute trips the CSS-context filter).
func (*TreeNode) Ext ¶ added in v0.14.0
Ext is the extension of a leaf's label (".go", ".tar.gz" → ".gz"), or "" for directories, extensionless names ("Makefile"), and dotfiles whose only dot is leading (".gitignore"). NameStem is the label minus Ext. The drawer renders them as separate spans so a long filename truncates within the stem while the extension stays visible (issue #56). Zero-arg so the template can call them.
type UIPrefs ¶ added in v0.16.0
type UIPrefs struct {
ShowResolved bool `json:"show_resolved"`
SchemeName string `json:"scheme_name"`
ThemeMode string `json:"theme_mode"`
FocusMode bool `json:"focus_mode"`
// TocCollapsed hides the desktop table-of-contents column (#137). A durable
// desktop reading preference like FocusMode — its per-side twin — so it lives
// here, not in the relaunch-wiped session store.
TocCollapsed bool `json:"toc_collapsed"`
HideMarks bool `json:"hide_marks"`
FileView bool `json:"file_view"`
RawMarkdown bool `json:"raw_markdown"`
RawHTML bool `json:"raw_html"`
// QueueGlobal shows the whole review's work in the queue panel instead of just
// the current file's (#171). Default false = this file — the queue answers "what
// is happening to the document in front of me"; flip it to see the review-wide
// backlog. A view preference like the rest, so it belongs here and not in the
// session store.
QueueGlobal bool `json:"queue_global"`
}
uiprefs.go persists the durable, per-user view-style preferences that mean the same thing in every repo (theme scheme, light/dark mode, focus, file-view, the raw-source toggles, and show-resolved).
Why a file instead of the framework's lvt:"persist" session store: that store is an in-memory SessionStore keyed by a browser cookie. A page reload keeps it, but prereview is a CLI the user relaunches constantly (often from a phone) — every relaunch spawns a fresh empty store, so the old cookie misses and EVERY persisted view pref resets to its zero value. Writing these prefs to a per-user file on disk bypasses the session entirely, so a preference set on the phone survives a page reload AND a server relaunch — the same "disk is the source of truth" contract comments.csv already relies on.
Only genuinely global view preferences live here. Per-repo / per-session state (Base, SelectedFile, DraftBody, EditingCommentID, selection ranges, …) stays lvt:"persist": that is session-continuity across a reconnect, not a durable user preference — and Base in particular (`HEAD~3`) is meaningless across repos.
type VersionListItem ¶ added in v0.18.0
type VersionListItem struct {
Seq int
Label string // human trigger label: "Original" / "Agent edit" / "Rolled back"
When string // local time-of-day the version was captured
Current bool // newest recorded version (≈ what's on disk now)
Viewing bool // currently open in the read-only version view
Deleted bool // tombstone: the file was absent at this version
// Changelog is the agent's AI-authored "what changed" for this version (#155),
// captured from its status message at the llm-done checkpoint — the primary
// summary the panel shows. Empty for baseline/rollback and batches where the
// agent wrote no message; the +add/−del Summary stat is the fallback then.
Changelog string
// Summary is the computed diff STAT (+add/−del) between this checkpoint and the
// previous one — derived on demand from the content-addressed blobs (never
// stored). Shown alongside the changelog and as the floor when there's no
// changelog. Counts only (no raw lines), so it scales to a large diff.
Summary VersionSummary
}
VersionListItem is one row in the per-file Versions panel — a presentation view over the store's FileHistory, newest first.
type VersionStore ¶ added in v0.18.0
type VersionStore struct {
// contains filtered or unexported fields
}
VersionStore is the append-only content-snapshot store rooted at a versions directory. Safe for concurrent use: the mutex serialises the read-modify-append of the timeline (the watcher goroutine, Mount, and RestoreVersion all call in).
func NewVersionStore ¶ added in v0.18.0
func NewVersionStore(root string) (*VersionStore, error)
NewVersionStore opens (creating if needed) a version store at root, which is <store>/.prereview/versions. It never wipes existing history — the store is the uncommitted version log and must survive server restarts.
func (*VersionStore) Blob ¶ added in v0.18.0
func (s *VersionStore) Blob(sha string) ([]byte, error)
Blob returns the raw content stored under sha.
func (*VersionStore) Checkpoint ¶ added in v0.18.0
func (s *VersionStore) Checkpoint(refs []FileRef, trigger, changelog string) (cp Checkpoint, created bool, err error)
Checkpoint snapshots every ref and appends a timeline entry. Each file is hashed; unchanged content reuses its existing blob (dedup). A ref whose file is absent is recorded as a tombstone (SHA "") rather than failing the whole checkpoint — a deleted file is still restorable state. To keep the timeline meaningful, a checkpoint whose scope is byte-identical to the previous one is skipped (created=false) — EXCEPT the very first (baseline), which always lands.
func (*VersionStore) Checkpoints ¶ added in v0.18.0
func (s *VersionStore) Checkpoints() ([]Checkpoint, error)
Checkpoints returns the full timeline in order. Missing timeline ⇒ empty.
func (*VersionStore) FileHistory ¶ added in v0.18.0
func (s *VersionStore) FileHistory(path string) ([]FileHistoryEntry, error)
FileHistory returns path's timeline: one entry per checkpoint where the file's content changed (or first appeared), oldest first. A tombstone (deletion) is included as an entry with SHA "".
func (*VersionStore) Restore ¶ added in v0.18.0
func (s *VersionStore) Restore(path string, seq int) ([]byte, error)
Restore returns path's content as recorded at checkpoint seq. It returns ErrVersionTombstone when the file was absent at that checkpoint (the caller restores by deleting the working file), and an error when seq is out of range or the path is not in that checkpoint.
type VersionSummary ¶ added in v0.23.1
type VersionSummary struct {
Added int
Removed int
Initial bool // the file's first recorded version — nothing to diff against
Deleted bool // the file was removed at this version
Binary bool // binary content — line counts are meaningless
}
VersionSummary is the computed diff stat shown on a version row.
Source Files
¶
- anchor.go
- applied.go
- comment.go
- controller.go
- controller_comments.go
- controller_emit.go
- controller_files.go
- controller_quiz.go
- controller_reply.go
- controller_search.go
- controller_selection.go
- controller_snapshot.go
- controller_status.go
- controller_suggestions.go
- controller_versions.go
- controller_view.go
- decision.go
- filetree.go
- helpers.go
- hidden.go
- keymap.go
- llmstatus.go
- processed.go
- prompt.go
- quiz.go
- readprogress.go
- search.go
- session.go
- state.go
- state_queue.go
- state_quiz.go
- state_views.go
- stream.go
- suggestion.go
- thread.go
- uiprefs.go
- versions.go