Clean C/S surfaces down to assistant and settings
This commit is contained in:
parent
067c673e88
commit
0fdb560ddb
@ -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
|
||||
|
||||
@ -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 baseline:assistant send -> ACP routing -> working directory -> artifact/result |
|
||||
| `Linux` | 共享 desktop Flutter 壳,但未见与 macOS 同强度的平台专项收口 | 进程启动、路径、secure storage、文件选择、CLI 挂载更容易出现平台漂移 | 把 `DesktopPlatformService`、路径规范化、CLI 启动能力做成显式 Linux 验证层,补最小功能矩阵测试 |
|
||||
| `Windows` | 共享 desktop Flutter 壳,但 shell quoting / path separator 风险最高 | task thread working directory、subprocess 参数转义、存储后端兼容性是主要风险点 | 为 Windows 增加工作目录/命令转义专项验证,避免把 macOS 假设直接推广到 Windows |
|
||||
| `iOS` | 移动端主形态是 bridge thin client;本地 runtime 默认关闭 | 当前强项是配对、设置、账户、bridge setup code;弱项是 dormant workspace 目的地仍保留在模型里 | 保持 iOS 只承载 assistant + workspace hub + settings 主链,并把 dormant destinations 从壳层枚举进一步剥离 |
|
||||
| `Android` | 与 iOS 共用 mobile shell,但扫描/权限/系统行为波动更大 | QR pairing、剪贴板、文件选择、通知/后台行为更容易受系统差异影响 | 把扫码、setup-code、连接恢复做成 Android 专项回归集合,确保 bridge thin-client 路线稳定 |
|
||||
- `InstancesController`
|
||||
- `ConnectorsController`
|
||||
|
||||
## Architecture Review Suggestions
|
||||
|
||||
1. **统一 surface 单一事实源**
|
||||
目前 `feature_flags.yaml`、`UiFeatureAccess.destinationMappingsInternal`、`workspace_page_registry.dart`、`AppShell._desktopDestinations` 同时参与裁剪。建议收敛成“manifest -> access -> shell”单链,registry 只保留已允许的规格,避免同一页面在三个地方各自决定是否可达。
|
||||
|
||||
2. **显式区分“当前 surface”与“仓库保留页”**
|
||||
`TasksPage`、`SkillsPage`、`ModulesPage`、`McpServerPage`、`ClawHubPage` 目前都属于“代码存在,但当前主壳层不主推”的状态。建议在目录或文档上明确 `current` / `dormant` / `legacy-present`,降低维护误判。
|
||||
|
||||
3. **把 runtime core 与 page shell 拆开评审**
|
||||
`Agents`、`MCP/ACP`、`Skills` 的真实主链大量在 `lib/runtime` 与 `AppControllerDesktop*` 扩展里,而不在页面壳层。后续评审应以 transport / controller / protocol 为主,不要被 `ModulesPage` 这类聚合页误导。
|
||||
|
||||
4. **确认 ClawHub 的产品定位**
|
||||
当前 `ClawHubPage` 更像一个本地命令台 / placeholder shell,而不是与 `SkillsPage` 同等级的真实数据面。建议要么升级为真实 marketplace backend 面,要么明确标记为 legacy tool shell。
|
||||
|
||||
5. **让生成文档与当前 manifest 同步**
|
||||
仓库已有 `docs/plans/xworkmate-ui-feature-matrix.md`,但它描述的 flag 状态已经落后于当前实现。建议把 feature matrix / inventory 变成可重复生成文档,避免“文档说 enabled,壳层却不显示”的结构漂移。
|
||||
|
||||
## Conclusion: 主链 vs 受限 vs 兼容
|
||||
|
||||
- `主链 / Active`:`Assistant`、`Settings`、`MCP/ACP runtime`、`Agent runtime core`、`Mobile Workspace`、`Feature Manifest Fallback`
|
||||
- `受限 / Gated`:`TasksPage`、`SkillsPage`、`ModulesPage`、`McpServerPage` 以及 mobile workspace 中的 `skills/nodes/agents/mcp_server/claw_hub`
|
||||
- `兼容壳 / Alias`:`navigateTo(aiGateway|secrets)` -> `openSettings(gateway)`、`WorkspaceDestination.account` -> `Settings`
|
||||
- `陈旧残留 / Legacy-present`:`ClawHubPage` 及其命令台式实现、仍保留在 registry 但不属于当前桌面主页面栈的页面规格
|
||||
|
||||
对实现者最重要的结论只有一条:**当前仓库的真实主链不是“所有页面都还在线”,而是“页面、manifest、registry、shell 四层并存,真正当前可达的 surface 已经明显窄于仓库残留代码面”。**
|
||||
- 继续坚持 `feature_flags.yaml -> UiFeatureManifest/AppCapabilities -> Shell/Registry` 的单一 surface 事实源,不再引入第二套 alias 或 dormant registry。
|
||||
- `xworkmate-app` 不再维护独立模块壳;任何新的 bridge 能力都只能落到 `assistant` 或 `settings`,不能恢复 `tasks/modules/...` 独立 page matrix。
|
||||
- provider、routing、bridge endpoint、managed account sync 的真源继续归 `xworkmate-bridge` 合同与同步链拥有,app 只做消费与最小本地编排。
|
||||
- 不再维护兼容 alias、休眠 destination、伪模块矩阵;发现新的 `legacy / fallback / compat` 残留时,默认动作仍然是删除而不是保留占位。
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
@ -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),
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
63
test/features/app/app_shell_surface_test.dart
Normal file
63
test/features/app/app_shell_surface_test.dart
Normal 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 |
26
test/runtime/ui_feature_manifest_mobile_surface_test.dart
Normal file
26
test/runtime/ui_feature_manifest_mobile_surface_test.dart
Normal 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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user