feat: plugins v1.5

This commit is contained in:
2026-04-29 01:14:30 +02:00
parent 6920f93b41
commit eabbc08896
59 changed files with 2197 additions and 352 deletions

View File

@@ -5,6 +5,7 @@ import {
Output,
computed,
inject,
input,
signal
} from '@angular/core';
import { Router } from '@angular/router';
@@ -21,13 +22,14 @@ import {
lucideStore,
lucideX
} from '@ng-icons/lucide';
import type { PluginCapabilityId } from '../../../../shared-kernel';
import type { PluginCapabilityId, TojuPluginInstallScope } from '../../../../shared-kernel';
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
import { PluginHostService } from '../../application/services/plugin-host.service';
import { PluginLoggerService } from '../../application/services/plugin-logger.service';
import { PluginRegistryService } from '../../application/services/plugin-registry.service';
import { PluginRequirementStateService } from '../../application/services/plugin-requirement-state.service';
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
import { getPluginInstallScope } from '../../domain/logic/plugin-install-scope.logic';
import type { RegisteredPlugin } from '../../domain/models/plugin-runtime.models';
import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component';
@@ -60,6 +62,8 @@ type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirem
export class PluginManagerComponent {
@Output() readonly closed = new EventEmitter<void>();
readonly scope = input<TojuPluginInstallScope>('client');
readonly capabilities = inject(PluginCapabilityService);
readonly host = inject(PluginHostService);
readonly logger = inject(PluginLoggerService);
@@ -71,7 +75,12 @@ export class PluginManagerComponent {
readonly busyPluginId = signal<string | null>(null);
readonly busyAll = signal(false);
readonly selectedPluginId = signal<string | null>(null);
readonly entries = this.registry.entries;
readonly allEntries = this.registry.entries;
readonly entries = computed(() => this.allEntries().filter((entry) => this.entryBelongsToScope(entry)));
readonly managerTitle = computed(() => this.scope() === 'server' ? 'Server plugins' : 'Client plugins');
readonly managerDescription = computed(() => this.scope() === 'server'
? 'Plugins installed for the current chat server.'
: 'Global client plugins installed on this device.');
readonly selectedPlugin = computed(() => {
const selectedPluginId = this.selectedPluginId();
@@ -89,17 +98,18 @@ export class PluginManagerComponent {
.slice(-20) : [];
});
readonly extensionCounts = computed(() => ({
appPages: this.uiRegistry.appPages().length,
channelSections: this.uiRegistry.channelSections().length,
composerActions: this.uiRegistry.composerActions().length,
embeds: this.uiRegistry.embeds().length,
profileActions: this.uiRegistry.profileActions().length,
settingsPages: this.uiRegistry.settingsPages().length,
sidePanels: this.uiRegistry.sidePanels().length,
toolbarActions: this.uiRegistry.toolbarActions().length
appPages: this.uiRegistry.appPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
channelSections: this.uiRegistry.channelSectionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
composerActions: this.uiRegistry.composerActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
embeds: this.uiRegistry.embedRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
profileActions: this.uiRegistry.profileActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
settingsPages: this.uiRegistry.settingsPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
sidePanels: this.uiRegistry.sidePanelRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length,
toolbarActions: this.uiRegistry.toolbarActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length
}));
readonly requirementComparisons = this.requirementState.comparisons;
readonly uiConflicts = this.uiRegistry.conflicts;
readonly requirementComparisons = computed(() => this.scope() === 'server' ? this.requirementState.comparisons() : []);
readonly uiConflicts = computed(() => this.uiRegistry.conflicts()
.filter((conflict) => conflict.pluginIds.some((pluginId) => this.hasVisiblePlugin(pluginId))));
readonly selectedRequirement = computed(() => {
const selectedPlugin = this.selectedPlugin();
@@ -113,6 +123,10 @@ export class PluginManagerComponent {
? this.uiRegistry.settingsPageRecords().filter((record) => record.pluginId === selectedPlugin.manifest.id)
: [];
});
readonly emptyTitle = computed(() => this.scope() === 'server' ? 'No server plugins installed.' : 'No client plugins installed.');
readonly emptyBody = computed(() => this.scope() === 'server'
? 'Server-scoped plugins use scope: server in toju-plugin.json.'
: 'Client-scoped plugins use scope: client or omit scope in toju-plugin.json.');
readonly selectedDocs = computed(() => {
const manifest = this.selectedPlugin()?.manifest;
@@ -176,11 +190,21 @@ export class PluginManagerComponent {
}
}
async activate(entry: RegisteredPlugin): Promise<void> {
this.busyPluginId.set(entry.manifest.id);
try {
await this.host.activatePluginById(entry.manifest.id);
} finally {
this.busyPluginId.set(null);
}
}
async unload(entry: RegisteredPlugin): Promise<void> {
this.busyPluginId.set(entry.manifest.id);
try {
await this.host.deactivatePlugin(entry.manifest.id);
await this.host.deactivatePlugin(entry.manifest.id, { forgetActivation: true });
} finally {
this.busyPluginId.set(null);
}
@@ -194,6 +218,10 @@ export class PluginManagerComponent {
return this.selectedPlugin()?.manifest.id === entry.manifest.id;
}
isActive(entry: RegisteredPlugin): boolean {
return this.host.isPluginActive(entry.manifest.id);
}
close(): void {
this.closed.emit();
}
@@ -205,4 +233,12 @@ export class PluginManagerComponent {
trackCapability(index: number, capability: PluginCapabilityId): string {
return capability;
}
private entryBelongsToScope(entry: RegisteredPlugin): boolean {
return getPluginInstallScope(entry.manifest) === this.scope();
}
private hasVisiblePlugin(pluginId: string): boolean {
return this.entries().some((entry) => entry.manifest.id === pluginId);
}
}