From 0fdb560ddb99ebb11cc1e60e67d9f7fa1bfeb1e5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 13 Apr 2026 12:14:12 +0800 Subject: [PATCH] Clean C/S surfaces down to assistant and settings --- config/feature_flags.yaml | 374 ++------ ...rkmate-core-module-inventory-2026-04-13.md | 412 ++++----- lib/app/app_capabilities.dart | 3 - lib/app/app_controller_desktop_core.dart | 27 +- lib/app/app_controller_desktop_gateway.dart | 4 - .../app_controller_desktop_navigation.dart | 82 +- ...pp_controller_desktop_runtime_helpers.dart | 4 - ...pp_controller_desktop_thread_sessions.dart | 5 - lib/app/app_shell_desktop.dart | 18 +- lib/app/app_store_policy.dart | 32 - lib/app/ui_feature_manifest_core.dart | 48 +- lib/app/workspace_navigation.dart | 16 - lib/app/workspace_page_registry.dart | 80 -- lib/features/claw_hub/claw_hub_page.dart | 503 ----------- lib/features/mcp_server/mcp_server_page.dart | 181 ---- lib/features/mobile/mobile_shell.dart | 1 - lib/features/mobile/mobile_shell_core.dart | 122 +-- lib/features/mobile/mobile_shell_nav.dart | 1 - lib/features/mobile/mobile_shell_sheet.dart | 1 - lib/features/mobile/mobile_shell_strip.dart | 1 - .../mobile/mobile_shell_workspace.dart | 450 ---------- lib/features/modules/modules_page.dart | 799 ------------------ lib/features/skills/skills_page.dart | 480 ----------- lib/features/tasks/tasks_page.dart | 583 ------------- lib/models/app_models.dart | 255 +----- lib/runtime/runtime_controllers_entities.dart | 69 -- lib/widgets/assistant_focus_panel_core.dart | 24 - .../assistant_focus_panel_previews.dart | 456 +--------- test/features/app/app_shell_surface_test.dart | 63 ++ test/golden/goldens/assistant_lower_pane.png | Bin 41657 -> 37898 bytes ..._feature_manifest_mobile_surface_test.dart | 26 + 31 files changed, 366 insertions(+), 4754 deletions(-) delete mode 100644 lib/features/claw_hub/claw_hub_page.dart delete mode 100644 lib/features/mcp_server/mcp_server_page.dart delete mode 100644 lib/features/mobile/mobile_shell_workspace.dart delete mode 100644 lib/features/modules/modules_page.dart delete mode 100644 lib/features/skills/skills_page.dart delete mode 100644 lib/features/tasks/tasks_page.dart create mode 100644 test/features/app/app_shell_surface_test.dart create mode 100644 test/runtime/ui_feature_manifest_mobile_surface_test.dart 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 630fb6d211ba233b39189c33b91464aac87b703a..234954a9e4ac47fe6f7157f56eff93c88e39c122 100644 GIT binary patch literal 37898 zcmeFa2UL^U8Yr3>+gK187@Cw39EvE2gc7<^r3#320;2Q|p*MBJ&}C>!7l?>J=)F2L z=~Y5UdI?2(CvV3&bMDE{th?^I>%H~PTkmE$n!&xl{q?&4-+%`ya%30iFTh|hvitJV z>M+<9a~SNj&`)QAf@5tU?^U^sGYE zt@?z0834}z@$nm7do_M!d)SMtV&sh@6N^lD+2T2TxtrrV;~V(i<`q**zapoM0tEsy zwM>QRw2PY>E($eceCA&rIluq2w^j;mM z2v-k`53|zkWpf|Hz;X37sJkYIo7T$c2I8?+;Sli{wsp0PV_PeEYpL%K^ zeNjKh%x-c2{K0xHZYKV*ooM;D2X!yc(C=5*Kj|OcQJ#)Zv2X+!unC4gq=mqDgN2`E z!t|r7S5xtdo8AGANzV6rrw6VUb?ZO)Fp+VJ4Ab{U$*g;n(o~+a!L3hv7#?200$~bl zj=iULiUT8y+u=>vaVdRjL!~ZHZ+1X-)oL7UhFo3KK<+8jyNPZ-U>f$_ zH<56eJ$^M#@HwoDL55c2nRe+D4Fe07Uqq@f$0=0^#kZx1FTu#y2A=t=TX{JP1z>qL zK4H+^9|EkjwjbM#$m%jNLXHuAX%h_hS}aHkso89|YwYw{whE6m%|oUcd#fwt3EEB# zMOD!jwX^hfp}dI@)tM!FUt5Y1GWV<7tgbZi+~;hFs#1fuo47JTAb%YhtM(ME-YLqF zQA(F0pLr*~HQ3pxShJrQJ}kh(GFAot(h2{luNo;jHxx#2JB-31!)A98Sz|JrKX)*i zzd?C3D4VA9C~;y}!dT24h(`RMpGS7RtYkZlbSh}LHBe_EQQ;7{wDi(y$m2eNIgg{V z#S7lZ$N5xdpAijTxJ*uL@1N}zw5?b5p5daHTI-V9dvmUVj)f($%7L}o9{(t97JmBM z#7+$};oD41>S{TEn6oQCqUp<#!*nF|0b7L5rZ)nIXV%f?z%))gp=LjN72n&W^3#9f z2fpKT1H%1ZHT;EQyoP?&ozVEX_TT9#UPb6csxu!Ay%N*5s-Jj5U(`V!*(2KVx}FXK zf%Y?-41EWsnb5VKT)Gt-_CDRP;S1!xqf)2Hz)V)VR?cMJg3y)d+)@~LC(~UyT(eU`tx6B#>Si2iRzB5 z?*AOI@FIuc9dyx~6Bt0y!Iaw7)Z(&njUPyN8{L+YrcY$+u)mjP%ojYw)|lGcX(WtK z;hCxKtg=fsE}QQXKx{XvigS|ggBeBj(^>3JBpo)Vhw{Z=u^6~SM6cb#$GEI4cgf_t z1KQb$l~SNM@J!VVrG#ux91>nzHmNlb2ery$oXMxm_8l9vuDW)!v1soP)PSZ>FYTQ0XRGUTO(t zSJu!C7P8D>BF?m9Zhf)}oGdK+>M8hrzObb8a3#~xo{MopK2W=}4haf|XR&_glQw6& z-bME4tC#W^%;(F+6*4vw5Wo*%o#G)l+8cAT^yGF3)twLbHx7}rnT}*(Nr|L4KvIV# zoTZO;3DrBH=EcLk()9l5@7VR5I}hp4m;2;-k4sAR&O(6u+3Z{o_rErEi5+a{<>se} z*1LpYNm*6w(`98zX$DSQDwqPYhkIm7nqJ`AcPeNR81r?}o(EPT$3i`?rxG7a6+Co= ztoLjEDjM!Dv9Z@_rxZT{bo6%YY+;ps&lCBg{{4xG>k1hZ6b@T0ZfcID<`E0lzcrm9 z1>m=_`(o8c@VFW0i&$J*+TSsgAnduV#rPsvFrON?Z?BG3T^;w?`trfMX25lsFDZa9 z>w8>eKIGDsM`i)^xY^-AeKNJqLRxw{eiC@L&zL@$_-mNL{1X;*X}PS^oBKG3FyR{v z7d9BES#a=2qs{C_&@_ggx72%YZhP+@0QWG*bM$!AKyZG5JC$utzG-73t@ZVRx=K

