671 lines
26 KiB
Rust
671 lines
26 KiB
Rust
use std::path::Path;
|
|
use std::sync::LazyLock;
|
|
use std::time::Duration;
|
|
|
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
|
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
|
use ai::index::local_project_index::LocalProjectIndexManager;
|
|
use markdown_parser::FormattedTextFragment;
|
|
use rand::seq::SliceRandom;
|
|
use warpui::keymap::Keystroke;
|
|
use warpui::r#async::{SpawnedFutureHandle, Timer};
|
|
use warpui::{AppContext, Entity, ModelContext, SingletonEntity};
|
|
|
|
use crate::ai::persisted_workspace::PersistedWorkspace;
|
|
use crate::palette::PaletteMode;
|
|
use crate::server::telemetry::PaletteSource;
|
|
use crate::settings::AISettings;
|
|
use crate::terminal::input::SET_INPUT_MODE_AGENT_ACTION_NAME;
|
|
use crate::terminal::view::init::{
|
|
CANCEL_COMMAND_KEYBINDING, SELECT_PREVIOUS_BLOCK_ACTION_NAME,
|
|
TOGGLE_AUTOEXECUTE_MODE_KEYBINDING,
|
|
};
|
|
use crate::util::bindings::trigger_to_keystroke;
|
|
use crate::workspace::view::{
|
|
TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME, TOGGLE_RIGHT_PANEL_BINDING_NAME,
|
|
};
|
|
use crate::workspace::WorkspaceAction;
|
|
use crate::workspaces::user_workspaces::UserWorkspaces;
|
|
|
|
/// Trait for tip implementations that can be displayed to users.
|
|
/// Tips provide helpful information with optional links and keybindings.
|
|
pub trait AITip: Clone {
|
|
/// Returns the keystroke for this tip, if applicable.
|
|
fn keystroke(&self, app: &AppContext) -> Option<Keystroke>;
|
|
|
|
/// Returns the documentation link for this tip, if available.
|
|
fn link(&self) -> Option<String>;
|
|
|
|
/// Returns the raw description text for this tip.
|
|
fn description(&self) -> &str;
|
|
|
|
/// Converts the tip to formatted text fragments for rendering.
|
|
/// Default implementation adds "Tip: " prefix and parses backtick-wrapped text as inline code.
|
|
fn to_formatted_text(&self, _app: &AppContext) -> Vec<FormattedTextFragment> {
|
|
let text = format!("Tip: {}", self.description());
|
|
|
|
// Style backtick-wrapped text as inline code
|
|
let parts: Vec<&str> = text.split('`').collect();
|
|
let mut fragments = Vec::new();
|
|
for (i, part) in parts.iter().enumerate() {
|
|
if part.is_empty() {
|
|
continue;
|
|
}
|
|
if i % 2 == 0 {
|
|
fragments.push(FormattedTextFragment::plain_text(part.to_string()));
|
|
} else {
|
|
fragments.push(FormattedTextFragment::inline_code(part.to_string()));
|
|
}
|
|
}
|
|
fragments
|
|
}
|
|
|
|
/// Checks if this tip is applicable in the current context.
|
|
/// Default implementation returns true (tip is always applicable).
|
|
fn is_tip_applicable(
|
|
&self,
|
|
_current_working_directory: Option<&str>,
|
|
_app: &AppContext,
|
|
) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Kinds of agent tips for organizing and filtering.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum AgentTipKind {
|
|
CodebaseContext,
|
|
WarpDrive,
|
|
General,
|
|
Mcp,
|
|
SlashCommands,
|
|
/// Tips about adding context (files, blocks, URLs, images, @-mentions, rules)
|
|
Context,
|
|
/// Tips about code editors, file trees, and code review panes
|
|
Code,
|
|
/// Tips about local-to-cloud handoff
|
|
Handoff,
|
|
}
|
|
|
|
static DEFAULT_TIPS: LazyLock<Vec<AgentTip>> = LazyLock::new(|| {
|
|
vec![
|
|
AgentTip {
|
|
description: "`/` to open the slash-command menu and access quick agent actions.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/slash-commands".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to toggle natural language detection and switch between agent and terminal input.".to_string(),
|
|
link: Some("https://docs.warp.dev/terminal/input/universal-input#input-modes".to_string()),
|
|
binding_name: Some(SET_INPUT_MODE_AGENT_ACTION_NAME),
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "`/plan` <prompt> to create a plan for the agent before executing.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/planning".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to open the Command Palette and access Warp actions and shortcuts.".to_string(),
|
|
link: Some("https://docs.warp.dev/terminal/command-palette".to_string()),
|
|
binding_name: Some(TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME),
|
|
action: Some(WorkspaceAction::OpenPalette {
|
|
mode: PaletteMode::Command,
|
|
source: PaletteSource::AgentTip,
|
|
query: None,
|
|
}),
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Store reusable workflows, notebooks, and prompts in your".to_string(),
|
|
link: Some("https://docs.warp.dev/knowledge-and-collaboration/warp-drive".to_string()),
|
|
binding_name: None,
|
|
action: Some(WorkspaceAction::OpenGalaxyDrive),
|
|
kind: AgentTipKind::WarpDrive,
|
|
},
|
|
AgentTip {
|
|
description: "Enter a new prompt to redirect the agent while it's running.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "`@` to add context from files, blocks, or Galaxy Drive objects to your prompt.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/using-to-add-context".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to attach the prior command output as agent context.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/blocks-as-context#attaching-blocks-as-context".to_string()),
|
|
binding_name: Some(SELECT_PREVIOUS_BLOCK_ACTION_NAME),
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "`/init` to index the repo so the agent can understand your codebase.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/codebase-context".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::CodebaseContext,
|
|
},
|
|
AgentTip {
|
|
description: "Add agent profiles to customize permissions and models per session.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Right-click a block to fork the conversation from that point.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/conversation-forking".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Right-click a block to copy a conversation's output.".to_string(),
|
|
link: Some("https://docs.warp.dev/terminal/blocks/block-actions#copy-input-output-of-block".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Drag an image into the pane to attach it as agent context.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/images-as-context".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "Prompt the agent to control interactive tools like node, python, postgres, gdb, or vim.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/full-terminal-use".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to open the code review panel and review the agent's changes.".to_string(),
|
|
link: Some("https://docs.warp.dev/code/code-review".to_string()),
|
|
binding_name: Some(TOGGLE_RIGHT_PANEL_BINDING_NAME),
|
|
action: None,
|
|
kind: AgentTipKind::Code,
|
|
},
|
|
AgentTip {
|
|
description: "`/add-mcp` to add an MCP server to your workspace.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/mcp".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Mcp,
|
|
},
|
|
AgentTip {
|
|
description: "`/open-mcp-servers` to view and share MCP servers with your team.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Mcp,
|
|
},
|
|
AgentTip {
|
|
description: "`/create-environment` to turn a repo into a remote docker environment an agent can run in.".to_string(),
|
|
link: Some("https://docs.warp.dev/reference/cli/integration-setup".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "`/add-prompt` to create a reusable prompt for repeatable workflows.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::WarpDrive,
|
|
},
|
|
AgentTip {
|
|
description: "`/add-rule` to create a global agent rule.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/rules".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "`/fork` to create a fresh copy of the current conversation, optionally with a new prompt.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/conversation-forking".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "`/open-code-review` to open the code review panel and inspect agent-generated diffs.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: Some(WorkspaceAction::ToggleRightPanel),
|
|
kind: AgentTipKind::Code,
|
|
},
|
|
AgentTip {
|
|
description: "`/new` to start a new agent conversation with clean context.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "`/compact` to summarize the current conversation and free up space in the context window.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "`/usage` to show your current AI credits usage.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Right-click selected text to attach it as agent context.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/blocks-as-context#attaching-blocks-as-context".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "Use `AGENTS.md` or `CLAUDE.md` to apply project-scoped rules.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/rules#project-rules-1".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "Paste a URL to attach that webpage as context for the agent.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/local-agents/agent-context/urls-as-context".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Context,
|
|
},
|
|
AgentTip {
|
|
description: "Wormhole a remote SSH session to enable the agent inside that environment.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Switch agent profiles to quickly change models and agent permissions.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "`/init` to generate a `GALAXY.md` file and define project rules for the agent.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/rules".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::SlashCommands,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to auto-approve the agent's commands and diffs for the rest of the session.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/capabilities/full-terminal-use#session-level-approvals".to_string()),
|
|
binding_name: Some(TOGGLE_AUTOEXECUTE_MODE_KEYBINDING),
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "Type `&` or use the handoff chip to move a local conversation to the cloud.".to_string(),
|
|
link: None,
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::Handoff,
|
|
},
|
|
AgentTip {
|
|
description: "Enable desktop notifications to get an alert when an agent needs your attention.".to_string(),
|
|
link: Some("https://docs.warp.dev/agent-platform/cloud-agents/managing-cloud-agents#in-app-agent-notifications".to_string()),
|
|
binding_name: None,
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
AgentTip {
|
|
description: "<keybinding> to cancel the current agent task.".to_string(),
|
|
link: None,
|
|
binding_name: Some(CANCEL_COMMAND_KEYBINDING),
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
},
|
|
]
|
|
});
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct AgentTip {
|
|
/// The text that will be displayed to the user. This is parsed such that:
|
|
/// "Tip: " is added as a prefix,
|
|
/// "<keybinding>" is replaced with user-defined and platform-specific keybinding referenced by binding_name,
|
|
/// `text` that is wrapped in backticks is formatted as inline code
|
|
pub description: String,
|
|
pub link: Option<String>,
|
|
pub binding_name: Option<&'static str>,
|
|
pub action: Option<WorkspaceAction>,
|
|
/// The kind of the tip, used for filtering and organization
|
|
pub kind: AgentTipKind,
|
|
}
|
|
|
|
impl AITip for AgentTip {
|
|
fn keystroke(&self, app: &AppContext) -> Option<Keystroke> {
|
|
let binding_name = self.binding_name?;
|
|
|
|
// Special case: voice input uses settings, not editable bindings
|
|
if binding_name == "FN" {
|
|
return AISettings::as_ref(app).voice_input_toggle_key.keystroke();
|
|
}
|
|
|
|
if let Some(binding) = app.editable_bindings().find(|b| b.name == binding_name) {
|
|
return trigger_to_keystroke(binding.trigger);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn link(&self) -> Option<String> {
|
|
self.link.clone()
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
&self.description
|
|
}
|
|
|
|
fn to_formatted_text(&self, app: &AppContext) -> Vec<FormattedTextFragment> {
|
|
let mut text = format!("Tip: {}", self.description);
|
|
|
|
// Replace <keybinding> with the actual keybinding string
|
|
if let Some(keystroke) = self.keystroke(app) {
|
|
text = text.replace("<keybinding>", &keystroke.displayed());
|
|
}
|
|
|
|
// Style backtick-wrapped text as inline code
|
|
let parts: Vec<&str> = text.split('`').collect();
|
|
let mut fragments = Vec::new();
|
|
for (i, part) in parts.iter().enumerate() {
|
|
if part.is_empty() {
|
|
continue;
|
|
}
|
|
if i % 2 == 0 {
|
|
fragments.push(FormattedTextFragment::plain_text(part.to_string()));
|
|
} else {
|
|
fragments.push(FormattedTextFragment::inline_code(part.to_string()));
|
|
}
|
|
}
|
|
|
|
fragments
|
|
}
|
|
|
|
fn is_tip_applicable(&self, current_working_directory: Option<&str>, app: &AppContext) -> bool {
|
|
// Tips about indexing the repo are only applicable if the current directory is not already indexed.
|
|
if matches!(self.kind, AgentTipKind::CodebaseContext) {
|
|
let Some(cwd) = current_working_directory else {
|
|
return true;
|
|
};
|
|
let Some(root) = PersistedWorkspace::as_ref(app).root_for_workspace(Path::new(cwd))
|
|
else {
|
|
return true;
|
|
};
|
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
|
return !LocalProjectIndexManager::as_ref(app).is_ready_for_path(root);
|
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
|
return CodebaseIndexManager::as_ref(app)
|
|
.get_codebase_index_status_for_path(root, app)
|
|
.is_none();
|
|
}
|
|
// Handoff tips only apply when the feature is available and enabled.
|
|
if matches!(self.kind, AgentTipKind::Handoff) {
|
|
return AISettings::as_ref(app).is_cloud_handoff_enabled(app);
|
|
}
|
|
// Tips whose description references a keybinding placeholder should only be shown
|
|
// when the keybinding is actually configured, so we never display the raw
|
|
// "<keybinding>" string to users.
|
|
if self.description.contains("<keybinding>") && self.keystroke(app).is_none() {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
impl WorkspaceAction {
|
|
pub fn display_text(&self) -> Option<String> {
|
|
match self {
|
|
WorkspaceAction::OpenPalette { .. } => Some("Open palette".to_string()),
|
|
WorkspaceAction::OpenGalaxyDrive => Some("Galaxy Drive.".to_string()),
|
|
WorkspaceAction::ToggleRightPanel => Some("Show diff view".to_string()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to build the list of agent tips, including the voice tip if enabled.
|
|
pub fn get_agent_tips(ctx: &AppContext) -> Vec<AgentTip> {
|
|
let mut tips = DEFAULT_TIPS.clone();
|
|
|
|
if cfg!(feature = "voice_input")
|
|
&& UserWorkspaces::as_ref(ctx).is_voice_enabled()
|
|
&& AISettings::as_ref(ctx).is_voice_input_enabled(ctx)
|
|
{
|
|
tips.push(AgentTip {
|
|
description: "Hold <keybinding> to speak your prompt directly to the agent."
|
|
.to_string(),
|
|
link: Some(
|
|
"https://docs.warp.dev/agent-platform/local-agents/interacting-with-agents/voice"
|
|
.to_string(),
|
|
),
|
|
binding_name: Some("FN"),
|
|
action: None,
|
|
kind: AgentTipKind::General,
|
|
});
|
|
}
|
|
|
|
tips
|
|
}
|
|
|
|
/// A model for managing tips with cooldown logic.
|
|
/// Generic over any type implementing the AITip trait.
|
|
pub struct AITipModel<T: AITip> {
|
|
tips: Vec<T>,
|
|
current_tip: Option<T>,
|
|
cooldown_handle: Option<SpawnedFutureHandle>,
|
|
}
|
|
|
|
impl<T: AITip + 'static> AITipModel<T> {
|
|
/// Creates a new AITipModel with the given tips.
|
|
/// Selects a random initial tip from the provided tips.
|
|
///
|
|
/// # Panics
|
|
/// Panics if the tips vector is empty.
|
|
pub fn new(tips: Vec<T>) -> Self {
|
|
use rand::seq::SliceRandom;
|
|
debug_assert!(!tips.is_empty(), "AITipModel must have at least one tip");
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let current_tip = tips.choose(&mut rng).cloned();
|
|
|
|
Self {
|
|
tips,
|
|
current_tip,
|
|
cooldown_handle: None,
|
|
}
|
|
}
|
|
|
|
/// Returns the current tip, if one has been selected.
|
|
pub fn current_tip(&self) -> Option<&T> {
|
|
self.current_tip.as_ref()
|
|
}
|
|
}
|
|
|
|
impl<T: AITip + 'static> Entity for AITipModel<T> {
|
|
type Event = ();
|
|
}
|
|
|
|
// Specific implementation for AgentTip
|
|
impl AITipModel<AgentTip> {
|
|
/// Creates a new AITipModel for AgentTips.
|
|
/// This is the constructor used for the singleton model.
|
|
pub fn new_for_agent_tips(ctx: &AppContext) -> Self {
|
|
let tips = get_agent_tips(ctx);
|
|
// Pick an applicable tip so we never show a raw "<keybinding>" placeholder on first render.
|
|
let current_tip = Self::pick_random_applicable_tip(&tips, None, ctx);
|
|
|
|
Self {
|
|
tips,
|
|
current_tip,
|
|
cooldown_handle: None,
|
|
}
|
|
}
|
|
|
|
/// Rebuilds the tip pool from current settings and invalidates the current tip
|
|
/// if it is no longer applicable. Resets the cooldown timer so the revalidated
|
|
/// tip is shown for the full cooldown period before the next rotation.
|
|
pub fn revalidate_tips(&mut self, ctx: &mut ModelContext<Self>) {
|
|
self.tips = get_agent_tips(ctx);
|
|
|
|
// If the current tip is no longer in the pool or no longer applicable, pick a new one.
|
|
let should_replace = self
|
|
.current_tip
|
|
.as_ref()
|
|
.map(|current_tip| {
|
|
let still_in_pool = self
|
|
.tips
|
|
.iter()
|
|
.any(|tip| tip.description == current_tip.description);
|
|
|
|
!still_in_pool || !current_tip.is_tip_applicable(None, ctx)
|
|
})
|
|
.unwrap_or(true);
|
|
|
|
if should_replace {
|
|
let new_tip = Self::pick_random_applicable_tip(&self.tips, None, ctx);
|
|
if new_tip.is_some() || self.current_tip.is_some() {
|
|
self.current_tip = new_tip;
|
|
self.reset_cooldown(ctx);
|
|
ctx.notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Refreshes the current tip with a new random selection that is applicable
|
|
/// for the given working directory.
|
|
/// Only updates if not in cooldown period (60 seconds).
|
|
pub fn maybe_refresh_tip(
|
|
&mut self,
|
|
current_working_directory: Option<&str>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
// Don't update if cooldown is active
|
|
if self.cooldown_handle.is_some() {
|
|
return;
|
|
}
|
|
|
|
// Rebuild tips from current settings so changes are picked up.
|
|
self.tips = get_agent_tips(ctx);
|
|
|
|
self.current_tip =
|
|
Self::pick_random_applicable_tip(&self.tips, current_working_directory, ctx);
|
|
|
|
// Start 60-second cooldown
|
|
let handle = ctx.spawn(
|
|
async {
|
|
Timer::after(Duration::from_secs(60)).await;
|
|
},
|
|
|me, _, _| {
|
|
me.cooldown_handle = None;
|
|
},
|
|
);
|
|
self.cooldown_handle = Some(handle);
|
|
ctx.notify();
|
|
}
|
|
|
|
/// Picks a random applicable tip from the given pool, filtered by working directory.
|
|
/// Returns `None` if no tips are applicable.
|
|
fn pick_random_applicable_tip(
|
|
tips: &[AgentTip],
|
|
current_working_directory: Option<&str>,
|
|
ctx: &AppContext,
|
|
) -> Option<AgentTip> {
|
|
let available: Vec<&AgentTip> = tips
|
|
.iter()
|
|
.filter(|tip| tip.is_tip_applicable(current_working_directory, ctx))
|
|
.collect();
|
|
let mut rng = rand::thread_rng();
|
|
available.choose(&mut rng).copied().cloned()
|
|
}
|
|
|
|
/// Resets the cooldown timer so the current tip is shown for the full
|
|
/// cooldown period before the next rotation.
|
|
fn reset_cooldown(&mut self, ctx: &mut ModelContext<Self>) {
|
|
if let Some(handle) = self.cooldown_handle.take() {
|
|
handle.abort();
|
|
}
|
|
let handle = ctx.spawn(
|
|
async {
|
|
Timer::after(Duration::from_secs(60)).await;
|
|
},
|
|
|me, _, _| {
|
|
me.cooldown_handle = None;
|
|
},
|
|
);
|
|
self.cooldown_handle = Some(handle);
|
|
}
|
|
}
|
|
|
|
impl SingletonEntity for AITipModel<AgentTip> {}
|
|
|
|
// Specific implementation for CloudModeTip
|
|
impl AITipModel<crate::terminal::view::ambient_agent::CloudModeTip> {
|
|
/// Refreshes the current tip with a new random selection.
|
|
/// Only updates if not in cooldown period (60 seconds).
|
|
pub fn maybe_refresh_tip(&mut self, ctx: &mut ModelContext<Self>) {
|
|
// Don't update if cooldown is active
|
|
if self.cooldown_handle.is_some() {
|
|
return;
|
|
}
|
|
|
|
// Select a random tip
|
|
let mut rng = rand::thread_rng();
|
|
self.current_tip = self.tips.choose(&mut rng).cloned();
|
|
|
|
// Start 60-second cooldown
|
|
let handle = ctx.spawn(
|
|
async {
|
|
Timer::after(Duration::from_secs(60)).await;
|
|
},
|
|
|me, _, _| {
|
|
me.cooldown_handle = None;
|
|
},
|
|
);
|
|
self.cooldown_handle = Some(handle);
|
|
ctx.notify();
|
|
}
|
|
|
|
/// Resets the cooldown timer without changing the current tip.
|
|
/// This ensures the current tip will be shown for the full cooldown period.
|
|
pub fn reset_cooldown(&mut self, ctx: &mut ModelContext<Self>) {
|
|
// Cancel any existing cooldown
|
|
if let Some(handle) = self.cooldown_handle.take() {
|
|
handle.abort();
|
|
}
|
|
|
|
// Start a new 60-second cooldown
|
|
let handle = ctx.spawn(
|
|
async {
|
|
Timer::after(Duration::from_secs(60)).await;
|
|
},
|
|
|me, _, _| {
|
|
me.cooldown_handle = None;
|
|
},
|
|
);
|
|
self.cooldown_handle = Some(handle);
|
|
}
|
|
}
|