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

+9 -6
View File
@@ -25,7 +25,7 @@ pub enum PermissionReply {
struct PendingPermission {
event: Event,
tx: oneshot::Sender<(PermissionReply, Option<String>)>,
tx: oneshot::Sender<(PermissionReply, Option<String>, Option<Vec<usize>>)>,
}
struct PendingQuestion {
@@ -41,6 +41,8 @@ pub struct Runtime {
pub approved: HashSet<String>,
/// The model request currently running (for clients that connect mid-step).
pub step: Option<Value>,
/// Reasoning effort chosen for the current turn (overrides the model's default).
pub effort: Option<String>,
}
pub struct App {
@@ -436,7 +438,7 @@ impl App {
input: Value,
diff: Option<String>,
cancel: CancellationToken,
) -> (PermissionReply, Option<String>) {
) -> (PermissionReply, Option<String>, Option<Vec<usize>>) {
let id = crate::types::new_id();
let event = Event::PermissionRequest {
id: id.clone(),
@@ -451,17 +453,18 @@ impl App {
self.permissions.lock().unwrap().insert(id.clone(), PendingPermission { event: event.clone(), tx });
self.emit(event);
let res = tokio::select! {
r = rx => r.unwrap_or((PermissionReply::Reject, None)),
_ = cancel.cancelled() => (PermissionReply::Reject, Some("aborted".into())),
r = rx => r.unwrap_or((PermissionReply::Reject, None, None)),
_ = cancel.cancelled() => (PermissionReply::Reject, Some("aborted".into()), None),
};
self.permissions.lock().unwrap().remove(&id);
self.emit(Event::PermissionResolved { id, session_id: session_id.to_string() });
res
}
pub fn reply_permission(&self, id: &str, reply: PermissionReply, message: Option<String>) -> Result<()> {
/// `hunks`: for file edits, the indices of the diff hunks the user accepted (None = all).
pub fn reply_permission(&self, id: &str, reply: PermissionReply, message: Option<String>, hunks: Option<Vec<usize>>) -> Result<()> {
let p = self.permissions.lock().unwrap().remove(id).ok_or_else(|| anyhow!("permission not pending"))?;
let _ = p.tx.send((reply, message));
let _ = p.tx.send((reply, message, hunks));
Ok(())
}
}