Use installed callable apps for TUI mentions (#39051)

## Why

The app directory is a discovery catalog and can include apps that are not
callable in the active thread. Using it for `$` suggestions can therefore
offer mentions that cannot be used.

## What changed

- Load mention candidates from `app/installed`, enrich callable apps through
  bounded `app/read` batches, and retain compatibility with app servers that
  do not support `app/installed`.
- Keep the mention catalog separate from the `/apps` directory cache and scope
  results to the current thread, workspace, and account.
- Refresh mentions after app notifications, enablement changes, configuration
  updates, and apps MCP readiness, while immediately removing disabled apps.

## Testing

Add coverage for batching and fallback behavior, stale scoped results,
notification refreshes, enablement changes, MCP startup races, and filtering
non-callable apps from mentions.

GitOrigin-RevId: 4d9e20794aed30cc97120170792fb555b86fe1e7
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-08-17 19:11:34 +00:00
1 parent d7d526b81d
commit fe5889928c
17 files changed
+759 -116

No files matched your search

+24
View File
@@ -822,6 +822,14 @@ impl App {
self.fetch_connectors_list(app_server, force_refetch);
}
}
AppEvent::FetchInstalledConnectorMentions {
force_refresh,
generation,
} => {
if generation == self.chat_widget.connector_scope_generation() {
self.fetch_installed_connector_mentions(app_server, force_refresh, generation);
}
}
AppEvent::PluginInstallAuthAdvance { refresh_connectors } => {
if refresh_connectors {
self.chat_widget.refresh_connectors(/*force_refetch*/ true);
@@ -1353,6 +1361,20 @@ impl App {
self.chat_widget.on_connectors_loaded(result, is_final);
}
}
AppEvent::InstalledConnectorMentionsLoaded {
thread_id,
cwd,
generation,
result,
} => {
if thread_id == self.current_displayed_thread_id()
&& cwd.as_path() == self.chat_widget.config_ref().cwd.as_path()
&& generation == self.chat_widget.connector_scope_generation()
{
self.chat_widget
.on_connector_mentions_loaded(generation, result);
}
}
AppEvent::UpdateReasoningEffort(effort) => {
self.on_update_reasoning_effort(effort.clone());
self.sync_active_thread_reasoning_setting(app_server, effort)
@@ -2719,6 +2741,8 @@ impl App {
fn refresh_plugin_mentions_after_config_write(&mut self) {
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
self.chat_widget
.refresh_connector_mentions(/*force_refresh*/ true);
}
async fn apply_keymap_clear(&mut self, context: String, action: String) {