feat: 重构左侧边栏 - 添加 ClawHub 和 AI Gateway,将设置移至底部

This commit is contained in:
Haitao Pan 2026-03-13 20:56:04 +08:00
parent 19f49acdd6
commit 4d2cb683f2
6 changed files with 894 additions and 7 deletions

View File

@ -1,7 +1,9 @@
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/mobile/ios_mobile_shell.dart';
import '../features/modules/modules_page.dart';
import '../features/secrets/secrets_page.dart';
@ -327,10 +329,18 @@ class _AppShellState extends State<AppShell> {
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,
),

View File

@ -0,0 +1,393 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../widgets/metric_card.dart';
import '../../widgets/section_header.dart';
import '../../widgets/section_tabs.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
class AiGatewayPage extends StatefulWidget {
const AiGatewayPage({
super.key,
required this.controller,
required this.onOpenDetail,
});
final AppController controller;
final ValueChanged<DetailPanelData> onOpenDetail;
@override
State<AiGatewayPage> createState() => _AiGatewayPageState();
}
class _AiGatewayPageState extends State<AiGatewayPage> {
AiGatewayTab _tab = AiGatewayTab.models;
@override
Widget build(BuildContext context) {
final controller = widget.controller;
final palette = context.palette;
final metrics = [
MetricSummary(
label: appText('网关状态', 'Gateway'),
value: controller.connection.status.label,
caption: controller.connection.remoteAddress ?? appText('未连接', 'Disconnected'),
icon: Icons.wifi_tethering_rounded,
status: _connectionStatus(controller.connection.status),
),
MetricSummary(
label: appText('活跃模型', 'Active Models'),
value: '${controller.models.length}',
caption: controller.models.isNotEmpty
? controller.models.first.name
: appText('', 'None'),
icon: Icons.psychology_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(
title: 'AI Gateway',
subtitle: appText(
'AI 代理与模型网关配置管理中心。',
'AI proxy and model gateway configuration center.',
),
),
const SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: metrics.map((m) => MetricCard(metric: m)).toList(),
),
const SizedBox(height: 24),
SectionTabs<AiGatewayTab>(
tabs: AiGatewayTab.values,
selected: _tab,
onSelect: (t) => setState(() => _tab = t),
labelFor: (t) => t.label,
),
const SizedBox(height: 16),
_buildTabContent(context, _tab, controller),
],
),
);
},
);
}
Widget _buildTabContent(BuildContext context, AiGatewayTab tab, AppController controller) {
final palette = context.palette;
switch (tab) {
case AiGatewayTab.models:
return SurfaceCard(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.psychology_rounded, color: palette.accent, size: 20),
const SizedBox(width: 8),
Text(
appText('模型列表', 'Model List'),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: palette.textPrimary,
),
),
const Spacer(),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add_rounded, size: 18),
label: Text(appText('添加模型', 'Add Model')),
),
],
),
const SizedBox(height: 16),
if (controller.models.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text(
appText('暂无配置的模型', 'No models configured'),
style: TextStyle(color: palette.textSecondary),
),
),
)
else
...controller.models.map((model) => _ModelCard(
model: model,
onTap: () {},
)),
],
),
),
);
case AiGatewayTab.agents:
return SurfaceCard(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.hub_rounded, color: palette.accent, size: 20),
const SizedBox(width: 8),
Text(
appText('代理列表', 'Agent List'),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: palette.textPrimary,
),
),
const Spacer(),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add_rounded, size: 18),
label: Text(appText('添加代理', 'Add Agent')),
),
],
),
const SizedBox(height: 16),
if (controller.agents.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text(
appText('暂无配置的代理', 'No agents configured'),
style: TextStyle(color: palette.textSecondary),
),
),
)
else
...controller.agents.map((agent) => _AgentCard(
agent: agent,
onTap: () {},
)),
],
),
),
);
case AiGatewayTab.endpoints:
return SurfaceCard(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.endpoint_rounded, color: palette.accent, size: 20),
const SizedBox(width: 8),
Text(
appText('端点配置', 'Endpoint Configuration'),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: palette.textPrimary,
),
),
],
),
const SizedBox(height: 16),
_EndpointCard(
name: 'OpenAI',
endpoint: 'https://api.openai.com/v1',
status: 'Connected',
onTap: () {},
),
const SizedBox(height: 12),
_EndpointCard(
name: 'Azure OpenAI',
endpoint: 'https://*.openai.azure.com',
status: 'Disconnected',
onTap: () {},
),
],
),
),
);
}
}
StatusInfo? _connectionStatus(RuntimeConnectionStatus status) {
return switch (status) {
RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success),
RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent),
RuntimeConnectionStatus.disconnected => const StatusInfo('Disconnected', StatusTone.neutral),
RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger),
};
}
}
enum AiGatewayTab { models, agents, endpoints }
extension AiGatewayTabCopy on AiGatewayTab {
String get label => switch (this) {
AiGatewayTab.models => appText('模型', 'Models'),
AiGatewayTab.agents => appText('代理', 'Agents'),
AiGatewayTab.endpoints => appText('端点', 'Endpoints'),
};
}
class _ModelCard extends StatelessWidget {
const _ModelCard({required this.model, required this.onTap});
final dynamic model;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Card(
margin: const EdgeInsets.only(bottom: 8),
color: palette.surfaceSecondary,
elevation: 0,
child: ListTile(
onTap: onTap,
leading: Icon(Icons.psychology_rounded, color: palette.accent),
title: Text(model.name ?? 'Unknown', style: TextStyle(color: palette.textPrimary)),
subtitle: Text(
model.provider ?? 'Unknown provider',
style: TextStyle(color: palette.textSecondary),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Active',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
Icon(Icons.chevron_right, color: palette.textMuted),
],
),
),
);
}
}
class _AgentCard extends StatelessWidget {
const _AgentCard({required this.agent, required this.onTap});
final dynamic agent;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Card(
margin: const EdgeInsets.only(bottom: 8),
color: palette.surfaceSecondary,
elevation: 0,
child: ListTile(
onTap: onTap,
leading: Icon(Icons.hub_rounded, color: palette.accent),
title: Text(agent.name ?? 'Unknown', style: TextStyle(color: palette.textPrimary)),
subtitle: Text(
agent.capabilities?.join(', ') ?? 'No capabilities',
style: TextStyle(color: palette.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.chevron_right, color: palette.textMuted),
],
),
),
);
}
}
class _EndpointCard extends StatelessWidget {
const _EndpointCard({
required this.name,
required this.endpoint,
required this.status,
required this.onTap,
});
final String name;
final String endpoint;
final String status;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final isConnected = status == 'Connected';
return Card(
color: palette.surfaceSecondary,
elevation: 0,
child: ListTile(
onTap: onTap,
leading: Icon(
Icons.endpoint_rounded,
color: isConnected ? palette.accent : palette.textMuted,
),
title: Text(name, style: TextStyle(color: palette.textPrimary)),
subtitle: Text(
endpoint,
style: TextStyle(color: palette.textSecondary, fontFamily: 'monospace'),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isConnected
? Colors.green.withOpacity(0.2)
: Colors.grey.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
status,
style: TextStyle(
fontSize: 12,
color: isConnected ? Colors.green : palette.textMuted,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}

View File

@ -0,0 +1,461 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.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() => _ClawHubPageState();
}
class _ClawHubPageState extends State<ClawHubPage> {
final _searchController = TextEditingController();
final _commandController = TextEditingController();
final _scrollController = ScrollController();
final List<ClawHubLogEntry> _logs = [];
bool _isExecuting = false;
@override
void dispose() {
_searchController.dispose();
_commandController.dispose();
_scrollController.dispose();
super.dispose();
}
void _addLog(String message, {ClawHubLogType type = ClawHubLogType.info}) {
setState(() {
_logs.add(ClawHubLogEntry(
timestamp: DateTime.now(),
message: message,
type: type,
));
});
// Auto-scroll to bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
});
}
void _executeCommand(String input) {
if (input.trim().isEmpty) return;
_addLog('\$ clawhub \$input', type: ClawHubLogType.command);
_commandController.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':
_handleSearch(args);
break;
case 'install':
_handleInstall(args);
break;
case 'update':
_handleUpdate(args);
break;
case 'help':
case '--help':
case '-h':
_showHelp();
break;
default:
_addLog(
'Unknown command: \$command. Type "clawhub help" for available commands.',
type: ClawHubLogType.error,
);
}
}
void _handleSearch(List<String> args) {
final query = args.join(' ');
if (query.isEmpty) {
_addLog('Usage: clawhub search "<query>"', type: ClawHubLogType.warning);
return;
}
setState(() => _isExecuting = true);
_addLog('Searching for "\$query"...');
// Simulate search results
Future.delayed(const Duration(milliseconds: 800), () {
setState(() => _isExecuting = false);
_addLog('');
_addLog('Found 3 packages:', type: ClawHubLogType.success);
_addLog(' ├─ skill-analyzer v1.2.0 Code analysis skill');
_addLog(' ├─ feishu-connector v2.1.3 Feishu integration');
_addLog(' └─ azure-deploy v3.0.1 Azure deployment helper');
_addLog('');
_addLog('Use "clawhub install <slug>" to install a package.');
});
}
void _handleInstall(List<String> args) {
if (args.isEmpty) {
_addLog('Usage: clawhub install <slug>', type: ClawHubLogType.warning);
return;
}
final slug = args[0];
setState(() => _isExecuting = true);
_addLog('Installing \$slug...');
Future.delayed(const Duration(milliseconds: 1200), () {
setState(() => _isExecuting = false);
_addLog('✓ Successfully installed \$slug', type: ClawHubLogType.success);
_addLog(' Location: ~/.clawhub/skills/\$slug');
_addLog(' Run "clawhub update" to check for updates.');
});
}
void _handleUpdate(List<String> args) {
final isAll = args.contains('--all') || args.contains('-a');
final slug = isAll ? null : (args.isNotEmpty ? args[0] : null);
setState(() => _isExecuting = true);
if (isAll) {
_addLog('Checking for updates...');
Future.delayed(const Duration(milliseconds: 1000), () {
setState(() => _isExecuting = false);
_addLog('✓ All packages are up to date', type: ClawHubLogType.success);
});
} else if (slug != null) {
_addLog('Updating \$slug...');
Future.delayed(const Duration(milliseconds: 800), () {
setState(() => _isExecuting = false);
_addLog('\$slug updated to latest version', type: ClawHubLogType.success);
});
} else {
_addLog('Usage: clawhub update <slug> or clawhub update --all',
type: ClawHubLogType.warning);
setState(() => _isExecuting = false);
}
}
void _showHelp() {
_addLog('');
_addLog('ClawHub Package Manager', type: ClawHubLogType.success);
_addLog('Usage: clawhub <command> [options]');
_addLog('');
_addLog('Commands:');
_addLog(' search "<query>" Search for packages');
_addLog(' install <slug> Install a package');
_addLog(' update <slug> Update a specific package');
_addLog(' update --all Update all packages');
_addLog(' help Show this help message');
_addLog('');
}
@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(
title: 'ClawHub',
subtitle: appText(
'NPM 风格的包管理中心,支持搜索、安装和更新 Skills。',
'NPM-style package manager for skills.',
),
),
const SizedBox(height: 24),
SectionHeader(
title: appText('终端', 'Terminal'),
icon: Icons.terminal_rounded,
),
const SizedBox(height: 12),
SurfaceCard(
child: Container(
height: 400,
decoration: BoxDecoration(
color: palette.surfaceSecondary.withOpacity(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 (_isExecuting)
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: _scrollController,
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
return _LogLine(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: _commandController,
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: _executeCommand,
),
),
IconButton(
icon: Icon(
Icons.send_rounded,
size: 18,
color: palette.accent,
),
onPressed: () =>
_executeCommand(_commandController.text),
visualDensity: VisualDensity.compact,
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
SectionHeader(
title: appText('快速操作', 'Quick Actions'),
icon: Icons.bolt_rounded,
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_QuickActionButton(
icon: Icons.search_rounded,
label: appText('搜索技能', 'Search Skills'),
onTap: () => _executeCommand('search analytics'),
),
_QuickActionButton(
icon: Icons.download_rounded,
label: appText('安装技能', 'Install Skill'),
onTap: () => _executeCommand('install example-skill'),
),
_QuickActionButton(
icon: Icons.update_rounded,
label: appText('更新全部', 'Update All'),
onTap: () => _executeCommand('update --all'),
),
_QuickActionButton(
icon: Icons.help_outline_rounded,
label: appText('查看帮助', 'View Help'),
onTap: () => _executeCommand('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 _LogLine extends StatelessWidget {
const _LogLine({required this.entry, required this.palette});
final ClawHubLogEntry entry;
final AppPalette palette;
Color get _color {
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: _color,
height: 1.4,
),
),
);
}
}
class _QuickActionButton extends StatelessWidget {
const _QuickActionButton({
required this.icon,
required this.label,
required this.onTap,
});
final IconData icon;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Material(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: palette.accent),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: palette.textPrimary,
),
),
],
),
),
),
);
}
}

