Merge branch 'codex/cs-mainline-cleanup'

This commit is contained in:
Haitao Pan 2026-04-13 12:14:17 +08:00
commit 5b347ea239
31 changed files with 366 additions and 4754 deletions

View File

@ -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

View File

@ -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<br/>workspace_navigation.dart<br/>ui_feature_manifest_core.dart<br/>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<br/>feature switches"]
D1["Desktop APP<br/>AppShell._desktopDestinations"]
D2["Mobile APP<br/>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 baselineassistant 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` 残留时,默认动作仍然是删除而不是保留占位。

View File

@ -8,7 +8,6 @@ class AppCapabilities {
required this.supportsLocalGateway,
required this.supportsRelayGateway,
required this.supportsDesktopRuntime,
required this.supportsDiagnostics,
});
final Set<WorkspaceDestination> 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,
);
}
}

View File

@ -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<GatewaySessionSummary> get assistantSessions =>
assistantSessionsInternal();
List<GatewayInstanceSummary> get instances =>
instancesControllerInternal.items;
List<GatewaySkillSummary> get skills => skillsControllerInternal.items;
List<GatewayConnectorSummary> get connectors =>
connectorsControllerInternal.items;
List<GatewayModelSummary> get models => modelsControllerInternal.items;
List<GatewayCronJobSummary> 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,

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -321,11 +321,6 @@ extension AppControllerDesktopThreadSessions on AppController {
String gatewayAddressLabelInternal(GatewayConnectionProfile profile) =>
gatewayAddressLabelThreadSessionInternal(profile);
List<SecretReferenceEntry> get secretReferences =>
settingsControllerInternal.buildSecretReferences();
List<SecretAuditEntry> get secretAuditTrail =>
settingsControllerInternal.auditTrail;
List<RuntimeLogEntry> get runtimeLogs => runtimeInternal.logs;
List<AssistantFocusEntry> get assistantNavigationDestinations =>
normalizeAssistantNavigationDestinations(
appUiState.assistantNavigationDestinations,

View File

@ -34,9 +34,6 @@ class _AppShellState extends State<AppShell> {
static const _mobileDestinations = [
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.skills,
WorkspaceDestination.secrets,
WorkspaceDestination.settings,
];
@ -189,10 +186,7 @@ class _AppShellState extends State<AppShell> {
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<AppShell> {
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<AppShell> {
WorkspaceDestination _resolveDesktopDestination(
WorkspaceDestination destination,
) {
if (destination == WorkspaceDestination.account) {
return WorkspaceDestination.settings;
}
if (_desktopDestinations.contains(destination)) {
return destination;
}

View File

@ -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',

View File

@ -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, Map<String, WorkspaceDestination>>{
UiFeaturePlatform.mobile: <String, WorkspaceDestination>{
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: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
@ -397,11 +362,7 @@ class UiFeatureAccess {
},
UiFeaturePlatform.web: <String, WorkspaceDestination>{
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);

View File

@ -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);

View File

@ -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 = <WorkspaceDestination, WorkspacePageSpec>{
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 = <WorkspaceDestination, WorkspacePageSpec>{
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({

View File

@ -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<DetailPanelData> onOpenDetail;
@override
State<ClawHubPage> createState() => ClawHubPageStateInternal();
}
class ClawHubPageStateInternal extends State<ClawHubPage> {
final searchControllerInternal = TextEditingController();
final commandControllerInternal = TextEditingController();
final scrollControllerInternal = ScrollController();
final List<ClawHubLogEntry> 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) : <String>[];
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<String> args) {
final query = args.join(' ');
if (query.isEmpty) {
addLogInternal(
'Usage: clawhub search "<query>"',
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 <slug>" to install a package.');
});
}
void handleInstallInternal(List<String> args) {
if (args.isEmpty) {
addLogInternal(
'Usage: clawhub install <slug>',
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<String> 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 <slug> or clawhub update --all',
type: ClawHubLogType.warning,
);
setState(() => isExecutingInternal = false);
}
}
void showHelpInternal() {
addLogInternal('');
addLogInternal('ClawHub Package Manager', type: ClawHubLogType.success);
addLogInternal('Usage: clawhub <command> [options]');
addLogInternal('');
addLogInternal('Commands:');
addLogInternal(' search "<query>" Search for packages');
addLogInternal(' install <slug> Install a package');
addLogInternal(' update <slug> 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,
),
),
],
),
),
),
);
}
}

View File

@ -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<DetailPanelData> 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(),
),
],
),
);
},
);
}
}

View File

@ -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';

View File

