host: the palette commands of site tabs are the host's (tab_commands, tab_run_command)

tab_commands keeps the list each site tab reported (validated by plugins::commands, accepted only
from a tab on the site's origin), clears it when a new document starts, tells the shell, and checks
the shell-only tab_run_command: only a command that tab reported, only while it is on the site.
tab_run_command joins commands::SHELL (guard checked by assert_host_guards); the app's tab_report
hands its `commands` to tab_commands::reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CRTkafa 2026-10-01 14:22:00 +07:00
parent 6b37744226
commit d40bf273bd
4 changed files with 84 additions and 0 deletions

View file

@ -940,6 +940,14 @@ async fn features_set(webview: Webview, app: AppHandle, features: serde_json::Ma
set_features(&app, features)
}
/// Runs a plugin's palette command in tab `id`: only a command that tab reported, only while it is
/// on the site. The command's code runs in the page; the tab gets keyboard focus for what it opens.
#[tauri::command]
async fn tab_run_command(webview: Webview, app: AppHandle, id: String, command: String) -> Result<(), String> {
shell_only(&webview)?;
crate::tab_commands::run(&app, &id, &command)
}
/// The onboarding mode: what was chosen, whether the first-run screen was answered, and whether
/// this app was set up before modes existed.
#[tauri::command]
@ -1126,6 +1134,7 @@ fn tab_builder(app: &AppHandle, hooks: &Arc<dyn SiteHooks>, tab: &NewTab, url: U
}
let finished = matches!(payload.event(), PageLoadEvent::Finished);
if !finished {
crate::tab_commands::cleared(app, tab.label());
load_hooks.page_started(app, &tab);
// An engine that resets page zoom on navigation still shows this tab's.
host::restore_tab_zoom(app, &tab);
@ -1239,6 +1248,7 @@ fn host_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 's
mode_set,
settings_get,
settings_set,
tab_run_command,
tab_shortcut,
tab_link_drag,
open_panel

View file

@ -38,6 +38,7 @@ pub const SHELL: &[&str] = &[
"mode_set",
"settings_get",
"settings_set",
"tab_run_command",
];
/// Tab-only: any tab webview, speaking only about itself.

View file

@ -93,6 +93,8 @@ mod dev_commands;
#[cfg(feature = "app")]
pub mod theme_commands;
#[cfg(feature = "app")]
pub mod tab_commands;
#[cfg(feature = "app")]
pub mod alerts_commands;
#[cfg(feature = "app")]
mod backup_commands;

View file

@ -0,0 +1,71 @@
//! The plugin commands in the shell's command palette, native side, for every app: the list each
//! site tab last reported ([`reported`]; validated by [`crate::plugins::commands`]), kept per tab,
//! forwarded to the shell, and the check behind the shell-only `tab_run_command` ([`run`]: only a
//! command that tab reported, only while it is on the site; the command's code runs in the page).
use crate::app::config;
use crate::host::{emit_shell, tab_webview};
use crate::plugins::commands::{self as palette, PaletteCommand};
use serde::Serialize;
use std::collections::HashMap;
use std::sync::{Mutex, MutexGuard, OnceLock};
use tauri::{AppHandle, Webview};
fn lists() -> MutexGuard<'static, HashMap<String, Vec<PaletteCommand>>> {
static LISTS: OnceLock<Mutex<HashMap<String, Vec<PaletteCommand>>>> = OnceLock::new();
LISTS.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
/// Stores what tab `label` has (empty = none) and tells the shell when it changed.
fn store(app: &AppHandle, label: &str, list: Vec<PaletteCommand>) {
let Some(id) = crate::policy::tab_id(label) else { return };
let changed = {
let mut lists = lists();
let changed = lists.get(label).map(Vec::as_slice).unwrap_or(&[]) != list.as_slice();
if list.is_empty() {
lists.remove(label);
} else {
lists.insert(label.to_owned(), list.clone());
}
changed
};
if changed {
#[derive(Clone, Serialize)]
struct Commands {
id: String,
commands: Vec<PaletteCommand>,
}
emit_shell(app, palette::EVENT, Commands { id: id.to_owned(), commands: list });
}
}
/// A new document started in tab `label`: its plugins register their commands again.
pub(crate) fn cleared(app: &AppHandle, label: &str) {
store(app, label, Vec::new());
}
/// A tab reported its commands. Accepted only from a tab that is on the site right now; the list
/// is validated as a whole (any bad entry rejects the report).
pub fn reported(app: &AppHandle, webview: &Webview, list: Vec<PaletteCommand>) -> Result<(), String> {
if !webview.url().is_ok_and(|url| config(app).site.is_origin(&url)) {
return Ok(());
}
store(app, webview.label(), palette::validate(list)?);
Ok(())
}
/// Runs command `command` in tab `id` (the caller checked it is the shell).
pub(crate) fn run(app: &AppHandle, id: &str, command: &str) -> Result<(), String> {
let label = crate::policy::tab_label(id);
if !lists().get(&label).is_some_and(|list| list.iter().any(|item| item.id == command)) {
return Err("VALIDATION: that tab has no such command".into());
}
let site = config(app).site;
let tab = tab_webview(app, id).ok_or("VALIDATION: no such tab")?;
if !tab.url().is_ok_and(|url| site.is_origin(&url)) {
return Err("VALIDATION: the tab is not on the site".into());
}
let script = palette::run_script(site, command)?;
let _ = tab.set_focus();
tab.eval(&script).map_err(|error| format!("TABS: {error}"))
}