View File

@ -44,8 +44,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
'Capability center for gateway, nodes, agents, skills, and connectors.',
),
WorkspaceDestination.secrets => appText(
'Vault、Provider 凭证与审计信息的轻量管理面。',
'Lightweight management for vault, provider credentials, and audit data.',
'Vault 密码保险箱,安全存储密钥、凭证与审计信息。',
'Vault password safe for secure storage of keys, credentials and audit data.',
),
WorkspaceDestination.aiGateway => appText(
'AI Gateway 代理与模型网关配置管理。',
'AI Gateway proxy and model gateway configuration.',
),
WorkspaceDestination.settings => appText(
'全局配置中心,只负责系统设置与诊断,不承担业务模块入口。',

View File

@ -41,8 +41,9 @@ class SidebarNavigation extends StatelessWidget {
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.modules,
WorkspaceDestination.clawHub,
WorkspaceDestination.secrets,
WorkspaceDestination.settings,
WorkspaceDestination.aiGateway,
];
@override
@ -253,7 +254,9 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
WorkspaceDestination.assistant => Icons.auto_awesome_rounded,
WorkspaceDestination.tasks => Icons.task_alt_rounded,
WorkspaceDestination.modules => Icons.extension_rounded,
WorkspaceDestination.secrets => Icons.key_rounded,
WorkspaceDestination.clawHub => Icons.hub_rounded,
WorkspaceDestination.secrets => Icons.security_rounded,
WorkspaceDestination.aiGateway => Icons.smart_toy_rounded,
WorkspaceDestination.settings => Icons.tune_rounded,
WorkspaceDestination.account => Icons.account_circle_rounded,
};
@ -264,7 +267,9 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
WorkspaceDestination.assistant => appText('助手', 'Assistant'),
WorkspaceDestination.tasks => appText('任务', 'Tasks'),
WorkspaceDestination.modules => appText('模块', 'Modules'),
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
WorkspaceDestination.clawHub => 'ClawHub',
WorkspaceDestination.secrets => appText('密钥 / Vault', 'Secrets / Vault'),
WorkspaceDestination.aiGateway => 'AI Gateway',
WorkspaceDestination.settings => appText('设置', 'Settings'),
WorkspaceDestination.account => appText('账户', 'Account'),
};
@ -334,6 +339,12 @@ class SidebarFooter extends StatelessWidget {
onPressed: onCycleSidebarState,
),
const SizedBox(height: AppSpacing.xs),
_SidebarActionButton(
icon: Icons.tune_rounded,
tooltip: appText('设置', 'Settings'),
onPressed: onOpenSettings,
),
const SizedBox(height: AppSpacing.xs),
_SidebarAccountTile(
selected: accountSelected,
onTap: onOpenAccount,
@ -375,6 +386,12 @@ class SidebarFooter extends StatelessWidget {
tooltip: _sidebarStateLabel(sidebarState),
onPressed: onCycleSidebarState,
),
const SizedBox(width: AppSpacing.xs),
_SidebarActionButton(
icon: Icons.tune_rounded,
tooltip: appText('设置', 'Settings'),
onPressed: onOpenSettings,
),
],
),
const SizedBox(height: AppSpacing.xs),
@ -390,8 +407,9 @@ class SidebarFooter extends StatelessWidget {
IconData _sidebarStateIcon(AppSidebarState state) {
return switch (state) {
AppSidebarState.expanded => Icons.sidebar_rounded,
AppSidebarState.expanded => Icons.view_sidebar_rounded,
AppSidebarState.collapsed => Icons.menu_rounded,
AppSidebarState.hidden => Icons.view_sidebar_rounded,
};
}
@ -399,6 +417,7 @@ class SidebarFooter extends StatelessWidget {
return switch (state) {
AppSidebarState.expanded => appText('收起侧边栏', 'Collapse sidebar'),
AppSidebarState.collapsed => appText('展开侧边栏', 'Expand sidebar'),
AppSidebarState.hidden => appText('展开侧边栏', 'Expand sidebar'),
};
}
}

View File

@ -2,7 +2,7 @@ name: xworkmate
description: "XWorkmate desktop-first AI workspace shell."
publish_to: 'none'
version: latest
version: 0.1.0+1
build-date: 2026-03-12
build-id: acc3a06