@ -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<MobileShell> {
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<MobileShell> {
rootLabel: appText('移动端', 'Mobile'),
destination: WorkspaceDestination.settings,
sectionLabel: appText('集成', 'Integrations'),
settingsTab: SettingsTab.gateway,
gatewayProfileIndex: kGatewayRemoteProfileIndex,
prefersGatewaySetupCode: false,
),
@ -185,6 +108,7 @@ class MobileShellStateInternal extends State<MobileShell> {
rootLabel: appText('移动端', 'Mobile'),
destination: WorkspaceDestination.settings,
sectionLabel: appText('集成', 'Integrations'),
settingsTab: SettingsTab.gateway,
gatewayProfileIndex: kGatewayRemoteProfileIndex,
prefersGatewaySetupCode: true,
),
@ -258,9 +182,6 @@ class MobileShellStateInternal extends State<MobileShell> {
);
return;
}
if (!mounted) {
return;
}
final codeController = TextEditingController();
final enteredCode = await showDialog<String>(
context: context,
@ -347,18 +268,8 @@ class MobileShellStateInternal extends State<MobileShell> {
}
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<MobileShell> {
final availableTabs = <MobileShellTab>[
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<String>('mobile-shell-workspace')
: ValueKey<String>(
'mobile-shell-${widget.controller.destination.name}',
);
final destinationKey = ValueKey<String>(
'mobile-shell-${widget.controller.destination.name}',
);
final detailPanel = widget.controller.detailPanel;
final palette = context.palette;
return Scaffold(

View File

@ -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({

View File

@ -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 {

View File

@ -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 {

View File

@ -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<WorkspaceDestination> onSelectDestination;
@override
Widget build(BuildContext context) {
final connection = controller.connection;
final palette = context.palette;
final features = controller.featuresFor(UiFeaturePlatform.mobile);
final entries =
<WorkspaceEntryInternal>[
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),
),
);
}
}

View File

@ -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<DetailPanelData> onOpenDetail;
final ModulesTab? initialTab;
@override
State<ModulesPage> createState() => _ModulesPageState();
}
class _ModulesPageState extends State<ModulesPage> {
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<ModulesTab> 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<DetailPanelData> 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<DetailPanelData> 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<DetailPanelData> 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<GatewaySkillSummary> 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: <String>[
appText('统一路由', 'Unified routing'),
appText('xworkmate-bridge', 'xworkmate-bridge'),
],
skills: currentMode == AssistantExecutionTarget.gateway
? gatewaySkills.map((item) => item.name).toList()
: const <String>[],
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<String> chips;
final List<String> 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);
}

View File

@ -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<DetailPanelData> onOpenDetail;
@override
State<SkillsPage> createState() => _SkillsPageState();
}
class _SkillsPageState extends State<SkillsPage> {
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<GatewaySkillSummary> 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<GatewaySkillSummary> skills;
final String? selectedSkillKey;
final ValueChanged<GatewaySkillSummary> 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<String> 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);

View File

@ -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<DetailPanelData> onOpenDetail;
@override
State<TasksPage> createState() => _TasksPageState();
}
class _TasksPageState extends State<TasksPage> {
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<DerivedTaskItem> 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<DerivedTaskItem> items;
final String? selectedTaskId;
final ValueChanged<DerivedTaskItem> 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<String>('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),
};

View File

@ -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<AssistantFocusEntry> kAssistantNavigationDestinationDefaults =
const List<AssistantFocusEntry> kAssistantNavigationDestinationCandidates =
<AssistantFocusEntry>[
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;

View File

@ -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<GatewayInstanceSummary> itemsInternal = const <GatewayInstanceSummary>[];
bool loadingInternal = false;
String? errorInternal;
List<GatewayInstanceSummary> get items => itemsInternal;
bool get loading => loadingInternal;
String? get error => errorInternal;
Future<void> refresh() async {
if (!runtimeInternal.isConnected) {
itemsInternal = const <GatewayInstanceSummary>[];
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<GatewayConnectorSummary> itemsInternal =
const <GatewayConnectorSummary>[];
bool loadingInternal = false;
String? errorInternal;
List<GatewayConnectorSummary> get items => itemsInternal;
bool get loading => loadingInternal;
String? get error => errorInternal;
Future<void> refresh() async {
if (!runtimeInternal.isConnected) {
itemsInternal = const <GatewayConnectorSummary>[];
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);

View File

@ -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,
),

View File

@ -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 = <DerivedTaskItem>[
...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<String>()
.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,
),
),
);
}
}

View File

@ -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);
});
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -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>{
WorkspaceDestination.assistant,
WorkspaceDestination.settings,
},
);
});
});
}