w(oYMqDcG7KaC9PpypGic?aM#0DlQX#6nF z*C~^QtkdUsW9?6?oB(*1wxQ+iKKgLH`n$Wy5F7S0$S(@_zbYtA|LA7Id3fLFr<`4{ z&(K5Q5Ad>(ji^=&c0Gvsk?j5n7>w6@YO8=wKV>o(^1f~M4C=mJBIHlCMxP=plASKs z2Wc9~$B&V%r#1l41$2)b0gdMaaBy;%B-H7RNTogVJP#)U1cTLjqv5$qQ1oC-U$6Jr z&&{?gQd-QjMci#F&OQw#@awh2gORz(2%c(Z3l_3*kG)nFGF8cwkS6v&eZmC58LBoy zWWI5BGPHU-h=vR$UQwvhl^R$0?a{i5-9ZqMO=}R=27@ ztZ6uI<-kx`IIy#%Q*pNMK=}X7!EUsSl5p%f9zs*I|K;7Qb?xI_a>DK7otk4@>h{s& z@#BrorWiwlY@w&;i_m4`?Z(t7Cc+VU81=9_#;p9bLPqx%|N8=QQv;Br) zZI`i(ZKzIl9Sg9lI-L1S+MK89SJGuSy+d?x%;)G@x>$#VFp=}+KNn!bs&gnFLxn}r z6zk!(Cj-nE+?<8bz?$W%ABxdZZlnkE{GQ-oK zIptb7LH6qy2>&dvl_M*Ci(esIO);c8dj8;NjMw2%T4YKyr(<=N7KE8!*D6tSDu-X& z1t-65FMqZX*|hIi9tBzQa85OS(JIZsc9O4n_2#}{%INS_XX~Azt6`5k4tFF(b*(SL z88Z1(@CT(OtsvA66$cbIhySrFDJ`ANUtV3Gg-=~=ecPlSXg)mKBg+NhBhxGSI?HUn zuGd-~?{BG@FfnL{@&QlTax8>h3Nz^wn7pK@uE1*O)P$b({{`1yY55!H^ zUM}~szmnv&hADO0Fpb1s^$?odUIN+dKMF%2a&zq5tDvaOPW@rzLNipb)Cy1=Q5FAIp7%pKgS?;Psl(?>rh1k{XTY|iEY1sF8Z?^|Oj&`xL>&lan1_?dT z_ycC9Tg9%zUG{b$G*D@6%XLqS&X*Qzj>mumyW{i)S>;gY^DJaV{U1=`W`VG}#ukK( z3a5IkZxX2=HHtvP^(#?l>0OGYK~$`cZ=`cn0ntr6eu5JeR+$G=pB3-=VBMu3Rs7+~&LL@sLzJw_T6E{wlsVsptS47l(l$Q6~nk3QoSn83y&i zgS_QdD=CR?s#}@uMW{_ek0&z&cQlX0e;D)B8&vv)AIL;UndKA~!gHp&Ewk6UyWuTs zExRD$8CpJ%jHK`0AtzS^2*#_YMY7CI4M(!fP?>fw_MtyuWHb0Jv(;MGY_hv|W}f9J zax(gwS}}5yqB(-ob6u$?AM;|pY�QY=M2H}XB(KU&OGJMhV~BkavhDdzaH4pmj% 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 Fe*-ckSkM3f 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, + }, + ); + }); + }); +}