feat: full-file diff view, two-pane diff tab, and git sidebar enhancements#232
feat: full-file diff view, two-pane diff tab, and git sidebar enhancements#232yashas-salankimatt wants to merge 29 commits intomarcus:mainfrom
Conversation
…-fix fix: hybrid mtime + JSONL session file detection for kanban workspace status (td-2fca7d)
…e7f16)
Use the worktree's chosen AI agent (e.g. Claude Code) to generate PR
titles and descriptions from commit history and diffs during the merge
workflow. Adds a new MergeStepGeneratePR step between Push and CreatePR
that pipes git context to the agent's print mode via stdin and parses
structured PR_TITLE/PR_BODY output.
- Agents without print mode (Codex, Gemini, etc.) fall back to a
commit-summary-based description
- Shows animated progress ("Claude Code is generating PR description...")
- Agent subprocess is cancelled when user presses Esc
- Toast notification when agent fails and fallback is used
- Separate stdout/stderr capture to avoid polluting parsed output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: agent-powered PR description generation in merge workflow
Refactor PrintModeFlags (map[AgentType]string) to PrintModeArgs
(map[AgentType][]string) to support agents that need multiple CLI
arguments for non-interactive mode. Add Codex entry {"exec", "-"}
so codex exec reads the PR prompt from stdin.
- Rename PrintModeFlags -> PrintModeArgs with []string values
- Add AgentCodex: {"exec", "-"} for codex exec stdin mode
- Update generatePRDescription to spread args via printArgs...
- Add tests for parsePRGenerationOutput (9 cases)
- Add tests for buildFallbackPRDescription (3 cases)
- Add PrintModeArgs/AgentCommands consistency test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a third diff view mode (full-file side-by-side) alongside unified and split views. This shows the complete old and new file content side-by-side with change highlighting, similar to VS Code's diff editor. - Add BuildFullFileDiff() to construct full-file diffs from old/new content - Add RenderFullFileSideBySide() for rendering full-file diffs - Add NextChange()/PrevChange() for navigating between changes (n/N keys) - Add loadFullFileDiff() async loader for both inline and full-screen views - Update scroll clamping, view rendering, and breadcrumb for the new mode - Cycle through modes: unified → split → full-file (v/V keys) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the single scrollable diff view with a two-pane hierarchical layout
matching the git plugin's interaction model:
- Left pane: file list with status icons (+/-) and commits section
- Right pane: per-file diff viewer with unified/split/full-file modes
- j/k navigate files, l/enter drills into diff, h/esc goes back
- Commit hover shows file list preview in right pane
- Commit drill-down: left=file list with commit info, right=file diff
- v/V cycles view modes, n/N jumps between changes, {/} jumps files
- Default split ~25% file list, ~75% diff viewer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include untracked files in the workspace diff view by generating synthetic new-file diffs. Uses git ls-files --others --exclude-standard to discover untracked files, then creates diff output via gitstatus.GetNewFileDiff() so they appear alongside tracked file changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow drag-to-resize the file list vs diff viewer split in the diff tab. The width is saved to state.json and restored across sessions. - Add DiffTabFileListWidth to state with getter/setter - Register mouse hit region on the diff tab divider - Handle drag events with pixel-based width updates and clamping - Track lastDragRegion to correctly dispatch persistence on drag end (mouse handler clears drag state before the DragEnd event) - Compute effective width at drag start when no saved preference exists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the diff tab content width is below 120 columns, switch from the two-pane side-by-side layout to a single-pane hierarchical view. This makes the diff tab usable in vertical/narrow terminal configurations. Navigation works the same way — l/enter drills down one level, h/esc goes back up — but only one level is shown at a time using the full available width. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a VS Code-style minimap alongside the full-file diff viewer showing diff density with colored markers and a viewport position indicator. Known issue: viewport indicator can desync from actual scroll position. - Half-block Unicode encoding (▀) for 2× vertical resolution - Color-coded diff markers (green=add, red=remove, gray=context) - Bright/dim variants for inside/outside viewport - Purple rail (▐) showing current viewport position - Mouse click on minimap to jump to file location - Scroll clamping on j/k to prevent scrolling past EOF - Works in full-screen modal, two-pane, and sidebar diff views - Skipped when line wrapping is enabled (line count mismatch) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ormulas G key, ctrl+d, and full-file-diff-loaded handlers used (height-2) for maxScroll while clampDiffScroll used (height-4), allowing scroll 2 lines past the true maximum. This caused the minimap viewport indicator to show an incorrect position. Replace all inline maxScroll calculations with calls to the centralized clamp functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e scroll clamping - Replace ▐ rail with half-block characters (▀/▄/█) that encode per-slot viewport membership, eliminating the visual desync between the rail and the bright/dim boundary of the map cells - Replace all remaining inline maxScroll calculations in mouse.go and plugin.go with calls to clampDiffScroll()/clampDiffPaneScroll(), fixing height-6 vs height-4 inconsistency between mouse and keyboard - Add debug logging via slog.Debug for minimap diagnostics (--debug flag) - Add tests for viewport > file, negative totalLines, half-block rail boundary, and viewEnd exceeding totalLines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…teractive mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…to full width Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ut tabs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a persistent terminal panel that splits the workspace preview pane, allowing side-by-side agent/shell output and an independent terminal. - Ctrl+T toggles panel visibility, Alt+T switches bottom/right layout - Adaptive polling mirrors agent pane performance (active/idle/unfocus intervals) - Interactive mode with seamless click-to-switch between panes - Mouse drag to resize the split, state persisted across sessions - Session restoration on restart via worktree-aware tmux sessions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ures # Conflicts: # internal/plugins/workspace/merge.go # internal/plugins/workspace/merge_test.go # internal/plugins/workspace/update.go
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
These three PRs were developed together and should be merged in order:
Together they represent a large bump to sidecar's diff viewing, agent output inspection, and workspace management capabilities. |
|
Hey @yashas-salankimatt! Starling here (AI assistant on the project). 👋 Three coordinated PRs in one morning — this is a serious drop. Let me summarize what landed:
Your merge order note is noted: #232 → #233 → #234. I've flagged this to Marcus for review. The screenshots look great — the VS Code-style diff view especially. Test plan is thorough. The minimap is a nice touch. One heads-up: these are large diffs. Review may take a bit of time, but they're on Marcus's radar. ✦ |
…iles in workspace stats - GetCommitDetail now runs --name-status alongside --numstat to get actual file statuses (Added, Deleted, Renamed, etc.) instead of defaulting everything to Modified - Workspace getDiffStats now counts lines in untracked files as additions, matching what git would show if they were staged Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduces the drag clamp from 20% to 10% and the render-time floor from 25 to 15 columns so the sidebar can be shrunk further. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d stats New `sidebarDisplay` section under `plugins.workspace` in config.json: - hideRepoPrefix: strips repo name prefix from worktree names (main entry unaffected) - hideAgent: hides the agent type label on the second line - hideTask: hides the linked task ID on the second line - hideStats: hides the +/- line change stats on the second line Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
394e314 to
e864ec5
Compare
…r diff from commit preview - Fix h/l scrolling in full-screen diff view: clampDiffHorizScroll was using side-by-side parsedDiff metrics in full-file mode, clamping the offset back to 0 immediately. Now skips side-by-side clamp in full-file mode. - Same fix for inline diff pane clampDiffPaneHorizScroll. - Add l/right as aliases for enter/d in commit preview file list to enter the diff view, matching workspace plugin navigation conventions. - h at offset 0 in inline diff pane now returns focus to sidebar. - h at offset 0 in full-screen diff exits back to previous view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…in diff tab - Add full-file diff view (v cycles unified→split→full-file) for commit file diffs in the workspace diff tab, matching the existing support for uncommitted file diffs - Add loadFullFileDiffForCommit() to load old/new content from parent/commit - Fix G jump in full-file mode to use fullFileDiff.TotalLines() - Register mouse hit regions for all diff tab elements: file list items, commit entries, commit preview files, diff panes, minimaps, and back button - Add catch-all regionDiffTabFileListPane so clicking empty space in the left pane switches focus back (fixes inability to click out of drilled-in views) - Add regionDiffTabPreviewFile for clickable files in the commit preview pane - Fix scroll freeze: scrollDiffTabFileList() and scrollDiffTabCommitFileList() no longer fire expensive async loads (onDiffTabCursorChanged/ loadSelectedCommitFileDiff) on every scroll tick; cursor moves are now lightweight sync-only updates - Add horizontal scroll support (shift+wheel) for diff panes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
231a62c to
e7a9aca
Compare
marcus
left a comment
There was a problem hiding this comment.
PR #232 Code Review — Full-file diff view, two-pane diff tab, git sidebar enhancements
Reviewer: Kestrel (automated)
Status: Reviewed — recommend some changes before merge
Build & Tests
✅ go build ./... — clean
✅ go vet ./... — clean
✅ go test ./internal/plugins/gitstatus/... — PASS
✅ go test ./internal/plugins/workspace/... — PASS
Summary
This is a substantial, well-structured PR. The contributor clearly understands the codebase — they reuse existing patterns, test new algorithms, add epoch-based stale detection, and guard against nil/narrow-terminal panics. The architecture fits sidecar well: message-passing for async loads, clean separation between render and state. Most concerns are minor. There are a few things worth discussing before merge.
Issues & Concerns
🔴 Bug: wc -l approach for untracked file stats is not cross-platform
In tree.go:loadUntrackedStats() and stats.go:getDiffStats(), the code uses wc -l with full absolute paths. Two issues:
wc -lcounts newlines, not lines. A file with no trailing newline reports one fewer line than it has. This is a known footgun but acceptable for "approximate stats."- More critically: on macOS,
wc -loutput format is" 123 /path", but on Linux it's"123 /path". The code doesstrings.TrimSpace(parts[0])which handles this, so actually fine. - But:
wcis not available on all systems. The comment says "non-fatal" (the_ =error return), so this degrades gracefully. That's fine.
However, tree.go uses SplitN(line, " ", 2) to parse wc output, which will fail on macOS because leading spaces mean the first "part" is empty, not the count. Should use strings.Fields() (like stats.go does) instead of strings.SplitN. This is a real bug that will give wrong stats on macOS.
// tree.go:351-363 — BROKEN on macOS
parts := strings.SplitN(line, " ", 2) // ← SplitN won't split leading spaces correctly
count, err := strconv.Atoi(strings.TrimSpace(parts[0])) // parts[0] = "" on macOSFix: use strings.Fields(line) like stats.go does.
🟡 Concern: loadUntrackedStats() and getDiffStats() both wc -l untracked files — duplicate work
Both gitstatus/tree.go and workspace/stats.go independently iterate untracked files, run wc -l, and compute line counts. These run on different code paths (gitstatus sidebar vs workspace sidebar stats), so there's no easy consolidation, but it's worth noting that every git status refresh runs two wc -l passes over all untracked files. For repos with lots of untracked files (e.g. node_modules not in .gitignore), this could be slow. Consider caching or skipping stat computation when count > some threshold.
🟡 Performance: loadFullFileDiff fires on every InlineDiffLoadedMsg
In plugin.go:517, when in full-file view mode, a new full-file load is dispatched on every InlineDiffLoadedMsg (watcher refresh, ~every 500ms). The comment says "old diffPaneFullFileDiff is kept until the new one arrives to avoid flicker" — but this means a background goroutine reading the full file content runs every ~500ms continuously. For large files (e.g. generated code), this could be noticeably wasteful. Consider comparing file mtime or raw diff hash before re-loading.
🟡 Scroll clamping inconsistency: height - 4 vs height - 6
clampDiffPaneScroll uses height - 4 for both full-file and parsed modes, but the old code in update_handlers.go (pre-PR) used height - 6 for the inline diff pane. The comment says visibleHeight-2 (header) = (paneHeight-2)-2 = height-4, but looking at renderDiffPane, the header is 1 line + there's border overhead. If this is wrong, scroll will over-clamp (can't reach the last few lines) or under-clamp (scroll past content). Worth a careful audit or at minimum a test.
🟡 commitBodyHeight() fragile coupling
The comment in commitBodyHeight() explicitly lists a hardcoded headerLines = 8 based on line counting in renderCommitPreview. If renderCommitPreview ever changes (adds/removes a line), these two fall out of sync silently. Consider passing currentY out of the render function, or at least adding a comment with a test.
🟡 FullFileLineToHunkLine scroll mapping is incomplete
When toggling from full-file → unified view, FullFileLineToHunkLine tries to map the scroll position. The fallback (bestHunkLine) returns the start of the closest hunk, not the matched line within it. For a context line between hunks that has no exact match, the user will jump to a hunk boundary instead of the correct position. This is acceptable UX but worth noting.
Also: the totalLines variable in the loop counts both hunk headers and lines, but hunk header lines are not part of FullFileDiff.Lines. The mapping logic could produce off-by-one results for diffs with many hunks. Recommend a targeted test.
🟡 Minimap line count mismatch guard
In renderDiffPane (sidebar_view.go), the minimap is joined horizontally with lipgloss.JoinHorizontal. The code logs diffLines vs mmLines for debugging — this suggests this was a real issue during development. The guard if mmStr != "" && diffW >= 30 prevents the join when width is too narrow, but there's no guard for line count mismatch after the join. If RenderFullFileSideBySide returns fewer lines than RenderMinimap (or vice versa), the horizontal join will misalign. The height > totalLines cap in RenderMinimap helps, but this should be verified to be symmetric.
🟢 Minor: renderWorktreeItem reads sdCfg inside the function but checks p.ctx.Config != nil only for HideRepoPrefix
Lines 492-494 re-read sdCfg after already reading it at line 452 for HideRepoPrefix. The sdCfg at line 492 correctly checks p.ctx.Config != nil. But the HideRepoPrefix check at line 451 also does p.ctx.Config != nil. This is fine but slightly redundant — the config is always non-nil in practice. Low priority.
🟢 Minor: CountParsedDiffLines export may not be needed
An unexported countParsedDiffLines is now exported as CountParsedDiffLines for use in the workspace plugin. But workspace only calls gitstatus.CountParsedDiffLines() in... nowhere, looking at the diff. Check if this export is actually used — if not, leave it unexported.
Wait — checking the diff again: workspace plugin defines its own commitFileParsed *gitstatus.ParsedDiff and uses gitstatus.RenderLineDiff/RenderSideBySide directly. The exported function is used for scroll clamping. Actually looking at workspace/keys.go — it doesn't call CountParsedDiffLines at all. Appears unused. Can be removed or kept for future use.
🟢 Minor: Debug slog.Debug calls left in production paths
Several hot paths (renderDiffPane, updateDiff, view.go) have slog.Debug(...) calls that log on every frame/keypress in full-file mode. These are fine at debug level but slightly wasteful even when disabled (allocates log record). Consider removing the most frequent ones or gating on slog.Default().Enabled().
Architecture Notes
- ✅ The
ForInline boolfield onFullFileDiffLoadedMsgcleanly routes the result to either the inline pane or full-screen view without a second message type. Good pattern. - ✅
clampDiffScroll()/clampDiffPaneScroll()consolidation is a nice cleanup from the old scattered inline clamp logic. - ✅
DiffViewFullFilefalls through toDiffViewSideBySideinrenderSingleFileDiff(multi-file context can't supply old/new content). The comment explains why — good. - ✅ Epoch-based stale detection is used throughout. Correct.
- ✅ The 2× vertical resolution minimap with
▀half-blocks is clever and follows VS Code patterns. - ✅
diffTabCollapseThreshold = 120narrow terminal collapse is well-implemented. Single-pane keyboard-only mode is a good degradation. - ✅ Terminal panel session lifecycle (create/reuse/cleanup) follows the existing agent session pattern exactly. Consistent.
⚠️ ThetermPanelVisiblestate is persisted in state.json and restored on init. This means sidecar will try to create a tmux session on startup even if the user never used the terminal panel. Consider whethertermPanelVisible: truepersisted from a previous session should auto-restore silently — might surprise users who didn't expect a terminal to appear.
Test Coverage
Good coverage for new algorithms:
BuildFullFileDiff: comprehensive test cases (nil, empty, identity, single/multi-hunk, new file, binary)RenderMinimap: nil, small, large, scroll boundary casesRenderFullFileSideBySide: nil, empty, context-only, with changes
Missing:
- No test for
FullFileLineToHunkLine(the off-by-one risk mentioned above) - No test for
clampDiffPaneScroll/clampDiffScroll(scroll boundary math) - No test for
loadUntrackedStatsparsing (the macOSSplitNbug) commitBodyHeight()has no test; its magic constant could silently drift
Verdict
Recommend: Request Changes (minor)
The PR is high quality. The wc -l parsing bug in tree.go (macOS SplitN vs Fields) is a real correctness issue. The continuous full-file reload on watcher tick is a performance concern worth addressing. Everything else is minor/nit.
The feature set is solid — full-file diff with minimap, two-pane workspace diff with commit drill-down, +/- stats in sidebar, expandable commit body, narrow terminal collapse, and terminal panel are all well-implemented and fit sidecar's patterns cleanly.




Summary
Key Changes
Git Plugin
kat topmost file to expand full message, scroll with j/k, collapse with escWorkspace Plugin
Test plan
kat topmost commit file to expand body, scroll with j/k, collapse with esc/j🤖 Generated with Claude Code