Skip to content

Commit 050101e

Browse files
Alan Shumclaude
andcommitted
feat: untracked file support, path abbreviation, fast model defaults (v0.6.0)
- Show untracked files in diff view and sidebar with [untracked]/[U] badges - Discover via `git ls-files --others --exclude-standard`, generate synthetic unified diff, merge with regular git diff before parsing - Binary detection (null bytes), 1MB size cap, path traversal validation - Structural sampling for LLM summaries: head/mid/tail instead of first 4 lines - Abbreviate long paths in sidebar to fit: s/a/c/s-a/routes.ts - Default model changed from sonnet to haiku (Claude) / gemini-flash (Copilot) for faster grouping response times on this classification task Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 171846c commit 050101e

File tree

11 files changed

+569
-134
lines changed

11 files changed

+569
-134
lines changed

.claude/skills/review.md

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# Rust Code Review
22

3-
Review Rust code changes for correctness, safety, and idiomatic patterns.
3+
Review Rust code changes for correctness, safety, idiomatic patterns, regressions, and scope discipline. Automatically split oversized commits.
44

55
## Steps
66

77
1. Get the diff to review:
88
```bash
99
git diff origin/main...HEAD
1010
```
11-
If origin/main doesn't exist (new repo), use:
11+
If that's empty, try unstaged changes:
1212
```bash
13-
git diff --cached HEAD
13+
git diff
1414
```
15-
If that's empty, diff all commits:
15+
If origin/main doesn't exist (new repo), use:
1616
```bash
17-
git log --oneline --all
17+
git diff --cached HEAD
1818
```
1919

