Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,90 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>

<Expander x:Uid="ConfigPage_Maintenance" x:Name="MaintenanceExpander"
Header="Session maintenance"
AutomationProperties.AutomationId="ConfigPageSessionMaintenance">
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16">
<StackPanel Spacing="14">
<TextBlock x:Uid="ConfigPage_MaintenanceDescription"
Text="Control automatic session retention and disk cleanup on this gateway. Changes use the Save changes button below."
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"/>

<Grid ColumnSpacing="16" RowSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<ComboBox x:Uid="ConfigPage_MaintenanceMode" x:Name="MaintenanceModeComboBox"
Grid.Row="0" Grid.Column="0"
Header="Mode"
HorizontalAlignment="Stretch"
SelectionChanged="OnMaintenanceValueChanged">
<ComboBoxItem x:Uid="ConfigPage_MaintenanceEnforce" Content="Enforce cleanup" Tag="enforce"/>
<ComboBoxItem x:Uid="ConfigPage_MaintenanceWarn" Content="Warn only" Tag="warn"/>
</ComboBox>
<TextBox x:Uid="ConfigPage_MaintenancePruneAfter" x:Name="PruneAfterTextBox"
Grid.Row="0" Grid.Column="1"
Header="Prune inactive sessions after"
PlaceholderText="30d"
TextChanged="OnMaintenanceValueChanged"/>

<NumberBox x:Uid="ConfigPage_MaintenanceMaxEntries" x:Name="MaxEntriesNumberBox"
Grid.Row="1" Grid.Column="0"
Header="Maximum session entries"
Minimum="1"
Maximum="2147483647"
SpinButtonPlacementMode="Compact"
ValueChanged="OnMaintenanceNumberChanged"/>
<TextBox x:Uid="ConfigPage_MaintenanceArchiveRetention" x:Name="ResetArchiveRetentionTextBox"
Grid.Row="1" Grid.Column="1"
Header="Reset archive retention"
PlaceholderText="14d"
TextChanged="OnMaintenanceValueChanged"/>

<TextBox x:Uid="ConfigPage_MaintenanceMaxDisk" x:Name="MaxDiskBytesTextBox"
Grid.Row="2" Grid.Column="0"
Header="Maximum disk usage"
PlaceholderText="10gb"
TextChanged="OnMaintenanceValueChanged"/>
<TextBox x:Uid="ConfigPage_MaintenanceHighWater" x:Name="HighWaterBytesTextBox"
Grid.Row="2" Grid.Column="1"
Header="Cleanup target"
PlaceholderText="80% of maximum (default)"
TextChanged="OnMaintenanceValueChanged"/>
</Grid>

<Grid ColumnSpacing="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ToggleSwitch x:Uid="ConfigPage_MaintenanceDisableArchiveExpiry" x:Name="KeepResetArchivesToggle"
Header="Disable age-based archive expiry"
OnContent="On" OffContent="Off"
Toggled="OnMaintenanceOptionToggled"/>
<ToggleSwitch x:Uid="ConfigPage_MaintenanceLimitDisk" x:Name="LimitDiskUsageToggle"
Grid.Column="1"
Header="Limit session disk usage"
OnContent="On" OffContent="Off"
Toggled="OnMaintenanceOptionToggled"/>
</Grid>
</StackPanel>
</Border>
</Expander>
</StackPanel>

<!-- Editor controls -->
Expand Down
200 changes: 198 additions & 2 deletions src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public sealed partial class ConfigPage : Page
private ConfigEditorSnapshot _serverSnapshot = ConfigEditorSnapshot.Empty;
private ConfigEditorSnapshot _editSnapshot = ConfigEditorSnapshot.Empty;
private IOperatorGatewayClient? _permissionClient;
private IOperatorGatewayClient? _configClient;

