feat(commit): only commit files modified by current session#179
feat(commit): only commit files modified by current session#179paaauldev wants to merge 6 commits intocoollabsio:mainfrom
Conversation
When user clicks "Commit" or "Commit & Push", only the files modified by the current session are committed, not all pending changes. - Add git_snapshot field to SessionMetadata to track base commit - Add get_current_commit(), get_changed_files_since(), and stage_files() functions in git_status.rs - Update create_commit_with_ai command to accept session_id and filter files changed since session started - Pass session_id from frontend useGitOperations hook Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to make AI-assisted commits session-scoped by committing only files changed during the active chat session (tracked via a git snapshot). It also includes additional backend changes related to Plane project/issue support.
Changes:
- Add
sessionIdtocreate_commit_with_aiinvocations and thread it through the backend dispatcher. - Introduce session-based git helpers (
get_current_commit,get_changed_files_since,stage_files) and use them to stage changes for commits. - Add
git_snapshottoSessionMetadatato store the session’s base commit.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/chat/hooks/useGitOperations.ts | Passes sessionId to commit operations so the backend can scope staging/commits to the active session. |
| src-tauri/src/projects/git_status.rs | Adds git helper functions to support session-based commit filtering and selective staging. |
| src-tauri/src/projects/commands.rs | Updates create_commit_with_ai to optionally stage only session-changed files; extends project settings with Plane fields. |
| src-tauri/src/http_server/dispatch.rs | Wires sessionId into the WebSocket command dispatcher; adds Plane issue command routes. |
| src-tauri/src/chat/types.rs | Adds git_snapshot to session metadata for tracking the base commit of a session. |
| pub fn get_changed_files_since(repo_path: &str, base_commit: &str) -> Result<Vec<String>, String> { | ||
| let output = silent_command("git") | ||
| .args(["diff", "--name-only", &format!("{base_commit}..HEAD")]) | ||
| .current_dir(repo_path) | ||
| .output() | ||
| .map_err(|e| format!("Failed to run git diff: {e}"))?; | ||
|
|
There was a problem hiding this comment.
get_changed_files_since() uses git diff --name-only {base_commit}..HEAD, which only compares commits and will be empty when the session only has working tree (uncommitted) changes. This will cause the session-based commit path to miss the intended files and fall back to staging everything. Use a diff that compares the working tree to the base snapshot (and include untracked files separately) so uncommitted changes are captured.
| /// Stage specific files for commit (instead of stage_all) | ||
| pub fn stage_files(repo_path: &str, files: &[String]) -> Result<(), String> { | ||
| if files.is_empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let mut cmd = silent_command("git"); | ||
| cmd.arg("add").args(files); | ||
|
|
||
| let output = cmd | ||
| .current_dir(repo_path) | ||
| .output() | ||
| .map_err(|e| format!("Failed to stage files: {e}"))?; | ||
|
|
||
| if !output.status.success() { | ||
| let stderr = String::from_utf8_lossy(&output.stderr); | ||
| return Err(format!("Failed to stage files: {stderr}")); | ||
| } |
There was a problem hiding this comment.
stage_files() runs git add without a -- separator and without -A/--all. Filenames starting with - could be interpreted as options, and deletions won't be staged via plain git add <path> (deleted paths can error or be skipped). Prefer git add -A -- <files...> (or equivalent) to safely handle arbitrary paths and deletions.
| Ok(_) => { | ||
| // No files changed since snapshot - try to stage all anyway (might be untracked files) | ||
| stage_all_changes(&worktree_path)?; | ||
| } |
There was a problem hiding this comment.
When get_changed_files_since() returns an empty list, this falls back to stage_all_changes(), which can stage and commit unrelated changes from other sessions (the exact behavior this PR is trying to prevent). Instead of staging all, consider returning a clear error like "No changes in this session" or staging only session-relevant files (e.g., diff vs snapshot + untracked).
| plane_api_key: Option<String>, | ||
| plane_url: Option<String>, | ||
| plane_workspace_id: Option<String>, | ||
| plane_project_id: Option<String>, |
There was a problem hiding this comment.
Adding these new plane_* parameters changes the update_project_settings function signature. The HTTP/WebSocket dispatcher calls update_project_settings(...) positionally and must be updated to pass the additional arguments (even if they’re None), otherwise this will not compile.
| // ===================================================================== | ||
| // Plane Issues | ||
| // ===================================================================== | ||
| "list_plane_workspaces" => { | ||
| let project_id: String = field(&args, "projectId", "project_id")?; | ||
| let result = crate::projects::list_plane_workspaces(app.clone(), project_id).await?; | ||
| to_value(result) | ||
| } |
There was a problem hiding this comment.
This PR introduces Plane settings and a large set of list_/search_/get_plane_* dispatcher commands, but the PR description only discusses session-based commits. If the Plane work is intentional, please update the PR description/scope (or split into a separate PR) to keep review and rollback risk manageable.
| // 2. Check for uncommitted changes | ||
| let status = get_git_status(&worktree_path)?; | ||
| if status.trim().is_empty() { | ||
| if push { |
There was a problem hiding this comment.
The commit message prompt uses status from git status --short before session-based staging, so it can include unrelated workspace changes (e.g., from another session) even if only some files are staged/committed. This can mislead the AI into mentioning files that won't be part of the commit; consider generating a status scoped to staged changes (or to the session file set) after staging.
| // 1. Get git snapshot for session-based filtering | ||
| let git_snapshot = if let Some(ref sid) = session_id { | ||
| // Try to load session metadata to get existing snapshot | ||
| if let Ok(Some(metadata)) = crate::chat::storage::load_metadata(&app, sid) { | ||
| if metadata.git_snapshot.is_some() { | ||
| metadata.git_snapshot.clone() | ||
| } else { | ||
| // No snapshot exists yet - capture current commit and save it | ||
| let current_commit = get_current_commit(&worktree_path).ok(); | ||
| if current_commit.is_some() { | ||
| // Save snapshot to session metadata | ||
| let _ = crate::chat::storage::with_metadata_mut( | ||
| &app, | ||
| sid, | ||
| &metadata.worktree_id, | ||
| &metadata.name, | ||
| metadata.order, | ||
| |m| { | ||
| m.git_snapshot = current_commit.clone(); | ||
| Ok(()) | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
This code captures and persists git_snapshot lazily when creating the first commit, which doesn't match the stated goal of "base commit when session starts" and can produce a wrong snapshot if HEAD moved between session start and first commit attempt. It would be more reliable to set git_snapshot when the session is created/opened, and handle/save errors from with_metadata_mut instead of discarding them with let _ = ....
| field_opt(&args, "customProfileName", "custom_profile_name")?; | ||
| let reasoning_effort: Option<String> = | ||
| field_opt(&args, "reasoningEffort", "reasoning_effort")?; | ||
| let session_id: Option<String> = from_field_opt(&args, "sessionId")?; | ||
| let result = crate::projects::create_commit_with_ai( |
There was a problem hiding this comment.
from_field_opt(&args, "sessionId") only accepts the camelCase key. Most other optional args in this dispatcher use field_opt() to support both camelCase and snake_case; using field_opt(&args, "sessionId", "session_id") here would keep behavior consistent and avoid breaking clients that send session_id.
| /// Git commit hash when session started (for filtering commit to session-specific changes) | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub git_snapshot: Option<String>, | ||
|
|
There was a problem hiding this comment.
SessionMetadata::new() (and any other struct literals) must be updated to initialize the new git_snapshot field. As written, adding this non-Default struct field will cause a compile error due to a missing field in the struct literal initialization.
|
I don't think all of this is needed. |
In windows at least is commiting all the files, not just the files from each session |
- Add include_co_author preference (disabled by default) - Add toggle in Settings > General preferences - Modify git commit functions to add Co-Authored-By when enabled Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Not just on windows, on Linux as well |
Summary
git_snapshotfield toSessionMetadatato track the base commit when session startsget_current_commit(),get_changed_files_since(), andstage_files()ingit_status.rscreate_commit_with_aicommand to acceptsession_idand filter files changed since session startedsessionIdfrom frontenduseGitOperationshookTest plan
🤖 Generated with Claude Code