refactor: unify settings drill-in navigation
This commit is contained in:
parent
50b8a4dc98
commit
e988c8e23b
137
docs/plans/2026-03-20.md
Normal file
137
docs/plans/2026-03-20.md
Normal file
@ -0,0 +1,137 @@
|
||||
# XWorkmate 菜单层级、状态页与面包屑优化实施计划
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
本轮改造的首要约束是:保持截图对应的 `Assistant` 首页 UI 设计、侧栏分配和主壳布局不变,不新增新的一级路由,不改变首页主工作区的视觉结构。
|
||||
|
||||
在这个前提下,导航和页面职责收敛为稳定的三级模型:
|
||||
|
||||
- 1 级:保持现有 `WorkspaceDestination` 一级入口与侧栏分配
|
||||
- 2 级:配置相关页面只负责快速查看状态和少量高频操作
|
||||
- 3 级:详细设置参数统一进入 `Settings` 体系下的独立 detail 视图
|
||||
|
||||
本轮默认决策:
|
||||
|
||||
- `Assistant` 首页是唯一 Home
|
||||
- `主页` 面包屑统一调用 `navigateHome()`
|
||||
- 配置型页面采用 `二级状态页 -> 三级参数页`
|
||||
- 二级页可以快速查看状态,但不再承载完整高级表单
|
||||
- 三级参数页挂在 `Settings` 体系内,不新增新的一级路由
|
||||
- 本轮不做 runtime 大拆分,但不得新增新的 `runtime -> app` 反向依赖
|
||||
|
||||
## 2. 导航层级与页面职责
|
||||
|
||||
### 一级入口保持不变
|
||||
|
||||
现有一级入口继续保留:
|
||||
|
||||
- `Assistant`
|
||||
- `Tasks`
|
||||
- `Skills`
|
||||
- `Nodes`
|
||||
- `Agents`
|
||||
- `MCP Hub`
|
||||
- `ClawHub`
|
||||
- `Secrets`
|
||||
- `AI Gateway`
|
||||
- `Settings`
|
||||
- `Account`
|
||||
|
||||
### 二级状态页
|
||||
|
||||
本轮重点覆盖四类配置型页面:
|
||||
|
||||
- `Modules`
|
||||
- `AI Gateway`
|
||||
- `Secrets`
|
||||
- `Settings`
|
||||
|
||||
二级状态页统一遵守以下边界:
|
||||
|
||||
- 展示连接状态、健康状态、当前模式、最近结果和关键摘要
|
||||
- 保留少量高频动作,例如刷新、重试连接、进入编辑设置
|
||||
- 不再承载完整高级表单
|
||||
- 不再出现重复的“打开全页”式独立配置入口
|
||||
|
||||
### 三级参数页
|
||||
|
||||
详细参数通过 `Settings` 的 detail 视图承载,第一批 detail 包含:
|
||||
|
||||
- `Gateway Connection`
|
||||
- `AI Gateway Integration`
|
||||
- `Vault Provider`
|
||||
- `Ollama Provider`
|
||||
- `External Agents`
|
||||
- `Advanced Diagnostics`
|
||||
|
||||
`Settings` 是详细参数的唯一权威入口。`Modules / AI Gateway / Secrets` 只负责状态和进入编辑。
|
||||
|
||||
## 3. 架构收口与导航注册表
|
||||
|
||||
### 控制器状态
|
||||
|
||||
`AppController` 承担统一导航状态入口,新增以下能力:
|
||||
|
||||
- 配置型页面的 tab 状态
|
||||
- `Settings` drill-in 状态
|
||||
- `openSettings({tab, detail})`
|
||||
- `closeSettingsDetail()`
|
||||
- `navigateHome()` 返回 `Assistant` 首页并清空 detail 状态
|
||||
|
||||
### 共享导航注册表
|
||||
|
||||
`AppShell` 与 `MobileShell` 共用同一套页面注册元数据,统一描述:
|
||||
|
||||
- 一级入口
|
||||
- Desktop / Mobile 共用的页面 builder
|
||||
- `Settings` detail 的初始 tab / detail 注入
|
||||
- `Modules / Secrets / AI Gateway` 的初始 tab 注入
|
||||
|
||||
本轮优先收口 `AppShell / MobileShell` 的 destination -> page 映射重复,不在 feature 页面里继续扩散新的页面分发逻辑。
|
||||
|
||||
### 面包屑规则
|
||||
|
||||
非首页页面统一支持通过面包屑回到 `Assistant` 首页:
|
||||
|
||||
- 一级工作页:`主页 / 当前页`
|
||||
- 二级状态页:`主页 / 一级入口 / 当前状态页`
|
||||
- 三级参数页:`主页 / 一级入口 / 二级状态页 / 当前参数页`
|
||||
|
||||
`Settings` detail 页面允许保留来源上下文。例如:
|
||||
|
||||
- `主页 / 模块 / 网关 / Gateway 连接参数`
|
||||
- `主页 / 密钥 / Vault / Vault 提供方参数`
|
||||
- `主页 / AI Gateway / 模型 / AI Gateway 集成参数`
|
||||
|
||||
## 4. 测试与验收标准
|
||||
|
||||
### 功能验收
|
||||
|
||||
- 首页截图对应 UI、侧栏、主工作区、输入区视觉不变
|
||||
- `Modules / AI Gateway / Secrets` 只展示状态摘要和少量高频动作
|
||||
- 详细参数通过 `Settings` detail 页面编辑
|
||||
- 任意非首页页面可通过 `主页` 面包屑回到 `Assistant` 首页主路由
|
||||
- `Settings` detail 页面可通过来源面包屑返回对应二级状态页
|
||||
|
||||
### 架构验收
|
||||
|
||||
- `AppShell` 与 `MobileShell` 共用同一套页面注册元数据
|
||||
- 本次导航改造不新增新的 `runtime -> app` 反向依赖
|
||||
- 导航规则和 breadcrumb 生成逻辑不再散落在各 feature 页面中重复实现
|
||||
|
||||
### 回归测试
|
||||
|
||||
本轮至少覆盖以下回归:
|
||||
|
||||
- `settings_page`
|
||||
- `modules_page`
|
||||
- `ai_gateway_page`
|
||||
- `assistant_page`
|
||||
- `sidebar_navigation`
|
||||
|
||||
补充的行为验证:
|
||||
|
||||
- 二级页点击 `编辑设置` 进入对应三级参数页
|
||||
- 三级参数页 breadcrumb 可返回来源状态页
|
||||
- 任意非首页页点击 `主页` 回到 `Assistant` 首页主路由
|
||||
- 二级页不再承载完整高级表单
|
||||
@ -128,6 +128,12 @@ class AppController extends ChangeNotifier {
|
||||
WorkspaceDestination _destination = WorkspaceDestination.assistant;
|
||||
ThemeMode _themeMode = ThemeMode.light;
|
||||
AppSidebarState _sidebarState = AppSidebarState.expanded;
|
||||
ModulesTab _modulesTab = ModulesTab.gateway;
|
||||
SecretsTab _secretsTab = SecretsTab.vault;
|
||||
AiGatewayTab _aiGatewayTab = AiGatewayTab.models;
|
||||
SettingsTab _settingsTab = SettingsTab.general;
|
||||
SettingsDetailPage? _settingsDetail;
|
||||
SettingsNavigationContext? _settingsNavigationContext;
|
||||
DetailPanelData? _detailPanel;
|
||||
bool _initializing = true;
|
||||
String? _bootstrapError;
|
||||
@ -137,6 +143,13 @@ class AppController extends ChangeNotifier {
|
||||
WorkspaceDestination get destination => _destination;
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
AppSidebarState get sidebarState => _sidebarState;
|
||||
ModulesTab get modulesTab => _modulesTab;
|
||||
SecretsTab get secretsTab => _secretsTab;
|
||||
AiGatewayTab get aiGatewayTab => _aiGatewayTab;
|
||||
SettingsTab get settingsTab => _settingsTab;
|
||||
SettingsDetailPage? get settingsDetail => _settingsDetail;
|
||||
SettingsNavigationContext? get settingsNavigationContext =>
|
||||
_settingsNavigationContext;
|
||||
DetailPanelData? get detailPanel => _detailPanel;
|
||||
bool get initializing => _initializing;
|
||||
String? get bootstrapError => _bootstrapError;
|
||||
@ -670,10 +683,7 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
byKey.putIfAbsent(
|
||||
normalizedSessionKey,
|
||||
() => _assistantSessionSummaryFor(
|
||||
normalizedSessionKey,
|
||||
record: record,
|
||||
),
|
||||
() => _assistantSessionSummaryFor(normalizedSessionKey, record: record),
|
||||
);
|
||||
}
|
||||
|
||||
@ -701,10 +711,25 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void navigateTo(WorkspaceDestination destination) {
|
||||
if (_destination == destination) {
|
||||
final nextModulesTab = switch (destination) {
|
||||
WorkspaceDestination.nodes => ModulesTab.nodes,
|
||||
WorkspaceDestination.agents => ModulesTab.agents,
|
||||
_ => _modulesTab,
|
||||
};
|
||||
final shouldClearSettingsDrillIn =
|
||||
_settingsDetail != null || _settingsNavigationContext != null;
|
||||
final changed =
|
||||
_destination != destination ||
|
||||
_detailPanel != null ||
|
||||
shouldClearSettingsDrillIn ||
|
||||
nextModulesTab != _modulesTab;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_destination = destination;
|
||||
_modulesTab = nextModulesTab;
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
_detailPanel = null;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -716,9 +741,13 @@ class AppController extends ChangeNotifier {
|
||||
: 'main';
|
||||
final destinationChanged = _destination != WorkspaceDestination.assistant;
|
||||
final detailChanged = _detailPanel != null;
|
||||
final settingsDrillInChanged =
|
||||
_settingsDetail != null || _settingsNavigationContext != null;
|
||||
_destination = WorkspaceDestination.assistant;
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
_detailPanel = null;
|
||||
if (destinationChanged || detailChanged) {
|
||||
if (destinationChanged || detailChanged || settingsDrillInChanged) {
|
||||
notifyListeners();
|
||||
}
|
||||
if (_sessionsController.currentSessionKey != mainSessionKey) {
|
||||
@ -726,6 +755,135 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void openModules({ModulesTab tab = ModulesTab.gateway}) {
|
||||
final destination = tab == ModulesTab.agents
|
||||
? WorkspaceDestination.agents
|
||||
: WorkspaceDestination.nodes;
|
||||
final changed =
|
||||
_destination != destination ||
|
||||
_modulesTab != tab ||
|
||||
_detailPanel != null ||
|
||||
_settingsDetail != null ||
|
||||
_settingsNavigationContext != null;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_destination = destination;
|
||||
_modulesTab = tab;
|
||||
_detailPanel = null;
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setModulesTab(ModulesTab tab) {
|
||||
if (_modulesTab == tab) {
|
||||
return;
|
||||
}
|
||||
_modulesTab = tab;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void openSecrets({SecretsTab tab = SecretsTab.vault}) {
|
||||
final changed =
|
||||
_destination != WorkspaceDestination.secrets ||
|
||||
_secretsTab != tab ||
|
||||
_detailPanel != null ||
|
||||
_settingsDetail != null ||
|
||||
_settingsNavigationContext != null;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_destination = WorkspaceDestination.secrets;
|
||||
_secretsTab = tab;
|
||||
_detailPanel = null;
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSecretsTab(SecretsTab tab) {
|
||||
if (_secretsTab == tab) {
|
||||
return;
|
||||
}
|
||||
_secretsTab = tab;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void openAiGateway({AiGatewayTab tab = AiGatewayTab.models}) {
|
||||
final changed =
|
||||
_destination != WorkspaceDestination.aiGateway ||
|
||||
_aiGatewayTab != tab ||
|
||||
_detailPanel != null ||
|
||||
_settingsDetail != null ||
|
||||
_settingsNavigationContext != null;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_destination = WorkspaceDestination.aiGateway;
|
||||
_aiGatewayTab = tab;
|
||||
_detailPanel = null;
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setAiGatewayTab(AiGatewayTab tab) {
|
||||
if (_aiGatewayTab == tab) {
|
||||
return;
|
||||
}
|
||||
_aiGatewayTab = tab;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void openSettings({
|
||||
SettingsTab tab = SettingsTab.general,
|
||||
SettingsDetailPage? detail,
|
||||
SettingsNavigationContext? navigationContext,
|
||||
}) {
|
||||
final resolvedTab = detail?.tab ?? tab;
|
||||
final changed =
|
||||
_destination != WorkspaceDestination.settings ||
|
||||
_settingsTab != resolvedTab ||
|
||||
_settingsDetail != detail ||
|
||||
_settingsNavigationContext != navigationContext ||
|
||||
_detailPanel != null;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_destination = WorkspaceDestination.settings;
|
||||
_settingsTab = resolvedTab;
|
||||
_settingsDetail = detail;
|
||||
_settingsNavigationContext = navigationContext;
|
||||
_detailPanel = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSettingsTab(SettingsTab tab, {bool clearDetail = true}) {
|
||||
final changed =
|
||||
_settingsTab != tab ||
|
||||
(clearDetail &&
|
||||
(_settingsDetail != null || _settingsNavigationContext != null));
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
_settingsTab = tab;
|
||||
if (clearDetail) {
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void closeSettingsDetail() {
|
||||
if (_settingsDetail == null && _settingsNavigationContext == null) {
|
||||
return;
|
||||
}
|
||||
_settingsDetail = null;
|
||||
_settingsNavigationContext = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void cycleSidebarState() {
|
||||
_sidebarState = switch (_sidebarState) {
|
||||
AppSidebarState.expanded => AppSidebarState.collapsed,
|
||||
@ -1219,9 +1377,11 @@ class AppController extends ChangeNotifier {
|
||||
sessionKey,
|
||||
title: title.trim(),
|
||||
executionTarget:
|
||||
executionTarget ?? assistantExecutionTargetForSession(currentSessionKey),
|
||||
executionTarget ??
|
||||
assistantExecutionTargetForSession(currentSessionKey),
|
||||
messageViewMode:
|
||||
messageViewMode ?? assistantMessageViewModeForSession(currentSessionKey),
|
||||
messageViewMode ??
|
||||
assistantMessageViewModeForSession(currentSessionKey),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
_notifyIfActive();
|
||||
|
||||
@ -1,24 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/account/account_page.dart';
|
||||
import '../features/ai_gateway/ai_gateway_page.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/mobile/mobile_shell.dart';
|
||||
import '../features/modules/modules_page.dart';
|
||||
import '../features/secrets/secrets_page.dart';
|
||||
import '../features/settings/settings_page.dart';
|
||||
import '../features/skills/skills_page.dart';
|
||||
import '../features/tasks/tasks_page.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import '../widgets/assistant_focus_panel.dart';
|
||||
import '../widgets/detail_drawer.dart';
|
||||
import '../widgets/pane_resize_handle.dart';
|
||||
import '../widgets/sidebar_navigation.dart';
|
||||
import 'app_controller.dart';
|
||||
import 'workspace_page_registry.dart';
|
||||
|
||||
class AppShell extends StatefulWidget {
|
||||
const AppShell({super.key, required this.controller});
|
||||
@ -399,59 +390,12 @@ class _AppShellState extends State<AppShell> {
|
||||
WorkspaceDestination destination,
|
||||
ValueChanged<DetailPanelData> onOpenDetail,
|
||||
) {
|
||||
return switch (destination) {
|
||||
WorkspaceDestination.assistant => AssistantPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
navigationPanelBuilder:
|
||||
widget.controller.sidebarState == AppSidebarState.hidden
|
||||
? null
|
||||
: (_) => AssistantFocusPanel(controller: widget.controller),
|
||||
showStandaloneTaskRail: false,
|
||||
unifiedPaneStartsCollapsed:
|
||||
widget.controller.sidebarState == AppSidebarState.collapsed,
|
||||
),
|
||||
WorkspaceDestination.tasks => TasksPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.skills => SkillsPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.nodes => ModulesPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: ModulesTab.nodes,
|
||||
),
|
||||
WorkspaceDestination.agents => ModulesPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: ModulesTab.agents,
|
||||
),
|
||||
WorkspaceDestination.mcpServer => McpServerPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.clawHub => ClawHubPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.secrets => SecretsPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.aiGateway => AiGatewayPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.settings => SettingsPage(
|
||||
controller: widget.controller,
|
||||
),
|
||||
WorkspaceDestination.account => AccountPage(
|
||||
controller: widget.controller,
|
||||
),
|
||||
};
|
||||
return buildWorkspacePage(
|
||||
destination: destination,
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
surface: WorkspacePageSurface.desktop,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
79
lib/app/workspace_navigation.dart
Normal file
79
lib/app/workspace_navigation.dart
Normal file
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../widgets/top_bar.dart';
|
||||
import 'app_controller.dart';
|
||||
|
||||
List<AppBreadcrumbItem> buildWorkspaceBreadcrumbs({
|
||||
required AppController controller,
|
||||
required String rootLabel,
|
||||
String? sectionLabel,
|
||||
String? detailLabel,
|
||||
VoidCallback? onRootTap,
|
||||
}) {
|
||||
final items = <AppBreadcrumbItem>[
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
AppBreadcrumbItem(label: rootLabel, onTap: onRootTap),
|
||||
];
|
||||
if (sectionLabel != null && sectionLabel.trim().isNotEmpty) {
|
||||
items.add(AppBreadcrumbItem(label: sectionLabel));
|
||||
}
|
||||
if (detailLabel != null && detailLabel.trim().isNotEmpty) {
|
||||
items.add(AppBreadcrumbItem(label: detailLabel));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
List<AppBreadcrumbItem> buildSettingsBreadcrumbs(
|
||||
AppController controller, {
|
||||
required SettingsTab tab,
|
||||
SettingsDetailPage? detail,
|
||||
SettingsNavigationContext? navigationContext,
|
||||
}) {
|
||||
if (detail == null) {
|
||||
return buildWorkspaceBreadcrumbs(
|
||||
controller: controller,
|
||||
rootLabel: appText('设置', 'Settings'),
|
||||
sectionLabel: tab.label,
|
||||
);
|
||||
}
|
||||
|
||||
return buildWorkspaceBreadcrumbs(
|
||||
controller: controller,
|
||||
rootLabel: navigationContext?.rootLabel ?? appText('设置', 'Settings'),
|
||||
sectionLabel: navigationContext?.sectionLabel ?? tab.label,
|
||||
detailLabel: detail.label,
|
||||
onRootTap: navigationContext == null
|
||||
? () => controller.openSettings(tab: tab)
|
||||
: () => openSettingsNavigationContext(controller, navigationContext),
|
||||
);
|
||||
}
|
||||
|
||||
void openSettingsNavigationContext(
|
||||
AppController controller,
|
||||
SettingsNavigationContext context,
|
||||
) {
|
||||
if (context.modulesTab != null) {
|
||||
controller.openModules(tab: context.modulesTab!);
|
||||
return;
|
||||
}
|
||||
if (context.secretsTab != null) {
|
||||
controller.openSecrets(tab: context.secretsTab!);
|
||||
return;
|
||||
}
|
||||
if (context.aiGatewayTab != null) {
|
||||
controller.openAiGateway(tab: context.aiGatewayTab!);
|
||||
return;
|
||||
}
|
||||
if (context.settingsTab != null ||
|
||||
context.destination == WorkspaceDestination.settings) {
|
||||
controller.openSettings(tab: context.settingsTab ?? SettingsTab.general);
|
||||
return;
|
||||
}
|
||||
controller.navigateTo(context.destination);
|
||||
}
|
||||
176
lib/app/workspace_page_registry.dart
Normal file
176
lib/app/workspace_page_registry.dart
Normal file
@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/account/account_page.dart';
|
||||
import '../features/ai_gateway/ai_gateway_page.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/secrets/secrets_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 '../widgets/assistant_focus_panel.dart';
|
||||
import 'app_controller.dart';
|
||||
|
||||
enum WorkspacePageSurface { desktop, mobile }
|
||||
|
||||
typedef WorkspacePageBuilder =
|
||||
Widget Function(
|
||||
AppController controller,
|
||||
ValueChanged<DetailPanelData> onOpenDetail,
|
||||
);
|
||||
|
||||
class WorkspacePageSpec {
|
||||
const WorkspacePageSpec({
|
||||
required this.destination,
|
||||
required this.desktopBuilder,
|
||||
required this.mobileBuilder,
|
||||
});
|
||||
|
||||
final WorkspaceDestination destination;
|
||||
final WorkspacePageBuilder desktopBuilder;
|
||||
final WorkspacePageBuilder mobileBuilder;
|
||||
}
|
||||
|
||||
final Map<WorkspaceDestination, WorkspacePageSpec> _workspacePageSpecs =
|
||||
<WorkspaceDestination, WorkspacePageSpec>{
|
||||
WorkspaceDestination.assistant: WorkspacePageSpec(
|
||||
destination: WorkspaceDestination.assistant,
|
||||
desktopBuilder: (controller, onOpenDetail) => AssistantPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
navigationPanelBuilder:
|
||||
controller.sidebarState == AppSidebarState.hidden
|
||||
? null
|
||||
: (_) => AssistantFocusPanel(controller: controller),
|
||||
showStandaloneTaskRail: false,
|
||||
unifiedPaneStartsCollapsed:
|
||||
controller.sidebarState == AppSidebarState.collapsed,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => AssistantPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
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) => SecretsPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: controller.secretsTab,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => SecretsPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: controller.secretsTab,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.aiGateway: WorkspacePageSpec(
|
||||
destination: WorkspaceDestination.aiGateway,
|
||||
desktopBuilder: (controller, onOpenDetail) => AiGatewayPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: controller.aiGatewayTab,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => AiGatewayPage(
|
||||
controller: controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
initialTab: controller.aiGatewayTab,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.settings: WorkspacePageSpec(
|
||||
destination: WorkspaceDestination.settings,
|
||||
desktopBuilder: (controller, onOpenDetail) => SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.account: WorkspacePageSpec(
|
||||
destination: WorkspaceDestination.account,
|
||||
desktopBuilder: (controller, onOpenDetail) =>
|
||||
AccountPage(controller: controller),
|
||||
mobileBuilder: (controller, onOpenDetail) =>
|
||||
AccountPage(controller: controller),
|
||||
),
|
||||
};
|
||||
|
||||
Widget buildWorkspacePage({
|
||||
required WorkspaceDestination destination,
|
||||
required AppController controller,
|
||||
required ValueChanged<DetailPanelData> onOpenDetail,
|
||||
required WorkspacePageSurface surface,
|
||||
}) {
|
||||
final spec = _workspacePageSpecs[destination]!;
|
||||
return switch (surface) {
|
||||
WorkspacePageSurface.desktop => spec.desktopBuilder(
|
||||
controller,
|
||||
onOpenDetail,
|
||||
),
|
||||
WorkspacePageSurface.mobile => spec.mobileBuilder(controller, onOpenDetail),
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
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/platform_environment.dart';
|
||||
@ -16,17 +17,34 @@ class AiGatewayPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
this.initialTab,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
final AiGatewayTab? initialTab;
|
||||
|
||||
@override
|
||||
State<AiGatewayPage> createState() => _AiGatewayPageState();
|
||||
}
|
||||
|
||||
class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
AiGatewayTab _tab = AiGatewayTab.models;
|
||||
late AiGatewayTab _tab;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tab = widget.initialTab ?? widget.controller.aiGatewayTab;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AiGatewayPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final nextTab = widget.initialTab ?? widget.controller.aiGatewayTab;
|
||||
if (nextTab != _tab) {
|
||||
setState(() => _tab = nextTab);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -67,20 +85,24 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: [
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
const AppBreadcrumbItem(label: 'AI Gateway'),
|
||||
AppBreadcrumbItem(label: _tab.label),
|
||||
],
|
||||
breadcrumbs: buildWorkspaceBreadcrumbs(
|
||||
controller: controller,
|
||||
rootLabel: 'AI Gateway',
|
||||
sectionLabel: _tab.label,
|
||||
),
|
||||
title: 'AI Gateway',
|
||||
subtitle: appText(
|
||||
'AI 代理与模型网关配置管理中心。',
|
||||
'AI proxy and model gateway configuration center.',
|
||||
),
|
||||
trailing: FilledButton.tonalIcon(
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: _aiGatewayDetailForTab(_tab),
|
||||
navigationContext: _aiGatewayNavigationContext(_tab),
|
||||
),
|
||||
icon: const Icon(Icons.tune_rounded),
|
||||
label: Text(appText('编辑设置', 'Edit settings')),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Wrap(
|
||||
@ -92,11 +114,12 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
SectionTabs(
|
||||
items: AiGatewayTab.values.map((t) => t.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (label) => setState(
|
||||
() => _tab = AiGatewayTab.values.firstWhere(
|
||||
onChanged: (label) => setState(() {
|
||||
_tab = AiGatewayTab.values.firstWhere(
|
||||
(t) => t.label == label,
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.openAiGateway(tab: _tab);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTabContent(context, _tab, controller),
|
||||
@ -140,7 +163,12 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {},
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: SettingsDetailPage.aiGatewayIntegration,
|
||||
navigationContext: _aiGatewayNavigationContext(
|
||||
AiGatewayTab.models,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(appText('添加模型', 'Add Model')),
|
||||
),
|
||||
@ -188,7 +216,12 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {},
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: SettingsDetailPage.externalAgents,
|
||||
navigationContext: _aiGatewayNavigationContext(
|
||||
AiGatewayTab.agents,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(appText('添加代理', 'Add Agent')),
|
||||
),
|
||||
@ -279,7 +312,7 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_CodexIntegrationCard(controller: controller),
|
||||
_CodexIntegrationSummaryCard(controller: controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -309,14 +342,20 @@ class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
}
|
||||
}
|
||||
|
||||
enum AiGatewayTab { models, agents, endpoints, tools }
|
||||
SettingsNavigationContext _aiGatewayNavigationContext(AiGatewayTab tab) {
|
||||
return SettingsNavigationContext(
|
||||
rootLabel: 'AI Gateway',
|
||||
destination: WorkspaceDestination.aiGateway,
|
||||
sectionLabel: tab.label,
|
||||
aiGatewayTab: tab,
|
||||
);
|
||||
}
|
||||
|
||||
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'),
|
||||
SettingsDetailPage _aiGatewayDetailForTab(AiGatewayTab tab) {
|
||||
return switch (tab) {
|
||||
AiGatewayTab.agents ||
|
||||
AiGatewayTab.tools => SettingsDetailPage.externalAgents,
|
||||
_ => SettingsDetailPage.aiGatewayIntegration,
|
||||
};
|
||||
}
|
||||
|
||||
@ -469,16 +508,100 @@ class _EndpointCard extends StatelessWidget {
|
||||
// Codex Integration Section
|
||||
// ============================================
|
||||
|
||||
class _CodexIntegrationCard extends StatefulWidget {
|
||||
const _CodexIntegrationCard({required this.controller});
|
||||
class _CodexIntegrationSummaryCard extends StatelessWidget {
|
||||
const _CodexIntegrationSummaryCard({required this.controller});
|
||||
|
||||
final AppController controller;
|
||||
|
||||
@override
|
||||
State<_CodexIntegrationCard> createState() => _CodexIntegrationCardState();
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final cooperationLabel = switch (controller.codexCooperationState) {
|
||||
CodexCooperationState.notStarted => appText('未启动', 'Not started'),
|
||||
CodexCooperationState.bridgeOnly => appText(
|
||||
'已启动,但未注册到 Gateway',
|
||||
'Started, not registered to the gateway',
|
||||
),
|
||||
CodexCooperationState.registered => appText(
|
||||
'已启动并已注册到 Gateway',
|
||||
'Started and registered to the gateway',
|
||||
),
|
||||
};
|
||||
|
||||
return Card(
|
||||
color: palette.surfaceSecondary,
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('Codex CLI 集成', 'Codex CLI Integration'),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
appText(
|
||||
'二级页只保留运行状态和快速入口,详细参数统一进入 Settings detail。',
|
||||
'The status page keeps only runtime state and quick entry points. Detailed parameters live in Settings detail.',
|
||||
),
|
||||
style: TextStyle(fontSize: 13, color: palette.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StatusRow(
|
||||
label: appText('运行时模式', 'Runtime mode'),
|
||||
value: controller.effectiveCodeAgentRuntimeMode.label,
|
||||
),
|
||||
_StatusRow(
|
||||
label: appText('Bridge 状态', 'Bridge status'),
|
||||
value: controller.isCodexBridgeEnabled
|
||||
? appText('运行中', 'Running')
|
||||
: appText('未启用', 'Disabled'),
|
||||
),
|
||||
_StatusRow(
|
||||
label: appText('Gateway 协同状态', 'Gateway cooperation'),
|
||||
value: cooperationLabel,
|
||||
),
|
||||
_StatusRow(
|
||||
label: appText('Binary 状态', 'Binary status'),
|
||||
value: controller.hasDetectedCodexCli
|
||||
? appText('已就绪', 'Ready')
|
||||
: appText('未检测到', 'Not found'),
|
||||
detail: controller.resolvedCodexCliPath,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: SettingsDetailPage.externalAgents,
|
||||
navigationContext: _aiGatewayNavigationContext(
|
||||
AiGatewayTab.tools,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.tune_rounded),
|
||||
label: Text(appText('编辑详细设置', 'Edit detailed settings')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CodexIntegrationCardState extends State<_CodexIntegrationCard> {
|
||||
class CodexIntegrationCard extends StatefulWidget {
|
||||
const CodexIntegrationCard({super.key, required this.controller});
|
||||
|
||||
final AppController controller;
|
||||
|
||||
@override
|
||||
State<CodexIntegrationCard> createState() => _CodexIntegrationCardState();
|
||||
}
|
||||
|
||||
class _CodexIntegrationCardState extends State<CodexIntegrationCard> {
|
||||
bool _isExporting = false;
|
||||
String? _exportPath;
|
||||
String? _errorMessage;
|
||||
@ -493,7 +616,7 @@ class _CodexIntegrationCardState extends State<_CodexIntegrationCard> {
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _CodexIntegrationCard oldWidget) {
|
||||
void didUpdateWidget(covariant CodexIntegrationCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final nextValue = widget.controller.configuredCodexCliPath;
|
||||
if (_pathController.text != nextValue) {
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../features/account/account_page.dart';
|
||||
import '../../features/ai_gateway/ai_gateway_page.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/secrets/secrets_page.dart';
|
||||
import '../../features/settings/settings_page.dart';
|
||||
import '../../features/skills/skills_page.dart';
|
||||
import '../../features/tasks/tasks_page.dart';
|
||||
import '../../app/workspace_page_registry.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
@ -169,53 +160,12 @@ class _MobileShellState extends State<MobileShell> {
|
||||
}
|
||||
|
||||
final destination = widget.controller.destination;
|
||||
return switch (destination) {
|
||||
WorkspaceDestination.assistant => AssistantPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
showStandaloneTaskRail: false,
|
||||
),
|
||||
WorkspaceDestination.tasks => TasksPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.skills => SkillsPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.nodes => ModulesPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
initialTab: ModulesTab.nodes,
|
||||
),
|
||||
WorkspaceDestination.agents => ModulesPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
initialTab: ModulesTab.agents,
|
||||
),
|
||||
WorkspaceDestination.mcpServer => McpServerPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.clawHub => ClawHubPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.secrets => SecretsPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.aiGateway => AiGatewayPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
),
|
||||
WorkspaceDestination.settings => SettingsPage(
|
||||
controller: widget.controller,
|
||||
),
|
||||
WorkspaceDestination.account => AccountPage(
|
||||
controller: widget.controller,
|
||||
),
|
||||
};
|
||||
return buildWorkspacePage(
|
||||
destination: destination,
|
||||
controller: widget.controller,
|
||||
onOpenDetail: _openDetailSheet,
|
||||
surface: WorkspacePageSurface.mobile,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/workspace_navigation.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
@ -13,7 +14,7 @@ import '../../widgets/status_badge.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
|
||||
class ModulesPage extends StatefulWidget {
|
||||
class ModulesPage extends StatefulWidget {
|
||||
const ModulesPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
@ -29,14 +30,21 @@ import '../../widgets/top_bar.dart';
|
||||
State<ModulesPage> createState() => _ModulesPageState();
|
||||
}
|
||||
|
||||
class _ModulesPageState extends State<ModulesPage> {
|
||||
ModulesTab _tab = ModulesTab.gateway;
|
||||
class _ModulesPageState extends State<ModulesPage> {
|
||||
late ModulesTab _tab;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialTab != null) {
|
||||
_tab = widget.initialTab!;
|
||||
_tab = widget.initialTab ?? widget.controller.modulesTab;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ModulesPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final nextTab = widget.initialTab ?? widget.controller.modulesTab;
|
||||
if (nextTab != _tab) {
|
||||
setState(() => _tab = nextTab);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,15 +85,11 @@ import '../../widgets/top_bar.dart';
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: [
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
AppBreadcrumbItem(label: appText('模块', 'Modules')),
|
||||
AppBreadcrumbItem(label: _tab.label),
|
||||
],
|
||||
breadcrumbs: buildWorkspaceBreadcrumbs(
|
||||
controller: controller,
|
||||
rootLabel: appText('模块', 'Modules'),
|
||||
sectionLabel: _tab.label,
|
||||
),
|
||||
title: appText('模块', 'Modules'),
|
||||
subtitle: appText(
|
||||
'管理 Gateway、代理、节点、技能和平台服务。',
|
||||
@ -123,7 +127,7 @@ import '../../widgets/top_bar.dart';
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
controller.openSettings(tab: SettingsTab.gateway),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(appText('接入模块', 'Add Module')),
|
||||
),
|
||||
@ -134,11 +138,12 @@ import '../../widgets/top_bar.dart';
|
||||
SectionTabs(
|
||||
items: ModulesTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = ModulesTab.values.firstWhere(
|
||||
onChanged: (value) => setState(() {
|
||||
_tab = ModulesTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.openModules(tab: _tab);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
LayoutBuilder(
|
||||
@ -197,6 +202,22 @@ import '../../widgets/top_bar.dart';
|
||||
}
|
||||
}
|
||||
|
||||
SettingsNavigationContext _modulesNavigationContext(ModulesTab tab) {
|
||||
return SettingsNavigationContext(
|
||||
rootLabel: appText('模块', 'Modules'),
|
||||
destination: WorkspaceDestination.nodes,
|
||||
sectionLabel: tab.label,
|
||||
modulesTab: tab,
|
||||
);
|
||||
}
|
||||
|
||||
SettingsDetailPage _modulesDetailForTab(ModulesTab tab) {
|
||||
return switch (tab) {
|
||||
ModulesTab.agents => SettingsDetailPage.externalAgents,
|
||||
_ => SettingsDetailPage.gatewayConnection,
|
||||
};
|
||||
}
|
||||
|
||||
class _GatewayPanel extends StatelessWidget {
|
||||
const _GatewayPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
@ -338,9 +359,13 @@ class _GatewayPanel extends StatelessWidget {
|
||||
child: Text(appText('刷新会话', 'Refresh sessions')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
child: Text(appText('配置', 'Configure')),
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: _modulesDetailForTab(ModulesTab.gateway),
|
||||
navigationContext: _modulesNavigationContext(
|
||||
ModulesTab.gateway,
|
||||
),
|
||||
),
|
||||
child: Text(appText('编辑设置', 'Edit settings')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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';
|
||||
@ -16,17 +17,34 @@ class SecretsPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
this.initialTab,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
final SecretsTab? initialTab;
|
||||
|
||||
@override
|
||||
State<SecretsPage> createState() => _SecretsPageState();
|
||||
}
|
||||
|
||||
class _SecretsPageState extends State<SecretsPage> {
|
||||
SecretsTab _tab = SecretsTab.vault;
|
||||
late SecretsTab _tab;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tab = widget.initialTab ?? widget.controller.secretsTab;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SecretsPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final nextTab = widget.initialTab ?? widget.controller.secretsTab;
|
||||
if (nextTab != _tab) {
|
||||
setState(() => _tab = nextTab);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -40,15 +58,11 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: [
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
AppBreadcrumbItem(label: appText('密钥', 'Secrets')),
|
||||
AppBreadcrumbItem(label: _tab.label),
|
||||
],
|
||||
breadcrumbs: buildWorkspaceBreadcrumbs(
|
||||
controller: controller,
|
||||
rootLabel: appText('密钥', 'Secrets'),
|
||||
sectionLabel: _tab.label,
|
||||
),
|
||||
title: appText('密钥', 'Secrets'),
|
||||
subtitle: appText(
|
||||
'管理密钥提供方、凭证和模块间的安全引用。',
|
||||
@ -76,7 +90,7 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
controller.openSettings(tab: SettingsTab.gateway),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(appText('新增密钥', 'Add Secret')),
|
||||
),
|
||||
@ -87,11 +101,12 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
SectionTabs(
|
||||
items: SecretsTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = SecretsTab.values.firstWhere(
|
||||
onChanged: (value) => setState(() {
|
||||
_tab = SecretsTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.openSecrets(tab: _tab);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
switch (_tab) {
|
||||
@ -120,6 +135,24 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsNavigationContext _secretsNavigationContext(SecretsTab tab) {
|
||||
return SettingsNavigationContext(
|
||||
rootLabel: appText('密钥', 'Secrets'),
|
||||
destination: WorkspaceDestination.secrets,
|
||||
sectionLabel: tab.label,
|
||||
secretsTab: tab,
|
||||
);
|
||||
}
|
||||
|
||||
SettingsDetailPage _secretsDetailForTab(SecretsTab tab) {
|
||||
return switch (tab) {
|
||||
SecretsTab.vault => SettingsDetailPage.vaultProvider,
|
||||
SecretsTab.providers => SettingsDetailPage.ollamaProvider,
|
||||
SecretsTab.audit => SettingsDetailPage.diagnosticsAdvanced,
|
||||
SecretsTab.localStore => SettingsDetailPage.ollamaProvider,
|
||||
};
|
||||
}
|
||||
|
||||
class _VaultPanel extends StatelessWidget {
|
||||
const _VaultPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
@ -203,9 +236,13 @@ class _VaultPanel extends StatelessWidget {
|
||||
child: Text(appText('连接测试', 'Test Connection')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
child: Text(appText('配置', 'Configure')),
|
||||
onPressed: () => controller.openSettings(
|
||||
detail: _secretsDetailForTab(SecretsTab.vault),
|
||||
navigationContext: _secretsNavigationContext(
|
||||
SecretsTab.vault,
|
||||
),
|
||||
),
|
||||
child: Text(appText('编辑设置', 'Edit settings')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../app/workspace_navigation.dart';
|
||||
import '../ai_gateway/ai_gateway_page.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/runtime_controllers.dart';
|
||||
@ -18,10 +20,14 @@ class SettingsPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.initialTab = SettingsTab.general,
|
||||
this.initialDetail,
|
||||
this.navigationContext,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final SettingsTab initialTab;
|
||||
final SettingsDetailPage? initialDetail;
|
||||
final SettingsNavigationContext? navigationContext;
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
@ -31,6 +37,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
static const _storedSecretMask = '****';
|
||||
|
||||
late SettingsTab _tab;
|
||||
SettingsDetailPage? _detail;
|
||||
SettingsNavigationContext? _navigationContext;
|
||||
late final TextEditingController _aiGatewayNameController;
|
||||
late final TextEditingController _aiGatewayUrlController;
|
||||
late final TextEditingController _aiGatewayApiKeyRefController;
|
||||
@ -55,6 +63,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tab = widget.initialTab;
|
||||
_detail = widget.initialDetail;
|
||||
_navigationContext = widget.navigationContext;
|
||||
_aiGatewayNameController = TextEditingController();
|
||||
_aiGatewayUrlController = TextEditingController();
|
||||
_aiGatewayApiKeyRefController = TextEditingController();
|
||||
@ -65,6 +75,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_runtimeLogFilterController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SettingsPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.initialTab != _tab) {
|
||||
_tab = widget.initialTab;
|
||||
}
|
||||
if (widget.initialDetail != _detail) {
|
||||
_detail = widget.initialDetail;
|
||||
}
|
||||
if (widget.navigationContext != _navigationContext) {
|
||||
_navigationContext = widget.navigationContext;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aiGatewayNameController.dispose();
|
||||
@ -84,81 +108,72 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
_tab = controller.settingsTab;
|
||||
_detail = controller.settingsDetail;
|
||||
_navigationContext = controller.settingsNavigationContext;
|
||||
final settings = controller.settings;
|
||||
final showingDetail = _detail != null;
|
||||
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,
|
||||
),
|
||||
AppBreadcrumbItem(label: appText('设置', 'Settings')),
|
||||
AppBreadcrumbItem(label: _tab.label),
|
||||
],
|
||||
breadcrumbs: buildSettingsBreadcrumbs(
|
||||
controller,
|
||||
tab: _tab,
|
||||
detail: _detail,
|
||||
navigationContext: _navigationContext,
|
||||
),
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: appText(
|
||||
'配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for $kProductBrandName.',
|
||||
),
|
||||
subtitle: showingDetail
|
||||
? appText(
|
||||
'当前正在编辑详细设置参数,保存后会回写到对应状态页。',
|
||||
'You are editing detailed settings. Saved values flow back to the related status page.',
|
||||
)
|
||||
: appText(
|
||||
'配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for $kProductBrandName.',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
width: showingDetail ? 168 : 220,
|
||||
child: showingDetail
|
||||
? OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
controller.closeSettingsDetail();
|
||||
setState(() {
|
||||
_detail = null;
|
||||
_navigationContext = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
label: Text(appText('返回概览', 'Back to overview')),
|
||||
)
|
||||
: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: SettingsTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = SettingsTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
if (!showingDetail) ...[
|
||||
SectionTabs(
|
||||
items: SettingsTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(() {
|
||||
_tab = SettingsTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
);
|
||||
_detail = null;
|
||||
_navigationContext = null;
|
||||
controller.setSettingsTab(_tab);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...switch (_tab) {
|
||||
SettingsTab.general => _buildGeneral(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.workspace => _buildWorkspace(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.gateway => _buildGateway(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.agents => _buildAgents(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.appearance => _buildAppearance(context, controller),
|
||||
SettingsTab.diagnostics => _buildDiagnostics(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
SettingsTab.experimental => _buildExperimental(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.about => _buildAbout(context, controller),
|
||||
},
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
..._buildContentForCurrentState(context, controller, settings),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -166,6 +181,134 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildContentForCurrentState(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
if (_detail != null) {
|
||||
return _buildDetailContent(context, controller, settings, _detail!);
|
||||
}
|
||||
|
||||
return switch (_tab) {
|
||||
SettingsTab.general => _buildGeneral(context, controller, settings),
|
||||
SettingsTab.workspace => _buildWorkspace(context, controller, settings),
|
||||
SettingsTab.gateway => _buildGateway(context, controller, settings),
|
||||
SettingsTab.agents => _buildAgents(context, controller, settings),
|
||||
SettingsTab.appearance => _buildAppearance(context, controller),
|
||||
SettingsTab.diagnostics => _buildDiagnostics(context, controller),
|
||||
SettingsTab.experimental => _buildExperimental(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.about => _buildAbout(context, controller),
|
||||
};
|
||||
}
|
||||
|
||||
List<Widget> _buildDetailContent(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
SettingsDetailPage detail,
|
||||
) {
|
||||
final gatewaySections = _buildGateway(context, controller, settings);
|
||||
final workspaceSections = _buildWorkspace(context, controller, settings);
|
||||
return switch (detail) {
|
||||
SettingsDetailPage.gatewayConnection => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'集中编辑 Gateway 连接、设备配对和会话级连接入口。',
|
||||
'Edit gateway connection, device pairing, and session-level connection entry points in one place.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...gatewaySections.take(3),
|
||||
],
|
||||
SettingsDetailPage.aiGatewayIntegration => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'统一管理 AI Gateway 地址、API Key、模型目录同步和默认选择。',
|
||||
'Manage AI Gateway endpoint, API key, model catalog sync, and default selections from one screen.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (gatewaySections.isNotEmpty) gatewaySections.last,
|
||||
],
|
||||
SettingsDetailPage.vaultProvider => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'只在这里维护 Vault 地址、命名空间和安全 token 引用。',
|
||||
'Maintain Vault endpoint, namespace, and secure token references here.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (gatewaySections.length > 4) gatewaySections[4],
|
||||
],
|
||||
SettingsDetailPage.ollamaProvider => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'本地与云端 Ollama 提供方参数统一放在这个 detail 页面中维护。',
|
||||
'Local and cloud Ollama provider settings live in this dedicated detail page.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...workspaceSections.skip(1),
|
||||
],
|
||||
SettingsDetailPage.externalAgents => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'多 Agent 协作、角色编排和外部 CLI 工具的详细参数集中在这里。',
|
||||
'Detailed multi-agent collaboration, role orchestration, and external CLI settings are edited here.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._buildAgents(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
CodexIntegrationCard(controller: controller),
|
||||
],
|
||||
SettingsDetailPage.diagnosticsAdvanced => <Widget>[
|
||||
_buildDetailIntro(
|
||||
context,
|
||||
title: detail.label,
|
||||
description: appText(
|
||||
'高级诊断集中展示网关诊断、运行日志和设备信息。',
|
||||
'Advanced diagnostics centralize gateway diagnostics, runtime logs, and device information.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._buildDiagnostics(context, controller),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildDetailIntro(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String description,
|
||||
}) {
|
||||
return SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 10),
|
||||
Text(description, style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildGeneral(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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';
|
||||
@ -45,6 +46,10 @@ class _SkillsPageState extends State<SkillsPage> {
|
||||
.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(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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';
|
||||
@ -77,6 +78,10 @@ class _TasksPageState extends State<TasksPage> {
|
||||
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(
|
||||
|
||||
@ -215,6 +215,85 @@ extension SettingsTabCopy on SettingsTab {
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
ollamaProvider,
|
||||
externalAgents,
|
||||
diagnosticsAdvanced,
|
||||
}
|
||||
|
||||
extension SettingsDetailPageCopy on SettingsDetailPage {
|
||||
String get label => switch (this) {
|
||||
SettingsDetailPage.gatewayConnection => appText(
|
||||
'Gateway 连接参数',
|
||||
'Gateway Connection',
|
||||
),
|
||||
SettingsDetailPage.aiGatewayIntegration => appText(
|
||||
'AI Gateway 集成参数',
|
||||
'AI Gateway Integration',
|
||||
),
|
||||
SettingsDetailPage.vaultProvider => appText(
|
||||
'Vault 提供方参数',
|
||||
'Vault Provider',
|
||||
),
|
||||
SettingsDetailPage.ollamaProvider => appText(
|
||||
'Ollama 提供方参数',
|
||||
'Ollama 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.ollamaProvider => SettingsTab.workspace,
|
||||
SettingsDetailPage.externalAgents => SettingsTab.agents,
|
||||
SettingsDetailPage.diagnosticsAdvanced => SettingsTab.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
@immutable
|
||||
class SettingsNavigationContext {
|
||||
const SettingsNavigationContext({
|
||||
required this.rootLabel,
|
||||
required this.destination,
|
||||
this.sectionLabel,
|
||||
this.modulesTab,
|
||||
this.secretsTab,
|
||||
this.aiGatewayTab,
|
||||
this.settingsTab,
|
||||
});
|
||||
|
||||
final String rootLabel;
|
||||
final WorkspaceDestination destination;
|
||||
final String? sectionLabel;
|
||||
final ModulesTab? modulesTab;
|
||||
final SecretsTab? secretsTab;
|
||||
final AiGatewayTab? aiGatewayTab;
|
||||
final SettingsTab? settingsTab;
|
||||
}
|
||||
|
||||
enum AccountTab { profile, workspace, sessions }
|
||||
|
||||
extension AccountTabCopy on AccountTab {
|
||||
|
||||
@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_palette.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'top_bar.dart';
|
||||
|
||||
class DesktopWorkspaceScaffold extends StatelessWidget {
|
||||
const DesktopWorkspaceScaffold({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.breadcrumbs = const <AppBreadcrumbItem>[],
|
||||
this.eyebrow,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
@ -15,6 +17,7 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final List<AppBreadcrumbItem> breadcrumbs;
|
||||
final String? eyebrow;
|
||||
final String? title;
|
||||
final String? subtitle;
|
||||
@ -25,6 +28,7 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final hasHeader =
|
||||
breadcrumbs.isNotEmpty ||
|
||||
(title != null && title!.trim().isNotEmpty) ||
|
||||
(subtitle != null && subtitle!.trim().isNotEmpty) ||
|
||||
toolbar != null;
|
||||
@ -43,6 +47,10 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
|
||||
final header = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (breadcrumbs.isNotEmpty) ...[
|
||||
AppBreadcrumbs(items: breadcrumbs),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
if (eyebrow != null && eyebrow!.trim().isNotEmpty) ...[
|
||||
Text(
|
||||
eyebrow!,
|
||||
|
||||
@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/ai_gateway/ai_gateway_page.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/codex_runtime.dart';
|
||||
import 'package:xworkmate/runtime/device_identity_store.dart';
|
||||
import 'package:xworkmate/runtime/gateway_runtime.dart';
|
||||
@ -14,6 +16,8 @@ import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
_FakeGatewayRuntime()
|
||||
: super(
|
||||
@ -50,84 +54,123 @@ class _FakeCodexRuntime extends CodexRuntime {
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('AiGatewayPage shows Codex bridge runtime states', (
|
||||
testWidgets('AiGatewayPage edit settings opens detail context', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
late AppController controller;
|
||||
await tester.runAsync(() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = SecureConfigStore();
|
||||
controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: RuntimeCoordinator(
|
||||
gateway: _FakeGatewayRuntime(),
|
||||
codex: _FakeCodexRuntime(),
|
||||
),
|
||||
);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
});
|
||||
addTearDown(() => controller.dispose());
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1600, 1000);
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
supportedLocales: const [Locale('zh'), Locale('en')],
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
home: Scaffold(
|
||||
body: AiGatewayPage(controller: controller, onOpenDetail: (_) {}),
|
||||
),
|
||||
),
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: AiGatewayPage(controller: controller, onOpenDetail: (_) {}),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('工具'));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('External Codex CLI'), findsOneWidget);
|
||||
expect(find.text('Built-in Codex (Experimental)'), findsOneWidget);
|
||||
expect(find.text('未检测到'), findsOneWidget);
|
||||
|
||||
await tester.tap(
|
||||
find.widgetWithText(ChoiceChip, 'Built-in Codex (Experimental)'),
|
||||
);
|
||||
await tester.tap(find.text('编辑设置'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
controller.settings.codeAgentRuntimeMode,
|
||||
CodeAgentRuntimeMode.builtIn,
|
||||
);
|
||||
|
||||
late Directory tempDir;
|
||||
late File codexBinary;
|
||||
await tester.runAsync(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('codex-ai-gateway-page-');
|
||||
codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
expect(controller.destination, WorkspaceDestination.settings);
|
||||
expect(controller.settingsDetail, SettingsDetailPage.aiGatewayIntegration);
|
||||
expect(
|
||||
controller.settingsNavigationContext?.aiGatewayTab,
|
||||
AiGatewayTab.models,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Settings external agents detail shows Codex bridge runtime states',
|
||||
(WidgetTester tester) async {
|
||||
late AppController controller;
|
||||
await tester.runAsync(() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = SecureConfigStore();
|
||||
controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: RuntimeCoordinator(
|
||||
gateway: _FakeGatewayRuntime(),
|
||||
codex: _FakeCodexRuntime(),
|
||||
),
|
||||
);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
});
|
||||
addTearDown(() => controller.dispose());
|
||||
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1600, 1000);
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
controller.openSettings(
|
||||
detail: SettingsDetailPage.externalAgents,
|
||||
navigationContext: SettingsNavigationContext(
|
||||
rootLabel: 'AI Gateway',
|
||||
destination: WorkspaceDestination.aiGateway,
|
||||
sectionLabel: AiGatewayTab.tools.label,
|
||||
aiGatewayTab: AiGatewayTab.tools,
|
||||
),
|
||||
);
|
||||
});
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('已就绪'), findsOneWidget);
|
||||
expect(find.text(codexBinary.path), findsAtLeastNWidgets(1));
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
supportedLocales: const [Locale('zh'), Locale('en')],
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
home: Scaffold(
|
||||
body: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('External Codex CLI'), findsOneWidget);
|
||||
expect(find.text('Built-in Codex (Experimental)'), findsOneWidget);
|
||||
expect(find.text('未检测到'), findsOneWidget);
|
||||
|
||||
final builtInChip = find.widgetWithText(
|
||||
ChoiceChip,
|
||||
'Built-in Codex (Experimental)',
|
||||
);
|
||||
await tester.ensureVisible(builtInChip);
|
||||
await tester.tap(builtInChip);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
controller.settings.codeAgentRuntimeMode,
|
||||
CodeAgentRuntimeMode.builtIn,
|
||||
);
|
||||
|
||||
late Directory tempDir;
|
||||
late File codexBinary;
|
||||
await tester.runAsync(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp(
|
||||
'codex-ai-gateway-page-',
|
||||
);
|
||||
codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
),
|
||||
);
|
||||
});
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('已就绪'), findsOneWidget);
|
||||
expect(find.text(codexBinary.path), findsAtLeastNWidgets(1));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _waitFor(bool Function() predicate) async {
|
||||
|
||||
@ -16,17 +16,27 @@ void main() {
|
||||
child: ModulesPage(controller: controller, onOpenDetail: (_) {}),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('配置').first);
|
||||
await tester.tap(find.text('编辑设置').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.destination, WorkspaceDestination.settings);
|
||||
expect(controller.settingsDetail, SettingsDetailPage.gatewayConnection);
|
||||
expect(
|
||||
controller.settingsNavigationContext?.modulesTab,
|
||||
ModulesTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('连接器'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('连接 Gateway 后可加载连接器状态'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('连接 Gateway 后可加载连接器状态'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('接入模块'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.destination, WorkspaceDestination.settings);
|
||||
expect(controller.settingsTab, SettingsTab.gateway);
|
||||
expect(controller.settingsDetail, isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/desktop_platform_service.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
@ -185,4 +186,38 @@ void main() {
|
||||
|
||||
expect(controller.runtimeLogs, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPage detail mode returns to overview', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
controller.openSettings(
|
||||
detail: SettingsDetailPage.gatewayConnection,
|
||||
navigationContext: SettingsNavigationContext(
|
||||
rootLabel: '模块',
|
||||
destination: WorkspaceDestination.nodes,
|
||||
sectionLabel: ModulesTab.gateway.label,
|
||||
modulesTab: ModulesTab.gateway,
|
||||
),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Gateway 连接参数'), findsWidgets);
|
||||
expect(find.text('返回概览'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('返回概览'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.settingsDetail, isNull);
|
||||
expect(find.text('搜索设置'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user