private string _selectedPath = "";
private string _searchText = "";
Expand All @@ -47,6 +48,7 @@ public sealed partial class ConfigPage : Page
private bool _jsonPreviewCollapsedByUser;
private bool _refreshConfigAfterReconnect;
private bool _refreshConfigWhenGatewayAvailable;
private bool _syncingMaintenanceControls = true;
private ContentDialog? _reconnectDialog;
private TextBlock? _reconnectDialogMessage;
private GridLength _jsonPreviewExpandedWidth = new(360);
Expand All @@ -64,6 +66,7 @@ public sealed partial class ConfigPage : Page
public ConfigPage()
{
InitializeComponent();
_syncingMaintenanceControls = false;
NavigationCacheMode = NavigationCacheMode.Required;
Unloaded += (_, _) =>
{
Expand Down Expand Up @@ -109,7 +112,9 @@ public void Initialize()

_appState = CurrentApp.AppState!;
_appState.PropertyChanged += OnAppStateChanged;
SubscribePermissionClient(CurrentApp.GatewayClient);
var currentClient = CurrentApp.GatewayClient;
ResetForGatewayClientChange(currentClient);
SubscribePermissionClient(currentClient);
Logger.Info("[ConfigPage] Initialize");
if (CompleteReconnectIfReady())
return;
Expand Down Expand Up @@ -147,6 +152,7 @@ public void UpdateConfig(JsonElement config)
_serverSnapshot = ConfigEditorModel.CaptureSnapshot(configSnapshot);
if (_pendingChanges.Count == 0)
_editSnapshot = _serverSnapshot;
SyncMaintenanceControlsFromDraft();

if (configSnapshot.TryGetProperty("path", out var pathEl) &&
pathEl.ValueKind == JsonValueKind.String)
Expand Down Expand Up @@ -214,7 +220,10 @@ private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e)
if (_appState!.ConfigSchema.HasValue) UpdateConfigSchema(_appState.ConfigSchema.Value);
break;
case nameof(AppState.Status):
SubscribePermissionClient(CurrentApp.GatewayClient);
{
var currentClient = CurrentApp.GatewayClient;
ResetForGatewayClientChange(currentClient);
SubscribePermissionClient(currentClient);
UpdateConnectionBanner();
UpdatePermissionBanner();
var configPermissionState = GetConfigPermissionState();
Expand All @@ -234,6 +243,7 @@ configPermissionState is (ConfigPermissionState.ReadOnly or ConfigPermissionStat
}
UpdateMetaAndButtons();
break;
}
}
}

Expand Down Expand Up @@ -472,6 +482,14 @@ private void ApplyEditorChanges(string sectionPath, SchemaConfigChangedEventArgs
_validationErrors[fullPath] = error;
}

if (sectionPath.StartsWith("session.maintenance", StringComparison.Ordinal) ||
args.Changes.Keys.Any(path =>
(string.IsNullOrEmpty(sectionPath) ? path : $"{sectionPath}.{path}")
.StartsWith("session.maintenance", StringComparison.Ordinal)))
{
SyncMaintenanceControlsFromDraft();
}

