diff --git a/config/feature_flags.yaml b/config/feature_flags.yaml index 7cb3b850..0e0762ff 100644 --- a/config/feature_flags.yaml +++ b/config/feature_flags.yaml @@ -11,79 +11,12 @@ mobile: build_modes: [debug, profile, release] description: Mobile assistant destination ui_surface: mobile_shell - tasks: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile tasks destination - ui_surface: mobile_shell - workspace: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace hub destination - ui_surface: mobile_shell - secrets: - enabled: true - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile secrets destination - ui_surface: mobile_shell settings: enabled: true release_tier: stable build_modes: [debug, profile, release] description: Mobile settings destination ui_surface: mobile_shell - workspace: - skills: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace skills launcher - ui_surface: mobile_workspace_hub - nodes: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace nodes launcher - ui_surface: mobile_workspace_hub - agents: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace agents launcher - ui_surface: mobile_workspace_hub - mcp_server: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile workspace MCP launcher - ui_surface: mobile_workspace_hub - claw_hub: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile workspace ClawHub launcher - ui_surface: mobile_workspace_hub - connectors: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile workspace connectors launcher - ui_surface: mobile_workspace_hub - ai_gateway: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace AI Gateway launcher - ui_surface: mobile_workspace_hub - account: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile workspace account launcher - ui_surface: mobile_workspace_hub assistant: direct_ai: enabled: true @@ -113,7 +46,7 @@ mobile: enabled: true release_tier: experimental build_modes: [debug, profile, release] - description: Mobile multi-agent toggle in assistant composer + description: Mobile multi-agent assistant controls ui_surface: assistant_page local_runtime: enabled: false @@ -122,47 +55,35 @@ mobile: description: Mobile does not expose desktop runtime controls ui_surface: assistant_page settings: - general: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile settings general tab - ui_surface: settings_page - workspace: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile settings workspace tab - ui_surface: settings_page gateway: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Mobile settings gateway tab + description: Mobile bridge and integration settings ui_surface: settings_page account_access: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Mobile account access section + description: Mobile account access settings ui_surface: settings_page vault_server: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Mobile Vault server integration section + description: Mobile vault server settings ui_surface: settings_page gateway_self_hosted_base: enabled: false release_tier: experimental build_modes: [debug, profile, release] - description: Mobile self-hosted base connection controls + description: Mobile self-hosted gateway base controls ui_surface: settings_page gateway_advanced_custom_mode: enabled: false release_tier: experimental build_modes: [debug, profile, release] - description: Mobile advanced custom override mode + description: Mobile advanced gateway override controls ui_surface: settings_page gateway_setup_code: enabled: false @@ -170,36 +91,6 @@ mobile: build_modes: [debug, profile, release] description: Mobile gateway setup code editor ui_surface: settings_page - agents: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile settings multi-agent tab - ui_surface: settings_page - appearance: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile settings appearance tab - ui_surface: settings_page - diagnostics: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile settings diagnostics tab - ui_surface: settings_page - experimental: - enabled: true - release_tier: experimental - build_modes: [debug, profile, release] - description: Mobile settings experimental tab - ui_surface: settings_page - about: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Mobile settings about tab - ui_surface: settings_page experimental_canvas: enabled: true release_tier: experimental @@ -227,79 +118,12 @@ desktop: build_modes: [debug, profile, release] description: Desktop assistant destination ui_surface: sidebar_navigation - tasks: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop tasks destination - ui_surface: sidebar_navigation - skills: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop skills destination - ui_surface: sidebar_navigation - nodes: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop nodes destination - ui_surface: sidebar_navigation - agents: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop agents destination - ui_surface: sidebar_navigation - mcp_server: - enabled: true - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop MCP Hub destination - ui_surface: sidebar_navigation - claw_hub: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop ClawHub destination - ui_surface: sidebar_navigation - secrets: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop secrets destination - ui_surface: sidebar_navigation - ai_gateway: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop AI Gateway destination - ui_surface: sidebar_navigation settings: enabled: true release_tier: stable build_modes: [debug, profile, release] description: Desktop settings destination ui_surface: sidebar_navigation - account: - enabled: false - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop account destination - ui_surface: sidebar_navigation - workspace: - claw_hub: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop workspace ClawHub tab - ui_surface: modules_page - connectors: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop workspace connectors tab - ui_surface: modules_page assistant: direct_ai: enabled: true @@ -329,56 +153,44 @@ desktop: enabled: true release_tier: beta build_modes: [debug, profile, release] - description: Desktop multi-agent toggle in assistant composer + description: Desktop multi-agent assistant controls ui_surface: assistant_page local_runtime: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Desktop local runtime and gateway orchestration entry + description: Desktop local runtime and gateway orchestration controls ui_surface: assistant_page settings: - general: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop settings general tab - ui_surface: settings_page - workspace: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop settings workspace tab - ui_surface: settings_page gateway: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Desktop settings gateway tab + description: Desktop bridge and integration settings ui_surface: settings_page account_access: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Desktop account access section + description: Desktop account access settings ui_surface: settings_page vault_server: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Desktop Vault server integration section + description: Desktop vault server settings ui_surface: settings_page gateway_self_hosted_base: enabled: false release_tier: experimental build_modes: [debug, profile, release] - description: Desktop self-hosted base connection controls + description: Desktop self-hosted gateway base controls ui_surface: settings_page gateway_advanced_custom_mode: enabled: false release_tier: experimental build_modes: [debug, profile, release] - description: Desktop advanced custom override mode + description: Desktop advanced gateway override controls ui_surface: settings_page gateway_setup_code: enabled: false @@ -386,36 +198,6 @@ desktop: build_modes: [debug, profile, release] description: Desktop gateway setup code editor ui_surface: settings_page - agents: - enabled: false - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop settings multi-agent tab - ui_surface: settings_page - appearance: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop settings appearance tab - ui_surface: settings_page - diagnostics: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop settings diagnostics tab - ui_surface: settings_page - experimental: - enabled: true - release_tier: experimental - build_modes: [debug, profile, release] - description: Desktop settings experimental tab - ui_surface: settings_page - about: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Desktop settings about tab - ui_surface: settings_page experimental_canvas: enabled: true release_tier: experimental @@ -443,36 +225,6 @@ web: build_modes: [debug, profile, release] description: Web assistant destination ui_surface: web_shell - tasks: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web tasks destination - ui_surface: web_shell - skills: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web skills destination - ui_surface: web_shell - nodes: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web nodes destination - ui_surface: web_shell - secrets: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web secrets destination - ui_surface: web_shell - ai_gateway: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web LLM API destination - ui_surface: web_shell settings: enabled: true release_tier: stable @@ -485,89 +237,89 @@ web: release_tier: stable build_modes: [debug, profile, release] description: Web direct AI assistant mode - ui_surface: web_assistant_page + ui_surface: assistant_page + local_gateway: + enabled: false + release_tier: experimental + build_modes: [] + description: Web does not expose local gateway controls + ui_surface: assistant_page relay_gateway: enabled: true release_tier: stable build_modes: [debug, profile, release] description: Web relay gateway assistant mode - ui_surface: web_assistant_page + ui_surface: assistant_page file_attachments: enabled: true release_tier: stable build_modes: [debug, profile, release] description: Web file attachment action in assistant composer - ui_surface: web_assistant_page + ui_surface: assistant_page multi_agent: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web multi-agent toggle in assistant composer - ui_surface: web_assistant_page - local_gateway: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web local gateway assistant mode - ui_surface: web_assistant_page + enabled: false + release_tier: experimental + build_modes: [] + description: Web multi-agent controls disabled + ui_surface: assistant_page local_runtime: enabled: false release_tier: experimental build_modes: [] - description: Web does not expose desktop runtime controls - ui_surface: web_assistant_page + description: Web does not expose local runtime controls + ui_surface: assistant_page settings: - general: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web settings general tab - ui_surface: web_settings_page gateway: enabled: true release_tier: stable build_modes: [debug, profile, release] - description: Web settings gateway tab - ui_surface: web_settings_page + description: Web bridge and integration settings + ui_surface: settings_page account_access: enabled: true - release_tier: beta - build_modes: [] - description: Web does not expose account access section - ui_surface: web_settings_page + release_tier: stable + build_modes: [debug, profile, release] + description: Web account access settings + ui_surface: settings_page vault_server: - enabled: true - release_tier: beta + enabled: false + release_tier: experimental build_modes: [] - description: Web does not expose vault server integration - ui_surface: web_settings_page + description: Web vault server settings disabled + ui_surface: settings_page gateway_self_hosted_base: enabled: false release_tier: experimental build_modes: [] - description: Web does not expose self-hosted base connection controls - ui_surface: web_settings_page + description: Web self-hosted gateway base controls disabled + ui_surface: settings_page gateway_advanced_custom_mode: enabled: false release_tier: experimental build_modes: [] - description: Web does not expose advanced custom override mode - ui_surface: web_settings_page + description: Web advanced gateway override controls disabled + ui_surface: settings_page gateway_setup_code: enabled: false release_tier: experimental build_modes: [] - description: Web does not expose gateway setup code editor - ui_surface: web_settings_page - appearance: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web settings appearance tab - ui_surface: web_settings_page - about: - enabled: true - release_tier: stable - build_modes: [debug, profile, release] - description: Web settings about tab - ui_surface: web_settings_page + description: Web gateway setup code editor disabled + ui_surface: settings_page + experimental_canvas: + enabled: false + release_tier: experimental + build_modes: [] + description: Web experimental canvas host toggle disabled + ui_surface: settings_page + experimental_bridge: + enabled: false + release_tier: experimental + build_modes: [] + description: Web experimental bridge toggle disabled + ui_surface: settings_page + experimental_debug: + enabled: false + release_tier: experimental + build_modes: [] + description: Web experimental debug runtime toggle disabled + ui_surface: settings_page diff --git a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md index f94ced45..77ff5ee6 100644 --- a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md +++ b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md @@ -4,308 +4,214 @@ Last Updated: 2026-04-13 ## Repo Context -本文件按仓库真实代码形态盘点 XWorkmate 当前核心模块,不只描述“产品主链”,也显式标出仍然留在仓库中的受限入口、别名入口与陈旧残留。 +本仓库当前已经收敛到 `assistant + settings` 双端极简 surface。 -平台观察按两大产品面组织: - -- `Desktop APP`:`macOS / Linux / Windows` -- `Mobile APP`:`iOS / Android` - -状态口径: - -- `Active`:当前 surface 仍然直接承载主链 -- `Gated`:代码存在,但是否可达取决于 manifest / platform / shell 映射 -- `Alias`:主要是跳转或折叠到别的当前页面 -- `Legacy-present`:仓库中仍有代码,但不属于当前主要 surface - -当前仓库需要特别注意的事实: - -- 桌面端真实壳层由 [`lib/app/app_shell_desktop.dart`](../../lib/app/app_shell_desktop.dart) 控制,当前页面栈只保留 `assistant + settings` -- `workspace_page_registry.dart` 仍然保留 `tasks / skills / nodes / agents / mcpServer / clawHub / account` -- `feature_flags.yaml`、`UiFeatureAccess.destinationMappingsInternal`、`AppShell._desktopDestinations` 不是完全同一口径 +- `Desktop APP` 与 `Mobile APP` 顶层都只保留 `assistant`、`settings` +- `feature_flags.yaml` 是 surface 可见性的唯一声明源 +- `UiFeatureManifest / UiFeatureAccess / AppCapabilities` 负责把 manifest 解析成当前平台允许能力 +- `AppShell / MobileShell` 与 `workspace_page_registry.dart` 已同步收敛到同一口径,不再存在“manifest 允许但 shell/registry 仍保留旧页”的双重真源 +- `xworkmate-app` 当前只保留与 `xworkmate-bridge` C/S 主链直接相关的 surface、gate、controller、runtime 合同 +- 已删除独立 `tasks / skills / modules / mcp / claw_hub / secrets / ai_gateway / account` 页面入口、alias 路由与对应枚举残留 ## Overall Layering ```mermaid -flowchart LR - subgraph APP["lib/app"] - A1["workspace_page_registry.dart
workspace_navigation.dart
ui_feature_manifest_core.dart
ui_feature_manifest_fallback.dart"] - A2["AppShell / AppControllerDesktop*"] +flowchart TD + subgraph L1["Surface Visibility"] + A1["config/feature_flags.yaml"] + A2["UiFeatureManifest / UiFeatureAccess / AppCapabilities"] + A3["AppShell / MobileShell / workspace_page_registry.dart"] + A4["AssistantPage / SettingsPage"] end - subgraph FEATURES["lib/features"] - F1["AssistantPage"] - F2["SettingsPage"] - F3["TasksPage"] - F4["ModulesPage"] - F5["SkillsPage"] - F6["ClawHubPage"] - F7["McpServerPage"] - F8["MobileShell"] + subgraph L2["Local Orchestration"] + B1["AppControllerDesktop*"] + B2["SettingsController"] + B3["GatewaySessionsController"] + B4["GatewayChatController"] + B5["GatewayAgentsController"] + B6["DerivedTasksController"] + B7["SkillsController"] end - subgraph RUNTIME["lib/runtime"] - R1["SettingsController"] - R2["DerivedTasksController"] - R3["GatewayAcpClient"] - R4["ExternalCodeAgentAcpDesktopTransport"] - R5["GoTaskServiceClient"] - R6["AgentRegistry"] - R7["MultiAgentOrchestrator"] - R8["SettingsStore"] + subgraph L3["Bridge Contract"] + C1["GatewayAcpClient"] + C2["ExternalCodeAgentAcpDesktopTransport"] + C3["GoTaskServiceClient"] + C4["Managed bridge/account sync contract"] end - A1 --> F1 - A1 --> F2 - A1 --> F3 - A1 --> F4 - A1 --> F5 - A1 --> F6 - A1 --> F7 - A1 --> F8 + subgraph L4["Upstream Adapters"] + D1["xworkmate-bridge"] + D2["Upstream providers / agent runtimes / ACP adapters"] + end - A2 --> F1 - A2 --> F2 - A2 --> F8 - A2 --> R1 - A2 --> R2 - A2 --> R3 - A2 --> R4 - A2 --> R5 - A2 --> R6 - A2 --> R7 - - F1 --> R2 - F1 --> R3 - F1 --> R4 - F1 --> R5 - F2 --> R1 - F2 --> R8 - F3 --> R2 - F4 --> R6 - F4 --> R7 - F7 --> R3 - - R1 --> R8 - R4 --> R3 - R4 --> R5 - R7 --> R5 + A1 --> A2 --> A3 --> A4 + A4 --> B1 + B1 --> B2 + B1 --> B3 + B1 --> B4 + B1 --> B5 + B1 --> B6 + B1 --> B7 + B1 --> C1 + B1 --> C2 + B1 --> C3 + B2 --> C4 + C1 --> D1 + C2 --> D1 + C3 --> D1 + D1 --> D2 ``` ## Surface And Gate Flow ```mermaid flowchart TD - M1["config/feature_flags.yaml"] - M2["fallbackUiFeatureManifestYamlInternal"] - M3["UiFeatureManifestLoader / UiFeatureManifest"] - M4["UiFeatureAccess.allowedDestinations
feature switches"] - - D1["Desktop APP
AppShell._desktopDestinations"] - D2["Mobile APP
MobileShellTab / MobileWorkspaceLauncher"] - D3["workspace_page_registry.dart"] - + M1["feature_flags.yaml"] + M2["UiFeatureManifest / AppCapabilities"] + M3["AppShell / MobileShell"] + M4["workspace_page_registry.dart"] P1["AssistantPage"] P2["SettingsPage"] - P3["TasksPage"] - P4["ModulesPage"] - P5["SkillsPage"] - P6["McpServerPage"] - P7["ClawHubPage"] + C1["AppController"] + R1["GoTaskServiceClient / GatewayAcpClient"] + B1["xworkmate-bridge"] + U1["upstream adapters"] - M1 --> M3 + M1 --> M2 M2 --> M3 - M3 --> M4 - - M4 --> D1 - M4 --> D2 - M4 --> D3 - - D1 --> P1 - D1 --> P2 - - D2 --> P1 - D2 --> P2 - D2 --> P3 - D2 --> P4 - D2 --> P5 - D2 --> P6 - D2 --> P7 - - D3 --> P1 - D3 --> P2 - D3 --> P3 - D3 --> P4 - D3 --> P5 - D3 --> P6 - D3 --> P7 + M2 --> M4 + M3 --> P1 + M3 --> P2 + M4 --> P1 + M4 --> P2 + P1 --> C1 + P2 --> C1 + C1 --> R1 + R1 --> B1 + B1 --> U1 ``` +当前真实口径: + +- 没有 fallback manifest +- 没有 `secrets -> settings`、`ai_gateway -> settings`、`account -> settings` 兼容别名 +- 没有独立 `modules`/`workspace hub`/`fake module matrix` + ## Global Summary -> `Current Status` 按模块组总体判断;平台差异在后面的 `Desktop APP`、`Mobile APP` 和详细表中展开。 +`Current Status` 按模块组总体判断;平台差异在后面的 `Desktop APP`、`Mobile APP` 和详细段落中展开。 -| Module Group | Primary Paths | App Entry | Feature/Page Class | Runtime/Core Classes | Core Functions / Extensions | Surface | Gate / Routing Source | Current Status | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| Assistant | `lib/features/assistant/*`, `lib/app/app_shell_desktop.dart` | `AppShell`, `workspace_page_registry.dart` | `AssistantPage` | `GatewayAcpClient`, `ExternalCodeAgentAcpDesktopTransport`, `GoTaskServiceClient` | `submitPromptInternal`, `buildMainWorkspaceInternal`, `setAssistantExecutionTarget`, `buildExternalAcpRoutingForSessionInternal` | Desktop + Mobile | surface mapping + direct route | `Active` | -| Settings | `lib/features/settings/*`, `lib/runtime/runtime_controllers_settings*` | `AppShell`, `navigateTo/openSettings` | `SettingsPage`, `SettingsAccountPanel` | `SettingsController`, `SettingsStore` | `_loginAccount`, `_syncAccount`, `loginAccount`, `syncAccountSettings`, `reloadDerivedStateInternal` | Desktop + Mobile | surface mapping + settings alias | `Active` | -| Tasks | `lib/features/tasks/tasks_page.dart`, `lib/runtime/runtime_controllers_derived_tasks.dart` | `workspace_page_registry.dart`, dormant mobile/desktop route slots | `TasksPage` | `DerivedTasksController`, `DesktopTaskThreadRepository` | `recompute`, `taskItemsForTab`, `switchSession` | Registry present, shell not primary | desktop manifest / mobile manifest / surface mapping | `Gated` | -| Agents | `lib/runtime/agent_registry.dart`, `lib/runtime/multi_agent_*` | Assistant runtime lane + `ModulesPage` tabs | `ModulesPage` (agents tab) | `AgentRegistry`, `MultiAgentOrchestrator`, `MultiAgentMountManager` | `register`, `listAgents`, `runCollaboration`, `runEngineerInternal` | Assistant runtime + dormant module UI | runtime only + surface mapping | `Active` | -| Modules | `lib/features/modules/modules_page.dart` | `navigateTo`, `openModules`, `workspace_page_registry.dart` | `ModulesPage` | `AgentRegistry`, `InstancesController`, `SkillsController` | `_normalizeTab`, `_isTabVisible`, `_visibleTabs`, `openModules` | Registry present, current shell弱化 | surface mapping + desktop manifest | `Gated` | -| MCP/ACP | `lib/features/mcp_server/mcp_server_page.dart`, `lib/runtime/*acp*` | Assistant execution lane, registry, routing extensions | `McpServerPage` | `GatewayAcpClient`, `GoTaskServiceClient`, `ExternalCodeAgentAcpDesktopTransport` | `resolveExternalAcpRouting`, `executeTask`, `loadExternalAcpCapabilities`, `resolveBridgeAcpEndpointInternal` | Runtime mainline + dormant MCP page | runtime only + desktop manifest | `Active` | -| Skills / ClawHub | `lib/features/skills/skills_page.dart`, `lib/features/claw_hub/claw_hub_page.dart` | registry + mobile workspace launcher | `SkillsPage`, `ClawHubPage` | `SkillDirectoryAccessService`, `SkillsController` | `refresh`, `_resolveSelectedSkill`, `executeCommandInternal` | Skills有数据面,ClawHub偏占位壳 | mobile manifest / desktop manifest | `Gated` | -| Mobile Workspace | `lib/features/mobile/*` | compact mobile path in `AppShell`, `MobileShell` | `MobileShell`, `MobileWorkspaceLauncherInternal` | shared `AppController`, `DerivedTasksController` | `tabForDestinationInternal`, `selectTabInternal`, `buildCurrentPageInternal`, `showPairingGuidePageFlowInternal` | iOS + Android | mobile manifest + surface mapping | `Active` | -| Feature Manifest Fallback | `config/feature_flags.yaml`, `lib/app/ui_feature_manifest*.dart` | `UiFeatureManifestLoader`, `featuresFor()` | N/A | `UiFeatureManifest`, `UiFeatureAccess` | `forPlatform`, `allowedDestinations`, `sanitizeSettingsTab`, `load()` | Cross-platform | direct route | `Active` | - -## Desktop APP (`macOS / Linux / Windows`) - -### Desktop Surface Summary - -| Concern | Current Repo Truth | Notes | +| Module Group | Current Status | Current Repo Truth | | --- | --- | --- | -| Main shell | `AppShell` desktop path | 当前实际桌面页面栈只构建 `assistant + settings` | -| Dormant registry pages | `TasksPage`, `SkillsPage`, `ModulesPage`, `McpServerPage`, `ClawHubPage` | 仍保留在 `workspace_page_registry.dart` | -| Runtime richness | Assistant + bridge + ACP + multi-agent 最完整 | Desktop 是唯一完整本地 runtime / external ACP 宿主 | -| Risk | manifest / registry / shell 三份口径并存 | 结构评审重点应放在“单一事实源” | +| Desktop APP | Active | 顶层 only `assistant + settings` | +| Mobile APP | Active | 顶层 only `assistant + settings` | +| Assistant | Active | 保留 bridge/runtime 主链与任务/技能数据面 | +| Settings | Active | 保留 bridge/account/integration 主链 | +| Tasks | Removed surface | 不再有独立页面,仅保留 assistant/task-state 数据 | +| Modules | Removed surface | 不再有独立页面、tab、registry、alias | -## Mobile APP (`iOS / Android`) +## Desktop APP (macOS / Linux / Windows) -### Mobile Surface Summary +Status: `Active` -| Concern | Current Repo Truth | Notes | -| --- | --- | --- | -| Main shell | `MobileShell` | 当前主入口是 `assistant / workspace / secrets / settings` | -| Workspace hub | `MobileWorkspaceLauncherInternal` | 实际条目由 `features.allowedDestinations.contains(...)` 决定 | -| Pairing / bridge | `mobile_gateway_pairing_guide_page.dart` + setup-code flow | 移动端是典型 bridge thin client | -| Risk | `MobileShellTab` 与 manifest 允许项之间存在保留目的地 | 例如 `tasks` tab 枚举仍在,但 manifest 已关闭 | +- 桌面顶层 shell 只保留 `assistant`、`settings` +- `workspace_page_registry.dart` 只注册 `assistant`、`settings` +- 不再保留 `tasks / skills / modules / mcp / claw_hub / account` 独立桌面入口 +- 设置相关 bridge/account/integration 操作全部收口到 `SettingsPage` +- Assistant 仍然承载完整 desktop bridge/runtime 主链 + +## Mobile APP (iOS / Android) + +Status: `Active` + +- 移动端顶层 tab 只保留 `assistant`、`settings` +- 删除 `workspace / tasks / secrets` 顶层 tab +- 删除 `MobileWorkspaceLauncherInternal` +- 配对、Bridge connect、setup code 等流程保留为 `settings` detail flow 与 mobile-safe strip/sheet 能力,不再占独立 top-level surface ## Assistant -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/app/app_shell_desktop.dart` | app | `AppShell`, `_AppShellState` | `_desktopDestinations`, `_mobileDestinations`, `_createSidebarConversation`, `_pageForDestination` | `AppController`, `workspace_page_registry`, `UiFeatureAccess` | Desktop shell, compact mobile path | Desktop + Mobile | surface mapping | `Active` | 桌面实际只渲染 `assistant + settings` | -| `lib/app/workspace_page_registry.dart` | app | `WorkspacePageSpec` | `workspacePageSpecsInternal`, `buildWorkspacePage` | feature pages | `AppShell`, `MobileShell` | Desktop + Mobile | direct route | `Active` | registry 仍保留多余页面规格 | -| `lib/features/assistant/assistant_page_main.dart` | feature | `AssistantPage`, `AssistantPageStateInternal` | `build`, `handleComposerContentHeightChangedInternal` | `AppController`, runtime models, focus/artifact widgets | registry, `AppShell` | Desktop + Mobile | direct route | `Active` | 对话主页面壳层 | -| `lib/features/assistant/assistant_page_state_closure.dart` | feature | `AssistantPageStateClosureInternal` | `buildMainWorkspaceInternal` | `AssistantPageStateInternal`, widgets, controller | `AssistantPage` | Desktop + Mobile | direct route | `Active` | 负责主工作区布局、conversation/composer 拼装 | -| `lib/features/assistant/assistant_page_state_actions.dart` | feature | `AssistantPageStateActionsInternal` | `pickAttachmentsInternal`, `submitPromptInternal`, `buildAttachmentPayloadsInternal`, `pickAutoAgentInternal` | `AppController`, file selector, runtime models | `AssistantPage` | Desktop + Mobile | direct route | `Active` | 助手主要动作闭包 | -| `lib/app/app_controller_desktop_workspace_execution.dart` | app | `AppControllerDesktopWorkspaceExecution` | `setAssistantExecutionTarget`, `setAssistantSingleAgentProvider`, `applyAssistantExecutionTargetInternal` | `AppController`, thread binding, settings runtime | `AssistantPage` | Desktop | runtime only | `Active` | 桌面执行 target / provider / thread 绑定主链 | -| `lib/app/app_controller_desktop_external_acp_routing.dart` | app | `AppControllerDesktopExternalAcpRouting` | `buildExternalAcpRoutingForSessionInternal` | assistant thread records, `GoTaskServiceClient` models | Desktop assistant execution | Desktop | runtime only | `Active` | 把 session 级显式选择折叠成 ACP routing config | -| `lib/widgets/assistant_focus_panel.dart` + `assistant_artifact_sidebar.dart` | feature | Focus / Artifact side panels | panel build/render helpers | `AssistantArtifactSnapshot`, controller focus state | `AssistantPage` | Desktop + Mobile | direct route | `Active` | 属于 assistant 主链侧边闭包,不是独立模块 | +Status: `Active` + +保留的主链 runtime / controller: + +- `GatewayAcpClient` +- `ExternalCodeAgentAcpDesktopTransport` +- `GoTaskServiceClient` +- `GatewaySessionsController` +- `GatewayChatController` +- `GatewayAgentsController` +- `DerivedTasksController` +- `SkillsController` + +当前 Assistant 事实: + +- provider catalog 只来自 bridge capabilities,不再恢复任何 preset / backfill / fallback provider truth +- task state 仍在 assistant 内被消费,但不再拥有独立 `TasksPage` +- skills 数据仍在 assistant 内被消费,但不再拥有独立 `SkillsPage` +- assistant focus 只保留仍有真实落点的 `settings / language / theme` ## Settings -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/settings/settings_page_core.dart` | feature | `SettingsPage`, `_SettingsPageState` | `_saveAccountProfile`, `_loginAccount`, `_syncAccount`, `_verifyAccountMfa`, `_refreshBridgeCapabilities` | `AppController`, `SettingsController`, `SettingsAccountPanel` | registry, `AppShell` | Desktop + Mobile | surface mapping | `Active` | 当前设置主页面 | -| `lib/features/settings/settings_account_panel.dart` | feature | `SettingsAccountPanel`, `_SignedOutAccountPanel`, `_PendingMfaAccountPanel`, `_SignedInAccountPanel` | `build` | `SettingsSnapshot`, `AccountSyncState` | `SettingsPage` | Desktop + Mobile | direct route | `Active` | 账户登录 / MFA / 同步 UI 壳层 | -| `lib/runtime/runtime_controllers_settings.dart` | runtime | `SettingsController` | `initialize`, `refreshDerivedState`, `saveSnapshot`, `saveGatewaySecrets` | `SettingsStore`, secure refs, runtime models | `SettingsPage`, app runtime | Desktop + Mobile | runtime only | `Active` | 设置控制器根对象 | -| `lib/runtime/runtime_controllers_settings_account.dart` | runtime | `SettingsControllerAccountExtension` | `loginAccount`, `verifyAccountMfa`, `syncAccountSettings`, `reloadDerivedStateInternal`, `loadEffectiveGatewayToken` | `SettingsController`, secure storage | `SettingsPage`, bridge/account flow | Desktop + Mobile | runtime only | `Active` | 对外暴露账户同步与 secret 解析 API | -| `lib/runtime/runtime_controllers_settings_account_impl.dart` | runtime | account impl helpers | `loginAccountSettingsInternal`, `completeAccountSignInSettingsInternal`, `restoreAccountSessionSettingsInternal`, `syncAccountSettingsInternal` | `AccountRuntimeClient`, `SettingsStore` | `SettingsControllerAccountExtension` | Desktop + Mobile | runtime only | `Active` | 当前 bridge/account 合同链核心 | -| `lib/runtime/settings_store.dart` | runtime | `SettingsStore` | snapshot / secure refs / account session persistence API | local storage, secure storage | `SettingsController` | Desktop + Mobile | runtime only | `Active` | 设置、账号、线程元数据统一存储层 | +Status: `Active` + +当前设置面已经收敛为单一 bridge/settings 主链: + +- `SettingsTab` 只保留 `gateway` +- `SettingsDetailPage` 只保留 `gatewayConnection` +- `SettingsNavigationContext` 只保留当前真实 detail flow 所需字段 +- 账户登录、MFA、同步与 managed bridge contract 回写都收口在 `SettingsPage + SettingsAccountPanel` + +已删除的旧设置残留: + +- `ModulesTab` +- `SecretsTab` +- `AiGatewayTab` +- `aiGatewayIntegration` +- `externalAgents` +- `diagnosticsAdvanced` +- `vaultProvider` ## Tasks -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/tasks/tasks_page.dart` | feature | `TasksPage`, `_TasksPageState` | `build`, `_matchesQuery`, `_resolveSelectedTask` | `AppController`, `DerivedTasksController` | registry | Desktop + Mobile registry | desktop manifest / mobile manifest | `Gated` | 页面存在,但当前主 shell 不再把它作为首要入口 | -| `lib/runtime/runtime_controllers_derived_tasks.dart` | runtime | `DerivedTasksController` | `recompute`, `statusForSessionInternal`, `timeLabelInternal`, `durationLabelInternal` | sessions, `TaskThread`, scheduler data | `TasksPage`, mobile workspace hero stats | Cross-platform | runtime only | `Active` | 任务聚合的真实数据源 | -| `lib/app/task_thread_repositories.dart` | app | `DesktopTaskThreadRepository`, `WebTaskThreadRepository` | `replace`, `replaceAll`, `removeWhere`, `flush` | `TaskThread` | app thread/session flow | Desktop + Web | runtime only | `Active` | 任务线程持久化仓储,不是页面但直接供 task 聚合链路使用 | -| `lib/app/app_controller_desktop_thread_sessions.dart` | app | `AppControllerDesktopThreadSessions` | session switch / assistant session normalization APIs | `AppController`, task repositories | Assistant + tasks data source | Desktop | runtime only | `Active` | 任务页依赖其提供 session/thread 事实 | +Status: `Removed surface` -## Agents +保留范围仅限 assistant/task-state 数据面: -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/runtime/agent_registry.dart` | runtime | `AgentRegistry` | `register`, `unregister`, `listAgents`, `clearRegistration` | `GatewayRuntime` | assistant runtime, modules agent tab | Cross-platform runtime | runtime only | `Active` | 代理发现与注册中心 | -| `lib/runtime/multi_agent_orchestrator_core.dart` | runtime | `MultiAgentOrchestrator` | `updateConfig`, `enable`, `disable`, `runCollaboration`, `abort` | `MultiAgentConfig`, CLI/HTTP factories | assistant multi-agent flow | Desktop-focused runtime | runtime only | `Active` | 多代理协作核心编排器 | -| `lib/runtime/multi_agent_orchestrator_workflow.dart` | runtime | `MultiAgentOrchestratorWorkflowInternal` | `runArchitectInternal`, `runEngineerInternal`, `runTesterInternal`, `runFixInternal`, `runCliPromptInternal` | orchestrator core, CLI tools | `MultiAgentOrchestrator` | Desktop runtime | runtime only | `Active` | 角色工作流实现层 | -| `lib/runtime/multi_agent_mounts.dart` | runtime | `MultiAgentMountManager`, `CliMountAdapter` | `reconcile`, `_reconcileLocally`, adapter `reconcile()` | Codex/Opencode/Aris bridges | multi-agent config sync | Desktop runtime | runtime only | `Active` | 多 CLI 挂载目标协调层 | -| `lib/runtime/runtime_models_multi_agent.dart` | runtime | `MultiAgentConfig`, `ManagedSkillEntry`, `ManagedMcpServerEntry` | config/model copy & state carriers | runtime models | orchestrator + settings + assistant | Cross-platform models | runtime only | `Active` | agents 模块的配置与状态模型 | -| `lib/features/modules/modules_page.dart` | feature | `ModulesPage` agents tab shell | `_normalizeTab`, `_isTabVisible` | `AgentRegistry`, controller state | registry route | Desktop registry | desktop manifest / surface mapping | `Gated` | 代理 UI 与 runtime core 是两层,不应混为一个模块 | +- `DerivedTasksController` +- `DesktopTaskThreadRepository` +- assistant 内部 task rail / session/task 聚合 + +已删除: + +- `TasksPage` +- 顶层 `WorkspaceDestination.tasks` +- `MobileShellTab.tasks` ## Modules -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/modules/modules_page.dart` | feature | `ModulesPage`, `_ModulesPageState` | `_normalizeTab`, `_isTabVisible`, `_visibleTabs`, `_tabForLabel`, `build` | `AppController`, `UiFeatureAccess`, agents/instances/skills data | registry | Desktop registry | surface mapping + desktop manifest | `Gated` | 现存聚合页;当前桌面主 shell 不直接暴露 | -| `lib/app/app_controller_desktop_navigation.dart` | app | `AppControllerDesktopNavigation` | `navigateTo`, `openModules`, `openSettings`, `openSecrets`, `openAiGateway` | `capabilities`, settings/module tabs | shells, pages | Desktop + Mobile controller API | surface mapping + settings alias | `Active` | 模块/设置别名折叠逻辑在这里 | -| `lib/app/workspace_navigation.dart` | app | breadcrumb/navigation helpers | `buildWorkspaceBreadcrumbs`, `buildSettingsBreadcrumbs`, `openSettingsNavigationContext` | `AppController`, nav context | feature pages | Desktop + Mobile | direct route | `Active` | 模块页与设置页共享导航上下文装配 | -| `lib/app/workspace_page_registry.dart` | app | destination -> page registry | `workspacePageSpecsInternal`, `buildWorkspacePage` | all feature pages | shells | Desktop + Mobile | direct route | `Active` | `nodes`/`agents` 仍然映射回 `ModulesPage` | +Status: `Removed surface` -## MCP / ACP +已删除: -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/mcp_server/mcp_server_page.dart` | feature | `McpServerPage` | `build` | `AppController.connectors`, detail panel | registry route | Desktop registry | desktop manifest | `Gated` | 页面存在,但当前桌面主 shell 不直接显示 | -| `lib/runtime/acp_endpoint_paths.dart` | runtime | `AcpEndpointPaths` | ACP path constants | runtime URI builders | gateway/desktop transport | Cross-platform runtime | runtime only | `Active` | ACP 端点路径单点定义 | -| `lib/runtime/gateway_acp_client.dart` | runtime | `GatewayAcpClient` | capability load, session RPC, notification/result merge | HTTP / ACP RPC | assistant + bridge runtime | Cross-platform runtime | runtime only | `Active` | ACP 主客户端 | -| `lib/runtime/go_task_service_client.dart` | runtime | request/result/value models + transport abstractions | `toExternalAcpParams`, `goTaskServiceResultFromAcpResponse`, `goTaskServiceUpdateFromAcpNotification` | ACP payload contracts | desktop transport, app controller | Cross-platform runtime | runtime only | `Active` | 任务执行统一协议面 | -| `lib/runtime/external_code_agent_acp_desktop_transport.dart` | runtime | `ExternalCodeAgentAcpDesktopTransport` | `loadExternalAcpCapabilities`, `resolveExternalAcpRouting`, `executeTask`, `cancelTask`, `closeTask` | `GatewayAcpClient`, endpoint resolver | desktop assistant runtime | Desktop | runtime only | `Active` | 桌面 external ACP transport | -| `lib/app/app_controller_desktop_external_acp_routing.dart` | app | `AppControllerDesktopExternalAcpRouting` | `buildExternalAcpRoutingForSessionInternal` | assistant thread state, `GoTaskServiceClient` models | desktop assistant execution | Desktop | runtime only | `Active` | session 事实 -> ACP routing config | -| `lib/app/app_controller_desktop_runtime_helpers.dart` | app | `AppControllerDesktopRuntimeHelpers` | `resolveBridgeAcpEndpointInternal` and runtime resolver helpers | settings/account sync, runtime models | desktop assistant runtime | Desktop | runtime only | `Active` | Bridge 端点解析与桌面 runtime 帮助函数 | +- `ModulesPage` +- `SkillsPage` +- `McpServerPage` +- `ClawHubPage` +- `WorkspaceDestination.skills / nodes / agents / mcpServer / clawHub` +- `openModules` / module alias navigation API +- `workspace_page_registry` 中所有模块类 destination spec -## Skills / ClawHub +同时清理的孤儿 controller: -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/skills/skills_page.dart` | feature | `SkillsPage`, `_SkillsPageState` | `build`, `_matchesQuery`, `_resolveSelectedSkill` | `AppController.skills`, `SkillsController` | registry route | Desktop registry / mobile workspace registry | desktop manifest / mobile manifest | `Gated` | 技能页是真数据页,但当前主 shell 不直接暴露 | -| `lib/features/claw_hub/claw_hub_page.dart` | feature | `ClawHubPage`, `ClawHubPageStateInternal` | `executeCommandInternal`, `handleSearchInternal`, `handleInstallInternal`, `handleUpdateInternal` | local controllers only | registry route | Desktop registry / mobile workspace registry | desktop manifest / mobile manifest | `Legacy-present` | 更像 UI placeholder shell,不是当前真实后端主链 | -| `lib/runtime/skill_directory_access.dart` | runtime | `SkillDirectoryAccessService` + platform impls | `pickDirectory`, `grant`, platform-specific access methods | file selector / macOS access | skills install/import flows | Cross-platform runtime | runtime only | `Active` | 技能目录访问能力的真实后端 | -| `lib/features/mobile/mobile_shell_workspace.dart` | feature | `MobileWorkspaceLauncherInternal` | workspace entries build via `features.allowedDestinations.contains(...)` | `UiFeatureAccess`, controller | mobile workspace hub | Mobile | mobile manifest | `Gated` | `skills / nodes / agents / mcp / claw_hub` 都在这里被最终筛掉或放行 | - -## Mobile Workspace - -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `lib/features/mobile/mobile_shell_core.dart` | feature | `MobileShell`, `MobileShellStateInternal`, `MobileShellTab` | `tabForDestinationInternal`, `selectTabInternal`, `buildCurrentPageInternal`, `showPairingGuidePageFlowInternal`, `showMobileSafeSheetInternal` | `AppController`, `workspace_page_registry`, feature manifest | iOS + Android | mobile shell | mobile manifest + surface mapping | `Active` | 移动端主壳层 | -| `lib/features/mobile/mobile_shell_workspace.dart` | feature | `MobileWorkspaceLauncherInternal` | workspace entry filtering and hub build | `UiFeatureAccess`, controller stats | `MobileShell` | iOS + Android | mobile manifest | `Active` | 工作区入口聚合面 | -| `lib/features/mobile/mobile_shell_nav.dart` | feature | `BottomPillNavInternal` | bottom nav build | `MobileShellTab` state | `MobileShell` | iOS + Android | direct route | `Active` | 移动底部导航壳 | -| `lib/features/mobile/mobile_shell_sheet.dart` | feature | `MobileSafeSheetInternal` | connection/health sheet build | controller runtime state | `MobileShell` | iOS + Android | direct route | `Active` | 移动安全/连接抽屉面 | -| `lib/features/mobile/mobile_shell_strip.dart` | feature | `MobileSafeStripInternal` | top strip build | controller runtime state | `MobileShell` | iOS + Android | direct route | `Active` | 移动顶部状态条 | -| `lib/features/mobile/mobile_gateway_pairing_guide_page.dart` | feature | `MobileGatewayPairingGuidePage`, `_MobileGatewayQrScannerPageState` | pairing guide + QR setup flow | controller connect/setup-code APIs | `MobileShell` | iOS + Android | direct route | `Active` | bridge 配对引导页 | - -## Feature Manifest Fallback - -| Path | Layer | Primary Class / Extension | Key Functions / Methods | Depends On | Used By | Surface | Gate | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `config/feature_flags.yaml` | app | manifest source file | flag definitions by platform/module/feature | YAML loader | `UiFeatureManifestLoader` | Cross-platform | direct route | `Active` | 仓库主 manifest 源 | -| `lib/app/ui_feature_manifest_core.dart` | app | `UiFeatureManifest`, `UiFeatureAccess`, `UiFeatureManifestLoader` | `forPlatform`, `allowedDestinations`, `availableSettingsTabs`, `sanitizeExecutionTarget`, `load` | manifest YAML, runtime models | `AppController.featuresFor()` | Cross-platform | direct route | `Active` | 解析与运行时访问层 | -| `lib/app/ui_feature_manifest_fallback.dart` | app | `fallbackUiFeatureManifestYamlInternal` | embedded fallback YAML | `UiFeatureManifest.fromYamlString` | loader fallback path | Cross-platform | direct route | `Active` | fallback 定义仍保留完整多平台矩阵 | -| `lib/app/workspace_page_registry.dart` | app | page registry | `workspacePageSpecsInternal`, `buildWorkspacePage` | feature pages | shells | Cross-platform | surface mapping | `Active` | manifest 并不自动裁剪 registry | -| `lib/app/app_shell_desktop.dart` | app | `AppShell` shell filter | `_desktopDestinations`, `_mobileDestinations` | controller capabilities, registry | root shell | Desktop + Mobile | surface mapping | `Active` | 当前真实 surface 比 manifest/registry 更窄 | - -## Five-Platform Architecture Review - -| Platform | Current Shape | Architecture Review | Recommendation | -| --- | --- | --- | --- | -| `macOS` | 最完整的 desktop runtime 宿主;assistant + settings 是当前真实桌面主链 | 本地 workspace 绑定、external ACP、bridge 合同链、multi-agent 都优先围绕 macOS 成熟 | 把 macOS 明确设为 desktop reference platform,并补一条端到端 smoke baseline:assistant send -> ACP routing -> working directory -> artifact/result | -| `Linux` | 共享 desktop Flutter 壳,但未见与 macOS 同强度的平台专项收口 | 进程启动、路径、secure storage、文件选择、CLI 挂载更容易出现平台漂移 | 把 `DesktopPlatformService`、路径规范化、CLI 启动能力做成显式 Linux 验证层,补最小功能矩阵测试 | -| `Windows` | 共享 desktop Flutter 壳,但 shell quoting / path separator 风险最高 | task thread working directory、subprocess 参数转义、存储后端兼容性是主要风险点 | 为 Windows 增加工作目录/命令转义专项验证,避免把 macOS 假设直接推广到 Windows | -| `iOS` | 移动端主形态是 bridge thin client;本地 runtime 默认关闭 | 当前强项是配对、设置、账户、bridge setup code;弱项是 dormant workspace 目的地仍保留在模型里 | 保持 iOS 只承载 assistant + workspace hub + settings 主链,并把 dormant destinations 从壳层枚举进一步剥离 | -| `Android` | 与 iOS 共用 mobile shell,但扫描/权限/系统行为波动更大 | QR pairing、剪贴板、文件选择、通知/后台行为更容易受系统差异影响 | 把扫码、setup-code、连接恢复做成 Android 专项回归集合,确保 bridge thin-client 路线稳定 | +- `InstancesController` +- `ConnectorsController` ## Architecture Review Suggestions -1. **统一 surface 单一事实源** - 目前 `feature_flags.yaml`、`UiFeatureAccess.destinationMappingsInternal`、`workspace_page_registry.dart`、`AppShell._desktopDestinations` 同时参与裁剪。建议收敛成“manifest -> access -> shell”单链,registry 只保留已允许的规格,避免同一页面在三个地方各自决定是否可达。 - -2. **显式区分“当前 surface”与“仓库保留页”** - `TasksPage`、`SkillsPage`、`ModulesPage`、`McpServerPage`、`ClawHubPage` 目前都属于“代码存在,但当前主壳层不主推”的状态。建议在目录或文档上明确 `current` / `dormant` / `legacy-present`,降低维护误判。 - -3. **把 runtime core 与 page shell 拆开评审** - `Agents`、`MCP/ACP`、`Skills` 的真实主链大量在 `lib/runtime` 与 `AppControllerDesktop*` 扩展里,而不在页面壳层。后续评审应以 transport / controller / protocol 为主,不要被 `ModulesPage` 这类聚合页误导。 - -4. **确认 ClawHub 的产品定位** - 当前 `ClawHubPage` 更像一个本地命令台 / placeholder shell,而不是与 `SkillsPage` 同等级的真实数据面。建议要么升级为真实 marketplace backend 面,要么明确标记为 legacy tool shell。 - -5. **让生成文档与当前 manifest 同步** - 仓库已有 `docs/plans/xworkmate-ui-feature-matrix.md`,但它描述的 flag 状态已经落后于当前实现。建议把 feature matrix / inventory 变成可重复生成文档,避免“文档说 enabled,壳层却不显示”的结构漂移。 - -## Conclusion: 主链 vs 受限 vs 兼容 - -- `主链 / Active`:`Assistant`、`Settings`、`MCP/ACP runtime`、`Agent runtime core`、`Mobile Workspace`、`Feature Manifest Fallback` -- `受限 / Gated`:`TasksPage`、`SkillsPage`、`ModulesPage`、`McpServerPage` 以及 mobile workspace 中的 `skills/nodes/agents/mcp_server/claw_hub` -- `兼容壳 / Alias`:`navigateTo(aiGateway|secrets)` -> `openSettings(gateway)`、`WorkspaceDestination.account` -> `Settings` -- `陈旧残留 / Legacy-present`:`ClawHubPage` 及其命令台式实现、仍保留在 registry 但不属于当前桌面主页面栈的页面规格 - -对实现者最重要的结论只有一条:**当前仓库的真实主链不是“所有页面都还在线”,而是“页面、manifest、registry、shell 四层并存,真正当前可达的 surface 已经明显窄于仓库残留代码面”。** +- 继续坚持 `feature_flags.yaml -> UiFeatureManifest/AppCapabilities -> Shell/Registry` 的单一 surface 事实源,不再引入第二套 alias 或 dormant registry。 +- `xworkmate-app` 不再维护独立模块壳;任何新的 bridge 能力都只能落到 `assistant` 或 `settings`,不能恢复 `tasks/modules/...` 独立 page matrix。 +- provider、routing、bridge endpoint、managed account sync 的真源继续归 `xworkmate-bridge` 合同与同步链拥有,app 只做消费与最小本地编排。 +- 不再维护兼容 alias、休眠 destination、伪模块矩阵;发现新的 `legacy / fallback / compat` 残留时,默认动作仍然是删除而不是保留占位。 diff --git a/lib/app/app_capabilities.dart b/lib/app/app_capabilities.dart index de591ce3..b9fbc0be 100644 --- a/lib/app/app_capabilities.dart +++ b/lib/app/app_capabilities.dart @@ -8,7 +8,6 @@ class AppCapabilities { required this.supportsLocalGateway, required this.supportsRelayGateway, required this.supportsDesktopRuntime, - required this.supportsDiagnostics, }); final Set allowedDestinations; @@ -16,7 +15,6 @@ class AppCapabilities { final bool supportsLocalGateway; final bool supportsRelayGateway; final bool supportsDesktopRuntime; - final bool supportsDiagnostics; bool supportsDestination(WorkspaceDestination destination) { return allowedDestinations.contains(destination); @@ -29,7 +27,6 @@ class AppCapabilities { supportsLocalGateway: access.supportsLocalGateway, supportsRelayGateway: access.supportsRelayGateway, supportsDesktopRuntime: access.supportsDesktopRuntime, - supportsDiagnostics: access.supportsDiagnostics, ); } } diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index e4e9e96c..fd115b6c 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -166,15 +166,9 @@ class AppController extends ChangeNotifier { chatControllerInternal = GatewayChatController( runtimeCoordinatorInternal.gateway, ); - instancesControllerInternal = InstancesController( - runtimeCoordinatorInternal.gateway, - ); skillsControllerInternal = SkillsController( runtimeCoordinatorInternal.gateway, ); - connectorsControllerInternal = ConnectorsController( - runtimeCoordinatorInternal.gateway, - ); modelsControllerInternal = ModelsController( runtimeCoordinatorInternal.gateway, settingsControllerInternal, @@ -253,9 +247,7 @@ class AppController extends ChangeNotifier { agentsControllerInternal.dispose(); sessionsControllerInternal.dispose(); chatControllerInternal.dispose(); - instancesControllerInternal.dispose(); skillsControllerInternal.dispose(); - connectorsControllerInternal.dispose(); modelsControllerInternal.dispose(); cronJobsControllerInternal.dispose(); devicesControllerInternal.dispose(); @@ -279,9 +271,7 @@ class AppController extends ChangeNotifier { late final GatewayAgentsController agentsControllerInternal; late final GatewaySessionsController sessionsControllerInternal; late final GatewayChatController chatControllerInternal; - late final InstancesController instancesControllerInternal; late final SkillsController skillsControllerInternal; - late final ConnectorsController connectorsControllerInternal; late final ModelsController modelsControllerInternal; late final CronJobsController cronJobsControllerInternal; late final DevicesController devicesControllerInternal; @@ -330,10 +320,7 @@ class AppController extends ChangeNotifier { WorkspaceDestination destinationInternal = WorkspaceDestination.assistant; ThemeMode themeModeInternal = ThemeMode.light; AppSidebarState sidebarStateInternal = AppSidebarState.expanded; - ModulesTab modulesTabInternal = ModulesTab.nodes; - SecretsTab secretsTabInternal = SecretsTab.vault; - AiGatewayTab aiGatewayTabInternal = AiGatewayTab.models; - SettingsTab settingsTabInternal = SettingsTab.general; + SettingsTab settingsTabInternal = SettingsTab.gateway; SettingsDetailPage? settingsDetailInternal; SettingsNavigationContext? settingsNavigationContextInternal; DetailPanelData? detailPanelInternal; @@ -402,9 +389,6 @@ class AppController extends ChangeNotifier { ); ThemeMode get themeMode => themeModeInternal; AppSidebarState get sidebarState => sidebarStateInternal; - ModulesTab get modulesTab => modulesTabInternal; - SecretsTab get secretsTab => secretsTabInternal; - AiGatewayTab get aiGatewayTab => aiGatewayTabInternal; SettingsTab get settingsTab => settingsTabInternal; SettingsDetailPage? get settingsDetail => settingsDetailInternal; SettingsNavigationContext? get settingsNavigationContext => @@ -451,9 +435,7 @@ class AppController extends ChangeNotifier { MultiAgentMountManager get multiAgentMountManager => multiAgentMountManagerInternal; GatewayChatController get chatController => chatControllerInternal; - InstancesController get instancesController => instancesControllerInternal; SkillsController get skillsController => skillsControllerInternal; - ConnectorsController get connectorsController => connectorsControllerInternal; ModelsController get modelsController => modelsControllerInternal; CronJobsController get cronJobsController => cronJobsControllerInternal; DevicesController get devicesController => devicesControllerInternal; @@ -487,11 +469,7 @@ class AppController extends ChangeNotifier { sessionsControllerInternal.sessions; List get assistantSessions => assistantSessionsInternal(); - List get instances => - instancesControllerInternal.items; List get skills => skillsControllerInternal.items; - List get connectors => - connectorsControllerInternal.items; List get models => modelsControllerInternal.items; List get cronJobs => cronJobsControllerInternal.items; GatewayDevicePairingList get devices => devicesControllerInternal.items; @@ -697,9 +675,6 @@ class AppController extends ChangeNotifier { void navigateHome() => AppControllerDesktopNavigation(this).navigateHome(); - void openModules({ModulesTab tab = ModulesTab.nodes}) => - AppControllerDesktopNavigation(this).openModules(tab: tab); - void openSettings({ SettingsTab tab = SettingsTab.gateway, SettingsDetailPage? detail, diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart index badbb2d6..0682be39 100644 --- a/lib/app/app_controller_desktop_gateway.dart +++ b/lib/app/app_controller_desktop_gateway.dart @@ -239,9 +239,7 @@ extension AppControllerDesktopGateway on AppController { await agentsControllerInternal.refresh(); await sessionsControllerInternal.refresh(); chatControllerInternal.clear(); - await instancesControllerInternal.refresh(); await skillsControllerInternal.refresh(); - await connectorsControllerInternal.refresh(); await modelsControllerInternal.refresh(); await cronJobsControllerInternal.refresh(); devicesControllerInternal.clear(); @@ -278,13 +276,11 @@ extension AppControllerDesktopGateway on AppController { await refreshGatewayHealth(); await refreshAgents(); await refreshSessions(); - await instancesControllerInternal.refresh(); await skillsControllerInternal.refresh( agentId: agentsControllerInternal.selectedAgentId.isEmpty ? null : agentsControllerInternal.selectedAgentId, ); - await connectorsControllerInternal.refresh(); await modelsControllerInternal.refresh(); await cronJobsControllerInternal.refresh(); await devicesControllerInternal.refresh(quiet: true); diff --git a/lib/app/app_controller_desktop_navigation.dart b/lib/app/app_controller_desktop_navigation.dart index 12f3474b..72f7f650 100644 --- a/lib/app/app_controller_desktop_navigation.dart +++ b/lib/app/app_controller_desktop_navigation.dart @@ -50,29 +50,17 @@ extension AppControllerDesktopNavigation on AppController { if (!capabilities.supportsDestination(destination)) { return; } - if (destination == WorkspaceDestination.aiGateway || - destination == WorkspaceDestination.secrets) { - openSettings(tab: SettingsTab.gateway); - return; - } - final nextModulesTab = switch (destination) { - WorkspaceDestination.nodes => ModulesTab.nodes, - WorkspaceDestination.agents => ModulesTab.agents, - _ => modulesTabInternal, - }; final shouldClearSettingsDrillIn = settingsDetailInternal != null || settingsNavigationContextInternal != null; final changed = destinationInternal != destination || detailPanelInternal != null || - shouldClearSettingsDrillIn || - nextModulesTab != modulesTabInternal; + shouldClearSettingsDrillIn; if (!changed) { return; } destinationInternal = destination; - modulesTabInternal = nextModulesTab; settingsDetailInternal = null; settingsNavigationContextInternal = null; detailPanelInternal = null; @@ -107,74 +95,6 @@ extension AppControllerDesktopNavigation on AppController { } } - void openModules({ModulesTab tab = ModulesTab.nodes}) { - if (tab == ModulesTab.gateway) { - openSettings(tab: SettingsTab.gateway); - return; - } - final destination = tab == ModulesTab.agents - ? WorkspaceDestination.agents - : WorkspaceDestination.nodes; - if (!capabilities.supportsDestination(destination)) { - return; - } - final changed = - destinationInternal != destination || - modulesTabInternal != tab || - detailPanelInternal != null || - settingsDetailInternal != null || - settingsNavigationContextInternal != null; - if (!changed) { - return; - } - destinationInternal = destination; - modulesTabInternal = tab; - detailPanelInternal = null; - settingsDetailInternal = null; - settingsNavigationContextInternal = null; - notifyListeners(); - } - - void setModulesTab(ModulesTab tab) { - if (modulesTabInternal == tab) { - return; - } - modulesTabInternal = tab; - notifyListeners(); - } - - void openSecrets({SecretsTab tab = SecretsTab.vault}) { - if (!capabilities.supportsDestination(WorkspaceDestination.settings)) { - return; - } - secretsTabInternal = tab; - openSettings(tab: SettingsTab.gateway); - } - - void setSecretsTab(SecretsTab tab) { - if (secretsTabInternal == tab) { - return; - } - secretsTabInternal = tab; - notifyListeners(); - } - - void openAiGateway({AiGatewayTab tab = AiGatewayTab.models}) { - if (!capabilities.supportsDestination(WorkspaceDestination.settings)) { - return; - } - aiGatewayTabInternal = tab; - openSettings(tab: SettingsTab.gateway); - } - - void setAiGatewayTab(AiGatewayTab tab) { - if (aiGatewayTabInternal == tab) { - return; - } - aiGatewayTabInternal = tab; - notifyListeners(); - } - void openSettings({ SettingsTab tab = SettingsTab.gateway, SettingsDetailPage? detail, diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index f9b2ce81..dbc5ac3b 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -470,9 +470,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { agentsControllerInternal.addListener(relayChildChangeInternal); sessionsControllerInternal.addListener(relayChildChangeInternal); chatControllerInternal.addListener(relayChildChangeInternal); - instancesControllerInternal.addListener(relayChildChangeInternal); skillsControllerInternal.addListener(relayChildChangeInternal); - connectorsControllerInternal.addListener(relayChildChangeInternal); modelsControllerInternal.addListener(relayChildChangeInternal); cronJobsControllerInternal.addListener(relayChildChangeInternal); devicesControllerInternal.addListener(relayChildChangeInternal); @@ -488,9 +486,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { agentsControllerInternal.removeListener(relayChildChangeInternal); sessionsControllerInternal.removeListener(relayChildChangeInternal); chatControllerInternal.removeListener(relayChildChangeInternal); - instancesControllerInternal.removeListener(relayChildChangeInternal); skillsControllerInternal.removeListener(relayChildChangeInternal); - connectorsControllerInternal.removeListener(relayChildChangeInternal); modelsControllerInternal.removeListener(relayChildChangeInternal); cronJobsControllerInternal.removeListener(relayChildChangeInternal); devicesControllerInternal.removeListener(relayChildChangeInternal); diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 2c3091a5..81a9e3eb 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -321,11 +321,6 @@ extension AppControllerDesktopThreadSessions on AppController { String gatewayAddressLabelInternal(GatewayConnectionProfile profile) => gatewayAddressLabelThreadSessionInternal(profile); - List get secretReferences => - settingsControllerInternal.buildSecretReferences(); - List get secretAuditTrail => - settingsControllerInternal.auditTrail; - List get runtimeLogs => runtimeInternal.logs; List get assistantNavigationDestinations => normalizeAssistantNavigationDestinations( appUiState.assistantNavigationDestinations, diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 98f83139..e552541b 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -34,9 +34,6 @@ class _AppShellState extends State { static const _mobileDestinations = [ WorkspaceDestination.assistant, - WorkspaceDestination.tasks, - WorkspaceDestination.skills, - WorkspaceDestination.secrets, WorkspaceDestination.settings, ]; @@ -189,10 +186,7 @@ class _AppShellState extends State { final showPinnedDetail = controller.detailPanel != null && constraints.maxWidth > 1280; - final mobileDestination = - controller.destination == WorkspaceDestination.account - ? WorkspaceDestination.settings - : controller.destination; + final mobileDestination = controller.destination; final availableMobileDestinations = _mobileDestinations .where(controller.capabilities.supportsDestination) .toList(growable: false); @@ -320,9 +314,10 @@ class _AppShellState extends State { onExpandFromCollapsed: () => _toggleSidebarVisibility(controller), onOpenHome: controller.navigateHome, - onOpenAccount: () => controller.navigateTo( - WorkspaceDestination.account, - ), + onOpenAccount: () => + controller.openSettings( + tab: SettingsTab.gateway, + ), onOpenThemeToggle: () => controller.setThemeMode( controller.themeMode == ThemeMode.dark @@ -500,9 +495,6 @@ class _AppShellState extends State { WorkspaceDestination _resolveDesktopDestination( WorkspaceDestination destination, ) { - if (destination == WorkspaceDestination.account) { - return WorkspaceDestination.settings; - } if (_desktopDestinations.contains(destination)) { return destination; } diff --git a/lib/app/app_store_policy.dart b/lib/app/app_store_policy.dart index b33bf977..f08d2054 100644 --- a/lib/app/app_store_policy.dart +++ b/lib/app/app_store_policy.dart @@ -27,38 +27,6 @@ UiFeatureManifest applyAppleAppStorePolicy( var next = manifest; final disabledPaths = <(UiFeaturePlatform, String, String)>[ - ( - hostPlatform, - 'navigation', - _featureKeyLeaf(UiFeatureKeys.navigationAgents), - ), - ( - hostPlatform, - 'navigation', - _featureKeyLeaf(UiFeatureKeys.navigationMcpServer), - ), - ( - hostPlatform, - 'navigation', - _featureKeyLeaf(UiFeatureKeys.navigationClawHub), - ), - (hostPlatform, 'workspace', _featureKeyLeaf(UiFeatureKeys.workspaceAgents)), - ( - hostPlatform, - 'workspace', - _featureKeyLeaf(UiFeatureKeys.workspaceMcpServer), - ), - ( - hostPlatform, - 'workspace', - _featureKeyLeaf(UiFeatureKeys.workspaceClawHub), - ), - (hostPlatform, 'settings', _featureKeyLeaf(UiFeatureKeys.settingsAgents)), - ( - hostPlatform, - 'settings', - _featureKeyLeaf(UiFeatureKeys.settingsExperimental), - ), ( hostPlatform, 'settings', diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index f7cd5ba6..217f7d2a 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -36,26 +36,7 @@ UiFeaturePlatform resolveUiFeaturePlatformFromContext(BuildContext context) { abstract final class UiFeatureKeys { static const navigationAssistant = 'navigation.assistant'; - static const navigationTasks = 'navigation.tasks'; - static const navigationWorkspace = 'navigation.workspace'; - static const navigationSkills = 'navigation.skills'; - static const navigationNodes = 'navigation.nodes'; - static const navigationAgents = 'navigation.agents'; - static const navigationMcpServer = 'navigation.mcp_server'; - static const navigationClawHub = 'navigation.claw_hub'; - static const navigationSecrets = 'navigation.secrets'; - static const navigationAiGateway = 'navigation.ai_gateway'; static const navigationSettings = 'navigation.settings'; - static const navigationAccount = 'navigation.account'; - - static const workspaceSkills = 'workspace.skills'; - static const workspaceNodes = 'workspace.nodes'; - static const workspaceAgents = 'workspace.agents'; - static const workspaceMcpServer = 'workspace.mcp_server'; - static const workspaceClawHub = 'workspace.claw_hub'; - static const workspaceConnectors = 'workspace.connectors'; - static const workspaceAiGateway = 'workspace.ai_gateway'; - static const workspaceAccount = 'workspace.account'; static const assistantDirectAi = 'assistant.direct_ai'; static const assistantLocalGateway = 'assistant.local_gateway'; @@ -64,8 +45,6 @@ abstract final class UiFeatureKeys { static const assistantMultiAgent = 'assistant.multi_agent'; static const assistantLocalRuntime = 'assistant.local_runtime'; - static const settingsGeneral = 'settings.general'; - static const settingsWorkspace = 'settings.workspace'; static const settingsGateway = 'settings.gateway'; static const settingsAccountAccess = 'settings.account_access'; static const settingsVaultServer = 'settings.vault_server'; @@ -74,11 +53,6 @@ abstract final class UiFeatureKeys { static const settingsGatewayAdvancedCustomMode = 'settings.gateway_advanced_custom_mode'; static const settingsGatewaySetupCode = 'settings.gateway_setup_code'; - static const settingsAgents = 'settings.agents'; - static const settingsAppearance = 'settings.appearance'; - static const settingsDiagnostics = 'settings.diagnostics'; - static const settingsExperimental = 'settings.experimental'; - static const settingsAbout = 'settings.about'; static const settingsExperimentalCanvas = 'settings.experimental_canvas'; static const settingsExperimentalBridge = 'settings.experimental_bridge'; static const settingsExperimentalDebug = 'settings.experimental_debug'; @@ -380,16 +354,7 @@ class UiFeatureAccess { >{ UiFeaturePlatform.mobile: { UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant, - UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks, - UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets, UiFeatureKeys.navigationSettings: WorkspaceDestination.settings, - UiFeatureKeys.workspaceSkills: WorkspaceDestination.skills, - UiFeatureKeys.workspaceNodes: WorkspaceDestination.nodes, - UiFeatureKeys.workspaceAgents: WorkspaceDestination.agents, - UiFeatureKeys.workspaceMcpServer: WorkspaceDestination.mcpServer, - UiFeatureKeys.workspaceClawHub: WorkspaceDestination.clawHub, - UiFeatureKeys.workspaceAiGateway: WorkspaceDestination.aiGateway, - UiFeatureKeys.workspaceAccount: WorkspaceDestination.account, }, UiFeaturePlatform.desktop: { UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant, @@ -397,11 +362,7 @@ class UiFeatureAccess { }, UiFeaturePlatform.web: { UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant, - UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks, - UiFeatureKeys.navigationSkills: WorkspaceDestination.skills, - UiFeatureKeys.navigationNodes: WorkspaceDestination.nodes, - UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets, - UiFeatureKeys.navigationAiGateway: WorkspaceDestination.aiGateway, + UiFeatureKeys.navigationSettings: WorkspaceDestination.settings, }, }; @@ -439,9 +400,7 @@ class UiFeatureAccess { return allowed; } - bool get showsWorkspaceHub => - platform == UiFeaturePlatform.mobile && - isEnabledPath(UiFeatureKeys.navigationWorkspace); + bool get showsWorkspaceHub => false; bool get supportsDirectAi => isEnabledPath(UiFeatureKeys.assistantDirectAi); @@ -461,9 +420,6 @@ class UiFeatureAccess { platform == UiFeaturePlatform.desktop && isEnabledPath(UiFeatureKeys.assistantLocalRuntime); - bool get supportsDiagnostics => - isEnabledPath(UiFeatureKeys.settingsDiagnostics); - bool get supportsAccountAccess => isEnabledPath(UiFeatureKeys.settingsAccountAccess); diff --git a/lib/app/workspace_navigation.dart b/lib/app/workspace_navigation.dart index d08e34ce..d3f4a0c3 100644 --- a/lib/app/workspace_navigation.dart +++ b/lib/app/workspace_navigation.dart @@ -58,22 +58,6 @@ void openSettingsNavigationContext( AppController controller, SettingsNavigationContext context, ) { - if (context.modulesTab != null) { - if (context.modulesTab == ModulesTab.gateway) { - controller.openSettings(tab: SettingsTab.gateway); - return; - } - controller.openModules(tab: context.modulesTab!); - return; - } - if (context.secretsTab != null) { - controller.openSettings(tab: SettingsTab.gateway); - return; - } - if (context.aiGatewayTab != null) { - controller.openSettings(tab: SettingsTab.gateway); - return; - } if (context.settingsTab != null || context.destination == WorkspaceDestination.settings) { controller.openSettings(tab: context.settingsTab ?? SettingsTab.gateway); diff --git a/lib/app/workspace_page_registry.dart b/lib/app/workspace_page_registry.dart index 6de266a2..39072140 100644 --- a/lib/app/workspace_page_registry.dart +++ b/lib/app/workspace_page_registry.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; import '../features/assistant/assistant_page.dart'; -import '../features/claw_hub/claw_hub_page.dart'; -import '../features/mcp_server/mcp_server_page.dart'; -import '../features/modules/modules_page.dart'; import '../features/settings/settings_page.dart'; -import '../features/skills/skills_page.dart'; -import '../features/tasks/tasks_page.dart'; import '../models/app_models.dart'; import 'app_controller.dart'; @@ -45,74 +40,6 @@ workspacePageSpecsInternal = { showStandaloneTaskRail: false, ), ), - WorkspaceDestination.tasks: WorkspacePageSpec( - destination: WorkspaceDestination.tasks, - desktopBuilder: (controller, onOpenDetail) => - TasksPage(controller: controller, onOpenDetail: onOpenDetail), - mobileBuilder: (controller, onOpenDetail) => - TasksPage(controller: controller, onOpenDetail: onOpenDetail), - ), - WorkspaceDestination.skills: WorkspacePageSpec( - destination: WorkspaceDestination.skills, - desktopBuilder: (controller, onOpenDetail) => - SkillsPage(controller: controller, onOpenDetail: onOpenDetail), - mobileBuilder: (controller, onOpenDetail) => - SkillsPage(controller: controller, onOpenDetail: onOpenDetail), - ), - WorkspaceDestination.nodes: WorkspacePageSpec( - destination: WorkspaceDestination.nodes, - desktopBuilder: (controller, onOpenDetail) => ModulesPage( - controller: controller, - onOpenDetail: onOpenDetail, - initialTab: controller.modulesTab, - ), - mobileBuilder: (controller, onOpenDetail) => ModulesPage( - controller: controller, - onOpenDetail: onOpenDetail, - initialTab: controller.modulesTab, - ), - ), - WorkspaceDestination.agents: WorkspacePageSpec( - destination: WorkspaceDestination.agents, - desktopBuilder: (controller, onOpenDetail) => ModulesPage( - controller: controller, - onOpenDetail: onOpenDetail, - initialTab: controller.modulesTab, - ), - mobileBuilder: (controller, onOpenDetail) => ModulesPage( - controller: controller, - onOpenDetail: onOpenDetail, - initialTab: controller.modulesTab, - ), - ), - WorkspaceDestination.mcpServer: WorkspacePageSpec( - destination: WorkspaceDestination.mcpServer, - desktopBuilder: (controller, onOpenDetail) => - McpServerPage(controller: controller, onOpenDetail: onOpenDetail), - mobileBuilder: (controller, onOpenDetail) => - McpServerPage(controller: controller, onOpenDetail: onOpenDetail), - ), - WorkspaceDestination.clawHub: WorkspacePageSpec( - destination: WorkspaceDestination.clawHub, - desktopBuilder: (controller, onOpenDetail) => - ClawHubPage(controller: controller, onOpenDetail: onOpenDetail), - mobileBuilder: (controller, onOpenDetail) => - ClawHubPage(controller: controller, onOpenDetail: onOpenDetail), - ), - WorkspaceDestination.secrets: WorkspacePageSpec( - destination: WorkspaceDestination.secrets, - desktopBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - mobileBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - ), - WorkspaceDestination.aiGateway: WorkspacePageSpec( - destination: WorkspaceDestination.aiGateway, - desktopBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - mobileBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - ), WorkspaceDestination.settings: WorkspacePageSpec( destination: WorkspaceDestination.settings, desktopBuilder: (controller, onOpenDetail) => SettingsPage( @@ -128,13 +55,6 @@ workspacePageSpecsInternal = { navigationContext: controller.settingsNavigationContext, ), ), - WorkspaceDestination.account: WorkspacePageSpec( - destination: WorkspaceDestination.account, - desktopBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - mobileBuilder: (controller, onOpenDetail) => - SettingsPage(controller: controller, initialTab: SettingsTab.gateway), - ), }; Widget buildWorkspacePage({ diff --git a/lib/features/claw_hub/claw_hub_page.dart b/lib/features/claw_hub/claw_hub_page.dart deleted file mode 100644 index e6fab4eb..00000000 --- a/lib/features/claw_hub/claw_hub_page.dart +++ /dev/null @@ -1,503 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../theme/app_palette.dart'; -import '../../widgets/section_header.dart'; -import '../../widgets/surface_card.dart'; -import '../../widgets/top_bar.dart'; - -class ClawHubPage extends StatefulWidget { - const ClawHubPage({ - super.key, - required this.controller, - required this.onOpenDetail, - }); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - State createState() => ClawHubPageStateInternal(); -} - -class ClawHubPageStateInternal extends State { - final searchControllerInternal = TextEditingController(); - final commandControllerInternal = TextEditingController(); - final scrollControllerInternal = ScrollController(); - final List logsInternal = []; - bool isExecutingInternal = false; - - @override - void dispose() { - searchControllerInternal.dispose(); - commandControllerInternal.dispose(); - scrollControllerInternal.dispose(); - super.dispose(); - } - - void addLogInternal( - String message, { - ClawHubLogType type = ClawHubLogType.info, - }) { - setState(() { - logsInternal.add( - ClawHubLogEntry( - timestamp: DateTime.now(), - message: message, - type: type, - ), - ); - }); - // Auto-scroll to bottom - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollControllerInternal.hasClients) { - scrollControllerInternal.animateTo( - scrollControllerInternal.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - }); - } - - void executeCommandInternal(String input) { - if (input.trim().isEmpty) return; - - addLogInternal('\$ clawhub \$input', type: ClawHubLogType.command); - commandControllerInternal.clear(); - - final parts = input.trim().split(RegExp(r'\s+')); - final command = parts.isNotEmpty ? parts[0] : ''; - final args = parts.length > 1 ? parts.sublist(1) : []; - - switch (command) { - case 'search': - handleSearchInternal(args); - break; - case 'install': - handleInstallInternal(args); - break; - case 'update': - handleUpdateInternal(args); - break; - case 'help': - case '--help': - case '-h': - showHelpInternal(); - break; - default: - addLogInternal( - 'Unknown command: \$command. Type "clawhub help" for available commands.', - type: ClawHubLogType.error, - ); - } - } - - void handleSearchInternal(List args) { - final query = args.join(' '); - if (query.isEmpty) { - addLogInternal( - 'Usage: clawhub search ""', - type: ClawHubLogType.warning, - ); - return; - } - - setState(() => isExecutingInternal = true); - addLogInternal('Searching for "\$query"...'); - - // Simulate search results - Future.delayed(const Duration(milliseconds: 800), () { - setState(() => isExecutingInternal = false); - addLogInternal(''); - addLogInternal('Found 3 packages:', type: ClawHubLogType.success); - addLogInternal(' ├─ skill-analyzer v1.2.0 Code analysis skill'); - addLogInternal(' ├─ feishu-connector v2.1.3 Feishu integration'); - addLogInternal( - ' └─ azure-deploy v3.0.1 Azure deployment helper', - ); - addLogInternal(''); - addLogInternal('Use "clawhub install " to install a package.'); - }); - } - - void handleInstallInternal(List args) { - if (args.isEmpty) { - addLogInternal( - 'Usage: clawhub install ', - type: ClawHubLogType.warning, - ); - return; - } - - setState(() => isExecutingInternal = true); - addLogInternal('Installing ${args[0]}...'); - - Future.delayed(const Duration(milliseconds: 1200), () { - setState(() => isExecutingInternal = false); - addLogInternal( - '✓ Successfully installed ${args[0]}', - type: ClawHubLogType.success, - ); - addLogInternal(' Location: ~/.clawhub/skills/${args[0]}'); - addLogInternal(' Run "clawhub update" to check for updates.'); - }); - } - - void handleUpdateInternal(List args) { - final isAll = args.contains('--all') || args.contains('-a'); - final slug = isAll ? null : (args.isNotEmpty ? args[0] : null); - - setState(() => isExecutingInternal = true); - - if (isAll) { - addLogInternal('Checking for updates...'); - Future.delayed(const Duration(milliseconds: 1000), () { - setState(() => isExecutingInternal = false); - addLogInternal( - '✓ All packages are up to date', - type: ClawHubLogType.success, - ); - }); - } else if (slug != null) { - addLogInternal('Updating \$slug...'); - Future.delayed(const Duration(milliseconds: 800), () { - setState(() => isExecutingInternal = false); - addLogInternal( - '✓ \$slug updated to latest version', - type: ClawHubLogType.success, - ); - }); - } else { - addLogInternal( - 'Usage: clawhub update or clawhub update --all', - type: ClawHubLogType.warning, - ); - setState(() => isExecutingInternal = false); - } - } - - void showHelpInternal() { - addLogInternal(''); - addLogInternal('ClawHub Package Manager', type: ClawHubLogType.success); - addLogInternal('Usage: clawhub [options]'); - addLogInternal(''); - addLogInternal('Commands:'); - addLogInternal(' search "" Search for packages'); - addLogInternal(' install Install a package'); - addLogInternal(' update Update a specific package'); - addLogInternal(' update --all Update all packages'); - addLogInternal(' help Show this help message'); - addLogInternal(''); - } - - @override - Widget build(BuildContext context) { - final palette = context.palette; - - return AnimatedBuilder( - animation: widget.controller, - builder: (context, _) { - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TopBar( - breadcrumbs: [ - AppBreadcrumbItem( - label: appText('主页', 'Home'), - icon: Icons.home_rounded, - onTap: widget.controller.navigateHome, - ), - const AppBreadcrumbItem(label: 'ClawHub'), - ], - title: 'ClawHub', - subtitle: appText( - 'NPM 风格的包管理中心,支持搜索、安装和更新 Skills。', - 'NPM-style package manager for skills.', - ), - ), - const SizedBox(height: 24), - SectionHeader( - title: appText('终端', 'Terminal'), - subtitle: appText('执行终端命令', 'Execute terminal commands'), - ), - const SizedBox(height: 12), - SurfaceCard( - child: Container( - height: 400, - decoration: BoxDecoration( - color: palette.surfaceSecondary.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - // Terminal header - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - ), - ), - child: Row( - children: [ - Icon( - Icons.terminal_rounded, - size: 16, - color: palette.textSecondary, - ), - const SizedBox(width: 8), - Text( - 'clawhub', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: palette.textSecondary, - ), - ), - const Spacer(), - if (isExecutingInternal) - SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - color: palette.accent, - ), - ), - ], - ), - ), - // Terminal output - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - child: ListView.builder( - controller: scrollControllerInternal, - itemCount: logsInternal.length, - itemBuilder: (context, index) { - final log = logsInternal[index]; - return LogLineInternal( - entry: log, - palette: palette, - ); - }, - ), - ), - ), - // Command input - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - border: Border( - top: BorderSide(color: palette.strokeSoft), - ), - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(12), - ), - ), - child: Row( - children: [ - Text( - '\$', - style: TextStyle( - fontFamily: 'monospace', - fontSize: 14, - fontWeight: FontWeight.w600, - color: palette.accent, - ), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: commandControllerInternal, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 14, - color: palette.textPrimary, - ), - decoration: InputDecoration( - hintText: appText( - '输入命令 (search, install, update)', - 'Type command (search, install, update)', - ), - hintStyle: TextStyle( - fontFamily: 'monospace', - fontSize: 14, - color: palette.textMuted, - ), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - onSubmitted: executeCommandInternal, - ), - ), - IconButton( - icon: Icon( - Icons.send_rounded, - size: 18, - color: palette.accent, - ), - onPressed: () => executeCommandInternal( - commandControllerInternal.text, - ), - visualDensity: VisualDensity.compact, - ), - ], - ), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - SectionHeader( - title: appText('快速操作', 'Quick Actions'), - subtitle: appText('常用操作快捷入口', 'Quick access to common actions'), - ), - const SizedBox(height: 12), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - QuickActionButtonInternal( - icon: Icons.search_rounded, - label: appText('搜索技能', 'Search Skills'), - onTap: () => executeCommandInternal('search analytics'), - ), - QuickActionButtonInternal( - icon: Icons.download_rounded, - label: appText('安装技能', 'Install Skill'), - onTap: () => - executeCommandInternal('install example-skill'), - ), - QuickActionButtonInternal( - icon: Icons.update_rounded, - label: appText('更新全部', 'Update All'), - onTap: () => executeCommandInternal('update --all'), - ), - QuickActionButtonInternal( - icon: Icons.help_outline_rounded, - label: appText('查看帮助', 'View Help'), - onTap: () => executeCommandInternal('help'), - ), - ], - ), - ], - ), - ); - }, - ); - } -} - -enum ClawHubLogType { info, command, success, warning, error } - -class ClawHubLogEntry { - final DateTime timestamp; - final String message; - final ClawHubLogType type; - - ClawHubLogEntry({ - required this.timestamp, - required this.message, - required this.type, - }); -} - -class LogLineInternal extends StatelessWidget { - const LogLineInternal({ - super.key, - required this.entry, - required this.palette, - }); - - final ClawHubLogEntry entry; - final AppPalette palette; - - Color get colorInternal { - switch (entry.type) { - case ClawHubLogType.command: - return palette.accent; - case ClawHubLogType.success: - return Colors.green; - case ClawHubLogType.warning: - return Colors.orange; - case ClawHubLogType.error: - return Colors.red; - case ClawHubLogType.info: - return palette.textPrimary; - } - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - entry.message, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 13, - color: colorInternal, - height: 1.4, - ), - ), - ); - } -} - -class QuickActionButtonInternal extends StatelessWidget { - const QuickActionButtonInternal({ - super.key, - required this.icon, - required this.label, - required this.onTap, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - - return Material( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(8), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: palette.accent), - const SizedBox(width: 8), - Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: palette.textPrimary, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/mcp_server/mcp_server_page.dart b/lib/features/mcp_server/mcp_server_page.dart deleted file mode 100644 index 2570d553..00000000 --- a/lib/features/mcp_server/mcp_server_page.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/runtime_models.dart'; -import '../../widgets/surface_card.dart'; -import '../../widgets/top_bar.dart'; - -class McpServerPage extends StatelessWidget { - const McpServerPage({ - super.key, - required this.controller, - required this.onOpenDetail, - }); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - Widget build(BuildContext context) { - final items = controller.connectors; - - return AnimatedBuilder( - animation: controller, - builder: (context, _) { - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TopBar( - breadcrumbs: [ - AppBreadcrumbItem( - label: appText('主页', 'Home'), - icon: Icons.home_rounded, - onTap: controller.navigateHome, - ), - const AppBreadcrumbItem(label: 'MCP Hub'), - ], - title: 'MCP Hub', - subtitle: appText( - '管理 MCP 服务器连接与工具配置。', - 'Manage MCP server connections and tool configurations.', - ), - trailing: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - SizedBox( - width: 220, - child: TextField( - decoration: InputDecoration( - hintText: appText('搜索服务器', 'Search servers'), - prefixIcon: Icon(Icons.search_rounded), - ), - ), - ), - IconButton( - onPressed: () async { - await controller.connectorsController.refresh(); - }, - icon: const Icon(Icons.refresh_rounded), - ), - ], - ), - ), - const SizedBox(height: 24), - if (items.isEmpty) - SurfaceCard( - child: Text( - controller.connection.status == - RuntimeConnectionStatus.connected - ? appText( - '当前没有连接的 MCP 服务器。', - 'No MCP servers connected.', - ) - : appText( - '恢复 xworkmate-bridge 连接后可查看 MCP 服务器。', - 'MCP servers are visible again after xworkmate-bridge reconnects.', - ), - ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: items - .map( - (connector) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SurfaceCard( - onTap: () => onOpenDetail( - DetailPanelData( - title: connector.label, - subtitle: appText('连接器', 'Connector'), - icon: Icons.dns_rounded, - status: StatusInfo( - connector.status, - connector.connected - ? StatusTone.success - : StatusTone.neutral, - ), - description: connector.detailLabel, - meta: connector.meta, - actions: [appText('配置', 'Configure')], - sections: [ - DetailSection( - title: appText('详情', 'Details'), - items: [ - DetailItem( - label: appText('ID', 'ID'), - value: connector.id, - ), - DetailItem( - label: appText('状态', 'Status'), - value: connector.status, - ), - DetailItem( - label: appText('已配置', 'Configured'), - value: connector.configured - ? appText('是', 'Yes') - : appText('否', 'No'), - ), - DetailItem( - label: appText('已启用', 'Enabled'), - value: connector.enabled - ? appText('是', 'Yes') - : appText('否', 'No'), - ), - ], - ), - ], - ), - ), - child: Row( - children: [ - Expanded( - flex: 4, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - connector.label, - style: Theme.of( - context, - ).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - connector.detailLabel, - style: Theme.of( - context, - ).textTheme.bodySmall, - ), - ], - ), - ), - Expanded( - flex: 2, - child: Text( - connector.connected - ? appText('已连接', 'Connected') - : appText('未连接', 'Disconnected'), - ), - ), - const Icon(Icons.chevron_right_rounded), - ], - ), - ), - ), - ) - .toList(), - ), - ], - ), - ); - }, - ); - } -} diff --git a/lib/features/mobile/mobile_shell.dart b/lib/features/mobile/mobile_shell.dart index 08cc6907..c8bea1d9 100644 --- a/lib/features/mobile/mobile_shell.dart +++ b/lib/features/mobile/mobile_shell.dart @@ -1,5 +1,4 @@ export 'mobile_shell_core.dart'; export 'mobile_shell_strip.dart'; export 'mobile_shell_sheet.dart'; -export 'mobile_shell_workspace.dart'; export 'mobile_shell_nav.dart'; diff --git a/lib/features/mobile/mobile_shell_core.dart b/lib/features/mobile/mobile_shell_core.dart index 937d2fba..ffd5d5db 100644 --- a/lib/features/mobile/mobile_shell_core.dart +++ b/lib/features/mobile/mobile_shell_core.dart @@ -1,7 +1,9 @@ // ignore_for_file: unused_import, unnecessary_import import 'dart:async'; + import 'package:flutter/material.dart'; + import '../../app/app_controller.dart'; import '../../app/ui_feature_manifest.dart'; import '../../app/workspace_page_registry.dart'; @@ -12,36 +14,24 @@ import '../../theme/app_palette.dart'; import '../../theme/app_theme.dart'; import '../../widgets/detail_drawer.dart'; import 'mobile_gateway_pairing_guide_page.dart'; -import 'mobile_shell_strip.dart'; -import 'mobile_shell_sheet.dart'; -import 'mobile_shell_workspace.dart'; import 'mobile_shell_nav.dart'; +import 'mobile_shell_sheet.dart'; +import 'mobile_shell_strip.dart'; -enum MobileShellTab { assistant, tasks, workspace, secrets, settings } +enum MobileShellTab { assistant, settings } extension MobileShellTabPresentationInternal on MobileShellTab { String get label => switch (this) { MobileShellTab.assistant => appText('助手', 'Assistant'), - MobileShellTab.tasks => appText('任务', 'Tasks'), - MobileShellTab.workspace => appText('工作区', 'Workspace'), - MobileShellTab.secrets => appText('密钥', 'Secrets'), MobileShellTab.settings => appText('设置', 'Settings'), }; IconData get icon => switch (this) { MobileShellTab.assistant => Icons.chat_bubble_outline_rounded, - MobileShellTab.tasks => Icons.layers_rounded, - MobileShellTab.workspace => Icons.grid_view_rounded, - MobileShellTab.secrets => Icons.key_rounded, MobileShellTab.settings => Icons.settings_rounded, }; } -const tealSoftInternal = Color(0xFFDDF3EF); -const tealLineInternal = Color(0xFF49A892); -const violetSoftInternal = Color(0xFFECE2FF); -const violetLineInternal = Color(0xFF7A61B6); - class MobileShell extends StatefulWidget { const MobileShell({super.key, required this.controller}); @@ -52,92 +42,24 @@ class MobileShell extends StatefulWidget { } class MobileShellStateInternal extends State { - bool showWorkspaceHubInternal = false; - late WorkspaceDestination lastDestinationInternal; - - @override - void initState() { - super.initState(); - lastDestinationInternal = widget.controller.destination; - widget.controller.addListener(handleControllerChangedInternal); - } - - @override - void didUpdateWidget(covariant MobileShell oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller == widget.controller) { - return; - } - oldWidget.controller.removeListener(handleControllerChangedInternal); - lastDestinationInternal = widget.controller.destination; - widget.controller.addListener(handleControllerChangedInternal); - } - - @override - void dispose() { - widget.controller.removeListener(handleControllerChangedInternal); - super.dispose(); - } - - void handleControllerChangedInternal() { - final destination = widget.controller.destination; - if (destination == lastDestinationInternal) { - return; - } - lastDestinationInternal = destination; - if (showWorkspaceHubInternal && mounted) { - setState(() { - showWorkspaceHubInternal = false; - }); - } - } - MobileShellTab tabForDestinationInternal(WorkspaceDestination destination) { return switch (destination) { WorkspaceDestination.assistant => MobileShellTab.assistant, - WorkspaceDestination.tasks => MobileShellTab.tasks, - WorkspaceDestination.skills || - WorkspaceDestination.nodes || - WorkspaceDestination.agents || - WorkspaceDestination.mcpServer || - WorkspaceDestination.clawHub || - WorkspaceDestination.aiGateway => MobileShellTab.workspace, - WorkspaceDestination.secrets => MobileShellTab.secrets, WorkspaceDestination.settings => MobileShellTab.settings, - WorkspaceDestination.account => MobileShellTab.settings, }; } void selectTabInternal(MobileShellTab tab) { switch (tab) { case MobileShellTab.assistant: - setState(() => showWorkspaceHubInternal = false); widget.controller.navigateTo(WorkspaceDestination.assistant); return; - case MobileShellTab.tasks: - setState(() => showWorkspaceHubInternal = false); - widget.controller.navigateTo(WorkspaceDestination.tasks); - return; - case MobileShellTab.workspace: - prefetchMobileSafeStateInternal(); - setState(() => showWorkspaceHubInternal = true); - return; - case MobileShellTab.secrets: - setState(() => showWorkspaceHubInternal = false); - widget.controller.navigateTo(WorkspaceDestination.secrets); - return; case MobileShellTab.settings: - setState(() => showWorkspaceHubInternal = false); widget.controller.navigateTo(WorkspaceDestination.settings); return; } } - void openWorkspaceDestinationInternal(WorkspaceDestination destination) { - setState(() => showWorkspaceHubInternal = false); - widget.controller.navigateTo(destination); - } - void openDetailSheetInternal(DetailPanelData detail) { widget.controller.openDetail(detail); } @@ -157,6 +79,7 @@ class MobileShellStateInternal extends State { rootLabel: appText('移动端', 'Mobile'), destination: WorkspaceDestination.settings, sectionLabel: appText('集成', 'Integrations'), + settingsTab: SettingsTab.gateway, gatewayProfileIndex: kGatewayRemoteProfileIndex, prefersGatewaySetupCode: false, ), @@ -185,6 +108,7 @@ class MobileShellStateInternal extends State { rootLabel: appText('移动端', 'Mobile'), destination: WorkspaceDestination.settings, sectionLabel: appText('集成', 'Integrations'), + settingsTab: SettingsTab.gateway, gatewayProfileIndex: kGatewayRemoteProfileIndex, prefersGatewaySetupCode: true, ), @@ -258,9 +182,6 @@ class MobileShellStateInternal extends State { ); return; } - if (!mounted) { - return; - } final codeController = TextEditingController(); final enteredCode = await showDialog( context: context, @@ -347,18 +268,8 @@ class MobileShellStateInternal extends State { } Widget buildCurrentPageInternal() { - final features = widget.controller.featuresFor(UiFeaturePlatform.mobile); - if (showWorkspaceHubInternal && features.showsWorkspaceHub) { - return MobileWorkspaceLauncherInternal( - controller: widget.controller, - onOpenGatewayConnect: showConnectSheetInternal, - onSelectDestination: openWorkspaceDestinationInternal, - ); - } - - final destination = widget.controller.destination; return buildWorkspacePage( - destination: destination, + destination: widget.controller.destination, controller: widget.controller, onOpenDetail: openDetailSheetInternal, surface: WorkspacePageSurface.mobile, @@ -376,25 +287,16 @@ class MobileShellStateInternal extends State { final availableTabs = [ if (features.isEnabledPath(UiFeatureKeys.navigationAssistant)) MobileShellTab.assistant, - if (features.isEnabledPath(UiFeatureKeys.navigationTasks)) - MobileShellTab.tasks, - if (features.showsWorkspaceHub) MobileShellTab.workspace, - if (features.isEnabledPath(UiFeatureKeys.navigationSecrets)) - MobileShellTab.secrets, if (features.isEnabledPath(UiFeatureKeys.navigationSettings)) MobileShellTab.settings, ]; - final currentTab = showWorkspaceHubInternal - ? MobileShellTab.workspace - : tabForDestinationInternal(widget.controller.destination); + final currentTab = tabForDestinationInternal(widget.controller.destination); final resolvedCurrentTab = availableTabs.contains(currentTab) ? currentTab : (availableTabs.isEmpty ? currentTab : availableTabs.first); - final destinationKey = showWorkspaceHubInternal - ? const ValueKey('mobile-shell-workspace') - : ValueKey( - 'mobile-shell-${widget.controller.destination.name}', - ); + final destinationKey = ValueKey( + 'mobile-shell-${widget.controller.destination.name}', + ); final detailPanel = widget.controller.detailPanel; final palette = context.palette; return Scaffold( diff --git a/lib/features/mobile/mobile_shell_nav.dart b/lib/features/mobile/mobile_shell_nav.dart index 16669a27..c9c9e60c 100644 --- a/lib/features/mobile/mobile_shell_nav.dart +++ b/lib/features/mobile/mobile_shell_nav.dart @@ -15,7 +15,6 @@ import 'mobile_gateway_pairing_guide_page.dart'; import 'mobile_shell_core.dart'; import 'mobile_shell_strip.dart'; import 'mobile_shell_sheet.dart'; -import 'mobile_shell_workspace.dart'; class BottomPillNavInternal extends StatelessWidget { const BottomPillNavInternal({ diff --git a/lib/features/mobile/mobile_shell_sheet.dart b/lib/features/mobile/mobile_shell_sheet.dart index e491730e..8c8d8517 100644 --- a/lib/features/mobile/mobile_shell_sheet.dart +++ b/lib/features/mobile/mobile_shell_sheet.dart @@ -14,7 +14,6 @@ import '../../widgets/detail_drawer.dart'; import 'mobile_gateway_pairing_guide_page.dart'; import 'mobile_shell_core.dart'; import 'mobile_shell_strip.dart'; -import 'mobile_shell_workspace.dart'; import 'mobile_shell_nav.dart'; class MobileSafeSheetInternal extends StatelessWidget { diff --git a/lib/features/mobile/mobile_shell_strip.dart b/lib/features/mobile/mobile_shell_strip.dart index 855c9930..16ff9c61 100644 --- a/lib/features/mobile/mobile_shell_strip.dart +++ b/lib/features/mobile/mobile_shell_strip.dart @@ -14,7 +14,6 @@ import '../../widgets/detail_drawer.dart'; import 'mobile_gateway_pairing_guide_page.dart'; import 'mobile_shell_core.dart'; import 'mobile_shell_sheet.dart'; -import 'mobile_shell_workspace.dart'; import 'mobile_shell_nav.dart'; class MobileSafeStripInternal extends StatelessWidget { diff --git a/lib/features/mobile/mobile_shell_workspace.dart b/lib/features/mobile/mobile_shell_workspace.dart deleted file mode 100644 index 8f205d3c..00000000 --- a/lib/features/mobile/mobile_shell_workspace.dart +++ /dev/null @@ -1,450 +0,0 @@ -// ignore_for_file: unused_import, unnecessary_import - -import 'dart:async'; -import 'package:flutter/material.dart'; -import '../../app/app_controller.dart'; -import '../../app/ui_feature_manifest.dart'; -import '../../app/workspace_page_registry.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/runtime_models.dart'; -import '../../theme/app_palette.dart'; -import '../../theme/app_theme.dart'; -import '../../widgets/detail_drawer.dart'; -import 'mobile_gateway_pairing_guide_page.dart'; -import 'mobile_shell_core.dart'; -import 'mobile_shell_strip.dart'; -import 'mobile_shell_sheet.dart'; -import 'mobile_shell_nav.dart'; - -class MobileWorkspaceLauncherInternal extends StatelessWidget { - const MobileWorkspaceLauncherInternal({ - super.key, - required this.controller, - required this.onOpenGatewayConnect, - required this.onSelectDestination, - }); - - final AppController controller; - final VoidCallback onOpenGatewayConnect; - final ValueChanged onSelectDestination; - - @override - Widget build(BuildContext context) { - final connection = controller.connection; - final palette = context.palette; - final features = controller.featuresFor(UiFeaturePlatform.mobile); - final entries = - [ - WorkspaceEntryInternal( - destination: WorkspaceDestination.skills, - subtitle: appText('技能包与依赖状态', 'Packages and dependency status'), - iconColor: palette.accent, - iconBackground: palette.accentMuted, - ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.nodes, - subtitle: appText('边缘节点与实例', 'Edge nodes and instances'), - iconColor: tealLineInternal, - iconBackground: tealSoftInternal, - ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.agents, - subtitle: appText('代理运行态与配置', 'Agent state and configuration'), - iconColor: palette.warning, - iconBackground: palette.warning.withValues(alpha: 0.12), - ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.mcpServer, - subtitle: appText('MCP 连接与工具注册', 'MCP endpoints and tools'), - iconColor: palette.accent, - iconBackground: palette.accentMuted, - ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.clawHub, - subtitle: appText('技能与模板市场', 'Marketplace and templates'), - iconColor: violetLineInternal, - iconBackground: violetSoftInternal, - ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.aiGateway, - subtitle: appText('模型与代理网关', 'Models and agent gateway'), - iconColor: palette.accent, - iconBackground: palette.accentMuted, - ), - ] - .where( - (entry) => - features.allowedDestinations.contains(entry.destination), - ) - .toList(growable: false); - - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(18, 18, 18, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LauncherHeaderInternal( - title: appText('工作区', 'Workspace'), - subtitle: appText( - 'Android 与 iOS 统一移动入口,集中访问全部核心模块。', - 'Shared mobile entry for Android and iOS with access to all core modules.', - ), - primaryLabel: connection.status == RuntimeConnectionStatus.connected - ? appText('查看连接', 'Connection') - : appText('连接 Bridge', 'Connect Bridge'), - secondaryLabel: appText('返回助手', 'Open Assistant'), - onPrimaryPressed: onOpenGatewayConnect, - onSecondaryPressed: () => - onSelectDestination(WorkspaceDestination.assistant), - ), - const SizedBox(height: 18), - WorkspaceHeroInternal( - connection: connection, - activeAgentName: controller.activeAgentName, - sessionCount: controller.sessions.length, - runningTaskCount: controller.tasksController.running.length, - ), - const SizedBox(height: 18), - LayoutBuilder( - builder: (context, constraints) { - final columns = constraints.maxWidth >= 760 ? 2 : 1; - final width = columns == 2 - ? (constraints.maxWidth - 16) / 2 - : constraints.maxWidth; - return Wrap( - spacing: 16, - runSpacing: 16, - children: entries - .map( - (entry) => SizedBox( - width: width, - child: WorkspaceShortcutCardInternal( - entry: entry, - onTap: () => onSelectDestination(entry.destination), - ), - ), - ) - .toList(), - ); - }, - ), - ], - ), - ); - } -} - -class WorkspaceEntryInternal { - const WorkspaceEntryInternal({ - required this.destination, - required this.subtitle, - required this.iconColor, - required this.iconBackground, - }); - - final WorkspaceDestination destination; - final String subtitle; - final Color iconColor; - final Color iconBackground; -} - -class LauncherHeaderInternal extends StatelessWidget { - const LauncherHeaderInternal({ - super.key, - required this.title, - required this.subtitle, - required this.primaryLabel, - required this.secondaryLabel, - required this.onPrimaryPressed, - required this.onSecondaryPressed, - }); - - final String title; - final String subtitle; - final String primaryLabel; - final String secondaryLabel; - final VoidCallback onPrimaryPressed; - final VoidCallback onSecondaryPressed; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final palette = context.palette; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: palette.textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - subtitle, - style: theme.textTheme.bodyLarge?.copyWith( - color: palette.textSecondary, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - GradientActionButtonInternal( - label: primaryLabel, - onPressed: onPrimaryPressed, - ), - OutlinedButton.icon( - onPressed: onSecondaryPressed, - icon: const Icon(Icons.arrow_outward_rounded), - label: Text(secondaryLabel), - ), - ], - ), - ], - ); - } -} - -class WorkspaceHeroInternal extends StatelessWidget { - const WorkspaceHeroInternal({ - super.key, - required this.connection, - required this.activeAgentName, - required this.sessionCount, - required this.runningTaskCount, - }); - - final GatewayConnectionSnapshot connection; - final String activeAgentName; - final int sessionCount; - final int runningTaskCount; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final palette = context.palette; - final statusLabel = connection.status == RuntimeConnectionStatus.connected - ? appText('会话已就绪', 'Session Ready') - : appText('等待接入', 'Awaiting Connection'); - final statusColor = connection.status == RuntimeConnectionStatus.connected - ? palette.success - : palette.textSecondary; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: palette.surfacePrimary, - borderRadius: BorderRadius.circular(AppRadius.card), - border: Border.all(color: palette.strokeSoft), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - statusLabel, - style: theme.textTheme.labelLarge?.copyWith(color: statusColor), - ), - const SizedBox(height: 10), - Text( - connection.remoteAddress ?? 'xworkmate.svc.plus', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: palette.textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - activeAgentName, - style: theme.textTheme.bodyLarge?.copyWith( - color: palette.textSecondary, - ), - ), - const SizedBox(height: 18), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - HeroMetricInternal( - label: appText('会话', 'Sessions'), - value: '$sessionCount', - icon: Icons.chat_bubble_outline_rounded, - ), - HeroMetricInternal( - label: appText('运行任务', 'Running'), - value: '$runningTaskCount', - icon: Icons.play_circle_outline_rounded, - ), - HeroMetricInternal( - label: appText('状态', 'Status'), - value: connection.status.label, - icon: Icons.monitor_heart_outlined, - ), - ], - ), - ], - ), - ); - } -} - -class HeroMetricInternal extends StatelessWidget { - const HeroMetricInternal({ - super.key, - required this.label, - required this.value, - required this.icon, - }); - - final String label; - final String value; - final IconData icon; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final palette = context.palette; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: palette.surfaceSecondary.withValues(alpha: 0.94), - borderRadius: BorderRadius.circular(AppRadius.card), - border: Border.all(color: palette.strokeSoft), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: palette.accent), - const SizedBox(width: 8), - Text( - '$label · $value', - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textPrimary, - ), - ), - ], - ), - ); - } -} - -class WorkspaceShortcutCardInternal extends StatelessWidget { - const WorkspaceShortcutCardInternal({ - super.key, - required this.entry, - required this.onTap, - }); - - final WorkspaceEntryInternal entry; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final palette = context.palette; - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppRadius.card), - child: Ink( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: palette.surfacePrimary, - borderRadius: BorderRadius.circular(AppRadius.card), - border: Border.all(color: palette.strokeSoft), - ), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: entry.iconBackground, - borderRadius: BorderRadius.circular(AppRadius.card), - ), - child: Icon( - entry.destination.icon, - color: entry.iconColor, - size: 22, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entry.destination.label, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: palette.textPrimary, - ), - ), - const SizedBox(height: 4), - Text( - entry.subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - Icon(Icons.chevron_right_rounded, color: palette.textSecondary), - ], - ), - ), - ), - ); - } -} - -class GradientActionButtonInternal extends StatelessWidget { - const GradientActionButtonInternal({ - super.key, - required this.label, - required this.onPressed, - }); - - final String label; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [palette.accent, palette.accentHover], - ), - borderRadius: BorderRadius.circular(AppRadius.button), - ), - child: FilledButton( - onPressed: onPressed, - style: FilledButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - shadowColor: Colors.transparent, - minimumSize: const Size(0, AppSizes.buttonHeightMobile), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), - ), - child: Text(label), - ), - ); - } -} diff --git a/lib/features/modules/modules_page.dart b/lib/features/modules/modules_page.dart deleted file mode 100644 index 6aca7c08..00000000 --- a/lib/features/modules/modules_page.dart +++ /dev/null @@ -1,799 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../app/ui_feature_manifest.dart'; -import '../../app/workspace_navigation.dart'; -import '../../app/app_metadata.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/runtime_models.dart'; -import '../../widgets/metric_card.dart'; -import '../../widgets/section_header.dart'; -import '../../widgets/section_tabs.dart'; -import '../../widgets/status_badge.dart'; -import '../../widgets/surface_card.dart'; -import '../../widgets/top_bar.dart'; - -class ModulesPage extends StatefulWidget { - const ModulesPage({ - super.key, - required this.controller, - required this.onOpenDetail, - this.initialTab, - }); - - final AppController controller; - final ValueChanged onOpenDetail; - final ModulesTab? initialTab; - - @override - State createState() => _ModulesPageState(); -} - -class _ModulesPageState extends State { - late ModulesTab _tab; - - ModulesTab _normalizeTab(ModulesTab tab) { - final normalized = tab == ModulesTab.gateway ? ModulesTab.nodes : tab; - if (_isTabVisible(normalized)) { - return normalized; - } - return ModulesTab.skills; - } - - bool _isTabVisible(ModulesTab tab) { - if (tab == ModulesTab.clawHub) { - final features = widget.controller.featuresFor(UiFeaturePlatform.desktop); - return features.isEnabledPath(UiFeatureKeys.workspaceClawHub); - } - if (tab == ModulesTab.connectors) { - final features = widget.controller.featuresFor(UiFeaturePlatform.desktop); - return features.isEnabledPath(UiFeatureKeys.workspaceConnectors); - } - return true; - } - - List get _visibleTabs => ModulesTab.values - .where((item) => item != ModulesTab.gateway) - .where(_isTabVisible) - .toList(growable: false); - - ModulesTab _tabForLabel(String value) { - return _visibleTabs.firstWhere( - (item) => item.label == value, - orElse: () => ModulesTab.skills, - ); - } - - @override - void initState() { - super.initState(); - _tab = _normalizeTab(widget.initialTab ?? widget.controller.modulesTab); - } - - @override - void didUpdateWidget(covariant ModulesPage oldWidget) { - super.didUpdateWidget(oldWidget); - final nextTab = _normalizeTab( - widget.initialTab ?? widget.controller.modulesTab, - ); - if (nextTab != _tab) { - setState(() => _tab = nextTab); - } - } - - @override - Widget build(BuildContext context) { - final controller = widget.controller; - final metrics = [ - MetricSummary( - label: appText('网关', 'Gateway'), - value: controller.connection.status.label, - caption: controller.connection.remoteAddress ?? kAppVersionLabel, - icon: Icons.wifi_tethering_rounded, - status: _connectionStatus(controller.connection.status), - ), - MetricSummary( - label: appText('节点', 'Nodes'), - value: '${controller.instances.length}', - caption: appText( - '${controller.instances.where((item) => item.mode == 'active').length} 个活跃实例', - '${controller.instances.where((item) => item.mode == 'active').length} active', - ), - icon: Icons.developer_board_rounded, - ), - MetricSummary( - label: appText('代理', 'Agents'), - value: '${controller.agents.length}', - caption: controller.activeAgentName, - icon: Icons.hub_rounded, - ), - ]; - - return AnimatedBuilder( - animation: controller, - builder: (context, _) { - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TopBar( - breadcrumbs: buildWorkspaceBreadcrumbs( - controller: controller, - rootLabel: appText('模块', 'Modules'), - sectionLabel: _tab.label, - ), - title: appText('模块', 'Modules'), - subtitle: appText( - '管理代理、节点、技能和平台服务。', - 'Manage agents, nodes, skills, and platform services.', - ), - trailing: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - SizedBox( - width: 220, - child: TextField( - decoration: InputDecoration( - hintText: appText('搜索模块', 'Search modules'), - prefixIcon: Icon(Icons.search_rounded), - ), - ), - ), - IconButton( - onPressed: () async { - await controller.refreshGatewayHealth(); - await controller.refreshAgents(); - await controller.refreshSessions(); - await controller.instancesController.refresh(); - await controller.skillsController.refresh( - agentId: controller.selectedAgentId.isEmpty - ? null - : controller.selectedAgentId, - ); - await controller.modelsController.refresh(); - await controller.cronJobsController.refresh(); - }, - icon: const Icon(Icons.refresh_rounded), - ), - FilledButton.tonalIcon( - onPressed: () => - controller.openSettings(tab: SettingsTab.gateway), - icon: const Icon(Icons.add_rounded), - label: Text(appText('打开设置中心', 'Open Settings')), - ), - ], - ), - ), - const SizedBox(height: 24), - SectionTabs( - items: _visibleTabs.map((item) => item.label).toList(), - value: _tab.label, - onChanged: (value) => setState(() { - _tab = _tabForLabel(value); - controller.openModules(tab: _tab); - }), - ), - const SizedBox(height: 24), - LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth > 980 - ? (constraints.maxWidth - 32) / 3 - : constraints.maxWidth > 640 - ? (constraints.maxWidth - 16) / 2 - : constraints.maxWidth; - return Wrap( - spacing: 16, - runSpacing: 16, - children: metrics - .map( - (metric) => SizedBox( - width: width, - child: MetricCard(metric: metric), - ), - ) - .toList(), - ); - }, - ), - const SizedBox(height: 28), - switch (_tab) { - ModulesTab.nodes => _NodesPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - ModulesTab.agents => _AgentsPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - ModulesTab.skills => _SkillsPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - ModulesTab.clawHub => _SkillsPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - ModulesTab.connectors => _SkillsPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - ModulesTab.gateway => _NodesPanel( - controller: controller, - onOpenDetail: widget.onOpenDetail, - ), - }, - ], - ), - ); - }, - ); - } -} - -class _NodesPanel extends StatelessWidget { - const _NodesPanel({required this.controller, required this.onOpenDetail}); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - Widget build(BuildContext context) { - final items = controller.instances; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - title: appText('节点', 'Nodes'), - subtitle: appText( - '来自 Gateway 运行时的在线实例与存在性数据。', - 'Live system-presence data from the gateway runtime.', - ), - ), - const SizedBox(height: 16), - if (items.isEmpty) - SurfaceCard( - child: Text( - controller.connection.status == RuntimeConnectionStatus.connected - ? appText('暂时还没有上报在线实例。', 'No live instances reported yet.') - : appText( - '恢复 xworkmate-bridge 连接后可加载实例与在线状态。', - 'Instances and presence return after xworkmate-bridge reconnects.', - ), - ), - ) - else - ...items.map( - (node) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SurfaceCard( - onTap: () => onOpenDetail( - DetailPanelData( - title: node.host ?? node.id, - subtitle: appText('实例', 'Instance'), - icon: Icons.developer_board_rounded, - status: _instanceStatus(node), - description: node.text, - meta: [ - node.platform ?? appText('未知', 'unknown'), - node.deviceFamily ?? appText('未知', 'unknown'), - ], - actions: [appText('刷新', 'Refresh')], - sections: [ - DetailSection( - title: appText('运行时', 'Runtime'), - items: [ - DetailItem(label: 'IP', value: node.ip ?? 'n/a'), - DetailItem( - label: 'Version', - value: node.version ?? 'n/a', - ), - DetailItem( - label: appText('模式', 'Mode'), - value: node.mode ?? 'n/a', - ), - DetailItem( - label: appText('最近输入', 'Last Input'), - value: node.lastInputSeconds == null - ? 'n/a' - : '${node.lastInputSeconds}s', - ), - ], - ), - ], - ), - ), - child: Row( - children: [ - Expanded( - flex: 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - node.host ?? node.id, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - '${node.platform ?? appText('未知', 'unknown')} · ${node.deviceFamily ?? appText('未知', 'unknown')}', - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - Expanded( - flex: 2, - child: StatusBadge(status: _instanceStatus(node)), - ), - Expanded(flex: 2, child: Text(node.version ?? 'n/a')), - Expanded(flex: 2, child: Text(node.mode ?? 'n/a')), - const Icon(Icons.chevron_right_rounded), - ], - ), - ), - ), - ), - ], - ); - } -} - -class _AgentsPanel extends StatelessWidget { - const _AgentsPanel({required this.controller, required this.onOpenDetail}); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - Widget build(BuildContext context) { - final items = controller.agents; - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth > 1220 - ? (constraints.maxWidth - 32) / 3 - : constraints.maxWidth > 760 - ? (constraints.maxWidth - 16) / 2 - : constraints.maxWidth; - if (items.isEmpty) { - return SurfaceCard( - child: Text( - controller.connection.status == RuntimeConnectionStatus.connected - ? appText( - '网关当前没有返回代理列表。', - 'No agents reported by the gateway.', - ) - : appText( - '恢复 xworkmate-bridge 连接后可加载代理。', - 'Agents return after xworkmate-bridge reconnects.', - ), - ), - ); - } - return Wrap( - spacing: 16, - runSpacing: 16, - children: items - .map( - (agent) => SizedBox( - width: width, - child: SurfaceCard( - onTap: () => onOpenDetail( - DetailPanelData( - title: agent.name, - subtitle: appText('代理', 'Agent'), - icon: Icons.hub_rounded, - status: controller.selectedAgentId == agent.id - ? StatusInfo( - appText('已选中', 'Selected'), - StatusTone.accent, - ) - : StatusInfo( - appText('可用', 'Available'), - StatusTone.success, - ), - description: appText( - '可用于会话路由的 Gateway 执行代理。', - 'Gateway operator agent available for session routing.', - ), - meta: [agent.id, agent.theme], - actions: [ - appText('选择', 'Select'), - appText('打开会话', 'Open Session'), - ], - sections: [ - DetailSection( - title: appText('身份信息', 'Identity'), - items: [ - DetailItem( - label: appText('名称', 'Name'), - value: agent.name, - ), - DetailItem(label: 'ID', value: agent.id), - DetailItem( - label: appText('主题', 'Theme'), - value: agent.theme, - ), - ], - ), - ], - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - agent.name, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - StatusBadge( - status: controller.selectedAgentId == agent.id - ? StatusInfo( - appText('已选中', 'Selected'), - StatusTone.accent, - ) - : StatusInfo( - appText('就绪', 'Ready'), - StatusTone.success, - ), - compact: true, - ), - ], - ), - const SizedBox(height: 10), - Text( - 'ID: ${agent.id}', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 14), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FilledButton.tonal( - onPressed: () => controller.selectAgent(agent.id), - child: Text(appText('选择', 'Select')), - ), - OutlinedButton( - onPressed: () => controller.refreshSessions(), - child: Text(appText('打开', 'Open')), - ), - ], - ), - ], - ), - ), - ), - ) - .toList(), - ); - }, - ); - } -} - -class _SkillsPanel extends StatelessWidget { - const _SkillsPanel({required this.controller, required this.onOpenDetail}); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - Widget build(BuildContext context) { - final items = controller.skills; - final currentMode = controller.currentAssistantExecutionTarget; - final modeCards = _buildModeCards(items, currentMode); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - title: appText('技能模式', 'Skill modes'), - subtitle: appText( - '用相同界面简洁区分 Agent 与 Gateway 两种路径,以及各自可用的技能包。', - 'Keep the same page structure while separating the agent and gateway paths and their available skill packs.', - ), - ), - const SizedBox(height: 16), - LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth > 1220 - ? (constraints.maxWidth - 32) / 3 - : constraints.maxWidth > 760 - ? (constraints.maxWidth - 16) / 2 - : constraints.maxWidth; - return Wrap( - spacing: 16, - runSpacing: 16, - children: modeCards - .map( - (card) => SizedBox( - width: width, - child: _SkillModeCard(data: card), - ), - ) - .toList(), - ); - }, - ), - const SizedBox(height: 24), - SectionHeader( - title: appText('技能明细', 'Skill details'), - subtitle: appText( - '保留当前运行时返回的原始技能列表,便于查看状态、来源和依赖。', - 'Keep the raw runtime skill list for status, source, and dependency inspection.', - ), - ), - const SizedBox(height: 16), - if (items.isEmpty) - SurfaceCard( - child: Text( - controller.connection.status == RuntimeConnectionStatus.connected - ? appText( - '当前网关或代理没有加载技能。', - 'No skills loaded for the active gateway / agent.', - ) - : appText( - '恢复 xworkmate-bridge 连接后可加载技能。', - 'Skills return after xworkmate-bridge reconnects.', - ), - ), - ) - else - ...items.map( - (skill) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SurfaceCard( - onTap: () => onOpenDetail( - DetailPanelData( - title: skill.name, - subtitle: appText('技能', 'Skill'), - icon: Icons.extension_rounded, - status: skill.disabled - ? StatusInfo( - appText('已禁用', 'Disabled'), - StatusTone.warning, - ) - : StatusInfo( - appText('已启用', 'Enabled'), - StatusTone.success, - ), - description: skill.description, - meta: [skill.source, skill.skillKey], - actions: [appText('刷新', 'Refresh')], - sections: [ - DetailSection( - title: appText('依赖要求', 'Requirements'), - items: [ - DetailItem( - label: appText('缺失二进制', 'Missing bins'), - value: skill.missingBins.isEmpty - ? appText('无', 'None') - : skill.missingBins.join(', '), - ), - DetailItem( - label: appText('缺失环境变量', 'Missing env'), - value: skill.missingEnv.isEmpty - ? appText('无', 'None') - : skill.missingEnv.join(', '), - ), - DetailItem( - label: appText('缺失配置', 'Missing config'), - value: skill.missingConfig.isEmpty - ? appText('无', 'None') - : skill.missingConfig.join(', '), - ), - ], - ), - ], - ), - ), - child: Row( - children: [ - Expanded( - flex: 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - skill.name, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - skill.description, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - Expanded( - flex: 2, - child: StatusBadge( - status: skill.disabled - ? StatusInfo( - appText('已禁用', 'Disabled'), - StatusTone.warning, - ) - : StatusInfo( - appText('已启用', 'Enabled'), - StatusTone.success, - ), - ), - ), - Expanded(flex: 2, child: Text(skill.source)), - Expanded( - flex: 2, - child: Text(skill.primaryEnv ?? 'workspace'), - ), - const Icon(Icons.chevron_right_rounded), - ], - ), - ), - ), - ), - ], - ); - } - - List<_SkillModeCardData> _buildModeCards( - List items, - AssistantExecutionTarget currentMode, - ) { - final gatewaySkills = items.toList(growable: false); - return <_SkillModeCardData>[ - _SkillModeCardData( - title: appText('Gateway', 'Gateway'), - subtitle: appText( - '通过 xworkmate-bridge 暴露运行时技能,统一承接当前 gateway 路径。', - 'Expose runtime skill packs through xworkmate-bridge as the single gateway path.', - ), - icon: Icons.lan_rounded, - status: currentMode == AssistantExecutionTarget.gateway - ? StatusInfo(appText('当前模式', 'Current mode'), StatusTone.accent) - : StatusInfo(appText('可切换', 'Available'), StatusTone.success), - chips: [ - appText('统一路由', 'Unified routing'), - appText('xworkmate-bridge', 'xworkmate-bridge'), - ], - skills: currentMode == AssistantExecutionTarget.gateway - ? gatewaySkills.map((item) => item.name).toList() - : const [], - emptyLabel: appText( - '切换到 Gateway 模式后,将显示当前 bridge 返回的技能包。', - 'Switch to gateway mode to inspect the active skill packs returned by the bridge.', - ), - ), - ]; - } -} - -class _SkillModeCardData { - const _SkillModeCardData({ - required this.title, - required this.subtitle, - required this.icon, - required this.status, - required this.chips, - required this.skills, - required this.emptyLabel, - }); - - final String title; - final String subtitle; - final IconData icon; - final StatusInfo status; - final List chips; - final List skills; - final String emptyLabel; -} - -class _SkillModeCard extends StatelessWidget { - const _SkillModeCard({required this.data}); - - final _SkillModeCardData data; - - @override - Widget build(BuildContext context) { - return SurfaceCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar(radius: 20, child: Icon(data.icon, size: 20)), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data.title, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 4), - StatusBadge(status: data.status, compact: true), - ], - ), - ), - ], - ), - const SizedBox(height: 14), - Text(data.subtitle, style: Theme.of(context).textTheme.bodySmall), - if (data.chips.isNotEmpty) ...[ - const SizedBox(height: 14), - Wrap( - spacing: 8, - runSpacing: 8, - children: data.chips - .map( - (item) => Chip( - label: Text(item), - visualDensity: VisualDensity.compact, - ), - ) - .toList(), - ), - ], - const SizedBox(height: 14), - Text( - appText('可用技能包', 'Available skill packs'), - style: Theme.of(context).textTheme.labelLarge, - ), - const SizedBox(height: 8), - if (data.skills.isEmpty) - Text(data.emptyLabel, style: Theme.of(context).textTheme.bodySmall) - else - Wrap( - spacing: 8, - runSpacing: 8, - children: data.skills - .map( - (item) => Chip( - label: Text(item), - visualDensity: VisualDensity.compact, - ), - ) - .toList(), - ), - ], - ), - ); - } -} - -StatusInfo _connectionStatus(RuntimeConnectionStatus status) => - switch (status) { - RuntimeConnectionStatus.connected => StatusInfo( - appText('健康', 'Healthy'), - StatusTone.success, - ), - RuntimeConnectionStatus.connecting => StatusInfo( - appText('连接中', 'Connecting'), - StatusTone.accent, - ), - RuntimeConnectionStatus.error => StatusInfo( - appText('错误', 'Error'), - StatusTone.danger, - ), - RuntimeConnectionStatus.offline => StatusInfo( - appText('离线', 'Offline'), - StatusTone.neutral, - ), - }; - -StatusInfo _instanceStatus(GatewayInstanceSummary item) { - final mode = (item.mode ?? '').toLowerCase(); - if (mode.contains('error') || mode.contains('warn')) { - return StatusInfo(appText('告警', 'Warning'), StatusTone.warning); - } - if (mode.contains('active') || mode.contains('online')) { - return StatusInfo(appText('在线', 'Online'), StatusTone.success); - } - return StatusInfo(appText('已发现', 'Seen'), StatusTone.neutral); -} diff --git a/lib/features/skills/skills_page.dart b/lib/features/skills/skills_page.dart deleted file mode 100644 index 573c98b9..00000000 --- a/lib/features/skills/skills_page.dart +++ /dev/null @@ -1,480 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../app/workspace_navigation.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/runtime_models.dart'; -import '../../theme/app_palette.dart'; -import '../../widgets/desktop_workspace_scaffold.dart'; -import '../../widgets/status_badge.dart'; -import '../../widgets/surface_card.dart'; - -class SkillsPage extends StatefulWidget { - const SkillsPage({ - super.key, - required this.controller, - required this.onOpenDetail, - }); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - State createState() => _SkillsPageState(); -} - -class _SkillsPageState extends State { - final TextEditingController _searchController = TextEditingController(); - String _query = ''; - String? _selectedSkillKey; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.controller, - builder: (context, _) { - final controller = widget.controller; - final skills = controller.skills - .where(_matchesQuery) - .toList(growable: false); - final selected = _resolveSelectedSkill(skills); - return DesktopWorkspaceScaffold( - breadcrumbs: buildWorkspaceBreadcrumbs( - controller: controller, - rootLabel: WorkspaceDestination.skills.label, - ), - eyebrow: appText('技能与能力包', 'Skills and capabilities'), - title: appText('技能工作台', 'Skills workspace'), - subtitle: appText( - '左侧浏览技能包,右侧查看描述、依赖和使用建议。', - 'Browse skills on the left, inspect descriptions, dependencies, and usage on the right.', - ), - toolbar: Wrap( - spacing: 10, - runSpacing: 10, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - width: 240, - child: TextField( - controller: _searchController, - onChanged: (value) { - setState(() { - _query = value.trim().toLowerCase(); - }); - }, - decoration: InputDecoration( - hintText: appText('搜索技能', 'Search skills'), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: _query.isEmpty - ? null - : IconButton( - tooltip: appText('清除', 'Clear'), - onPressed: () { - _searchController.clear(); - setState(() { - _query = ''; - }); - }, - icon: const Icon(Icons.close_rounded), - ), - ), - ), - ), - IconButton( - tooltip: appText('刷新技能', 'Refresh skills'), - onPressed: () async { - await controller.skillsController.refresh( - agentId: controller.selectedAgentId.isEmpty - ? null - : controller.selectedAgentId, - ); - }, - icon: const Icon(Icons.refresh_rounded), - ), - FilledButton.tonalIcon( - onPressed: () => - controller.navigateTo(WorkspaceDestination.assistant), - icon: const Icon(Icons.auto_awesome_rounded), - label: Text(appText('回到对话使用', 'Use in assistant')), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: SurfaceCard( - padding: EdgeInsets.zero, - borderRadius: 20, - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Row( - children: [ - SizedBox( - width: 360, - child: _SkillsListPanel( - skills: skills, - selectedSkillKey: selected?.skillKey, - onSelectSkill: (skill) { - setState(() { - _selectedSkillKey = skill.skillKey; - }); - }, - ), - ), - Container(width: 1, color: context.palette.strokeSoft), - Expanded( - child: _SkillDetailPanel( - controller: controller, - selected: selected, - ), - ), - ], - ), - ), - ), - ), - ); - }, - ); - } - - bool _matchesQuery(GatewaySkillSummary skill) { - if (_query.isEmpty) { - return true; - } - final haystack = [ - skill.name, - skill.description, - skill.source, - skill.skillKey, - skill.primaryEnv ?? '', - ].join(' ').toLowerCase(); - return haystack.contains(_query); - } - - GatewaySkillSummary? _resolveSelectedSkill(List skills) { - if (skills.isEmpty) { - return null; - } - for (final skill in skills) { - if (skill.skillKey == _selectedSkillKey) { - return skill; - } - } - return skills.first; - } -} - -class _SkillsListPanel extends StatelessWidget { - const _SkillsListPanel({ - required this.skills, - required this.selectedSkillKey, - required this.onSelectSkill, - }); - - final List skills; - final String? selectedSkillKey; - final ValueChanged onSelectSkill; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), - child: Row( - children: [ - Text( - appText('技能列表', 'Skill list'), - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(width: 8), - Text( - '${skills.length}', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: palette.textMuted), - ), - ], - ), - ), - Container(height: 1, color: palette.strokeSoft), - Expanded( - child: skills.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(20), - child: Text( - appText( - '当前没有可展示的技能。', - 'No skills are available right now.', - ), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - ), - ), - ), - ) - : ListView.separated( - padding: const EdgeInsets.all(10), - itemCount: skills.length, - separatorBuilder: (_, _) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final skill = skills[index]; - return _SkillListTile( - skill: skill, - selected: skill.skillKey == selectedSkillKey, - onTap: () => onSelectSkill(skill), - ); - }, - ), - ), - ], - ); - } -} - -class _SkillListTile extends StatelessWidget { - const _SkillListTile({ - required this.skill, - required this.selected, - required this.onTap, - }); - - final GatewaySkillSummary skill; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return Material( - color: selected ? palette.surfacePrimary : Colors.transparent, - borderRadius: BorderRadius.circular(18), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(18), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - color: selected ? palette.surfaceSecondary : Colors.transparent, - boxShadow: selected - ? [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.06), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] - : const [], - ), - child: Text( - skill.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: selected ? palette.textPrimary : null, - ), - ), - ), - ), - ); - } -} - -class _SkillDetailPanel extends StatelessWidget { - const _SkillDetailPanel({required this.controller, required this.selected}); - - final AppController controller; - final GatewaySkillSummary? selected; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - if (selected == null) { - return Center( - child: Text( - appText('选择左侧技能查看详情。', 'Select a skill on the left.'), - style: Theme.of( - context, - ).textTheme.bodyLarge?.copyWith(color: palette.textSecondary), - ), - ); - } - - return Padding( - padding: const EdgeInsets.all(18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - Text( - selected!.name, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - StatusBadge( - status: selected!.disabled - ? _skillStatus( - appText('已禁用', 'Disabled'), - StatusTone.warning, - ) - : _skillStatus( - appText('已启用', 'Enabled'), - StatusTone.success, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - selected!.description, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - height: 1.5, - ), - ), - const SizedBox(height: 18), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - _DependencyCard( - title: appText('缺失二进制', 'Missing bins'), - values: selected!.missingBins, - ), - _DependencyCard( - title: appText('缺失环境变量', 'Missing env'), - values: selected!.missingEnv, - ), - _DependencyCard( - title: appText('缺失配置', 'Missing config'), - values: selected!.missingConfig, - ), - ], - ), - const SizedBox(height: 18), - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appText('在对话中使用', 'Use in the assistant'), - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - Text( - appText( - '回到 Assistant 后,可通过下方建议按钮或直接描述需求来调用该技能上下文。', - 'After returning to Assistant, use the suggested chips or describe the task directly to route into this skill context.', - ), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - height: 1.45, - ), - ), - ], - ), - ), - const Spacer(), - Wrap( - spacing: 10, - runSpacing: 10, - children: [ - FilledButton.icon( - onPressed: () => - controller.navigateTo(WorkspaceDestination.assistant), - icon: const Icon(Icons.auto_awesome_rounded), - label: Text(appText('去对话中使用', 'Use in assistant')), - ), - OutlinedButton.icon( - onPressed: () async { - await controller.skillsController.refresh( - agentId: controller.selectedAgentId.isEmpty - ? null - : controller.selectedAgentId, - ); - }, - icon: const Icon(Icons.refresh_rounded), - label: Text(appText('刷新', 'Refresh')), - ), - ], - ), - ], - ), - ); - } -} - -class _DependencyCard extends StatelessWidget { - const _DependencyCard({required this.title, required this.values}); - - final String title; - final List values; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return Container( - width: 220, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 8), - Text( - values.isEmpty ? appText('无', 'None') : values.join(', '), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.45, - ), - ), - ], - ), - ); - } -} - -StatusInfo _skillStatus(String label, StatusTone tone) => - StatusInfo(label, tone); diff --git a/lib/features/tasks/tasks_page.dart b/lib/features/tasks/tasks_page.dart deleted file mode 100644 index fd414607..00000000 --- a/lib/features/tasks/tasks_page.dart +++ /dev/null @@ -1,583 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../app/workspace_navigation.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../runtime/runtime_models.dart'; -import '../../theme/app_palette.dart'; -import '../../widgets/desktop_workspace_scaffold.dart'; -import '../../widgets/metric_card.dart'; -import '../../widgets/section_tabs.dart'; -import '../../widgets/status_badge.dart'; -import '../../widgets/surface_card.dart'; - -class TasksPage extends StatefulWidget { - const TasksPage({ - super.key, - required this.controller, - required this.onOpenDetail, - }); - - final AppController controller; - final ValueChanged onOpenDetail; - - @override - State createState() => _TasksPageState(); -} - -class _TasksPageState extends State { - TasksTab _tab = TasksTab.queue; - final TextEditingController _searchController = TextEditingController(); - String _query = ''; - String? _selectedTaskId; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final controller = widget.controller; - final allItems = controller.taskItemsForTab(_tabKey); - final items = allItems.where(_matchesQuery).toList(growable: false); - final selected = _resolveSelectedTask(items); - final metrics = [ - MetricSummary( - label: appText('总数', 'Total'), - value: '${controller.tasksController.totalCount}', - caption: appText('任务 / 会话聚合', 'Task / session aggregate'), - icon: Icons.layers_rounded, - ), - MetricSummary( - label: appText('运行中', 'Running'), - value: '${controller.tasksController.running.length}', - caption: appText('当前活跃执行', 'Active executions'), - icon: Icons.play_circle_outline_rounded, - status: _taskStatusInfo('Running'), - ), - MetricSummary( - label: appText('失败', 'Failed'), - value: '${controller.tasksController.failed.length}', - caption: appText('中断或报错', 'Interrupted or failed'), - icon: Icons.error_outline_rounded, - status: _taskStatusInfo('Failed'), - ), - MetricSummary( - label: appText('计划中', 'Scheduled'), - value: '${controller.tasksController.scheduled.length}', - caption: appText('来自 cron 调度器', 'Loaded from cron scheduler'), - icon: Icons.event_repeat_rounded, - ), - ]; - - return AnimatedBuilder( - animation: controller, - builder: (context, _) { - final palette = context.palette; - return DesktopWorkspaceScaffold( - breadcrumbs: buildWorkspaceBreadcrumbs( - controller: controller, - rootLabel: WorkspaceDestination.tasks.label, - ), - eyebrow: appText('任务与线程', 'Tasks and sessions'), - title: appText('任务工作台', 'Task workspace'), - subtitle: appText( - '左侧筛选和切换任务,右侧查看当前任务详情。', - 'Filter and switch tasks on the left, inspect the current task on the right.', - ), - toolbar: Wrap( - spacing: 10, - runSpacing: 10, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - width: 240, - child: TextField( - controller: _searchController, - onChanged: (value) { - setState(() { - _query = value.trim().toLowerCase(); - }); - }, - decoration: InputDecoration( - hintText: appText('搜索任务 / 会话', 'Search tasks / sessions'), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: _query.isEmpty - ? null - : IconButton( - tooltip: appText('清除', 'Clear'), - onPressed: () { - _searchController.clear(); - setState(() { - _query = ''; - }); - }, - icon: const Icon(Icons.close_rounded), - ), - ), - ), - ), - IconButton( - tooltip: appText('刷新任务', 'Refresh tasks'), - onPressed: controller.refreshSessions, - icon: const Icon(Icons.refresh_rounded), - ), - if (_tab == TasksTab.scheduled) - Chip( - avatar: const Icon(Icons.lock_outline_rounded, size: 16), - label: Text( - appText('计划任务只读', 'Scheduled tasks are read-only'), - ), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - SectionTabs( - items: TasksTab.values.map((item) => item.label).toList(), - value: _tab.label, - onChanged: (value) { - setState(() { - _tab = TasksTab.values.firstWhere( - (item) => item.label == value, - ); - _selectedTaskId = null; - }); - }, - ), - const SizedBox(height: 16), - SizedBox( - height: 172, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: metrics.length, - separatorBuilder: (_, _) => const SizedBox(width: 12), - itemBuilder: (context, index) => SizedBox( - width: 240, - child: MetricCard(metric: metrics[index]), - ), - ), - ), - const SizedBox(height: 16), - Expanded( - child: SurfaceCard( - padding: EdgeInsets.zero, - borderRadius: 20, - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Row( - children: [ - SizedBox( - width: 360, - child: _TaskListPanel( - tab: _tab, - items: items, - selectedTaskId: selected?.id, - onSelectTask: (task) { - setState(() { - _selectedTaskId = task.id; - }); - }, - ), - ), - Container(width: 1, color: palette.strokeSoft), - Expanded( - child: _TaskDetailPanel( - controller: controller, - tab: _tab, - selected: selected, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - }, - ); - } - - String get _tabKey => _tab.label; - - bool _matchesQuery(DerivedTaskItem item) { - if (_query.isEmpty) { - return true; - } - final haystack = [ - item.title, - item.summary, - item.owner, - item.surface, - item.sessionKey, - ].join(' ').toLowerCase(); - return haystack.contains(_query); - } - - DerivedTaskItem? _resolveSelectedTask(List items) { - if (items.isEmpty) { - return null; - } - for (final item in items) { - if (item.id == _selectedTaskId) { - return item; - } - } - return items.first; - } -} - -class _TaskListPanel extends StatelessWidget { - const _TaskListPanel({ - required this.tab, - required this.items, - required this.selectedTaskId, - required this.onSelectTask, - }); - - final TasksTab tab; - final List items; - final String? selectedTaskId; - final ValueChanged onSelectTask; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final emptyLabel = tab == TasksTab.scheduled - ? appText('当前没有计划任务。', 'No scheduled tasks right now.') - : appText('当前筛选下没有任务。', 'No tasks match the current filter.'); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), - child: Row( - children: [ - Text( - appText('任务列表', 'Task list'), - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(width: 8), - Text( - '${items.length}', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: palette.textMuted), - ), - ], - ), - ), - Container(height: 1, color: palette.strokeSoft), - Expanded( - child: items.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(20), - child: Text( - emptyLabel, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - ), - ), - ), - ) - : ListView.separated( - padding: const EdgeInsets.all(10), - itemCount: items.length, - separatorBuilder: (_, _) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final task = items[index]; - final selected = task.id == selectedTaskId; - return _TaskListTile( - task: task, - selected: selected, - onTap: () => onSelectTask(task), - ); - }, - ), - ), - ], - ); - } -} - -class _TaskListTile extends StatelessWidget { - const _TaskListTile({ - required this.task, - required this.selected, - required this.onTap, - }); - - final DerivedTaskItem task; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return Material( - color: selected ? palette.surfacePrimary : Colors.transparent, - borderRadius: BorderRadius.circular(18), - child: InkWell( - key: ValueKey('tasks-list-item-${task.id}'), - onTap: onTap, - borderRadius: BorderRadius.circular(18), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: selected ? palette.surfaceSecondary : Colors.transparent, - borderRadius: BorderRadius.circular(18), - boxShadow: selected - ? [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.06), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] - : const [], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - task.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 10), - StatusBadge(status: _taskStatusInfo(task.status)), - ], - ), - const SizedBox(height: 8), - Text( - task.summary, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.4, - ), - ), - const SizedBox(height: 10), - Wrap( - spacing: 10, - runSpacing: 6, - children: [ - _InlineMeta(label: task.owner), - _InlineMeta(label: task.startedAtLabel), - _InlineMeta(label: task.surface), - ], - ), - ], - ), - ), - ), - ); - } -} - -class _TaskDetailPanel extends StatelessWidget { - const _TaskDetailPanel({ - required this.controller, - required this.tab, - required this.selected, - }); - - final AppController controller; - final TasksTab tab; - final DerivedTaskItem? selected; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - if (selected == null) { - return Center( - child: Text( - appText('选择左侧任务查看详情。', 'Select a task on the left.'), - style: Theme.of( - context, - ).textTheme.bodyLarge?.copyWith(color: palette.textSecondary), - ), - ); - } - - return Padding( - key: const Key('tasks-detail-panel'), - padding: const EdgeInsets.all(18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - selected!.title, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - StatusBadge(status: _taskStatusInfo(selected!.status)), - ], - ), - const SizedBox(height: 8), - Text( - selected!.summary, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, - height: 1.5, - ), - ), - const SizedBox(height: 18), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - _DetailStat( - label: appText('任务来源', 'Surface'), - value: selected!.surface, - ), - _DetailStat( - label: appText('执行代理', 'Owner'), - value: selected!.owner, - ), - _DetailStat( - label: appText('开始时间', 'Started'), - value: selected!.startedAtLabel, - ), - _DetailStat( - label: appText('耗时', 'Duration'), - value: selected!.durationLabel, - ), - ], - ), - const SizedBox(height: 18), - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appText('会话上下文', 'Conversation context'), - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - SelectableText( - selected!.sessionKey, - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - const Spacer(), - Align( - alignment: Alignment.centerRight, - child: OutlinedButton.icon( - onPressed: controller.refreshSessions, - icon: const Icon(Icons.refresh_rounded), - label: Text(appText('刷新', 'Refresh')), - ), - ), - ], - ), - ); - } -} - -class _DetailStat extends StatelessWidget { - const _DetailStat({required this.label, required this.value}); - - final String label; - final String value; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - return Container( - constraints: const BoxConstraints(minWidth: 160), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: palette.textMuted), - ), - const SizedBox(height: 4), - Text(value, style: Theme.of(context).textTheme.labelLarge), - ], - ), - ); - } -} - -class _InlineMeta extends StatelessWidget { - const _InlineMeta({required this.label}); - - final String label; - - @override - Widget build(BuildContext context) { - return Text( - label, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: context.palette.textMuted), - ); - } -} - -StatusInfo _taskStatusInfo(String status) => switch (status) { - 'running' || - 'Running' => StatusInfo(appText('运行中', 'Running'), StatusTone.accent), - 'failed' || - 'Failed' => StatusInfo(appText('失败', 'Failed'), StatusTone.danger), - 'queued' || - 'Queued' => StatusInfo(appText('排队中', 'Queued'), StatusTone.neutral), - _ => StatusInfo(appText('可继续', 'Open'), StatusTone.success), -}; diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index 55afd0b8..d64740da 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -4,45 +4,18 @@ import '../i18n/app_language.dart'; enum WorkspaceDestination { assistant, - tasks, - skills, - nodes, - agents, - mcpServer, - clawHub, - secrets, - aiGateway, settings, - account, } extension WorkspaceDestinationCopy on WorkspaceDestination { String get label => switch (this) { WorkspaceDestination.assistant => appText('助手', 'Assistant'), - WorkspaceDestination.tasks => appText('任务', 'Tasks'), - WorkspaceDestination.skills => appText('技能', 'Skills'), - WorkspaceDestination.nodes => appText('节点', 'Nodes'), - WorkspaceDestination.agents => appText('代理', 'Agents'), - WorkspaceDestination.mcpServer => 'MCP Hub', - WorkspaceDestination.clawHub => 'ClawHub', - WorkspaceDestination.secrets => appText('密钥', 'Secrets'), - WorkspaceDestination.aiGateway => 'LLM API', WorkspaceDestination.settings => appText('设置', 'Settings'), - WorkspaceDestination.account => appText('在线账户', 'Online Account'), }; IconData get icon => switch (this) { WorkspaceDestination.assistant => Icons.chat_bubble_outline_rounded, - WorkspaceDestination.tasks => Icons.layers_rounded, - WorkspaceDestination.skills => Icons.auto_awesome_rounded, - WorkspaceDestination.nodes => Icons.developer_board_rounded, - WorkspaceDestination.agents => Icons.hub_rounded, - WorkspaceDestination.mcpServer => Icons.dns_rounded, - WorkspaceDestination.clawHub => Icons.extension_rounded, - WorkspaceDestination.secrets => Icons.key_rounded, - WorkspaceDestination.aiGateway => Icons.smart_toy_rounded, WorkspaceDestination.settings => Icons.tune_rounded, - WorkspaceDestination.account => Icons.account_circle_rounded, }; String get description => switch (this) { @@ -50,45 +23,9 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { 'AI 主入口,优先承接自然输入和高频工作发起。', 'Primary AI entry point for natural input and frequent task starts.', ), - WorkspaceDestination.tasks => appText( - '任务队列、运行态、失败项和调度历史的统一视图。', - 'Unified view for queue, running, failed, and history.', - ), - WorkspaceDestination.skills => appText( - '管理技能包与能力扩展,浏览和安装 ClawHub 技能。', - 'Manage skill packages and extensions, browse and install from ClawHub.', - ), - WorkspaceDestination.nodes => appText( - '管理边缘节点与实例,监控运行状态与负载。', - 'Manage edge nodes and instances, monitor status and load.', - ), - WorkspaceDestination.agents => appText( - '管理代理实例,配置行为与能力。', - 'Manage agent instances, configure behaviors and capabilities.', - ), - WorkspaceDestination.mcpServer => appText( - '管理 MCP Hub 连接与工具配置。', - 'Manage MCP Hub connections and tool configurations.', - ), - WorkspaceDestination.clawHub => appText( - '浏览和安装技能包、代理模板与连接器。', - 'Browse and install skill packages, agent templates and connectors.', - ), - WorkspaceDestination.secrets => appText( - '密钥与 Vault 配置统一收口到设置中心。', - 'Secrets and Vault configuration now live in the Settings center.', - ), - WorkspaceDestination.aiGateway => appText( - 'LLM API 配置统一收口到设置中心。', - 'LLM API configuration now lives in the Settings center.', - ), WorkspaceDestination.settings => appText( - '全局配置中心,只负责系统设置与诊断,不承担业务模块入口。', - 'Global settings and diagnostics, separated from business modules.', - ), - WorkspaceDestination.account => appText( - '在线账户、工作区切换、登录会话与 ACP Bridge Server 同步管理。', - 'Online account, workspace switching, login sessions, and ACP Bridge Server sync.', + '桥接、账户与集成配置统一收口到设置中心。', + 'Bridge, account, and integration settings are consolidated in Settings.', ), }; @@ -106,14 +43,6 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { } enum AssistantFocusEntry { - tasks, - skills, - nodes, - agents, - mcpServer, - clawHub, - secrets, - aiGateway, settings, language, theme, @@ -121,69 +50,21 @@ enum AssistantFocusEntry { extension AssistantFocusEntryCopy on AssistantFocusEntry { String get label => switch (this) { - AssistantFocusEntry.tasks => appText('任务', 'Tasks'), - AssistantFocusEntry.skills => appText('技能', 'Skills'), - AssistantFocusEntry.nodes => appText('节点', 'Nodes'), - AssistantFocusEntry.agents => appText('代理', 'Agents'), - AssistantFocusEntry.mcpServer => 'MCP Hub', - AssistantFocusEntry.clawHub => 'ClawHub', - AssistantFocusEntry.secrets => appText('密钥', 'Secrets'), - AssistantFocusEntry.aiGateway => 'LLM API', AssistantFocusEntry.settings => appText('设置', 'Settings'), AssistantFocusEntry.language => appText('语言', 'Language'), AssistantFocusEntry.theme => appText('主题/亮度', 'Theme / Brightness'), }; IconData get icon => switch (this) { - AssistantFocusEntry.tasks => Icons.layers_rounded, - AssistantFocusEntry.skills => Icons.auto_awesome_rounded, - AssistantFocusEntry.nodes => Icons.developer_board_rounded, - AssistantFocusEntry.agents => Icons.hub_rounded, - AssistantFocusEntry.mcpServer => Icons.dns_rounded, - AssistantFocusEntry.clawHub => Icons.extension_rounded, - AssistantFocusEntry.secrets => Icons.key_rounded, - AssistantFocusEntry.aiGateway => Icons.smart_toy_rounded, AssistantFocusEntry.settings => Icons.tune_rounded, AssistantFocusEntry.language => Icons.translate_rounded, AssistantFocusEntry.theme => Icons.brightness_6_rounded, }; String get description => switch (this) { - AssistantFocusEntry.tasks => appText( - '任务队列、运行态、失败项和调度历史的统一视图。', - 'Unified view for queue, running, failed, and history.', - ), - AssistantFocusEntry.skills => appText( - '管理技能包与能力扩展,浏览和安装 ClawHub 技能。', - 'Manage skill packages and extensions, browse and install from ClawHub.', - ), - AssistantFocusEntry.nodes => appText( - '管理边缘节点与实例,监控运行状态与负载。', - 'Manage edge nodes and instances, monitor status and load.', - ), - AssistantFocusEntry.agents => appText( - '管理代理实例,配置行为与能力。', - 'Manage agent instances, configure behaviors and capabilities.', - ), - AssistantFocusEntry.mcpServer => appText( - '管理 MCP Hub 连接与工具配置。', - 'Manage MCP Hub connections and tool configurations.', - ), - AssistantFocusEntry.clawHub => appText( - '浏览和安装技能包、代理模板与连接器。', - 'Browse and install skill packages, agent templates and connectors.', - ), - AssistantFocusEntry.secrets => appText( - '密钥与 Vault 配置统一收口到设置中心。', - 'Secrets and Vault configuration now live in the Settings center.', - ), - AssistantFocusEntry.aiGateway => appText( - 'LLM API 配置统一收口到设置中心。', - 'LLM API configuration now lives in the Settings center.', - ), AssistantFocusEntry.settings => appText( - '全局配置中心,只负责系统设置与诊断,不承担业务模块入口。', - 'Global settings and diagnostics, separated from business modules.', + '打开设置中心,管理 Bridge、账户与集成配置。', + 'Open Settings to manage bridge, account, and integration configuration.', ), AssistantFocusEntry.language => appText( '快速切换中英文界面语言,无需先进入设置页。', @@ -196,14 +77,6 @@ extension AssistantFocusEntryCopy on AssistantFocusEntry { }; WorkspaceDestination? get destination => switch (this) { - AssistantFocusEntry.tasks => WorkspaceDestination.tasks, - AssistantFocusEntry.skills => WorkspaceDestination.skills, - AssistantFocusEntry.nodes => WorkspaceDestination.nodes, - AssistantFocusEntry.agents => WorkspaceDestination.agents, - AssistantFocusEntry.mcpServer => WorkspaceDestination.mcpServer, - AssistantFocusEntry.clawHub => WorkspaceDestination.clawHub, - AssistantFocusEntry.secrets => WorkspaceDestination.secrets, - AssistantFocusEntry.aiGateway => WorkspaceDestination.aiGateway, AssistantFocusEntry.settings => WorkspaceDestination.settings, AssistantFocusEntry.language => null, AssistantFocusEntry.theme => null, @@ -228,17 +101,8 @@ extension AssistantFocusEntryCopy on AssistantFocusEntry { static AssistantFocusEntry fromDestination(WorkspaceDestination destination) { return switch (destination) { - WorkspaceDestination.tasks => AssistantFocusEntry.tasks, - WorkspaceDestination.skills => AssistantFocusEntry.skills, - WorkspaceDestination.nodes => AssistantFocusEntry.nodes, - WorkspaceDestination.agents => AssistantFocusEntry.agents, - WorkspaceDestination.mcpServer => AssistantFocusEntry.mcpServer, - WorkspaceDestination.clawHub => AssistantFocusEntry.clawHub, - WorkspaceDestination.secrets => AssistantFocusEntry.secrets, - WorkspaceDestination.aiGateway => AssistantFocusEntry.aiGateway, WorkspaceDestination.settings => AssistantFocusEntry.settings, - WorkspaceDestination.assistant || - WorkspaceDestination.account => throw ArgumentError.value( + WorkspaceDestination.assistant => throw ArgumentError.value( destination, 'destination', 'Focused assistant entries only support pinnable workspace targets.', @@ -252,14 +116,6 @@ const List kAssistantNavigationDestinationDefaults = const List kAssistantNavigationDestinationCandidates = [ - AssistantFocusEntry.tasks, - AssistantFocusEntry.skills, - AssistantFocusEntry.nodes, - AssistantFocusEntry.agents, - AssistantFocusEntry.mcpServer, - AssistantFocusEntry.clawHub, - AssistantFocusEntry.secrets, - AssistantFocusEntry.aiGateway, AssistantFocusEntry.settings, AssistantFocusEntry.language, AssistantFocusEntry.theme, @@ -300,84 +156,15 @@ extension AssistantModeCopy on AssistantMode { }; } -enum TasksTab { queue, running, history, failed, scheduled } - -extension TasksTabCopy on TasksTab { - String get label => switch (this) { - TasksTab.queue => appText('队列', 'Queue'), - TasksTab.running => appText('运行中', 'Running'), - TasksTab.history => appText('历史', 'History'), - TasksTab.failed => appText('失败', 'Failed'), - TasksTab.scheduled => appText('计划中', 'Scheduled'), - }; -} - -enum ModulesTab { gateway, nodes, agents, skills, clawHub, connectors } - -extension ModulesTabCopy on ModulesTab { - String get label => switch (this) { - ModulesTab.gateway => appText('网关', 'Gateway'), - ModulesTab.nodes => appText('节点', 'Nodes'), - ModulesTab.agents => appText('代理', 'Agents'), - ModulesTab.skills => appText('技能', 'Skills'), - ModulesTab.clawHub => 'ClawHub', - ModulesTab.connectors => appText('连接器', 'Connectors'), - }; -} - -enum SecretsTab { vault, localStore, providers, audit } - -extension SecretsTabCopy on SecretsTab { - String get label => switch (this) { - SecretsTab.vault => 'Vault', - SecretsTab.localStore => appText('本地存储', 'Local Store'), - SecretsTab.providers => appText('提供方', 'Providers'), - SecretsTab.audit => appText('审计', 'Audit'), - }; -} - -enum SettingsTab { - general, - workspace, - gateway, - agents, - appearance, - diagnostics, - experimental, - about, -} +enum SettingsTab { gateway } extension SettingsTabCopy on SettingsTab { String get label => switch (this) { - SettingsTab.general => appText('通用', 'General'), - SettingsTab.workspace => appText('工作区', 'Workspace'), SettingsTab.gateway => appText('集成', 'Integrations'), - SettingsTab.agents => appText('多 Agent', 'Multi-Agent'), - SettingsTab.appearance => appText('外观', 'Appearance'), - SettingsTab.diagnostics => appText('诊断', 'Diagnostics'), - SettingsTab.experimental => appText('实验特性', 'Experimental'), - SettingsTab.about => appText('关于', 'About'), }; } -enum AiGatewayTab { models, agents, endpoints, tools } - -extension AiGatewayTabCopy on AiGatewayTab { - String get label => switch (this) { - AiGatewayTab.models => appText('模型', 'Models'), - AiGatewayTab.agents => appText('代理', 'Agents'), - AiGatewayTab.endpoints => appText('端点', 'Endpoints'), - AiGatewayTab.tools => appText('工具', 'Tools'), - }; -} - -enum SettingsDetailPage { - gatewayConnection, - aiGatewayIntegration, - vaultProvider, - externalAgents, - diagnosticsAdvanced, -} +enum SettingsDetailPage { gatewayConnection } extension SettingsDetailPageCopy on SettingsDetailPage { String get label => switch (this) { @@ -385,30 +172,10 @@ extension SettingsDetailPageCopy on SettingsDetailPage { 'Gateway 连接参数', 'Gateway Connection', ), - SettingsDetailPage.aiGatewayIntegration => appText( - 'LLM 接入点', - 'LLM Endpoints', - ), - SettingsDetailPage.vaultProvider => appText( - 'Vault 提供方参数', - 'Vault Provider', - ), - SettingsDetailPage.externalAgents => appText( - '多 Agent 协作参数', - 'External Agents', - ), - SettingsDetailPage.diagnosticsAdvanced => appText( - '高级诊断参数', - 'Advanced Diagnostics', - ), }; SettingsTab get tab => switch (this) { - SettingsDetailPage.gatewayConnection || - SettingsDetailPage.aiGatewayIntegration || - SettingsDetailPage.vaultProvider => SettingsTab.gateway, - SettingsDetailPage.externalAgents => SettingsTab.agents, - SettingsDetailPage.diagnosticsAdvanced => SettingsTab.diagnostics, + SettingsDetailPage.gatewayConnection => SettingsTab.gateway, }; } @@ -418,9 +185,6 @@ class SettingsNavigationContext { required this.rootLabel, required this.destination, this.sectionLabel, - this.modulesTab, - this.secretsTab, - this.aiGatewayTab, this.settingsTab, this.gatewayProfileIndex, this.prefersGatewaySetupCode, @@ -429,9 +193,6 @@ class SettingsNavigationContext { final String rootLabel; final WorkspaceDestination destination; final String? sectionLabel; - final ModulesTab? modulesTab; - final SecretsTab? secretsTab; - final AiGatewayTab? aiGatewayTab; final SettingsTab? settingsTab; final int? gatewayProfileIndex; final bool? prefersGatewaySetupCode; diff --git a/lib/runtime/runtime_controllers_entities.dart b/lib/runtime/runtime_controllers_entities.dart index 6639b3e4..7817eca8 100644 --- a/lib/runtime/runtime_controllers_entities.dart +++ b/lib/runtime/runtime_controllers_entities.dart @@ -11,40 +11,6 @@ import 'runtime_controllers_settings.dart'; import 'runtime_controllers_gateway.dart'; import 'runtime_controllers_derived_tasks.dart'; -class InstancesController extends ChangeNotifier { - InstancesController(this.runtimeInternal); - - final GatewayRuntime runtimeInternal; - - List itemsInternal = const []; - bool loadingInternal = false; - String? errorInternal; - - List get items => itemsInternal; - bool get loading => loadingInternal; - String? get error => errorInternal; - - Future refresh() async { - if (!runtimeInternal.isConnected) { - itemsInternal = const []; - errorInternal = null; - notifyListeners(); - return; - } - loadingInternal = true; - errorInternal = null; - notifyListeners(); - try { - itemsInternal = await runtimeInternal.listInstances(); - } catch (error) { - errorInternal = error.toString(); - } finally { - loadingInternal = false; - notifyListeners(); - } - } -} - class SkillsController extends ChangeNotifier { SkillsController(this.runtimeInternal); @@ -79,41 +45,6 @@ class SkillsController extends ChangeNotifier { } } -class ConnectorsController extends ChangeNotifier { - ConnectorsController(this.runtimeInternal); - - final GatewayRuntime runtimeInternal; - - List itemsInternal = - const []; - bool loadingInternal = false; - String? errorInternal; - - List get items => itemsInternal; - bool get loading => loadingInternal; - String? get error => errorInternal; - - Future refresh() async { - if (!runtimeInternal.isConnected) { - itemsInternal = const []; - errorInternal = null; - notifyListeners(); - return; - } - loadingInternal = true; - errorInternal = null; - notifyListeners(); - try { - itemsInternal = await runtimeInternal.listConnectors(); - } catch (error) { - errorInternal = error.toString(); - } finally { - loadingInternal = false; - notifyListeners(); - } - } -} - class ModelsController extends ChangeNotifier { ModelsController(this.runtimeInternal, this.settingsControllerInternal); diff --git a/lib/widgets/assistant_focus_panel_core.dart b/lib/widgets/assistant_focus_panel_core.dart index 53eb0984..524ac4ac 100644 --- a/lib/widgets/assistant_focus_panel_core.dart +++ b/lib/widgets/assistant_focus_panel_core.dart @@ -318,30 +318,6 @@ class AssistantFocusPreviewInternal extends StatelessWidget { @override Widget build(BuildContext context) { return switch (destination) { - AssistantFocusEntry.tasks => TasksFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.skills => SkillsFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.nodes => NodesFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.agents => AgentsFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.mcpServer => McpFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.clawHub => ClawHubFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.secrets => SecretsFocusPreviewInternal( - controller: controller, - ), - AssistantFocusEntry.aiGateway => AiGatewayFocusPreviewInternal( - controller: controller, - ), AssistantFocusEntry.settings => SettingsFocusPreviewInternal( controller: controller, ), diff --git a/lib/widgets/assistant_focus_panel_previews.dart b/lib/widgets/assistant_focus_panel_previews.dart index 03db1d42..d865980f 100644 --- a/lib/widgets/assistant_focus_panel_previews.dart +++ b/lib/widgets/assistant_focus_panel_previews.dart @@ -1,364 +1,17 @@ // ignore_for_file: unused_import, unnecessary_import import 'package:flutter/material.dart'; + import '../app/app_controller.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/runtime_models.dart'; import '../theme/app_palette.dart'; +import 'assistant_focus_panel_core.dart'; +import 'assistant_focus_panel_support.dart'; import 'chrome_quick_action_buttons.dart'; import 'settings_focus_quick_actions.dart'; import 'surface_card.dart'; -import 'assistant_focus_panel_core.dart'; -import 'assistant_focus_panel_support.dart'; - -class TasksFocusPreviewInternal extends StatelessWidget { - const TasksFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = [ - ...typedController.tasksController.running.take(2), - ...typedController.tasksController.queue.take(2), - ...typedController.tasksController.history.take(1), - ].take(4).toList(growable: false); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FocusPillInternal( - label: appText( - '运行中 ${typedController.tasksController.running.length}', - 'Running ${typedController.tasksController.running.length}', - ), - ), - FocusPillInternal( - label: appText( - '队列 ${typedController.tasksController.queue.length}', - 'Queue ${typedController.tasksController.queue.length}', - ), - ), - FocusPillInternal( - label: appText( - '计划 ${typedController.tasksController.scheduled.length}', - 'Scheduled ${typedController.tasksController.scheduled.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - PreviewEmptyStateInternal( - message: - typedController.connection.status == - RuntimeConnectionStatus.connected - ? appText('当前没有任务摘要。', 'No task summary yet.') - : appText( - '恢复 xworkmate-bridge 连接后这里会显示任务摘要。', - 'Task summaries appear here after xworkmate-bridge reconnects.', - ), - ) - else - ...items.map( - (item) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: item.title, - subtitle: item.summary, - trailing: item.status, - ), - ), - ), - ], - ); - } -} - -class SkillsFocusPreviewInternal extends StatelessWidget { - const SkillsFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.skills.take(4).toList(growable: false); - if (items.isEmpty) { - final bridgeEndpointMissing = - typedController.resolveExternalAcpEndpointForTargetInternal( - AssistantExecutionTarget.gateway, - ) == - null; - return PreviewEmptyStateInternal( - message: bridgeEndpointMissing - ? appText( - 'Bridge Server 当前不可用。恢复后这里会显示线程技能摘要。', - 'The bridge server is currently unavailable. Thread skill summaries will appear here after it recovers.', - ) - : typedController.connection.status == RuntimeConnectionStatus.connected - ? appText( - '当前代理没有已加载技能。', - 'No skills are loaded for the active agent.', - ) - : appText( - '恢复 xworkmate-bridge 连接后可查看技能摘要。', - 'Skill summaries are available again after xworkmate-bridge reconnects.', - ), - ); - } - return Column( - children: items - .map( - (skill) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: skill.name, - subtitle: skill.description, - trailing: skill.disabled - ? appText('已禁用', 'Disabled') - : appText('已启用', 'Enabled'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class NodesFocusPreviewInternal extends StatelessWidget { - const NodesFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.instances.take(4).toList(growable: false); - if (items.isEmpty) { - return PreviewEmptyStateInternal( - message: appText('当前没有节点可显示。', 'No nodes are available right now.'), - ); - } - return Column( - children: items - .map( - (instance) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: instance.host?.trim().isNotEmpty == true - ? instance.host! - : instance.id, - subtitle: - [instance.platform, instance.deviceFamily, instance.ip] - .whereType() - .where((item) => item.trim().isNotEmpty) - .join(' · '), - trailing: instance.mode ?? appText('未知', 'Unknown'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class AgentsFocusPreviewInternal extends StatelessWidget { - const AgentsFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.agents.take(5).toList(growable: false); - if (items.isEmpty) { - return PreviewEmptyStateInternal( - message: appText('当前没有代理摘要。', 'No agents are available right now.'), - ); - } - return Column( - children: items - .map( - (agent) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: '${agent.emoji} ${agent.name}', - subtitle: agent.id, - trailing: agent.name == typedController.activeAgentName - ? appText('当前', 'Active') - : agent.theme, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class McpFocusPreviewInternal extends StatelessWidget { - const McpFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.connectors.take(4).toList(growable: false); - if (items.isEmpty) { - return PreviewEmptyStateInternal( - message: appText( - '当前没有 MCP 连接器。恢复 xworkmate-bridge 连接后这里会显示工具摘要。', - 'No MCP connectors yet. Tool summaries appear here after xworkmate-bridge reconnects.', - ), - ); - } - return Column( - children: items - .map( - (connector) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: connector.label, - subtitle: connector.detailLabel, - trailing: connector.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class ClawHubFocusPreviewInternal extends StatelessWidget { - const ClawHubFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final skillCount = typedController.skills.length; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FocusPillInternal( - label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'), - ), - FocusPillInternal( - label: appText( - '关注入口 ${typedController.assistantNavigationDestinations.length}', - 'Pinned ${typedController.assistantNavigationDestinations.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - PreviewEmptyStateInternal( - message: appText( - 'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。', - 'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.', - ), - ), - ], - ); - } -} - -class SecretsFocusPreviewInternal extends StatelessWidget { - const SecretsFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.secretReferences - .take(4) - .toList(growable: false); - if (items.isEmpty) { - return PreviewEmptyStateInternal( - message: appText( - '当前没有密钥引用摘要。', - 'No masked secret references are available yet.', - ), - ); - } - return Column( - children: items - .map( - (secret) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: secret.name, - subtitle: '${secret.provider} · ${secret.module}', - trailing: secret.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class AiGatewayFocusPreviewInternal extends StatelessWidget { - const AiGatewayFocusPreviewInternal({super.key, required this.controller}); - - final AssistantFocusControllerInternal controller; - - @override - Widget build(BuildContext context) { - final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.models.take(4).toList(growable: false); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FocusPillInternal(label: typedController.connection.status.label), - FocusPillInternal( - label: appText( - '模型 ${typedController.models.length}', - 'Models ${typedController.models.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - PreviewEmptyStateInternal( - message: appText( - '当前没有 LLM API 模型摘要。', - 'No LLM API model summary is available yet.', - ), - ) - else - ...items.map( - (model) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FocusListTileInternal( - title: model.name, - subtitle: model.provider, - trailing: model.id, - ), - ), - ), - ], - ); - } -} class SettingsFocusPreviewInternal extends StatelessWidget { const SettingsFocusPreviewInternal({super.key, required this.controller}); @@ -524,42 +177,41 @@ class FocusListTileInternal extends StatelessWidget { Widget build(BuildContext context) { final palette = context.palette; final theme = Theme.of(context); - return Container( - width: double.infinity, padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), decoration: BoxDecoration( color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(12), border: Border.all(color: palette.strokeSoft), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ], ), ), - const SizedBox(height: 4), - Text( - subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.3, - ), - ), - const SizedBox(height: 8), + const SizedBox(width: 12), Text( trailing, - maxLines: 1, - overflow: TextOverflow.ellipsis, style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w700, color: palette.textPrimary, ), ), @@ -568,59 +220,3 @@ class FocusListTileInternal extends StatelessWidget { ); } } - -class FocusPillInternal extends StatelessWidget { - const FocusPillInternal({super.key, required this.label}); - - final String label; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(999), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - label, - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textSecondary, - ), - ), - ); - } -} - -class PreviewEmptyStateInternal extends StatelessWidget { - const PreviewEmptyStateInternal({super.key, required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - message, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.35, - ), - ), - ); - } -} diff --git a/test/features/app/app_shell_surface_test.dart b/test/features/app/app_shell_surface_test.dart new file mode 100644 index 00000000..f4771189 --- /dev/null +++ b/test/features/app/app_shell_surface_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/app/app_shell_desktop.dart'; +import 'package:xworkmate/theme/app_theme.dart'; + +void main() { + group('AppShell surface cleanup', () { + testWidgets('mobile shell only exposes assistant and settings tabs', ( + tester, + ) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(430, 932); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final controller = AppController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light().copyWith(platform: TargetPlatform.android), + home: AppShell(controller: controller), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('助手'), findsOneWidget); + expect(find.text('设置'), findsOneWidget); + expect(find.text('任务'), findsNothing); + expect(find.text('工作区'), findsNothing); + expect(find.text('密钥'), findsNothing); + }); + + testWidgets('desktop shell switches between assistant and settings', ( + tester, + ) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1440, 960); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final controller = AppController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light().copyWith(platform: TargetPlatform.macOS), + home: AppShell(controller: controller), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('assistant-conversation-shell')), findsOneWidget); + expect(find.byKey(const Key('settings-account-panel-card')), findsNothing); + + controller.openSettings(); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('settings-account-panel-card')), findsOneWidget); + }); + }); +} diff --git a/test/golden/goldens/assistant_lower_pane.png b/test/golden/goldens/assistant_lower_pane.png index 630fb6d2..234954a9 100644 Binary files a/test/golden/goldens/assistant_lower_pane.png and b/test/golden/goldens/assistant_lower_pane.png differ diff --git a/test/runtime/ui_feature_manifest_mobile_surface_test.dart b/test/runtime/ui_feature_manifest_mobile_surface_test.dart new file mode 100644 index 00000000..1bee9c54 --- /dev/null +++ b/test/runtime/ui_feature_manifest_mobile_surface_test.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/ui_feature_manifest.dart'; +import 'package:xworkmate/models/app_models.dart'; + +void main() { + group('Mobile feature manifest cleanup', () { + test('repo config only exposes assistant and settings on mobile', () { + final raw = File('config/feature_flags.yaml').readAsStringSync(); + final manifest = UiFeatureManifest.fromYamlString(raw); + final mobile = manifest.forPlatform( + UiFeaturePlatform.mobile, + buildMode: UiFeatureBuildMode.debug, + ); + + expect( + mobile.allowedDestinations, + { + WorkspaceDestination.assistant, + WorkspaceDestination.settings, + }, + ); + }); + }); +}