//! The General page, row for row after T3 Code's: New threads, //! Organization, Behavior, Projects & threads, Confirmations, Text //! generation, About, Diagnostics and the folded Legacy features. use gpui_kit::component::h_flex; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use super::widgets::{ RichOption, Section, SettingRow, choice, code, edit, folded, model_control, reasoning_control, reset, rich_choice, stepper, switch, switch_then, text_field, }; use super::{Page, SettingsView}; use crate::claude::PermissionMode; use crate::models::Catalog; use crate::session::Reasoning; use crate::settings::{ self, BackgroundActivity, FollowUp, ProjectOrder, QuitShortcut, SendShortcut, Streaming, Submodules, TimeFormat, WorkspaceMode, }; use crate::ui::metal::{self, MetalButton}; use crate::ui::{pickers, workspace}; const WORKSPACES: &[(WorkspaceMode, &str)] = &[(WorkspaceMode::Local, "Current checkout"), (WorkspaceMode::Worktree, "New worktree")]; const SUBMODULES: &[(Submodules, &str)] = &[(Submodules::Recursive, "Recursive"), (Submodules::TopLevel, "Top level only"), (Submodules::Skip, "Skip")]; const PROJECT_ORDERS: &[(ProjectOrder, &str)] = &[ (ProjectOrder::LastMessage, "Last user message"), (ProjectOrder::Added, "Created at"), (ProjectOrder::Name, "Name"), ]; /// `(notify, sound)`. const NOTIFICATIONS: &[((bool, bool), &str)] = &[ ((false, false), "Off"), ((true, false), "Notifications only"), ((false, true), "Sound only"), ((true, true), "Notifications with sound"), ]; const TIME_FORMATS: &[(TimeFormat, &str)] = &[(TimeFormat::System, "System default"), (TimeFormat::Hour12, "12-hour"), (TimeFormat::Hour24, "24-hour")]; const STREAMING: &[(Streaming, &str)] = &[ (Streaming::Turn, "Wait for the full response"), (Streaming::Paragraphs, "Show finished paragraphs"), (Streaming::Tokens, "Stream every token"), ]; const DIFF_STATES: &[(bool, &str)] = &[(false, "Expanded"), (true, "Collapsed")]; const DIFF_LAYOUTS: &[(bool, &str)] = &[(false, "Stacked"), (true, "Split")]; const SEND_SHORTCUTS: &[(SendShortcut, &str)] = &[ (SendShortcut::Enter, "Enter"), (SendShortcut::ModEnterMultiline, "Ctrl + Enter for multiline prompts"), (SendShortcut::ModEnter, "Ctrl + Enter always"), ]; const FOLLOW_UPS: &[(FollowUp, &str)] = &[(FollowUp::Queue, "Queue"), (FollowUp::Steer, "Steer")]; const BACKGROUND: &[(BackgroundActivity, &str)] = &[ (BackgroundActivity::Balanced, "Balanced"), (BackgroundActivity::Always, "Performance"), (BackgroundActivity::Minimal, "Battery saver"), ]; const QUIT_SHORTCUTS: &[(QuitShortcut, &str)] = &[(QuitShortcut::Press, "Direct"), (QuitShortcut::Hold, "Hold"), (QuitShortcut::Double, "Double press")]; /// T3 Code's description of each permission mode. fn permission_description(mode: PermissionMode) -> &'static str { match mode { PermissionMode::Default => "Ask before commands and file changes.", PermissionMode::AcceptEdits => "Auto-approve edits, ask before other actions.", PermissionMode::Auto => "Supported providers approve routine actions; others still ask.", PermissionMode::Plan => "Plan first, without editing files or running commands.", PermissionMode::BypassPermissions => "Allow commands and edits without prompts.", } } impl SettingsView { pub(super) fn render_general(&mut self, window: &mut Window, cx: &mut Context) -> Vec { let p = metal::palette(cx); let saved = settings::load(); let view = cx.weak_entity(); let row = SettingRow::new; // ---- New threads ---- let model_pick = { let view = view.clone(); move |key: Option, window: &mut Window, cx: &mut App| { edit(&view, window, cx, move |settings| settings.default_model = key) } }; let reasoning_pick = { let view = view.clone(); move |reasoning: Option, window: &mut Window, cx: &mut App| { edit(&view, window, cx, move |settings| { settings.default_reasoning = reasoning.map(|reasoning| reasoning.key().to_string()) }) } }; let mut permissions = vec![RichOption { value: None, icon: None, label: "Automatic", description: "Start with the permissions of the thread you have open.", }]; permissions.extend(PermissionMode::ALL.into_iter().map(|mode| RichOption { value: Some(mode), icon: Some(pickers::access_icon(mode)), label: mode.label(), description: permission_description(mode), })); let current_access = saved.default_access.as_deref().and_then(PermissionMode::from_cli); let new_threads = Section::new("New threads") .id("project-defaults") .row( row("Model") .id("default-model") .description("Default model for new threads. Projects can override it.") .when(saved.default_model.is_none(), |row| row.status("Automatic")) .reset(reset(&view, |s| (s.default_model.clone(), s.default_reasoning.clone()), |s, (model, reasoning)| { s.default_model = model; s.default_reasoning = reasoning; })) .control( h_flex() .gap(px(6.)) .child(model_control("default-model", saved.default_model.clone(), "Automatic", false, model_pick, cx)) .child(reasoning_control( "default-reasoning", saved.default_reasoning.as_deref().and_then(Reasoning::from_key), "Automatic", reasoning_pick, )), ), ) .row( row("Permissions") .id("default-permissions") .description("Default permissions for new threads. Pi and jcode threads always run with full access.") .reset(reset(&view, |s| s.default_access.clone(), |s, access| s.default_access = access)) .control(rich_choice("default-access", current_access, permissions, &view, |settings, mode| { settings.default_access = mode.map(|mode: PermissionMode| mode.as_cli().to_string()) })), ) .row( row("Workspace") .id("new-threads") .description("Where new threads start. The start screen can switch it for each thread.") .reset(reset(&view, |s| s.workspace, |s, mode| s.workspace = mode)) .control(choice("workspace", saved.workspace, WORKSPACES, &view, |settings, mode| settings.workspace = mode)), ) .row( row("Submodules") .id("worktree-submodules") .description("How new worktrees populate git submodules.") .reset(reset(&view, |s| s.submodules, |s, mode| s.submodules = mode)) .control(choice("submodules", saved.submodules, SUBMODULES, &view, |settings, mode| settings.submodules = mode)), ) .render(cx); // ---- Organization ---- let mut organization = Section::new("Organization") .row( row("Project grouping") .id("project-grouping") .description("Combine matching repositories across environments.") .reset(reset(&view, |s| s.project_grouping, |s, grouping| s.project_grouping = grouping)) // On groups by repository, as T3 Code's switch does; off keeps every project apart. .control(switch( "project-grouping", saved.project_grouping != crate::settings::ProjectGrouping::Separate, &view, p, |settings, on| { settings.project_grouping = if on { crate::settings::ProjectGrouping::Repository } else { crate::settings::ProjectGrouping::Separate } }, )), ) .row( row("Project order") .id("project-order") .description("Order of projects in the sidebar project picker and command palette.") .reset(reset(&view, |s| s.project_order, |s, order| s.project_order = order)) .control(choice("project-order", saved.project_order, PROJECT_ORDERS, &view, |settings, order| { settings.project_order = order })), ) .row( row("Auto-resume limited threads") .id("auto-resume-limited-threads") .description( "Resume usage-limit stops at the reported reset time. Each thread can cancel its scheduled continuation.", ) .reset(reset(&view, |s| s.auto_resume_limited, |s, on| s.auto_resume_limited = on)) .control(switch("auto-resume", saved.auto_resume_limited, &view, p, |settings, on| { settings.auto_resume_limited = on })), ) .row( row("Snooze limited threads") .id("snooze-limited-threads") .description( "Snooze usage-limit stops until the reported reset time. Combine with auto-resume to continue when they wake.", ) .reset(reset(&view, |s| s.snooze_limited, |s, on| s.snooze_limited = on)) .control(switch("snooze-limited", saved.snooze_limited, &view, p, |settings, on| settings.snooze_limited = on)), ) .row( row("Working section (beta)") .id("working-shelf") .description( "Fold working and monitoring threads into a Working section. They return to the top of the inbox when they need you.", ) .reset(reset(&view, |s| s.working_section, |s, on| s.working_section = on)) .control(switch("working-section", saved.working_section, &view, p, |settings, on| { settings.working_section = on })), ) .row( row("Auto-settle merged threads") .id("auto-settle-merged-threads") .description("Settle a thread when its pull request merges. Closed pull requests still settle automatically.") .reset(reset(&view, |s| s.auto_settle_merged, |s, on| s.auto_settle_merged = on)) .control(switch("auto-settle-merged", saved.auto_settle_merged, &view, p, |settings, on| { settings.auto_settle_merged = on })), ) .row( row("Auto-settle inactive threads") .id("auto-settle-inactive-threads") .description("Sidebar threads with no activity for this long settle automatically.") .reset(reset(&view, |s| (s.auto_settle_inactive, s.inactive_days), |s, (on, days)| { s.auto_settle_inactive = on; s.inactive_days = days; })) .control(switch("auto-settle-inactive", saved.auto_settle_inactive, &view, p, |settings, on| { settings.auto_settle_inactive = on; if on { settings.inactive_days = 3; } })), ); if saved.auto_settle_inactive { organization = organization.row( row("Days of inactivity before auto-settle") .id("days-before-auto-settle") .description("Any new activity un-settles a thread automatically.") .control(stepper("inactive-days", u64::from(saved.inactive_days), 1, (1, 90), "", &view, p, |settings, days| { settings.inactive_days = days as u32 })), ); } let organization = organization.render(cx); // ---- Behavior ---- let can_notify = workspace::find_program(&["notify-send"]).is_some(); let notification_mode = (saved.notify, saved.notify_sound); let streaming_description = match saved.streaming { Streaming::Turn => "Text appears once the agent finishes its turn.", Streaming::Paragraphs => "Each paragraph or code block appears as soon as it is complete.", Streaming::Tokens => "Text appears token by token as the agent writes it.", }; let follow_up_description = format!( "Queue follow-ups while the agent runs or steer the current run. {}", match saved.send_shortcut { SendShortcut::Enter => "Press Ctrl + Enter to do the opposite for one message.", SendShortcut::ModEnter => "Press Ctrl + Shift + Enter to do the opposite for one message.", SendShortcut::ModEnterMultiline => { "Press Ctrl + Enter for single-line prompts or Ctrl + Shift + Enter for multiline prompts to do the opposite for one message." } } ); let background_description = match saved.background_activity { BackgroundActivity::Balanced => { "Pauses health checks and git refreshes while the window has been idle for a while, or in low power mode." } BackgroundActivity::Always => "Allows background checks whenever the app is open.", BackgroundActivity::Minimal => "Also pauses background checks whenever the window is in the background.", }; let behavior = Section::new("Behavior") .row( row("Thread notifications") .id("thread-notifications") .description(if can_notify || notification_mode.0 == false { "System alerts when a thread finishes, fails, or needs input or approval. Applies to this device while Vectrum Code is open." } else { "Notifications need notify-send (libnotify), which isn't installed. Sound only is still available." }) .reset(reset(&view, |s| (s.notify, s.notify_sound), |s, (notify, sound)| { s.notify = notify; s.notify_sound = sound; })) .control(choice("thread-notifications", notification_mode, NOTIFICATIONS, &view, |settings, (notify, sound)| { settings.notify = notify; settings.notify_sound = sound; })), ) .row( row("In-app notifications") .id("in-app-notifications") .description( "Show a toast when another thread finishes, fails, or needs input or approval while this app has focus.", ) .control(switch("in-app-notifications", saved.in_app_notifications, &view, p, |settings, on| { settings.in_app_notifications = on })), ) .row( row("Time format") .id("time-format") .description("System default follows your OS clock preference.") .reset(reset(&view, |s| s.time_format, |s, format| s.time_format = format)) .control(choice("time-format", saved.time_format, TIME_FORMATS, &view, |settings, format| { settings.time_format = format })), ) .row( row("Response streaming") .id("response-streaming") .description(streaming_description) .reset(reset(&view, |s| s.streaming, |s, streaming| s.streaming = streaming)) .control(choice("streaming", saved.streaming, STREAMING, &view, |settings, streaming| { settings.streaming = streaming })), ) .row( row("Fold finished turns") .id("fold-turns") .description("Tuck a finished turn's tool calls and interim messages into one \u{201c}Worked for\u{201d} row.") .reset(reset(&view, |s| s.fold_turns, |s, on| s.fold_turns = on)) .control(switch("fold-turns", saved.fold_turns, &view, p, |settings, on| settings.fold_turns = on)), ) .row( row("Show costs") .id("show-costs") .description("What each turn and thread cost, in the transcript and the project panel.") .reset(reset(&view, |s| s.show_costs, |s, on| s.show_costs = on)) .control(switch("show-costs", saved.show_costs, &view, p, |settings, on| settings.show_costs = on)), ) .row( row("Hide whitespace changes") .id("hide-whitespace-changes") .description("Set whether the diff panel ignores whitespace-only edits by default.") .reset(reset(&view, |s| s.hide_whitespace, |s, on| s.hide_whitespace = on)) .control(switch("hide-whitespace", saved.hide_whitespace, &view, p, |settings, on| { settings.hide_whitespace = on })), ) .row( row("Default diff file state") .id("default-diff-file-state") .description("Start with files expanded or collapsed when opening diffs.") .reset(reset(&view, |s| s.diff_collapsed, |s, collapsed| s.diff_collapsed = collapsed)) .control(choice("diff-collapsed", saved.diff_collapsed, DIFF_STATES, &view, |settings, collapsed| { settings.diff_collapsed = collapsed })), ) .row( row("Diff layout") .id("diff-layout") .description("Show diffs stacked or side by side. The toggle in the diff toolbar changes this too.") .reset(reset(&view, |s| s.diff_side_by_side, |s, split| s.diff_side_by_side = split)) .control(choice("diff-layout", saved.diff_side_by_side, DIFF_LAYOUTS, &view, |settings, split| { settings.diff_side_by_side = split })), ) .row( row("Proactive panels") .id("proactive-panels") .description("Open Changes for edits to at least 3 files or 50 lines.") .reset(reset(&view, |s| s.proactive_panels, |s, on| s.proactive_panels = on)) .control(switch("proactive-panels", saved.proactive_panels, &view, p, |settings, on| { settings.proactive_panels = on })), ) .row( row("Show skills in slash menu") .id("skills-in-slash-menu") .description("Also include skills in the / command menu. Skills always appear when you type $.") .reset(reset(&view, |s| s.slash_skills, |s, on| s.slash_skills = on)) .control(switch("slash-skills", saved.slash_skills, &view, p, |settings, on| settings.slash_skills = on)), ) .row( row("Rich text composer") .id("composer-rich-text") .description("Show formatted Markdown as you type.") .reset(reset(&view, |s| s.rich_composer, |s, on| s.rich_composer = on)) .control(switch("rich-composer", saved.rich_composer, &view, p, |settings, on| settings.rich_composer = on)), ) .row( row("Collapse composer on scroll") .id("composer-collapse") .description( "Rest the composer of an existing thread into a single line when you scroll the conversation. Focus the composer or start typing to expand it again.", ) .reset(reset(&view, |s| s.collapse_composer, |s, on| s.collapse_composer = on)) .control(switch("collapse-composer", saved.collapse_composer, &view, p, |settings, on| { settings.collapse_composer = on })), ) .row( row("Send shortcut") .id("send-shortcut") .description("Choose when Enter sends a prompt or inserts a new line") .reset(reset(&view, |s| s.send_shortcut, |s, shortcut| s.send_shortcut = shortcut)) .control(choice("send-shortcut", saved.send_shortcut, SEND_SHORTCUTS, &view, |settings, shortcut| { settings.send_shortcut = shortcut })), ) .row( row("Follow-up behavior") .id("follow-up-behavior") .description(follow_up_description) .reset(reset(&view, |s| s.follow_up, |s, follow_up| s.follow_up = follow_up)) .control(choice("follow-up", saved.follow_up, FOLLOW_UPS, &view, |settings, follow_up| { settings.follow_up = follow_up })), ) .row( row("Provider update checks") .id("provider-update-checks") .description("Check installed provider CLIs for newer available versions.") .reset(reset(&view, |s| s.update_checks, |s, on| s.update_checks = on)) .control(switch_then( "update-checks", saved.update_checks, &view, p, |settings, on| settings.update_checks = on, Catalog::refresh_all, )), ) .row( row("Continue threads after restarts") .id("continue-threads-after-server-update") .description("Automatically resume interrupted threads after an update, crash, or machine restart.") .reset(reset(&view, |s| s.continue_after_restart, |s, on| s.continue_after_restart = on)) .control(switch("continue-after-restart", saved.continue_after_restart, &view, p, |settings, on| { settings.continue_after_restart = on })), ) .row( row("Background activity") .id("background-activity") .description(background_description) .reset(reset(&view, |s| s.background_activity, |s, activity| s.background_activity = activity)) .control(choice("background-activity", saved.background_activity, BACKGROUND, &view, |settings, activity| { settings.background_activity = activity })), ) .render(cx); // ---- Projects & threads ---- let projects = Section::new("Projects & threads") .row( row("Start from origin") .id("start-from-origin") .description("Creates the worktree from the latest matching branch on origin instead of your local branch.") .reset(reset(&view, |s| s.start_from_origin, |s, on| s.start_from_origin = on)) .control(switch("start-from-origin", saved.start_from_origin, &view, p, |settings, on| { settings.start_from_origin = on })), ) .row( row("Add project starts in") .id("add-project-starts-in") .description("Leave empty to use \u{201c}~/\u{201d} when the Add Project browser opens.") .reset(reset(&view, |s| s.add_project_dir.clone(), |s, dir| s.add_project_dir = dir)) .control(text_field(&self.add_project_dir, 288., window, cx)), ) .render(cx); // ---- Confirmations ---- let confirmations = Section::new("Confirmations") .row( row("Unpin confirmation") .id("unpin-confirmation") .description("Ask before unpinning a thread from the pinned section.") .reset(reset(&view, |s| s.confirm_unpin, |s, on| s.confirm_unpin = on)) .control(switch("confirm-unpin", saved.confirm_unpin, &view, p, |settings, on| settings.confirm_unpin = on)), ) .row( row("Archive confirmation") .id("archive-confirmation") .description("Require a second click on the inline archive action before a thread is archived.") .reset(reset(&view, |s| s.confirm_settle, |s, on| s.confirm_settle = on)) .control(switch("confirm-settle", saved.confirm_settle, &view, p, |settings, on| settings.confirm_settle = on)), ) .row( row("Delete confirmation") .id("delete-confirmation") .description("Ask before deleting a thread and its chat history.") .reset(reset(&view, |s| s.confirm_delete, |s, on| s.confirm_delete = on)) .control(switch("confirm-delete", saved.confirm_delete, &view, p, |settings, on| settings.confirm_delete = on)), ) .row( row("Quit shortcut") .id("quit-confirmation") .description("Hold mode also quits on two quick presses.") .reset(reset(&view, |s| s.quit_shortcut, |s, shortcut| s.quit_shortcut = shortcut)) .control(choice("quit-shortcut", saved.quit_shortcut, QUIT_SHORTCUTS, &view, |settings, shortcut| { settings.quit_shortcut = shortcut })), ) .render(cx); // ---- Text generation ---- let text_pick = { let view = view.clone(); move |key: Option, window: &mut Window, cx: &mut App| { edit(&view, window, cx, move |settings| settings.text_model = key.unwrap_or_default()) } }; let text_reasoning_pick = { let view = view.clone(); move |reasoning: Option, window: &mut Window, cx: &mut App| { edit(&view, window, cx, move |settings| { settings.text_reasoning = reasoning.map(|reasoning| reasoning.key().to_string()) }) } }; let text_model = Some(saved.text_model.clone()).filter(|key| !key.is_empty()); let text_generation = Section::new("Text generation") .row( row("Text generation model") .id("text-generation-model") .description("Used for thread titles and worktree branch names. Off keeps the first line of the first message.") .reset(reset(&view, |s| (s.text_model.clone(), s.text_reasoning.clone()), |s, (model, reasoning)| { s.text_model = model; s.text_reasoning = reasoning; })) .control( h_flex() .gap(px(6.)) .child(model_control("text-model", text_model.clone(), "Off", true, text_pick, cx)) .when(text_model.is_some(), |this| { this.child(reasoning_control( "text-reasoning", saved.text_reasoning.as_deref().and_then(Reasoning::from_key), "Default", text_reasoning_pick, )) }), ), ) .render(cx); // ---- About ---- let about = Section::new("About") .row( row("Version") .id("version") .title_suffix(code(env!("CARGO_PKG_VERSION"), p)) .description("Current version of the application."), ) .render(cx); // ---- Diagnostics ---- let diagnostics = Section::new("Diagnostics") .row( row("Diagnostics") .id("diagnostics") .description("Inspect processes, resource use, and logs on this environment.") .control( MetalButton::new("view-diagnostics") .compact() .label("View diagnostics") .on_click(cx.listener(|this, _, _, cx| this.open_page(Page::Diagnostics, cx))), ), ) .row( row("Open source licenses") .id("open-source-licenses") .description("Notices for dependencies, assets, and optional tools used by Vectrum Code.") .control( MetalButton::new("view-licenses") .compact() .label("View licenses") .on_click(cx.listener(|this, _, _, cx| this.open_page(Page::Licenses, cx))), ), ) .render(cx); // ---- Legacy features ---- let legacy = folded( "legacy", "Legacy features", self.open_folds.contains("legacy"), vec![ row("Plan mode (legacy)") .id("legacy-plan-mode") .description("Restore Build/Plan, /plan, /default, and Shift+Tab. Off uses build mode.") .reset(reset(&view, |s| s.plan_mode, |s, on| s.plan_mode = on)) .control(switch("plan-mode", saved.plan_mode, &view, p, |settings, on| settings.plan_mode = on)) .into_any_element(), row("Context window indicator (legacy)") .id("legacy-context-window-indicator") .description("Shows context window usage as a circular indicator in the composer.") .reset(reset(&view, |s| s.context_meter, |s, on| s.context_meter = on)) .control(switch("context-meter", saved.context_meter, &view, p, |settings, on| settings.context_meter = on)) .into_any_element(), row("Sidebar (legacy)") .id("legacy-sidebar") .description("Restore per-project thread trees instead of the default flat sidebar.") .reset(reset(&view, |s| s.legacy_sidebar, |s, on| s.legacy_sidebar = on)) .control(switch("legacy-sidebar", saved.legacy_sidebar, &view, p, |settings, on| settings.legacy_sidebar = on)) .into_any_element(), ], &view, p, ); vec![new_threads, organization, behavior, projects, confirmations, text_generation, about, diagnostics, legacy] } }