RenderTree();
UpdateSelectedJsonPreviewForCurrentSelection();
UpdateMetaAndButtons();
Expand Down Expand Up @@ -665,6 +683,7 @@ private void OnDiscardChanges(object sender, RoutedEventArgs e)
_pendingChanges.Clear();
_validationErrors.Clear();
_editSnapshot = ConfigEditorSnapshot.Empty;
SyncMaintenanceControlsFromDraft();
ShowStatus(
L("ConfigPage_StatusChangesDiscardedTitle"),
L("ConfigPage_StatusChangesDiscardedMessage"),
Expand All @@ -679,6 +698,12 @@ private void OnResetSection(object sender, RoutedEventArgs e)
{
RemoveSectionEntries(_selectedPath, _pendingChanges);
RemoveSectionEntries(_selectedPath, _validationErrors);
if (string.IsNullOrEmpty(_selectedPath) ||
_selectedPath.StartsWith("session.maintenance", StringComparison.Ordinal) ||
"session.maintenance".StartsWith(_selectedPath + ".", StringComparison.Ordinal))
{
SyncMaintenanceControlsFromDraft();
}
var sectionLabel = string.IsNullOrEmpty(_selectedPath) ? L("ConfigPage_FullConfig") : _selectedPath;
ShowStatus(
L("ConfigPage_StatusSectionResetTitle"),
Expand Down Expand Up @@ -894,12 +919,183 @@ private void UpdateMetaAndButtons()
SaveStatusIcon.Glyph = BuildSaveStatusGlyph(permissionState, dirtyCount, invalidCount);
AccessSummaryText.Text = BuildAccessSummaryText(permissionState, dirtyCount, invalidCount);
SetDetailEditingEnabled(!editingLocked);
SetMaintenanceEditingEnabled(!editingLocked && _serverSnapshot.HasRoot);
ResetSectionButton.IsEnabled = !editingLocked;

SaveButton.IsEnabled = !_saving && !_loading && canWriteConfig && dirtyCount > 0 && invalidCount == 0;
DiscardButton.IsEnabled = !editingLocked && dirtyCount > 0;
}

private void SyncMaintenanceControlsFromDraft()
{
if (!_serverSnapshot.HasRoot || MaintenanceModeComboBox is null)
return;

var root = _pendingChanges.Count == 0
? _serverSnapshot.Root
: ConfigEditorModel.ApplyChanges(_serverSnapshot.Root, _pendingChanges);
var settings = SessionMaintenanceConfigModel.Read(root);
_syncingMaintenanceControls = true;
try
{
MaintenanceModeComboBox.SelectedIndex = settings.Mode == "warn" ? 1 : 0;
PruneAfterTextBox.Text = settings.PruneAfter;
MaxEntriesNumberBox.Value = settings.MaxEntries;
KeepResetArchivesToggle.IsOn = settings.KeepResetArchives;
ResetArchiveRetentionTextBox.Text = settings.ResetArchiveRetention;
LimitDiskUsageToggle.IsOn = settings.LimitDiskUsage;
MaxDiskBytesTextBox.Text = settings.MaxDiskBytes;
HighWaterBytesTextBox.Text = settings.HighWaterBytes == "0" ? "" : settings.HighWaterBytes;
}
finally
{
_syncingMaintenanceControls = false;
}
SetMaintenanceEditingEnabled(!IsConfigEditingLocked(GetConfigPermissionState()));
}

private void ResetForGatewayClientChange(IOperatorGatewayClient? client)
{
if (ReferenceEquals(_configClient, client))
return;

_configClient = client;
_pendingChanges.Clear();
_validationErrors.Clear();
_serverSnapshot = ConfigEditorSnapshot.Empty;
_editSnapshot = ConfigEditorSnapshot.Empty;
_lastConfig = null;
_lastSchema = null;
_initialized = false;
_syncingMaintenanceControls = true;
try
{
var defaults = SessionMaintenanceSettings.Defaults;
MaintenanceModeComboBox.SelectedIndex = 0;
PruneAfterTextBox.Text = defaults.PruneAfter;
MaxEntriesNumberBox.Value = defaults.MaxEntries;
KeepResetArchivesToggle.IsOn = defaults.KeepResetArchives;
ResetArchiveRetentionTextBox.Text = defaults.ResetArchiveRetention;
LimitDiskUsageToggle.IsOn = defaults.LimitDiskUsage;
MaxDiskBytesTextBox.Text = defaults.MaxDiskBytes;
HighWaterBytesTextBox.Text = defaults.HighWaterBytes;
}
finally
{
_syncingMaintenanceControls = false;
}
}

private SessionMaintenanceSettings CaptureMaintenanceSettings()
{
var mode = (MaintenanceModeComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "enforce";
var maxEntriesValue = MaxEntriesNumberBox.Value;
var maxEntries = double.IsFinite(maxEntriesValue) &&
maxEntriesValue >= 1 &&
maxEntriesValue <= int.MaxValue &&
maxEntriesValue == Math.Truncate(maxEntriesValue)
? Convert.ToInt64(maxEntriesValue)
: 0;
return new SessionMaintenanceSettings(
mode,
PruneAfterTextBox.Text,
maxEntries,
KeepResetArchivesToggle.IsOn,
ResetArchiveRetentionTextBox.Text,
LimitDiskUsageToggle.IsOn,
MaxDiskBytesTextBox.Text,
HighWaterBytesTextBox.Text);
}

private void OnMaintenanceValueChanged(object sender, RoutedEventArgs e)
{
if (!_syncingMaintenanceControls)
ApplyMaintenanceDraft();
}

private void OnMaintenanceNumberChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
{
if (!_syncingMaintenanceControls)
ApplyMaintenanceDraft();
}

private void OnMaintenanceOptionToggled(object sender, RoutedEventArgs e)
{
if (_syncingMaintenanceControls)
return;
ApplyMaintenanceDraft();
SetMaintenanceEditingEnabled(!IsConfigEditingLocked(GetConfigPermissionState()));
}

private void ApplyMaintenanceDraft()
{
if (IsConfigEditingLocked(GetConfigPermissionState()) || !_serverSnapshot.HasRoot)
return;
if (_pendingChanges.Count == 0)
_editSnapshot = _serverSnapshot;

const string section = "session.maintenance";
RemoveSectionEntries(section, _pendingChanges);
RemoveSectionEntries(section, _validationErrors);

var settings = CaptureMaintenanceSettings();
_pendingChanges[$"{section}.mode"] = settings.Mode;
_pendingChanges[$"{section}.pruneAfter"] = settings.PruneAfter.Trim();
_pendingChanges[$"{section}.maxEntries"] = settings.MaxEntries;
_pendingChanges[$"{section}.resetArchiveRetention"] = settings.KeepResetArchives
? false
: settings.ResetArchiveRetention.Trim();
_pendingChanges[$"{section}.maxDiskBytes"] = settings.LimitDiskUsage
? settings.MaxDiskBytes.Trim()
: false;
_pendingChanges[$"{section}.highWaterBytes"] = string.IsNullOrWhiteSpace(settings.HighWaterBytes)
? 0L
: settings.HighWaterBytes.Trim();

foreach (var (field, _) in SessionMaintenanceConfigModel.Validate(settings))
_validationErrors[$"{section}.{MaintenanceFieldPath(field)}"] = MaintenanceValidationMessage(field);

RenderTree();
UpdateSelectedJsonPreviewForCurrentSelection();
UpdateMetaAndButtons();
}

private void SetMaintenanceEditingEnabled(bool isEnabled)
{
if (MaintenanceModeComboBox is null)
return;
MaintenanceModeComboBox.IsEnabled = isEnabled;
PruneAfterTextBox.IsEnabled = isEnabled;
MaxEntriesNumberBox.IsEnabled = isEnabled;
KeepResetArchivesToggle.IsEnabled = isEnabled;
ResetArchiveRetentionTextBox.IsEnabled = isEnabled && !KeepResetArchivesToggle.IsOn;
LimitDiskUsageToggle.IsEnabled = isEnabled;
MaxDiskBytesTextBox.IsEnabled = isEnabled && LimitDiskUsageToggle.IsOn;
HighWaterBytesTextBox.IsEnabled = isEnabled && LimitDiskUsageToggle.IsOn;
}

private static string MaintenanceFieldPath(string field) => field switch
{
"Mode" => "mode",
"Prune after" => "pruneAfter",
"Maximum entries" => "maxEntries",
"Reset archive retention" => "resetArchiveRetention",
"Maximum disk usage" => "maxDiskBytes",
"High-water target" => "highWaterBytes",
_ => field,
};

private static string MaintenanceValidationMessage(string field) => field switch
{
"Mode" => L("ConfigPage_MaintenanceModeError"),
"Prune after" => L("ConfigPage_MaintenancePruneAfterError"),
"Maximum entries" => L("ConfigPage_MaintenanceMaxEntriesError"),
"Reset archive retention" => L("ConfigPage_MaintenanceArchiveRetentionError"),
"Maximum disk usage" => L("ConfigPage_MaintenanceMaxDiskError"),
"High-water target" => L("ConfigPage_MaintenanceHighWaterError"),
_ => field,
};

private bool IsConfigEditingLocked(ConfigPermissionState permissionState) =>
_saving ||
_refreshConfigAfterReconnect ||
Expand Down
Loading