Add installable agent lifecycle extensions

This commit is contained in:
Georg Bauer
2026-08-30 16:05:09 +02:00
parent 1ac559bbd0
commit 3977261bd3
10 changed files with 2838 additions and 61 deletions

View File

@@ -240,6 +240,122 @@ impl App {
]
.spacing(10),
);
let mut installed_extensions = column![].spacing(10);
if self.extensions.extensions.is_empty() {
installed_extensions = installed_extensions.push(
text("No agent extensions are installed.")
.size(12)
.color(muted_text()),
);
}
for extension in &self.extensions.extensions {
let id = extension.id.clone();
let update_id = id.clone();
let uninstall_id = id.clone();
let enabled = extension.enabled;
let toggle_control = toggle(enabled)
.label(extension.name.clone())
.on_toggle(move |enabled| Message::ToggleExtension(id.clone(), enabled));
let details = format!(
"{}{}{} skills • hooks: {}",
extension.version,
extension.author,
extension.skill_count,
extension.hook_names(),
);
let source = format!(
"{}{} • commit {}",
extension.source_url,
extension
.requested_ref
.as_ref()
.map_or_else(String::new, |reference| format!(" @ {reference}")),
&extension.resolved_commit[..extension.resolved_commit.len().min(12)],
);
let mut row_content = column![
row![
toggle_control.width(Length::Fill),
action_button("Update").on_press(Message::UpdateExtension(update_id)),
action_button("Uninstall")
.on_press(Message::RequestUninstallExtension(uninstall_id)),
]
.spacing(8)
.align_y(Alignment::Center),
text(&extension.description).size(12),
text(details).size(12).color(muted_text()),
text(source).size(11).color(muted_text()),
]
.spacing(5);
if let Some(error) = &extension.last_error {
row_content = row_content.push(
text(format!("Last hook error: {error}"))
.size(12)
.style(iced::widget::text::danger),
);
}
if self.pending_extension_trust.as_deref() == Some(extension.id.as_str()) {
row_content = row_content.push(
column![
text("Trust this extension's command hooks?").size(13),
text("Hooks run local programs with your user permissions. DS4Server isolates their environment, limits runtime and output, and never invokes a shell, but the installed code can still read or change files you can access.")
.size(12),
row![
action_button("Cancel").on_press(Message::CancelExtensionTrust),
action_button("Trust and enable")
.on_press(Message::ConfirmExtensionTrust),
]
.spacing(8),
]
.spacing(7),
);
}
if self.pending_extension_uninstall.as_deref() == Some(extension.id.as_str()) {
row_content = row_content.push(
column![
text("Remove this extension and its stored session data?").size(13),
row![
action_button("Cancel").on_press(Message::CancelUninstallExtension),
action_button("Uninstall").on_press(Message::ConfirmUninstallExtension),
]
.spacing(8),
]
.spacing(7),
);
}
installed_extensions = installed_extensions
.push(row_content)
.push(rule::horizontal(1));
}
let mut extension_content = column![
text("Install a portable Codex plugin from an HTTPS Git repository. Updates are manual and preserve the selected ref.")
.size(12),
text_input("https://example.com/owner/extension.git", &self.extension_source)
.on_input(Message::PreferenceExtensionSourceChanged)
.padding(9),
row![
text_input("Optional branch, tag, or commit", &self.extension_ref)
.on_input(Message::PreferenceExtensionRefChanged)
.padding(9)
.width(Length::Fill),
action_button("Install").on_press(Message::InstallExtension),
]
.spacing(8)
.align_y(Alignment::Center),
installed_extensions,
]
.spacing(10);
if let Some(label) = self.extension_operation_label() {
extension_content = extension_content.push(text(format!("{label}")).size(12));
}
if let Some(error) = &self.extension_error {
extension_content =
extension_content.push(text(error).size(12).style(iced::widget::text::danger));
}
let extension_group = preference_group(
PreferenceSection::Extensions,
"AGENT EXTENSIONS",
extension_content,
);
let git_group = preference_group(
PreferenceSection::Git,
"GIT DIFFS",
@@ -743,6 +859,7 @@ impl App {
model_group,
endpoint_group,
dev_brain_group,
extension_group,
git_group,
prompt_group,
generation_group,