feat: AI based permission checks

This commit is contained in:
Georg Bauer
2026-07-30 09:47:54 +02:00
parent c101ca1380
commit 73d7c8af27
14 changed files with 413 additions and 28 deletions

View File

@@ -16,7 +16,7 @@ use preferences::{parse_optional_gib, parse_streaming_cache};
use crate::config::{
Config, DevBrainConfig, EndpointConfig, GitConfig, GitDiffAlgorithm, GitDiffLayout,
GitDiffWhitespace,
GitDiffWhitespace, PermissionMode,
};
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(any(target_os = "macos", test))]
@@ -76,6 +76,7 @@ pub(crate) struct App {
restore_dev_brain_confirmation: bool,
selected_project: Option<i32>,
selected_session: Option<i32>,
pub(super) permission_mode: PermissionMode,
/// Unsaved sessions, keyed by project. A draft only becomes a `sessions` row
/// when its first chat turn is stored, so empty ones vanish on restart.
drafts: HashMap<i32, String>,
@@ -178,6 +179,7 @@ pub(crate) struct App {
struct ChatSnapshot {
selected_project: Option<i32>,
selected_session: Option<i32>,
permission_mode: PermissionMode,
composer: text_editor::Content,
queued_inputs: VecDeque<String>,
conversation: Vec<ChatMessage>,
@@ -349,6 +351,7 @@ pub(crate) enum Message {
FocusNext,
FocusPrevious,
PreferenceModelChanged(ModelChoice),
PreferencePermissionModeChanged(PermissionMode),
PreferenceLegacyMtpChanged(bool),
PreferenceDsparkChanged(bool),
PreferenceTimeoutChanged(String),
@@ -446,6 +449,7 @@ pub(crate) enum Message {
CreateSession(i32),
DraftProjectChanged(i32),
SwitchGitBranch(String),
PermissionModeChanged(PermissionMode),
ToggleGitFile(PathBuf),
OpenGitDiff(PathBuf),
SetGitDiffLayout(GitDiffLayout),
@@ -502,6 +506,7 @@ impl App {
#[cfg(target_os = "macos")]
crate::engine::sweep_transient_cache(&transient_cache_path());
let context_limit = config.generation.context_tokens.max(0) as u32;
let default_permission_mode = config.default_permission_mode;
// Reopen on the project we left, with a fresh draft chat.
let last_project = config
.interface
@@ -538,6 +543,7 @@ impl App {
restore_dev_brain_confirmation: false,
selected_project: last_project,
selected_session: None,
permission_mode: default_permission_mode,
drafts,
git_states: HashMap::new(),
git_worktrees: HashMap::new(),
@@ -653,6 +659,7 @@ impl App {
let config = Config::default();
let preference_draft = PreferenceDraft::from_saved(&config);
let context_limit = config.generation.context_tokens.max(0) as u32;
let default_permission_mode = config.default_permission_mode;
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
let git_diff_layout = config.git.diff_layout;
@@ -683,6 +690,7 @@ impl App {
restore_dev_brain_confirmation: false,
selected_project: None,
selected_session: None,
permission_mode: default_permission_mode,
drafts: HashMap::new(),
git_states: HashMap::new(),
git_worktrees: HashMap::new(),
@@ -780,6 +788,7 @@ impl App {
ChatSnapshot {
selected_project: self.selected_project.take(),
selected_session: self.selected_session.take(),
permission_mode: self.permission_mode,
composer: std::mem::take(&mut self.composer),
queued_inputs: std::mem::take(&mut self.queued_inputs),
conversation: std::mem::take(&mut self.conversation),
@@ -828,6 +837,7 @@ impl App {
fn restore_chat_snapshot(&mut self, snapshot: ChatSnapshot) {
self.selected_project = snapshot.selected_project;
self.selected_session = snapshot.selected_session;
self.permission_mode = snapshot.permission_mode;
self.composer = snapshot.composer;
self.queued_inputs = snapshot.queued_inputs;
self.conversation = snapshot.conversation;
@@ -1512,6 +1522,34 @@ impl App {
}
Message::DraftProjectChanged(project_id) => self.move_draft_to_project(project_id),
Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch),
Message::PermissionModeChanged(mode) => {
#[cfg(target_os = "macos")]
if self.active_tools.is_some() {
self.error = Some(
"Wait for the current tool batch before changing permission mode.".into(),
);
return Task::none();
}
if let Some(session_id) = self.selected_session {
let Some(database) = &mut self.database else {
self.error = Some("The project database is unavailable.".into());
return Task::none();
};
match database.set_session_permission_mode(session_id, mode) {
Ok(()) => {
self.permission_mode = mode;
self.error = None;
self.reload_projects();
}
Err(error) => {
self.error = Some(format!("Could not save permission mode: {error}"));
}
}
} else {
self.permission_mode = mode;
self.error = None;
}
}
Message::ToggleGitFile(path) => self.toggle_git_file(path),
Message::OpenGitDiff(path) => self.open_git_diff(&path),
Message::SetGitDiffLayout(layout) => self.git_diff_layout = layout,
@@ -1681,6 +1719,7 @@ impl App {
session.context_used,
session.context_limit,
session.last_tokens_per_second,
session.permission_mode(),
)
});
let Some(database) = &mut self.database else {
@@ -1722,7 +1761,8 @@ impl App {
self.selected_session = Some(session_id);
self.system_prompt_seen_at = 0;
self.queued_inputs.clear();
let (used, limit, tokens_per_second) = saved_context.unwrap_or_default();
let (used, limit, tokens_per_second, permission_mode) =
saved_context.unwrap_or((0, 0, None, PermissionMode::default()));
self.context_used = used.max(0) as u32;
self.context_limit = if limit > 0 {
limit as u32
@@ -1730,6 +1770,7 @@ impl App {
self.config.generation.context_tokens.max(0) as u32
};
self.tokens_per_second = tokens_per_second;
self.permission_mode = permission_mode;
self.error = restore_error;
self.reload_projects();
return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);