2020
2. Run `cargo clippy` for lint checks:
@@ -27,16 +27,61 @@ Review Rust code changes for correctness, safety, and idiomatic patterns.
2727
cd /Users/kshum/Documents/gitproj/semantic-diff && cargo build 2>&1
2828
```
2929

30-
4. Review the diff for:
30+
4. Run `cargo test` to catch regressions:
31+
```bash
32+
cd /Users/kshum/Documents/gitproj/semantic-diff && cargo test 2>&1
33+
```
34+
35+
5. **Regression analysis** — Review the diff for changes that may break existing behavior:
36+
- Changed function signatures (parameters added/removed/retyped)
37+
- Modified match arms or conditional branches that alter control flow
38+
- Removed or renamed public items (structs, enums, functions, methods)
39+
- Changed default values or config parsing logic
40+
- Modified serialization/deserialization formats
41+
- Altered event handling or key binding behavior
42+
Flag each potential regression with the affected area and severity.
43+
44+
6. **Code quality review** — Check for:
3145
- Unsafe code without justification
3246
- Unwrap/expect on fallible operations in non-test code
3347
- Missing error handling
3448
- Unused dependencies
3549
- Dead code that should be cleaned up
3650

37-
5. After review completes, write the state file to allow push:
51+
7. **Scope analysis — Detect out-of-scope changes:**
52+
Classify every changed file into a logical concern (e.g., "config parsing", "diff engine", "UI rendering", "file tree sidebar", "grouper logic"). Then:
53+
- Identify the **primary intent** of the changeset (the concern touching the most files/lines)
54+
- Flag files whose concern does **not** match the primary intent as **out-of-scope**
55+
- Flag unrelated formatting-only, refactor-only, or drive-by fixes as out-of-scope
56+
- Report a scope summary table:
57+
| Concern | Files | In-scope? |
58+
|---------|-------|-----------|
59+
60+
8. **Auto-split into smaller commits** if the changeset spans multiple concerns:
61+
- Group changed files by concern from step 7
62+
- For each concern group, stage only its files and create a focused commit
63+
- Use clear commit message prefixes: `feat:`, `fix:`, `refactor:`, `chore:`
64+
- Commit order: foundational changes first (models/types → logic → UI → config)
65+
- If a single file contains changes for multiple concerns, use `git add -p` to stage only the relevant hunks
66+
- **Ask the user for confirmation** before creating any commits, showing the proposed split plan:
67+
```
68+
Proposed commit split:
69+
1. feat(diff): add untracked file support — src/diff/mod.rs, src/diff/untracked.rs, src/diff/parser.rs
70+
2. refactor(grouper): improve group filtering — src/grouper/mod.rs
71+
3. feat(ui): enhance file tree with search and icons — src/ui/file_tree.rs, src/ui/diff_view.rs
72+
4. fix(config): update default settings — src/config.rs, src/main.rs
73+
```
74+
- Only proceed with committing after user approval
75+
76+
9. After review and commits complete, write the state file to allow push:
3877
```bash
3978
cd /Users/kshum/Documents/gitproj/semantic-diff && git rev-parse HEAD > "$(git rev-parse --git-dir)/.pre-push-reviewed"
4079
```
4180

42-
6. Report findings as a short summary.
81+
10. Report findings as a structured summary:
82+
- **Build/Lint**: pass/fail
83+
- **Tests**: pass/fail (number of tests)
84+
- **Regressions**: list of potential regressions found
85+
- **Scope**: in-scope vs out-of-scope breakdown
86+
- **Commits**: split plan (proposed or executed)
87+
- **Action items**: anything the user should address

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "semantic-diff"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
edition = "2021"
55
description = "A terminal diff viewer with AI-powered semantic grouping (Claude CLI / Copilot)"
66
license = "MIT"

src/config.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ impl Config {
4949
pub fn default_config() -> Self {
5050
Self {
5151
preferred_ai_cli: None,
52-
claude_model: "sonnet".to_string(),
53-
copilot_model: "sonnet".to_string(),
52+
claude_model: "haiku".to_string(),
53+
copilot_model: "gemini-flash".to_string(),
5454
theme_mode: ThemeMode::Auto,
5555
}
5656
}
@@ -119,16 +119,16 @@ const DEFAULT_CONFIG: &str = r#"{
119119
120120
// Claude CLI settings
121121
"claude": {
122-
// Model to use: "sonnet", "opus", "haiku"
122+
// Model to use: "haiku" (fast, default), "sonnet" (balanced), "opus" (powerful)
123123
// Cross-backend models are mapped automatically:
124124
// gemini-flash -> haiku, gemini-pro -> sonnet
125-
"model": "sonnet"
125+
"model": "haiku"
126126
},
127127
128128
// Copilot CLI settings
129129
"copilot": {
130-
// Model to use: "sonnet", "opus", "haiku", "gemini-flash", "gemini-pro"
131-
"model": "sonnet"
130+
// Model to use: "gemini-flash" (fast, default), "sonnet", "opus", "haiku", "gemini-pro"
131+
"model": "gemini-flash"
132132
}
133133
134134
// Theme: "dark", "light", or "auto" (detects from terminal)
@@ -188,7 +188,7 @@ pub fn load() -> Config {
188188

189189
/// Map any model name to the closest Claude CLI model.
190190
fn resolve_model_for_claude(model: Option<&str>) -> String {
191-
let tier = model.map(model_tier).unwrap_or(ModelTier::Balanced);
191+
let tier = model.map(model_tier).unwrap_or(ModelTier::Fast);
192192
match tier {
193193
ModelTier::Fast => "haiku",
194194
ModelTier::Balanced => "sonnet",
@@ -215,7 +215,7 @@ fn resolve_model_for_copilot(model: Option<&str>) -> String {
215215
.to_string(),
216216
}
217217
}
218-
None => "sonnet".to_string(),
218+
None => "gemini-flash".to_string(),
219219
}
220220
}
221221

@@ -348,24 +348,24 @@ mod tests {
348348
assert_eq!(resolve_model_for_claude(Some("sonnet")), "sonnet");
349349
assert_eq!(resolve_model_for_claude(Some("opus")), "opus");
350350
assert_eq!(resolve_model_for_claude(Some("gemini-pro")), "sonnet");
351-
assert_eq!(resolve_model_for_claude(None), "sonnet");
351+
assert_eq!(resolve_model_for_claude(None), "haiku");
352352
}
353353

354354
#[test]
355355
fn test_resolve_copilot_model() {
356356
assert_eq!(resolve_model_for_copilot(Some("gemini-flash")), "gemini-flash");
357357
assert_eq!(resolve_model_for_copilot(Some("sonnet")), "sonnet");
358358
assert_eq!(resolve_model_for_copilot(Some("haiku")), "haiku");
359-
assert_eq!(resolve_model_for_copilot(None), "sonnet");
359+
assert_eq!(resolve_model_for_copilot(None), "gemini-flash");
360360
}
361361

362362
#[test]
363363
fn test_default_config_parses() {
364364
let stripped = strip_json_comments(DEFAULT_CONFIG);
365365
let raw: RawConfig = serde_json::from_str(&stripped).unwrap();
366366
assert!(raw.preferred_ai_cli.is_none());
367-
assert_eq!(raw.claude.model.as_deref(), Some("sonnet"));
368-
assert_eq!(raw.copilot.model.as_deref(), Some("sonnet"));
367+
assert_eq!(raw.claude.model.as_deref(), Some("haiku"));
368+
assert_eq!(raw.copilot.model.as_deref(), Some("gemini-flash"));
369369
}
370370

371371
#[test]

src/diff/mod.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,50 @@
11
mod parser;
2+
pub mod untracked;
23

34
pub use parser::parse;
45

6+
/// Append synthetic diff for untracked files to a raw diff string, parse the
7+
/// combined result, and mark the untracked files in the returned DiffData.
8+
pub fn parse_with_untracked(raw_diff: &str) -> (DiffData, String) {
9+
let untracked_paths = untracked::discover_untracked_files();
10+
parse_with_untracked_paths(raw_diff, &untracked_paths)
11+
}
12+
13+
/// Same as `parse_with_untracked` but accepts pre-discovered untracked paths.
14+
/// Returns `(diff_data, combined_raw_diff)`.
15+
pub fn parse_with_untracked_paths(raw_diff: &str, untracked_paths: &[String]) -> (DiffData, String) {
16+
if untracked_paths.is_empty() {
17+
let data = parse(raw_diff);
18+
return (data, raw_diff.to_string());
19+
}
20+
21+
let (untracked_diff, binary_untracked) =
22+
untracked::generate_untracked_diff(untracked_paths);
23+
24+
let combined = if untracked_diff.is_empty() {
25+
raw_diff.to_string()
26+
} else {
27+
format!("{raw_diff}{untracked_diff}")
28+
};
29+
30+
let mut data = parse(&combined);
31+
32+
// Mark untracked files by matching their paths
33+
let untracked_set: std::collections::HashSet<&str> =
34+
untracked_paths.iter().map(|s| s.as_str()).collect();
35+
for file in &mut data.files {
36+
let path = file.target_file.trim_start_matches("b/");
37+
if untracked_set.contains(path) {
38+
file.is_untracked = true;
39+
}
40+
}
41+
42+
// Add binary untracked files to the binary list
43+
data.binary_files.extend(binary_untracked);
44+
45+
(data, combined)
46+
}
47+
548
/// Top-level parsed diff result.
649
#[derive(Debug, Clone)]
750
pub struct DiffData {
@@ -15,6 +58,7 @@ pub struct DiffFile {
1558
pub source_file: String,
1659
pub target_file: String,
1760
pub is_rename: bool,
61+
pub is_untracked: bool,
1862
pub hunks: Vec<Hunk>,
1963
pub added_count: usize,
2064
pub removed_count: usize,

src/diff/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub fn parse(raw: &str) -> DiffData {
8282
source_file: source,
8383
target_file: target,
8484
is_rename,
85+
is_untracked: false,
8586
hunks,
8687
added_count: pf.added(),
8788
removed_count: pf.removed(),

0 commit comments

Comments
 (0)