feat(session,viewer): record review mode and diff refs in session

Store ReviewMode, DiffFrom, DiffTo, and DiffCommit in SessionHistory
and persist them to JSONL (conditionally, only when non-empty). Display
mode in viewer list page and show version details (from/to or commit)
on the session detail page based on review mode.
This commit is contained in:
kite 2026-06-05 09:42:25 +08:00
parent 55c6bca1f8
commit 0ac1193040
7 changed files with 121 additions and 19 deletions

View file

@ -56,6 +56,10 @@ type Args struct {
// Commit is a single commit hash to review (vs its parent).
Commit string
// ReviewMode is one of "workspace", "range", or "commit".
// When empty, it is derived from From/To/Commit at session creation time.
ReviewMode string
// Template loaded from YAML config file.
Template template.Template
@ -230,7 +234,16 @@ func New(args Args) *Agent {
}
if args.Session == nil {
gitBranch := detectGitBranch(args.RepoDir)
args.Session = session.New(args.RepoDir, gitBranch, args.Model)
mode := args.ReviewMode
if mode == "" {
mode = reviewModeString(args.From, args.To, args.Commit)
}
args.Session = session.New(args.RepoDir, gitBranch, args.Model, session.SessionOptions{
ReviewMode: mode,
DiffFrom: args.From,
DiffTo: args.To,
DiffCommit: args.Commit,
})
}
return &Agent{
args: args,
@ -1302,6 +1315,16 @@ func buildMessageXML(msgs []llm.Message) string {
return sb.String()
}
func reviewModeString(from, to, commit string) string {
if commit != "" {
return session.ReviewModeCommit
}
if from != "" && to != "" {
return session.ReviewModeRange
}
return session.ReviewModeWorkspace
}
// detectGitBranch returns the current git branch name for the given repo, or empty string on failure.
func detectGitBranch(repoDir string) string {
cmd := exec.Command("git", "-C", repoDir, "rev-parse", "--abbrev-ref", "HEAD")

View file

@ -22,6 +22,12 @@ const (
ReLocationTask TaskType = "re_location_task"
)
const (
ReviewModeWorkspace = "workspace"
ReviewModeRange = "range"
ReviewModeCommit = "commit"
)
// SessionHistory is the top-level container for an entire CR run.
// It is safe for concurrent use by multiple goroutines.
type SessionHistory struct {
@ -30,6 +36,10 @@ type SessionHistory struct {
RepoDir string
GitBranch string
Model string
ReviewMode string
DiffFrom string
DiffTo string
DiffCommit string
StartTime time.Time
EndTime time.Time
persist *jsonlWriter
@ -81,19 +91,31 @@ type ToolResultRecord struct {
Result string
}
// SessionOptions holds optional metadata for a new session.
type SessionOptions struct {
ReviewMode string
DiffFrom string
DiffTo string
DiffCommit string
}
// New creates a new SessionHistory with the given repo directory.
func New(repoDir, gitBranch, model string) *SessionHistory {
func New(repoDir, gitBranch, model string, opts SessionOptions) *SessionHistory {
sessionID := generateUUID()
sh := &SessionHistory{
SessionID: sessionID,
RepoDir: repoDir,
GitBranch: gitBranch,
Model: model,
ReviewMode: opts.ReviewMode,
DiffFrom: opts.DiffFrom,
DiffTo: opts.DiffTo,
DiffCommit: opts.DiffCommit,
StartTime: time.Now(),
FileSessions: make(map[string]*FileSession),
}
p, err := newJSONLWriter(sessionID, repoDir, gitBranch, model)
p, err := newJSONLWriter(sessionID, repoDir, gitBranch, model, opts)
if err != nil {
fmt.Printf("[ocr session] warning: failed to create session writer: %v\n", err)
} else {

View file

@ -17,23 +17,31 @@ import (
// $HOME/.opencodereview/sessions/<encoded-repo-path>/<session-id>.jsonl.
// It is safe for concurrent use by multiple goroutines.
type jsonlWriter struct {
mu sync.Mutex
sessionID string
repoDir string
gitBranch string
model string
file *os.File
writer *bufio.Writer
lastUUID string // tracks chain of records via parentUuid
mu sync.Mutex
sessionID string
repoDir string
gitBranch string
model string
reviewMode string
diffFrom string
diffTo string
diffCommit string
file *os.File
writer *bufio.Writer
lastUUID string // tracks chain of records via parentUuid
}
// newJSONLWriter creates and opens a new JSONL writer for the given session.
func newJSONLWriter(sessionID, repoDir, gitBranch, model string) (*jsonlWriter, error) {
func newJSONLWriter(sessionID, repoDir, gitBranch, model string, opts SessionOptions) (*jsonlWriter, error) {
jw := &jsonlWriter{
sessionID: sessionID,
repoDir: repoDir,
gitBranch: gitBranch,
model: model,
sessionID: sessionID,
repoDir: repoDir,
gitBranch: gitBranch,
model: model,
reviewMode: opts.ReviewMode,
diffFrom: opts.DiffFrom,
diffTo: opts.DiffTo,
diffCommit: opts.DiffCommit,
}
if err := jw.open(); err != nil {
return nil, err
@ -126,6 +134,18 @@ func (jw *jsonlWriter) WriteSessionStart(startTime time.Time) string {
"gitBranch": jw.gitBranch,
"model": jw.model,
}
if jw.reviewMode != "" {
rec["reviewMode"] = jw.reviewMode
}
if jw.diffFrom != "" {
rec["diffFrom"] = jw.diffFrom
}
if jw.diffTo != "" {
rec["diffTo"] = jw.diffTo
}
if jw.diffCommit != "" {
rec["diffCommit"] = jw.diffCommit
}
jw.mu.Lock()
defer jw.mu.Unlock()

View file

@ -154,7 +154,7 @@ func TestSetErrorIncrementsCounter(t *testing.T) {
func TestSetErrorWritesJSONL(t *testing.T) {
repoDir := t.TempDir()
sh := New(repoDir, "main", "test-model")
sh := New(repoDir, "main", "test-model", SessionOptions{ReviewMode: ReviewModeWorkspace})
defer sh.Finalize()
fs := sh.GetOrCreateFileSession("foo.go")
@ -193,7 +193,7 @@ func TestSetErrorWritesJSONL(t *testing.T) {
func TestSessionEndIncludesFailures(t *testing.T) {
repoDir := t.TempDir()
sh := New(repoDir, "main", "test-model")
sh := New(repoDir, "main", "test-model", SessionOptions{ReviewMode: ReviewModeWorkspace})
fs := sh.GetOrCreateFileSession("bar.go")
for i := 0; i < 3; i++ {

View file

@ -80,6 +80,10 @@ type SessionSummary struct {
CWD string
GitBranch string
Model string
ReviewMode string
DiffFrom string
DiffTo string
DiffCommit string
FilesReviewed []string
DurationSec float64
FileCount int
@ -149,6 +153,18 @@ func peekSession(path string) (SessionSummary, error) {
if model, ok := rec["model"].(string); ok {
summary.Model = model
}
if rm, ok := rec["reviewMode"].(string); ok {
summary.ReviewMode = rm
}
if v, ok := rec["diffFrom"].(string); ok {
summary.DiffFrom = v
}
if v, ok := rec["diffTo"].(string); ok {
summary.DiffTo = v
}
if v, ok := rec["diffCommit"].(string); ok {
summary.DiffCommit = v
}
}
}
@ -274,6 +290,18 @@ func LoadSession(root, encodedRepo, sessionID string) (*ViewSession, error) {
if model, ok := rec["model"].(string); ok {
vs.Summary.Model = model
}
if rm, ok := rec["reviewMode"].(string); ok {
vs.Summary.ReviewMode = rm
}
if v, ok := rec["diffFrom"].(string); ok {
vs.Summary.DiffFrom = v
}
if v, ok := rec["diffTo"].(string); ok {
vs.Summary.DiffTo = v
}
if v, ok := rec["diffCommit"].(string); ok {
vs.Summary.DiffCommit = v
}
case "llm_request":
fp, _ := rec["filePath"].(string)

View file

@ -14,6 +14,14 @@
<div class="meta">
<span><strong>CWD:</strong> {{.Session.Summary.CWD}}</span>
<span><strong>Branch:</strong> {{.Session.Summary.GitBranch}}</span>
<span><strong>Mode:</strong> {{if .Session.Summary.ReviewMode}}{{.Session.Summary.ReviewMode}}{{else}}-{{end}}</span>
{{if eq .Session.Summary.ReviewMode "range"}}
<span><strong>From:</strong> <code>{{.Session.Summary.DiffFrom}}</code></span>
<span><strong>To:</strong> <code>{{.Session.Summary.DiffTo}}</code></span>
{{end}}
{{if eq .Session.Summary.ReviewMode "commit"}}
<span><strong>Commit:</strong> <code>{{.Session.Summary.DiffCommit}}</code></span>
{{end}}
<span><strong>Model:</strong> <code>{{.Session.Summary.Model}}</code></span>
<span><strong>Duration:</strong> {{formatDuration .Session.Summary.DurationSec}}</span>
<span><strong>Files:</strong> {{.Session.Summary.FileCount}}</span>

View file

@ -12,12 +12,13 @@
<h2>Sessions: {{.RepoName}}</h2>
{{if .Sessions}}
<table class="table">
<thead><tr><th>Session ID</th><th>Branch</th><th>Model</th><th>Files</th><th>Duration</th><th>Started At</th></tr></thead>
<thead><tr><th>Session ID</th><th>Branch</th><th>Mode</th><th>Model</th><th>Files</th><th>Duration</th><th>Started At</th></tr></thead>
<tbody>
{{range .Sessions}}
<tr>
<td><a href="/r/{{$.EncodedRepo}}/{{.SessionID}}">{{.SessionID | printf "%.8s"}}…</a></td>
<td>{{.GitBranch}}</td>
<td>{{if .ReviewMode}}{{.ReviewMode}}{{else}}-{{end}}</td>
<td><code>{{.Model}}</code></td>
<td>{{.FileCount}}</td>
<td>{{formatDuration .DurationSec}}</td>