using System.Diagnostics;
using CodexProviderSync.Core;
namespace CodexProviderSync.App;
public sealed class MainForm : Form
{
private readonly CodexSyncService _syncService = new();
private readonly SettingsService _settingsService = new();
private readonly ComboBox _codexHomeCombo = new() { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDown };
private readonly Button _browseButton = new() { Text = "Browse...", AutoSize = true };
private readonly Button _refreshButton = new() { Text = "Refresh", AutoSize = true };
private readonly RichTextBox _statusBox = new()
{
Dock = DockStyle.Fill,
ReadOnly = true,
WordWrap = false,
Font = new Font("Consolas", 10F),
BackColor = SystemColors.Window
};
private readonly ListView _providerList = new()
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
HideSelection = false,
MultiSelect = false
};
private readonly TextBox _manualProviderText = new() { Dock = DockStyle.Fill };
private readonly Button _addProviderButton = new() { Text = "添加", AutoSize = true };
private readonly Button _removeProviderButton = new() { Text = "删除手动项", AutoSize = true };
private readonly Label _selectedProviderValue = new()
{
AutoSize = false,
Dock = DockStyle.Fill,
Height = 32,
Font = new Font(SystemFonts.DefaultFont, FontStyle.Bold),
TextAlign = ContentAlignment.MiddleLeft
};
private readonly CheckBox _updateConfigCheck = new()
{
AutoSize = true,
Margin = new Padding(0, 4, 8, 0)
};
private readonly CheckBox _restoreConfigCheck = new() { Text = "恢复配置文件(config.toml)", Checked = false, AutoSize = true };
private readonly CheckBox _restoreDatabaseCheck = new() { Text = "恢复线程数据库(SQLite)", Checked = true, AutoSize = true };
private readonly CheckBox _restoreSessionsCheck = new() { Text = "恢复会话文件元数据(rollout)", Checked = true, AutoSize = true };
private readonly Label _updateConfigLabel = new()
{
AutoSize = false,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
Text = "同步时改写 config.toml"
};
private readonly NumericUpDown _backupRetentionInput = new()
{
Minimum = 1,
Maximum = 100000,
Value = AppConstants.DefaultBackupRetentionCount,
Width = 72
};
private readonly Button _executeButton = new() { Text = "立即同步", Dock = DockStyle.Fill, Height = 40 };
private readonly Button _restoreButton = new() { Text = "恢复备份", Dock = DockStyle.Fill, Height = 40 };
private readonly Button _openBackupButton = new() { Text = "打开备份目录", Dock = DockStyle.Fill, Height = 40 };
private readonly Button _pruneBackupsButton = new() { Text = "清理旧备份", Dock = DockStyle.Fill, Height = 40 };
private readonly Label _busyLabel = new() { AutoSize = true, ForeColor = Color.DarkGreen, Text = "Ready" };
private readonly Label _warningLine1 = new()
{
AutoSize = false,
Dock = DockStyle.Fill,
BackColor = Color.FromArgb(255, 244, 214),
ForeColor = Color.FromArgb(120, 53, 15),
TextAlign = ContentAlignment.MiddleLeft,
Text = "执行前先关闭 Codex CLI / App"
};
private readonly Label _warningLine2 = new()
{
AutoSize = false,
Dock = DockStyle.Fill,
BackColor = Color.FromArgb(255, 244, 214),
ForeColor = Color.FromArgb(120, 53, 15),
TextAlign = ContentAlignment.MiddleLeft,
Text = "以及 app-server / 相关终端"
};
private readonly TextBox _logBox = new()
{
Dock = DockStyle.Fill,
ReadOnly = true,
Multiline = true,
ScrollBars = ScrollBars.Both,
Font = new Font("Consolas", 10F),
BackColor = SystemColors.Window
};
private AppSettings _settings = new();
private StatusSnapshot? _currentStatus;
private bool _loadingSettings;
public MainForm()
{
Text = "Codex Provider Sync";
MinimumSize = new Size(1180, 760);
StartPosition = FormStartPosition.CenterScreen;
_providerList.Columns.Add("Provider", 180);
_providerList.Columns.Add("来源", 180);
_providerList.Columns.Add("当前", 70);
_providerList.Columns.Add("手动", 70);
_providerList.Columns.Add("已保存", 70);
BuildLayout();
WireEvents();
}
protected override async void OnLoad(EventArgs e)
{
base.OnLoad(e);
await LoadStateAsync();
}
///
/// Brings the running form back to the foreground. Invoked by the
/// single-instance focus broker when a second copy of CodexProviderSync
/// is launched and asks the first copy to take focus.
///
public new void BringToFront()
{
if (InvokeRequired)
{
BeginInvoke(new Action(BringToFront));
return;
}
if (WindowState == FormWindowState.Minimized)
{
WindowState = FormWindowState.Normal;
}
Show();
Activate();
TopMost = true;
TopMost = false;
Focus();
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
PersistUiState();
base.OnFormClosing(e);
}
private void BuildLayout()
{
TableLayoutPanel root = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
Padding = new Padding(12)
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 68));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 32));
root.Controls.Add(BuildTopPanel(), 0, 0);
root.Controls.Add(BuildMainPanel(), 0, 1);
root.Controls.Add(BuildLogPanel(), 0, 2);
Controls.Add(root);
}
private Control BuildTopPanel()
{
GroupBox group = new()
{
Text = "Codex Home",
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink
};
TableLayoutPanel panel = new()
{
Dock = DockStyle.Top,
ColumnCount = 4,
AutoSize = true,
Padding = new Padding(10)
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
Label recentLabel = new()
{
Text = "最近使用会自动保留在下拉列表中",
AutoSize = true,
ForeColor = SystemColors.GrayText,
Anchor = AnchorStyles.Left
};
panel.Controls.Add(_codexHomeCombo, 0, 0);
panel.Controls.Add(_browseButton, 1, 0);
panel.Controls.Add(_refreshButton, 2, 0);
panel.Controls.Add(recentLabel, 3, 0);
group.Controls.Add(panel);
return group;
}
private Control BuildMainPanel()
{
TableLayoutPanel main = new()
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 1,
Margin = new Padding(0, 12, 0, 12)
};
main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
main.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 380));
main.Controls.Add(BuildStatusGroup(), 0, 0);
main.Controls.Add(BuildProviderGroup(), 1, 0);
main.Controls.Add(BuildActionGroup(), 2, 0);
return main;
}
private Control BuildStatusGroup()
{
GroupBox group = new() { Text = "当前状态", Dock = DockStyle.Fill };
group.Controls.Add(_statusBox);
return group;
}
private Control BuildProviderGroup()
{
GroupBox group = new() { Text = "Provider 列表", Dock = DockStyle.Fill };
TableLayoutPanel panel = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3,
Padding = new Padding(8)
};
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
FlowLayoutPanel addPanel = new()
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
AutoSize = true,
WrapContents = false
};
addPanel.Controls.Add(new Label { Text = "手动添加:", AutoSize = true, Margin = new Padding(0, 8, 8, 0) });
_manualProviderText.Width = 220;
addPanel.Controls.Add(_manualProviderText);
addPanel.Controls.Add(_addProviderButton);
FlowLayoutPanel hintPanel = new()
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
AutoSize = true,
WrapContents = false
};
hintPanel.Controls.Add(_removeProviderButton);
hintPanel.Controls.Add(new Label
{
Text = "Refresh 时会把扫描到的新 Provider 自动并入持久化列表",
AutoSize = true,
Margin = new Padding(12, 8, 0, 0),
ForeColor = SystemColors.GrayText
});
panel.Controls.Add(_providerList, 0, 0);
panel.Controls.Add(addPanel, 0, 1);
panel.Controls.Add(hintPanel, 0, 2);
group.Controls.Add(panel);
return group;
}
private Control BuildActionGroup()
{
GroupBox group = new() { Text = "执行", Dock = DockStyle.Fill, MinimumSize = new Size(420, 0) };
TableLayoutPanel panel = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 11,
Padding = new Padding(12)
};
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
panel.Controls.Add(new Label { Text = "目标 Provider", AutoSize = true }, 0, 0);
panel.Controls.Add(_selectedProviderValue, 0, 1);
panel.Controls.Add(BuildUpdateConfigPanel(), 0, 2);
panel.Controls.Add(BuildWarningPanel(), 0, 3);
panel.Controls.Add(BuildBackupRetentionPanel(), 0, 4);
panel.Controls.Add(_executeButton, 0, 5);
panel.Controls.Add(BuildRestoreOptionsPanel(), 0, 6);
panel.Controls.Add(_restoreButton, 0, 7);
panel.Controls.Add(_openBackupButton, 0, 8);
panel.Controls.Add(_pruneBackupsButton, 0, 9);
panel.Controls.Add(_busyLabel, 0, 10);
group.Controls.Add(panel);
return group;
}
private Control BuildUpdateConfigPanel()
{
TableLayoutPanel panel = new()
{
Dock = DockStyle.Fill,
ColumnCount = 2,
Margin = new Padding(0, 2, 0, 8)
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.Controls.Add(_updateConfigCheck, 0, 0);
panel.Controls.Add(_updateConfigLabel, 1, 0);
return panel;
}
private Control BuildWarningPanel()
{
Panel panel = new()
{
Dock = DockStyle.Fill,
Height = 86,
Padding = new Padding(10, 8, 10, 8),
Margin = new Padding(0, 4, 0, 8),
BackColor = Color.FromArgb(255, 244, 214)
};
TableLayoutPanel textLayout = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2,
BackColor = Color.FromArgb(255, 244, 214),
Margin = new Padding(0),
Padding = new Padding(0)
};
textLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
textLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
textLayout.Controls.Add(_warningLine1, 0, 0);
textLayout.Controls.Add(_warningLine2, 0, 1);
panel.Controls.Add(textLayout);
return panel;
}
private Control BuildBackupRetentionPanel()
{
TableLayoutPanel panel = new()
{
Dock = DockStyle.Fill,
ColumnCount = 3,
Margin = new Padding(0, 2, 0, 8),
AutoSize = true
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.Controls.Add(new Label
{
Text = "自动保留最近",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 6, 8, 0)
}, 0, 0);
panel.Controls.Add(_backupRetentionInput, 1, 0);
panel.Controls.Add(new Label
{
Text = "份备份",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(8, 6, 0, 0)
}, 2, 0);
return panel;
}
private Control BuildRestoreOptionsPanel()
{
TableLayoutPanel panel = new()
{
Dock = DockStyle.Fill,
ColumnCount = 1,
Margin = new Padding(0, 0, 0, 6),
AutoSize = true
};
panel.RowCount = 4;
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.Controls.Add(new Label
{
Text = "恢复内容",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 2, 0, 4)
}, 0, 0);
panel.Controls.Add(_restoreConfigCheck, 0, 1);
panel.Controls.Add(_restoreDatabaseCheck, 0, 2);
panel.Controls.Add(_restoreSessionsCheck, 0, 3);
return panel;
}
private Control BuildLogPanel()
{
GroupBox group = new() { Text = "执行日志", Dock = DockStyle.Fill };
group.Controls.Add(_logBox);
return group;
}
private void WireEvents()
{
_browseButton.Click += async (_, _) => await BrowseCodexHomeAsync();
_refreshButton.Click += async (_, _) => await RefreshStatusAsync();
_addProviderButton.Click += async (_, _) => await AddManualProviderAsync();
_removeProviderButton.Click += async (_, _) => await RemoveManualProviderAsync();
_backupRetentionInput.ValueChanged += async (_, _) => await PersistBackupRetentionAsync();
_executeButton.Click += async (_, _) => await ExecuteSyncOrSwitchAsync();
_restoreButton.Click += async (_, _) => await RestoreBackupAsync();
_openBackupButton.Click += (_, _) => OpenBackupFolder();
_pruneBackupsButton.Click += async (_, _) => await PruneBackupsAsync();
_providerList.SelectedIndexChanged += (_, _) => UpdateSelectionLabel();
_codexHomeCombo.Leave += async (_, _) => await PersistHomeSelectionAsync();
}
private async Task LoadStateAsync()
{
_loadingSettings = true;
_settings = await _settingsService.LoadAsync();
ApplyWindowBounds(_settings.WindowBounds);
ReloadRecentHomes();
_codexHomeCombo.Text = _settings.LastCodexHome ?? AppConstants.DefaultCodexHome();
_backupRetentionInput.Value = Math.Max(_backupRetentionInput.Minimum, Math.Min(_backupRetentionInput.Maximum, _settings.BackupRetentionCount));
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已加载设置: {_settingsService.SettingsPath}");
_loadingSettings = false;
await RefreshStatusAsync();
}
private async Task RefreshStatusAsync()
{
string codexHome = CurrentCodexHome();
await RunBusyAsync("Refreshing...", () => RefreshStatusCoreAsync(codexHome));
}
private async Task BrowseCodexHomeAsync()
{
using FolderBrowserDialog dialog = new()
{
Description = "选择 .codex 目录",
UseDescriptionForTitle = true,
InitialDirectory = Directory.Exists(CurrentCodexHome()) ? CurrentCodexHome() : AppConstants.DefaultCodexHome(),
ShowNewFolderButton = false
};
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
_codexHomeCombo.Text = dialog.SelectedPath;
await PersistHomeSelectionAsync();
await RefreshStatusAsync();
}
private async Task PersistHomeSelectionAsync()
{
string codexHome = CurrentCodexHome();
if (string.IsNullOrWhiteSpace(codexHome))
{
return;
}
_settings = _settingsService.RecordCodexHome(_settings, codexHome);
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
await _settingsService.SaveAsync(_settings);
ReloadRecentHomes();
}
private async Task PersistBackupRetentionAsync()
{
if (_loadingSettings)
{
return;
}
_settings = _settingsService.UpdateState(
_settings,
SelectedProvider(),
_settings.LastBackupDirectory,
CaptureWindowBounds(),
CurrentBackupRetentionCount());
await _settingsService.SaveAsync(_settings);
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已更新自动保留数: {CurrentBackupRetentionCount()}");
}
private async Task AddManualProviderAsync()
{
string provider = _manualProviderText.Text.Trim();
if (string.IsNullOrWhiteSpace(provider))
{
MessageBox.Show(this, "请输入要添加的 Provider ID。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
_settings = _settingsService.AddManualProvider(_settings, provider);
await _settingsService.SaveAsync(_settings);
_manualProviderText.Clear();
ReloadProviderList();
SelectProvider(provider);
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已添加手动 Provider: {provider}");
}
private async Task RemoveManualProviderAsync()
{
string? provider = SelectedProvider();
if (string.IsNullOrWhiteSpace(provider))
{
MessageBox.Show(this, "请先选择要删除的 Provider。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
_settings = _settingsService.RemoveManualProvider(_settings, provider);
await _settingsService.SaveAsync(_settings);
ReloadProviderList();
SelectProvider(_currentStatus?.CurrentProvider.Provider);
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已删除手动 Provider: {provider}");
}
private async Task ExecuteSyncOrSwitchAsync()
{
string? provider = SelectedProvider();
if (string.IsNullOrWhiteSpace(provider))
{
MessageBox.Show(this, "请先选择目标 Provider。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!ConfirmCodexClosed("执行同步前,请先关闭已打开的 Codex CLI、Codex App、app-server 和相关终端。是否继续?"))
{
return;
}
await RunBusyAsync("Executing...", async () =>
{
string codexHome = CurrentCodexHome();
int backupRetentionCount = CurrentBackupRetentionCount();
SyncResult result = _updateConfigCheck.Checked
? await Task.Run(async () => await _syncService.RunSwitchAsync(codexHome, provider, backupRetentionCount))
: await Task.Run(async () => await _syncService.RunSyncAsync(codexHome, provider: provider, keepCount: backupRetentionCount));
_settings = _settingsService.UpdateState(_settings, provider, result.BackupDir, CaptureWindowBounds(), backupRetentionCount);
await _settingsService.SaveAsync(_settings);
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 执行完成");
AppendLog(TextFormatter.FormatSyncResult(result, _updateConfigCheck.Checked ? "已切换并同步" : "已同步"));
AppendLog(string.Empty);
await RefreshStatusCoreAsync(codexHome);
SelectProvider(provider);
});
}
private async Task RestoreBackupAsync()
{
string backupRoot = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
string initialBackupDir = Directory.Exists(_settings.LastBackupDirectory)
? _settings.LastBackupDirectory!
: backupRoot;
using FolderBrowserDialog dialog = new()
{
Description = "选择要恢复的 backup 目录",
UseDescriptionForTitle = true,
InitialDirectory = initialBackupDir,
ShowNewFolderButton = false
};
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
if (!_restoreConfigCheck.Checked && !_restoreDatabaseCheck.Checked && !_restoreSessionsCheck.Checked)
{
MessageBox.Show(this, "请至少选择一种要恢复的内容。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string restoreTargets = string.Join("、", new[]
{
_restoreConfigCheck.Checked ? "配置文件(config.toml)" : null,
_restoreDatabaseCheck.Checked ? "线程数据库(SQLite)" : null,
_restoreSessionsCheck.Checked ? "会话文件元数据(rollout)" : null
}.Where(static item => item is not null));
DialogResult confirm = MessageBox.Show(
this,
$"确认恢复以下备份?{Environment.NewLine}{Environment.NewLine}{dialog.SelectedPath}{Environment.NewLine}{Environment.NewLine}将覆盖当前的: {restoreTargets}。",
Text,
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning);
if (confirm != DialogResult.OK)
{
return;
}
if (!ConfirmCodexClosed("恢复 backup 前,请先关闭已打开的 Codex CLI、Codex App、app-server 和相关终端。是否继续?"))
{
return;
}
await RunBusyAsync("Restoring...", async () =>
{
string codexHome = CurrentCodexHome();
RestoreResult result = await Task.Run(async () => await _syncService.RunRestoreAsync(
codexHome,
dialog.SelectedPath,
new RestoreBackupOptions
{
RestoreConfig = _restoreConfigCheck.Checked,
RestoreDatabase = _restoreDatabaseCheck.Checked,
RestoreSessions = _restoreSessionsCheck.Checked
}));
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), dialog.SelectedPath, CaptureWindowBounds(), CurrentBackupRetentionCount());
await _settingsService.SaveAsync(_settings);
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 恢复完成");
AppendLog(TextFormatter.FormatRestoreResult(result));
AppendLog(string.Empty);
await RefreshStatusCoreAsync(codexHome);
});
}
private void OpenBackupFolder()
{
string path = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
Directory.CreateDirectory(path);
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
});
}
private async Task PruneBackupsAsync()
{
if (!ConfirmBackupPrune())
{
return;
}
await RunBusyAsync("Cleaning backups...", async () =>
{
string codexHome = CurrentCodexHome();
BackupPruneResult result = await Task.Run(async () => await _syncService.RunPruneBackupsAsync(codexHome, CurrentBackupRetentionCount()));
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 旧备份清理完成");
AppendLog(TextFormatter.FormatBackupPruneResult(result));
AppendLog(string.Empty);
await RefreshStatusCoreAsync(codexHome);
});
}
private void ReloadRecentHomes()
{
string currentText = _codexHomeCombo.Text;
_codexHomeCombo.BeginUpdate();
_codexHomeCombo.Items.Clear();
foreach (string home in _settings.RecentCodexHomes)
{
_codexHomeCombo.Items.Add(home);
}
_codexHomeCombo.EndUpdate();
if (!string.IsNullOrWhiteSpace(currentText))
{
_codexHomeCombo.Text = currentText;
}
}
private void ReloadProviderList()
{
_providerList.BeginUpdate();
_providerList.Items.Clear();
if (_currentStatus is not null)
{
foreach (ProviderOption option in _syncService.BuildProviderOptions(_currentStatus, _settings))
{
ListViewItem item = new(option.Id)
{
Tag = option.Id
};
item.SubItems.Add(TextFormatter.FormatProviderSources(option));
item.SubItems.Add(option.IsCurrentProvider ? "Yes" : string.Empty);
item.SubItems.Add(option.IsManual ? "Yes" : string.Empty);
item.SubItems.Add(option.IsSaved ? "Yes" : string.Empty);
_providerList.Items.Add(item);
}
}
_providerList.EndUpdate();
SelectProvider(_settings.LastSelectedProvider ?? _currentStatus?.CurrentProvider.Provider);
UpdateSelectionLabel();
}
private void SelectProvider(string? provider)
{
if (string.IsNullOrWhiteSpace(provider))
{
return;
}
_providerList.SelectedItems.Clear();
foreach (ListViewItem item in _providerList.Items)
{
if (string.Equals(item.Tag as string, provider, StringComparison.Ordinal))
{
item.Selected = true;
item.Focused = true;
item.EnsureVisible();
break;
}
}
}
private void UpdateSelectionLabel()
{
string? provider = SelectedProvider();
_selectedProviderValue.Text = string.IsNullOrWhiteSpace(provider) ? "未选择" : provider;
}
private string? SelectedProvider()
{
return _providerList.SelectedItems.Count == 0 ? null : _providerList.SelectedItems[0].Tag as string;
}
private string CurrentCodexHome()
{
string text = _codexHomeCombo.Text.Trim();
return string.IsNullOrWhiteSpace(text) ? AppConstants.DefaultCodexHome() : text;
}
private int CurrentBackupRetentionCount()
{
return Decimal.ToInt32(_backupRetentionInput.Value);
}
private void PersistUiState()
{
try
{
_settings = _settingsService.RecordCodexHome(_settings, CurrentCodexHome());
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
_settingsService.Save(_settings);
}
catch
{
// Ignore shutdown persistence failures.
}
}
private async Task RefreshStatusCoreAsync(string codexHome)
{
_currentStatus = await Task.Run(async () => await _syncService.GetStatusAsync(codexHome));
_settings = _settingsService.RecordCodexHome(_settings, _currentStatus.CodexHome);
_settings = _settingsService.MergeDetectedProviders(_settings, _syncService.ExtractDetectedProviderIds(_currentStatus));
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
await _settingsService.SaveAsync(_settings);
_statusBox.Text = TextFormatter.FormatStatus(_currentStatus);
ReloadRecentHomes();
ReloadProviderList();
_codexHomeCombo.Text = _currentStatus.CodexHome;
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已刷新: {_currentStatus.CodexHome}");
}
private WindowBoundsState CaptureWindowBounds()
{
Rectangle bounds = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds;
return new WindowBoundsState
{
X = bounds.X,
Y = bounds.Y,
Width = bounds.Width,
Height = bounds.Height,
Maximized = WindowState == FormWindowState.Maximized
};
}
private void ApplyWindowBounds(WindowBoundsState? bounds)
{
if (bounds is null || bounds.Width < 800 || bounds.Height < 600)
{
Size = new Size(1280, 820);
return;
}
StartPosition = FormStartPosition.Manual;
Bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width, bounds.Height);
if (bounds.Maximized)
{
WindowState = FormWindowState.Maximized;
}
}
private async Task RunBusyAsync(string stateText, Func action)
{
SetBusy(true, stateText);
try
{
await action();
}
catch (Exception error)
{
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 错误: {error}");
MessageBox.Show(this, error.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetBusy(false, "Ready");
}
}
private void SetBusy(bool busy, string stateText)
{
UseWaitCursor = busy;
_busyLabel.Text = stateText;
_busyLabel.ForeColor = busy ? Color.DarkOrange : Color.DarkGreen;
_browseButton.Enabled = !busy;
_refreshButton.Enabled = !busy;
_addProviderButton.Enabled = !busy;
_removeProviderButton.Enabled = !busy;
_updateConfigCheck.Enabled = !busy;
_backupRetentionInput.Enabled = !busy;
_restoreConfigCheck.Enabled = !busy;
_restoreDatabaseCheck.Enabled = !busy;
_restoreSessionsCheck.Enabled = !busy;
_executeButton.Enabled = !busy;
_restoreButton.Enabled = !busy;
_openBackupButton.Enabled = !busy;
_pruneBackupsButton.Enabled = !busy;
_providerList.Enabled = !busy;
_manualProviderText.Enabled = !busy;
_codexHomeCombo.Enabled = !busy;
}
private void AppendLog(string message)
{
if (_logBox.TextLength > 0)
{
_logBox.AppendText(Environment.NewLine);
}
_logBox.AppendText(message);
_logBox.SelectionStart = _logBox.TextLength;
_logBox.ScrollToCaret();
}
private bool ConfirmCodexClosed(string message)
{
return MessageBox.Show(
this,
message,
Text,
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning) == DialogResult.OK;
}
private bool ConfirmBackupPrune()
{
string message =
$"确认清理旧备份?{Environment.NewLine}{Environment.NewLine}" +
$"将只保留最近 {CurrentBackupRetentionCount()} 份受本工具管理的备份。{Environment.NewLine}" +
"被删除的旧备份无法直接恢复。";
return MessageBox.Show(
this,
message,
Text,
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning) == DialogResult.OK;
}
}