-
+
@if (showIntentSuggestion()) {
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.scss b/apps/showcase_ui/src/app/pages/home/home.component.scss
index c1a5e68..4fb2884 100644
--- a/apps/showcase_ui/src/app/pages/home/home.component.scss
+++ b/apps/showcase_ui/src/app/pages/home/home.component.scss
@@ -5166,3 +5166,5 @@ $text-light: #94a3b8;
transform: translateY(0) scale(1);
}
}
+
+.composer-card > app-task-plugins { margin-bottom: 8px; }
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.ts b/apps/showcase_ui/src/app/pages/home/home.component.ts
index 81a66e5..664eddd 100644
--- a/apps/showcase_ui/src/app/pages/home/home.component.ts
+++ b/apps/showcase_ui/src/app/pages/home/home.component.ts
@@ -18,7 +18,7 @@ import { ModelSettingsComponent } from '../../components/model-settings/model-se
import { Component, signal, computed, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { TaskTargetComponent, TaskTargetOptions } from '../../components/task-target/task-target.component';
+import { TaskPluginsComponent, TaskPluginOptions } from '../../components/task-plugins/task-plugins.component';
import { Router } from '@angular/router';
import { AgentService } from '../../services/agent.service';
import { SystemService, formatAdbRestartFeedback } from '../../services/system.service';
@@ -106,7 +106,7 @@ type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote';
@Component({
selector: 'app-home',
standalone: true,
- imports: [FormsModule, ModelSettingsComponent, CapabilitySettingsComponent, TaskTargetComponent],
+ imports: [FormsModule, ModelSettingsComponent, CapabilitySettingsComponent, TaskPluginsComponent],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './home.component.scss'
@@ -320,8 +320,8 @@ export class HomeComponent implements OnInit, OnDestroy {
});
// Computed helper states delegating to SystemService
- taskTarget = signal
({ execution_target: 'android' });
- public isReady = computed(() => this.taskTarget().execution_target === 'computer' || this.systemService.isReady());
+ taskPlugins = signal({});
+ public isReady = computed(() => this.systemService.isReady());
public hasReadinessReport = computed(() => this.systemService.hasReadinessReport());
public isLoading = computed(() => this.systemService.isLoading());
public isRestartingAdb = computed(() => this.systemService.isRestartingAdb());
@@ -813,7 +813,7 @@ export class HomeComponent implements OnInit, OnDestroy {
this.selectedProfile() === 'pro'
? { verificationLevel: this.verificationLevel().id, explorerMode: this.explorerMode().id }
: undefined,
- { ...this.taskTarget() }
+ { ...this.taskPlugins() }
)
.subscribe({
next: () => {
diff --git a/apps/showcase_ui/src/app/pages/plugins/plugins.component.html b/apps/showcase_ui/src/app/pages/plugins/plugins.component.html
index cbee4bc..af354aa 100644
--- a/apps/showcase_ui/src/app/pages/plugins/plugins.component.html
+++ b/apps/showcase_ui/src/app/pages/plugins/plugins.component.html
@@ -1,6 +1,6 @@
- ARTEMIS · 扩展能力插件
为测试接入电脑和其他工具,统一执行并保留证据。
-
+
@if (error()) { {{error()}}
}
@if (notice()) { {{notice()}}
}
@for (resource of blocked(); track resource[0]) {
@@ -18,7 +18,7 @@
@if (loading()) { 正在读取插件…
}
@else if (!installed().length) { extension还没有安装插件
从市场安装经过验证的扩展,或在下方导入自己的插件。
}
@for (plugin of installed(); track plugin.manifest.id) {
-
extension
{{plugin.manifest.name}}
v{{plugin.manifest.version}} · {{plugin.source === 'marketplace' ? '已验证市场插件' : '本地 / 自定义'}}{{plugin.enabled ? '已启用' : '未启用'}}
+ extension
{{plugin.manifest.name}}
v{{plugin.manifest.version}} · {{plugin.source === 'marketplace' ? '插件市场' : '本地 / 自定义'}}{{plugin.enabled ? '已启用' : '未启用'}}
{{plugin.manifest.description || '暂无描述'}}
{{plugin.profiles.length}} 套连接配置
@@ -28,10 +28,11 @@
添加自定义 MCP / Skill
} @else {
- @if (catalogError()) { {{catalogError()}}
}
- @else if (!market().length) { 首批插件验证中
验证通过的插件会出现在这里;不会自动安装未经验证的版本。
}
+ @if (catalogLoading()) { progress_activity正在加载插件市场
正在获取插件与验证记录…
}
+ @else if (catalogError()) { {{catalogError()}}
}
+ @else if (!market().length) { 暂无可安装插件
当前市场尚未收录安装包,可稍后刷新。
}
@for (entry of market(); track entry.manifest.id + entry.manifest.version) {
-
verified
{{entry.manifest.name}}
v{{entry.manifest.version}} · {{entry.manifest.platforms.join(' / ')}}{{entry.manifest.description}}
验证记录
{{entry.verification}}
+
verified
{{entry.manifest.name}}
v{{entry.manifest.version}} · {{entry.manifest.platforms.join(' / ')}}@if (entry.release_stage === 'preview') {
开发预览 }
{{entry.manifest.description}}
验证范围与限制
{{entry.verification}}
}
}
diff --git a/apps/showcase_ui/src/app/pages/plugins/plugins.component.scss b/apps/showcase_ui/src/app/pages/plugins/plugins.component.scss
index 1b5f184..aba1650 100644
--- a/apps/showcase_ui/src/app/pages/plugins/plugins.component.scss
+++ b/apps/showcase_ui/src/app/pages/plugins/plugins.component.scss
@@ -5,12 +5,12 @@ header { justify-content: space-between; margin-bottom: 28px; }
h1 { margin: 8px 0; font-size: 30px; letter-spacing: -.6px; } h2 { margin: 0 0 6px; font-size: 17px; }
p { color: #64748b; line-height: 1.7; white-space: pre-wrap; margin: 8px 0 16px; }
.eyebrow { color: #3b6e9c; font-size: 11px; letter-spacing: 1.4px; font-weight: 600; }
-button { border: 1px solid #dce3eb; border-radius: 8px; background: #fff; color: #334155; padding: 8px 14px; font-size: 13px; cursor: pointer; white-space: nowrap; }
+button { border: 1px solid #dce3eb; border-radius: 20px; background: #fff; color: #334155; padding: 8px 16px; font-size: 13px; cursor: pointer; white-space: nowrap; }
button:hover:not(:disabled) { border-color: #9fb7d3; background: #f0f6ff; } button:disabled { opacity: .5; cursor: not-allowed; }
button.primary { color: #fff; background: #2563eb; border-color: #2563eb; } button.danger { color: #b91c1c; border-color: #fecaca; }
.text-button { border: 0; margin-left: auto; color: #64748b; } :focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px; }
-.tabs { display: flex; gap: 8px; margin-bottom: 24px; border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; }
-.tabs button { border: 0; background: transparent; }.tabs button.active { color: #1d4ed8; background: #eaf1ff; font-weight: 600; }.tabs span { margin-left: 8px; }
+.tabs { display: flex; gap: 4px; width: fit-content; margin-bottom: 24px; background: #f1f5f9; padding: 4px; border-radius: 24px; }
+.tabs button { border: 0; background: transparent; }.tabs button.active { color: #1d4ed8; background: #fff; box-shadow: 0 1px 4px #0f172a0d; font-weight: 600; }.tabs span { margin-left: 8px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 18px; }
.card { padding: 22px; background: #fff; border: 1px solid #e1e7ef; border-radius: 14px; }.card > p { min-height: 48px; margin-top: 18px; }.card footer { margin-top: 20px; flex-wrap: wrap; }
.plugin-icon { display: grid; place-items: center; width: 44px; height: 44px; flex-shrink: 0; color: #4477ba; background: #eef4ff; border-radius: 12px; }
@@ -22,3 +22,9 @@ details { margin: 16px 0; }summary { cursor: pointer; color: #475569; padding: 8
label { display: flex; flex-direction: column; gap: 7px; margin: 16px 0; font-size: 13px; }input, select, textarea { border: 1px solid #d5dde7; background: #fff; border-radius: 8px; padding: 10px 12px; color: #1e293b; font-size: 13px; min-width: 0; width: 100%; }textarea { resize: vertical; font-family: ui-monospace, 'Microsoft YaHei', monospace; line-height: 1.6; }.inline > input, .inline > select { flex: 1; }
.modal-backdrop { position: fixed; inset: 0; background: #0f172a50; backdrop-filter: blur(3px); display: grid; place-items: center; z-index: 1300; padding: 24px; }.dialog { max-width: 620px; width: 100%; max-height: 90vh; overflow-y: auto; background: #fff; padding: 28px; border-radius: 16px; box-shadow: 0 24px 80px #0f172a30; }.dialog.small { max-width: 430px; }.dialog header { margin-bottom: 12px; }.dialog footer { justify-content: flex-end; margin-top: 24px; }.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }.checkbox { flex-direction: row; align-items: center; gap: 10px; }.checkbox input { width: auto; }
@media (max-width: 640px) { .plugins-page { padding: 80px 16px 32px; }.card { padding: 18px; }.dialog { padding: 20px; }.two-columns { grid-template-columns: 1fr; gap: 0; }.card-top { gap: 8px; }h2 { font-size: 15px; } }
+
+.loading-icon { animation: spin 1s linear infinite; }
+@keyframes spin { to { transform: rotate(360deg); } }
+@media (prefers-reduced-motion: reduce) { .loading-icon { animation: none; } }
+
+.badge.preview { background: #fffbeb; color: #92400e; }
diff --git a/apps/showcase_ui/src/app/pages/plugins/plugins.component.spec.ts b/apps/showcase_ui/src/app/pages/plugins/plugins.component.spec.ts
index e2eaa12..cf31cf7 100644
--- a/apps/showcase_ui/src/app/pages/plugins/plugins.component.spec.ts
+++ b/apps/showcase_ui/src/app/pages/plugins/plugins.component.spec.ts
@@ -20,6 +20,29 @@ describe('PluginsComponent', () => {
return fixture;
}
+ it('distinguishes market loading, failure, empty and available states', async () => {
+ const fixture = TestBed.createComponent(PluginsComponent);
+ fixture.detectChanges();
+ fixture.componentInstance.tab.set('market'); fixture.detectChanges();
+ expect(fixture.nativeElement.textContent).toContain('正在加载插件市场');
+ expect(fixture.nativeElement.textContent).not.toContain('暂无可安装插件');
+ http.expectOne('/api/plugins').flush({ plugins: [] });
+ http.expectOne('/api/plugins/catalog').flush({ detail: '暂时无法读取插件市场' }, { status: 502, statusText: 'Bad Gateway' });
+ await fixture.whenStable(); fixture.detectChanges();
+ expect(fixture.nativeElement.textContent).toContain('暂时无法读取插件市场');
+ expect(fixture.nativeElement.textContent).not.toContain('暂无可安装插件');
+ const retry = fixture.componentInstance.refreshCatalog();
+ http.expectOne('/api/plugins/catalog').flush({ plugins: [] });
+ await retry; fixture.detectChanges();
+ expect(fixture.nativeElement.textContent).toContain('暂无可安装插件');
+ const refresh = fixture.componentInstance.refreshCatalog();
+ http.expectOne('/api/plugins/catalog').flush({ plugins: [{ manifest: { id: 'desktop', name: '电脑控制', version: '1.0.0', platforms: ['win32'], description: '操作桌面及浏览器' }, verification: '开发预览:已验证基础表单操作' }] });
+ await refresh; fixture.detectChanges();
+ expect(fixture.nativeElement.textContent).toContain('电脑控制');
+ expect(fixture.nativeElement.textContent).toContain('开发预览');
+ expect(fixture.nativeElement.textContent).toContain('安装插件');
+ });
+
it('shows empty state and requires explicit confirmation before releasing a protected resource', async () => {
const fixture = setup();
await fixture.whenStable(); fixture.detectChanges();
diff --git a/apps/showcase_ui/src/app/pages/plugins/plugins.component.ts b/apps/showcase_ui/src/app/pages/plugins/plugins.component.ts
index 665e223..251dc5a 100644
--- a/apps/showcase_ui/src/app/pages/plugins/plugins.component.ts
+++ b/apps/showcase_ui/src/app/pages/plugins/plugins.component.ts
@@ -15,6 +15,7 @@ export class PluginsComponent implements OnInit, OnDestroy {
busy = signal(false);
error = signal('');
catalogError = signal('');
+ catalogLoading = signal(true);
notice = signal('');
blocked = signal<[string, string][]>([]);
confirmingResource = signal(null);
@@ -42,9 +43,10 @@ export class PluginsComponent implements OnInit, OnDestroy {
finally { this.loading.set(false); }
}
async refreshCatalog() {
- this.catalogError.set('');
+ this.catalogLoading.set(true); this.catalogError.set('');
try { this.market.set((await firstValueFrom(this.api.catalog())).plugins); }
catch (error) { this.catalogError.set(this.message(error)); }
+ finally { this.catalogLoading.set(false); }
}
installedVersion(id: string) { return this.installed().find(p => p.manifest.id === id)?.manifest.version; }
fields() { return Object.entries(this.editing()?.manifest.config || {}); }
diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
index 9a03b2d..815b90f 100644
--- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
+++ b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
@@ -185,7 +185,7 @@ limitations under the License.
-
+