first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,61 +1,45 @@
|
||||
use crate::features::FeatureFlag;
|
||||
use crate::report_if_error;
|
||||
use crate::settings::{InputSettings, WarpPromptSeparator};
|
||||
use crate::terminal::event::{BlockType, UserBlockCompleted};
|
||||
use crate::terminal::model::session::{ExecuteCommandOptions, Session, SessionsEvent};
|
||||
use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
|
||||
use crate::{
|
||||
debounce::debounce,
|
||||
editor::EditorView,
|
||||
menu::{MenuItem, MenuItemFields},
|
||||
terminal::{
|
||||
model::{
|
||||
block::{Block, BlockMetadata},
|
||||
session::Sessions,
|
||||
},
|
||||
session_settings::{
|
||||
GithubPrPromptChipDefaultValidation, SessionSettings, SessionSettingsChangedEvent,
|
||||
ToolbarChipSelection,
|
||||
},
|
||||
view::{ContextMenuAction, PromptPart, PromptPosition, TerminalAction},
|
||||
},
|
||||
};
|
||||
use futures::{pin_mut, FutureExt as _};
|
||||
use galaxy_completer::completer::{CommandExitStatus, CommandOutput};
|
||||
use galaxy_core::user_preferences::GetUserPreferences;
|
||||
use itertools::Itertools;
|
||||
use settings::Setting as _;
|
||||
|
||||
use super::ChipResult;
|
||||
use super::{
|
||||
chips_to_string,
|
||||
context_chip::{
|
||||
ChipAvailability, ChipDisabledReason, ChipFingerprintInput, ChipRuntimeCapabilities,
|
||||
ContextChip, Environment, ExternalCommandsAvailability, GeneratorContext, PromptGenerator,
|
||||
RefreshConfig, ShellCommandGenerator,
|
||||
},
|
||||
logging::{ChipCommandLogEntry, PromptChipExecutionPhase, PromptChipLogger},
|
||||
prompt::Prompt,
|
||||
ChipValue, ContextChipKind,
|
||||
};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::code_review::git_status_update::{GitRepoStatusEvent, GitRepoStatusModel};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::context_chips::display_chip::GitLineChanges;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxyui::WeakModelHandle;
|
||||
use galaxyui::{
|
||||
r#async::{SpawnedFutureHandle, Timer},
|
||||
AppContext, ViewHandle,
|
||||
};
|
||||
use galaxyui::{Entity, ModelAsRef, ModelContext, ModelHandle, SingletonEntity};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::{Hash as _, Hasher as _};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{pin_mut, FutureExt as _};
|
||||
use itertools::Itertools;
|
||||
use galaxy_completer::completer::CommandExitStatus;
|
||||
use galaxy_core::r#async::debounce;
|
||||
use galaxy_core::user_preferences::GetUserPreferences;
|
||||
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
|
||||
use galaxyui::{
|
||||
AppContext, Entity, ModelAsRef, ModelContext, ModelHandle, SingletonEntity, ViewHandle,
|
||||
WeakModelHandle,
|
||||
};
|
||||
|
||||
use super::context_chip::{
|
||||
ChipAvailability, ChipFingerprintInput, ChipRuntimeCapabilities, ContextChip, Environment,
|
||||
ExternalCommandsAvailability, GeneratorContext, PromptGenerator, RefreshConfig,
|
||||
ShellCommandGenerator,
|
||||
};
|
||||
use super::logging::{ChipCommandLogEntry, PromptChipExecutionPhase, PromptChipLogger};
|
||||
use super::prompt::Prompt;
|
||||
use super::{chips_to_string, ChipResult, ChipValue, ContextChipKind};
|
||||
use crate::code_review::git_repo_model::{GitRepoStatusEvent, GitRepoStatusModel};
|
||||
use crate::code_review::github_repo_model::{GitHubRepoEvent, GitHubRepoModel};
|
||||
use crate::context_chips::display_chip::GitLineChanges;
|
||||
use crate::editor::EditorView;
|
||||
use crate::features::FeatureFlag;
|
||||
use crate::menu::{MenuItem, MenuItemFields};
|
||||
use crate::settings::{InputSettings, WarpPromptSeparator};
|
||||
use crate::terminal::event::{BlockType, UserBlockCompleted};
|
||||
use crate::terminal::model::block::{Block, BlockMetadata};
|
||||
use crate::terminal::model::session::{ExecuteCommandOptions, Session, Sessions, SessionsEvent};
|
||||
use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
|
||||
use crate::terminal::session_settings::{
|
||||
SessionSettings, SessionSettingsChangedEvent, ToolbarChipSelection,
|
||||
};
|
||||
use crate::terminal::view::{ContextMenuAction, PromptPart, PromptPosition, TerminalAction};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "current_prompt_test.rs"]
|
||||
#[path = "current_prompt_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
const PROMPT_DEBOUNCE_PERIOD: Duration = Duration::from_millis(50);
|
||||
@@ -75,13 +59,6 @@ enum ChipUpdateStatus {
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum GithubPrPromptChipCommandOutcome {
|
||||
Validated,
|
||||
DeterministicAuthFailure,
|
||||
RetryableFailure,
|
||||
}
|
||||
|
||||
/// ChipState stores the state and point-in-time information related to a specific chip.
|
||||
/// For example, it's last computed value or a refresh handle.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -179,10 +156,13 @@ pub struct CurrentPrompt {
|
||||
prompt_chip_logger: PromptChipLogger,
|
||||
update_tx: async_channel::Sender<()>,
|
||||
|
||||
/// When set, `ShellGitBranch` chip values are driven by filesystem events from
|
||||
/// `GitRepoStatusModel` instead of the 30s periodic timer.
|
||||
#[cfg(feature = "local_fs")]
|
||||
/// When set, branch, branch status, and diff stats are populated from
|
||||
/// `GitRepoStatusModel` filesystem events.
|
||||
git_repo_status: Option<WeakModelHandle<GitRepoStatusModel>>,
|
||||
|
||||
/// When set, the `GithubPullRequest` chip value is populated from
|
||||
/// `GitHubRepoModel` for the current repository.
|
||||
github_repo_model: Option<WeakModelHandle<GitHubRepoModel>>,
|
||||
}
|
||||
|
||||
/// Context about the current terminal session, needed to update the prompt.
|
||||
@@ -217,7 +197,7 @@ impl CurrentPrompt {
|
||||
&SessionSettings::handle(ctx),
|
||||
Self::handle_session_settings_changed,
|
||||
);
|
||||
ctx.subscribe_to_model(&sessions, |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&sessions, |me, _, event, ctx| {
|
||||
if let SessionsEvent::EnvironmentVariablesUpdated { .. } = event {
|
||||
me.update_states_with_new_context(ctx);
|
||||
}
|
||||
@@ -252,8 +232,8 @@ impl CurrentPrompt {
|
||||
update_tx,
|
||||
same_line_prompt_enabled: prompt.as_ref(ctx).same_line_prompt_enabled(),
|
||||
separator: prompt.as_ref(ctx).separator(),
|
||||
#[cfg(feature = "local_fs")]
|
||||
git_repo_status: None,
|
||||
github_repo_model: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +246,7 @@ impl CurrentPrompt {
|
||||
) {
|
||||
// A WeakViewHandle is used here to avoid leaking the terminal model
|
||||
let weak_editor_handle = editor.downgrade();
|
||||
ctx.subscribe_to_view(&editor, move |me, _, ctx| {
|
||||
ctx.subscribe_to_view(&editor, move |me, _, _, ctx| {
|
||||
// CurrentPrompt exists and this fn is called even if we're not using warp prompt.
|
||||
// We don't need to do anything if we're honoring PS1 unless universal developer input
|
||||
// or AgentView is enabled (agent view needs chips regardless of PS1 setting).
|
||||
@@ -518,9 +498,21 @@ impl CurrentPrompt {
|
||||
return false;
|
||||
};
|
||||
|
||||
// A retryable failure (`Error`, `TimedOut`) is not a usable cached
|
||||
// result: `last_fingerprint` is recorded before the command runs, and
|
||||
// such failures intentionally do not populate `last_failure_fingerprint`.
|
||||
// Without this guard, the next periodic tick would treat the failed
|
||||
// attempt as a cache hit and never retry. Deterministic failures
|
||||
// continue to be suppressed via `last_failure_fingerprint`.
|
||||
let should_skip = self
|
||||
.states
|
||||
.get(chip_kind)
|
||||
.filter(|state| {
|
||||
!matches!(
|
||||
state.update_status,
|
||||
ChipUpdateStatus::Error | ChipUpdateStatus::TimedOut
|
||||
)
|
||||
})
|
||||
.and_then(|state| state.last_fingerprint.as_ref())
|
||||
.is_some_and(|existing| existing == &new_fingerprint);
|
||||
|
||||
@@ -619,26 +611,7 @@ impl CurrentPrompt {
|
||||
&self,
|
||||
values_opt: Option<Vec<String>>,
|
||||
) -> Option<Vec<String>> {
|
||||
values_opt.map(|values| {
|
||||
let mut trimmed: Vec<String> = values
|
||||
.into_iter()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
// We want to sort the branches so the current branch is first (denoted by *).
|
||||
// The rest of the branches maintain their relative order.
|
||||
trimmed.sort_by(|a, b| {
|
||||
let a_starts_with_star = a.starts_with('*');
|
||||
let b_starts_with_star = b.starts_with('*');
|
||||
b_starts_with_star.cmp(&a_starts_with_star)
|
||||
});
|
||||
|
||||
trimmed
|
||||
.into_iter()
|
||||
.map(|s| s.trim_start_matches('*').trim().to_string())
|
||||
.collect()
|
||||
})
|
||||
super::git_branch_on_click::filter_git_branch_on_click_values(values_opt)
|
||||
}
|
||||
|
||||
/// Perform a single update of the given chip.
|
||||
@@ -691,19 +664,6 @@ impl CurrentPrompt {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
// If the GithubPullRequest chip is disabled because `gh` is missing,
|
||||
// transition validation state to Suppressed so future default
|
||||
// resolution excludes it.
|
||||
if matches!(chip_kind, ContextChipKind::GithubPullRequest) {
|
||||
if let ChipAvailability::Disabled(ChipDisabledReason::RequiresExecutable {
|
||||
ref command,
|
||||
}) = availability
|
||||
{
|
||||
if command == "gh" {
|
||||
Self::maybe_suppress_github_pr_default(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.update_chip_value(chip_kind, None);
|
||||
self.update_on_click_value(chip_kind, None);
|
||||
self.set_chip_update_status(chip_kind, ChipUpdateStatus::Disabled);
|
||||
@@ -729,7 +689,6 @@ impl CurrentPrompt {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match generator {
|
||||
PromptGenerator::ShellCommand(cmd) => {
|
||||
let Some(exec_ctx) = self.prepare_shell_command_context(cmd, ctx) else {
|
||||
@@ -769,7 +728,7 @@ impl CurrentPrompt {
|
||||
.await;
|
||||
(value, timed_out, chip_kind, exec_ctx, chip_title)
|
||||
},
|
||||
move |me, (value, timed_out, chip_kind, exec_ctx, chip_title), ctx| {
|
||||
move |me, (value, timed_out, chip_kind, exec_ctx, chip_title), _| {
|
||||
logger.log_shell_command(&ChipCommandLogEntry {
|
||||
chip_kind: &chip_kind,
|
||||
chip_title: &chip_title,
|
||||
@@ -780,24 +739,22 @@ impl CurrentPrompt {
|
||||
output: value.as_ref(),
|
||||
timed_out,
|
||||
});
|
||||
// GitDiffStats has two value sources that can race when entering a repo:
|
||||
// this shell fallback (`git diff --shortstat HEAD`, tracked changes only)
|
||||
// and a repo-status watcher that also counts untracked files. If the
|
||||
// watcher attached while this fallback was in flight, drop the fallback's
|
||||
// result
|
||||
if matches!(chip_kind, ContextChipKind::GitDiffStats)
|
||||
&& me.is_updated_externally(&chip_kind)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if timed_out {
|
||||
if suppress_on_failure
|
||||
&& Self::should_cache_failure_fingerprint(
|
||||
&chip_kind,
|
||||
value.as_ref(),
|
||||
timed_out,
|
||||
)
|
||||
{
|
||||
if suppress_on_failure {
|
||||
if let Some(state) = me.states.get_mut(&chip_kind) {
|
||||
state.last_failure_fingerprint = current_fingerprint;
|
||||
}
|
||||
} else if suppress_on_failure {
|
||||
if let Some(state) = me.states.get_mut(&chip_kind) {
|
||||
if state.last_failure_fingerprint == current_fingerprint {
|
||||
state.last_failure_fingerprint = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
me.update_chip_value(&chip_kind, None);
|
||||
me.set_chip_update_status(&chip_kind, ChipUpdateStatus::TimedOut);
|
||||
@@ -821,29 +778,7 @@ impl CurrentPrompt {
|
||||
_ => (None, ChipUpdateStatus::Error, true),
|
||||
};
|
||||
|
||||
if matches!(chip_kind, ContextChipKind::GithubPullRequest) {
|
||||
match Self::github_pr_prompt_chip_command_outcome(
|
||||
value.as_ref(),
|
||||
timed_out,
|
||||
) {
|
||||
GithubPrPromptChipCommandOutcome::Validated => {
|
||||
Self::maybe_validate_github_pr_default(ctx);
|
||||
}
|
||||
GithubPrPromptChipCommandOutcome::DeterministicAuthFailure => {
|
||||
Self::maybe_suppress_github_pr_default(ctx);
|
||||
}
|
||||
GithubPrPromptChipCommandOutcome::RetryableFailure => {}
|
||||
}
|
||||
}
|
||||
|
||||
if suppress_on_failure
|
||||
&& failed
|
||||
&& Self::should_cache_failure_fingerprint(
|
||||
&chip_kind,
|
||||
value.as_ref(),
|
||||
timed_out,
|
||||
)
|
||||
{
|
||||
if suppress_on_failure && failed {
|
||||
if let Some(state) = me.states.get_mut(&chip_kind) {
|
||||
state.last_failure_fingerprint = current_fingerprint;
|
||||
}
|
||||
@@ -854,7 +789,8 @@ impl CurrentPrompt {
|
||||
}
|
||||
}
|
||||
}
|
||||
me.update_chip_value(&chip_kind, output.map(ChipValue::Text));
|
||||
let chip_value = output.map(ChipValue::Text);
|
||||
me.update_chip_value(&chip_kind, chip_value);
|
||||
me.set_chip_update_status(&chip_kind, status);
|
||||
},
|
||||
);
|
||||
@@ -1001,7 +937,7 @@ impl CurrentPrompt {
|
||||
chip_kind_clone
|
||||
},
|
||||
|me, chip_kind, ctx| {
|
||||
me.fetch_chip_value_at_interval(&chip_kind, None, None, false, ctx);
|
||||
me.fetch_chip_value_at_interval(&chip_kind, None, None, true, ctx);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1028,6 +964,43 @@ impl CurrentPrompt {
|
||||
let state = ChipState::new(chip_kind);
|
||||
self.states.insert(chip_kind.clone(), state);
|
||||
}
|
||||
if self.is_updated_externally(chip_kind) {
|
||||
// For chips updated externally (e.g. by the per-repo git status
|
||||
// filesystem watcher), avoid running the shell-based fallback
|
||||
// generator. Doing so can briefly overwrite the structured
|
||||
// watcher value with one that uses different semantics (for
|
||||
// example, the `GitDiffStats` shell fallback runs `git diff
|
||||
// --shortstat HEAD`, which excludes untracked files, whereas
|
||||
// the watcher counts untracked files as changes), causing the
|
||||
// chip to flicker between the tracked-only count and the
|
||||
// all-files count when untracked files are present.
|
||||
//
|
||||
// If a chip provides an `initial_value_generator` that sources
|
||||
// from the prompt context (rather than running a shell
|
||||
// command), use it for a fast initial value until the watcher
|
||||
// emits a metadata-changed event.
|
||||
if let Some(initial_gen) = chip_kind.initial_value_generator() {
|
||||
self.fetch_chip_value_once(
|
||||
chip_kind,
|
||||
&initial_gen,
|
||||
chip.on_click_generator().cloned(),
|
||||
true,
|
||||
ctx,
|
||||
);
|
||||
} else {
|
||||
// Externally-updated chips without an `initial_value_generator`
|
||||
// are left blank after a state rebuild (`states.clear()` in
|
||||
// `handle_prompt_changed`, or `clear_cache()` on a session
|
||||
// change) until their backing model emits a change event.
|
||||
// `GithubPullRequest` only emits when cached PR info actually
|
||||
// changes, so after a rebuild there may be no event to restore
|
||||
// the already-cached value until the next periodic refresh.
|
||||
if matches!(chip_kind, ContextChipKind::GithubPullRequest) {
|
||||
self.sync_pr_chip_from_model(ctx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match chip.refresh_config() {
|
||||
RefreshConfig::OnDemandOnly => {
|
||||
@@ -1040,25 +1013,13 @@ impl CurrentPrompt {
|
||||
);
|
||||
}
|
||||
RefreshConfig::Periodically { .. } => {
|
||||
if self.is_updated_externally(chip_kind) {
|
||||
let initial_gen = chip_kind.initial_value_generator();
|
||||
let generator = initial_gen.as_ref().unwrap_or(chip.generator());
|
||||
self.fetch_chip_value_once(
|
||||
chip_kind,
|
||||
generator,
|
||||
chip.on_click_generator().cloned(),
|
||||
true,
|
||||
ctx,
|
||||
);
|
||||
} else {
|
||||
self.fetch_chip_value_at_interval(
|
||||
chip_kind,
|
||||
chip_kind.initial_value_generator(),
|
||||
chip.on_click_generator().cloned(),
|
||||
true,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
self.fetch_chip_value_at_interval(
|
||||
chip_kind,
|
||||
chip_kind.initial_value_generator(),
|
||||
chip.on_click_generator().cloned(),
|
||||
true,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
RefreshConfig::OnFileChanges { filepath } => {
|
||||
log::debug!("Unimplemented: would've watched changes to filepath: {filepath}");
|
||||
@@ -1133,8 +1094,6 @@ impl CurrentPrompt {
|
||||
/// existing states map with new information.
|
||||
/// This is called when the context gets updated (ie. a new block metadata is received).
|
||||
fn update_states_with_new_context_and_session(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
self.maybe_unsuppress_github_pr_default(ctx);
|
||||
|
||||
// 1. Terminating existing spawned operations.
|
||||
self.clear_chips_and_cache();
|
||||
|
||||
@@ -1148,6 +1107,7 @@ impl CurrentPrompt {
|
||||
/// spot, and changing Prompt configuration most likely doesn't mean updating the context.
|
||||
fn handle_prompt_changed(
|
||||
&mut self,
|
||||
_: ModelHandle<Prompt>,
|
||||
_prompt_event: &<Prompt as Entity>::Event,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -1164,6 +1124,7 @@ impl CurrentPrompt {
|
||||
|
||||
fn handle_session_settings_changed(
|
||||
&mut self,
|
||||
_: ModelHandle<SessionSettings>,
|
||||
event: &SessionSettingsChangedEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -1192,6 +1153,12 @@ impl CurrentPrompt {
|
||||
// Recompute which chips to run when the agent footer config changes.
|
||||
self.update_states_with_new_context(ctx);
|
||||
}
|
||||
if let SessionSettingsChangedEvent::GithubPrChipDefaultValidation { .. } = event {
|
||||
// Re-resolve the default prompt's chip list (which gates the
|
||||
// PR chip on `is_suppressed()`) and re-run chips with the new
|
||||
// suppression state.
|
||||
self.update_states_with_new_context(ctx);
|
||||
}
|
||||
|
||||
if let SessionSettingsChangedEvent::CLIAgentToolbarChipSelectionSetting { .. } = event {
|
||||
self.update_states_with_new_context(ctx);
|
||||
@@ -1220,7 +1187,6 @@ impl CurrentPrompt {
|
||||
ctx: &mut galaxyui::AppContext,
|
||||
) -> futures_util::future::BoxFuture<'static, ()> {
|
||||
use futures_util::FutureExt;
|
||||
use itertools::Itertools;
|
||||
// This structure prevents the returned Future from referencing self.
|
||||
let chip_futures = self
|
||||
.states
|
||||
@@ -1258,7 +1224,12 @@ impl CurrentPrompt {
|
||||
.any(|handle| !handle.abort_handle().is_aborted())
|
||||
}
|
||||
|
||||
fn handle_model_event(&mut self, event: &ModelEvent, ctx: &mut ModelContext<Self>) {
|
||||
fn handle_model_event(
|
||||
&mut self,
|
||||
_: ModelHandle<ModelEventDispatcher>,
|
||||
event: &ModelEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let ModelEvent::AfterBlockCompleted(after_block_completed) = event {
|
||||
if let BlockType::User(UserBlockCompleted { command, .. }) =
|
||||
&after_block_completed.block_type
|
||||
@@ -1407,9 +1378,9 @@ impl CurrentPrompt {
|
||||
}
|
||||
|
||||
/// Set the per-repo git status model handle. When `Some`, subscribes to
|
||||
/// metadata-changed events so `ShellGitBranch` and `GitDiffStats` are updated
|
||||
/// by filesystem events instead of the 30s periodic timer.
|
||||
#[cfg(feature = "local_fs")]
|
||||
/// metadata events so git-backed prompt chips are updated from the
|
||||
/// per-repo status model. PR info is handled separately by
|
||||
/// [`Self::set_github_repo_model`].
|
||||
pub fn set_git_repo_status(
|
||||
&mut self,
|
||||
handle: Option<WeakModelHandle<GitRepoStatusModel>>,
|
||||
@@ -1422,169 +1393,162 @@ impl CurrentPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
// Repo detached, clear git chips that require repository metadata.
|
||||
if handle.is_none() {
|
||||
for chip_kind in [
|
||||
ContextChipKind::GitDiffStats,
|
||||
ContextChipKind::GitBranchStatus,
|
||||
] {
|
||||
if let Some(state) = self.states.get_mut(&chip_kind) {
|
||||
state.clear_abort_handlers();
|
||||
state.clear_cache();
|
||||
}
|
||||
}
|
||||
let _ = self.update_tx.try_send(());
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(weak) = handle {
|
||||
if let Some(strong) = weak.upgrade(ctx) {
|
||||
self.git_repo_status = Some(weak);
|
||||
ctx.subscribe_to_model(&strong, |me, event, ctx| match event {
|
||||
ctx.subscribe_to_model(&strong, |me, _, event, ctx| match event {
|
||||
GitRepoStatusEvent::MetadataChanged => {
|
||||
let metadata = me
|
||||
.git_repo_status
|
||||
.as_ref()
|
||||
.and_then(|w| w.upgrade(ctx))
|
||||
.and_then(|h| h.as_ref(ctx).metadata().cloned());
|
||||
|
||||
let Some(metadata) = metadata else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Update ShellGitBranch.
|
||||
let new_branch = ChipValue::Text(metadata.current_branch_name.clone());
|
||||
let current_branch = me
|
||||
.latest_chip_value(&ContextChipKind::ShellGitBranch)
|
||||
.cloned();
|
||||
if current_branch.as_ref() != Some(&new_branch) {
|
||||
me.update_chip_value(
|
||||
&ContextChipKind::ShellGitBranch,
|
||||
Some(new_branch),
|
||||
);
|
||||
// Refresh the branch dropdown so it stays in sync.
|
||||
let chip_kind = ContextChipKind::ShellGitBranch;
|
||||
if let Some(chip) = chip_kind.to_chip() {
|
||||
if let Some(on_click_gen) = chip.on_click_generator().cloned() {
|
||||
me.refresh_on_click_values(&chip_kind, on_click_gen, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update GitDiffStats with structured data directly.
|
||||
let new_diff_stats = ChipValue::GitDiffStats(
|
||||
GitLineChanges::from_diff_stats(&metadata.stats_against_head),
|
||||
);
|
||||
let current_diff_stats = me
|
||||
.latest_chip_value(&ContextChipKind::GitDiffStats)
|
||||
.cloned();
|
||||
if current_diff_stats.as_ref() != Some(&new_diff_stats) {
|
||||
me.update_chip_value(
|
||||
&ContextChipKind::GitDiffStats,
|
||||
Some(new_diff_stats),
|
||||
);
|
||||
}
|
||||
me.apply_git_repo_metadata(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
// Eagerly populate chips if metadata is already available (the
|
||||
// initial `refresh_metadata` in `GitRepoStatusModel::new` may
|
||||
// have completed before we subscribed). If it hasn't finished
|
||||
// yet, the subscription above will catch the `MetadataChanged`
|
||||
// event when it does.
|
||||
if strong.as_ref(ctx).metadata(ctx).is_some() {
|
||||
self.apply_git_repo_metadata(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the per-repo GitHub-info model handle. When `Some`, subscribes to
|
||||
/// its events so the `GithubPullRequest` chip value is updated.
|
||||
pub fn set_github_repo_model(
|
||||
&mut self,
|
||||
handle: Option<WeakModelHandle<GitHubRepoModel>>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Unsubscribe from the previous model, if any.
|
||||
if let Some(old_weak) = self.github_repo_model.take() {
|
||||
if let Some(old_strong) = old_weak.upgrade(ctx) {
|
||||
ctx.unsubscribe_from_model(&old_strong);
|
||||
}
|
||||
}
|
||||
|
||||
if handle.is_none() {
|
||||
// GitHub-info handle detached: clear any stale PR chip state.
|
||||
if let Some(state) = self.states.get_mut(&ContextChipKind::GithubPullRequest) {
|
||||
state.clear_abort_handlers();
|
||||
state.clear_cache();
|
||||
}
|
||||
let _ = self.update_tx.try_send(());
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(weak) = handle {
|
||||
if let Some(strong) = weak.upgrade(ctx) {
|
||||
self.github_repo_model = Some(weak);
|
||||
// Only PR info drives the chip value; repository name/owner
|
||||
// changes don't affect it.
|
||||
ctx.subscribe_to_model(&strong, |me, _, event, ctx| match event {
|
||||
GitHubRepoEvent::PrInfoChanged => {
|
||||
me.sync_pr_chip_from_model(ctx);
|
||||
}
|
||||
GitHubRepoEvent::RepositoryInfoChanged => {}
|
||||
});
|
||||
|
||||
// Eagerly populate the PR chip if PR info has already landed.
|
||||
self.sync_pr_chip_from_model(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the current `GitRepoStatusModel` metadata and push it into the
|
||||
/// git-backed chip states.
|
||||
fn apply_git_repo_metadata(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let metadata = self
|
||||
.git_repo_status
|
||||
.as_ref()
|
||||
.and_then(|w| w.upgrade(ctx))
|
||||
.and_then(|h| h.as_ref(ctx).metadata(ctx).cloned());
|
||||
|
||||
let Some(metadata) = metadata else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Update ShellGitBranch.
|
||||
let new_branch = ChipValue::Text(metadata.current_branch_name.clone());
|
||||
let current_branch = self
|
||||
.latest_chip_value(&ContextChipKind::ShellGitBranch)
|
||||
.cloned();
|
||||
if current_branch.as_ref() != Some(&new_branch) {
|
||||
self.update_chip_value(&ContextChipKind::ShellGitBranch, Some(new_branch));
|
||||
// Refresh the branch dropdown so it stays in sync.
|
||||
let chip_kind = ContextChipKind::ShellGitBranch;
|
||||
if let Some(chip) = chip_kind.to_chip() {
|
||||
if let Some(on_click_gen) = chip.on_click_generator().cloned() {
|
||||
self.refresh_on_click_values(&chip_kind, on_click_gen, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let new_branch_status = ChipValue::GitBranchStatus(metadata.branch_tracking_status.clone());
|
||||
let current_branch_status = self
|
||||
.latest_chip_value(&ContextChipKind::GitBranchStatus)
|
||||
.cloned();
|
||||
if current_branch_status.as_ref() != Some(&new_branch_status) {
|
||||
self.update_chip_value(&ContextChipKind::GitBranchStatus, Some(new_branch_status));
|
||||
}
|
||||
|
||||
// Update GitDiffStats with structured data directly.
|
||||
let new_diff_stats = ChipValue::GitDiffStats(GitLineChanges::from_diff_stats(
|
||||
&metadata.stats_against_head,
|
||||
));
|
||||
let current_diff_stats = self
|
||||
.latest_chip_value(&ContextChipKind::GitDiffStats)
|
||||
.cloned();
|
||||
if current_diff_stats.as_ref() != Some(&new_diff_stats) {
|
||||
self.update_chip_value(&ContextChipKind::GitDiffStats, Some(new_diff_stats));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads PR info from the per-repo `GitHubRepoModel` and updates the
|
||||
/// `GithubPullRequest` chip value if it differs from the current one.
|
||||
fn sync_pr_chip_from_model(&mut self, ctx: &AppContext) {
|
||||
let new_pr_value = self
|
||||
.github_repo_model
|
||||
.as_ref()
|
||||
.and_then(|w| w.upgrade(ctx))
|
||||
.and_then(|h| {
|
||||
h.as_ref(ctx)
|
||||
.pr_info(ctx)
|
||||
.map(|info| ChipValue::Text(info.url.clone()))
|
||||
});
|
||||
let current_pr = self
|
||||
.latest_chip_value(&ContextChipKind::GithubPullRequest)
|
||||
.cloned();
|
||||
if current_pr != new_pr_value {
|
||||
self.update_chip_value(&ContextChipKind::GithubPullRequest, new_pr_value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when the given chip's value is updated externally
|
||||
/// (e.g. by a filesystem watcher) and the periodic timer should be skipped.
|
||||
fn is_updated_externally(&self, chip_kind: &ContextChipKind) -> bool {
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
if matches!(
|
||||
chip_kind,
|
||||
ContextChipKind::ShellGitBranch | ContextChipKind::GitDiffStats
|
||||
) {
|
||||
return self.git_repo_status.is_some();
|
||||
}
|
||||
}
|
||||
let _ = chip_kind;
|
||||
false
|
||||
}
|
||||
|
||||
/// Heuristic check for `gh` CLI authentication errors in stderr output.
|
||||
fn is_gh_auth_error(stderr: &str) -> bool {
|
||||
let lower = stderr.to_lowercase();
|
||||
lower.contains("not logged in")
|
||||
|| lower.contains("authentication required")
|
||||
|| lower.contains("gh auth login")
|
||||
}
|
||||
|
||||
fn github_pr_prompt_chip_command_outcome(
|
||||
output: Option<&CommandOutput>,
|
||||
timed_out: bool,
|
||||
) -> GithubPrPromptChipCommandOutcome {
|
||||
if timed_out {
|
||||
return GithubPrPromptChipCommandOutcome::RetryableFailure;
|
||||
}
|
||||
|
||||
match output {
|
||||
Some(command_output) if command_output.status == CommandExitStatus::Success => {
|
||||
GithubPrPromptChipCommandOutcome::Validated
|
||||
}
|
||||
Some(command_output) => {
|
||||
let stderr = String::from_utf8(command_output.stderr.clone()).unwrap_or_default();
|
||||
if Self::is_gh_auth_error(&stderr) {
|
||||
GithubPrPromptChipCommandOutcome::DeterministicAuthFailure
|
||||
} else {
|
||||
GithubPrPromptChipCommandOutcome::RetryableFailure
|
||||
}
|
||||
}
|
||||
None => GithubPrPromptChipCommandOutcome::RetryableFailure,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_cache_failure_fingerprint(
|
||||
chip_kind: &ContextChipKind,
|
||||
output: Option<&CommandOutput>,
|
||||
timed_out: bool,
|
||||
) -> bool {
|
||||
if !matches!(chip_kind, ContextChipKind::GithubPullRequest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
Self::github_pr_prompt_chip_command_outcome(output, timed_out),
|
||||
GithubPrPromptChipCommandOutcome::DeterministicAuthFailure
|
||||
)
|
||||
}
|
||||
|
||||
fn maybe_suppress_github_pr_default(ctx: &mut ModelContext<Self>) {
|
||||
let current = *SessionSettings::as_ref(ctx).github_pr_chip_default_validation;
|
||||
if current != GithubPrPromptChipDefaultValidation::Suppressed {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings
|
||||
.github_pr_chip_default_validation
|
||||
.set_value(GithubPrPromptChipDefaultValidation::Suppressed, ctx));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// On session changes (including app startup), re-check whether a previously
|
||||
/// suppressed PR chip should get another chance. Suppression is sticky across
|
||||
/// restarts, but if the user has since installed `gh`, resetting to Unvalidated
|
||||
/// lets the normal chip execution path re-validate or re-suppress.
|
||||
fn maybe_unsuppress_github_pr_default(&self, ctx: &mut ModelContext<Self>) {
|
||||
if !SessionSettings::as_ref(ctx)
|
||||
.github_pr_chip_default_validation
|
||||
.is_suppressed()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let gh_on_path = self
|
||||
.with_current_generator_context(ctx, |generator_context| {
|
||||
generator_context.active_session.is_some_and(|session| {
|
||||
session.has_loaded_external_commands()
|
||||
&& session.executable_names().any(|name| name == "gh")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if gh_on_path {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings
|
||||
.github_pr_chip_default_validation
|
||||
.set_value(GithubPrPromptChipDefaultValidation::Unvalidated, ctx));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_validate_github_pr_default(ctx: &mut ModelContext<Self>) {
|
||||
let current = *SessionSettings::as_ref(ctx).github_pr_chip_default_validation;
|
||||
if current == GithubPrPromptChipDefaultValidation::Unvalidated {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings
|
||||
.github_pr_chip_default_validation
|
||||
.set_value(GithubPrPromptChipDefaultValidation::Validated, ctx));
|
||||
});
|
||||
match chip_kind {
|
||||
ContextChipKind::ShellGitBranch
|
||||
| ContextChipKind::GitBranchStatus
|
||||
| ContextChipKind::GitDiffStats => self.git_repo_status.is_some(),
|
||||
ContextChipKind::GithubPullRequest => self.github_repo_model.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user