Add worktrees, per-hunk review, effort per message, regenerate, voice input

- Isolated git worktree per session (create, status, merge, discard)
- Permission card: accept file-edit hunks individually; agent learns what was rejected
- Reasoning effort per message; regenerate the last answer with another model/effort
- Voice input via the Web Speech API
- Folder-up button in the file browser; ssh, rsync and mc in the image

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 22:29:58 +01:00
1 parent df1924b0a1
commit fca1c41087
22 files changed
+821 -32

No files matched your search

+38 -1
View File
@@ -22,6 +22,27 @@ fn image_type(path: &Path) -> Option<&'static str> {
}
}
/// Apply only the selected hunks (indices of the 3-line-context groups, as in
/// `unified_diff`) of the change old -> new. Returns (result, total hunks).
pub fn apply_hunks(old: &str, new: &str, selected: &[usize]) -> (String, usize) {
let diff = similar::TextDiff::from_lines(old, new);
let groups = diff.grouped_ops(3);
let total = groups.len();
let mut out = String::new();
for op in diff.ops() {
let in_selected = op.tag() != similar::DiffTag::Equal && groups.iter().enumerate().any(|(i, g)| selected.contains(&i) && g.contains(op));
let lines: Vec<&str> = if in_selected {
diff.new_slices()[op.new_range()].to_vec()
} else {
diff.old_slices()[op.old_range()].to_vec()
};
for l in lines {
out.push_str(l);
}
}
(out, total)
}
pub fn unified_diff(path: &str, old: &str, new: &str) -> String {
similar::TextDiff::from_lines(old, new)
.unified_diff()
@@ -235,7 +256,7 @@ impl Tool for Edit {
}
}
fn apply_edit(old: &str, input: &Value) -> Result<String> {
pub fn apply_edit(old: &str, input: &Value) -> Result<String> {
let from = arg_str(input, "old_string")?;
let to = arg_str(input, "new_string")?;
if from.is_empty() {
@@ -484,3 +505,19 @@ mod tests {
assert!(apply_edit("xyz", &input).is_err());
}
}
#[cfg(test)]
mod hunk_tests {
#[test]
fn partial_apply() {
let old: String = (1..=30).map(|i| format!("line {i}\n")).collect();
let new = old.replace("line 2\n", "line TWO\n").replace("line 25\n", "line 25\nextra\n");
let (only_first, total) = super::apply_hunks(&old, &new, &[0]);
assert_eq!(total, 2);
assert!(only_first.contains("line TWO") && !only_first.contains("extra"));
let (only_second, _) = super::apply_hunks(&old, &new, &[1]);
assert!(!only_second.contains("line TWO") && only_second.contains("extra"));
assert_eq!(super::apply_hunks(&old, &new, &[0, 1]).0, new);
assert_eq!(super::apply_hunks(&old, &new, &[]).0, old);
}
}