Files
ClaudeDo/src/ClaudeDo.Ui/Views/MainWindow.axaml
T
mika kuns 589b9e75f3 fix(ui): unfreeze the operation spinner and attribute rebind churn
Three causes behind the stuck indicator:
- OperationStatus.End did not retire the generation, so tick work posted
  while the dispatcher was blocked drained afterwards and set
  ShowIndicator back to true on a finished operation.
- The startup update check ran in Task.Run; OperationStatus writes its
  observable properties on the calling thread, so the final
  ShowIndicator=false never reached the binding.
- OperationIndicator overwrote its own DataContext from the Status
  property, which re-targeted the call-site binding. It now scopes the
  DataContext to an inner panel and collapses itself when Status is
  null; every call site switched from DataContext= to Status=.

Also gates the footer connection pill in the command body instead of
CanExecute (a disabled command greyed out the ONLINE chip), and extends
OperationTiming: pid per line, 4 MB rollover, fast successes dropped
(failures always kept), and DetailsIsland.BindAsync now carries the
selection trigger via TasksIslandViewModel.SelectFrom.
2026-08-12 13:50:05 +02:00

309 lines
14 KiB
XML

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:controls="using:ClaudeDo.Ui.Views.Controls"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.MainWindow"
x:DataType="vm:IslandsShellViewModel"
Title="ClaudeDo"
Width="1280" Height="820" MinWidth="780" MinHeight="600"
Background="{DynamicResource VoidBrush}"
Icon="avares://ClaudeDo.Ui/Assets/ClaudeTask.ico"
CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1">
<Window.Resources>
<converters:WorkerLogLevelToBrushConverter x:Key="WorkerLogLevelToBrush"/>
</Window.Resources>
<Window.KeyBindings>
<!-- OemQuestion is the '#' key on a German layout: binding it here swallowed the
character app-wide. Ctrl+K is what the search box advertises anyway. -->
<KeyBinding Gesture="Ctrl+K" Command="{Binding FocusSearchCommand}"/>
<KeyBinding Gesture="Ctrl+N" Command="{Binding FocusAddTaskCommand}"/>
</Window.KeyBindings>
<Grid x:Name="RootGrid" RowDefinitions="36,*,22">
<!-- Custom title bar -->
<Border Grid.Row="0"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Left: brand block -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center" Margin="14,0,0,0">
<!-- App icon (matches taskbar) -->
<Image Source="avares://ClaudeDo.Ui/Assets/ClaudeTask.ico"
Width="16" Height="16"
VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
<!-- CLAUDEDO label -->
<TextBlock Classes="title-brand-name eyebrow"
Text="CLAUDEDO"
Foreground="{DynamicResource TextBrush}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
<!-- separator dot -->
<TextBlock Classes="meta"
Text="·"
VerticalAlignment="Center"/>
<!-- current list name -->
<TextBlock Classes="meta"
Text="{Binding Lists.SelectedList.Name, Converter={StaticResource UpperCase}}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
<!-- Help menu -->
<Menu Margin="12,0,0,0"
Background="Transparent"
VerticalAlignment="Center">
<MenuItem Header="{loc:Tr shell.menu.worker}"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource TextDimBrush}">
<MenuItem Header="{loc:Tr shell.menu.restartWorker}"
Command="{Binding RestartWorkerCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
Command="{Binding CheckForUpdatesCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.usageMonitor}"
Command="{Binding OpenUsageMonitorCommand}"/>
</MenuItem>
<MenuItem Header="{loc:Tr shell.menu.repositories}"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource TextDimBrush}">
<MenuItem Header="{loc:Tr shell.menu.addRepos}" Command="{Binding OpenRepoImportCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.worktrees}"
Command="{Binding OpenWorktreesOverviewGlobalCommand}"/>
</MenuItem>
<MenuItem Header="{loc:Tr shell.menu.help}"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource TextDimBrush}">
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
</MenuItem>
</Menu>
<controls:OperationIndicator Status="{Binding UpdateCheck.Op}" Margin="8,0,0,0"/>
</StackPanel>
<!-- Middle: draggable strip -->
<Border Grid.Column="1" Background="Transparent"
PointerPressed="OnTitleBarPressed" />
<!-- Right: window controls -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="0"
VerticalAlignment="Center" Margin="0,0,4,0">
<Button Classes="title-ctrl"
Command="{Binding OpenMissionControlCommand}"
ToolTip.Tip="{loc:Tr missionControl.windowTitle}">
<PathIcon Data="{StaticResource Icon.Grid}" Width="12" Height="12"/>
</Button>
<Button Classes="title-ctrl" Click="OnMinimize">
<PathIcon Data="{StaticResource Icon.WinMin}" Width="10" Height="10"/>
</Button>
<Button Classes="title-ctrl" Click="OnToggleMax">
<PathIcon x:Name="MaxIcon" Data="{StaticResource Icon.WinMax}" Width="10" Height="10"/>
</Button>
<Button Classes="title-ctrl close" Click="OnClose">
<PathIcon Data="{StaticResource Icon.WinClose}" Width="10" Height="10"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- Background gradient layer -->
<Border Grid.Row="1">
<Border.Background>
<RadialGradientBrush Center="50%,50%" GradientOrigin="50%,50%" RadiusX="70%" RadiusY="70%">
<GradientStop Offset="0" Color="{StaticResource DeepColor}" />
<GradientStop Offset="1" Color="{StaticResource VoidColor}" />
</RadialGradientBrush>
</Border.Background>
</Border>
<!-- Three islands (user-resizable) -->
<Grid Grid.Row="1" Margin="7">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260" MinWidth="200"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="320"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="460" MinWidth="280"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Classes="island" Margin="3">
<islands:ListsIslandView DataContext="{Binding Lists}"/>
</Border>
<GridSplitter Grid.Column="1"
Width="3"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeDirection="Columns"
ResizeBehavior="PreviousAndNext"/>
<Border Grid.Column="2" Classes="island" Margin="3">
<islands:TasksIslandView DataContext="{Binding Tasks}"/>
</Border>
<GridSplitter Grid.Column="3"
Width="3"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeDirection="Columns"
ResizeBehavior="PreviousAndNext"
IsVisible="{Binding ShowDetails}"/>
<Border Grid.Column="4" Classes="island" Margin="3"
IsVisible="{Binding ShowDetails}">
<islands:DetailsIslandView DataContext="{Binding Details}"/>
</Border>
</Grid>
<!-- Footer: connection status -->
<Border Grid.Row="2"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<DockPanel LastChildFill="True" Margin="14,0">
<!-- Left: connection pill (click to open worker help) -->
<Button DockPanel.Dock="Left"
Command="{Binding OpenWorkerConnectionHelpCommand}"
Background="Transparent" BorderThickness="0" Padding="0"
Cursor="Hand" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusRunningBrush}"
IsVisible="{Binding Worker.IsConnected}"/>
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusReviewBrush}"
IsVisible="{Binding Worker.IsReconnecting}"/>
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusErrorBrush}"
IsVisible="{Binding IsOffline}"/>
<TextBlock Classes="eyebrow"
Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!-- Left: usage pill (click opens the usage monitor, wired in a later subtask) -->
<controls:UsagePill DockPanel.Dock="Left"
DataContext="{Binding UsagePill}"
Margin="14,0,0,0"
VerticalAlignment="Center"/>
<!-- Right: worker log line — click to open the Log Visualizer overlay -->
<Button DockPanel.Dock="Right"
Command="{Binding OpenLogVisualizerCommand}"
Background="Transparent" BorderThickness="0" Padding="6,0"
Cursor="Hand" VerticalAlignment="Center"
ToolTip.Tip="{loc:Tr modals.logVisualizer.openTooltip}">
<Panel>
<TextBlock Classes="meta"
Text="{loc:Tr modals.logVisualizer.footerHint}"
IsVisible="{Binding !IsWorkerLogVisible}"
Foreground="{DynamicResource TextMuteBrush}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
<TextBlock Classes="meta"
Text="{Binding WorkerLogText}"
IsVisible="{Binding IsWorkerLogVisible}"
Foreground="{Binding WorkerLogLevel, Converter={StaticResource WorkerLogLevelToBrush}}"
LetterSpacing="1.4"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
</Panel>
</Button>
<!-- Right: prime status notification -->
<TextBlock DockPanel.Dock="Right"
Classes="meta"
Text="{Binding PrimeStatus}"
VerticalAlignment="Center"
Margin="12,0,0,0"
IsVisible="{Binding PrimeStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<!-- Spacer between pill and log -->
<Panel/>
</DockPanel>
</Border>
<!-- Update popup card — bottom-right corner, only when update available -->
<Border Grid.Row="0" Grid.RowSpan="3"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0,0,16,30"
ZIndex="100"
IsVisible="{Binding IsUpdateBannerVisible}"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="14,10">
<StackPanel Spacing="8" Width="260">
<TextBlock Classes="body">
<Run Text="{loc:Tr shell.update.available}"/>
<Run Text=" v"/>
<Run Text="{Binding UpdateCheck.CurrentVersion}"/>
<Run Text=" → v"/>
<Run Text="{Binding UpdateBannerLatestVersion}"/>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Classes="btn"
Content="{loc:Tr shell.update.dismiss}"
Command="{Binding DismissBannerCommand}"/>
<Button Classes="btn"
Content="{loc:Tr shell.update.updateNow}"
Command="{Binding UpdateNowCommand}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Stale-worker notice — bottom-right corner, above the update popup. Persistent
(no auto-dismiss timer): the running worker predates the selected list's merged
HEAD, so a restart is needed before "verified" claims against it are trustworthy. -->
<Border Grid.Row="0" Grid.RowSpan="3"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0,0,16,100"
ZIndex="100"
IsVisible="{Binding IsStaleWorkerBannerVisible}"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="14,10">
<StackPanel Spacing="8" Width="260">
<TextBlock Classes="body" Text="{loc:Tr shell.staleWorker.message}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Classes="btn"
Content="{loc:Tr shell.menu.restartWorker}"
Command="{Binding RestartWorkerCommand}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- External merge popup card — bottom-left corner, while an MCP session owns an
unresolved unit-merge conflict. Does not auto-dismiss; stays until the merge ends. -->
<Border Grid.Row="0" Grid.RowSpan="3"
VerticalAlignment="Bottom" HorizontalAlignment="Left"
Margin="16,0,0,30"
ZIndex="100"
IsVisible="{Binding IsExternalMergeBannerVisible}"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource PeatBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="14,10">
<StackPanel Spacing="8" Width="300">
<TextBlock Classes="body" Text="{loc:Tr shell.externalMerge.banner}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Classes="btn"
Content="{loc:Tr shell.externalMerge.open}"
Command="{Binding OpenExternalMergeConflictCommand}"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Window>